@humanbased/crosscheck 1.2.0-beta.81 → 1.3.0-beta.100
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/README.md +2 -1
- package/crosscheck.config.example.yml +23 -2
- package/dist/__tests__/adoption.test.d.ts +2 -0
- package/dist/__tests__/adoption.test.d.ts.map +1 -0
- package/dist/__tests__/adoption.test.js +175 -0
- package/dist/__tests__/adoption.test.js.map +1 -0
- package/dist/__tests__/auto-fix-branch.test.d.ts +2 -0
- package/dist/__tests__/auto-fix-branch.test.d.ts.map +1 -0
- package/dist/__tests__/auto-fix-branch.test.js +143 -0
- package/dist/__tests__/auto-fix-branch.test.js.map +1 -0
- package/dist/__tests__/board.test.js +2 -0
- package/dist/__tests__/board.test.js.map +1 -1
- package/dist/__tests__/codex-env.test.d.ts +2 -0
- package/dist/__tests__/codex-env.test.d.ts.map +1 -0
- package/dist/__tests__/codex-env.test.js +70 -0
- package/dist/__tests__/codex-env.test.js.map +1 -0
- package/dist/__tests__/codex.test.js +89 -1
- package/dist/__tests__/codex.test.js.map +1 -1
- package/dist/__tests__/commit-subject.test.d.ts +2 -0
- package/dist/__tests__/commit-subject.test.d.ts.map +1 -0
- package/dist/__tests__/commit-subject.test.js +72 -0
- package/dist/__tests__/commit-subject.test.js.map +1 -0
- package/dist/__tests__/credential-free-origin.test.d.ts +2 -0
- package/dist/__tests__/credential-free-origin.test.d.ts.map +1 -0
- package/dist/__tests__/credential-free-origin.test.js +59 -0
- package/dist/__tests__/credential-free-origin.test.js.map +1 -0
- package/dist/__tests__/detector.test.js +87 -1
- package/dist/__tests__/detector.test.js.map +1 -1
- package/dist/__tests__/error-classification.test.js +34 -0
- package/dist/__tests__/error-classification.test.js.map +1 -1
- package/dist/__tests__/fix.test.js +37 -1
- package/dist/__tests__/fix.test.js.map +1 -1
- package/dist/__tests__/harness-section.test.d.ts +2 -0
- package/dist/__tests__/harness-section.test.d.ts.map +1 -0
- package/dist/__tests__/harness-section.test.js +46 -0
- package/dist/__tests__/harness-section.test.js.map +1 -0
- package/dist/__tests__/kickass.test.js +31 -0
- package/dist/__tests__/kickass.test.js.map +1 -1
- package/dist/__tests__/linear-worker.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +2 -2
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/onboard-preservation.test.js +0 -1
- package/dist/__tests__/onboard-preservation.test.js.map +1 -1
- package/dist/__tests__/optimize.test.js +1 -1
- package/dist/__tests__/optimize.test.js.map +1 -1
- package/dist/__tests__/pr-status.test.js +213 -3
- package/dist/__tests__/pr-status.test.js.map +1 -1
- package/dist/__tests__/pr-workflow-state.test.js +247 -7
- package/dist/__tests__/pr-workflow-state.test.js.map +1 -1
- package/dist/__tests__/repository-guidance.test.js +21 -21
- package/dist/__tests__/repository-guidance.test.js.map +1 -1
- package/dist/__tests__/reviewed-sha.test.d.ts +2 -0
- package/dist/__tests__/reviewed-sha.test.d.ts.map +1 -0
- package/dist/__tests__/reviewed-sha.test.js +116 -0
- package/dist/__tests__/reviewed-sha.test.js.map +1 -0
- package/dist/__tests__/runner.test.js +178 -2
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/skill-broker.test.js +44 -2
- package/dist/__tests__/skill-broker.test.js.map +1 -1
- package/dist/__tests__/superseded-fix-pr.test.d.ts +2 -0
- package/dist/__tests__/superseded-fix-pr.test.d.ts.map +1 -0
- package/dist/__tests__/superseded-fix-pr.test.js +225 -0
- package/dist/__tests__/superseded-fix-pr.test.js.map +1 -0
- package/dist/__tests__/webhook.test.js +60 -0
- package/dist/__tests__/webhook.test.js.map +1 -1
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adoption.d.ts +6 -0
- package/dist/commands/adoption.d.ts.map +1 -0
- package/dist/commands/adoption.js +132 -0
- package/dist/commands/adoption.js.map +1 -0
- package/dist/commands/detect-step.d.ts.map +1 -1
- package/dist/commands/detect-step.js +5 -1
- package/dist/commands/detect-step.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +2 -2
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/issue.js +1 -1
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/kickass.d.ts +1 -1
- package/dist/commands/kickass.d.ts.map +1 -1
- package/dist/commands/kickass.js +36 -22
- package/dist/commands/kickass.js.map +1 -1
- package/dist/commands/linear-test.js.map +1 -1
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +30 -3
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/optimize.d.ts.map +1 -1
- package/dist/commands/optimize.js +1 -0
- package/dist/commands/optimize.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +40 -4
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +6 -0
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +132 -61
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/loader.js +6 -6
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +19 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/github/detector.d.ts.map +1 -1
- package/dist/github/detector.js +28 -1
- package/dist/github/detector.js.map +1 -1
- package/dist/github/merge.d.ts +0 -5
- package/dist/github/merge.d.ts.map +1 -1
- package/dist/github/merge.js +0 -9
- package/dist/github/merge.js.map +1 -1
- package/dist/github/reviewed-sha.d.ts +13 -0
- package/dist/github/reviewed-sha.d.ts.map +1 -0
- package/dist/github/reviewed-sha.js +62 -0
- package/dist/github/reviewed-sha.js.map +1 -0
- package/dist/github/superseded-fix-pr.d.ts +19 -0
- package/dist/github/superseded-fix-pr.d.ts.map +1 -0
- package/dist/github/superseded-fix-pr.js +97 -0
- package/dist/github/superseded-fix-pr.js.map +1 -0
- package/dist/github/webhook.d.ts +4 -1
- package/dist/github/webhook.d.ts.map +1 -1
- package/dist/github/webhook.js +8 -1
- package/dist/github/webhook.js.map +1 -1
- package/dist/lib/adoption.d.ts +64 -0
- package/dist/lib/adoption.d.ts.map +1 -0
- package/dist/lib/adoption.js +165 -0
- package/dist/lib/adoption.js.map +1 -0
- package/dist/lib/annotation.js +0 -7
- package/dist/lib/annotation.js.map +1 -1
- package/dist/lib/auto-fix-branch.d.ts +35 -0
- package/dist/lib/auto-fix-branch.d.ts.map +1 -0
- package/dist/lib/auto-fix-branch.js +88 -0
- package/dist/lib/auto-fix-branch.js.map +1 -0
- package/dist/lib/clone.d.ts +1 -0
- package/dist/lib/clone.d.ts.map +1 -1
- package/dist/lib/clone.js +38 -1
- package/dist/lib/clone.js.map +1 -1
- package/dist/lib/comment-bodies.d.ts.map +1 -1
- package/dist/lib/comment-bodies.js +6 -3
- package/dist/lib/comment-bodies.js.map +1 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +17 -1
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/pr-picker.d.ts +1 -1
- package/dist/lib/pr-picker.d.ts.map +1 -1
- package/dist/lib/pr-picker.js +10 -5
- package/dist/lib/pr-picker.js.map +1 -1
- package/dist/lib/pr-status.d.ts +2 -1
- package/dist/lib/pr-status.d.ts.map +1 -1
- package/dist/lib/pr-status.js +70 -6
- package/dist/lib/pr-status.js.map +1 -1
- package/dist/lib/pr-workflow-state.d.ts +17 -1
- package/dist/lib/pr-workflow-state.d.ts.map +1 -1
- package/dist/lib/pr-workflow-state.js +110 -5
- package/dist/lib/pr-workflow-state.js.map +1 -1
- package/dist/lib/repo-picker.d.ts.map +1 -1
- package/dist/lib/repo-picker.js +0 -1
- package/dist/lib/repo-picker.js.map +1 -1
- package/dist/lib/runner.d.ts +31 -1
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +460 -58
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/vendor.d.ts +1 -0
- package/dist/lib/vendor.d.ts.map +1 -1
- package/dist/lib/vendor.js +6 -0
- package/dist/lib/vendor.js.map +1 -1
- package/dist/lib/workflow.d.ts +4 -0
- package/dist/lib/workflow.d.ts.map +1 -1
- package/dist/lib/workflow.js +4 -1
- package/dist/lib/workflow.js.map +1 -1
- package/dist/linear/comment.d.ts.map +1 -1
- package/dist/linear/comment.js.map +1 -1
- package/dist/reviewers/address.d.ts +1 -1
- package/dist/reviewers/address.d.ts.map +1 -1
- package/dist/reviewers/address.js +1 -1
- package/dist/reviewers/address.js.map +1 -1
- package/dist/reviewers/codex-env.d.ts +12 -0
- package/dist/reviewers/codex-env.d.ts.map +1 -0
- package/dist/reviewers/codex-env.js +54 -0
- package/dist/reviewers/codex-env.js.map +1 -0
- package/dist/reviewers/codex.d.ts +16 -1
- package/dist/reviewers/codex.d.ts.map +1 -1
- package/dist/reviewers/codex.js +110 -33
- package/dist/reviewers/codex.js.map +1 -1
- package/dist/reviewers/fix.d.ts +1 -1
- package/dist/reviewers/fix.d.ts.map +1 -1
- package/dist/reviewers/fix.js +16 -6
- package/dist/reviewers/fix.js.map +1 -1
- package/dist/skills/broker.d.ts +2 -1
- package/dist/skills/broker.d.ts.map +1 -1
- package/dist/skills/broker.js +28 -2
- package/dist/skills/broker.js.map +1 -1
- package/docs/metrics.md +115 -0
- package/docs/trust.md +159 -0
- package/get-started.md +115 -5
- package/package.json +3 -1
package/dist/commands/watch.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execSync, spawn } from 'child_process';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { createWebhookServer } from '../github/webhook.js';
|
|
4
4
|
import { createGithubClient, getCommitMessage, registerOrgWebhook, deleteOrgWebhook, registerRepoWebhook, deleteRepoWebhook, patchOrgWebhookEvents, patchRepoWebhookEvents, findOrgWebhook, findRepoWebhook, listUserRepos, checkRepoAccessible, } from '../github/client.js';
|
|
5
|
+
import { closeSupersededAutoFixPRs } from '../github/superseded-fix-pr.js';
|
|
5
6
|
import { detectOriginFull, assignReviewer } from '../github/detector.js';
|
|
6
7
|
import { loadConfig, getGithubToken, getWebhookSecret, resolveConfigPath, promptDeploymentMode, detectScopesForDeployment, patchDeploymentConfig, detectGitHubLogin, getLinearCredentials, } from '../config/loader.js';
|
|
7
8
|
import { randomFortune } from '../lib/fortune.js';
|
|
@@ -10,7 +11,7 @@ import { initLogger, log as fileLog, logError, logUncaught } from '../lib/logger
|
|
|
10
11
|
import { isAuthorAllowed } from '../lib/filter.js';
|
|
11
12
|
import { runWorkflow } from '../lib/runner.js';
|
|
12
13
|
import { loadWorkflow, linearWritePossible, DEFAULT_RECHECK_INSTRUCTIONS } from '../lib/workflow.js';
|
|
13
|
-
import { filterStepsByTypes, formatRepoWorkflowSteps,
|
|
14
|
+
import { filterStepsByTypes, formatRepoWorkflowSteps, isReviewOnlyWorkflow, readRepoWorkflowStepTypes, resolveRepoWorkflowSteps, workflowHasStep } from '../lib/repo-workflow.js';
|
|
14
15
|
import { fetchStepHistory, identifyNextWorkflowStep, decideReviewOnly } from '../lib/pr-workflow-state.js';
|
|
15
16
|
import { parseAnnotation } from '../lib/annotation.js';
|
|
16
17
|
import { PRBoard, fmtTime, FMT_TIME_WIDTH } from '../lib/board.js';
|
|
@@ -129,6 +130,21 @@ function openTunnel(localPort) {
|
|
|
129
130
|
});
|
|
130
131
|
});
|
|
131
132
|
}
|
|
133
|
+
// PR history is what decides whether a step runs at all — the approval stop, the
|
|
134
|
+
// per-SHA dedup, the resume point. Acting without it means posting a duplicate review
|
|
135
|
+
// or modifying an already-approved commit, so callers fail closed rather than assume an
|
|
136
|
+
// empty history. One short retry first: the usual cause is a transient GitHub blip or a
|
|
137
|
+
// rate-limit tick, and deferring the event strands the PR until something else happens.
|
|
138
|
+
const HISTORY_RETRY_DELAY_MS = 3_000;
|
|
139
|
+
async function fetchStepHistoryWithRetry(owner, repo, prNumber, token) {
|
|
140
|
+
try {
|
|
141
|
+
return await fetchStepHistory(owner, repo, prNumber, token);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, HISTORY_RETRY_DELAY_MS));
|
|
145
|
+
return await fetchStepHistory(owner, repo, prNumber, token);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
132
148
|
export async function runWatch(opts = {}) {
|
|
133
149
|
const configPath = opts.config;
|
|
134
150
|
let config = loadConfig(configPath);
|
|
@@ -211,12 +227,16 @@ export async function runWatch(opts = {}) {
|
|
|
211
227
|
// Last-reviewed diff hash per PR — skip reviews when a new SHA has identical diff vs base
|
|
212
228
|
// (force-push, amend, no-op rebase). Persisted so restarts don't re-review unchanged content.
|
|
213
229
|
const diffHashes = new PersistentDiffHashMap();
|
|
214
|
-
//
|
|
215
|
-
|
|
230
|
+
// Completed round count per PR — the auto-loop's round counter, which history cannot
|
|
231
|
+
// supply (it returns the last recheck's round unchanged across iterations).
|
|
216
232
|
const prRoundCounts = new Map();
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
233
|
+
// Comment IDs the issue_comment bridge has already handled, so a webhook redelivery
|
|
234
|
+
// of the same comment does not dispatch twice. There is no need to guess at which
|
|
235
|
+
// comments are "ours": webhook.ts forwards only `trigger=kickass` annotations, and
|
|
236
|
+
// this watcher's own reviews carry trigger=watch/comment/backtrace, so they never
|
|
237
|
+
// reach the bridge. Every comment that does reach it is a kickass dispatch whose
|
|
238
|
+
// verdict this watcher is meant to act on — history detection decides how.
|
|
239
|
+
const handledCommentIds = new Set();
|
|
220
240
|
// Idle tracking — reset on any PR activity; used by the idle-issue timer.
|
|
221
241
|
let lastActivityAt = Date.now();
|
|
222
242
|
let idleAnalysisRunning = false;
|
|
@@ -235,6 +255,7 @@ export async function runWatch(opts = {}) {
|
|
|
235
255
|
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'author_not_allowed', author: params.author });
|
|
236
256
|
return;
|
|
237
257
|
}
|
|
258
|
+
const prKey = `${owner}/${repoName}#${prNumber}`;
|
|
238
259
|
const { origin, method: originMethod } = await detectOriginFull(params.body ?? '', params.headRef, owner, repoName, prNumber, config, token, params.author);
|
|
239
260
|
// Smart-switch: when cross-vendor is degraded, override to single-vendor with the healthy vendor
|
|
240
261
|
const ss = getSmartSwitch();
|
|
@@ -265,6 +286,7 @@ export async function runWatch(opts = {}) {
|
|
|
265
286
|
html_url: `https://github.com/${owner}/${repoName}/pull/${prNumber}`,
|
|
266
287
|
user: { login: params.author },
|
|
267
288
|
...(params.labels !== undefined && { labels: params.labels.map(name => ({ name })) }),
|
|
289
|
+
...(params.createdAt !== undefined && { created_at: params.createdAt }),
|
|
268
290
|
};
|
|
269
291
|
if (!acquirePRLock(owner, repoName, prNumber, params.headSha)) {
|
|
270
292
|
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'in_progress_local' });
|
|
@@ -284,21 +306,26 @@ export async function runWatch(opts = {}) {
|
|
|
284
306
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'lock' }, err);
|
|
285
307
|
return;
|
|
286
308
|
}
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
309
|
+
// Determine the correct starting step from PR comment history. Webhook, backtrace,
|
|
310
|
+
// and auto_loop events always ask — there is no PR-level session fast-path.
|
|
311
|
+
//
|
|
312
|
+
// There used to be one (`reviewedPRKeys`): reviewed-in-this-session meant "run the
|
|
313
|
+
// next event as a recheck". It was wrong by construction on two axes. It is
|
|
314
|
+
// SHA-agnostic, so a push it had never seen inherited the previous SHA's routing;
|
|
315
|
+
// and it only knows what THIS process did, so an approval, a forced re-review, or a
|
|
316
|
+
// fix pushed by `kickass`, `crosscheck run`, or a second watcher was invisible to
|
|
317
|
+
// it. Each case where it guessed wrong grew its own guard — recheck-no-fix depth,
|
|
318
|
+
// repo overrides, approvals — and every one of those guards did the same thing:
|
|
319
|
+
// undo the guess and ask history. So ask history. It costs one call per event, and
|
|
320
|
+
// it is the only thing that sees the PR as it actually is.
|
|
321
|
+
//
|
|
322
|
+
// Comment triggers ask too. The SHA-specific fast path they used to take ran an
|
|
323
|
+
// externally-posted NEEDS_WORK as a recheck instead of routing to fix, because it
|
|
324
|
+
// skipped detection whenever this session had already touched the SHA.
|
|
325
|
+
let isRecheckRun = false;
|
|
326
|
+
// auto_loop's round is pre-computed from the completed round count rather than
|
|
327
|
+
// from history, which returns the last recheck's round unchanged across iterations
|
|
328
|
+
// and would defeat the max_rounds cap.
|
|
302
329
|
let round = params.action === 'auto_loop'
|
|
303
330
|
? (prRoundCounts.get(prKey) ?? 1) + 1
|
|
304
331
|
: isRecheckRun ? (prRoundCounts.get(prKey) ?? 1) + 1 : 1;
|
|
@@ -312,17 +339,6 @@ export async function runWatch(opts = {}) {
|
|
|
312
339
|
isRecheckRun = false;
|
|
313
340
|
round = 1;
|
|
314
341
|
}
|
|
315
|
-
// A recheck-no-fix depth (`review,recheck`) decides review-vs-recheck per SHA from
|
|
316
|
-
// the last verdict: an unresolved review gets a recheck, an APPROVE gets a fresh
|
|
317
|
-
// review (identifyNextWorkflowStep). The session fast-path above is PR-level and
|
|
318
|
-
// SHA-agnostic, so it would coerce every later push in this process into a recheck
|
|
319
|
-
// and never consult that decision. Force history detection instead. Comment-
|
|
320
|
-
// triggered runs keep their SHA-specific fast path — a kickass re-review targets a
|
|
321
|
-
// SHA history detection would report as already complete.
|
|
322
|
-
if (isRecheckWithoutFix(repoStepOverride) && isRecheckRun && params.action !== 'comment') {
|
|
323
|
-
isRecheckRun = false;
|
|
324
|
-
round = 1;
|
|
325
|
-
}
|
|
326
342
|
if (reviewOnlyForRepo) {
|
|
327
343
|
// Review-only comes from a per-repo override (crosscheck alter --review-only).
|
|
328
344
|
// Restrict the workflow to review step(s) and dedup on the exact head sha so
|
|
@@ -339,7 +355,7 @@ export async function runWatch(opts = {}) {
|
|
|
339
355
|
round = 1;
|
|
340
356
|
resolvedSteps = reviewSteps;
|
|
341
357
|
try {
|
|
342
|
-
const history = await
|
|
358
|
+
const history = await fetchStepHistoryWithRetry(owner, repoName, prNumber, token);
|
|
343
359
|
const decision = decideReviewOnly(history, params.headSha);
|
|
344
360
|
if (decision.alreadyReviewed) {
|
|
345
361
|
await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'success');
|
|
@@ -349,7 +365,18 @@ export async function runWatch(opts = {}) {
|
|
|
349
365
|
}
|
|
350
366
|
round = decision.round;
|
|
351
367
|
}
|
|
352
|
-
catch {
|
|
368
|
+
catch (err) {
|
|
369
|
+
// The dedup this mode relies on lives in that history. Reviewing without it
|
|
370
|
+
// would post a second review on a SHA already covered.
|
|
371
|
+
// Released as `failure`, not `success`: this SHA was not reviewed, and
|
|
372
|
+
// `crosscheck/review` is a commit status a repo can require for merge — a
|
|
373
|
+
// green one here would let an unreviewed HEAD satisfy branch protection
|
|
374
|
+
// during a GitHub outage.
|
|
375
|
+
await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'failure');
|
|
376
|
+
releasePRLock(owner, repoName, prNumber, params.headSha);
|
|
377
|
+
fileLog({ level: 'warn', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'history_unavailable', sha: params.headSha, error: err instanceof Error ? err.message : String(err) });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
353
380
|
}
|
|
354
381
|
else if (!isRecheckRun) {
|
|
355
382
|
try {
|
|
@@ -358,18 +385,34 @@ export async function runWatch(opts = {}) {
|
|
|
358
385
|
// fresh so live edits to workflow.yml apply across events.
|
|
359
386
|
const globalWorkflow = loadWorkflow(process.cwd());
|
|
360
387
|
const allSteps = repoStepOverride ? filterStepsByTypes(globalWorkflow, repoStepOverride) : globalWorkflow;
|
|
361
|
-
const history = await
|
|
362
|
-
|
|
388
|
+
const history = await fetchStepHistoryWithRetry(owner, repoName, prNumber, token);
|
|
389
|
+
// The webhook payload carries no usable `mergeable`, so ask — but only for
|
|
390
|
+
// workflows that can act on the answer. GitHub computes it lazily, and a null
|
|
391
|
+
// simply means the conflict route is not offered on this event. Unlike the
|
|
392
|
+
// history read above this one stays best-effort: not knowing whether the PR
|
|
393
|
+
// conflicts only costs a deferred resolve, not a wrong review.
|
|
394
|
+
let mergeable = null;
|
|
395
|
+
if (allSteps.some(s => s.type === 'conflict-resolve')) {
|
|
396
|
+
try {
|
|
397
|
+
const { data: freshPR } = await lockOctokit.rest.pulls.get({ owner, repo: repoName, pull_number: prNumber });
|
|
398
|
+
mergeable = freshPR.mergeable;
|
|
399
|
+
}
|
|
400
|
+
catch { /* best-effort — unknown mergeability just skips the conflict route */ }
|
|
401
|
+
}
|
|
402
|
+
const nextResult = identifyNextWorkflowStep(history, allSteps, params.headSha, { mergeable });
|
|
363
403
|
if (nextResult.step === null) {
|
|
364
404
|
// Workflow already complete for this HEAD sha — release lock and skip.
|
|
365
405
|
// Happens when a synchronize event fires after all steps are done.
|
|
366
406
|
await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'success');
|
|
367
407
|
releasePRLock(owner, repoName, prNumber, params.headSha);
|
|
368
|
-
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'workflow_complete', sha: params.headSha });
|
|
408
|
+
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: nextResult.stopReason ?? 'workflow_complete', sha: params.headSha });
|
|
369
409
|
return;
|
|
370
410
|
}
|
|
371
411
|
if (nextResult.hasExistingReview) {
|
|
372
|
-
|
|
412
|
+
// conflict-resolve is excluded: the run continues into the review step, and
|
|
413
|
+
// the merge commit it produces is new content owed a real review, not a
|
|
414
|
+
// recheck of findings the merge had nothing to do with.
|
|
415
|
+
isRecheckRun = nextResult.step.type !== 'review' && nextResult.step.type !== 'conflict-resolve';
|
|
373
416
|
// auto_loop pre-computed the correct incremented round from prRoundCounts;
|
|
374
417
|
// history detection returns the last recheck's round (unchanged across
|
|
375
418
|
// iterations), so overwriting here would defeat the max_rounds cap.
|
|
@@ -418,15 +461,26 @@ export async function runWatch(opts = {}) {
|
|
|
418
461
|
}
|
|
419
462
|
}
|
|
420
463
|
}
|
|
421
|
-
catch {
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
464
|
+
catch (err) {
|
|
465
|
+
// History still unreadable after the retry. Everything this function decides —
|
|
466
|
+
// whether HEAD is already approved, already reviewed, or mid-loop — lives in
|
|
467
|
+
// that history, so running anyway risks a second review on an approved commit
|
|
468
|
+
// or a fix re-applied to code that already has one. Defer instead: release the
|
|
469
|
+
// locks and let the next event, `kickass`, or `crosscheck run` pick the PR up.
|
|
470
|
+
//
|
|
471
|
+
// auto_loop is the exception. It is a continuation of a run this process just
|
|
472
|
+
// completed, so the state is known from the session rather than from history,
|
|
473
|
+
// and deferring would strand the loop — nothing else re-triggers it.
|
|
474
|
+
if (params.action !== 'auto_loop') {
|
|
475
|
+
// `failure`, not `success` — see the review-only branch above: a green
|
|
476
|
+
// `crosscheck/review` on a HEAD nobody reviewed can satisfy branch
|
|
477
|
+
// protection.
|
|
478
|
+
await releaseRemoteLock(lockOctokit, owner, repoName, params.headSha, 'failure');
|
|
479
|
+
releasePRLock(owner, repoName, prNumber, params.headSha);
|
|
480
|
+
fileLog({ level: 'warn', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'history_unavailable', sha: params.headSha, error: err instanceof Error ? err.message : String(err) });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
isRecheckRun = true;
|
|
430
484
|
}
|
|
431
485
|
}
|
|
432
486
|
const reviewStart = Date.now();
|
|
@@ -523,15 +577,9 @@ export async function runWatch(opts = {}) {
|
|
|
523
577
|
round,
|
|
524
578
|
trigger: params.action === 'backtrace' ? 'backtrace' : params.action === 'comment' ? 'comment' : 'watch',
|
|
525
579
|
});
|
|
526
|
-
|
|
527
|
-
//
|
|
528
|
-
// session caches. reviewedPRKeys is what makes the next event on this PR
|
|
529
|
-
// an `isRecheckRun`, and a review coerced to a recheck is deliberately
|
|
530
|
-
// never gated by max_rounds — so a lockfile-only PR that later gains
|
|
531
|
-
// source files would be "rechecked" against findings never posted.
|
|
580
|
+
// A strategy-skipped PR never ran a review, so it must not advance the
|
|
581
|
+
// auto-loop's round counter — that run posted nothing to count.
|
|
532
582
|
if (strategySkipped === undefined) {
|
|
533
|
-
reviewedPRKeys.add(prKey);
|
|
534
|
-
reviewedPRShaKeys.add(key); // key = "owner/repo#pr@sha"
|
|
535
583
|
prRoundCounts.set(prKey, round);
|
|
536
584
|
}
|
|
537
585
|
// Recompute the diff hash AFTER runWorkflow — workflow steps such as
|
|
@@ -720,6 +768,7 @@ export async function runWatch(opts = {}) {
|
|
|
720
768
|
baseRef: pr.base.ref, action: event.action,
|
|
721
769
|
...(pr.labels !== undefined && { labels: pr.labels.map(l => l.name) }),
|
|
722
770
|
...(pr.base.repo.default_branch !== undefined && { defaultBranch: pr.base.repo.default_branch }),
|
|
771
|
+
...(pr.created_at !== undefined && { createdAt: pr.created_at }),
|
|
723
772
|
});
|
|
724
773
|
}, (msg) => bLog(chalk.dim(fmtTime()) + ' ' + msg), fileLog, async (event) => {
|
|
725
774
|
const owner = event.repository.owner.login;
|
|
@@ -769,10 +818,11 @@ export async function runWatch(opts = {}) {
|
|
|
769
818
|
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'comment_stale_sha', sha: prData.head.sha, annotation_sha: annotationSha });
|
|
770
819
|
return;
|
|
771
820
|
}
|
|
772
|
-
if (
|
|
773
|
-
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: '
|
|
821
|
+
if (handledCommentIds.has(event.comment.id)) {
|
|
822
|
+
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'comment_already_handled', comment_id: event.comment.id });
|
|
774
823
|
return;
|
|
775
824
|
}
|
|
825
|
+
handledCommentIds.add(event.comment.id);
|
|
776
826
|
// GitHub can deliver issue_comment before ck run's finally block releases
|
|
777
827
|
// the remote lock. Retry with backoff; re-fetch the PR head on each attempt
|
|
778
828
|
// so we don't dispatch against a sha that advanced while we were waiting.
|
|
@@ -796,10 +846,6 @@ export async function runWatch(opts = {}) {
|
|
|
796
846
|
break;
|
|
797
847
|
}
|
|
798
848
|
}
|
|
799
|
-
if (reviewedPRShaKeys.has(`${owner}/${repoName}#${prNumber}@${prData.head.sha}`)) {
|
|
800
|
-
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'comment_self_trigger', sha: prData.head.sha });
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
803
849
|
if (await checkRemoteLock(octokit, owner, repoName, prData.head.sha).catch(() => false)) {
|
|
804
850
|
fileLog({ level: 'info', event: 'comment_trigger_deferred', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'in_progress_remote', attempt });
|
|
805
851
|
continue;
|
|
@@ -819,6 +865,7 @@ export async function runWatch(opts = {}) {
|
|
|
819
865
|
// depending on which trigger ran it.
|
|
820
866
|
...(prData.labels !== undefined && { labels: prData.labels.map((l) => l.name) }),
|
|
821
867
|
...(prData.base.repo?.default_branch !== undefined && { defaultBranch: prData.base.repo.default_branch }),
|
|
868
|
+
...(prData.created_at !== undefined && { createdAt: prData.created_at }),
|
|
822
869
|
});
|
|
823
870
|
break;
|
|
824
871
|
}
|
|
@@ -826,6 +873,29 @@ export async function runWatch(opts = {}) {
|
|
|
826
873
|
catch (err) {
|
|
827
874
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'comment_trigger' }, err);
|
|
828
875
|
}
|
|
876
|
+
}, async (event) => {
|
|
877
|
+
const owner = event.repository.owner.login;
|
|
878
|
+
const repoName = event.repository.name;
|
|
879
|
+
const prNumber = event.number;
|
|
880
|
+
try {
|
|
881
|
+
const octokit = createGithubClient(token);
|
|
882
|
+
const outcomes = await closeSupersededAutoFixPRs(octokit, owner, repoName, prNumber, event.pull_request.merge_commit_sha ?? null);
|
|
883
|
+
for (const outcome of outcomes) {
|
|
884
|
+
fileLog({
|
|
885
|
+
level: outcome.status === 'failed' ? 'warn' : 'info',
|
|
886
|
+
event: 'auto_fix_pr_superseded',
|
|
887
|
+
repo: `${owner}/${repoName}`, pr: prNumber,
|
|
888
|
+
fix_pr: outcome.prNumber, status: outcome.status,
|
|
889
|
+
...(outcome.reason && { reason: outcome.reason }),
|
|
890
|
+
});
|
|
891
|
+
if (outcome.status === 'closed') {
|
|
892
|
+
bLog(chalk.dim(fmtTime()) + ` closed superseded auto-fix PR ${owner}/${repoName}#${outcome.prNumber} (${prNumber} merged)`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
catch (err) {
|
|
897
|
+
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'auto_fix_supersede' }, err);
|
|
898
|
+
}
|
|
829
899
|
});
|
|
830
900
|
await new Promise((resolve, reject) => {
|
|
831
901
|
server.on('error', (err) => {
|
|
@@ -1107,6 +1177,7 @@ export async function runWatch(opts = {}) {
|
|
|
1107
1177
|
baseRef: pr.baseRef, action: 'backtrace',
|
|
1108
1178
|
...(pr.labels !== undefined && { labels: pr.labels }),
|
|
1109
1179
|
...(pr.defaultBranch !== undefined && { defaultBranch: pr.defaultBranch }),
|
|
1180
|
+
createdAt: pr.createdAt,
|
|
1110
1181
|
})));
|
|
1111
1182
|
}
|
|
1112
1183
|
catch (err) {
|