Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/actions/triage-issues/action.yml
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}).`);
15 changes: 15 additions & 0 deletions .github/github_actions_triage-issue_action_Version6.yml
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 +1 to +15
Copy link

Copilot AI Dec 11, 2025

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.yml but 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.yml path). The versioning suffix also suggests this may be a backup or draft that should not be included in the final PR.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +15
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The composite action is missing environment variable passthrough to the script execution. The script at .github/scripts/ensureMonthlyTriage.js requires GITHUB_TOKEN, REPO_OWNER, and REPO_NAME environment variables (as seen in the script's validation), but the action doesn't set these in the step that runs the script. The calling workflow may set these as env vars, but the composite action should explicitly pass them through or accept them as inputs to ensure the script functions correctly.

Copilot uses AI. Check for mistakes.
99 changes: 99 additions & 0 deletions .github/github_scripts_ensureMonthlyTriage.js
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
Copy link

Copilot AI Dec 11, 2025

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 or implement exponential backoff. For workflows that run monthly in repositories with many issues, consider adding error handling for rate limit exceptions to prevent workflow failures.

Copilot uses AI. Check for mistakes.
}
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
Copy link

Copilot AI Dec 11, 2025

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_ensureMonthlyTriage.js but all workflows and the composite action reference it as .github/scripts/ensureMonthlyTriage.js. The file should either be renamed to ensureMonthlyTriage.js and placed in a .github/scripts/ directory, or all references should be updated to match this unconventional naming pattern.

Copilot uses AI. Check for mistakes.
123 changes: 123 additions & 0 deletions .github/github_scripts_weeklyTriageComment.js
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++) {
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pagination limit in the JavaScript script is too restrictive. The workflow version uses page < 50 for pagination (lines 34 and 77 of weekly-content-review.yml), but this script limits pagination to page < 10. This means the script will only fetch up to 900 issues (10 pages × 100 per page minus the last incomplete page), potentially missing issues if there are more than ~900 matching results. Consider increasing this to match the workflow's limit of 50 pages or adding a configurable limit.

Suggested change
for (let page = 1; page < 10; page++) {
for (let page = 1; page < 50; page++) {

Copilot uses AI. Check for mistakes.
const resp = await octokit.request("GET /search/issues", {
q,
per_page: 100,
page,
});
const items = resp.data.items || [];
allItems = allItems.concat(items);
Copy link

Copilot AI Dec 11, 2025

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.

Suggested change
allItems = allItems.concat(items);
allItems = allItems.concat(items.filter(item => !item.pull_request));

Copilot uses AI. Check for mistakes.
if (items.length < 100) break;
Comment on lines +57 to +65
Copy link

Copilot AI Dec 11, 2025

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 uses AI. Check for mistakes.
}

// Categorization
const reTemplate = /\[REPLACE_WITH_MODULE_TITLE\]|N\/?A/i;
const reGrammar = /\b(grammar|spelling|typo|misspell(ed)?|proofread)\b/i;
const reDeprecated = /\b(deprecated|outdated|codeql|dependabot|projects?|security)\b/i;
const reSuggested =
/\b(update|improvement|improve|copilot|prompt|exercise|action|module|enterprise)\b/i;
const reOther =
/\b(broken|support|help|unable|issue|not issued|confused|experience|certificate)\b/i;

function categorize(title) {
if (reTemplate.test(title)) return "template";
if (reGrammar.test(title)) return "grammar";
if (reDeprecated.test(title)) return "deprecated";
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";
Comment on lines +81 to +83
Copy link

Copilot AI Dec 11, 2025

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.

Suggested change
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 uses AI. Check for mistakes.
return "other";
}

const buckets = { grammar: [], deprecated: [], suggested: [], other: [], template: [] };
for (const i of allItems) buckets[categorize(i.title)].push(i);

const rep = (arr) => (arr.length ? `#${arr[0].number}` : "");
const todayStr = new Date().toISOString().slice(0, 10);
const allIssueLines = allItems.map((i) => `- #${i.number}`).join("\n");

let md = `**${yearMonth} Weekly Triage Update (Issues opened since ${startDate})**\n`;
md += `- Checked as of: ${todayStr} (UTC)\n\n`;
md += `**Counts (last 7 days):**\n`;
md += `- Grammar/Spelling: ${buckets.grammar.length} ${rep(buckets.grammar)}\n`;
md += `- Deprecated/Outdated: ${buckets.deprecated.length} ${rep(buckets.deprecated)}\n`;
md += `- Suggested Content Updates: ${buckets.suggested.length} ${rep(buckets.suggested)}\n`;
md += `- Other: ${buckets.other.length} ${rep(buckets.other)}\n`;
md += `- Template-Incomplete: ${buckets.template.length} ${rep(buckets.template)}\n\n`;
md += `_Total new issues: ${allItems.length}_\n\n`;
md += `**Issue References (Last 7 Days):**\n${allIssueLines}\n\n`;
md += `:octocat: :copilot: Mona (Copilot) has reviewed these new issues.\n`;
md += `<!-- Search query: repo:${repoOwner}/${repoName} is:issue is:open created:>=${startDate} -->\n`;

await octokit.request(
"POST /repos/{owner}/{repo}/issues/{issue_number}/comments",
{
owner: repoOwner,
repo: repoName,
issue_number: triageIssue.number,
body: md,
}
);
Comment on lines +107 to +115
Copy link

Copilot AI Dec 11, 2025

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 uses AI. Check for mistakes.

console.log(`Posted weekly triage update to #${triageIssue.number}`);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Comment on lines +1 to +123
Copy link

Copilot AI Dec 11, 2025

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.

Copilot uses AI. Check for mistakes.
32 changes: 32 additions & 0 deletions .github/github_workflows_auto-triage-issue_Version7.yml
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
Copy link

Copilot AI Dec 11, 2025

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_workflows_auto-triage-issue_Version7.yml but should be placed in .github/workflows/ directory with a standard name like auto-triage-issue.yml. The underscores replacing directory separators and the version suffix suggest this is a draft or backup file that conflicts with the actual workflow at .github/workflows/auto-triage-issue.yml.

Suggested change
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

Copilot uses AI. Check for mistakes.
Loading