-
Notifications
You must be signed in to change notification settings - Fork 8
JS-driven monthly triage and weekly issue review automation #229
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
base: main
Are you sure you want to change the base?
Changes from all commits
dc6c896
cc5a4a9
b2dcfd2
e6e72c5
707ccaa
800c46d
9d995fe
2c08474
8cfc53b
b7cde8a
b0eba88
e764aad
510776e
3d5727c
32f8585
e64de1e
81b290a
f6a359c
f946beb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| name: "triage-issue" | ||
| description: "Composite action to create monthly triage issue and optionally close previous month" | ||
| inputs: | ||
| close-previous: | ||
| description: "Whether to close last month’s triage issue (true/false)" | ||
| required: false | ||
| default: "true" | ||
| label: | ||
| description: "Label to apply to triage issue" | ||
| required: false | ||
| default: "triage" | ||
| body-template: | ||
| description: "Custom body template (supports {{MONTH}} placeholder)" | ||
| required: false | ||
| default: | | ||
| ### Monthly GitHub Triage – {{MONTH}} | ||
|
|
||
| This automatically generated issue tracks triage activities for {{MONTH}}. | ||
|
|
||
| **Purpose** | ||
| - Collect new issues for classification. | ||
| - Track placeholders. | ||
| - Consolidate duplicates. | ||
|
|
||
| **Automation** | ||
| - Weekly summary comments (companion workflow). | ||
| - Future enhancements: stale detection, cross-links. | ||
|
|
||
| :octocat: :copilot: Created automatically. | ||
| runs: | ||
| using: "composite" | ||
| steps: | ||
| - name: Compute dates | ||
| id: dates | ||
| shell: bash | ||
| run: | | ||
| CURR=$(date -u +'%Y-%m') | ||
| PREV=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +'%Y-%m') | ||
| echo "curr=$CURR" >> $GITHUB_OUTPUT | ||
| echo "prev=$PREV" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Ensure triage issue | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const closePrev = (process.env.INPUT_CLOSE_PREVIOUS || 'true').toLowerCase() === 'true'; | ||
| const label = process.env.INPUT_LABEL || 'triage'; | ||
| const bodyTemplate = process.env.INPUT_BODY_TEMPLATE; | ||
| const curr = '${{ steps.dates.outputs.curr }}'; | ||
| const prev = '${{ steps.dates.outputs.prev }}'; | ||
| const currTitle = `GitHub Triage: ${curr}`; | ||
| const prevTitle = `GitHub Triage: ${prev}`; | ||
|
|
||
| async function findIssue(title) { | ||
| const perPage = 100; | ||
| for (let page = 1; page < 50; page++) { | ||
| const { data } = await github.rest.issues.listForRepo({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'all', | ||
| per_page: perPage, | ||
| page | ||
| }); | ||
| if (!data.length) break; | ||
| const hit = data.find(i => i.title === title); | ||
| if (hit) return hit; | ||
| if (data.length < perPage) break; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| if (closePrev) { | ||
| const prevIssue = await findIssue(prevTitle); | ||
| if (prevIssue && prevIssue.state === 'open') { | ||
| await github.rest.issues.update({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: prevIssue.number, | ||
| state: 'closed' | ||
| }); | ||
| core.info(`Closed previous triage issue #${prevIssue.number} (${prevTitle})`); | ||
| } else { | ||
| core.info(`Previous triage issue not open or not found (${prevTitle}).`); | ||
| } | ||
| } | ||
|
|
||
| const currIssue = await findIssue(currTitle); | ||
| if (currIssue) { | ||
| core.info(`Current triage issue already exists: #${currIssue.number}`); | ||
| return; | ||
| } | ||
|
|
||
| const body = bodyTemplate.replace(/{{MONTH}}/g, curr); | ||
| const created = await github.rest.issues.create({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| title: currTitle, | ||
| body, | ||
| labels: [label] | ||
| }); | ||
| core.notice(`Created triage issue #${created.data.number} (${currTitle}).`); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| name: "triage-issue" | ||
| description: "Composite action to run monthly triage JS script" | ||
| runs: | ||
| using: "composite" | ||
| steps: | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '18' | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| shell: bash | ||
| - name: Run monthly triage script | ||
| run: node .github/scripts/ensureMonthlyTriage.js | ||
| shell: bash | ||
|
Comment on lines
+13
to
+15
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| // ensureMonthlyTriage.js | ||
| const { Octokit } = require("@octokit/core"); | ||
|
|
||
| const githubToken = process.env.GITHUB_TOKEN; | ||
| const repoOwner = process.env.REPO_OWNER; | ||
| const repoName = process.env.REPO_NAME; | ||
|
|
||
| if (!githubToken || !repoOwner || !repoName) { | ||
| console.error("Missing required env: GITHUB_TOKEN, REPO_OWNER, REPO_NAME"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const octokit = new Octokit({ auth: githubToken }); | ||
|
|
||
| function getYearMonth(offset = 0) { | ||
| const d = new Date(); | ||
| d.setMonth(d.getMonth() + offset); | ||
| return d.toISOString().slice(0, 7); // YYYY-MM | ||
| } | ||
|
|
||
| async function findIssueByExactTitle(title) { | ||
| for (let page = 1; page < 50; page++) { | ||
| const resp = await octokit.request("GET /repos/{owner}/{repo}/issues", { | ||
| owner: repoOwner, | ||
| repo: repoName, | ||
| state: "all", | ||
| per_page: 100, | ||
| page, | ||
| }); | ||
| if (!resp.data.length) break; | ||
| const hit = resp.data.find((i) => i.title === title); | ||
| if (hit) return hit; | ||
| if (resp.data.length < 100) break; | ||
|
Comment on lines
+22
to
+33
|
||
| } | ||
| return null; | ||
| } | ||
|
|
||
| async function main() { | ||
| const currentYM = getYearMonth(0); | ||
| const previousYM = getYearMonth(-1); | ||
| const currentTitle = `GitHub Triage: ${currentYM}`; | ||
| const previousTitle = `GitHub Triage: ${previousYM}`; | ||
|
|
||
| // Close previous month issue if open | ||
| const prevIssue = await findIssueByExactTitle(previousTitle); | ||
| if (prevIssue && prevIssue.state === "open") { | ||
| await octokit.request( | ||
| "PATCH /repos/{owner}/{repo}/issues/{issue_number}", | ||
| { | ||
| owner: repoOwner, | ||
| repo: repoName, | ||
| issue_number: prevIssue.number, | ||
| state: "closed", | ||
| } | ||
| ); | ||
| console.log(`Closed previous triage issue #${prevIssue.number}`); | ||
| } else { | ||
| console.log(`No open previous triage issue (title: ${previousTitle})`); | ||
| } | ||
|
|
||
| // Ensure current month issue exists | ||
| const currIssue = await findIssueByExactTitle(currentTitle); | ||
| if (currIssue) { | ||
| console.log(`Current triage issue already exists: #${currIssue.number}`); | ||
| return; | ||
| } | ||
|
|
||
| const body = ` | ||
| ### Monthly GitHub Triage – ${currentYM} | ||
|
|
||
| Automatically generated tracking issue for ${currentYM}. | ||
|
|
||
| #### Purpose | ||
| - Collect newly opened issues for classification. | ||
| - Track placeholders (titles containing \`[REPLACE_WITH_MODULE_TITLE]\`). | ||
| - Identify consolidation / closure candidates. | ||
|
|
||
| #### Actions | ||
| - Apply governance labels. | ||
| - Escalate support or experience issues. | ||
| - Prepare weekly summaries (see companion workflow). | ||
|
|
||
| :octocat: :copilot: Created automatically. | ||
| `.trim(); | ||
|
|
||
| const created = await octokit.request("POST /repos/{owner}/{repo}/issues", { | ||
| owner: repoOwner, | ||
| repo: repoName, | ||
| title: currentTitle, | ||
| body, | ||
| labels: ["triage"], | ||
| }); | ||
| console.log(`Created new triage issue #${created.data.number}`); | ||
| } | ||
|
|
||
| main().catch((err) => { | ||
| console.error(err); | ||
| process.exit(1); | ||
| }); | ||
|
Comment on lines
+1
to
+99
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,123 @@ | ||||||||||||||
| // weeklyTriageComment.js | ||||||||||||||
| const { Octokit } = require("@octokit/core"); | ||||||||||||||
|
|
||||||||||||||
| const githubToken = process.env.GITHUB_TOKEN; | ||||||||||||||
| const repoOwner = process.env.REPO_OWNER; | ||||||||||||||
| const repoName = process.env.REPO_NAME; | ||||||||||||||
|
|
||||||||||||||
| if (!githubToken || !repoOwner || !repoName) { | ||||||||||||||
| console.error("Missing required env: GITHUB_TOKEN, REPO_OWNER, REPO_NAME"); | ||||||||||||||
| process.exit(1); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const octokit = new Octokit({ auth: githubToken }); | ||||||||||||||
|
|
||||||||||||||
| function getYearMonth(offset = 0) { | ||||||||||||||
| const d = new Date(); | ||||||||||||||
| d.setMonth(d.getMonth() + offset); | ||||||||||||||
| return d.toISOString().slice(0, 7); // YYYY-MM | ||||||||||||||
| } | ||||||||||||||
| function getStartDate(daysBack = 7) { | ||||||||||||||
| const d = new Date(); | ||||||||||||||
| d.setDate(d.getDate() - daysBack); | ||||||||||||||
| return d.toISOString().slice(0, 10); // YYYY-MM-DD | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| async function findIssue(title) { | ||||||||||||||
| for (let page = 1; page < 50; page++) { | ||||||||||||||
| const resp = await octokit.request("GET /repos/{owner}/{repo}/issues", { | ||||||||||||||
| owner: repoOwner, | ||||||||||||||
| repo: repoName, | ||||||||||||||
| state: "open", | ||||||||||||||
| per_page: 100, | ||||||||||||||
| page, | ||||||||||||||
| }); | ||||||||||||||
| if (!resp.data.length) break; | ||||||||||||||
| const hit = resp.data.find((i) => i.title === title); | ||||||||||||||
| if (hit) return hit; | ||||||||||||||
| if (resp.data.length < 100) break; | ||||||||||||||
| } | ||||||||||||||
| return null; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| async function main() { | ||||||||||||||
| const yearMonth = getYearMonth(); | ||||||||||||||
| const triageTitle = `GitHub Triage: ${yearMonth}`; | ||||||||||||||
| const triageIssue = await findIssue(triageTitle); | ||||||||||||||
| if (!triageIssue) { | ||||||||||||||
| throw new Error( | ||||||||||||||
| `Current triage issue "${triageTitle}" not found. Run monthly triage workflow first.` | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const startDate = getStartDate(7); | ||||||||||||||
| const q = `repo:${repoOwner}/${repoName} is:issue is:open created:>=${startDate}`; | ||||||||||||||
|
|
||||||||||||||
| let allItems = []; | ||||||||||||||
| for (let page = 1; page < 10; page++) { | ||||||||||||||
|
||||||||||||||
| for (let page = 1; page < 10; page++) { | |
| for (let page = 1; page < 50; page++) { |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GitHub Search API can return both issues and pull requests. While the query includes is:issue which should filter to only issues, it's safer to add an explicit filter to exclude pull requests. Consider filtering the items: allItems = allItems.concat(items.filter(item => !item.pull_request)) to ensure pull requests are never included in the triage comment, especially since the workflow is specifically for issue triage.
| allItems = allItems.concat(items); | |
| allItems = allItems.concat(items.filter(item => !item.pull_request)); |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pagination loop doesn't implement any rate limit handling or backoff strategy. While the GitHub API includes rate limit headers, this code doesn't check for rate limit errors (status 403 with specific error messages) or implement exponential backoff. For workflows that run frequently or repositories with many issues, consider adding error handling for rate limit exceptions (e.g., checking resp.status and X-RateLimit-Remaining header) to prevent workflow failures.
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The categorization logic has redundant template checks. In line 81, the condition checks !reTemplate.test(title) before testing reSuggested, but this is unnecessary since line 78 already returns 'template' if the template regex matches. Similarly, line 83 checks !reTemplate.test(title) again, which is also redundant. These additional checks can be removed to simplify the logic.
| if (!reTemplate.test(title) && reSuggested.test(title)) return "suggested"; | |
| if (reOther.test(title)) return "other"; | |
| if (/update|request/i.test(title) && !reTemplate.test(title)) return "suggested"; | |
| if (reSuggested.test(title)) return "suggested"; | |
| if (reOther.test(title)) return "other"; | |
| if (/update|request/i.test(title)) return "suggested"; |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script posts a comment even when there are no new issues in the last 7 days. When allItems.length is 0, the comment will show all category counts as 0 and display "Total new issues: 0" with an empty "Issue References" section. Consider adding a check to skip posting the comment (or post a different message) when no new issues are found, to avoid cluttering the triage issue with empty weekly updates.
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JavaScript filename uses an incorrect naming convention. The file is named github_scripts_weeklyTriageComment.js but the workflow references it as .github/scripts/weeklyTriageComment.js. The file should either be renamed to weeklyTriageComment.js and placed in a .github/scripts/ directory, or the workflow references should be updated to match this unconventional naming pattern.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: Auto Create Monthly GitHub Triage Issue (Create + Close Previous) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| schedule: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - cron: '5 7 1 * *' # 07:05 UTC on the 1st day of each month | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| issues: write | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ensure-triage-issue: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Checkout repo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uses: actions/checkout@v4 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Setup Node.js | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uses: actions/setup-node@v4 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| node-version: '18' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Install dependencies | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run: npm ci | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Run JS to create/close triage issue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| REPO_OWNER: githubpartners | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| REPO_NAME: microsoft-learn | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run: node .github/scripts/ensureMonthlyTriage.js | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+32
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: Auto Create Monthly GitHub Triage Issue (Create + Close Previous) | |
| on: | |
| schedule: | |
| - cron: '5 7 1 * *' # 07:05 UTC on the 1st day of each month | |
| workflow_dispatch: | |
| permissions: | |
| issues: write | |
| contents: read | |
| jobs: | |
| ensure-triage-issue: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '18' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run JS to create/close triage issue | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO_OWNER: githubpartners | |
| REPO_NAME: microsoft-learn | |
| run: node .github/scripts/ensureMonthlyTriage.js |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file appears to be a versioned duplicate with an unconventional filename. The file is named
github_actions_triage-issue_action_Version6.ymlbut uses underscores instead of directory separators. If this is meant to be the action definition, it should be placed at.github/actions/triage-issue/action.yml(or match the existing.github/actions/triage-issues/action.ymlpath). The versioning suffix also suggests this may be a backup or draft that should not be included in the final PR.