amend the contributor points calculation logic to handle the docs merge (#6127)

This commit is contained in:
Matt Fiddaman
2025-11-12 23:03:35 +00:00
committed by GitHub
parent d3d6b68bfe
commit 774e3b7f92
2 changed files with 133 additions and 133 deletions

View File

@@ -4,43 +4,32 @@ import pLimit from 'p-limit';
const limit = pLimit(50);
/** Repository-specific configuration for points calculation */
const REPOSITORY_CONFIG = new Map([
[
'actual',
{
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 0,
PR_REVIEW_POINT_TIERS: [
{ minChanges: 500, points: 8 },
{ minChanges: 100, points: 6 },
{ minChanges: 10, points: 2 },
{ minChanges: 0, points: 1 },
],
EXCLUDED_FILES: [
'yarn.lock',
'.yarn/**/*',
'packages/component-library/src/icons/**/*',
'release-notes/**/*',
],
},
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
// Point tiers for main repo changes (non-docs)
MAIN_PR_REVIEW_POINT_TIERS: [
{ minChanges: 500, points: 8 },
{ minChanges: 100, points: 6 },
{ minChanges: 10, points: 2 },
{ minChanges: 0, points: 1 },
],
[
'docs',
{
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 4,
PR_REVIEW_POINT_TIERS: [
{ minChanges: 2000, points: 6 },
{ minChanges: 200, points: 4 },
{ minChanges: 0, points: 2 },
],
EXCLUDED_FILES: ['yarn.lock', '.yarn/**/*'],
},
// Point tiers for docs changes (packages/docs/**)
DOCS_PR_REVIEW_POINT_TIERS: [
{ minChanges: 2000, points: 6 },
{ minChanges: 200, points: 4 },
{ minChanges: 0, points: 2 },
],
]);
EXCLUDED_FILES: [
'yarn.lock',
'.yarn/**/*',
'packages/component-library/src/icons/**/*',
'release-notes/**/*',
'upcoming-release-notes/**/*',
],
DOCS_FILES_PATTERN: 'packages/docs/**/*',
};
/**
* Get the start and end dates for the last month.
@@ -76,15 +65,14 @@ function getLastMonthDates() {
/**
* Used for calculating the monthly points each core contributor has earned.
* These are used for payouts depending.
* @param {string} repo - The repository to analyze ('actual' or 'docs')
* @returns {number} The total points earned for the repository
* @returns {Map} A map of contributor logins to their total points earned
*/
async function countContributorPoints(repo) {
async function countContributorPoints() {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const owner = 'actualbudget';
const config = REPOSITORY_CONFIG.get(repo);
const repo = 'actual';
const { since, until } = getLastMonthDates();
@@ -99,7 +87,8 @@ async function countContributorPoints(repo) {
Array.from(orgMemberLogins).map(login => [
login,
{
reviews: [], // Will store objects with PR number and points
mainReviews: [], // Will store objects with PR number and points for main repo changes
docsReviews: [], // Will store objects with PR number and points for docs changes
labelRemovals: [],
issueClosings: [],
points: 0,
@@ -156,48 +145,91 @@ async function countContributorPoints(repo) {
),
]);
const totalChanges = modifiedFiles
.filter(
file =>
!config.EXCLUDED_FILES.some(pattern =>
minimatch(file.filename, pattern),
),
)
.reduce((sum, file) => sum + file.additions + file.deletions, 0);
const filteredFiles = modifiedFiles.filter(
file =>
!CONFIG.EXCLUDED_FILES.some(pattern =>
minimatch(file.filename, pattern, { dot: true }),
),
);
const docsFiles = filteredFiles.filter(file =>
minimatch(file.filename, CONFIG.DOCS_FILES_PATTERN, { dot: true }),
);
const mainFiles = filteredFiles.filter(
file =>
!minimatch(file.filename, CONFIG.DOCS_FILES_PATTERN, { dot: true }),
);
const docsChanges = docsFiles.reduce(
(sum, file) => sum + file.additions + file.deletions,
0,
);
const mainChanges = mainFiles.reduce(
(sum, file) => sum + file.additions + file.deletions,
0,
);
const docsPoints =
docsChanges > 0
? (CONFIG.DOCS_PR_REVIEW_POINT_TIERS.find(
t => docsChanges >= t.minChanges,
)?.points ?? 0)
: 0;
const mainPoints =
mainChanges > 0 || docsChanges === 0
? (CONFIG.MAIN_PR_REVIEW_POINT_TIERS.find(
t => mainChanges >= t.minChanges,
)?.points ?? 0)
: 0;
const isReleasePR = pr.title.match(/🔖.*\d+\.\d+\.\d+/);
const prPoints =
config.PR_REVIEW_POINT_TIERS.find(t => totalChanges >= t.minChanges)
?.points ?? 0;
if (isReleasePR) {
if (stats.has(pr.user.login)) {
const creatorStats = stats.get(pr.user.login);
creatorStats.reviews.push({
// release PRs are created by the github-actions bot so we attribute points to the merger
const { data: prDetails } = await octokit.pulls.get({
owner,
repo,
pull_number: pr.number,
});
if (prDetails.merged_by && stats.has(prDetails.merged_by.login)) {
const mergerStats = stats.get(prDetails.merged_by.login);
mergerStats.mainReviews.push({
pr: pr.number.toString(),
points: config.POINTS_PER_RELEASE_PR,
isReleaseCreator: true,
points: CONFIG.POINTS_PER_RELEASE_PR,
isReleaseMerger: true,
});
creatorStats.points += config.POINTS_PER_RELEASE_PR;
mergerStats.points += CONFIG.POINTS_PER_RELEASE_PR;
}
} else {
const uniqueReviewers = new Set();
reviews.data
.filter(
review =>
stats.has(review.user?.login) &&
review.state === 'APPROVED' &&
!uniqueReviewers.has(review.user?.login),
)
.forEach(({ user: { login: reviewer } }) => {
reviews.data.forEach(review => {
if (
review.state === 'APPROVED' &&
stats.has(review.user?.login) &&
!uniqueReviewers.has(review.user?.login)
) {
const reviewer = review.user.login;
uniqueReviewers.add(reviewer);
const userStats = stats.get(reviewer);
userStats.reviews.push({
pr: pr.number.toString(),
points: prPoints,
});
userStats.points += prPoints;
});
if (docsPoints > 0) {
userStats.docsReviews.push({
pr: pr.number.toString(),
points: docsPoints,
});
userStats.points += docsPoints;
}
if (mainPoints > 0) {
userStats.mainReviews.push({
pr: pr.number.toString(),
points: mainPoints,
});
userStats.points += mainPoints;
}
}
});
}
}),
),
@@ -241,7 +273,7 @@ async function countContributorPoints(repo) {
const remover = event.actor.login;
const userStats = stats.get(remover);
userStats.labelRemovals.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
userStats.points += CONFIG.POINTS_PER_ISSUE_TRIAGE_ACTION;
}
if (
@@ -251,7 +283,7 @@ async function countContributorPoints(repo) {
const closer = event.actor.login;
const userStats = stats.get(closer);
userStats.issueClosings.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_CLOSING_ACTION;
userStats.points += CONFIG.POINTS_PER_ISSUE_CLOSING_ACTION;
}
});
}),
@@ -260,27 +292,39 @@ async function countContributorPoints(repo) {
// Print all statistics
printStats(
`PR Review Statistics (${repo})`,
stats => stats.reviews.length,
'Main Repo PR Review Statistics',
stats => stats.mainReviews.length,
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
.reviews.map(r => {
if (r.isReleaseCreator) {
return `#${r.pr} (${r.points}pts - Release Creator)`;
.mainReviews.map(r => {
if (r.isReleaseMerger) {
return `#${r.pr} (${r.points}pts - Release Merger)`;
}
return `#${r.pr} (${r.points}pts)`;
})
.join(', ')})`,
);
printStats(
`"Needs Triage" Label Removal Statistics (${repo})`,
'Docs PR Review Statistics',
stats => stats.docsReviews.length,
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
.docsReviews.map(r => `#${r.pr} (${r.points}pts)`)
.join(', ')})`,
);
printStats(
'"Needs Triage" Label Removal Statistics',
stats => stats.labelRemovals.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
);
printStats(
`Issue Closing Statistics (${repo})`,
'Issue Closing Statistics',
stats => stats.issueClosings.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
@@ -288,7 +332,7 @@ async function countContributorPoints(repo) {
// Print points summary
printStats(
`Points Summary (${repo})`,
'Points Summary',
stats => stats.points,
(user, userPoints) => `${user}: ${userPoints}`,
);
@@ -298,7 +342,7 @@ async function countContributorPoints(repo) {
(sum, userStats) => sum + userStats.points,
0,
);
console.log(`\nTotal points earned for ${repo}: ${totalPoints}`);
console.log(`\nTotal points earned: ${totalPoints}`);
// Return the points
return new Map(
@@ -309,55 +353,5 @@ async function countContributorPoints(repo) {
);
}
/**
* Calculate the points for both repositories and print cumulative results
*/
async function calculateCumulativePoints() {
// Get stats for each repository
const repoPointsResults = await Promise.all(
Array.from(REPOSITORY_CONFIG.keys()).map(countContributorPoints),
);
// Calculate cumulative stats
const cumulativeStats = new Map(repoPointsResults[0]);
// Combine stats from all repositories
for (let i = 1; i < repoPointsResults.length; i++) {
for (const [login, points] of repoPointsResults[i].entries()) {
if (!cumulativeStats.has(login)) {
cumulativeStats.set(login, 0);
}
cumulativeStats.set(login, cumulativeStats.get(login) + points);
}
}
// Print cumulative statistics
console.log('\n\nCUMULATIVE STATISTICS ACROSS ALL REPOSITORIES');
console.log('='.repeat(50));
console.log('\nCumulative Points Summary:');
console.log('='.repeat('Cumulative Points Summary'.length + 1));
const entries = Array.from(cumulativeStats.entries())
.filter(([, count]) => count > 0)
.sort((a, b) => b[1] - a[1]);
if (entries.length === 0) {
console.log('No cumulative points summary found.');
} else {
entries.forEach(([user, points]) => {
console.log(`${user}: ${points}`);
});
}
// Calculate and print total cumulative points
const totalCumulativePoints = Array.from(cumulativeStats.values()).reduce(
(sum, points) => sum + points,
0,
);
console.log('\nTotal cumulative points earned: ' + totalCumulativePoints);
}
// Run the calculations
calculateCumulativePoints().catch(console.error);
countContributorPoints().catch(console.error);

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [matt-fidd]
---
Amend the contributor points calculation logic to handle the docs merge