🔒 Gemini Scheduled Stale Issue Closer #12
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
| name: '🔒 Gemini Scheduled Stale Issue Closer' | |
| on: | |
| schedule: | |
| - cron: '0 0 * * 0' # Every Sunday at midnight UTC | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: 'Run in dry-run mode (no changes applied)' | |
| required: false | |
| default: false | |
| type: 'boolean' | |
| concurrency: | |
| group: '${{ github.workflow }}' | |
| cancel-in-progress: true | |
| defaults: | |
| run: | |
| shell: 'bash' | |
| jobs: | |
| close-stale-issues: | |
| runs-on: 'ubuntu-latest' | |
| permissions: | |
| issues: 'write' | |
| steps: | |
| - name: 'Generate GitHub App Token' | |
| id: 'generate_token' | |
| uses: 'actions/create-github-app-token@v1' | |
| with: | |
| app-id: '${{ secrets.APP_ID }}' | |
| private-key: '${{ secrets.PRIVATE_KEY }}' | |
| permission-issues: 'write' | |
| - name: 'Process Stale Issues' | |
| uses: 'actions/github-script@v7' | |
| env: | |
| DRY_RUN: '${{ inputs.dry_run }}' | |
| with: | |
| github-token: '${{ steps.generate_token.outputs.token }}' | |
| script: | | |
| const dryRun = process.env.DRY_RUN === 'true'; | |
| if (dryRun) { | |
| core.info('DRY RUN MODE ENABLED: No changes will be applied.'); | |
| } | |
| const batchLabel = 'Stale'; | |
| const threeMonthsAgo = new Date(); | |
| threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); | |
| const tenDaysAgo = new Date(); | |
| tenDaysAgo.setDate(tenDaysAgo.getDate() - 10); | |
| core.info(`Cutoff date for creation: ${threeMonthsAgo.toISOString()}`); | |
| core.info(`Cutoff date for updates: ${tenDaysAgo.toISOString()}`); | |
| const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open created:<${threeMonthsAgo.toISOString()}`; | |
| core.info(`Searching with query: ${query}`); | |
| const itemsToCheck = await github.paginate(github.rest.search.issuesAndPullRequests, { | |
| q: query, | |
| sort: 'created', | |
| order: 'asc', | |
| per_page: 100 | |
| }); | |
| core.info(`Found ${itemsToCheck.length} open issues to check.`); | |
| let processedCount = 0; | |
| for (const issue of itemsToCheck) { | |
| const createdAt = new Date(issue.created_at); | |
| const updatedAt = new Date(issue.updated_at); | |
| const reactionCount = issue.reactions.total_count; | |
| // Basic thresholds | |
| if (reactionCount >= 5) { | |
| continue; | |
| } | |
| // Skip if it has a maintainer label | |
| if (issue.labels.some(label => label.name.toLowerCase().includes('maintainer'))) { | |
| continue; | |
| } | |
| let isStale = updatedAt < tenDaysAgo; | |
| // If apparently active, check if it's only bot activity | |
| if (!isStale) { | |
| try { | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| per_page: 100, | |
| sort: 'created', | |
| direction: 'desc' | |
| }); | |
| const lastHumanComment = comments.data.find(comment => comment.user.type !== 'Bot'); | |
| if (lastHumanComment) { | |
| isStale = new Date(lastHumanComment.created_at) < tenDaysAgo; | |
| } else { | |
| // No human comments. Check if creator is human. | |
| if (issue.user.type !== 'Bot') { | |
| isStale = createdAt < tenDaysAgo; | |
| } else { | |
| isStale = true; // Bot created, only bot comments | |
| } | |
| } | |
| } catch (error) { | |
| core.warning(`Failed to fetch comments for issue #${issue.number}: ${error.message}`); | |
| continue; | |
| } | |
| } | |
| if (isStale) { | |
| processedCount++; | |
| const message = `Closing stale issue #${issue.number}: "${issue.title}" (${issue.html_url})`; | |
| core.info(message); | |
| if (!dryRun) { | |
| // Add label | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: [batchLabel] | |
| }); | |
| // Add comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: 'Hello! As part of our effort to keep our backlog manageable and focus on the most active issues, we are tidying up older reports.\n\nIt looks like this issue hasn\'t been active for a while, so we are closing it for now. However, if you are still experiencing this bug on the latest stable build, please feel free to comment on this issue or create a new one with updated details.\n\nThank you for your contribution!' | |
| }); | |
| // Close issue | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| state: 'closed', | |
| state_reason: 'not_planned' | |
| }); | |
| } | |
| } | |
| } | |
| core.info(`\nTotal issues processed: ${processedCount}`); |