diff --git a/.gitignore b/.gitignore index a9eefa79db..ab4c522e57 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ jest.debug.config.js /graph/client/src/assets/generated-task-graphs /nx-dev/nx-dev/public/documentation /nx-dev/nx-dev/public/images/open-graph +/scripts/issues-scraper/cached # Lerna creates this CHANGELOG.md diff --git a/package.json b/package.json index fed1e63d75..636984cb40 100644 --- a/package.json +++ b/package.json @@ -193,6 +193,7 @@ "loader-utils": "2.0.3", "lockfile-lint": "^4.7.6", "magic-string": "~0.26.2", + "markdown-factory": "^0.0.3", "memfs": "^3.0.1", "metro-resolver": "^0.74.1", "mini-css-extract-plugin": "~2.4.7", @@ -337,4 +338,3 @@ ] } } - diff --git a/scripts/issues-scraper/format-slack-message.ts b/scripts/issues-scraper/format-slack-message.ts index da2ebedf14..4b299b9433 100644 --- a/scripts/issues-scraper/format-slack-message.ts +++ b/scripts/issues-scraper/format-slack-message.ts @@ -1,4 +1,5 @@ -import { ReportData } from './model'; +import { ReportData, TrendData } from './model'; +import { table } from 'markdown-factory'; export function getSlackMessageJson(body: string) { return { @@ -17,7 +18,7 @@ export function getSlackMessageJson(body: string) { export function formatGhReport( currentData: ReportData, - trendData: ReportData, + trendData: TrendData, prevData: ReportData, unlabeledIssuesUrl: string ): string { @@ -38,68 +39,50 @@ Totals, Issues: ${currentData.totalIssueCount} ${formattedIssueDelta} Bugs: ${cu `Untriaged: ${currentData.untriagedIssueCount} ${formatDelta( trendData.untriagedIssueCount )}`, + `Closed since last report: ${currentData.totalClosed} ${formatDelta( + trendData.totalClosed + )}`, ]; - const sorted = Object.entries(currentData.scopes).sort( - ([, a], [, b]) => b.count - a.count - ); - - const { bugPadding, issuePadding, scopePadding } = getPaddingValues( - currentData, - trendData - ); + const sorted = Object.entries(currentData.scopes) + .sort(([, a], [, b]) => b.count - a.count) + .map(([scope, x]) => ({ + ...x, + scope, + })); bodyLines.push( - `| ${'Scope'.padEnd(scopePadding)} | ${'Issues'.padEnd( - issuePadding - )} | ${'Bugs'.padEnd(bugPadding)} |` + table(sorted, [ + { + field: 'scope', + label: 'Scope', + }, + { + label: 'Issues', + mapFn: (el) => + `${el.count} ${formatDelta(trendData.scopes[el.scope].count)}`, + }, + { + label: 'Bugs', + mapFn: (el) => + `${el.bugCount} ${formatDelta(trendData.scopes[el.scope].bugCount)}`, + }, + { + label: 'Closed', + mapFn: (el) => + `${el.closed} ${formatDelta(trendData.scopes[el.scope].closed)}`, + }, + ]) ); - bodyLines.push('='.repeat(scopePadding + issuePadding + bugPadding + 10)); - for (const [scope, data] of sorted) { - const formattedIssueDelta = formatDelta(trendData.scopes[scope].count); - const formattedBugDelta = formatDelta(trendData.scopes[scope].bugCount); - const issuesCell = `${data.count} ${formattedIssueDelta}`.padEnd( - issuePadding - ); - const bugCell = `${data.bugCount} ${formattedBugDelta}`.padEnd(bugPadding); - bodyLines.push( - `| ${scope.padEnd(scopePadding)} | ${issuesCell} | ${bugCell} |` - ); - } + const footer = '```'; return header + bodyLines.join('\n') + footer; } function formatDelta(delta: number | null): string { - if (delta === null || delta === 0) { + if (!delta) { return ''; } return delta < 0 ? `(${delta})` : `(+${delta})`; } - -function getPaddingValues(data: ReportData, trendData: ReportData) { - const scopes = Object.entries(data.scopes); - const scopePadding = Math.max(...scopes.map((x) => x[0].length)); - const issuePadding = - Math.max( - ...scopes.map( - (x) => - x[1].count.toString().length + - formatDelta(trendData.scopes[x[0]].count).length - ) - ) + 2; - const bugPadding = - Math.max( - ...scopes.map( - (x) => - x[1].bugCount.toString().length + - formatDelta(trendData.scopes[x[0]].bugCount).length - ) - ) + 2; - return { - scopePadding, - issuePadding, - bugPadding, - }; -} diff --git a/scripts/issues-scraper/index.ts b/scripts/issues-scraper/index.ts index d6e295cfa8..65cf164335 100644 --- a/scripts/issues-scraper/index.ts +++ b/scripts/issues-scraper/index.ts @@ -1,6 +1,6 @@ import { ensureDirSync, readJsonSync, writeJsonSync } from 'fs-extra'; import { dirname, join } from 'path'; -import { ReportData, ScopeData } from './model'; +import { ReportData, ScopeData, TrendData } from './model'; import { getScopeLabels, scrapeIssues } from './scrape-issues'; import { formatGhReport, getSlackMessageJson } from './format-slack-message'; import { setOutput } from '@actions/core'; @@ -10,8 +10,10 @@ import { readdirSync } from 'fs'; const CACHE_FILE = join(__dirname, 'cached', 'data.json'); async function main() { - const currentData = await scrapeIssues(); const oldData = getOldData(); + const currentData = await scrapeIssues( + oldData.collectedDate ? new Date(oldData.collectedDate) : undefined + ); const trendData = getTrendData(currentData, oldData); const formatted = formatGhReport( currentData, @@ -31,18 +33,21 @@ if (require.main === module) { }); } -function getTrendData(newData: ReportData, oldData: ReportData): ReportData { +function getTrendData(newData: ReportData, oldData: ReportData): TrendData { const scopeTrends: Record> = {}; for (const [scope, data] of Object.entries(newData.scopes)) { scopeTrends[scope] ??= {}; scopeTrends[scope].count = data.count - (oldData.scopes[scope]?.count ?? 0); scopeTrends[scope].bugCount = data.bugCount - (oldData.scopes[scope]?.bugCount ?? 0); + scopeTrends[scope].closed = + data.closed - (oldData.scopes[scope]?.closed ?? 0); } return { scopes: scopeTrends as Record, totalBugCount: newData.totalBugCount - oldData.totalBugCount, totalIssueCount: newData.totalIssueCount - oldData.totalIssueCount, + totalClosed: newData.totalClosed - oldData.totalClosed ?? 0, untriagedIssueCount: newData.untriagedIssueCount - oldData.untriagedIssueCount, }; @@ -66,6 +71,7 @@ function getOldData(): ReportData { totalBugCount: 0, totalIssueCount: 0, untriagedIssueCount: 0, + totalClosed: 0, }; } } diff --git a/scripts/issues-scraper/model.ts b/scripts/issues-scraper/model.ts index ba112d9657..f6ead5fbbb 100644 --- a/scripts/issues-scraper/model.ts +++ b/scripts/issues-scraper/model.ts @@ -1,12 +1,16 @@ export interface ScopeData { bugCount: number; count: number; + closed: number; } export interface ReportData { scopes: Record; totalBugCount: number; totalIssueCount: number; + totalClosed: number; untriagedIssueCount: number; collectedDate?: string; } + +export type TrendData = Omit; diff --git a/scripts/issues-scraper/scrape-issues.ts b/scripts/issues-scraper/scrape-issues.ts index 0affdf3f58..a091d799a6 100644 --- a/scripts/issues-scraper/scrape-issues.ts +++ b/scripts/issues-scraper/scrape-issues.ts @@ -3,12 +3,13 @@ import { ReportData, ScopeData } from './model'; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); -export async function scrapeIssues(): Promise { +export async function scrapeIssues(prevDate?: Date): Promise { const issues = getIssueIterator(); let total = 0; let totalBugs = 0; let untriagedIssueCount = 0; + let totalClosed = 0; const scopeLabels = await getScopeLabels(); const scopes: Record = {}; @@ -21,9 +22,20 @@ export async function scrapeIssues(): Promise { const bug = issue.labels.some( (x) => (typeof x === 'string' ? x : x.name) === 'type: bug' ); - if (bug) { - totalBugs += 1; + const closed = + issue.state === 'closed' && + issue.closed_at && + prevDate && + new Date(issue.closed_at) > prevDate; + + if (closed) { + totalClosed += 1; + } else if (issue.closed_at === null) { + if (bug) { + totalBugs += 1; + } } + let triaged = false; for (const scope of scopeLabels) { if ( @@ -31,15 +43,19 @@ export async function scrapeIssues(): Promise { (x) => x === scope || (typeof x === 'object' && x.name === scope) ) ) { - scopes[scope] ??= { bugCount: 0, count: 0 }; - if (bug) { - scopes[scope].bugCount += 1; + scopes[scope] ??= { bugCount: 0, count: 0, closed: 0 }; + if (closed) { + scopes[scope].closed += 1; + } else if (!issue.closed_at) { + if (bug) { + scopes[scope].bugCount += 1; + } + scopes[scope].count += 1; } - scopes[scope].count += 1; triaged = true; } } - if (!triaged) { + if (!triaged && !issue.closed_at) { untriagedIssueCount += 1; } } @@ -50,6 +66,7 @@ export async function scrapeIssues(): Promise { scopes: scopes, totalBugCount: totalBugs, totalIssueCount: total, + totalClosed, untriagedIssueCount, // Format is like: Mar 03 2023 collectedDate: new Date().toDateString().split(' ').slice(1).join(' '), @@ -61,6 +78,7 @@ const getIssueIterator = () => { owner: 'nrwl', repo: 'nx', per_page: 100, + state: 'all', }); }; diff --git a/yarn.lock b/yarn.lock index 708b5013a1..46a18a458d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18368,6 +18368,11 @@ markdown-escapes@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== +markdown-factory@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/markdown-factory/-/markdown-factory-0.0.3.tgz#18c9083da275de66097148aff78c6d710d21cfa6" + integrity sha512-y7XffnQ61exHdRdFmvp9MeJBGOpUIYJEdiNgj25VCCivA/oXs615ppGqH01lnG8zcZ9cEEPW1jIi1y/Xu4URPA== + marked@4.0.18: version "4.0.18" resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.18.tgz#cd0ac54b2e5610cfb90e8fd46ccaa8292c9ed569"