Script updates.

This commit is contained in:
Christian Gunderman
2026-04-23 07:57:37 -07:00
parent 9109505145
commit 87485c87a4
8 changed files with 367 additions and 93 deletions
+55 -14
View File
@@ -5,7 +5,7 @@
*/
import { execSync } from 'node:child_process';
import { updateSimulationCsv, execGh, getRepoInfo } from './utils.js';
import { updateSimulationCsv, execGh, getRepoInfo, getMaintainers, getMaintainerWorkload } from './utils.js';
const EXECUTE_ACTIONS = process.env.EXECUTE_ACTIONS === 'true';
@@ -14,11 +14,14 @@ async function run() {
console.log(`PR Nudge starting for ${owner}/${repo}... (EXECUTE_ACTIONS=${EXECUTE_ACTIONS})`);
try {
// 1. Fetch community PRs that pass CI but need review
const MAINTAINERS = await getMaintainers();
const WORKLOAD = await getMaintainerWorkload();
// 1. Fetch community PRs
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(first: 50, states: OPEN) {
pullRequests(first: 500, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) {
nodes {
number
author { login }
@@ -28,6 +31,7 @@ async function run() {
isDraft
reviewDecision
mergeable
assignees(first: 1) { nodes { login } }
commits(last: 1) {
nodes {
commit {
@@ -45,13 +49,20 @@ async function run() {
}
}
`;
const output = execSync(`gh api graphql -F owner=${owner} -F repo=${repo} -f query='${query}'`, { encoding: 'utf-8' });
const output = execSync(`gh api graphql -F owner=${owner} -F repo=${repo} -f query='${query}'`, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
const data = JSON.parse(output).data.repository;
const prs = data.pullRequests.nodes;
const actions = [];
const now = new Date();
// Sort maintainers by workload (ascending)
const sortedMaintainers = MAINTAINERS
.filter(m => m !== 'TOTAL_MAINTAINERS')
.sort((a, b) => (WORKLOAD[a] || 0) - (WORKLOAD[b] || 0));
let mIndex = 0;
for (const pr of prs) {
if (['MEMBER', 'OWNER', 'COLLABORATOR'].includes(pr.authorAssociation)) continue;
if (pr.isDraft) continue;
@@ -62,12 +73,24 @@ async function run() {
const isConflicting = pr.mergeable === 'CONFLICTING';
const updatedAt = new Date(pr.updatedAt);
const hoursSinceUpdate = (now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60);
const daysSinceUpdate = (now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24);
const hoursSinceUpdate = daysSinceUpdate * 24;
const hasNudgeLabel = pr.labels.nodes.some(l => l.name === 'status/nudge');
const hasConflictLabel = pr.labels.nodes.some(l => l.name === 'status/merge-conflict');
const hasAssignee = pr.assignees.nodes.length > 0;
// 1. Author Nudge for Conflicts
// 1. Terminal State: Close stale conflicts
if (isConflicting && hasConflictLabel && daysSinceUpdate > 14) {
actions.push({
number: pr.number,
type: 'close-conflict',
comment: `Hi @${pr.author?.login || 'author'}! This PR has had merge conflicts for over 14 days. We are closing it to keep the queue manageable. Please feel free to reopen it once you have resolved the conflicts and synchronized with the main branch.`
});
continue;
}
// 2. Author Nudge for Conflicts
if (isConflicting && !hasConflictLabel) {
actions.push({
number: pr.number,
@@ -77,14 +100,26 @@ async function run() {
continue;
}
// 2. Maintainer Nudge for Ready PRs
// 3. Maintainer Action for Ready PRs
if (isCiSuccess && isMergeable && pr.reviewDecision === 'REVIEW_REQUIRED') {
// Nudge maintainers if ready and no activity for 48 hours
if (hoursSinceUpdate > 48 && !hasNudgeLabel) {
if (!hasAssignee) {
// Assign a maintainer based on workload
const assignee = sortedMaintainers[mIndex % sortedMaintainers.length];
mIndex++;
WORKLOAD[assignee] = (WORKLOAD[assignee] || 0) + 1;
actions.push({
number: pr.number,
type: 'assign-reviewer',
assignee,
comment: `Hi @${assignee}! This community PR by @${pr.author?.login || 'author'} is ready for review (Mergeable + CI Success). Assigning to you based on current workload.`
});
} else if (hoursSinceUpdate > 48 && !hasNudgeLabel) {
// Nudge existing assignee if inactive for 48 hours
actions.push({
number: pr.number,
type: 'maintainer-nudge',
comment: `Hi maintainers! This community PR by @${pr.author?.login || 'author'} has passed all CI checks and is mergeable. It has been open and inactive for over 48 hours. Could someone please take a look? @google-gemini/gemini-cli-maintainers`
comment: `Hi @${pr.assignees.nodes[0].login}! This PR is ready and has been inactive for over 48 hours. Could you please take a look?`
});
}
}
@@ -96,15 +131,21 @@ async function run() {
for (const action of actions) {
try {
if (action.type === 'author-nudge-conflict') {
execGh(`pr edit ${action.number} --add-label "status/merge-conflict"`, EXECUTE_ACTIONS);
await execGh(`pr edit ${action.number} --add-label "status/merge-conflict"`, EXECUTE_ACTIONS);
simulationUpdates.set(action.number.toString(), { labels: 'status/merge-conflict' });
} else if (action.type === 'close-conflict') {
await execGh(`pr close ${action.number}`, EXECUTE_ACTIONS);
simulationUpdates.set(action.number.toString(), { state: 'CLOSED' });
} else if (action.type === 'assign-reviewer') {
await execGh(`pr edit ${action.number} --add-assignee "${action.assignee}" --add-label "status/nudge"`, EXECUTE_ACTIONS);
simulationUpdates.set(action.number.toString(), { assignee: action.assignee, labels: 'status/nudge' });
} else if (action.type === 'maintainer-nudge') {
execGh(`pr edit ${action.number} --add-label "status/nudge"`, EXECUTE_ACTIONS);
await execGh(`pr edit ${action.number} --add-label "status/nudge"`, EXECUTE_ACTIONS);
simulationUpdates.set(action.number.toString(), { labels: 'status/nudge' });
}
if (action.comment) {
execGh(`pr comment ${action.number} --body "${action.comment}"`, EXECUTE_ACTIONS);
await execGh(`pr comment ${action.number} --body "${action.comment}"`, EXECUTE_ACTIONS);
}
} catch (err) {
console.error(`Failed to process PR #${action.number}:`, err);
@@ -114,7 +155,7 @@ async function run() {
// 3. Update simulation
await updateSimulationCsv('prs-after.csv', simulationUpdates);
console.log(`Processed ${actions.length} PR nudges.`);
console.log(`Processed ${actions.length} PR nudges/actions.`);
} catch (err) {
console.error('Error in PR Nudge:', err);