diff --git a/.github/scripts/count-points.mjs b/.github/scripts/count-points.mjs index 181bf9f2bb..c051bc7ce8 100644 --- a/.github/scripts/count-points.mjs +++ b/.github/scripts/count-points.mjs @@ -8,6 +8,13 @@ const CONFIG = { POINTS_PER_ISSUE_TRIAGE_ACTION: 1, POINTS_PER_ISSUE_CLOSING_ACTION: 1, POINTS_PER_RELEASE_PR: 4, // Awarded to whoever merges the release PR + PR_CONTRIBUTION_POINTS: { + Features: 2, + Enhancements: 2, + Bugfix: 3, + Maintenance: 2, + Unknown: 2, + }, // Point tiers for code changes (non-docs) CODE_PR_REVIEW_POINT_TIERS: [ { minChanges: 500, points: 8 }, @@ -31,6 +38,116 @@ const CONFIG = { DOCS_FILES_PATTERN: 'packages/docs/**/*', }; +/** + * Parse category from release notes file content. + * @param {string} content - The content of the release notes file. + * @returns {string|null} The category or null if not found. + */ +function parseReleaseNotesCategory(content) { + if (!content) return null; + + // Extract YAML front matter + const frontMatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!frontMatterMatch) return null; + + // Extract category from front matter + const categoryMatch = frontMatterMatch[1].match(/^category:\s*(.+)$/m); + if (!categoryMatch) return null; + + return categoryMatch[1].trim(); +} + +/** + * Get the last commit SHA on or before a given date. + * @param {Octokit} octokit - The Octokit instance. + * @param {string} owner - Repository owner. + * @param {string} repo - Repository name. + * @param {Date} beforeDate - The date to find the last commit before. + * @returns {Promise} The commit SHA or null if not found. + */ +async function getLastCommitBeforeDate(octokit, owner, repo, beforeDate) { + try { + // Get the default branch from the repository + const { data: repoData } = await octokit.repos.get({ owner, repo }); + const defaultBranch = repoData.default_branch; + + const { data: commits } = await octokit.repos.listCommits({ + owner, + repo, + sha: defaultBranch, + until: beforeDate.toISOString(), + per_page: 1, + }); + + if (commits.length > 0) { + return commits[0].sha; + } + } catch { + // If error occurs, return null to fall back to default branch + } + + return null; +} + +/** + * Get the category and points for a PR by reading its release notes file. + * @param {Octokit} octokit - The Octokit instance. + * @param {string} owner - Repository owner. + * @param {string} repo - Repository name. + * @param {number} prNumber - PR number. + * @param {Date} monthEnd - The end date of the month to use as base revision. + * @returns {Object} Object with category and points, or null if error. + */ +async function getPRCategoryAndPoints( + octokit, + owner, + repo, + prNumber, + monthEnd, +) { + const releaseNotesPath = `upcoming-release-notes/${prNumber}.md`; + + try { + // Get the last commit of the month to use as base revision + const commitSha = await getLastCommitBeforeDate( + octokit, + owner, + repo, + monthEnd, + ); + + // Try to read the release notes file from the last commit of the month + const { data: fileContent } = await octokit.repos.getContent({ + owner, + repo, + path: releaseNotesPath, + ref: commitSha || undefined, // Use commit SHA if available, otherwise default branch + }); + + if (fileContent.content) { + // Decode base64 content + const content = Buffer.from(fileContent.content, 'base64').toString( + 'utf-8', + ); + const category = parseReleaseNotesCategory(content); + + if (category && CONFIG.PR_CONTRIBUTION_POINTS[category]) { + return { + category, + points: CONFIG.PR_CONTRIBUTION_POINTS[category], + }; + } + } + } catch { + // Do nothing + } + + return { + category: 'Unknown', + points: CONFIG.PR_CONTRIBUTION_POINTS.Unknown, + }; +} + /** * Get the start and end dates for the last month. * @returns {Object} An object containing the start and end dates. @@ -89,6 +206,7 @@ async function countContributorPoints() { { codeReviews: [], // Will store objects with PR number and points for main repo changes docsReviews: [], // Will store objects with PR number and points for docs changes + prContributions: [], // Will store objects with PR number, category, and points for PR author contributions labelRemovals: [], issueClosings: [], points: 0, @@ -202,6 +320,28 @@ async function countContributorPoints() { mergerStats.points += CONFIG.POINTS_PER_RELEASE_PR; } } else { + // Award points to PR author if they are a core maintainer + const prAuthor = pr.user?.login; + if (prAuthor && orgMemberLogins.has(prAuthor)) { + const categoryAndPoints = await getPRCategoryAndPoints( + octokit, + owner, + repo, + pr.number, + until, + ); + + if (categoryAndPoints) { + const authorStats = stats.get(prAuthor); + authorStats.prContributions.push({ + pr: pr.number.toString(), + category: categoryAndPoints.category, + points: categoryAndPoints.points, + }); + authorStats.points += categoryAndPoints.points; + } + } + const uniqueReviewers = new Set(); reviews.data.forEach(review => { if ( @@ -293,7 +433,7 @@ async function countContributorPoints() { // Print all statistics printStats( 'Code Review Statistics', - stats => stats.codeReviews.length, + stats => stats.codeReviews.reduce((sum, r) => sum + r.points, 0), (user, count) => `${user}: ${count} (PRs: ${stats .get(user) @@ -308,7 +448,7 @@ async function countContributorPoints() { printStats( 'Docs Review Statistics', - stats => stats.docsReviews.length, + stats => stats.docsReviews.reduce((sum, r) => sum + r.points, 0), (user, count) => `${user}: ${count} (PRs: ${stats .get(user) @@ -316,16 +456,27 @@ async function countContributorPoints() { .join(', ')})`, ); + printStats( + 'PR Contribution Statistics', + stats => stats.prContributions.reduce((sum, r) => sum + r.points, 0), + (user, count) => + `${user}: ${count} (PRs: ${stats + .get(user) + .prContributions.map(r => `#${r.pr} (${r.points}pts - ${r.category})`) + .join(', ')})`, + ); + printStats( '"Needs Triage" Label Removal Statistics', - stats => stats.labelRemovals.length, + stats => stats.labelRemovals.length * CONFIG.POINTS_PER_ISSUE_TRIAGE_ACTION, (user, count) => `${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`, ); printStats( 'Issue Closing Statistics', - stats => stats.issueClosings.length, + stats => + stats.issueClosings.length * CONFIG.POINTS_PER_ISSUE_CLOSING_ACTION, (user, count) => `${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`, ); diff --git a/upcoming-release-notes/6481.md b/upcoming-release-notes/6481.md new file mode 100644 index 0000000000..969d022504 --- /dev/null +++ b/upcoming-release-notes/6481.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +ci: update contributor point counting script to account for PR authors