diff --git a/.github/actions/triage-issues/action.yml b/.github/actions/triage-issues/action.yml new file mode 100644 index 00000000..dbd49e23 --- /dev/null +++ b/.github/actions/triage-issues/action.yml @@ -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}).`); diff --git a/.github/monthly-instructions.md b/.github/monthly-instructions.md new file mode 100644 index 00000000..4f92939c --- /dev/null +++ b/.github/monthly-instructions.md @@ -0,0 +1,54 @@ +# Below is a consolidated, step-by-step guide and the complete, updated code to implement an automated process that: + +- Ensures a monthly “GitHub Triage: YYYY-MM” issue exists and closes last month’s issue if still open +- Posts weekly comments to the current month’s triage issue, listing and categorizing issues opened in the last 7 days, with each issue reference on its own line +- Provides a reusable composite action for triage issue creation/closing +- Offers a workflow that uses the composite action +- Follow the steps and use the files exactly as provided. + +# Step-by-step implementation guide + +1. Choose your branch and prepare directories +- Decide your branch (example: update-monthly-review). +- Ensure you have the following directories: + - .github/workflows + - .github/actions/triage-issue + +2. Add the monthly triage workflow (creates current month’s triage issue and closes previous) +- This workflow runs monthly. It ensures an issue titled “GitHub Triage: YYYY-MM” exists and closes last month’s triage issue if it is still open. + +3. Add the weekly triage comment workflow +- This runs weekly and posts a comment to the current month’s triage issue with: + - Counts by category + - A list of each new issue in the last 7 days with one line per issue (e.g., “- #223”) + - A note that Mona (Copilot) reviewed +- It uses JavaScript via actions/github-script and the GitHub Search API (created:>=YYYY-MM-DD). + +4. Add the reusable composite action +- Encapsulates logic to: + - Optionally close last month’s triage issue + -Create the current month’s triage issue +- This makes future reuse and refactoring easier. + +5. Add a workflow that uses the composite action +- Example monthly workflow that calls the composite action instead of embedding the logic directly. +- Useful for decoupling and reusability. + +6. Commit and push +- Add all files. +- Commit with a message, such as: add monthly triage + weekly review workflows +- Push to your branch. + +7. Validate and optionally trigger +- Go to the GitHub repository’s Actions tab. +- Confirm workflows are present. +- Manually trigger them if desired via workflow_dispatch. +- Confirm labels and issue creation/comments work. + +8. Future enhancements +- Pagination beyond 1000 items if needed. +- Slack/Teams notifications. +- GraphQL query optimization for exact-title lookups. +- NLP-based categorization improvements. +- JSON artifact uploads for reports and dashboards. +- Templating: externalize comment templates into env vars or repository files. diff --git a/.github/workflows/auto-triage-issue.yml b/.github/workflows/auto-triage-issue.yml new file mode 100644 index 00000000..049c60f9 --- /dev/null +++ b/.github/workflows/auto-triage-issue.yml @@ -0,0 +1,97 @@ +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: Compute date context + id: dates + run: | + YEAR_MONTH=$(date -u +'%Y-%m') + PREV_YEAR_MONTH=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +'%Y-%m') + echo "year_month=$YEAR_MONTH" >> $GITHUB_OUTPUT + echo "prev_year_month=$PREV_YEAR_MONTH" >> $GITHUB_OUTPUT + + - name: Create current triage & close previous + uses: actions/github-script@v7 + with: + script: | + const current = core.getInput('current') || '${{ steps.dates.outputs.year_month }}'; + const previous = core.getInput('previous') || '${{ steps.dates.outputs.prev_year_month }}'; + const currentTitle = `GitHub Triage: ${current}`; + const previousTitle = `GitHub Triage: ${previous}`; + + async function findIssueByExactTitle(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; + } + + // Close previous month issue if open + const prevIssue = await findIssueByExactTitle(previousTitle); + 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.notice(`Closed previous triage issue #${prevIssue.number} (${previousTitle}).`); + } else { + core.info(`No open previous triage issue to close (title: ${previousTitle}).`); + } + + // Ensure current month issue + const currIssue = await findIssueByExactTitle(currentTitle); + if (currIssue) { + core.info(`Current triage issue already exists: #${currIssue.number}`); + return; + } + + const body = ` +### Monthly GitHub Triage – ${current} + +Automatically generated tracking issue for ${current}. + +#### 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 github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: currentTitle, + body, + labels: ['triage'] + }); + core.notice(`Created new triage issue #${created.data.number} (${currentTitle}).`); diff --git a/.github/workflows/use-composite-triage.yml b/.github/workflows/use-composite-triage.yml new file mode 100644 index 00000000..5a76cf82 --- /dev/null +++ b/.github/workflows/use-composite-triage.yml @@ -0,0 +1,29 @@ +name: Monthly Triage via Composite Action + +on: + schedule: + - cron: '2 7 1 * *' # 07:02 UTC on 1st day of month + workflow_dispatch: + +permissions: + issues: write + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - name: Run composite triage action + uses: ./.github/actions/triage-issue + with: + close-previous: "true" + label: "triage" + body-template: | + ### Monthly GitHub Triage – {{MONTH}} + Auto-created via composite action. + + **Goals** + - Classify new issues + - Close stale placeholders + - Prepare weekly summary comments + + :octocat: :copilot: Automation initialized. diff --git a/.github/workflows/weekly-content-review.yml b/.github/workflows/weekly-content-review.yml new file mode 100644 index 00000000..bff713a6 --- /dev/null +++ b/.github/workflows/weekly-content-review.yml @@ -0,0 +1,136 @@ +name: Weekly Triage Issue Comment (Last 7 Days New Issues) + +on: + schedule: + - cron: '27 13 * * 1' # 13:27 UTC every Monday (8:27 AM EST in November) + workflow_dispatch: + +permissions: + issues: write + contents: read + +jobs: + weekly-triage-update: + runs-on: ubuntu-latest + steps: + - name: Determine date context + id: dates + run: | + YEAR_MONTH=$(date -u +'%Y-%m') + START_DATE=$(date -u -d '7 days ago' +'%Y-%m-%d') + echo "year_month=$YEAR_MONTH" >> $GITHUB_OUTPUT + echo "start_date=$START_DATE" >> $GITHUB_OUTPUT + + - name: Locate current triage issue + id: find-issue + uses: actions/github-script@v7 + with: + script: | + const ym = '${{ steps.dates.outputs.year_month }}'; + const title = `GitHub Triage: ${ym}`; + + 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: 'open', + 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; + } + + const issue = await findIssue(title); + if (!issue) { + core.setFailed(`Current triage issue "${title}" not found. Run monthly triage workflow first.`); + return; + } + core.setOutput('issue_number', issue.number); + core.setOutput('issue_title', issue.title); + + - name: Collect new issues (last 7 days) + id: search + uses: actions/github-script@v7 + with: + script: | + const startDate = '${{ steps.dates.outputs.start_date }}'; + const q = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open created:>=${startDate}`; + const perPage = 100; + let page = 1; + let allItems = []; + + while (true) { + const resp = await github.rest.search.issuesAndPullRequests({ + q, + per_page: perPage, + page + }); + const items = resp.data.items || []; + allItems = allItems.concat(items); + if (items.length < perPage || allItems.length >= 1000) break; + page++; + } + + core.setOutput('raw_count', allItems.length); + core.setOutput('items_json', JSON.stringify(allItems)); + + - name: Build and post comment + uses: actions/github-script@v7 + with: + script: | + const issueNumber = Number('${{ steps.find-issue.outputs.issue_number }}'); + const startDate = '${{ steps.dates.outputs.start_date }}'; + const ym = '${{ steps.dates.outputs.year_month }}'; + const items = JSON.parse('${{ steps.search.outputs.items_json }}'); + const daysBack = 7; + + 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'; + return 'other'; + } + + const buckets = { grammar: [], deprecated: [], suggested: [], other: [], template: [] }; + for (const i of items) buckets[categorize(i.title)].push(i); + + const rep = arr => arr.length ? `#${arr[0].number}` : ''; + const todayStr = new Date().toISOString().split('T')[0]; + const allIssueLines = items.map(i => `- #${i.number}`).join('\n'); + + let md = `**${ym} Weekly Triage Update (Issues opened since ${startDate})**\n`; + md += `- Checked as of: ${todayStr} (UTC)\n\n`; + md += `**Counts (last ${daysBack} 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: ${items.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 += `\n`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: md + }); + core.notice(`Posted weekly triage update to #${issueNumber}.`);