mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-26 11:43:24 -07:00
Script updates.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -14,11 +14,11 @@ async function run() {
|
||||
console.log(`Stale Manager starting for ${owner}/${repo}... (EXECUTE_ACTIONS=${EXECUTE_ACTIONS})`);
|
||||
|
||||
try {
|
||||
// 1. Fetch open issues/PRs that might be stale
|
||||
const query = `
|
||||
// 1. Fetch open issues
|
||||
const issueQuery = `
|
||||
query($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issues(first: 200, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) {
|
||||
issues(first: 1000, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) {
|
||||
nodes {
|
||||
number
|
||||
authorAssociation
|
||||
@@ -28,7 +28,22 @@ async function run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
pullRequests(first: 200, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) {
|
||||
}
|
||||
}
|
||||
`;
|
||||
let issueOutput;
|
||||
try {
|
||||
issueOutput = execSync(`gh api graphql -F owner=${owner} -F repo=${repo} -f query='${issueQuery}'`, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch issues from GitHub:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. Fetch open PRs
|
||||
const prQuery = `
|
||||
query($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(first: 500, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) {
|
||||
nodes {
|
||||
number
|
||||
authorAssociation
|
||||
@@ -52,16 +67,17 @@ async function run() {
|
||||
}
|
||||
}
|
||||
`;
|
||||
let output;
|
||||
let prOutput;
|
||||
try {
|
||||
output = execSync(`gh api graphql -F owner=${owner} -F repo=${repo} -f query='${query}'`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
||||
prOutput = execSync(`gh api graphql -F owner=${owner} -F repo=${repo} -f query='${prQuery}'`, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch issues/PRs from GitHub:', err);
|
||||
console.error('Failed to fetch PRs from GitHub:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = JSON.parse(output).data.repository;
|
||||
const items = [...data.issues.nodes.map(i => ({...i, type: 'issue'})), ...data.pullRequests.nodes.map(p => ({...p, type: 'pr'}))];
|
||||
const issueData = JSON.parse(issueOutput).data.repository;
|
||||
const prData = JSON.parse(prOutput).data.repository;
|
||||
const items = [...issueData.issues.nodes.map(i => ({...i, type: 'issue'})), ...prData.pullRequests.nodes.map(p => ({...p, type: 'pr'}))];
|
||||
|
||||
const now = new Date();
|
||||
const actions = [];
|
||||
@@ -71,10 +87,23 @@ async function run() {
|
||||
const daysSinceUpdate = (now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24);
|
||||
const isMaintainerOnly = item.labels.nodes.some(l => l.name === '🔒 maintainer only');
|
||||
const isStale = item.labels.nodes.some(l => l.name === 'Stale');
|
||||
const needsInfo = item.labels.nodes.some(l => l.name === 'status/needs-info');
|
||||
const isCommunity = !['MEMBER', 'OWNER', 'COLLABORATOR'].includes(item.authorAssociation);
|
||||
|
||||
if (isMaintainerOnly) continue; // Maintainer issues have their own lifecycle
|
||||
|
||||
// Special handling for needs-info: mark stale faster
|
||||
if (needsInfo && !isStale && daysSinceUpdate > 7) {
|
||||
actions.push({
|
||||
number: item.number,
|
||||
target: item.type,
|
||||
type: 'label',
|
||||
label: 'Stale',
|
||||
comment: `Hi! This ${item.type} was marked as 'status/needs-info' but has had no activity for 7 days. We are labeling it as 'Stale'. It will be closed in 14 days if no further activity occurs. Thank you!`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safeguard: Don't mark as stale if it's a PR ready for review (Maintainer bottleneck)
|
||||
if (item.type === 'pr') {
|
||||
const ciState = item.commits?.nodes[0]?.commit?.statusCheckRollup?.state;
|
||||
@@ -124,14 +153,14 @@ async function run() {
|
||||
const simulationMap = action.target === 'pr' ? prSimulationUpdates : issueSimulationUpdates;
|
||||
|
||||
if (action.type === 'label') {
|
||||
execGh(`${cmdPrefix} edit ${action.number} --add-label "Stale"`, EXECUTE_ACTIONS);
|
||||
simulationMap.set(action.number.toString(), { labels: 'Stale' });
|
||||
await execGh(`${cmdPrefix} edit ${action.number} --add-label "${action.label}"`, EXECUTE_ACTIONS);
|
||||
simulationMap.set(action.number.toString(), { labels: action.label });
|
||||
}
|
||||
if (action.comment) {
|
||||
execGh(`${cmdPrefix} comment ${action.number} --body "${action.comment}"`, EXECUTE_ACTIONS);
|
||||
await execGh(`${cmdPrefix} comment ${action.number} --body "${action.comment}"`, EXECUTE_ACTIONS);
|
||||
}
|
||||
if (action.type === 'close') {
|
||||
execGh(`${cmdPrefix} close ${action.number}`, EXECUTE_ACTIONS);
|
||||
await execGh(`${cmdPrefix} close ${action.number}`, EXECUTE_ACTIONS);
|
||||
simulationMap.set(action.number.toString(), { state: 'CLOSED' });
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { getMaintainers, execGh, getRepoInfo, updateSimulationCsv } from './utils.js';
|
||||
import { getMaintainers, execGh, getRepoInfo, updateSimulationCsv, getMaintainerWorkload } from './utils.js';
|
||||
|
||||
const EXECUTE_ACTIONS = process.env.EXECUTE_ACTIONS === 'true';
|
||||
|
||||
@@ -15,13 +15,14 @@ async function run() {
|
||||
|
||||
try {
|
||||
const MAINTAINERS = await getMaintainers();
|
||||
console.log(`Fetched ${MAINTAINERS.length} maintainers.`);
|
||||
const WORKLOAD = await getMaintainerWorkload();
|
||||
console.log(`Fetched ${MAINTAINERS.length} maintainers and current workloads.`);
|
||||
|
||||
// 1. Fetch untriaged issues
|
||||
// 1. Fetch untriaged issues (Increase limit to process the backlog)
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issues(first: 100, states: OPEN, labels: ["status/need-triage"]) {
|
||||
issues(first: 1000, states: OPEN, labels: ["status/need-triage"], orderBy: {field: CREATED_AT, direction: ASC}) {
|
||||
nodes {
|
||||
number
|
||||
title
|
||||
@@ -46,43 +47,53 @@ async function run() {
|
||||
const issues = data.issues.nodes;
|
||||
|
||||
const actions = [];
|
||||
let maintainerIndex = Math.floor(Math.random() * MAINTAINERS.length);
|
||||
|
||||
// Sort maintainers by workload (ascending)
|
||||
const sortedMaintainers = MAINTAINERS
|
||||
.filter(m => m !== 'TOTAL_MAINTAINERS') // safeguard
|
||||
.sort((a, b) => (WORKLOAD[a] || 0) - (WORKLOAD[b] || 0));
|
||||
|
||||
let mIndex = 0;
|
||||
|
||||
for (const issue of issues) {
|
||||
if (issue.assignees.nodes.length > 0) continue;
|
||||
|
||||
const body = issue.body || '';
|
||||
const title = issue.title || '';
|
||||
const title = issue.title.toLowerCase();
|
||||
|
||||
// Better categorization
|
||||
const labelsToAdd: string[] = [];
|
||||
if (title.includes('bug') || body.toLowerCase().includes('expected behavior')) {
|
||||
labelsToAdd.push('type/bug');
|
||||
} else if (title.includes('feature') || title.includes('enhancement') || body.toLowerCase().includes('proposed change')) {
|
||||
labelsToAdd.push('type/feature');
|
||||
}
|
||||
|
||||
// Low quality check
|
||||
if (body.length < 50 || title.length < 10 || !body.includes('###')) {
|
||||
actions.push({
|
||||
number: issue.number,
|
||||
type: 'needs-info',
|
||||
labelsToAdd: ['status/needs-info'],
|
||||
labelsToRemove: ['status/need-triage'],
|
||||
comment: `Hi @${issue.author?.login || 'author'}! Thank you for the report. This issue seems to be missing some critical information or doesn't follow the template. Could you please provide more details? Labeling as 'status/needs-info' for now.`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Potential duplicate check (very naive but better than nothing)
|
||||
if (title.toLowerCase().includes('duplicate') || title.toLowerCase().includes('same as #')) {
|
||||
actions.push({
|
||||
number: issue.number,
|
||||
type: 'possible-duplicate',
|
||||
comment: `Hi @${issue.author?.login || 'author'}! This issue might be a duplicate of another existing issue. Labeling as 'status/possible-duplicate' for maintainer review.`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assign to a maintainer (round-robin)
|
||||
const assignee = MAINTAINERS[maintainerIndex % MAINTAINERS.length];
|
||||
maintainerIndex++;
|
||||
// Assign to the maintainer with the lowest workload
|
||||
const assignee = sortedMaintainers[mIndex % sortedMaintainers.length];
|
||||
mIndex++;
|
||||
// Increment local workload tracker to keep distribution even during this run
|
||||
WORKLOAD[assignee] = (WORKLOAD[assignee] || 0) + 1;
|
||||
|
||||
actions.push({
|
||||
number: issue.number,
|
||||
type: 'assign',
|
||||
assignee,
|
||||
comment: `Automated Triage: Assigning to @${assignee} for initial review. Please categorize and set priority.`
|
||||
labelsToAdd: [...labelsToAdd, 'status/manual-triage'],
|
||||
labelsToRemove: ['status/need-triage'],
|
||||
comment: `Automated Triage: Assigning to @${assignee} based on current workload. Please categorize and set priority.`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,19 +102,22 @@ async function run() {
|
||||
|
||||
for (const action of actions) {
|
||||
try {
|
||||
if (action.type === 'needs-info') {
|
||||
execGh(`issue edit ${action.number} --add-label "status/needs-info" --remove-label "status/need-triage"`, EXECUTE_ACTIONS);
|
||||
simulationUpdates.set(action.number.toString(), { labels: 'status/needs-info' });
|
||||
} else if (action.type === 'possible-duplicate') {
|
||||
execGh(`issue edit ${action.number} --add-label "status/possible-duplicate" --remove-label "status/need-triage"`, EXECUTE_ACTIONS);
|
||||
simulationUpdates.set(action.number.toString(), { labels: 'status/possible-duplicate' });
|
||||
} else if (action.type === 'assign') {
|
||||
execGh(`issue edit ${action.number} --add-assignee "${action.assignee}" --remove-label "status/need-triage" --add-label "status/manual-triage"`, EXECUTE_ACTIONS);
|
||||
simulationUpdates.set(action.number.toString(), { labels: 'status/manual-triage' });
|
||||
}
|
||||
const addLabels = action.labelsToAdd?.map(l => `"${l}"`).join(',') || '';
|
||||
const removeLabels = action.labelsToRemove?.map(l => `"${l}"`).join(',') || '';
|
||||
|
||||
let editCmd = `issue edit ${action.number}`;
|
||||
if (addLabels) editCmd += ` --add-label ${addLabels}`;
|
||||
if (removeLabels) editCmd += ` --remove-label ${removeLabels}`;
|
||||
if (action.assignee) editCmd += ` --add-assignee "${action.assignee}"`;
|
||||
|
||||
await execGh(editCmd, EXECUTE_ACTIONS);
|
||||
simulationUpdates.set(action.number.toString(), {
|
||||
labels: action.labelsToAdd?.join(', ') || '',
|
||||
assignee: action.assignee || ''
|
||||
});
|
||||
|
||||
if (action.comment) {
|
||||
execGh(`issue comment ${action.number} --body "${action.comment}"`, EXECUTE_ACTIONS);
|
||||
await execGh(`issue comment ${action.number} --body "${action.comment}"`, EXECUTE_ACTIONS);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to process issue #${action.number}:`, err);
|
||||
|
||||
@@ -36,14 +36,17 @@ export async function getMaintainers(): Promise<string[]> {
|
||||
|
||||
const lines = content.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('#') || !line.trim()) continue;
|
||||
const cleanLine = line.split('#')[0].trim();
|
||||
if (!cleanLine) continue;
|
||||
|
||||
// Match @user or @org/team
|
||||
const matches = line.match(/@[\w-]+\/[\w-]+|@[\w-]+/g);
|
||||
const matches = cleanLine.match(/@[\w-]+\/[\w-]+|@[\w-]+/g);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
if (match.includes('/')) {
|
||||
// For team mentions, we might want to expand them,
|
||||
// but for now let's just skip them or handle them if needed.
|
||||
// For teams, we should ideally expand them via gh api,
|
||||
// but for simulation/simple use, we'll just log it.
|
||||
console.log(`[INFO] Found team ownership: ${match}. Skipping team expansion for now.`);
|
||||
continue;
|
||||
}
|
||||
maintainers.add(match.replace('@', ''));
|
||||
@@ -52,6 +55,7 @@ export async function getMaintainers(): Promise<string[]> {
|
||||
}
|
||||
|
||||
if (maintainers.size === 0) {
|
||||
console.warn('No maintainers found in CODEOWNERS, using fallbacks.');
|
||||
return ['gundermanc', 'jackwotherspoon', 'DavidAPierce'];
|
||||
}
|
||||
|
||||
@@ -126,7 +130,14 @@ export async function updateSimulationCsv(
|
||||
for (const [colName, newValue] of Object.entries(update)) {
|
||||
const colIndex = header.indexOf(colName);
|
||||
if (colIndex !== -1) {
|
||||
columns[colIndex] = newValue;
|
||||
if (colName === 'labels') {
|
||||
const existingLabels = columns[colIndex].replace(/"/g, '').split(',').map(s => s.trim()).filter(Boolean);
|
||||
const newLabels = newValue.split(',').map(s => s.trim()).filter(Boolean);
|
||||
const combined = Array.from(new Set([...existingLabels, ...newLabels]));
|
||||
columns[colIndex] = `"${combined.join(', ')}"`;
|
||||
} else {
|
||||
columns[colIndex] = newValue.includes(',') ? `"${newValue}"` : newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,10 +147,37 @@ export async function updateSimulationCsv(
|
||||
await fs.writeFile(filePath, newLines.join('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads maintainer workload from maintainer_workload.csv.
|
||||
*/
|
||||
export async function getMaintainerWorkload(): Promise<Record<string, number>> {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), 'maintainer_workload.csv');
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const workload: Record<string, number> = {};
|
||||
|
||||
const lines = content.split('\n');
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line || line.startsWith('TOTAL') || line.startsWith('RATIO')) continue;
|
||||
|
||||
const columns = line.split(',');
|
||||
const maintainer = columns[0];
|
||||
const assignedIssues = parseInt(columns[1] || '0', 10);
|
||||
const assignedPrs = parseInt(columns[2] || '0', 10);
|
||||
workload[maintainer] = assignedIssues + assignedPrs;
|
||||
}
|
||||
return workload;
|
||||
} catch (err) {
|
||||
console.warn('maintainer_workload.csv not found or unreadable, returning empty workload.');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a gh command with logging and dry-run support.
|
||||
*/
|
||||
export function execGh(command: string, execute: boolean) {
|
||||
export async function execGh(command: string, execute: boolean) {
|
||||
if (!execute) {
|
||||
console.log(`[DRY RUN] Would execute: gh ${command}`);
|
||||
return;
|
||||
@@ -147,8 +185,11 @@ export function execGh(command: string, execute: boolean) {
|
||||
|
||||
console.log(`Executing: gh ${command}`);
|
||||
try {
|
||||
// Small delay to be nicer to the API and avoid race conditions if run concurrently
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
execSync(`gh ${command}`, { stdio: 'inherit' });
|
||||
} catch (err) {
|
||||
console.error(`Failed to execute gh ${command}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user