From b7440e8d7f4d365886b40749ca08f17ca94b8682 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:00:58 -0600 Subject: [PATCH] Add automated workflow for tagging aging bugs (#39284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automates bug triage by tagging issues based on age: `~old bug` for bugs ≥180 days, `~aging bug` for bugs ≥90 days. Relates to #39155. ## Changes **New workflow: `.github/workflows/tag-aging-bugs.yml`** - Runs daily at 8:06 UTC via cron, supports manual dispatch - Dry run mode (default: false) logs actions without modifying labels - Two-pass processing: 1. Bugs ≥180 days: adds `~old bug`, removes `~aging bug` if present 2. Bugs ≥90 days without either label: adds `~aging bug` - Uses github-script with pagination for scalability - Follows repo patterns (harden-runner, proper permissions) # Checklist for submitter - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] QA'd all new/changed functionality manually
Original prompt > Add a GitHub Actions workflow that runs daily at 8:06am UTC, and can be manually dispatched. In that workflow, retrieve all issues labelled `bug` created >= 180 days ago that don't include the `~old bug` tag, then for each bug add the `~old bug` tag and remove `~aging bug` if it is applied. Then retrieve all issues labelled `bug` created >= 90 days ago that has neither `~aging bug` nor `~old bug` tags, and add the `~aging bug` tag. Include a dry run workflow parameter, default off, that logs rather than setting the label.
--- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: iansltx <472804+iansltx@users.noreply.github.com> --- .github/workflows/tag-aging-bugs.yml | 189 +++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 .github/workflows/tag-aging-bugs.yml diff --git a/.github/workflows/tag-aging-bugs.yml b/.github/workflows/tag-aging-bugs.yml new file mode 100644 index 0000000000..75baa1b0b1 --- /dev/null +++ b/.github/workflows/tag-aging-bugs.yml @@ -0,0 +1,189 @@ +name: Tag aging bugs + +# This action will tag bugs based on their age: +# - Bugs >= 180 days old get tagged with ~old bug (and ~aging bug is removed) +# - Bugs >= 90 days old (but < 180 days) get tagged with ~aging bug + +on: + schedule: + # Daily at 8:06am UTC + - cron: "6 8 * * *" + workflow_dispatch: # Manual + inputs: + dry_run: + description: 'Dry run mode (log only, do not modify labels)' + required: false + type: boolean + default: false + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}} + cancel-in-progress: true + +defaults: + run: + # fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + +permissions: + contents: read + +jobs: + tag-bugs: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 + with: + egress-policy: audit + + - name: Tag aging bugs + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const dryRun = ${{ github.event.inputs.dry_run || false }}; + console.log(`Dry run mode: ${dryRun}`); + + // Calculate date thresholds + const now = new Date(); + const oldBugDate = new Date(now); + oldBugDate.setDate(oldBugDate.getDate() - 180); + const agingBugDate = new Date(now); + agingBugDate.setDate(agingBugDate.getDate() - 90); + + console.log(`Old bug threshold: ${oldBugDate.toISOString()}`); + console.log(`Aging bug threshold: ${agingBugDate.toISOString()}`); + + // Process old bugs (>= 180 days) + console.log('\n=== Processing old bugs (>= 180 days) ==='); + let page = 1; + let oldBugsProcessed = 0; + + while (true) { + const { data: oldBugs } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'bug', + state: 'open', + per_page: 100, + page: page, + sort: 'created', + direction: 'asc' + }); + + if (oldBugs.length === 0) break; + + for (const issue of oldBugs) { + const createdDate = new Date(issue.created_at); + + // Stop if we've passed the old bug threshold + if (createdDate > oldBugDate) { + page = Infinity; // Signal to stop pagination + break; + } + + const labels = issue.labels.map(label => label.name); + const hasOldBugLabel = labels.includes('~old bug'); + + if (!hasOldBugLabel) { + oldBugsProcessed++; + console.log(`Issue #${issue.number}: Created ${createdDate.toISOString()}`); + + if (dryRun) { + console.log(` [DRY RUN] Would add ~old bug label`); + if (labels.includes('~aging bug')) { + console.log(` [DRY RUN] Would remove ~aging bug label`); + } + } else { + // Add ~old bug label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['~old bug'] + }); + console.log(` Added ~old bug label`); + + // Remove ~aging bug if present + if (labels.includes('~aging bug')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: '~aging bug' + }); + console.log(` Removed ~aging bug label`); + } + } + } + } + + if (page === Infinity) break; + page++; + } + + console.log(`\nProcessed ${oldBugsProcessed} old bugs`); + + // Process aging bugs (>= 90 days but < 180 days) + console.log('\n=== Processing aging bugs (>= 90 days) ==='); + page = 1; + let agingBugsProcessed = 0; + + while (true) { + const { data: agingBugs } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'bug', + state: 'open', + per_page: 100, + page: page, + sort: 'created', + direction: 'asc' + }); + + if (agingBugs.length === 0) break; + + for (const issue of agingBugs) { + const createdDate = new Date(issue.created_at); + + // Skip if newer than aging threshold + if (createdDate > agingBugDate) { + page = Infinity; // Signal to stop pagination + break; + } + + const labels = issue.labels.map(label => label.name); + const hasAgingBugLabel = labels.includes('~aging bug'); + const hasOldBugLabel = labels.includes('~old bug'); + + // Only tag if it doesn't have either label + // (hasOldBugLabel check handles issues that became old since last run) + if (!hasAgingBugLabel && !hasOldBugLabel) { + agingBugsProcessed++; + console.log(`Issue #${issue.number}: Created ${createdDate.toISOString()}`); + + if (dryRun) { + console.log(` [DRY RUN] Would add ~aging bug label`); + } else { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['~aging bug'] + }); + console.log(` Added ~aging bug label`); + } + } + } + + if (page === Infinity) break; + page++; + } + + console.log(`\nProcessed ${agingBugsProcessed} aging bugs`); + console.log(`\n=== Summary ===`); + console.log(`Total old bugs tagged: ${oldBugsProcessed}`); + console.log(`Total aging bugs tagged: ${agingBugsProcessed}`);