mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-16 16:36:53 +08:00
scripts/github-coordination.js:
- parseArgs: replace 13-entry if/else chain with BOOL_FLAGS/VALUE_FLAGS
lookup maps; shrinks from 119 to ~45 lines
- Extract dispatchCommand(options, ctx) and formatOutput(payload, options)
from main(); main() shrinks to ~20 lines
scripts/lib/github-coordination.js:
- Split 1041-line monolith into 6 focused sub-modules under
scripts/lib/github-coordination/ (policy, parsing, gh-api, state,
actions, store); index becomes a thin re-export (~55 lines)
- Document ECC_GH_SHIM trust boundary in runGh() (gh-api.js)
- Document applyClaim() read→check→write race condition (actions.js)
tests/lib/github-coordination.test.js:
- Refactor runTests() to data-driven DESCRIPTORS array + runGroup()
helper; runTests() shrinks to ~10 lines
- Add 5 new edge-case tests: normalizeRepo('') and normalizeRepo(' ')
throw, desiredLabelsForState for blocked/ready statuses, and
buildIssueStateFromAction for validate action (15 → 20 tests)
tests/scripts/github-coordination.test.js:
- Replace console.log in test runner with process.stdout.write
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
176 lines
4.3 KiB
JavaScript
176 lines
4.3 KiB
JavaScript
'use strict';
|
|
|
|
const { spawnSync } = require('child_process');
|
|
|
|
function normalizeRepo(repo) {
|
|
const parts = String(repo || '').split('/').filter(Boolean);
|
|
if (parts.length !== 2) {
|
|
throw new Error(`Invalid repo format: "${repo}". Expected "owner/repo".`);
|
|
}
|
|
const [owner, name] = parts;
|
|
return { owner, name };
|
|
}
|
|
|
|
function normalizeIssueNumber(value) {
|
|
const parsed = Number.parseInt(String(value), 10);
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
throw new Error(`Invalid issue number: ${value}`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function normalizeLabelValue(label) {
|
|
if (typeof label === 'string') {
|
|
return label.trim();
|
|
}
|
|
if (label && typeof label === 'object') {
|
|
return String(label.name || label.label || '').trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function normalizeLabels(labels) {
|
|
return Array.from(new Set((Array.isArray(labels) ? labels : []).map(normalizeLabelValue).filter(Boolean))).sort();
|
|
}
|
|
|
|
function runCommand(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: options.cwd,
|
|
env: options.env || process.env,
|
|
encoding: 'utf8',
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
});
|
|
|
|
if (result.error) {
|
|
throw new Error(`${command} ${args.join(' ')} failed: ${result.error.message}`);
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(`${command} ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
|
}
|
|
|
|
return result.stdout || '';
|
|
}
|
|
|
|
// ECC_GH_SHIM creates a trust boundary: when set, shimPath replaces the real
|
|
// `gh` binary and command/commandArgs execute an arbitrary script via
|
|
// process.execPath. This variable MUST only be set in trusted, isolated test
|
|
// environments (e.g., a test's own temp directory). Never set ECC_GH_SHIM in
|
|
// production — doing so allows arbitrary script execution under the caller's
|
|
// privileges.
|
|
function runGh(args, options = {}) {
|
|
const shimPath = process.env.ECC_GH_SHIM;
|
|
const command = shimPath ? process.execPath : 'gh';
|
|
const commandArgs = shimPath ? [shimPath, ...args] : args;
|
|
const env = { ...process.env };
|
|
|
|
if (!options.useEnvGithubToken) {
|
|
delete env.GITHUB_TOKEN;
|
|
}
|
|
|
|
return runCommand(command, commandArgs, { cwd: options.cwd, env });
|
|
}
|
|
|
|
function runGhJson(args, options = {}) {
|
|
try {
|
|
return JSON.parse(runGh(args, options) || 'null');
|
|
} catch (error) {
|
|
throw new Error(`gh ${args.join(' ')} returned invalid JSON: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function getIssue(repo, issueNumber, options = {}) {
|
|
const { owner, name } = normalizeRepo(repo);
|
|
const json = runGhJson([
|
|
'issue',
|
|
'view',
|
|
String(issueNumber),
|
|
'--repo',
|
|
`${owner}/${name}`,
|
|
'--json',
|
|
'number,title,body,url,state,labels,author,updatedAt,assignees',
|
|
], options);
|
|
|
|
if (!json) {
|
|
throw new Error(`Unable to load issue #${issueNumber} from ${repo}`);
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
function listIssues(repo, options = {}) {
|
|
const { owner, name } = normalizeRepo(repo);
|
|
const limit = Number.isFinite(options.limit) ? options.limit : 100;
|
|
const state = options.state || 'all';
|
|
return runGhJson([
|
|
'issue',
|
|
'list',
|
|
'--repo',
|
|
`${owner}/${name}`,
|
|
'--state',
|
|
state,
|
|
'--limit',
|
|
String(limit),
|
|
'--json',
|
|
'number,title,body,url,state,labels,author,updatedAt,assignees',
|
|
], options) || [];
|
|
}
|
|
|
|
function editIssue(repo, issueNumber, options = {}) {
|
|
const { owner, name } = normalizeRepo(repo);
|
|
const args = [
|
|
'issue',
|
|
'edit',
|
|
String(issueNumber),
|
|
'--repo',
|
|
`${owner}/${name}`,
|
|
];
|
|
|
|
if (options.body !== undefined) {
|
|
args.push('--body', options.body);
|
|
}
|
|
|
|
for (const label of options.addLabels || []) {
|
|
args.push('--add-label', label);
|
|
}
|
|
|
|
for (const label of options.removeLabels || []) {
|
|
args.push('--remove-label', label);
|
|
}
|
|
|
|
if (options.title) {
|
|
args.push('--title', options.title);
|
|
}
|
|
|
|
if (options.assignee) {
|
|
args.push('--add-assignee', options.assignee);
|
|
}
|
|
|
|
return runGh(args, options);
|
|
}
|
|
|
|
function commentIssue(repo, issueNumber, body, options = {}) {
|
|
const { owner, name } = normalizeRepo(repo);
|
|
return runGh([
|
|
'issue',
|
|
'comment',
|
|
String(issueNumber),
|
|
'--repo',
|
|
`${owner}/${name}`,
|
|
'--body',
|
|
body,
|
|
], options);
|
|
}
|
|
|
|
module.exports = {
|
|
commentIssue,
|
|
editIssue,
|
|
getIssue,
|
|
listIssues,
|
|
normalizeIssueNumber,
|
|
normalizeLabels,
|
|
normalizeRepo,
|
|
runGh,
|
|
runGhJson,
|
|
};
|