@magnusekdahl/parallix 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
package/lib/tools/forgejo.js
CHANGED
|
@@ -18,9 +18,11 @@ const derivedRepoCache = new Map();
|
|
|
18
18
|
function codexSandboxHint() {
|
|
19
19
|
return 'Codex runtime cannot reach local Forgejo from Node subprocesses. Use the repo-local Codex config/profile that allows the workflow network path.';
|
|
20
20
|
}
|
|
21
|
+
/** @param {string} rootDir @param {string} remoteName @returns {string} */
|
|
21
22
|
function cacheKey(rootDir, remoteName) {
|
|
22
23
|
return `${rootDir}::${remoteName}`;
|
|
23
24
|
}
|
|
25
|
+
/** @param {string} rootDir @param {string} remoteName @returns {string|null} */
|
|
24
26
|
function deriveRepoFromGitRemote(rootDir, remoteName) {
|
|
25
27
|
if (derivedRepoCache.has(cacheKey(rootDir, remoteName))) {
|
|
26
28
|
return derivedRepoCache.get(cacheKey(rootDir, remoteName));
|
|
@@ -46,12 +48,13 @@ function deriveRepoFromGitRemote(rootDir, remoteName) {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/** @param {string} [explicitUser] @returns {string} */
|
|
49
52
|
function resolveForgejoUser(explicitUser) {
|
|
50
53
|
return explicitUser || process.env.FORGEJO_USER || DEFAULT_FORGEJO_USER;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
function resolveForgejoHome() {
|
|
54
|
-
if (process.env.FORGEJO_HOME) return process.env.FORGEJO_HOME;
|
|
57
|
+
if (process.env.FORGEJO_HOME) {return process.env.FORGEJO_HOME;}
|
|
55
58
|
const directLocal = path.join(process.cwd(), '.forgejo-local');
|
|
56
59
|
|
|
57
60
|
// In test environments, we MUST NOT fall back to the real Forgejo home.
|
|
@@ -83,8 +86,9 @@ function resolveForgejoHome() {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
/** @param {string} targetPath @returns {string|null} */
|
|
86
90
|
function normalizePathForComparison(targetPath) {
|
|
87
|
-
if (!targetPath) return null;
|
|
91
|
+
if (!targetPath) {return null;}
|
|
88
92
|
try {
|
|
89
93
|
return fs.realpathSync.native(targetPath);
|
|
90
94
|
} catch (_) {
|
|
@@ -92,11 +96,16 @@ function normalizePathForComparison(targetPath) {
|
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} targetPath
|
|
101
|
+
* @param {{forgejoHome?: string}} [options]
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
95
104
|
function isForgejoPath(targetPath, options = {}) {
|
|
96
105
|
const forgejoHome = options.forgejoHome || resolveForgejoHome();
|
|
97
106
|
const normalizedTarget = normalizePathForComparison(targetPath);
|
|
98
107
|
const normalizedForgejoHome = normalizePathForComparison(forgejoHome);
|
|
99
|
-
if (!normalizedTarget || !normalizedForgejoHome) return false;
|
|
108
|
+
if (!normalizedTarget || !normalizedForgejoHome) {return false;}
|
|
100
109
|
return normalizedTarget === normalizedForgejoHome || normalizedTarget.startsWith(normalizedForgejoHome + path.sep);
|
|
101
110
|
}
|
|
102
111
|
|
|
@@ -109,12 +118,21 @@ function resolveForgejoSettings(rootDir = process.cwd()) {
|
|
|
109
118
|
};
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
/**
|
|
122
|
+
* @param {{forgejoUser?: string, token?: string}} options
|
|
123
|
+
* @returns {{forgejoUser: string, token: string|null}}
|
|
124
|
+
*/
|
|
112
125
|
function resolveForgejoAuth(options = {}) {
|
|
113
126
|
const forgejoUser = resolveForgejoUser(options.forgejoUser);
|
|
114
127
|
const token = options.token || readToken(forgejoUser);
|
|
115
128
|
return { forgejoUser, token };
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} branch
|
|
133
|
+
* @param {{status?: number, statusCode?: number}} [apiErr]
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
118
136
|
function formatPrLookupFailure(branch, apiErr = {}) {
|
|
119
137
|
const sandboxNote = apiErr.status === 7 ? ` (${codexSandboxHint()})` : '';
|
|
120
138
|
if (apiErr.statusCode === 401 || apiErr.statusCode === 403) {
|
|
@@ -126,7 +144,14 @@ function formatPrLookupFailure(branch, apiErr = {}) {
|
|
|
126
144
|
return `failed to resolve PR for ${branch}${sandboxNote}`;
|
|
127
145
|
}
|
|
128
146
|
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} branch
|
|
149
|
+
* @param {string} [rootDir]
|
|
150
|
+
* @param {{forgejoUser?: string, token?: string, apiCall?: Function}} [options]
|
|
151
|
+
* @returns {{exists: boolean, error?: string, raw: string, number?: number, title?: string, state?: string, merged?: boolean, url?: string}}
|
|
152
|
+
*/
|
|
129
153
|
function getPrStatus(branch, rootDir = process.cwd(), options = {}) {
|
|
154
|
+
/** @type {{forgejoUser?: string, token?: string, apiCall?: Function}} */
|
|
130
155
|
const {
|
|
131
156
|
forgejoUser,
|
|
132
157
|
token: providedToken,
|
|
@@ -198,7 +223,7 @@ function resolveTokenFile(user) {
|
|
|
198
223
|
];
|
|
199
224
|
|
|
200
225
|
for (const candidate of candidates) {
|
|
201
|
-
if (candidate && fs.existsSync(candidate)) return candidate;
|
|
226
|
+
if (candidate && fs.existsSync(candidate)) {return candidate;}
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
return null;
|
|
@@ -216,7 +241,7 @@ function readToken(user) {
|
|
|
216
241
|
return process.env.FORGEJO_TOKEN;
|
|
217
242
|
}
|
|
218
243
|
const tokenFile = resolveTokenFile(resolvedUser);
|
|
219
|
-
if (!tokenFile) return null;
|
|
244
|
+
if (!tokenFile) {return null;}
|
|
220
245
|
return fs.readFileSync(tokenFile, 'utf8').trim();
|
|
221
246
|
}
|
|
222
247
|
|
|
@@ -227,7 +252,15 @@ function readToken(user) {
|
|
|
227
252
|
* @param {string} apiPath - Path relative to /api/v1/repos/<repo>
|
|
228
253
|
* @param {string} token - Forgejo PAT
|
|
229
254
|
* @param {object} [body] - Optional JSON body
|
|
230
|
-
* @returns {{ ok: boolean, data: any, status: number|null }}
|
|
255
|
+
* @returns {{ ok: boolean, data: any, status: number|null, statusCode: number|null, stderr: string|null, error: string|null }}
|
|
256
|
+
*/
|
|
257
|
+
/**
|
|
258
|
+
* @param {string} method
|
|
259
|
+
* @param {string} apiPath
|
|
260
|
+
* @param {string} token
|
|
261
|
+
* @param {object} [body]
|
|
262
|
+
* @param {{rootDir?: string}} [options]
|
|
263
|
+
* @returns {{ok: boolean, data: any, status: number|null, statusCode: number|null, stderr: string|null, error: string|null}}
|
|
231
264
|
*/
|
|
232
265
|
function forgejoApi(method, apiPath, token, body, options = {}) {
|
|
233
266
|
const { rootDir = process.cwd() } = options;
|
|
@@ -280,11 +313,22 @@ function forgejoApi(method, apiPath, token, body, options = {}) {
|
|
|
280
313
|
ok: statusCode >= 200 && statusCode < 300,
|
|
281
314
|
data,
|
|
282
315
|
status: result.status,
|
|
283
|
-
statusCode
|
|
316
|
+
statusCode,
|
|
317
|
+
stderr: result.stderr || null,
|
|
318
|
+
error: null
|
|
284
319
|
};
|
|
285
320
|
}
|
|
286
321
|
|
|
287
|
-
|
|
322
|
+
/**
|
|
323
|
+
* @param {string} method
|
|
324
|
+
* @param {string} apiPath
|
|
325
|
+
* @param {string} token
|
|
326
|
+
* @param {object} [body]
|
|
327
|
+
* @param {{rootDir?: string, timeout?: number}} [options]
|
|
328
|
+
* @returns {Promise<{ok: boolean, data: any, status: number|null, statusCode: number|null, stderr: string|null, error: string|null}>}
|
|
329
|
+
*/
|
|
330
|
+
async function forgejoApiAsync(method, apiPath, token, body, options = {}) {
|
|
331
|
+
/** @type {{rootDir?: string, timeout?: number}} */
|
|
288
332
|
const {
|
|
289
333
|
rootDir = process.cwd(),
|
|
290
334
|
timeout = HTTP_REQUEST_TIMEOUT
|
|
@@ -297,8 +341,8 @@ function forgejoApiAsync(method, apiPath, token, body, options = {}) {
|
|
|
297
341
|
|
|
298
342
|
return new Promise((resolve) => {
|
|
299
343
|
let settled = false;
|
|
300
|
-
const finish = (result) => {
|
|
301
|
-
if (settled) return;
|
|
344
|
+
const finish = /** @param {{ok: boolean, data: any, status: number|null, statusCode: number|null, stderr: string|null, error: string|null}} result */ (result) => {
|
|
345
|
+
if (settled) {return;}
|
|
302
346
|
settled = true;
|
|
303
347
|
resolve(result);
|
|
304
348
|
};
|
|
@@ -311,10 +355,10 @@ function forgejoApiAsync(method, apiPath, token, body, options = {}) {
|
|
|
311
355
|
'Content-Type': 'application/json',
|
|
312
356
|
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {})
|
|
313
357
|
}
|
|
314
|
-
}, (res) => {
|
|
358
|
+
}, /** @param {import('http').IncomingMessage} res */ (res) => {
|
|
315
359
|
let responseBody = '';
|
|
316
360
|
res.setEncoding('utf8');
|
|
317
|
-
res.on('data', chunk => {
|
|
361
|
+
res.on('data', /** @param {string|Buffer} chunk */ (chunk) => {
|
|
318
362
|
responseBody += chunk;
|
|
319
363
|
});
|
|
320
364
|
res.on('end', () => {
|
|
@@ -327,22 +371,24 @@ function forgejoApiAsync(method, apiPath, token, body, options = {}) {
|
|
|
327
371
|
}
|
|
328
372
|
}
|
|
329
373
|
finish({
|
|
330
|
-
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
374
|
+
ok: (res.statusCode || 0) >= 200 && (res.statusCode || 0) < 300,
|
|
331
375
|
data,
|
|
332
376
|
status: 0,
|
|
333
|
-
statusCode: res.statusCode
|
|
377
|
+
statusCode: res.statusCode ?? null,
|
|
378
|
+
stderr: null,
|
|
379
|
+
error: null
|
|
334
380
|
});
|
|
335
381
|
});
|
|
336
382
|
});
|
|
337
383
|
|
|
338
|
-
req.on('error', (error) => {
|
|
384
|
+
req.on('error', /** @param {{code?: string, message?: string}} error */ (error) => {
|
|
339
385
|
finish({
|
|
340
386
|
ok: false,
|
|
341
387
|
data: null,
|
|
342
388
|
status: null,
|
|
343
389
|
statusCode: null,
|
|
344
|
-
stderr: error.message,
|
|
345
|
-
error: ['ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH'].includes(error.code) ? codexSandboxHint() : null
|
|
390
|
+
stderr: error.message || null,
|
|
391
|
+
error: ['ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH'].includes(error.code || '') ? codexSandboxHint() : null
|
|
346
392
|
});
|
|
347
393
|
});
|
|
348
394
|
|
|
@@ -375,6 +421,13 @@ function forgejoApiAsync(method, apiPath, token, body, options = {}) {
|
|
|
375
421
|
* @param {object} [options]
|
|
376
422
|
* @returns {{ ok: boolean, url: string|null, error: string|null }}
|
|
377
423
|
*/
|
|
424
|
+
/**
|
|
425
|
+
* @param {string} branch
|
|
426
|
+
* @param {string} user
|
|
427
|
+
* @param {string} token
|
|
428
|
+
* @param {{rootDir?: string, apiCall?: Function, log?: Function, force?: boolean, forceWithLease?: boolean, verificationArea?: string, captureVerifiedTreeProofFn?: Function, assertVerifiedTreeProofFn?: Function}} [options]
|
|
429
|
+
* @returns {{ok: boolean, url?: string|null, error?: string|null, prNumber?: number}}
|
|
430
|
+
*/
|
|
378
431
|
function createPr(branch, user, token, options = {}) {
|
|
379
432
|
const {
|
|
380
433
|
rootDir = process.cwd(),
|
|
@@ -382,7 +435,6 @@ function createPr(branch, user, token, options = {}) {
|
|
|
382
435
|
log = fmt.log.info,
|
|
383
436
|
force = false,
|
|
384
437
|
forceWithLease = false,
|
|
385
|
-
gitFetch = fetchReviewBranch,
|
|
386
438
|
verificationArea = null,
|
|
387
439
|
captureVerifiedTreeProofFn = verification.captureVerifiedTreeProof,
|
|
388
440
|
assertVerifiedTreeProofFn = verification.assertVerifiedTreeProof
|
|
@@ -394,7 +446,7 @@ function createPr(branch, user, token, options = {}) {
|
|
|
394
446
|
} catch (_) {
|
|
395
447
|
primaryBranch = 'main';
|
|
396
448
|
}
|
|
397
|
-
if (branch === primaryBranch) return { ok: false, error: `cannot create a PR from ${primaryBranch}` };
|
|
449
|
+
if (branch === primaryBranch) {return { ok: false, error: `cannot create a PR from ${primaryBranch}` };}
|
|
398
450
|
|
|
399
451
|
// Resolve the PR base: for feature-branch missions the PR targets the recorded
|
|
400
452
|
// base branch; for legacy missions it falls back to the primary branch so
|
|
@@ -420,7 +472,7 @@ function createPr(branch, user, token, options = {}) {
|
|
|
420
472
|
const apiUser = user;
|
|
421
473
|
const apiToken = token;
|
|
422
474
|
const resolvedVerificationArea = verificationArea || verification.resolveVerificationAdapter(rootDir).defaultArea;
|
|
423
|
-
const authenticatedGitFetch = (branchName, dir, options = {}) => fetchReviewBranch(branchName, dir, {
|
|
475
|
+
const authenticatedGitFetch = /** @param {string} branchName @param {string} dir @param {{user?: string, token?: string}} [options] */ (branchName, dir, options = {}) => fetchReviewBranch(branchName, dir, {
|
|
424
476
|
...options,
|
|
425
477
|
user: gitUser,
|
|
426
478
|
token: gitToken,
|
|
@@ -463,13 +515,13 @@ function createPr(branch, user, token, options = {}) {
|
|
|
463
515
|
if (!pushArgsResult.ok) {
|
|
464
516
|
return { ok: false, error: pushArgsResult.error };
|
|
465
517
|
}
|
|
466
|
-
let pushArgs = pushArgsResult.pushArgs;
|
|
518
|
+
let pushArgs = pushArgsResult.pushArgs || [];
|
|
467
519
|
let pushResult = git.git(pushArgs, { stdio: ['ignore', 'pipe', 'pipe'], env: cLocaleEnv() });
|
|
468
|
-
if (pushResult.stdout) process.stdout.write(pushResult.stdout);
|
|
469
|
-
if (pushResult.stderr) process.stderr.write(pushResult.stderr);
|
|
520
|
+
if (pushResult.stdout) {process.stdout.write(pushResult.stdout);}
|
|
521
|
+
if (pushResult.stderr) {process.stderr.write(pushResult.stderr);}
|
|
470
522
|
|
|
471
523
|
if (pushResult.status !== 0) {
|
|
472
|
-
if (forceWithLease && isStaleInfoPushRejection(pushResult)) {
|
|
524
|
+
if (forceWithLease && isStaleInfoPushRejection(/** @type {{status?: number, stderr?: string, stdout?: string}} */ (pushResult))) {
|
|
473
525
|
log(`Stale push rejection for ${branch}; fetching and retrying...`);
|
|
474
526
|
pushArgsResult = buildCreatePrPushArgs(branch, remoteUrl, rootDir, {
|
|
475
527
|
force,
|
|
@@ -480,10 +532,10 @@ function createPr(branch, user, token, options = {}) {
|
|
|
480
532
|
if (!pushArgsResult.ok) {
|
|
481
533
|
return { ok: false, error: pushArgsResult.error };
|
|
482
534
|
}
|
|
483
|
-
pushArgs = pushArgsResult.pushArgs;
|
|
535
|
+
pushArgs = pushArgsResult.pushArgs || [];
|
|
484
536
|
pushResult = git.git(pushArgs, { stdio: ['ignore', 'pipe', 'pipe'], env: cLocaleEnv() });
|
|
485
|
-
if (pushResult.stdout) process.stdout.write(pushResult.stdout);
|
|
486
|
-
if (pushResult.stderr) process.stderr.write(pushResult.stderr);
|
|
537
|
+
if (pushResult.stdout) {process.stdout.write(pushResult.stdout);}
|
|
538
|
+
if (pushResult.stderr) {process.stderr.write(pushResult.stderr);}
|
|
487
539
|
}
|
|
488
540
|
if (pushResult.status !== 0) {
|
|
489
541
|
const pushError = (pushResult.stderr || pushResult.stdout || '').trim();
|
|
@@ -494,9 +546,9 @@ function createPr(branch, user, token, options = {}) {
|
|
|
494
546
|
// 3. Check if an OPEN PR already exists for this branch
|
|
495
547
|
const existingPrLookup = resolvePrAccess(branch, apiToken, { apiCall, slug, onlyOpen: true, forgejoUser: apiUser, rootDir });
|
|
496
548
|
|
|
497
|
-
if (isApiErrorResult(existingPrLookup)) {
|
|
498
|
-
const apiErr = existingPrLookup._apiError;
|
|
499
|
-
return { ok: false, error: `failed to check existing PR: ${apiErr.error || 'API error'}${apiErr.status === 7 ? ` (${codexSandboxHint()})` : ''}` };
|
|
549
|
+
if (existingPrLookup && isApiErrorResult(existingPrLookup)) {
|
|
550
|
+
const apiErr = /** @type {{error?: string, status?: number}} */ (existingPrLookup._apiError || {});
|
|
551
|
+
return { ok: false, error: `failed to check existing PR: ${(apiErr.error || 'API error')}${apiErr.status === 7 ? ` (${codexSandboxHint()})` : ''}` };
|
|
500
552
|
}
|
|
501
553
|
|
|
502
554
|
// 4. Return existing PR if already present
|
|
@@ -526,12 +578,18 @@ function createPr(branch, user, token, options = {}) {
|
|
|
526
578
|
return { ok: true, url: createResult.data.html_url, prNumber: createResult.data.number };
|
|
527
579
|
}
|
|
528
580
|
|
|
581
|
+
/**
|
|
582
|
+
* @param {string} branch
|
|
583
|
+
* @param {string|null} token
|
|
584
|
+
* @param {{apiCall?: Function, slug?: string|null, onlyOpen?: boolean, forgejoUser?: string, rootDir?: string}} [options]
|
|
585
|
+
* @returns {number|null|{_apiError?: object, _notFound?: boolean}}
|
|
586
|
+
*/
|
|
529
587
|
function getPrNumber(branch, token, options = {}) {
|
|
530
588
|
const resolved = resolvePrAccess(branch, token, options);
|
|
531
589
|
if (!resolved || isApiErrorResult(resolved)) {
|
|
532
590
|
return resolved;
|
|
533
591
|
}
|
|
534
|
-
return resolved.prNumber;
|
|
592
|
+
return resolved.prNumber ?? null;
|
|
535
593
|
}
|
|
536
594
|
|
|
537
595
|
/**
|
|
@@ -550,7 +608,14 @@ function getPrNumber(branch, token, options = {}) {
|
|
|
550
608
|
* @param {object} [options]
|
|
551
609
|
* @returns {string|null} The PR author's login, or null if undeterminable.
|
|
552
610
|
*/
|
|
611
|
+
/**
|
|
612
|
+
* @param {string} branch
|
|
613
|
+
* @param {string} token
|
|
614
|
+
* @param {{apiCall?: Function, resolvePrNumber?: Function, forgejoUser?: string, rootDir?: string}} [options]
|
|
615
|
+
* @returns {string|null}
|
|
616
|
+
*/
|
|
553
617
|
function getPrAuthor(branch, token, options = {}) {
|
|
618
|
+
/** @type {{apiCall?: Function, resolvePrNumber?: Function, forgejoUser?: string, rootDir?: string}} */
|
|
554
619
|
const {
|
|
555
620
|
apiCall = forgejoApi,
|
|
556
621
|
resolvePrNumber = getPrNumber,
|
|
@@ -560,13 +625,19 @@ function getPrAuthor(branch, token, options = {}) {
|
|
|
560
625
|
const slugMatch = branch.match(/^mission\/(task-\d+)/);
|
|
561
626
|
const slug = slugMatch ? slugMatch[1] : null;
|
|
562
627
|
const prNumber = resolvePrNumber(branch, token, { apiCall, slug, forgejoUser, rootDir });
|
|
563
|
-
if (isApiErrorResult(prNumber) || !prNumber) return null;
|
|
628
|
+
if (isApiErrorResult(prNumber) || !prNumber) {return null;}
|
|
564
629
|
|
|
565
630
|
const prRes = apiCall('GET', `/pulls/${prNumber}`, token, undefined, { rootDir });
|
|
566
|
-
if (!prRes.ok || !prRes.data || !prRes.data.user) return null;
|
|
631
|
+
if (!prRes.ok || !prRes.data || !prRes.data.user) {return null;}
|
|
567
632
|
return prRes.data.user.login || null;
|
|
568
633
|
}
|
|
569
634
|
|
|
635
|
+
/**
|
|
636
|
+
* @param {string} branch
|
|
637
|
+
* @param {string|null} token
|
|
638
|
+
* @param {{apiCall?: Function, pageSize?: number, maxPages?: number, slug?: string|null, onlyOpen?: boolean, forgejoUser?: string, rootDir?: string, reportNotFound?: boolean}} [options]
|
|
639
|
+
* @returns {{prNumber?: number, token?: string, _apiError?: object, _notFound?: boolean}|null}
|
|
640
|
+
*/
|
|
570
641
|
function resolvePrAccess(branch, token, options = {}) {
|
|
571
642
|
const {
|
|
572
643
|
apiCall = forgejoApi,
|
|
@@ -581,6 +652,7 @@ function resolvePrAccess(branch, token, options = {}) {
|
|
|
581
652
|
let lastApiError = null;
|
|
582
653
|
let sawSuccessfulLookup = false;
|
|
583
654
|
|
|
655
|
+
/** @param {string} state @param {string} t */
|
|
584
656
|
const searchInState = (state, t) => {
|
|
585
657
|
let consecutiveErrors = 0;
|
|
586
658
|
for (let page = 1; page <= maxPages; page += 1) {
|
|
@@ -618,16 +690,17 @@ function resolvePrAccess(branch, token, options = {}) {
|
|
|
618
690
|
return null;
|
|
619
691
|
};
|
|
620
692
|
|
|
693
|
+
/** @param {string} t */
|
|
621
694
|
const doLookup = (t) => {
|
|
622
695
|
const openResult = searchInState('open', t);
|
|
623
|
-
if (openResult) return openResult;
|
|
624
|
-
if (!onlyOpen) return searchInState('all', t);
|
|
696
|
+
if (openResult) {return openResult;}
|
|
697
|
+
if (!onlyOpen) {return searchInState('all', t);}
|
|
625
698
|
return null;
|
|
626
699
|
};
|
|
627
700
|
|
|
628
701
|
// 1. Try with the provided token
|
|
629
|
-
let prNumber = doLookup(token);
|
|
630
|
-
if (prNumber) return { prNumber, token };
|
|
702
|
+
let prNumber = doLookup(token || '');
|
|
703
|
+
if (prNumber) {return { prNumber, token: token || undefined };}
|
|
631
704
|
|
|
632
705
|
const currentUser = resolveForgejoUser(forgejoUser);
|
|
633
706
|
const triedUsers = [currentUser];
|
|
@@ -645,7 +718,7 @@ function resolvePrAccess(branch, token, options = {}) {
|
|
|
645
718
|
const fallbackToken = readToken(user);
|
|
646
719
|
if (fallbackToken) {
|
|
647
720
|
prNumber = doLookup(fallbackToken);
|
|
648
|
-
if (prNumber) return { prNumber, token: fallbackToken };
|
|
721
|
+
if (prNumber) {return { prNumber, token: fallbackToken };}
|
|
649
722
|
}
|
|
650
723
|
}
|
|
651
724
|
}
|
|
@@ -656,9 +729,11 @@ function resolvePrAccess(branch, token, options = {}) {
|
|
|
656
729
|
const settings = resolveForgejoSettings(rootDir);
|
|
657
730
|
fmt.log.info(`Current environment: FORGEJO_URL=${settings.url}, FORGEJO_REPO=${settings.repo}, FORGEJO_HOME=${resolveForgejoHome()}`);
|
|
658
731
|
if (lastApiError) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
732
|
+
/** @type {{status?: number, error?: string, stderr?: string|null}} */
|
|
733
|
+
const err = lastApiError;
|
|
734
|
+
fmt.log.warn(`API error encountered during lookup: status=${err.status || 0}, error=${err.error || 'unknown'}`);
|
|
735
|
+
if (err.stderr) {
|
|
736
|
+
fmt.log.info(`API stderr: ${err.stderr}`);
|
|
662
737
|
}
|
|
663
738
|
}
|
|
664
739
|
fmt.log.info(`curl --version: ${curlCheck.status === 0 ? curlCheck.stdout.split('\n')[0] : 'failed to run curl'}`);
|
|
@@ -673,7 +748,14 @@ function resolvePrAccess(branch, token, options = {}) {
|
|
|
673
748
|
return null;
|
|
674
749
|
}
|
|
675
750
|
|
|
751
|
+
/**
|
|
752
|
+
* @param {string} baseSlug
|
|
753
|
+
* @param {string} token
|
|
754
|
+
* @param {{apiCall?: Function, pageSize?: number, maxPages?: number}} [options]
|
|
755
|
+
* @returns {Array<{number: number, title: string, html_url: string, head: string}>}
|
|
756
|
+
*/
|
|
676
757
|
function listOpenPrsForSlug(baseSlug, token, options = {}) {
|
|
758
|
+
/** @type {{apiCall?: Function, pageSize?: number, maxPages?: number}} */
|
|
677
759
|
const {
|
|
678
760
|
apiCall = forgejoApi,
|
|
679
761
|
pageSize = 50,
|
|
@@ -687,11 +769,11 @@ function listOpenPrsForSlug(baseSlug, token, options = {}) {
|
|
|
687
769
|
const result = apiCall('GET', `/pulls?state=open&page=${page}&limit=${pageSize}&sort=recentupdate&direction=desc`, token);
|
|
688
770
|
if (!result.ok) {
|
|
689
771
|
consecutiveErrors++;
|
|
690
|
-
if (consecutiveErrors >= 3) break;
|
|
772
|
+
if (consecutiveErrors >= 3) {break;}
|
|
691
773
|
continue;
|
|
692
774
|
}
|
|
693
775
|
consecutiveErrors = 0;
|
|
694
|
-
if (!Array.isArray(result.data) || result.data.length === 0) break;
|
|
776
|
+
if (!Array.isArray(result.data) || result.data.length === 0) {break;}
|
|
695
777
|
|
|
696
778
|
for (const pr of result.data) {
|
|
697
779
|
const head = pr.head || {};
|
|
@@ -709,16 +791,23 @@ function listOpenPrsForSlug(baseSlug, token, options = {}) {
|
|
|
709
791
|
}
|
|
710
792
|
}
|
|
711
793
|
|
|
712
|
-
if (result.data.length < pageSize) break;
|
|
794
|
+
if (result.data.length < pageSize) {break;}
|
|
713
795
|
}
|
|
714
796
|
|
|
715
797
|
return prs;
|
|
716
798
|
}
|
|
717
799
|
|
|
800
|
+
/** @param {{_apiError?: object, _notFound?: boolean}} result @returns {boolean} */
|
|
718
801
|
function isApiErrorResult(result) {
|
|
719
|
-
return result && typeof result === 'object' && result._apiError && result._notFound === true;
|
|
802
|
+
return Boolean(result && typeof result === 'object' && result._apiError && result._notFound === true);
|
|
720
803
|
}
|
|
721
804
|
|
|
805
|
+
/**
|
|
806
|
+
* @param {string} user
|
|
807
|
+
* @param {string} token
|
|
808
|
+
* @param {string} rootDir
|
|
809
|
+
* @returns {string}
|
|
810
|
+
*/
|
|
722
811
|
function authenticatedReviewUrl(user, token, rootDir = process.cwd()) {
|
|
723
812
|
const { url: forgejoUrl, repo: forgejoRepo } = resolveForgejoSettings(rootDir);
|
|
724
813
|
const url = new URL(forgejoUrl);
|
|
@@ -728,23 +817,31 @@ function authenticatedReviewUrl(user, token, rootDir = process.cwd()) {
|
|
|
728
817
|
return `${protocol}//${user}:${token}@${host}/${forgejoRepo}.git`;
|
|
729
818
|
}
|
|
730
819
|
|
|
820
|
+
/** @param {string} rootDir @returns {string|null} */
|
|
731
821
|
function reviewRemoteUrl(rootDir = process.cwd()) {
|
|
732
822
|
const { url: forgejoUrl, repo: forgejoRepo } = resolveForgejoSettings(rootDir);
|
|
733
|
-
if (!forgejoUrl || !forgejoRepo) return null;
|
|
823
|
+
if (!forgejoUrl || !forgejoRepo) {return null;}
|
|
734
824
|
const url = new URL(forgejoUrl);
|
|
735
825
|
return `${url.protocol}//${url.host}/${forgejoRepo}.git`;
|
|
736
826
|
}
|
|
737
827
|
|
|
828
|
+
/**
|
|
829
|
+
* @param {string} user
|
|
830
|
+
* @param {string} token
|
|
831
|
+
* @param {string} rootDir
|
|
832
|
+
* @param {{verificationProof?: object|null, assertVerifiedTreeProofFn?: Function, gitRunner?: Function}} [opts]
|
|
833
|
+
* @returns {{ok: boolean, skipped?: boolean, status?: number, stderr?: string, error?: string|null}}
|
|
834
|
+
*/
|
|
738
835
|
function syncPrimaryBaseline(user, token, rootDir = process.cwd(), {
|
|
739
836
|
verificationProof = null,
|
|
740
837
|
assertVerifiedTreeProofFn = verification.assertVerifiedTreeProof,
|
|
741
838
|
gitRunner = git.git
|
|
742
839
|
} = {}) {
|
|
743
|
-
let
|
|
840
|
+
let primaryBranchName = 'main';
|
|
744
841
|
try {
|
|
745
|
-
|
|
842
|
+
primaryBranchName = getPrimaryBranch(rootDir);
|
|
746
843
|
} catch (_) {
|
|
747
|
-
|
|
844
|
+
primaryBranchName = 'main';
|
|
748
845
|
}
|
|
749
846
|
|
|
750
847
|
const proofCheck = assertVerifiedTreeProofFn(verificationProof, rootDir, { gitRunner });
|
|
@@ -752,8 +849,8 @@ function syncPrimaryBaseline(user, token, rootDir = process.cwd(), {
|
|
|
752
849
|
return { ok: false, error: proofCheck.error || 'verification-proof-mismatch' };
|
|
753
850
|
}
|
|
754
851
|
|
|
755
|
-
const primaryExists = gitRunner(['-C', rootDir, 'show-ref', '--verify', '--quiet', `refs/heads/${
|
|
756
|
-
if (primaryExists.status !== 0) return { ok: true, skipped: true };
|
|
852
|
+
const primaryExists = gitRunner(['-C', rootDir, 'show-ref', '--verify', '--quiet', `refs/heads/${primaryBranchName}`]);
|
|
853
|
+
if (primaryExists.status !== 0) {return { ok: true, skipped: true };}
|
|
757
854
|
|
|
758
855
|
const remoteUrl = authenticatedReviewUrl(user, token, rootDir);
|
|
759
856
|
// Force-push: the review remote's primary branch is a server-side mirror of our
|
|
@@ -761,7 +858,7 @@ function syncPrimaryBaseline(user, token, rootDir = process.cwd(), {
|
|
|
761
858
|
// sync. Without --force a diverged remote primary (rebases, amended baseline
|
|
762
859
|
// commits) rejects this as a non-fast-forward, aborting the sync and breaking
|
|
763
860
|
// the review loop. See missions/task-1318 "Required Forgejo fix".
|
|
764
|
-
const result = gitRunner(['-C', rootDir, 'push', '--force', remoteUrl, `${
|
|
861
|
+
const result = gitRunner(['-C', rootDir, 'push', '--force', remoteUrl, `${primaryBranchName}:${primaryBranchName}`], {
|
|
765
862
|
stdio: 'pipe'
|
|
766
863
|
});
|
|
767
864
|
|
|
@@ -773,6 +870,14 @@ function syncPrimaryBaseline(user, token, rootDir = process.cwd(), {
|
|
|
773
870
|
};
|
|
774
871
|
}
|
|
775
872
|
|
|
873
|
+
/**
|
|
874
|
+
* @param {string} baseBranch
|
|
875
|
+
* @param {string} user
|
|
876
|
+
* @param {string} token
|
|
877
|
+
* @param {string} rootDir
|
|
878
|
+
* @param {{gitRunner?: Function}} [opts]
|
|
879
|
+
* @returns {{ok: boolean, status?: number, stderr?: string, error?: string|null}}
|
|
880
|
+
*/
|
|
776
881
|
function ensureRemoteBaseBranch(baseBranch, user, token, rootDir = process.cwd(), {
|
|
777
882
|
gitRunner = git.git
|
|
778
883
|
} = {}) {
|
|
@@ -780,6 +885,7 @@ function ensureRemoteBaseBranch(baseBranch, user, token, rootDir = process.cwd()
|
|
|
780
885
|
// syncPrimaryBaseline only mirrors the primary branch, so the PR base would
|
|
781
886
|
// not exist on the review remote and PR creation would fail server-side.
|
|
782
887
|
// Mirror the local feature base branch here before the PR is opened.
|
|
888
|
+
/** @type {{status: number}} */
|
|
783
889
|
const exists = gitRunner(['-C', rootDir, 'show-ref', '--verify', '--quiet', `refs/heads/${baseBranch}`]);
|
|
784
890
|
if (exists.status !== 0) {
|
|
785
891
|
return { ok: false, error: `PR base branch ${baseBranch} does not exist locally` };
|
|
@@ -802,6 +908,13 @@ function ensureRemoteBaseBranch(baseBranch, user, token, rootDir = process.cwd()
|
|
|
802
908
|
};
|
|
803
909
|
}
|
|
804
910
|
|
|
911
|
+
/**
|
|
912
|
+
* @param {string} sourceRef
|
|
913
|
+
* @param {string} destinationRef
|
|
914
|
+
* @param {string} rootDir
|
|
915
|
+
* @param {{force?: boolean, forceWithLease?: boolean, user?: string, token?: string}} [opts]
|
|
916
|
+
* @returns {*}
|
|
917
|
+
*/
|
|
805
918
|
function pushReviewRef(sourceRef, destinationRef, rootDir = process.cwd(), {
|
|
806
919
|
force = false,
|
|
807
920
|
forceWithLease = false,
|
|
@@ -811,7 +924,7 @@ function pushReviewRef(sourceRef, destinationRef, rootDir = process.cwd(), {
|
|
|
811
924
|
const remote = token
|
|
812
925
|
? authenticatedReviewUrl(user || resolveForgejoUser(), token, rootDir)
|
|
813
926
|
: 'review';
|
|
814
|
-
const pushArgs = ['-C', rootDir, 'push', remote];
|
|
927
|
+
const pushArgs = /** @type {string[]} */ (['-C', rootDir, 'push', remote]);
|
|
815
928
|
if (forceWithLease) {
|
|
816
929
|
pushArgs.push('--force-with-lease');
|
|
817
930
|
} else if (force) {
|
|
@@ -819,11 +932,17 @@ function pushReviewRef(sourceRef, destinationRef, rootDir = process.cwd(), {
|
|
|
819
932
|
}
|
|
820
933
|
pushArgs.push(`${sourceRef}:${destinationRef}`);
|
|
821
934
|
const result = git.git(pushArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
822
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
823
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
935
|
+
if (result.stdout) {process.stdout.write(result.stdout);}
|
|
936
|
+
if (result.stderr) {process.stderr.write(result.stderr);}
|
|
824
937
|
return result;
|
|
825
938
|
}
|
|
826
939
|
|
|
940
|
+
/**
|
|
941
|
+
* @param {string} branch
|
|
942
|
+
* @param {string} rootDir
|
|
943
|
+
* @param {{user?: string, token?: string}} options
|
|
944
|
+
* @returns {*}
|
|
945
|
+
*/
|
|
827
946
|
function fetchReviewBranch(branch, rootDir = process.cwd(), options = {}) {
|
|
828
947
|
const { user, token } = options;
|
|
829
948
|
const source = token
|
|
@@ -839,6 +958,7 @@ function fetchReviewBranch(branch, rootDir = process.cwd(), options = {}) {
|
|
|
839
958
|
});
|
|
840
959
|
}
|
|
841
960
|
|
|
961
|
+
/** @param {string} branch @param {string} rootDir @returns {{ok: boolean, ref?: string, sha?: string, error?: string}} */
|
|
842
962
|
function resolveTrackingBranchSha(branch, rootDir = process.cwd()) {
|
|
843
963
|
const candidateRefs = [`refs/remotes/review/${branch}`, `refs/remotes/origin/${branch}`];
|
|
844
964
|
for (const ref of candidateRefs) {
|
|
@@ -856,7 +976,15 @@ function resolveTrackingBranchSha(branch, rootDir = process.cwd()) {
|
|
|
856
976
|
};
|
|
857
977
|
}
|
|
858
978
|
|
|
979
|
+
/**
|
|
980
|
+
* @param {string} branch
|
|
981
|
+
* @param {string} remoteUrl
|
|
982
|
+
* @param {string} rootDir
|
|
983
|
+
* @param {{force?: boolean, forceWithLease?: boolean, gitFetch?: Function, refreshTrackingRef?: boolean}} [options]
|
|
984
|
+
* @returns {{ok: boolean, pushArgs?: string[], error?: string}}
|
|
985
|
+
*/
|
|
859
986
|
function buildCreatePrPushArgs(branch, remoteUrl, rootDir = process.cwd(), options = {}) {
|
|
987
|
+
/** @type {{force?: boolean, forceWithLease?: boolean, gitFetch?: Function, refreshTrackingRef?: boolean}} */
|
|
860
988
|
const {
|
|
861
989
|
force = false,
|
|
862
990
|
forceWithLease = false,
|
|
@@ -911,6 +1039,12 @@ function buildCreatePrPushArgs(branch, remoteUrl, rootDir = process.cwd(), optio
|
|
|
911
1039
|
return { ok: true, pushArgs };
|
|
912
1040
|
}
|
|
913
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* @param {string} branch
|
|
1044
|
+
* @param {string} rootDir
|
|
1045
|
+
* @param {{user?: string, token?: string}} [opts]
|
|
1046
|
+
* @returns {*}
|
|
1047
|
+
*/
|
|
914
1048
|
function deleteReviewRef(branch, rootDir = process.cwd(), { user, token } = {}) {
|
|
915
1049
|
const remote = token
|
|
916
1050
|
? authenticatedReviewUrl(user || resolveForgejoUser(), token, rootDir)
|
|
@@ -918,17 +1052,19 @@ function deleteReviewRef(branch, rootDir = process.cwd(), { user, token } = {})
|
|
|
918
1052
|
const result = git.git(['-C', rootDir, 'push', remote, '--delete', branch], {
|
|
919
1053
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
920
1054
|
});
|
|
921
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
922
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
1055
|
+
if (result.stdout) {process.stdout.write(result.stdout);}
|
|
1056
|
+
if (result.stderr) {process.stderr.write(result.stderr);}
|
|
923
1057
|
return result;
|
|
924
1058
|
}
|
|
925
1059
|
|
|
1060
|
+
/** @param {string} commit @param {string} rootDir @returns {*} */
|
|
926
1061
|
function verifyCommitExists(commit, rootDir = process.cwd()) {
|
|
927
1062
|
return git.git(['-C', rootDir, 'rev-parse', '--verify', `${commit}^{commit}`], {
|
|
928
1063
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
929
1064
|
});
|
|
930
1065
|
}
|
|
931
1066
|
|
|
1067
|
+
/** @param {string} commit @param {string} [remoteRef] @param {string} rootDir @returns {*} */
|
|
932
1068
|
function remoteRefContainsCommit(commit, remoteRef = `refs/remotes/review/${getPrimaryBranch(process.cwd())}`, rootDir = process.cwd()) {
|
|
933
1069
|
return git.git(['-C', rootDir, 'merge-base', '--is-ancestor', commit, remoteRef], {
|
|
934
1070
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
@@ -939,10 +1075,12 @@ function remoteRefContainsCommit(commit, remoteRef = `refs/remotes/review/${getP
|
|
|
939
1075
|
// are localized to the operator's locale. We parse these messages to decide
|
|
940
1076
|
// control flow, so force a stable C locale on git calls whose stderr we
|
|
941
1077
|
// inspect, keeping detection language-independent.
|
|
1078
|
+
/** @returns {Record<string, string>} */
|
|
942
1079
|
function cLocaleEnv() {
|
|
943
1080
|
return { ...process.env, LC_ALL: 'C', LANG: 'C' };
|
|
944
1081
|
}
|
|
945
1082
|
|
|
1083
|
+
/** @param {{stderr?: string, stdout?: string}} result @returns {string} */
|
|
946
1084
|
function pushOutput(result) {
|
|
947
1085
|
return [result && result.stderr, result && result.stdout]
|
|
948
1086
|
.filter(Boolean)
|
|
@@ -955,12 +1093,14 @@ function pushOutput(result) {
|
|
|
955
1093
|
// "fatal: could not find remote ref refs/heads/<branch>". This must be
|
|
956
1094
|
// distinguished from real fetch failures (auth/network), which also exit
|
|
957
1095
|
// non-zero but do not carry this message and should still abort.
|
|
1096
|
+
/** @param {{stderr?: string, stdout?: string}} result @returns {boolean} */
|
|
958
1097
|
function isMissingRemoteRef(result) {
|
|
959
1098
|
const output = pushOutput(result).toLowerCase();
|
|
960
1099
|
return output.includes('could not find remote ref')
|
|
961
1100
|
|| output.includes("couldn't find remote ref");
|
|
962
1101
|
}
|
|
963
1102
|
|
|
1103
|
+
/** @param {{status?: number, stderr?: string, stdout?: string}} result @returns {boolean} */
|
|
964
1104
|
function isStaleInfoPushRejection(result) {
|
|
965
1105
|
return result && result.status !== 0 && /\bstale info\b|\bstale ref\b|fetch first/i.test(pushOutput(result));
|
|
966
1106
|
}
|
|
@@ -976,6 +1116,14 @@ function isStaleInfoPushRejection(result) {
|
|
|
976
1116
|
* @param {object} [options] - Optional overrides
|
|
977
1117
|
* @returns {{ state: string, submittedAt: string }|null} Most recent eligible review, or null
|
|
978
1118
|
*/
|
|
1119
|
+
/**
|
|
1120
|
+
* @param {string} branch
|
|
1121
|
+
* @param {string} reviewerUser
|
|
1122
|
+
* @param {string} sinceIso
|
|
1123
|
+
* @param {string} token
|
|
1124
|
+
* @param {{apiCall?: Function, forgejoUser?: string, rootDir?: string}} [options]
|
|
1125
|
+
* @returns {{state: string, submittedAt: string}|null}
|
|
1126
|
+
*/
|
|
979
1127
|
function getLatestReview(branch, reviewerUser, sinceIso, token, options = {}) {
|
|
980
1128
|
const {
|
|
981
1129
|
apiCall = forgejoApi,
|
|
@@ -986,27 +1134,33 @@ function getLatestReview(branch, reviewerUser, sinceIso, token, options = {}) {
|
|
|
986
1134
|
const slugMatch = branch.match(/^mission\/(task-\d+)/);
|
|
987
1135
|
const slug = slugMatch ? slugMatch[1] : null;
|
|
988
1136
|
const prAccess = resolvePrAccess(branch, token, { apiCall, slug, forgejoUser, rootDir });
|
|
989
|
-
if (!prAccess || isApiErrorResult(prAccess)) return null;
|
|
1137
|
+
if (!prAccess || isApiErrorResult(prAccess)) {return null;}
|
|
990
1138
|
|
|
991
1139
|
const result = apiCall('GET', `/pulls/${prAccess.prNumber}/reviews`, prAccess.token);
|
|
992
|
-
if (!result.ok || !Array.isArray(result.data)) return null;
|
|
1140
|
+
if (!result.ok || !Array.isArray(result.data)) {return null;}
|
|
993
1141
|
|
|
994
1142
|
const since = new Date(sinceIso).getTime();
|
|
995
1143
|
|
|
996
1144
|
const eligible = result.data
|
|
997
|
-
.filter(r => {
|
|
1145
|
+
.filter(/** @param {{user?: {login?: string}, submitted_at?: string, created_at?: string, state?: string}} r */ r => {
|
|
998
1146
|
const user = (r.user || {}).login;
|
|
999
1147
|
const submittedAt = r.submitted_at || r.created_at || '';
|
|
1000
1148
|
const submitted = submittedAt ? new Date(submittedAt).getTime() : 0;
|
|
1001
1149
|
return user === reviewerUser && submitted >= since;
|
|
1002
1150
|
})
|
|
1003
|
-
.map(r => ({ state: r.state, submittedAt: r.submitted_at || r.created_at || '' }))
|
|
1004
|
-
.sort((a, b) => new Date(a.submittedAt).getTime() - new Date(b.submittedAt).getTime());
|
|
1151
|
+
.map(/** @param {{state?: string, submitted_at?: string, created_at?: string}} r */ r => ({ state: r.state, submittedAt: r.submitted_at || r.created_at || '' }))
|
|
1152
|
+
.sort(/** @param {{submittedAt: string}} a @param {{submittedAt: string}} b */ (a, b) => new Date(a.submittedAt).getTime() - new Date(b.submittedAt).getTime());
|
|
1005
1153
|
|
|
1006
1154
|
return eligible.length > 0 ? eligible[eligible.length - 1] : null;
|
|
1007
1155
|
}
|
|
1008
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* @param {string} branch
|
|
1159
|
+
* @param {{forgejoUser?: string, token?: string, apiCall?: Function, rootDir?: string}} [options]
|
|
1160
|
+
* @returns {{ok: boolean, error?: string, reviewState?: string|null, prNumber?: number, defaultUserApproved?: boolean, raw?: string}}
|
|
1161
|
+
*/
|
|
1009
1162
|
function getLatestReviewDecision(branch, options = {}) {
|
|
1163
|
+
/** @type {{forgejoUser?: string, token?: string, apiCall?: Function, rootDir?: string}} */
|
|
1010
1164
|
const {
|
|
1011
1165
|
forgejoUser,
|
|
1012
1166
|
token: providedToken,
|
|
@@ -1023,13 +1177,13 @@ function getLatestReviewDecision(branch, options = {}) {
|
|
|
1023
1177
|
}
|
|
1024
1178
|
|
|
1025
1179
|
const prAccess = resolvePrAccess(branch, token, { apiCall, slug, forgejoUser, rootDir });
|
|
1026
|
-
if (isApiErrorResult(prAccess)) {
|
|
1027
|
-
const apiErr = prAccess._apiError;
|
|
1180
|
+
if (prAccess && isApiErrorResult(prAccess)) {
|
|
1181
|
+
const apiErr = /** @type {{error?: string, status?: number}} */ (prAccess._apiError || {});
|
|
1028
1182
|
return {
|
|
1029
1183
|
ok: false,
|
|
1030
1184
|
error: 'api-failed',
|
|
1031
1185
|
reviewState: null,
|
|
1032
|
-
raw: `failed to resolve PR for ${branch}${apiErr.status === 7 ? ` (${codexSandboxHint()})` : ''}`
|
|
1186
|
+
raw: /** @type {string|undefined} */ (`failed to resolve PR for ${branch}${apiErr.status === 7 ? ` (${codexSandboxHint()})` : ''}`)
|
|
1033
1187
|
};
|
|
1034
1188
|
}
|
|
1035
1189
|
if (!prAccess) {
|
|
@@ -1046,27 +1200,27 @@ function getLatestReviewDecision(branch, options = {}) {
|
|
|
1046
1200
|
// has approved, not the current CLI session user.
|
|
1047
1201
|
const defaultUserLogin = DEFAULT_FORGEJO_USER;
|
|
1048
1202
|
const reviews = result.data
|
|
1049
|
-
.map(review => ({
|
|
1203
|
+
.map(/** @param {{user?: {login?: string}, state?: string, submitted_at?: string, created_at?: string, dismissed?: boolean}} review */ (review) => ({
|
|
1050
1204
|
user: (review.user || {}).login || '?',
|
|
1051
1205
|
state: review.state || '',
|
|
1052
1206
|
submittedAt: review.submitted_at || review.created_at || '',
|
|
1053
1207
|
dismissed: !!review.dismissed
|
|
1054
1208
|
}))
|
|
1055
|
-
.filter(review => review.state && review.submittedAt && !review.dismissed)
|
|
1056
|
-
.sort((a, b) => new Date(a.submittedAt).getTime() - new Date(b.submittedAt).getTime());
|
|
1209
|
+
.filter(/** @param {{state: string, submittedAt: string, dismissed: boolean}} review */ (review) => review.state && review.submittedAt && !review.dismissed)
|
|
1210
|
+
.sort(/** @param {{submittedAt: string}} a @param {{submittedAt: string}} b */ (a, b) => new Date(a.submittedAt).getTime() - new Date(b.submittedAt).getTime());
|
|
1057
1211
|
|
|
1058
1212
|
if (reviews.length === 0) {
|
|
1059
1213
|
return { ok: true, prNumber, reviewState: null, defaultUserApproved: false };
|
|
1060
1214
|
}
|
|
1061
1215
|
|
|
1062
1216
|
// Find the latest formal decision overall
|
|
1063
|
-
const formalReviews = reviews.filter(r => r.state === 'APPROVED' || r.state === 'REQUEST_CHANGES');
|
|
1217
|
+
const formalReviews = reviews.filter(/** @param {{state: string}} r */ (r) => r.state === 'APPROVED' || r.state === 'REQUEST_CHANGES');
|
|
1064
1218
|
|
|
1065
1219
|
const finalState = formalReviews.length > 0
|
|
1066
1220
|
? formalReviews[formalReviews.length - 1].state
|
|
1067
1221
|
: reviews[reviews.length - 1].state;
|
|
1068
1222
|
|
|
1069
|
-
const defaultUserApproved = reviews.some(r => r.user === defaultUserLogin && r.state === 'APPROVED');
|
|
1223
|
+
const defaultUserApproved = reviews.some(/** @param {{user: string, state: string}} r */ (r) => r.user === defaultUserLogin && r.state === 'APPROVED');
|
|
1070
1224
|
|
|
1071
1225
|
return {
|
|
1072
1226
|
ok: true,
|
|
@@ -1088,6 +1242,7 @@ function getLatestReviewDecision(branch, options = {}) {
|
|
|
1088
1242
|
* @returns {string|null} Disposition value (CHANGES_MADE|PUSHBACK_ALL|PARKED|BLOCKED) or null
|
|
1089
1243
|
*/
|
|
1090
1244
|
function getLatestDisposition(branch, implementerUser, sinceIso, token, options = {}) {
|
|
1245
|
+
/** @type {{apiCall?: Function, forgejoUser?: string, rootDir?: string}} */
|
|
1091
1246
|
const {
|
|
1092
1247
|
apiCall = forgejoApi,
|
|
1093
1248
|
forgejoUser,
|
|
@@ -1097,80 +1252,82 @@ function getLatestDisposition(branch, implementerUser, sinceIso, token, options
|
|
|
1097
1252
|
const slugMatch = branch.match(/^mission\/(task-\d+)/);
|
|
1098
1253
|
const slug = slugMatch ? slugMatch[1] : null;
|
|
1099
1254
|
const prAccess = resolvePrAccess(branch, token, { apiCall, slug, forgejoUser, rootDir });
|
|
1100
|
-
if (isApiErrorResult(prAccess)) return null;
|
|
1101
|
-
if (!prAccess) return null;
|
|
1255
|
+
if (prAccess && isApiErrorResult(prAccess)) {return null;}
|
|
1256
|
+
if (!prAccess) {return null;}
|
|
1102
1257
|
const prNumber = prAccess.prNumber;
|
|
1103
1258
|
|
|
1104
1259
|
const result = apiCall('GET', `/issues/${prNumber}/comments`, prAccess.token);
|
|
1105
|
-
if (!result.ok || !Array.isArray(result.data)) return null;
|
|
1260
|
+
if (!result.ok || !Array.isArray(result.data)) {return null;}
|
|
1106
1261
|
|
|
1107
1262
|
const since = new Date(sinceIso).getTime();
|
|
1108
1263
|
|
|
1109
1264
|
const eligible = result.data
|
|
1110
|
-
.filter(c => {
|
|
1265
|
+
.filter(/** @param {{user?: {login?: string}, created_at?: string, body?: string}} c */ (c) => {
|
|
1111
1266
|
const user = (c.user || {}).login;
|
|
1112
1267
|
const createdStr = c.created_at || '';
|
|
1113
1268
|
const created = createdStr ? new Date(createdStr).getTime() : 0;
|
|
1114
1269
|
const body = c.body || '';
|
|
1115
1270
|
return user === implementerUser && created >= since && DISPOSITION_PATTERN.test(body);
|
|
1116
1271
|
})
|
|
1117
|
-
.map(c => {
|
|
1118
|
-
const match = DISPOSITION_PATTERN.exec(c.body);
|
|
1272
|
+
.map(/** @param {{user?: {login?: string}, created_at?: string, body?: string}} c */ (c) => {
|
|
1273
|
+
const match = DISPOSITION_PATTERN.exec(c.body || '');
|
|
1119
1274
|
return { disposition: match ? match[1] : null, createdAt: c.created_at || '' };
|
|
1120
1275
|
})
|
|
1121
|
-
.filter(e => e.disposition)
|
|
1122
|
-
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
1276
|
+
.filter(/** @param {{disposition: string|null}} e */ (e) => e.disposition)
|
|
1277
|
+
.sort(/** @param {{createdAt: string}} a @param {{createdAt: string}} b */ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
1123
1278
|
|
|
1124
1279
|
return eligible.length > 0 ? eligible[eligible.length - 1].disposition : null;
|
|
1125
1280
|
}
|
|
1126
1281
|
|
|
1127
|
-
async function getLatestReviewForPr(prNumber, reviewerUser, sinceIso, token, options = {}) {
|
|
1282
|
+
async function getLatestReviewForPr(/** @type {number} */ prNumber, /** @type {string} */ reviewerUser, /** @type {string} */ sinceIso, /** @type {string} */ token, options = {}) {
|
|
1283
|
+
/** @type {{apiCall?: Function}} */
|
|
1128
1284
|
const {
|
|
1129
1285
|
apiCall = forgejoApiAsync
|
|
1130
1286
|
} = options;
|
|
1131
1287
|
|
|
1132
1288
|
const result = await apiCall('GET', `/pulls/${prNumber}/reviews`, token);
|
|
1133
|
-
if (!result.ok || !Array.isArray(result.data)) return null;
|
|
1289
|
+
if (!result.ok || !Array.isArray(result.data)) {return null;}
|
|
1134
1290
|
|
|
1135
1291
|
const since = new Date(sinceIso).getTime();
|
|
1136
1292
|
|
|
1137
1293
|
const eligible = result.data
|
|
1138
|
-
.filter(r => {
|
|
1294
|
+
.filter(/** @param {{user?: {login?: string}, submitted_at?: string, created_at?: string, state?: string}} r */ r => {
|
|
1139
1295
|
const user = (r.user || {}).login;
|
|
1140
1296
|
const submittedAt = r.submitted_at || r.created_at || '';
|
|
1141
1297
|
const submitted = submittedAt ? new Date(submittedAt).getTime() : 0;
|
|
1142
1298
|
return user === reviewerUser && submitted >= since;
|
|
1143
1299
|
})
|
|
1144
|
-
.map(r => ({ state: r.state, submittedAt: r.submitted_at || r.created_at || '' }))
|
|
1145
|
-
.sort((a, b) => new Date(a.submittedAt).getTime() - new Date(b.submittedAt).getTime());
|
|
1300
|
+
.map(/** @param {{state?: string, submitted_at?: string, created_at?: string}} r */ r => ({ state: r.state, submittedAt: r.submitted_at || r.created_at || '' }))
|
|
1301
|
+
.sort(/** @param {{submittedAt: string}} a @param {{submittedAt: string}} b */ (a, b) => new Date(a.submittedAt).getTime() - new Date(b.submittedAt).getTime());
|
|
1146
1302
|
|
|
1147
1303
|
return eligible.length > 0 ? eligible[eligible.length - 1] : null;
|
|
1148
1304
|
}
|
|
1149
1305
|
|
|
1150
|
-
async function getLatestDispositionForPr(prNumber, implementerUser, sinceIso, token, options = {}) {
|
|
1306
|
+
async function getLatestDispositionForPr(/** @type {number} */ prNumber, /** @type {string} */ implementerUser, /** @type {string} */ sinceIso, /** @type {string} */ token, options = {}) {
|
|
1307
|
+
/** @type {{apiCall?: Function}} */
|
|
1151
1308
|
const {
|
|
1152
1309
|
apiCall = forgejoApiAsync
|
|
1153
1310
|
} = options;
|
|
1154
1311
|
|
|
1155
1312
|
const result = await apiCall('GET', `/issues/${prNumber}/comments`, token);
|
|
1156
|
-
if (!result.ok || !Array.isArray(result.data)) return null;
|
|
1313
|
+
if (!result.ok || !Array.isArray(result.data)) {return null;}
|
|
1157
1314
|
|
|
1158
1315
|
const since = new Date(sinceIso).getTime();
|
|
1159
1316
|
|
|
1160
1317
|
const eligible = result.data
|
|
1161
|
-
.filter(c => {
|
|
1318
|
+
.filter(/** @param {{user?: {login?: string}, created_at?: string, body?: string}} c */ (c) => {
|
|
1162
1319
|
const user = (c.user || {}).login;
|
|
1163
1320
|
const createdAt = c.created_at || '';
|
|
1164
1321
|
const created = createdAt ? new Date(createdAt).getTime() : 0;
|
|
1165
1322
|
const body = c.body || '';
|
|
1166
1323
|
return user === implementerUser && created >= since && DISPOSITION_PATTERN.test(body);
|
|
1167
1324
|
})
|
|
1168
|
-
.map(c => {
|
|
1169
|
-
const match = DISPOSITION_PATTERN.exec(c.body);
|
|
1325
|
+
.map(/** @param {{user?: {login?: string}, created_at?: string, body?: string}} c */ (c) => {
|
|
1326
|
+
const match = DISPOSITION_PATTERN.exec(c.body || '');
|
|
1170
1327
|
return { disposition: match ? match[1] : null, createdAt: c.created_at || '' };
|
|
1171
1328
|
})
|
|
1172
|
-
.filter(e => e.disposition)
|
|
1173
|
-
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
1329
|
+
.filter(/** @param {{disposition: string|null}} e */ (e) => e.disposition)
|
|
1330
|
+
.sort(/** @param {{createdAt: string}} a @param {{createdAt: string}} b */ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
1174
1331
|
|
|
1175
1332
|
return eligible.length > 0 ? eligible[eligible.length - 1].disposition : null;
|
|
1176
1333
|
}
|
|
@@ -1181,9 +1338,10 @@ async function getLatestDispositionForPr(prNumber, implementerUser, sinceIso, to
|
|
|
1181
1338
|
* @param {string} branch - Mission branch (e.g. 'mission/task-089')
|
|
1182
1339
|
* @param {string} token - Forgejo PAT
|
|
1183
1340
|
* @param {string} body - Comment body (markdown)
|
|
1184
|
-
* @returns {{ ok: boolean, data: any, status: number|null }}
|
|
1341
|
+
* @returns {{ ok: boolean, data: any, status: number|null, error?: string, raw?: string }}
|
|
1185
1342
|
*/
|
|
1186
1343
|
function postComment(branch, token, body, options = {}) {
|
|
1344
|
+
/** @type {{apiCall?: Function, resolvePrNumber?: Function, forgejoUser?: string}} */
|
|
1187
1345
|
const {
|
|
1188
1346
|
apiCall = forgejoApi,
|
|
1189
1347
|
resolvePrNumber = getPrNumber,
|
|
@@ -1193,10 +1351,10 @@ function postComment(branch, token, body, options = {}) {
|
|
|
1193
1351
|
const slug = slugMatch ? slugMatch[1] : null;
|
|
1194
1352
|
const prNumber = resolvePrNumber(branch, token, { apiCall, slug, forgejoUser });
|
|
1195
1353
|
if (isApiErrorResult(prNumber)) {
|
|
1196
|
-
const apiErr = prNumber._apiError;
|
|
1197
|
-
return { ok: false, data: null, status: null, error: 'api-failed', raw: `failed to resolve PR for ${branch}${apiErr.status === 7 ? ` (${codexSandboxHint()})` : ''}` };
|
|
1354
|
+
const apiErr = prNumber._apiError || {};
|
|
1355
|
+
return { ok: false, data: null, status: null, error: 'api-failed', raw: `failed to resolve PR for ${branch}${(apiErr.status || 0) === 7 ? ` (${codexSandboxHint()})` : ''}` };
|
|
1198
1356
|
}
|
|
1199
|
-
if (!prNumber) return { ok: false, data: null, status: null, error: 'pr-not-found' };
|
|
1357
|
+
if (!prNumber) {return { ok: false, data: null, status: null, error: 'pr-not-found' };}
|
|
1200
1358
|
return apiCall('POST', `/issues/${prNumber}/comments`, token, { body });
|
|
1201
1359
|
}
|
|
1202
1360
|
|
|
@@ -1213,9 +1371,10 @@ const REVIEW_OUTCOME_MAP = {
|
|
|
1213
1371
|
* @param {string} token - Forgejo PAT
|
|
1214
1372
|
* @param {string} outcome - 'approve' | 'request-changes' | 'comment'
|
|
1215
1373
|
* @param {string} summary - Review summary text
|
|
1216
|
-
* @returns {{ ok: boolean, data: any, status: number|null }}
|
|
1374
|
+
* @returns {{ ok: boolean, data: any, status: number|null, error?: string, raw?: string }}
|
|
1217
1375
|
*/
|
|
1218
1376
|
function postReview(branch, token, outcome, summary, options = {}) {
|
|
1377
|
+
/** @type {{apiCall?: Function, resolvePrNumber?: Function, forgejoUser?: string}} */
|
|
1219
1378
|
const {
|
|
1220
1379
|
apiCall = forgejoApi,
|
|
1221
1380
|
resolvePrNumber = getPrNumber,
|
|
@@ -1225,18 +1384,19 @@ function postReview(branch, token, outcome, summary, options = {}) {
|
|
|
1225
1384
|
const slug = slugMatch ? slugMatch[1] : null;
|
|
1226
1385
|
const prNumber = resolvePrNumber(branch, token, { apiCall, slug, forgejoUser });
|
|
1227
1386
|
if (isApiErrorResult(prNumber)) {
|
|
1228
|
-
const apiErr = prNumber._apiError;
|
|
1229
|
-
return { ok: false, data: null, status: null, error: 'api-failed', raw: `failed to resolve PR for ${branch}${apiErr.status === 7 ? ` (${codexSandboxHint()})` : ''}` };
|
|
1387
|
+
const apiErr = prNumber._apiError || {};
|
|
1388
|
+
return { ok: false, data: null, status: null, error: 'api-failed', raw: `failed to resolve PR for ${branch}${(apiErr.status || 0) === 7 ? ` (${codexSandboxHint()})` : ''}` };
|
|
1230
1389
|
}
|
|
1231
|
-
if (!prNumber) return { ok: false, data: null, status: null, error: 'pr-not-found' };
|
|
1390
|
+
if (!prNumber) {return { ok: false, data: null, status: null, error: 'pr-not-found' };}
|
|
1232
1391
|
|
|
1233
1392
|
// Resolve current head SHA for the PR to avoid submission failures if PR updated
|
|
1234
1393
|
const prRes = apiCall('GET', `/pulls/${prNumber}`, token);
|
|
1235
1394
|
const commit_id = (prRes.ok && prRes.data && prRes.data.head) ? prRes.data.head.sha : null;
|
|
1236
1395
|
|
|
1237
|
-
const event = REVIEW_OUTCOME_MAP[outcome];
|
|
1238
|
-
if (!event) return { ok: false, data: null, status: null, error: `unsupported-outcome: ${outcome}` };
|
|
1396
|
+
const event = /** @type {'APPROVED'|'REQUEST_CHANGES'|'COMMENT'|undefined} */ (REVIEW_OUTCOME_MAP[/** @type {keyof typeof REVIEW_OUTCOME_MAP} */ (outcome)]);
|
|
1397
|
+
if (!event) {return { ok: false, data: null, status: null, error: `unsupported-outcome: ${outcome}` };}
|
|
1239
1398
|
|
|
1399
|
+
/** @type {{body: string, event: string, commit_id?: string|null}} */
|
|
1240
1400
|
const payload = { body: summary, event };
|
|
1241
1401
|
if (commit_id) {
|
|
1242
1402
|
payload.commit_id = commit_id;
|
|
@@ -1260,6 +1420,7 @@ function postReview(branch, token, outcome, summary, options = {}) {
|
|
|
1260
1420
|
* @returns {Promise<boolean>} True if Forgejo is reachable, false otherwise
|
|
1261
1421
|
*/
|
|
1262
1422
|
function forgejoAvailable(url = process.env.FORGEJO_URL || 'http://localhost:3300', options = {}) {
|
|
1423
|
+
/** @type {{request?: Function, timeout?: number}} */
|
|
1263
1424
|
const {
|
|
1264
1425
|
request = http.request,
|
|
1265
1426
|
timeout = HTTP_REQUEST_TIMEOUT
|
|
@@ -1267,9 +1428,9 @@ function forgejoAvailable(url = process.env.FORGEJO_URL || 'http://localhost:330
|
|
|
1267
1428
|
const targetUrl = new URL(url);
|
|
1268
1429
|
|
|
1269
1430
|
return new Promise((resolve) => {
|
|
1270
|
-
const req = request(targetUrl, { method: 'GET', timeout }, (res) => {
|
|
1431
|
+
const req = request(targetUrl, { method: 'GET', timeout }, (/** @type {import('http').IncomingMessage} */ res) => {
|
|
1271
1432
|
req.destroy();
|
|
1272
|
-
resolve(res.statusCode !==
|
|
1433
|
+
resolve(res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300);
|
|
1273
1434
|
});
|
|
1274
1435
|
req.on('error', () => resolve(false));
|
|
1275
1436
|
req.on('timeout', () => {
|
|
@@ -1280,7 +1441,8 @@ function forgejoAvailable(url = process.env.FORGEJO_URL || 'http://localhost:330
|
|
|
1280
1441
|
});
|
|
1281
1442
|
}
|
|
1282
1443
|
|
|
1283
|
-
function syncMerged(branch, mergedCommit, options = {}) {
|
|
1444
|
+
function syncMerged(/** @type {string} */ branch, /** @type {string} */ mergedCommit, options = {}) {
|
|
1445
|
+
/** @type {{forgejoUser?: string, rootDir?: string, token?: string|null, baseBranch?: string|null, apiCall?: Function, resolvePrNumber?: Function, gitPush?: Function, gitFetch?: Function, gitContainsCommit?: Function, gitDelete?: Function, verifyCommit?: Function, log?: Function}} */
|
|
1284
1446
|
const {
|
|
1285
1447
|
forgejoUser,
|
|
1286
1448
|
rootDir = process.cwd(),
|
|
@@ -1304,7 +1466,7 @@ function syncMerged(branch, mergedCommit, options = {}) {
|
|
|
1304
1466
|
return { ok: false, error: 'missing-merged-commit' };
|
|
1305
1467
|
}
|
|
1306
1468
|
|
|
1307
|
-
const { token } = resolveForgejoAuth({ forgejoUser, token: providedToken });
|
|
1469
|
+
const { token } = resolveForgejoAuth({ forgejoUser, token: providedToken || undefined });
|
|
1308
1470
|
if (!token) {
|
|
1309
1471
|
return { ok: false, error: 'missing-token' };
|
|
1310
1472
|
}
|
|
@@ -1314,11 +1476,11 @@ function syncMerged(branch, mergedCommit, options = {}) {
|
|
|
1314
1476
|
|
|
1315
1477
|
const prNumber = resolvePrNumber(branch, token, { slug, rootDir });
|
|
1316
1478
|
if (isApiErrorResult(prNumber)) {
|
|
1317
|
-
const apiErr = prNumber._apiError;
|
|
1479
|
+
const apiErr = prNumber._apiError || {};
|
|
1318
1480
|
return {
|
|
1319
1481
|
ok: false,
|
|
1320
1482
|
error: 'api-failed',
|
|
1321
|
-
raw: `failed to resolve PR for ${branch}${apiErr.status === 7 ? ` (${codexSandboxHint()})` : ''}`
|
|
1483
|
+
raw: `failed to resolve PR for ${branch}${(apiErr.status || 0) === 7 ? ` (${codexSandboxHint()})` : ''}`
|
|
1322
1484
|
};
|
|
1323
1485
|
}
|
|
1324
1486
|
if (!prNumber) {
|
|
@@ -1330,7 +1492,7 @@ function syncMerged(branch, mergedCommit, options = {}) {
|
|
|
1330
1492
|
return { ok: false, error: 'missing-commit' };
|
|
1331
1493
|
}
|
|
1332
1494
|
|
|
1333
|
-
const verifyMergeState = (statusCode) => {
|
|
1495
|
+
const verifyMergeState = (/** @type {number} */ statusCode) => {
|
|
1334
1496
|
const prDetails = apiCall('GET', `/pulls/${prNumber}`, token, undefined, { rootDir });
|
|
1335
1497
|
if (!prDetails.ok) {
|
|
1336
1498
|
return { ok: false, error: 'merge-verify-failed', prNumber, statusCode: prDetails.statusCode };
|
|
@@ -1339,7 +1501,7 @@ function syncMerged(branch, mergedCommit, options = {}) {
|
|
|
1339
1501
|
const pr = prDetails.data || {};
|
|
1340
1502
|
const baseSha = pr.base ? pr.base.sha : null;
|
|
1341
1503
|
const headSha = pr.head ? pr.head.sha : null;
|
|
1342
|
-
const shaMatch = (a, b) => a && b && (a === b || a.startsWith(b) || b.startsWith(a));
|
|
1504
|
+
const shaMatch = (/** @type {string|null} */ a, /** @type {string|null} */ b) => a && b && (a === b || a.startsWith(b) || b.startsWith(a));
|
|
1343
1505
|
if (shaMatch(baseSha, mergedCommit) && shaMatch(headSha, mergedCommit)) {
|
|
1344
1506
|
log(`PR #${prNumber} (${branch}): confirmed head/base match ${mergedCommit} (already merged)`);
|
|
1345
1507
|
return { ok: true };
|
|
@@ -1449,9 +1611,10 @@ function syncMerged(branch, mergedCommit, options = {}) {
|
|
|
1449
1611
|
* @param {object} [options]
|
|
1450
1612
|
* @param {Function} [options.apiCall] - Forgejo API function, injectable for tests
|
|
1451
1613
|
* @param {Function} [options.log] - Logger, injectable for tests
|
|
1452
|
-
* @returns {
|
|
1614
|
+
* @returns {any[]|null} - Array of comment objects sorted by creation time, or null if comments could not be fetched
|
|
1453
1615
|
*/
|
|
1454
1616
|
function getCommentsSync(branch, token, options = {}) {
|
|
1617
|
+
/** @type {{apiCall?: Function, forgejoUser?: string, rootDir?: string, log?: Function}} */
|
|
1455
1618
|
const {
|
|
1456
1619
|
apiCall = forgejoApi,
|
|
1457
1620
|
forgejoUser,
|
|
@@ -1461,11 +1624,11 @@ function getCommentsSync(branch, token, options = {}) {
|
|
|
1461
1624
|
const slugMatch = branch.match(/^mission\/(task-\d+)/);
|
|
1462
1625
|
const slug = slugMatch ? slugMatch[1] : null;
|
|
1463
1626
|
const prAccess = resolvePrAccess(branch, token, { apiCall, slug, forgejoUser, rootDir });
|
|
1464
|
-
if (isApiErrorResult(prAccess)) {
|
|
1465
|
-
logger(`getComments API error resolving PR for ${branch}: status=${prAccess._apiError.status}`);
|
|
1627
|
+
if (prAccess && isApiErrorResult(prAccess)) {
|
|
1628
|
+
logger(`getComments API error resolving PR for ${branch}: status=${(/** @type {{status?: number}} */ (prAccess._apiError || {})).status || 0}`);
|
|
1466
1629
|
return null;
|
|
1467
1630
|
}
|
|
1468
|
-
if (!prAccess) return null;
|
|
1631
|
+
if (!prAccess) {return null;}
|
|
1469
1632
|
const prNumber = prAccess.prNumber;
|
|
1470
1633
|
const accessToken = prAccess.token;
|
|
1471
1634
|
|
|
@@ -1479,10 +1642,10 @@ function getCommentsSync(branch, token, options = {}) {
|
|
|
1479
1642
|
|
|
1480
1643
|
const issueComments = issueCommentsRes.data || [];
|
|
1481
1644
|
const reviews = reviewsRes.data || [];
|
|
1482
|
-
const allComments = [];
|
|
1645
|
+
const allComments = /** @type {Array<{kind: string, user: string, created: string, body: string, state?: string, location?: string}>} */ ([]);
|
|
1483
1646
|
|
|
1484
1647
|
// 1. Process issue comments
|
|
1485
|
-
issueComments.forEach(c => {
|
|
1648
|
+
issueComments.forEach(/** @param {{user?: {login?: string}, created_at?: string, body?: string}} c */ (c) => {
|
|
1486
1649
|
allComments.push({
|
|
1487
1650
|
kind: 'issue-comment',
|
|
1488
1651
|
user: (c.user || {}).login || '?',
|
|
@@ -1494,8 +1657,8 @@ function getCommentsSync(branch, token, options = {}) {
|
|
|
1494
1657
|
// 2. Process reviews and their inline comments
|
|
1495
1658
|
for (const r of reviews) {
|
|
1496
1659
|
const flags = [];
|
|
1497
|
-
if (r.stale) flags.push('stale');
|
|
1498
|
-
if (r.dismissed) flags.push('dismissed');
|
|
1660
|
+
if (r.stale) {flags.push('stale');}
|
|
1661
|
+
if (r.dismissed) {flags.push('dismissed');}
|
|
1499
1662
|
const kind = flags.length > 0 ? `review [${flags.join(', ')}]` : 'review';
|
|
1500
1663
|
|
|
1501
1664
|
allComments.push({
|
|
@@ -1509,12 +1672,12 @@ function getCommentsSync(branch, token, options = {}) {
|
|
|
1509
1672
|
// Fetch inline comments for this review
|
|
1510
1673
|
const inlineRes = apiCall('GET', `/pulls/${prNumber}/reviews/${r.id}/comments`, accessToken);
|
|
1511
1674
|
if (inlineRes.ok && Array.isArray(inlineRes.data)) {
|
|
1512
|
-
inlineRes.data.forEach(c => {
|
|
1675
|
+
inlineRes.data.forEach(/** @param {{user?: {login?: string}, created_at?: string, body?: string, path?: string, line?: number, original_line?: number}} c */ (c) => {
|
|
1513
1676
|
const iFlags = [];
|
|
1514
|
-
if (r.stale) iFlags.push('stale');
|
|
1515
|
-
if (r.dismissed) iFlags.push('dismissed');
|
|
1677
|
+
if (r.stale) {iFlags.push('stale');}
|
|
1678
|
+
if (r.dismissed) {iFlags.push('dismissed');}
|
|
1516
1679
|
let iKind = 'inline-comment';
|
|
1517
|
-
if (iFlags.length > 0) iKind += ` [${iFlags.join(', ')}]`;
|
|
1680
|
+
if (iFlags.length > 0) {iKind += ` [${iFlags.join(', ')}]`;}
|
|
1518
1681
|
|
|
1519
1682
|
const path = c.path || '';
|
|
1520
1683
|
const line = c.line || c.original_line || '';
|
|
@@ -1531,10 +1694,10 @@ function getCommentsSync(branch, token, options = {}) {
|
|
|
1531
1694
|
}
|
|
1532
1695
|
}
|
|
1533
1696
|
|
|
1534
|
-
return allComments.sort((a, b) => a.created.localeCompare(b.created));
|
|
1697
|
+
return allComments.sort(/** @param {{created: string}} a @param {{created: string}} b */ (a, b) => a.created.localeCompare(b.created));
|
|
1535
1698
|
}
|
|
1536
1699
|
|
|
1537
|
-
async function getComments(branch, token, options = {}) {
|
|
1700
|
+
async function getComments(/** @type {string} */ branch, /** @type {string} */ token, options = {}) {
|
|
1538
1701
|
return getCommentsSync(branch, token, options);
|
|
1539
1702
|
}
|
|
1540
1703
|
|
|
@@ -1544,22 +1707,21 @@ async function getComments(branch, token, options = {}) {
|
|
|
1544
1707
|
*
|
|
1545
1708
|
* @param {string} branch - Mission branch
|
|
1546
1709
|
* @param {string} token - Forgejo PAT
|
|
1547
|
-
* @param {string} user - Forgejo login
|
|
1548
1710
|
* @returns {Promise<Object>} - { ok: boolean, error: string }
|
|
1549
1711
|
*/
|
|
1550
|
-
async function closePr(branch, token
|
|
1712
|
+
async function closePr(branch, token) {
|
|
1551
1713
|
const slugMatch = branch.match(/^mission\/(task-\d+)/);
|
|
1552
1714
|
const slug = slugMatch ? slugMatch[1] : null;
|
|
1553
1715
|
const rootDir = process.cwd();
|
|
1554
1716
|
const prNumber = getPrNumber(branch, token, { slug, rootDir });
|
|
1555
1717
|
|
|
1556
|
-
if (isApiErrorResult(prNumber)) {
|
|
1557
|
-
const apiErr = prNumber._apiError;
|
|
1558
|
-
fmt.log.warn(`API error resolving PR for ${fmt.branch(branch)}: status=${apiErr.status}${apiErr.status === 7 ? ` (${codexSandboxHint()})` : ''}`);
|
|
1718
|
+
if (prNumber && typeof prNumber === 'object' && isApiErrorResult(prNumber)) {
|
|
1719
|
+
const apiErr = /** @type {{status?: number}} */ (prNumber._apiError || {});
|
|
1720
|
+
fmt.log.warn(`API error resolving PR for ${fmt.branch(branch)}: status=${apiErr.status || 0}${(apiErr.status || 0) === 7 ? ` (${codexSandboxHint()})` : ''}`);
|
|
1559
1721
|
return { ok: true };
|
|
1560
1722
|
}
|
|
1561
1723
|
|
|
1562
|
-
if (prNumber) {
|
|
1724
|
+
if (prNumber && typeof prNumber === 'number') {
|
|
1563
1725
|
// Check current state
|
|
1564
1726
|
const prDetails = forgejoApi('GET', `/pulls/${prNumber}`, token, undefined, { rootDir });
|
|
1565
1727
|
if (prDetails.ok) {
|
|
@@ -1619,7 +1781,6 @@ module.exports = {
|
|
|
1619
1781
|
verifyCommitExists,
|
|
1620
1782
|
remoteRefContainsCommit,
|
|
1621
1783
|
resolveTrackingBranchSha,
|
|
1622
|
-
resolveForgejoHome,
|
|
1623
1784
|
deriveRepoFromGitRemote,
|
|
1624
1785
|
resolveForgejoSettings,
|
|
1625
1786
|
reviewRemoteUrl,
|