generated from amazon-archives/__template_Apache-2.0
    
        
        - 
                Notifications
    You must be signed in to change notification settings 
- Fork 453
feat: add automated issue auto-close workflows with dry-run testing #832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
          
     Merged
      
      
    
  
     Merged
                    Changes from 17 commits
      Commits
    
    
            Show all changes
          
          
            19 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      acf4961
              
                feat: add GitHub workflow for auto-closing stale issues with dry-run …
              
              
                yonib05 23f7457
              
                fix: replace deprecated GitHub Search API with Issues API
              
              
                yonib05 cc6989d
              
                fix: resolve JavaScript syntax error in GitHub workflow
              
              
                yonib05 7a99388
              
                feat: remove label immediately on unauthorized comments
              
              
                yonib05 99f4c93
              
                feat: add optional replacement label when removing auto-close label
              
              
                yonib05 b478b08
              
                chore: Update Name of GitHub Action
              
              
                yonib05 04504ad
              
                chore: Testing closure by setting to one day
              
              
                yonib05 6a70cdc
              
                chore: Testing closure by setting to zero days
              
              
                yonib05 f3c2246
              
                fix: Allow for zero days
              
              
                yonib05 860605f
              
                chore: Test removal and replacement label
              
              
                yonib05 b87e171
              
                fix: Fix to allow manual dispatch inputs take presence over set env v…
              
              
                yonib05 fd7ef16
              
                fix: Fix to allow manual dispatch inputs take presence over set env v…
              
              
                yonib05 e81388c
              
                feat: Allow a closure message to be passed as an env variable
              
              
                yonib05 5b543e2
              
                chore: Prep for PR
              
              
                yonib05 b9281ee
              
                feature: Add autoclose 7 days
              
              
                yonib05 13def8d
              
                feat: Consolidate auto-close workflows into single matrix-based action
              
              
                yonib05 6a99002
              
                fix: Updated Comment syntax for yaml
              
              
                yonib05 75a84c8
              
                Create test.yml
              
              
                yonib05 f49434b
              
                Delete .github/workflows/test.yml
              
              
                yonib05 File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
There are no files selected for viewing
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -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') { | ||
|         
                  yonib05 marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| 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) { | ||
|         
                  Unshure marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issue.number, | ||
| labels: [config.replacementLabel] | ||
|         
                  Unshure marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| }); | ||
| 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}`); | ||
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
Uh oh!
There was an error while loading. Please reload this page.