From e99f32fe9ea7e5691378a17d2d57557b72189126 Mon Sep 17 00:00:00 2001 From: Ben Elan Date: Tue, 6 Feb 2024 18:10:46 -0800 Subject: [PATCH 1/5] ci: track issue estimates completed per milestone --- .github/scripts/trackMilestoneEstimates.js | 62 +++++++++++++++++++ .../workflows/track-milestone-estimates.yml | 19 ++++++ 2 files changed, 81 insertions(+) create mode 100644 .github/scripts/trackMilestoneEstimates.js create mode 100644 .github/workflows/track-milestone-estimates.yml diff --git a/.github/scripts/trackMilestoneEstimates.js b/.github/scripts/trackMilestoneEstimates.js new file mode 100644 index 00000000000..2742df50fa8 --- /dev/null +++ b/.github/scripts/trackMilestoneEstimates.js @@ -0,0 +1,62 @@ +const { writeFile } = require("fs/promises"); + +module.exports = async ({ github, context }) => { + const outputData = {}; + const outputPath = resolve(__dirname, "..", "milestone-estimates.json"); + + try { + const milestones = await github.rest.issues.listMilestones({ + owner: repoOwner, + repo: repoName, + state: "closed", + sort: "due_on", + per_page: 1, + direction: "desc", + }); + + if (milestones.data.length === 0) { + console.error("No closed milestones found."); + process.exit(1); + } + + const milestone = milestones.data[0]; + + outputData[milestone.number] = { + due_on: milestone.due_on, + title: milestone.title, + description: milestone.description, + open_issues: milestone.open_issues, + closed_issues: milestone.closed_issues, + issues_with_estimate: 0, + effort_estimate: 0, + }; + + const issues = await github.rest.issues.listForRepo({ + owner: repoOwner, + repo: repoName, + milestone: milestone.number, + state: "closed", + per_page: 100, + }); + + for (const issue of issues.data) { + if ("pull_request" in issue) { + continue; + } + + for (const label of issue.labels) { + if (label.name.match(/estimate/)) { + outputData[milestone.number].issues_with_estimate++; + outputData[milestone.number].effort_estimate += Number.parseInt(label.name.replace(/\D/g, "")); + break; + } + } + } + + await writeFile(outputPath, JSON.stringify(outputData, null, 2)); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } +}; diff --git a/.github/workflows/track-milestone-estimates.yml b/.github/workflows/track-milestone-estimates.yml new file mode 100644 index 00000000000..9704a699eb6 --- /dev/null +++ b/.github/workflows/track-milestone-estimates.yml @@ -0,0 +1,19 @@ +name: Track Milestone Estimates +on: + workflow_dispatch: +jobs: + estimates: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Create estimates data + uses: actions/github-script@v6 + with: + script: | + const action = require('${{ github.workspace }}/.github/scripts/trackMilestoneEstimates.js') + await action({github, context, core}) + - name: Archive estimate artifacts + uses: actions/upload-artifact@v4 + with: + name: milestone-estimates + path: .github/milestone-estimates.json From a15f812bcc6f8c5c179362382adfbb65098291ae Mon Sep 17 00:00:00 2001 From: Ben Elan Date: Mon, 30 Sep 2024 06:08:27 -0700 Subject: [PATCH 2/5] chore: cleanup --- .github/scripts/trackMilestoneEstimates.js | 86 +++++++++++-------- .../workflows/track-milestone-estimates.yml | 12 ++- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/.github/scripts/trackMilestoneEstimates.js b/.github/scripts/trackMilestoneEstimates.js index 2742df50fa8..d4620f563ee 100644 --- a/.github/scripts/trackMilestoneEstimates.js +++ b/.github/scripts/trackMilestoneEstimates.js @@ -1,59 +1,73 @@ +// @ts-check +const { resolve } = require("path"); const { writeFile } = require("fs/promises"); +/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */ module.exports = async ({ github, context }) => { - const outputData = {}; - const outputPath = resolve(__dirname, "..", "milestone-estimates.json"); + const { repo, owner } = context.repo; + + const outputJson = {}; + let outputCsv = "id,title,open_issues,closed_issues,due_on,description,remaining_estimate,completed_estimate"; + + const outputJsonPath = resolve(__dirname, "..", "milestone-estimates.json"); + const outputCsvPath = resolve(__dirname, "..", "milestone-estimates.csv"); try { const milestones = await github.rest.issues.listMilestones({ - owner: repoOwner, - repo: repoName, - state: "closed", + owner: owner, + repo: repo, + state: "all", sort: "due_on", - per_page: 1, + per_page: 100, direction: "desc", }); if (milestones.data.length === 0) { - console.error("No closed milestones found."); + console.error("No milestones found."); process.exit(1); } - const milestone = milestones.data[0]; - - outputData[milestone.number] = { - due_on: milestone.due_on, - title: milestone.title, - description: milestone.description, - open_issues: milestone.open_issues, - closed_issues: milestone.closed_issues, - issues_with_estimate: 0, - effort_estimate: 0, - }; - - const issues = await github.rest.issues.listForRepo({ - owner: repoOwner, - repo: repoName, - milestone: milestone.number, - state: "closed", - per_page: 100, - }); + for (const milestone of milestones.data) { + outputJson[milestone.number] = { + title: milestone.title, + due_on: milestone.due_on, + open_issues: milestone.open_issues, + closed_issues: milestone.closed_issues, + remaining_estimate: 0, + completed_estimate: 0, + }; - for (const issue of issues.data) { - if ("pull_request" in issue) { - continue; - } + const issues = await github.paginate(github.rest.issues.listForRepo, { + owner: owner, + repo: repo, + milestone: milestone.number, + state: "all", + per_page: 100, + }); - for (const label of issue.labels) { - if (label.name.match(/estimate/)) { - outputData[milestone.number].issues_with_estimate++; - outputData[milestone.number].effort_estimate += Number.parseInt(label.name.replace(/\D/g, "")); - break; + for (const issue of issues) { + if (issue.pull_request) { + continue; + } + + for (const label of issue.labels) { + const estimateLabelMatch = label.name.match(/estimate \- (\d+)/); + + if (estimateLabelMatch?.length > 1) { + outputJson[milestone.number][issue.state === "open" ? "remaining_estimate" : "completed_estimate"] += + Number.parseInt(estimateLabelMatch[1]); + + break; // assumes an issue will only have one estimate label + } } } + + outputCsv = `${outputCsv}\n${milestone.number},${Object.values(outputJson[milestone.number]).join(",")}`; } - await writeFile(outputPath, JSON.stringify(outputData, null, 2)); + await writeFile(outputCsvPath, outputCsv); + await writeFile(outputJsonPath, JSON.stringify(outputJson, null, 2)); + process.exit(0); } catch (error) { console.error(error); diff --git a/.github/workflows/track-milestone-estimates.yml b/.github/workflows/track-milestone-estimates.yml index 9704a699eb6..60d63842962 100644 --- a/.github/workflows/track-milestone-estimates.yml +++ b/.github/workflows/track-milestone-estimates.yml @@ -1,19 +1,23 @@ name: Track Milestone Estimates on: workflow_dispatch: + issues: + types: [closed] jobs: estimates: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Create estimates data - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const action = require('${{ github.workspace }}/.github/scripts/trackMilestoneEstimates.js') await action({github, context, core}) - - name: Archive estimate artifacts + - name: Upload estimates data uses: actions/upload-artifact@v4 with: name: milestone-estimates - path: .github/milestone-estimates.json + path: .github/milestone-estimates.* + if-no-files-found: error + overwrite: true From 863e148d83f9ff53468b5e3da5bbf7ea06235a07 Mon Sep 17 00:00:00 2001 From: Ben Elan Date: Mon, 30 Sep 2024 06:48:30 -0700 Subject: [PATCH 3/5] chore: fix type issues --- .github/scripts/trackMilestoneEstimates.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/scripts/trackMilestoneEstimates.js b/.github/scripts/trackMilestoneEstimates.js index d4620f563ee..64845e486da 100644 --- a/.github/scripts/trackMilestoneEstimates.js +++ b/.github/scripts/trackMilestoneEstimates.js @@ -7,7 +7,7 @@ module.exports = async ({ github, context }) => { const { repo, owner } = context.repo; const outputJson = {}; - let outputCsv = "id,title,open_issues,closed_issues,due_on,description,remaining_estimate,completed_estimate"; + let outputCsv = "id,title,due_on,open_issues,closed_issues,remaining_estimate,completed_estimate"; const outputJsonPath = resolve(__dirname, "..", "milestone-estimates.json"); const outputCsvPath = resolve(__dirname, "..", "milestone-estimates.csv"); @@ -38,9 +38,10 @@ module.exports = async ({ github, context }) => { }; const issues = await github.paginate(github.rest.issues.listForRepo, { + // @ts-ignore milestone.number is valid: https://docs.github.com/en/rest/issues/issues#list-repository-issues--parameters + milestone: milestone.number, owner: owner, repo: repo, - milestone: milestone.number, state: "all", per_page: 100, }); @@ -51,9 +52,13 @@ module.exports = async ({ github, context }) => { } for (const label of issue.labels) { - const estimateLabelMatch = label.name.match(/estimate \- (\d+)/); + if (typeof label === "string" || !label?.name) { + continue; + } + + const estimateLabelMatch = label.name.match(/estimate - (\d+)/); - if (estimateLabelMatch?.length > 1) { + if (estimateLabelMatch && estimateLabelMatch?.length > 1) { outputJson[milestone.number][issue.state === "open" ? "remaining_estimate" : "completed_estimate"] += Number.parseInt(estimateLabelMatch[1]); From 641d57cf732c95d5bf226dee0fdc84e876478c09 Mon Sep 17 00:00:00 2001 From: Ben Elan Date: Mon, 30 Sep 2024 07:44:47 -0700 Subject: [PATCH 4/5] chore: cleanup --- .github/scripts/trackMilestoneEstimates.js | 15 ++++++++------- .github/workflows/track-milestone-estimates.yml | 4 +--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/scripts/trackMilestoneEstimates.js b/.github/scripts/trackMilestoneEstimates.js index 64845e486da..31c0fd92719 100644 --- a/.github/scripts/trackMilestoneEstimates.js +++ b/.github/scripts/trackMilestoneEstimates.js @@ -1,17 +1,13 @@ // @ts-check -const { resolve } = require("path"); const { writeFile } = require("fs/promises"); /** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */ -module.exports = async ({ github, context }) => { +module.exports = async ({ github, context, core }) => { const { repo, owner } = context.repo; const outputJson = {}; let outputCsv = "id,title,due_on,open_issues,closed_issues,remaining_estimate,completed_estimate"; - const outputJsonPath = resolve(__dirname, "..", "milestone-estimates.json"); - const outputCsvPath = resolve(__dirname, "..", "milestone-estimates.csv"); - try { const milestones = await github.rest.issues.listMilestones({ owner: owner, @@ -70,8 +66,13 @@ module.exports = async ({ github, context }) => { outputCsv = `${outputCsv}\n${milestone.number},${Object.values(outputJson[milestone.number]).join(",")}`; } - await writeFile(outputCsvPath, outputCsv); - await writeFile(outputJsonPath, JSON.stringify(outputJson, null, 2)); + const stringifiedOutputJson = JSON.stringify(outputJson, null, 2); + + core.debug(`JSON Output:\n${stringifiedOutputJson}`); + core.debug(`\nCSV Output:\n${outputCsv}`); + + await writeFile("./milestone-estimates.csv", outputCsv); + await writeFile("./milestone-estimates.json", stringifiedOutputJson); process.exit(0); } catch (error) { diff --git a/.github/workflows/track-milestone-estimates.yml b/.github/workflows/track-milestone-estimates.yml index 60d63842962..b0917395397 100644 --- a/.github/workflows/track-milestone-estimates.yml +++ b/.github/workflows/track-milestone-estimates.yml @@ -18,6 +18,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: milestone-estimates - path: .github/milestone-estimates.* - if-no-files-found: error - overwrite: true + path: milestone-estimates.* From 7215b0557613271ef0a84d7e18d93d50c9fb21b9 Mon Sep 17 00:00:00 2001 From: Ben Elan Date: Mon, 30 Sep 2024 08:02:10 -0700 Subject: [PATCH 5/5] chore: cleanup --- .github/scripts/trackMilestoneEstimates.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/scripts/trackMilestoneEstimates.js b/.github/scripts/trackMilestoneEstimates.js index 31c0fd92719..34c7287c3ad 100644 --- a/.github/scripts/trackMilestoneEstimates.js +++ b/.github/scripts/trackMilestoneEstimates.js @@ -48,13 +48,9 @@ module.exports = async ({ github, context, core }) => { } for (const label of issue.labels) { - if (typeof label === "string" || !label?.name) { - continue; - } - - const estimateLabelMatch = label.name.match(/estimate - (\d+)/); + const estimateLabelMatch = (typeof label === "string" ? label : label?.name)?.match(/estimate - (\d+)/); - if (estimateLabelMatch && estimateLabelMatch?.length > 1) { + if (estimateLabelMatch?.length > 1) { outputJson[milestone.number][issue.state === "open" ? "remaining_estimate" : "completed_estimate"] += Number.parseInt(estimateLabelMatch[1]); @@ -69,7 +65,7 @@ module.exports = async ({ github, context, core }) => { const stringifiedOutputJson = JSON.stringify(outputJson, null, 2); core.debug(`JSON Output:\n${stringifiedOutputJson}`); - core.debug(`\nCSV Output:\n${outputCsv}`); + core.debug(`CSV Output:\n${outputCsv}`); await writeFile("./milestone-estimates.csv", outputCsv); await writeFile("./milestone-estimates.json", stringifiedOutputJson);