From acf49619b5457003c0ea02ee0503f8a438ba6b05 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Mon, 8 Sep 2025 14:44:49 -0400 Subject: [PATCH 01/19] feat: add GitHub workflow for auto-closing stale issues with dry-run support - Daily workflow checks issues with configurable label after X days - Removes label if unauthorized users comment, closes if only authorized users - Supports team-based or write-access authorization modes - Includes comprehensive input validation and error handling - Adds manual trigger with dry-run mode for safe testing --- .github/workflows/auto-close-3-days.yml | 243 ++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 .github/workflows/auto-close-3-days.yml diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml new file mode 100644 index 000000000..73523e628 --- /dev/null +++ b/.github/workflows/auto-close-3-days.yml @@ -0,0 +1,243 @@ +name: Auto Close Issues + +on: + schedule: + - cron: '0 9 * * *' # Daily at 9 AM UTC + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no actions taken, only logging)' + required: false + default: 'true' + type: boolean + +env: + LABEL_NAME: 'autoclose in 3 days' + DAYS_TO_WAIT: '3' + AUTHORIZED_USERS: '' # Comma-separated list (ex: user1,user2,user3) *required for 'users' mode + AUTH_MODE: 'write-access' # Options: 'users', 'write-access' + ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' + DRY_RUN: 'true' # Set to 'true' to enable dry-run mode (no actions taken) + +jobs: + auto-close: + runs-on: ubuntu-latest + steps: + - name: Validate configuration + uses: actions/github-script@v8 + with: + script: | + const { LABEL_NAME, DAYS_TO_WAIT, AUTH_MODE, AUTHORIZED_USERS, ISSUE_TYPES, DRY_RUN } = process.env; + + // Check for manual dry-run override + const isDryRun = '${{ inputs.dry_run }}' === 'true' || DRY_RUN === 'true'; + + // Validate required fields + if (!LABEL_NAME) throw new Error('LABEL_NAME is required'); + if (!DAYS_TO_WAIT || isNaN(parseInt(DAYS_TO_WAIT)) || parseInt(DAYS_TO_WAIT) < 1) { + throw new Error('DAYS_TO_WAIT must be a positive integer'); + } + if (!['users', 'write-access'].includes(AUTH_MODE)) { + throw new Error('AUTH_MODE must be "users" or "write-access"'); + } + if (!['issues', 'pulls', 'both'].includes(ISSUE_TYPES)) { + throw new Error('ISSUE_TYPES must be "issues", "pulls", or "both"'); + } + if (AUTH_MODE === 'users' && (!AUTHORIZED_USERS || AUTHORIZED_USERS.trim() === '')) { + throw new Error('AUTHORIZED_USERS is required when AUTH_MODE is "users"'); + } + + console.log('โœ… Configuration validated successfully'); + console.log(`Label: "${LABEL_NAME}", Days: ${DAYS_TO_WAIT}, Auth: ${AUTH_MODE}, Types: ${ISSUE_TYPES}`); + if (isDryRun) { + console.log('๐Ÿงช DRY-RUN MODE: No actions will be taken, only logging what would happen'); + } + + - name: Find and process labeled issues + uses: actions/github-script@v8 + with: + script: | + // Constants + const REQUIRED_PERMISSIONS = ['write', 'admin']; + const CLOSE_MESSAGE = `This issue has been automatically closed as it was marked for auto-closure by the team and no response was received within ${process.env.DAYS_TO_WAIT} days.`; + + // Check for dry-run mode + const isDryRun = '${{ inputs.dry_run }}' === 'true' || process.env.DRY_RUN === 'true'; + + // Parse configuration + const config = { + labelName: process.env.LABEL_NAME, + daysToWait: parseInt(process.env.DAYS_TO_WAIT), + authMode: process.env.AUTH_MODE, + authorizedUsers: process.env.AUTHORIZED_USERS?.split(',').map(u => u.trim()).filter(u => u) || [], + issueTypes: process.env.ISSUE_TYPES + }; + + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - config.daysToWait); + + // Authorization check function + async function isAuthorizedUser(username) { + try { + if (config.authMode === 'users') { + return config.authorizedUsers.includes(username); + } else if (config.authMode === 'write-access') { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: username + }); + return REQUIRED_PERMISSIONS.includes(data.permission); + } + } catch (error) { + console.log(`โš ๏ธ Failed to check authorization for ${username}: ${error.message}`); + return false; + } + return false; + } + + // Find issues with the target label + const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:open label:"${config.labelName}"`; + const typeFilter = config.issueTypes === 'pulls' ? 'is:pr' : + config.issueTypes === 'both' ? '' : 'is:issue'; + + try { + const { data: searchResults } = await github.rest.search.issuesAndPullRequests({ + q: `${searchQuery} ${typeFilter}`.trim(), + sort: 'updated', + order: 'desc' + }); + + const targetIssues = searchResults.items.filter(issue => { + if (config.issueTypes === 'issues' && issue.pull_request) return false; + if (config.issueTypes === 'pulls' && !issue.pull_request) return false; + return true; + }); + + console.log(`๐Ÿ” Found ${targetIssues.length} items with label "${config.labelName}"`); + + if (targetIssues.length === 0) { + console.log('โœ… No items to process'); + return; + } + + // Process each issue + let closedCount = 0; + let labelRemovedCount = 0; + let skippedCount = 0; + + for (const issue of targetIssues) { + console.log(`\n๐Ÿ“‹ Processing #${issue.number}: ${issue.title}`); + + try { + // Get label events to find when label was last added + const { data: events } = await github.rest.issues.listEvents({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number + }); + + const labelEvents = events + .filter(e => e.event === 'labeled' && e.label?.name === config.labelName) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + if (labelEvents.length === 0) { + console.log(`โš ๏ธ No label events found for #${issue.number}`); + skippedCount++; + continue; + } + + const lastLabelAdded = new Date(labelEvents[0].created_at); + const labelAdder = labelEvents[0].actor.login; + + // Check if enough time has passed + if (lastLabelAdded > cutoffDate) { + const daysRemaining = Math.ceil((lastLabelAdded - cutoffDate) / (1000 * 60 * 60 * 24)); + console.log(`โณ Label added too recently (${daysRemaining} days remaining)`); + skippedCount++; + continue; + } + + // Check comments since label was added + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + since: lastLabelAdded.toISOString() + }); + + let hasUnauthorizedComment = false; + + for (const comment of comments) { + // Skip comments from the person who added the label + if (comment.user.login === labelAdder) continue; + + const isAuthorized = await isAuthorizedUser(comment.user.login); + if (!isAuthorized) { + console.log(`โŒ Unauthorized comment from ${comment.user.login}`); + hasUnauthorizedComment = true; + break; + } + } + + if (hasUnauthorizedComment) { + // Remove label due to unauthorized comment + if (isDryRun) { + console.log(`๐Ÿงช DRY-RUN: Would remove ${config.labelName} label from #${issue.number}`); + } else { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: config.labelName + }); + console.log(`๐Ÿท๏ธ Removed ${config.labelName} label from #${issue.number}`); + } + labelRemovedCount++; + } else { + // Close the issue + if (isDryRun) { + console.log(`๐Ÿงช DRY-RUN: Would close #${issue.number} with comment`); + console.log(`๐Ÿงช DRY-RUN: Comment would be: "${CLOSE_MESSAGE}"`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: CLOSE_MESSAGE + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + + console.log(`๐Ÿ”’ Closed #${issue.number}`); + } + closedCount++; + } + } catch (error) { + console.log(`โŒ Error processing #${issue.number}: ${error.message}`); + skippedCount++; + } + } + + // Summary + console.log(`\n๐Ÿ“Š Summary:`); + if (isDryRun) { + console.log(` ๐Ÿงช DRY-RUN MODE - No actual changes made:`); + console.log(` โ€ข Issues that would be closed: ${closedCount}`); + console.log(` โ€ข Labels that would be removed: ${labelRemovedCount}`); + } else { + console.log(` โ€ข Issues closed: ${closedCount}`); + console.log(` โ€ข Labels removed: ${labelRemovedCount}`); + } + console.log(` โ€ข Issues skipped: ${skippedCount}`); + console.log(` โ€ข Total processed: ${targetIssues.length}`); + + } catch (error) { + console.log(`โŒ Failed to search for issues: ${error.message}`); + throw error; + } From 23f7457d8afa2a81dfee57de4389a83ad40107d3 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Mon, 8 Sep 2025 14:54:38 -0400 Subject: [PATCH 02/19] fix: replace deprecated GitHub Search API with Issues API - Replace github.rest.search.issuesAndPullRequests with github.rest.issues.listForRepo - Add pagination support to handle repositories with many labeled issues --- .github/workflows/auto-close-3-days.yml | 39 ++++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index 73523e628..22e3d0fbe 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -96,23 +96,34 @@ jobs: return false; } - // Find issues with the target label - const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:open label:"${config.labelName}"`; - const typeFilter = config.issueTypes === 'pulls' ? 'is:pr' : - config.issueTypes === 'both' ? '' : 'is:issue'; + // Find issues with the target label using Issues API instead of deprecated Search API + let allIssues = []; + let page = 1; + const perPage = 100; - try { - const { data: searchResults } = await github.rest.search.issuesAndPullRequests({ - q: `${searchQuery} ${typeFilter}`.trim(), + while (true) { + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: config.labelName, sort: 'updated', - order: 'desc' + direction: 'desc', + per_page: perPage, + page: page }); - const targetIssues = searchResults.items.filter(issue => { - if (config.issueTypes === 'issues' && issue.pull_request) return false; - if (config.issueTypes === 'pulls' && !issue.pull_request) return false; - return true; - }); + if (issues.length === 0) break; + allIssues = allIssues.concat(issues); + if (issues.length < perPage) break; + page++; + } + + const targetIssues = allIssues.filter(issue => { + if (config.issueTypes === 'issues' && issue.pull_request) return false; + if (config.issueTypes === 'pulls' && !issue.pull_request) return false; + return true; + }); console.log(`๐Ÿ” Found ${targetIssues.length} items with label "${config.labelName}"`); @@ -238,6 +249,6 @@ jobs: console.log(` โ€ข Total processed: ${targetIssues.length}`); } catch (error) { - console.log(`โŒ Failed to search for issues: ${error.message}`); + console.log(`โŒ Failed to fetch issues: ${error.message}`); throw error; } From cc6989d390d6865760ac41544794e6319339376c Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Mon, 8 Sep 2025 15:41:31 -0400 Subject: [PATCH 03/19] fix: resolve JavaScript syntax error in GitHub workflow - Add missing try block around main logic - Fix indentation alignment for proper nesting - Resolves "Unexpected token 'catch'" error --- .github/workflows/auto-close-3-days.yml | 53 +++++++++++++------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index 22e3d0fbe..c022a9c0c 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -97,33 +97,34 @@ jobs: } // Find issues with the target label using Issues API instead of deprecated Search API - let allIssues = []; - let page = 1; - const perPage = 100; - - while (true) { - const { data: issues } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: config.labelName, - sort: 'updated', - direction: 'desc', - per_page: perPage, - page: page - }); + try { + let allIssues = []; + let page = 1; + const perPage = 100; - if (issues.length === 0) break; - allIssues = allIssues.concat(issues); - if (issues.length < perPage) break; - page++; - } - - const targetIssues = allIssues.filter(issue => { - if (config.issueTypes === 'issues' && issue.pull_request) return false; - if (config.issueTypes === 'pulls' && !issue.pull_request) return false; - return true; - }); + while (true) { + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: config.labelName, + sort: 'updated', + direction: 'desc', + per_page: perPage, + page: page + }); + + if (issues.length === 0) break; + allIssues = allIssues.concat(issues); + if (issues.length < perPage) break; + page++; + } + + const targetIssues = allIssues.filter(issue => { + if (config.issueTypes === 'issues' && issue.pull_request) return false; + if (config.issueTypes === 'pulls' && !issue.pull_request) return false; + return true; + }); console.log(`๐Ÿ” Found ${targetIssues.length} items with label "${config.labelName}"`); From 7a993889f74b40eac8505b69cbe65679cbf6f4e0 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Mon, 8 Sep 2025 15:54:19 -0400 Subject: [PATCH 04/19] feat: remove label immediately on unauthorized comments - Check for unauthorized comments before time validation - Remove label instantly when non-authorized users respond --- .github/workflows/auto-close-3-days.yml | 63 +++++++++++++------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index c022a9c0c..bbbdc8d14 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -162,14 +162,6 @@ jobs: const lastLabelAdded = new Date(labelEvents[0].created_at); const labelAdder = labelEvents[0].actor.login; - // Check if enough time has passed - if (lastLabelAdded > cutoffDate) { - const daysRemaining = Math.ceil((lastLabelAdded - cutoffDate) / (1000 * 60 * 60 * 24)); - console.log(`โณ Label added too recently (${daysRemaining} days remaining)`); - skippedCount++; - continue; - } - // Check comments since label was added const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, @@ -193,7 +185,7 @@ jobs: } if (hasUnauthorizedComment) { - // Remove label due to unauthorized comment + // Remove label due to unauthorized comment (regardless of time elapsed) if (isDryRun) { console.log(`๐Ÿงช DRY-RUN: Would remove ${config.labelName} label from #${issue.number}`); } else { @@ -206,30 +198,39 @@ jobs: console.log(`๐Ÿท๏ธ Removed ${config.labelName} label from #${issue.number}`); } labelRemovedCount++; + continue; + } + + // Check if enough time has passed for auto-close + if (lastLabelAdded > cutoffDate) { + const daysRemaining = Math.ceil((lastLabelAdded - cutoffDate) / (1000 * 60 * 60 * 24)); + console.log(`โณ Label added too recently (${daysRemaining} days remaining)`); + skippedCount++; + continue; + } + + // Close the issue (only if time has elapsed AND no unauthorized comments) + if (isDryRun) { + console.log(`๐Ÿงช DRY-RUN: Would close #${issue.number} with comment`); + console.log(`๐Ÿงช DRY-RUN: Comment would be: "${CLOSE_MESSAGE}"`); } else { - // Close the issue - if (isDryRun) { - console.log(`๐Ÿงช DRY-RUN: Would close #${issue.number} with comment`); - console.log(`๐Ÿงช DRY-RUN: Comment would be: "${CLOSE_MESSAGE}"`); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: CLOSE_MESSAGE - }); - - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - state: 'closed' - }); - - console.log(`๐Ÿ”’ Closed #${issue.number}`); - } - closedCount++; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: CLOSE_MESSAGE + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + + console.log(`๐Ÿ”’ Closed #${issue.number}`); } + closedCount++; } catch (error) { console.log(`โŒ Error processing #${issue.number}: ${error.message}`); skippedCount++; From 99f4c93397eca03f0f6d610c26ecd0daa2ec5ddd Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Mon, 8 Sep 2025 16:00:11 -0400 Subject: [PATCH 05/19] feat: add optional replacement label when removing auto-close label - Add REPLACEMENT_LABEL environment variable for optional label substitution - Apply replacement label when unauthorized users comment and auto-close label is removed --- .github/workflows/auto-close-3-days.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index bbbdc8d14..8159f7008 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -18,6 +18,7 @@ env: AUTH_MODE: 'write-access' # Options: 'users', 'write-access' ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' DRY_RUN: 'true' # Set to 'true' to enable dry-run mode (no actions taken) + REPLACEMENT_LABEL: '' # Optional: Label to add when removing the auto-close label jobs: auto-close: @@ -70,7 +71,8 @@ jobs: daysToWait: parseInt(process.env.DAYS_TO_WAIT), authMode: process.env.AUTH_MODE, authorizedUsers: process.env.AUTHORIZED_USERS?.split(',').map(u => u.trim()).filter(u => u) || [], - issueTypes: process.env.ISSUE_TYPES + issueTypes: process.env.ISSUE_TYPES, + replacementLabel: process.env.REPLACEMENT_LABEL?.trim() || null }; const cutoffDate = new Date(); @@ -178,7 +180,7 @@ jobs: const isAuthorized = await isAuthorizedUser(comment.user.login); if (!isAuthorized) { - console.log(`โŒ Unauthorized comment from ${comment.user.login}`); + console.log(`โŒ New comment from ${comment.user.login}`); hasUnauthorizedComment = true; break; } @@ -188,6 +190,9 @@ jobs: // Remove label due to unauthorized comment (regardless of time elapsed) if (isDryRun) { console.log(`๐Ÿงช DRY-RUN: Would remove ${config.labelName} label from #${issue.number}`); + if (config.replacementLabel) { + console.log(`๐Ÿงช DRY-RUN: Would add ${config.replacementLabel} label to #${issue.number}`); + } } else { await github.rest.issues.removeLabel({ owner: context.repo.owner, @@ -196,6 +201,16 @@ jobs: name: config.labelName }); console.log(`๐Ÿท๏ธ Removed ${config.labelName} label from #${issue.number}`); + + if (config.replacementLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [config.replacementLabel] + }); + console.log(`๐Ÿท๏ธ Added ${config.replacementLabel} label to #${issue.number}`); + } } labelRemovedCount++; continue; From b478b083e8ce665187ee96ead2241ef07bb7ea5f Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 09:10:21 -0400 Subject: [PATCH 06/19] chore: Update Name of GitHub Action --- .github/workflows/auto-close-3-days.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index 8159f7008..b688c4678 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -1,8 +1,8 @@ -name: Auto Close Issues +name: Auto Close 3 Days on: schedule: - - cron: '0 9 * * *' # Daily at 9 AM UTC + - cron: '0 14 * * 1-5' # 9 AM EST (2 PM UTC) Monday through Friday workflow_dispatch: inputs: dry_run: @@ -60,7 +60,7 @@ jobs: script: | // Constants const REQUIRED_PERMISSIONS = ['write', 'admin']; - const CLOSE_MESSAGE = `This issue has been automatically closed as it was marked for auto-closure by the team and no response was received within ${process.env.DAYS_TO_WAIT} days.`; + const CLOSE_MESSAGE = `This issue has been automatically closed as it was marked for auto-closure by the team and no addtional responses was received within ${process.env.DAYS_TO_WAIT} days.`; // Check for dry-run mode const isDryRun = '${{ inputs.dry_run }}' === 'true' || process.env.DRY_RUN === 'true'; From 04504ad1824d93803e48210f5246e57e69a344aa Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 09:10:58 -0400 Subject: [PATCH 07/19] chore: Testing closure by setting to one day --- .github/workflows/auto-close-3-days.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index b688c4678..2ffe987d7 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -13,7 +13,7 @@ on: env: LABEL_NAME: 'autoclose in 3 days' - DAYS_TO_WAIT: '3' + DAYS_TO_WAIT: '1' AUTHORIZED_USERS: '' # Comma-separated list (ex: user1,user2,user3) *required for 'users' mode AUTH_MODE: 'write-access' # Options: 'users', 'write-access' ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' From 6a70cdcba44d1d8ae974bc98fccf0c6b07c6f776 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 09:12:39 -0400 Subject: [PATCH 08/19] chore: Testing closure by setting to zero days --- .github/workflows/auto-close-3-days.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index 2ffe987d7..5b67aa857 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -13,7 +13,7 @@ on: env: LABEL_NAME: 'autoclose in 3 days' - DAYS_TO_WAIT: '1' + DAYS_TO_WAIT: '0' AUTHORIZED_USERS: '' # Comma-separated list (ex: user1,user2,user3) *required for 'users' mode AUTH_MODE: 'write-access' # Options: 'users', 'write-access' ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' From f3c2246095ef616b8286ecdbfa5e04c1cbd63607 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 09:16:15 -0400 Subject: [PATCH 09/19] fix: Allow for zero days --- .github/workflows/auto-close-3-days.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index 5b67aa857..266b29891 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -35,8 +35,8 @@ jobs: // Validate required fields if (!LABEL_NAME) throw new Error('LABEL_NAME is required'); - if (!DAYS_TO_WAIT || isNaN(parseInt(DAYS_TO_WAIT)) || parseInt(DAYS_TO_WAIT) < 1) { - throw new Error('DAYS_TO_WAIT must be a positive integer'); + if (!DAYS_TO_WAIT || isNaN(parseInt(DAYS_TO_WAIT)) || parseInt(DAYS_TO_WAIT) < 0) { + throw new Error('DAYS_TO_WAIT must be a positive integer or zero'); } if (!['users', 'write-access'].includes(AUTH_MODE)) { throw new Error('AUTH_MODE must be "users" or "write-access"'); From 860605fee16bf11aea7141717c510813a6030b68 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 11:15:51 -0400 Subject: [PATCH 10/19] chore: Test removal and replacement label --- .github/workflows/auto-close-3-days.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index 266b29891..b1630966f 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -18,7 +18,7 @@ env: AUTH_MODE: 'write-access' # Options: 'users', 'write-access' ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' DRY_RUN: 'true' # Set to 'true' to enable dry-run mode (no actions taken) - REPLACEMENT_LABEL: '' # Optional: Label to add when removing the auto-close label + REPLACEMENT_LABEL: 'invalid' # Optional: Label to add when removing the auto-close label jobs: auto-close: From b87e171bbfd7f8472a0e5aa51849d9996ae87834 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 11:28:48 -0400 Subject: [PATCH 11/19] fix: Fix to allow manual dispatch inputs take presence over set env variables --- .github/workflows/auto-close-3-days.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index b1630966f..dd1cd22d1 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -17,7 +17,7 @@ env: AUTHORIZED_USERS: '' # Comma-separated list (ex: user1,user2,user3) *required for 'users' mode AUTH_MODE: 'write-access' # Options: 'users', 'write-access' ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' - DRY_RUN: 'true' # Set to 'true' to enable dry-run mode (no actions taken) + DRY_RUN: 'false' # Set to 'true' to enable dry-run mode (no actions taken) REPLACEMENT_LABEL: 'invalid' # Optional: Label to add when removing the auto-close label jobs: @@ -30,8 +30,10 @@ jobs: script: | const { LABEL_NAME, DAYS_TO_WAIT, AUTH_MODE, AUTHORIZED_USERS, ISSUE_TYPES, DRY_RUN } = process.env; + const isManualTrigger = !!github.event.workflow_dispatch; // Check for manual dry-run override - const isDryRun = '${{ inputs.dry_run }}' === 'true' || DRY_RUN === 'true'; + const isDryRun = isManualTrigger ? '${{ inputs.dry_run }}' === 'true' : DRY_RUN === 'true'; + // Validate required fields if (!LABEL_NAME) throw new Error('LABEL_NAME is required'); From fd7ef168abbaf0c8fe22c44a4d8572209feb91a8 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 15:46:33 -0400 Subject: [PATCH 12/19] fix: Fix to allow manual dispatch inputs take presence over set env variables --- .github/workflows/auto-close-3-days.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index dd1cd22d1..b421a10a5 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -28,9 +28,9 @@ jobs: uses: actions/github-script@v8 with: script: | - const { LABEL_NAME, DAYS_TO_WAIT, AUTH_MODE, AUTHORIZED_USERS, ISSUE_TYPES, DRY_RUN } = process.env; + const { LABEL_NAME, DAYS_TO_WAIT, AUTH_MODE, AUTHORIZED_USERS, ISSUE_TYPES, DRY_RUN, GITHUB_EVENT_NAME } = process.env; - const isManualTrigger = !!github.event.workflow_dispatch; + const isManualTrigger = GITHUB_EVENT_NAME === 'workflow_dispatch'; // Check for manual dry-run override const isDryRun = isManualTrigger ? '${{ inputs.dry_run }}' === 'true' : DRY_RUN === 'true'; From e81388ccd99a7de37cd484acdaf4051fd64cf5f6 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 15:50:11 -0400 Subject: [PATCH 13/19] feat: Allow a closure message to be passed as an env variable --- .github/workflows/auto-close-3-days.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index b421a10a5..45eb723a1 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -19,6 +19,7 @@ env: ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' DRY_RUN: 'false' # Set to 'true' to enable dry-run mode (no actions taken) REPLACEMENT_LABEL: 'invalid' # Optional: Label to add when removing the auto-close label + CLOSE_MESSAGE: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within the allotted time.' jobs: auto-close: @@ -62,7 +63,7 @@ jobs: script: | // Constants const REQUIRED_PERMISSIONS = ['write', 'admin']; - const CLOSE_MESSAGE = `This issue has been automatically closed as it was marked for auto-closure by the team and no addtional responses was received within ${process.env.DAYS_TO_WAIT} days.`; + const CLOSE_MESSAGE = process.env.CLOSE_MESSAGE; // Check for dry-run mode const isDryRun = '${{ inputs.dry_run }}' === 'true' || process.env.DRY_RUN === 'true'; From 5b543e2c95282d7210deb820f66593159b66c4eb Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 16:15:22 -0400 Subject: [PATCH 14/19] chore: Prep for PR --- .github/workflows/auto-close-3-days.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml index 45eb723a1..87f02f3ca 100644 --- a/.github/workflows/auto-close-3-days.yml +++ b/.github/workflows/auto-close-3-days.yml @@ -8,17 +8,17 @@ on: dry_run: description: 'Run in dry-run mode (no actions taken, only logging)' required: false - default: 'true' + default: 'false' type: boolean env: LABEL_NAME: 'autoclose in 3 days' - DAYS_TO_WAIT: '0' + DAYS_TO_WAIT: '3' AUTHORIZED_USERS: '' # Comma-separated list (ex: user1,user2,user3) *required for 'users' mode AUTH_MODE: 'write-access' # Options: 'users', 'write-access' ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' DRY_RUN: 'false' # Set to 'true' to enable dry-run mode (no actions taken) - REPLACEMENT_LABEL: 'invalid' # Optional: Label to add when removing the auto-close label + REPLACEMENT_LABEL: '' # Optional: Label to add when removing the auto-close label CLOSE_MESSAGE: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within the allotted time.' jobs: From b9281eeced838f705a25dcca2b4543b17a3824c3 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Tue, 9 Sep 2025 16:15:44 -0400 Subject: [PATCH 15/19] feature: Add autoclose 7 days --- .github/workflows/auto-close-7-days.yml | 274 ++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 .github/workflows/auto-close-7-days.yml diff --git a/.github/workflows/auto-close-7-days.yml b/.github/workflows/auto-close-7-days.yml new file mode 100644 index 000000000..70b0ff5cc --- /dev/null +++ b/.github/workflows/auto-close-7-days.yml @@ -0,0 +1,274 @@ +name: Auto Close 7 Days + +on: + schedule: + - cron: '0 14 * * 1-5' # 9 AM EST (2 PM UTC) Monday through Friday + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no actions taken, only logging)' + required: false + default: 'false' + type: boolean + +env: + LABEL_NAME: 'autoclose in 7 days' + DAYS_TO_WAIT: '7' + AUTHORIZED_USERS: '' # Comma-separated list (ex: user1,user2,user3) *required for 'users' mode + AUTH_MODE: 'write-access' # Options: 'users', 'write-access' + ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' + DRY_RUN: 'false' # Set to 'true' to enable dry-run mode (no actions taken) + REPLACEMENT_LABEL: '' # Optional: Label to add when removing the auto-close label + CLOSE_MESSAGE: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within the allotted time.' + +jobs: + auto-close: + runs-on: ubuntu-latest + steps: + - name: Validate configuration + uses: actions/github-script@v8 + with: + script: | + const { LABEL_NAME, DAYS_TO_WAIT, AUTH_MODE, AUTHORIZED_USERS, ISSUE_TYPES, DRY_RUN, GITHUB_EVENT_NAME } = process.env; + + const isManualTrigger = GITHUB_EVENT_NAME === 'workflow_dispatch'; + // Check for manual dry-run override + const isDryRun = isManualTrigger ? '${{ inputs.dry_run }}' === 'true' : DRY_RUN === 'true'; + + + // Validate required fields + if (!LABEL_NAME) throw new Error('LABEL_NAME is required'); + if (!DAYS_TO_WAIT || isNaN(parseInt(DAYS_TO_WAIT)) || parseInt(DAYS_TO_WAIT) < 0) { + throw new Error('DAYS_TO_WAIT must be a positive integer or zero'); + } + if (!['users', 'write-access'].includes(AUTH_MODE)) { + throw new Error('AUTH_MODE must be "users" or "write-access"'); + } + if (!['issues', 'pulls', 'both'].includes(ISSUE_TYPES)) { + throw new Error('ISSUE_TYPES must be "issues", "pulls", or "both"'); + } + if (AUTH_MODE === 'users' && (!AUTHORIZED_USERS || AUTHORIZED_USERS.trim() === '')) { + throw new Error('AUTHORIZED_USERS is required when AUTH_MODE is "users"'); + } + + console.log('โœ… Configuration validated successfully'); + console.log(`Label: "${LABEL_NAME}", Days: ${DAYS_TO_WAIT}, Auth: ${AUTH_MODE}, Types: ${ISSUE_TYPES}`); + if (isDryRun) { + console.log('๐Ÿงช DRY-RUN MODE: No actions will be taken, only logging what would happen'); + } + + - name: Find and process labeled issues + uses: actions/github-script@v8 + with: + script: | + // Constants + const REQUIRED_PERMISSIONS = ['write', 'admin']; + const CLOSE_MESSAGE = process.env.CLOSE_MESSAGE; + + // Check for dry-run mode + const isDryRun = '${{ inputs.dry_run }}' === 'true' || process.env.DRY_RUN === 'true'; + + // Parse configuration + const config = { + labelName: process.env.LABEL_NAME, + daysToWait: parseInt(process.env.DAYS_TO_WAIT), + authMode: process.env.AUTH_MODE, + authorizedUsers: process.env.AUTHORIZED_USERS?.split(',').map(u => u.trim()).filter(u => u) || [], + issueTypes: process.env.ISSUE_TYPES, + replacementLabel: process.env.REPLACEMENT_LABEL?.trim() || null + }; + + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - config.daysToWait); + + // Authorization check function + async function isAuthorizedUser(username) { + try { + if (config.authMode === 'users') { + return config.authorizedUsers.includes(username); + } else if (config.authMode === 'write-access') { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: username + }); + return REQUIRED_PERMISSIONS.includes(data.permission); + } + } catch (error) { + console.log(`โš ๏ธ Failed to check authorization for ${username}: ${error.message}`); + return false; + } + return false; + } + + // Find issues with the target label using Issues API instead of deprecated Search API + try { + let allIssues = []; + let page = 1; + const perPage = 100; + + while (true) { + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: config.labelName, + sort: 'updated', + direction: 'desc', + per_page: perPage, + page: page + }); + + if (issues.length === 0) break; + allIssues = allIssues.concat(issues); + if (issues.length < perPage) break; + page++; + } + + const targetIssues = allIssues.filter(issue => { + if (config.issueTypes === 'issues' && issue.pull_request) return false; + if (config.issueTypes === 'pulls' && !issue.pull_request) return false; + return true; + }); + + console.log(`๐Ÿ” Found ${targetIssues.length} items with label "${config.labelName}"`); + + if (targetIssues.length === 0) { + console.log('โœ… No items to process'); + return; + } + + // Process each issue + let closedCount = 0; + let labelRemovedCount = 0; + let skippedCount = 0; + + for (const issue of targetIssues) { + console.log(`\n๐Ÿ“‹ Processing #${issue.number}: ${issue.title}`); + + try { + // Get label events to find when label was last added + const { data: events } = await github.rest.issues.listEvents({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number + }); + + const labelEvents = events + .filter(e => e.event === 'labeled' && e.label?.name === config.labelName) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + if (labelEvents.length === 0) { + console.log(`โš ๏ธ No label events found for #${issue.number}`); + skippedCount++; + continue; + } + + const lastLabelAdded = new Date(labelEvents[0].created_at); + const labelAdder = labelEvents[0].actor.login; + + // Check comments since label was added + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + since: lastLabelAdded.toISOString() + }); + + let hasUnauthorizedComment = false; + + for (const comment of comments) { + // Skip comments from the person who added the label + if (comment.user.login === labelAdder) continue; + + const isAuthorized = await isAuthorizedUser(comment.user.login); + if (!isAuthorized) { + console.log(`โŒ New comment from ${comment.user.login}`); + hasUnauthorizedComment = true; + break; + } + } + + if (hasUnauthorizedComment) { + // Remove label due to unauthorized comment (regardless of time elapsed) + if (isDryRun) { + console.log(`๐Ÿงช DRY-RUN: Would remove ${config.labelName} label from #${issue.number}`); + if (config.replacementLabel) { + console.log(`๐Ÿงช DRY-RUN: Would add ${config.replacementLabel} label to #${issue.number}`); + } + } else { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: config.labelName + }); + console.log(`๐Ÿท๏ธ Removed ${config.labelName} label from #${issue.number}`); + + if (config.replacementLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [config.replacementLabel] + }); + console.log(`๐Ÿท๏ธ Added ${config.replacementLabel} label to #${issue.number}`); + } + } + labelRemovedCount++; + continue; + } + + // Check if enough time has passed for auto-close + if (lastLabelAdded > cutoffDate) { + const daysRemaining = Math.ceil((lastLabelAdded - cutoffDate) / (1000 * 60 * 60 * 24)); + console.log(`โณ Label added too recently (${daysRemaining} days remaining)`); + skippedCount++; + continue; + } + + // Close the issue (only if time has elapsed AND no unauthorized comments) + if (isDryRun) { + console.log(`๐Ÿงช DRY-RUN: Would close #${issue.number} with comment`); + console.log(`๐Ÿงช DRY-RUN: Comment would be: "${CLOSE_MESSAGE}"`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: CLOSE_MESSAGE + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + + console.log(`๐Ÿ”’ Closed #${issue.number}`); + } + closedCount++; + } catch (error) { + console.log(`โŒ Error processing #${issue.number}: ${error.message}`); + skippedCount++; + } + } + + // Summary + console.log(`\n๐Ÿ“Š Summary:`); + if (isDryRun) { + console.log(` ๐Ÿงช DRY-RUN MODE - No actual changes made:`); + console.log(` โ€ข Issues that would be closed: ${closedCount}`); + console.log(` โ€ข Labels that would be removed: ${labelRemovedCount}`); + } else { + console.log(` โ€ข Issues closed: ${closedCount}`); + console.log(` โ€ข Labels removed: ${labelRemovedCount}`); + } + console.log(` โ€ข Issues skipped: ${skippedCount}`); + console.log(` โ€ข Total processed: ${targetIssues.length}`); + + } catch (error) { + console.log(`โŒ Failed to fetch issues: ${error.message}`); + throw error; + } From 13def8d348f7963cb270603aac86d66c2da99c89 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Thu, 11 Sep 2025 10:40:53 -0400 Subject: [PATCH 16/19] feat: Consolidate auto-close workflows into single matrix-based action - Merge auto-close-3-days.yml and auto-close-7-days.yml into auto-close.yml - Use matrix strategy to handle both 3-day and 7-day label processing --- .github/workflows/auto-close-3-days.yml | 274 ------------------------ .github/workflows/auto-close-7-days.yml | 274 ------------------------ .github/workflows/auto-close.yml | 237 ++++++++++++++++++++ 3 files changed, 237 insertions(+), 548 deletions(-) delete mode 100644 .github/workflows/auto-close-3-days.yml delete mode 100644 .github/workflows/auto-close-7-days.yml create mode 100644 .github/workflows/auto-close.yml diff --git a/.github/workflows/auto-close-3-days.yml b/.github/workflows/auto-close-3-days.yml deleted file mode 100644 index 87f02f3ca..000000000 --- a/.github/workflows/auto-close-3-days.yml +++ /dev/null @@ -1,274 +0,0 @@ -name: Auto Close 3 Days - -on: - schedule: - - cron: '0 14 * * 1-5' # 9 AM EST (2 PM UTC) Monday through Friday - workflow_dispatch: - inputs: - dry_run: - description: 'Run in dry-run mode (no actions taken, only logging)' - required: false - default: 'false' - type: boolean - -env: - LABEL_NAME: 'autoclose in 3 days' - DAYS_TO_WAIT: '3' - AUTHORIZED_USERS: '' # Comma-separated list (ex: user1,user2,user3) *required for 'users' mode - AUTH_MODE: 'write-access' # Options: 'users', 'write-access' - ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' - DRY_RUN: 'false' # Set to 'true' to enable dry-run mode (no actions taken) - REPLACEMENT_LABEL: '' # Optional: Label to add when removing the auto-close label - CLOSE_MESSAGE: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within the allotted time.' - -jobs: - auto-close: - runs-on: ubuntu-latest - steps: - - name: Validate configuration - uses: actions/github-script@v8 - with: - script: | - const { LABEL_NAME, DAYS_TO_WAIT, AUTH_MODE, AUTHORIZED_USERS, ISSUE_TYPES, DRY_RUN, GITHUB_EVENT_NAME } = process.env; - - const isManualTrigger = GITHUB_EVENT_NAME === 'workflow_dispatch'; - // Check for manual dry-run override - const isDryRun = isManualTrigger ? '${{ inputs.dry_run }}' === 'true' : DRY_RUN === 'true'; - - - // Validate required fields - if (!LABEL_NAME) throw new Error('LABEL_NAME is required'); - if (!DAYS_TO_WAIT || isNaN(parseInt(DAYS_TO_WAIT)) || parseInt(DAYS_TO_WAIT) < 0) { - throw new Error('DAYS_TO_WAIT must be a positive integer or zero'); - } - if (!['users', 'write-access'].includes(AUTH_MODE)) { - throw new Error('AUTH_MODE must be "users" or "write-access"'); - } - if (!['issues', 'pulls', 'both'].includes(ISSUE_TYPES)) { - throw new Error('ISSUE_TYPES must be "issues", "pulls", or "both"'); - } - if (AUTH_MODE === 'users' && (!AUTHORIZED_USERS || AUTHORIZED_USERS.trim() === '')) { - throw new Error('AUTHORIZED_USERS is required when AUTH_MODE is "users"'); - } - - console.log('โœ… Configuration validated successfully'); - console.log(`Label: "${LABEL_NAME}", Days: ${DAYS_TO_WAIT}, Auth: ${AUTH_MODE}, Types: ${ISSUE_TYPES}`); - if (isDryRun) { - console.log('๐Ÿงช DRY-RUN MODE: No actions will be taken, only logging what would happen'); - } - - - name: Find and process labeled issues - uses: actions/github-script@v8 - with: - script: | - // Constants - const REQUIRED_PERMISSIONS = ['write', 'admin']; - const CLOSE_MESSAGE = process.env.CLOSE_MESSAGE; - - // Check for dry-run mode - const isDryRun = '${{ inputs.dry_run }}' === 'true' || process.env.DRY_RUN === 'true'; - - // Parse configuration - const config = { - labelName: process.env.LABEL_NAME, - daysToWait: parseInt(process.env.DAYS_TO_WAIT), - authMode: process.env.AUTH_MODE, - authorizedUsers: process.env.AUTHORIZED_USERS?.split(',').map(u => u.trim()).filter(u => u) || [], - issueTypes: process.env.ISSUE_TYPES, - replacementLabel: process.env.REPLACEMENT_LABEL?.trim() || null - }; - - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - config.daysToWait); - - // Authorization check function - async function isAuthorizedUser(username) { - try { - if (config.authMode === 'users') { - return config.authorizedUsers.includes(username); - } else if (config.authMode === 'write-access') { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: username - }); - return REQUIRED_PERMISSIONS.includes(data.permission); - } - } catch (error) { - console.log(`โš ๏ธ Failed to check authorization for ${username}: ${error.message}`); - return false; - } - return false; - } - - // Find issues with the target label using Issues API instead of deprecated Search API - try { - let allIssues = []; - let page = 1; - const perPage = 100; - - while (true) { - const { data: issues } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: config.labelName, - sort: 'updated', - direction: 'desc', - per_page: perPage, - page: page - }); - - if (issues.length === 0) break; - allIssues = allIssues.concat(issues); - if (issues.length < perPage) break; - page++; - } - - const targetIssues = allIssues.filter(issue => { - if (config.issueTypes === 'issues' && issue.pull_request) return false; - if (config.issueTypes === 'pulls' && !issue.pull_request) return false; - return true; - }); - - console.log(`๐Ÿ” Found ${targetIssues.length} items with label "${config.labelName}"`); - - if (targetIssues.length === 0) { - console.log('โœ… No items to process'); - return; - } - - // Process each issue - let closedCount = 0; - let labelRemovedCount = 0; - let skippedCount = 0; - - for (const issue of targetIssues) { - console.log(`\n๐Ÿ“‹ Processing #${issue.number}: ${issue.title}`); - - try { - // Get label events to find when label was last added - const { data: events } = await github.rest.issues.listEvents({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number - }); - - const labelEvents = events - .filter(e => e.event === 'labeled' && e.label?.name === config.labelName) - .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - - if (labelEvents.length === 0) { - console.log(`โš ๏ธ No label events found for #${issue.number}`); - skippedCount++; - continue; - } - - const lastLabelAdded = new Date(labelEvents[0].created_at); - const labelAdder = labelEvents[0].actor.login; - - // Check comments since label was added - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - since: lastLabelAdded.toISOString() - }); - - let hasUnauthorizedComment = false; - - for (const comment of comments) { - // Skip comments from the person who added the label - if (comment.user.login === labelAdder) continue; - - const isAuthorized = await isAuthorizedUser(comment.user.login); - if (!isAuthorized) { - console.log(`โŒ New comment from ${comment.user.login}`); - hasUnauthorizedComment = true; - break; - } - } - - if (hasUnauthorizedComment) { - // Remove label due to unauthorized comment (regardless of time elapsed) - if (isDryRun) { - console.log(`๐Ÿงช DRY-RUN: Would remove ${config.labelName} label from #${issue.number}`); - if (config.replacementLabel) { - console.log(`๐Ÿงช DRY-RUN: Would add ${config.replacementLabel} label to #${issue.number}`); - } - } else { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: config.labelName - }); - console.log(`๐Ÿท๏ธ Removed ${config.labelName} label from #${issue.number}`); - - if (config.replacementLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: [config.replacementLabel] - }); - console.log(`๐Ÿท๏ธ Added ${config.replacementLabel} label to #${issue.number}`); - } - } - labelRemovedCount++; - continue; - } - - // Check if enough time has passed for auto-close - if (lastLabelAdded > cutoffDate) { - const daysRemaining = Math.ceil((lastLabelAdded - cutoffDate) / (1000 * 60 * 60 * 24)); - console.log(`โณ Label added too recently (${daysRemaining} days remaining)`); - skippedCount++; - continue; - } - - // Close the issue (only if time has elapsed AND no unauthorized comments) - if (isDryRun) { - console.log(`๐Ÿงช DRY-RUN: Would close #${issue.number} with comment`); - console.log(`๐Ÿงช DRY-RUN: Comment would be: "${CLOSE_MESSAGE}"`); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: CLOSE_MESSAGE - }); - - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - state: 'closed' - }); - - console.log(`๐Ÿ”’ Closed #${issue.number}`); - } - closedCount++; - } catch (error) { - console.log(`โŒ Error processing #${issue.number}: ${error.message}`); - skippedCount++; - } - } - - // Summary - console.log(`\n๐Ÿ“Š Summary:`); - if (isDryRun) { - console.log(` ๐Ÿงช DRY-RUN MODE - No actual changes made:`); - console.log(` โ€ข Issues that would be closed: ${closedCount}`); - console.log(` โ€ข Labels that would be removed: ${labelRemovedCount}`); - } else { - console.log(` โ€ข Issues closed: ${closedCount}`); - console.log(` โ€ข Labels removed: ${labelRemovedCount}`); - } - console.log(` โ€ข Issues skipped: ${skippedCount}`); - console.log(` โ€ข Total processed: ${targetIssues.length}`); - - } catch (error) { - console.log(`โŒ Failed to fetch issues: ${error.message}`); - throw error; - } diff --git a/.github/workflows/auto-close-7-days.yml b/.github/workflows/auto-close-7-days.yml deleted file mode 100644 index 70b0ff5cc..000000000 --- a/.github/workflows/auto-close-7-days.yml +++ /dev/null @@ -1,274 +0,0 @@ -name: Auto Close 7 Days - -on: - schedule: - - cron: '0 14 * * 1-5' # 9 AM EST (2 PM UTC) Monday through Friday - workflow_dispatch: - inputs: - dry_run: - description: 'Run in dry-run mode (no actions taken, only logging)' - required: false - default: 'false' - type: boolean - -env: - LABEL_NAME: 'autoclose in 7 days' - DAYS_TO_WAIT: '7' - AUTHORIZED_USERS: '' # Comma-separated list (ex: user1,user2,user3) *required for 'users' mode - AUTH_MODE: 'write-access' # Options: 'users', 'write-access' - ISSUE_TYPES: 'issues' # Options: 'issues', 'pulls', 'both' - DRY_RUN: 'false' # Set to 'true' to enable dry-run mode (no actions taken) - REPLACEMENT_LABEL: '' # Optional: Label to add when removing the auto-close label - CLOSE_MESSAGE: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within the allotted time.' - -jobs: - auto-close: - runs-on: ubuntu-latest - steps: - - name: Validate configuration - uses: actions/github-script@v8 - with: - script: | - const { LABEL_NAME, DAYS_TO_WAIT, AUTH_MODE, AUTHORIZED_USERS, ISSUE_TYPES, DRY_RUN, GITHUB_EVENT_NAME } = process.env; - - const isManualTrigger = GITHUB_EVENT_NAME === 'workflow_dispatch'; - // Check for manual dry-run override - const isDryRun = isManualTrigger ? '${{ inputs.dry_run }}' === 'true' : DRY_RUN === 'true'; - - - // Validate required fields - if (!LABEL_NAME) throw new Error('LABEL_NAME is required'); - if (!DAYS_TO_WAIT || isNaN(parseInt(DAYS_TO_WAIT)) || parseInt(DAYS_TO_WAIT) < 0) { - throw new Error('DAYS_TO_WAIT must be a positive integer or zero'); - } - if (!['users', 'write-access'].includes(AUTH_MODE)) { - throw new Error('AUTH_MODE must be "users" or "write-access"'); - } - if (!['issues', 'pulls', 'both'].includes(ISSUE_TYPES)) { - throw new Error('ISSUE_TYPES must be "issues", "pulls", or "both"'); - } - if (AUTH_MODE === 'users' && (!AUTHORIZED_USERS || AUTHORIZED_USERS.trim() === '')) { - throw new Error('AUTHORIZED_USERS is required when AUTH_MODE is "users"'); - } - - console.log('โœ… Configuration validated successfully'); - console.log(`Label: "${LABEL_NAME}", Days: ${DAYS_TO_WAIT}, Auth: ${AUTH_MODE}, Types: ${ISSUE_TYPES}`); - if (isDryRun) { - console.log('๐Ÿงช DRY-RUN MODE: No actions will be taken, only logging what would happen'); - } - - - name: Find and process labeled issues - uses: actions/github-script@v8 - with: - script: | - // Constants - const REQUIRED_PERMISSIONS = ['write', 'admin']; - const CLOSE_MESSAGE = process.env.CLOSE_MESSAGE; - - // Check for dry-run mode - const isDryRun = '${{ inputs.dry_run }}' === 'true' || process.env.DRY_RUN === 'true'; - - // Parse configuration - const config = { - labelName: process.env.LABEL_NAME, - daysToWait: parseInt(process.env.DAYS_TO_WAIT), - authMode: process.env.AUTH_MODE, - authorizedUsers: process.env.AUTHORIZED_USERS?.split(',').map(u => u.trim()).filter(u => u) || [], - issueTypes: process.env.ISSUE_TYPES, - replacementLabel: process.env.REPLACEMENT_LABEL?.trim() || null - }; - - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - config.daysToWait); - - // Authorization check function - async function isAuthorizedUser(username) { - try { - if (config.authMode === 'users') { - return config.authorizedUsers.includes(username); - } else if (config.authMode === 'write-access') { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: username - }); - return REQUIRED_PERMISSIONS.includes(data.permission); - } - } catch (error) { - console.log(`โš ๏ธ Failed to check authorization for ${username}: ${error.message}`); - return false; - } - return false; - } - - // Find issues with the target label using Issues API instead of deprecated Search API - try { - let allIssues = []; - let page = 1; - const perPage = 100; - - while (true) { - const { data: issues } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: config.labelName, - sort: 'updated', - direction: 'desc', - per_page: perPage, - page: page - }); - - if (issues.length === 0) break; - allIssues = allIssues.concat(issues); - if (issues.length < perPage) break; - page++; - } - - const targetIssues = allIssues.filter(issue => { - if (config.issueTypes === 'issues' && issue.pull_request) return false; - if (config.issueTypes === 'pulls' && !issue.pull_request) return false; - return true; - }); - - console.log(`๐Ÿ” Found ${targetIssues.length} items with label "${config.labelName}"`); - - if (targetIssues.length === 0) { - console.log('โœ… No items to process'); - return; - } - - // Process each issue - let closedCount = 0; - let labelRemovedCount = 0; - let skippedCount = 0; - - for (const issue of targetIssues) { - console.log(`\n๐Ÿ“‹ Processing #${issue.number}: ${issue.title}`); - - try { - // Get label events to find when label was last added - const { data: events } = await github.rest.issues.listEvents({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number - }); - - const labelEvents = events - .filter(e => e.event === 'labeled' && e.label?.name === config.labelName) - .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - - if (labelEvents.length === 0) { - console.log(`โš ๏ธ No label events found for #${issue.number}`); - skippedCount++; - continue; - } - - const lastLabelAdded = new Date(labelEvents[0].created_at); - const labelAdder = labelEvents[0].actor.login; - - // Check comments since label was added - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - since: lastLabelAdded.toISOString() - }); - - let hasUnauthorizedComment = false; - - for (const comment of comments) { - // Skip comments from the person who added the label - if (comment.user.login === labelAdder) continue; - - const isAuthorized = await isAuthorizedUser(comment.user.login); - if (!isAuthorized) { - console.log(`โŒ New comment from ${comment.user.login}`); - hasUnauthorizedComment = true; - break; - } - } - - if (hasUnauthorizedComment) { - // Remove label due to unauthorized comment (regardless of time elapsed) - if (isDryRun) { - console.log(`๐Ÿงช DRY-RUN: Would remove ${config.labelName} label from #${issue.number}`); - if (config.replacementLabel) { - console.log(`๐Ÿงช DRY-RUN: Would add ${config.replacementLabel} label to #${issue.number}`); - } - } else { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: config.labelName - }); - console.log(`๐Ÿท๏ธ Removed ${config.labelName} label from #${issue.number}`); - - if (config.replacementLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: [config.replacementLabel] - }); - console.log(`๐Ÿท๏ธ Added ${config.replacementLabel} label to #${issue.number}`); - } - } - labelRemovedCount++; - continue; - } - - // Check if enough time has passed for auto-close - if (lastLabelAdded > cutoffDate) { - const daysRemaining = Math.ceil((lastLabelAdded - cutoffDate) / (1000 * 60 * 60 * 24)); - console.log(`โณ Label added too recently (${daysRemaining} days remaining)`); - skippedCount++; - continue; - } - - // Close the issue (only if time has elapsed AND no unauthorized comments) - if (isDryRun) { - console.log(`๐Ÿงช DRY-RUN: Would close #${issue.number} with comment`); - console.log(`๐Ÿงช DRY-RUN: Comment would be: "${CLOSE_MESSAGE}"`); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: CLOSE_MESSAGE - }); - - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - state: 'closed' - }); - - console.log(`๐Ÿ”’ Closed #${issue.number}`); - } - closedCount++; - } catch (error) { - console.log(`โŒ Error processing #${issue.number}: ${error.message}`); - skippedCount++; - } - } - - // Summary - console.log(`\n๐Ÿ“Š Summary:`); - if (isDryRun) { - console.log(` ๐Ÿงช DRY-RUN MODE - No actual changes made:`); - console.log(` โ€ข Issues that would be closed: ${closedCount}`); - console.log(` โ€ข Labels that would be removed: ${labelRemovedCount}`); - } else { - console.log(` โ€ข Issues closed: ${closedCount}`); - console.log(` โ€ข Labels removed: ${labelRemovedCount}`); - } - console.log(` โ€ข Issues skipped: ${skippedCount}`); - console.log(` โ€ข Total processed: ${targetIssues.length}`); - - } catch (error) { - console.log(`โŒ Failed to fetch issues: ${error.message}`); - throw error; - } diff --git a/.github/workflows/auto-close.yml b/.github/workflows/auto-close.yml new file mode 100644 index 000000000..3d957938f --- /dev/null +++ b/.github/workflows/auto-close.yml @@ -0,0 +1,237 @@ +name: Auto Close Issues + +on: + schedule: + - cron: '0 14 * * 1-5' # 9 AM EST (2 PM UTC) Monday through Friday + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no actions taken, only logging)' + required: false + default: 'false' + type: boolean + +jobs: + auto-close: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - label: 'autoclose in 3 days' + days: 3 + issue_types: 'issues' // issues/pulls/both + replacement_label: '' + closure_message: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within 3 days.' + dry_run: 'false' + - label: 'autoclose in 7 days' + days: 7 + issue_types: 'issues' // issues/pulls/both + replacement_label: '' + closure_message: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within 7 days.' + dry_run: 'false' + steps: + - name: Validate and process ${{ matrix.label }} + uses: actions/github-script@v8 + env: + LABEL_NAME: ${{ matrix.label }} + DAYS_TO_WAIT: ${{ matrix.days }} + AUTHORIZED_USERS: '' + AUTH_MODE: 'write-access' + ISSUE_TYPES: ${{ matrix.issue_types }} + DRY_RUN: ${{ matrix.dry_run }} + REPLACEMENT_LABEL: ${{ matrix.replacement_label }} + CLOSE_MESSAGE: ${{matrix.closure_message}} + with: + script: | + const REQUIRED_PERMISSIONS = ['write', 'admin']; + const CLOSE_MESSAGE = process.env.CLOSE_MESSAGE; + const isDryRun = '${{ inputs.dry_run }}' === 'true' || process.env.DRY_RUN === 'true'; + + const config = { + labelName: process.env.LABEL_NAME, + daysToWait: parseInt(process.env.DAYS_TO_WAIT), + authMode: process.env.AUTH_MODE, + authorizedUsers: process.env.AUTHORIZED_USERS?.split(',').map(u => u.trim()).filter(u => u) || [], + issueTypes: process.env.ISSUE_TYPES, + replacementLabel: process.env.REPLACEMENT_LABEL?.trim() || null + }; + + console.log(`๐Ÿท๏ธ Processing label: "${config.labelName}" (${config.daysToWait} days)`); + if (isDryRun) console.log('๐Ÿงช DRY-RUN MODE: No actions will be taken'); + + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - config.daysToWait); + + async function isAuthorizedUser(username) { + try { + if (config.authMode === 'users') { + return config.authorizedUsers.includes(username); + } else if (config.authMode === 'write-access') { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: username + }); + return REQUIRED_PERMISSIONS.includes(data.permission); + } + } catch (error) { + console.log(`โš ๏ธ Failed to check authorization for ${username}: ${error.message}`); + return false; + } + return false; + } + + let allIssues = []; + let page = 1; + + while (true) { + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: config.labelName, + sort: 'updated', + direction: 'desc', + per_page: 100, + page: page + }); + + if (issues.length === 0) break; + allIssues = allIssues.concat(issues); + if (issues.length < 100) break; + page++; + } + + const targetIssues = allIssues.filter(issue => { + if (config.issueTypes === 'issues' && issue.pull_request) return false; + if (config.issueTypes === 'pulls' && !issue.pull_request) return false; + return true; + }); + + console.log(`๐Ÿ” Found ${targetIssues.length} items with label "${config.labelName}"`); + + if (targetIssues.length === 0) { + console.log('โœ… No items to process'); + return; + } + + let closedCount = 0; + let labelRemovedCount = 0; + let skippedCount = 0; + + for (const issue of targetIssues) { + console.log(`\n๐Ÿ“‹ Processing #${issue.number}: ${issue.title}`); + + try { + const { data: events } = await github.rest.issues.listEvents({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number + }); + + const labelEvents = events + .filter(e => e.event === 'labeled' && e.label?.name === config.labelName) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + if (labelEvents.length === 0) { + console.log(`โš ๏ธ No label events found for #${issue.number}`); + skippedCount++; + continue; + } + + const lastLabelAdded = new Date(labelEvents[0].created_at); + const labelAdder = labelEvents[0].actor.login; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + since: lastLabelAdded.toISOString() + }); + + let hasUnauthorizedComment = false; + + for (const comment of comments) { + if (comment.user.login === labelAdder) continue; + + const isAuthorized = await isAuthorizedUser(comment.user.login); + if (!isAuthorized) { + console.log(`โŒ New comment from ${comment.user.login}`); + hasUnauthorizedComment = true; + break; + } + } + + if (hasUnauthorizedComment) { + if (isDryRun) { + console.log(`๐Ÿงช DRY-RUN: Would remove ${config.labelName} label from #${issue.number}`); + if (config.replacementLabel) { + console.log(`๐Ÿงช DRY-RUN: Would add ${config.replacementLabel} label to #${issue.number}`); + } + } else { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: config.labelName + }); + console.log(`๐Ÿท๏ธ Removed ${config.labelName} label from #${issue.number}`); + + if (config.replacementLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [config.replacementLabel] + }); + console.log(`๐Ÿท๏ธ Added ${config.replacementLabel} label to #${issue.number}`); + } + } + labelRemovedCount++; + continue; + } + + if (lastLabelAdded > cutoffDate) { + const daysRemaining = Math.ceil((lastLabelAdded - cutoffDate) / (1000 * 60 * 60 * 24)); + console.log(`โณ Label added too recently (${daysRemaining} days remaining)`); + skippedCount++; + continue; + } + + if (isDryRun) { + console.log(`๐Ÿงช DRY-RUN: Would close #${issue.number} with comment`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: CLOSE_MESSAGE + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + + console.log(`๐Ÿ”’ Closed #${issue.number}`); + } + closedCount++; + } catch (error) { + console.log(`โŒ Error processing #${issue.number}: ${error.message}`); + skippedCount++; + } + } + + console.log(`\n๐Ÿ“Š Summary for "${config.labelName}":`); + if (isDryRun) { + console.log(` ๐Ÿงช DRY-RUN MODE - No actual changes made:`); + console.log(` โ€ข Issues that would be closed: ${closedCount}`); + console.log(` โ€ข Labels that would be removed: ${labelRemovedCount}`); + } else { + console.log(` โ€ข Issues closed: ${closedCount}`); + console.log(` โ€ข Labels removed: ${labelRemovedCount}`); + } + console.log(` โ€ข Issues skipped: ${skippedCount}`); + console.log(` โ€ข Total processed: ${targetIssues.length}`); From 6a99002fe142049c961513ae7a68f1c58dce018e Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Thu, 11 Sep 2025 10:43:28 -0400 Subject: [PATCH 17/19] fix: Updated Comment syntax for yaml --- .github/workflows/auto-close.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-close.yml b/.github/workflows/auto-close.yml index 3d957938f..5c402f619 100644 --- a/.github/workflows/auto-close.yml +++ b/.github/workflows/auto-close.yml @@ -19,13 +19,13 @@ jobs: include: - label: 'autoclose in 3 days' days: 3 - issue_types: 'issues' // issues/pulls/both + issue_types: 'issues' #issues/pulls/both replacement_label: '' closure_message: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within 3 days.' dry_run: 'false' - label: 'autoclose in 7 days' days: 7 - issue_types: 'issues' // issues/pulls/both + issue_types: 'issues' # issues/pulls/both replacement_label: '' closure_message: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within 7 days.' dry_run: 'false' From 75a84c818ade16709b1efa9c08bb74c2ee48f5cd Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Thu, 11 Sep 2025 13:56:46 -0400 Subject: [PATCH 18/19] Create test.yml --- .github/workflows/test.yml | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..28a84a8ba --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,102 @@ +name: Triage on Comment + +on: + issue_comment: + types: [created] + +permissions: + issues: write + pull-requests: write + +env: + LABEL_AWAITING_TEAM: "Awaiting Team" + LABEL_AWAITING_CUSTOMER: "Awaiting Customer" + APPLY_TO_PULL_REQUESTS: "false" + TEAM_USERNAMES: "" # Comma-separated list, e.g. "user1,user2,user3" + PERMISSION_MODE: "write-access" # Either "users" or "write-access" + +jobs: + label-comment: + runs-on: ubuntu-latest + steps: + - name: Check user permissions + id: check-permissions + uses: actions/github-script@v8 + with: + script: | + const issueNumber = context.issue.number; + const issueType = github.event.issue.pull_request ? 'PR' : 'Issue'; + + // Skip if issue is a PR and not configured to support PR + if (issueType === 'PR' && process.env.APPLY_TO_PULL_REQUESTS !== 'true'){ + console.log(`Skipping Issue ${issueNumber} is a PR and PRs are not configured.`); + core.setOutput('skip', 'true'); + return; + } + + // Skip if commenter is a bot + if (context.actor.endsWith('[bot]')) { + console.log(`Skipping bot user: ${context.actor}`); + core.setOutput('skip', 'true'); + return; + } + + let hasWriteAccess = false; + const permissionMode = process.env.PERMISSION_MODE; + + console.log(`Processing ${issueType} #${issueNumber}, commenter: ${context.actor}, mode: ${permissionMode}`); + + if (permissionMode === 'users') { + const teamUsernames = process.env.TEAM_USERNAMES; + const userList = teamUsernames ? teamUsernames.split(',').map(u => u.trim()) : []; + hasWriteAccess = !userList.includes(context.actor); + console.log(`Username list mode - User ${context.actor} classified as: ${hasWriteAccess ? 'customer' : 'team member'}`); + } else { + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor + }); + hasWriteAccess = ['admin', 'write'].includes(permission.permission); + console.log(`Write-access mode - User ${context.actor} permission: ${permission.permission}, classified as: ${hasWriteAccess ? 'customer' : 'team member'}`); + } + + const labelToAdd = hasWriteAccess ? process.env.LABEL_AWAITING_CUSTOMER : process.env.LABEL_AWAITING_TEAM; + + console.log(`${issueType} #${issueNumber}: Will apply label "${labelToAdd}"`); + + core.setOutput('label-to-add', labelToAdd); + core.setOutput('skip', 'false'); + + - name: Update labels + if: steps.check-permissions.outputs.skip == 'false' + uses: actions/github-script@v8 + with: + script: | + const issueNumber = context.issue.number; + const labelToAdd = '${{ steps.check-permissions.outputs.label-to-add }}'; + const issueType = github.event.issue.pull_request ? 'PR' : 'Issue'; + + // Get current labels + const { data: issue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber + }); + + // Filter out conflicting labels and add new one + const conflictingLabels = [process.env.LABEL_AWAITING_TEAM, process.env.LABEL_AWAITING_CUSTOMER]; + const newLabels = issue.labels + .map(label => label.name) + .filter(name => !conflictingLabels.includes(name)) + .concat([labelToAdd]); + + // Replace all labels atomically + await github.rest.issues.setLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: newLabels + }); + + console.log(`${issueType} #${issueNumber}: Successfully applied label "${labelToAdd}"`); From f49434b4772170fde0f41391a3ece381b687c6ca Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Thu, 11 Sep 2025 13:57:19 -0400 Subject: [PATCH 19/19] Delete .github/workflows/test.yml --- .github/workflows/test.yml | 102 ------------------------------------- 1 file changed, 102 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 28a84a8ba..000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Triage on Comment - -on: - issue_comment: - types: [created] - -permissions: - issues: write - pull-requests: write - -env: - LABEL_AWAITING_TEAM: "Awaiting Team" - LABEL_AWAITING_CUSTOMER: "Awaiting Customer" - APPLY_TO_PULL_REQUESTS: "false" - TEAM_USERNAMES: "" # Comma-separated list, e.g. "user1,user2,user3" - PERMISSION_MODE: "write-access" # Either "users" or "write-access" - -jobs: - label-comment: - runs-on: ubuntu-latest - steps: - - name: Check user permissions - id: check-permissions - uses: actions/github-script@v8 - with: - script: | - const issueNumber = context.issue.number; - const issueType = github.event.issue.pull_request ? 'PR' : 'Issue'; - - // Skip if issue is a PR and not configured to support PR - if (issueType === 'PR' && process.env.APPLY_TO_PULL_REQUESTS !== 'true'){ - console.log(`Skipping Issue ${issueNumber} is a PR and PRs are not configured.`); - core.setOutput('skip', 'true'); - return; - } - - // Skip if commenter is a bot - if (context.actor.endsWith('[bot]')) { - console.log(`Skipping bot user: ${context.actor}`); - core.setOutput('skip', 'true'); - return; - } - - let hasWriteAccess = false; - const permissionMode = process.env.PERMISSION_MODE; - - console.log(`Processing ${issueType} #${issueNumber}, commenter: ${context.actor}, mode: ${permissionMode}`); - - if (permissionMode === 'users') { - const teamUsernames = process.env.TEAM_USERNAMES; - const userList = teamUsernames ? teamUsernames.split(',').map(u => u.trim()) : []; - hasWriteAccess = !userList.includes(context.actor); - console.log(`Username list mode - User ${context.actor} classified as: ${hasWriteAccess ? 'customer' : 'team member'}`); - } else { - const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: context.actor - }); - hasWriteAccess = ['admin', 'write'].includes(permission.permission); - console.log(`Write-access mode - User ${context.actor} permission: ${permission.permission}, classified as: ${hasWriteAccess ? 'customer' : 'team member'}`); - } - - const labelToAdd = hasWriteAccess ? process.env.LABEL_AWAITING_CUSTOMER : process.env.LABEL_AWAITING_TEAM; - - console.log(`${issueType} #${issueNumber}: Will apply label "${labelToAdd}"`); - - core.setOutput('label-to-add', labelToAdd); - core.setOutput('skip', 'false'); - - - name: Update labels - if: steps.check-permissions.outputs.skip == 'false' - uses: actions/github-script@v8 - with: - script: | - const issueNumber = context.issue.number; - const labelToAdd = '${{ steps.check-permissions.outputs.label-to-add }}'; - const issueType = github.event.issue.pull_request ? 'PR' : 'Issue'; - - // Get current labels - const { data: issue } = await github.rest.issues.get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber - }); - - // Filter out conflicting labels and add new one - const conflictingLabels = [process.env.LABEL_AWAITING_TEAM, process.env.LABEL_AWAITING_CUSTOMER]; - const newLabels = issue.labels - .map(label => label.name) - .filter(name => !conflictingLabels.includes(name)) - .concat([labelToAdd]); - - // Replace all labels atomically - await github.rest.issues.setLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: newLabels - }); - - console.log(`${issueType} #${issueNumber}: Successfully applied label "${labelToAdd}"`);