forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
package/lib/pr-bundle.js
CHANGED
|
@@ -86,12 +86,12 @@ async function gatherUnresolvedComments(adapter, { owner, repo, pr }) {
|
|
|
86
86
|
* Predict merge conflicts. `detectConflicts` is optional on the adapter; when
|
|
87
87
|
* absent or failing, report it as unsupported rather than throwing.
|
|
88
88
|
*/
|
|
89
|
-
async function gatherConflicts(adapter, { baseRef, cwd }) {
|
|
89
|
+
async function gatherConflicts(adapter, { baseRef, cwd, headRef }) {
|
|
90
90
|
if (typeof adapter.detectConflicts !== 'function') {
|
|
91
91
|
return { supported: false, reason: 'adapter has no detectConflicts capability' };
|
|
92
92
|
}
|
|
93
93
|
try {
|
|
94
|
-
return await adapter.detectConflicts({ baseRef, cwd });
|
|
94
|
+
return await adapter.detectConflicts({ baseRef, cwd, headRef });
|
|
95
95
|
} catch (error) {
|
|
96
96
|
return { supported: false, reason: error.message || 'conflict detection failed' };
|
|
97
97
|
}
|
|
@@ -148,9 +148,9 @@ async function gatherPrBundle({ pr, owner, repo, base, baseRef, cwd, adapter })
|
|
|
148
148
|
// silently yields a null required set. `pr` lets the adapter fall back to the
|
|
149
149
|
// rollup `isRequired` set when branch protection is unreadable in CI.
|
|
150
150
|
const requiredRaw = await adapter.readRequiredChecks({ owner, repo, base, pr });
|
|
151
|
-
const divergence = await adapter.readDivergence({ baseRef, cwd });
|
|
151
|
+
const divergence = await adapter.readDivergence({ baseRef, cwd, headRef: state.headSha });
|
|
152
152
|
const comments = await gatherUnresolvedComments(adapter, { owner, repo, pr });
|
|
153
|
-
const conflicts = await gatherConflicts(adapter, { baseRef, cwd });
|
|
153
|
+
const conflicts = await gatherConflicts(adapter, { baseRef, cwd, headRef: state.headSha });
|
|
154
154
|
|
|
155
155
|
const requiredSet = requiredRaw === undefined ? null : requiredRaw;
|
|
156
156
|
|
|
@@ -172,8 +172,7 @@ async function gatherPrBundle({ pr, owner, repo, base, baseRef, cwd, adapter })
|
|
|
172
172
|
state: String(state.state || 'OPEN').toUpperCase(),
|
|
173
173
|
},
|
|
174
174
|
ci: buildCi(state.checks, requiredSet),
|
|
175
|
-
//
|
|
176
|
-
// null) — `rollup` is the CI path where branch protection is unreadable.
|
|
175
|
+
// The authoritative required-check source (`protection` or null).
|
|
177
176
|
requiredSource: adapter.lastRequiredSource || null,
|
|
178
177
|
branch: {
|
|
179
178
|
ahead: divergence.ahead || 0,
|
|
@@ -35,6 +35,17 @@ const INFRA_CONCLUSIONS = new Set(['CANCELLED', 'TIMED_OUT', 'STALE', 'STARTUP_F
|
|
|
35
35
|
/** Conclusions that mean the code is genuinely broken — NEVER auto-rerun these. */
|
|
36
36
|
const REAL_FAILURE_CONCLUSIONS = new Set(['FAILURE', 'ERROR', 'ACTION_REQUIRED']);
|
|
37
37
|
|
|
38
|
+
const AUTO_ACTION_STATUS = Object.freeze({
|
|
39
|
+
PASS: 'PASS',
|
|
40
|
+
UNCHANGED: 'UNCHANGED',
|
|
41
|
+
STALE: 'STALE',
|
|
42
|
+
INCOMPLETE: 'INCOMPLETE',
|
|
43
|
+
CONFLICT: 'CONFLICT',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const MAX_AUTO_ACTIONS = 8;
|
|
47
|
+
const MAX_AUTO_ATTEMPTS = 5;
|
|
48
|
+
|
|
38
49
|
/**
|
|
39
50
|
* Pull the numeric Actions run id from a job/run "details" URL
|
|
40
51
|
* (`.../actions/runs/<run>/job/<job>` or `.../actions/runs/<run>`). Returns null
|
|
@@ -99,16 +110,40 @@ function decideUpdateBranch(payload, opts = {}) {
|
|
|
99
110
|
return { should: true, reason: 'otherwise-clean-behind — only blocker is behind-base; merge base in' };
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
function indexFailures(failures) {
|
|
114
|
+
const byName = new Map();
|
|
115
|
+
for (const failure of failures) {
|
|
116
|
+
if (!failure?.name || byName.has(failure.name)) continue;
|
|
117
|
+
byName.set(failure.name, {
|
|
118
|
+
conclusion: String(failure.conclusion || '').toUpperCase(),
|
|
119
|
+
url: failure.jobUrl || failure.detailsUrl || null,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return byName;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function selectRerunChecks(failingNames, failuresByName) {
|
|
126
|
+
const checks = [];
|
|
127
|
+
for (const name of failingNames) {
|
|
128
|
+
const failure = failuresByName.get(name);
|
|
129
|
+
if (!failure?.conclusion) {
|
|
130
|
+
return { reason: `required check "${name}" has no known conclusion — cannot confirm flaky, fail closed` };
|
|
131
|
+
}
|
|
132
|
+
if (REAL_FAILURE_CONCLUSIONS.has(failure.conclusion)) {
|
|
133
|
+
return { reason: `required check "${name}" is a real failure (${failure.conclusion}) — never rerun` };
|
|
134
|
+
}
|
|
135
|
+
if (!INFRA_CONCLUSIONS.has(failure.conclusion)) {
|
|
136
|
+
return { reason: `required check "${name}" conclusion ${failure.conclusion} is not classified infrastructural — fail closed` };
|
|
137
|
+
}
|
|
138
|
+
checks.push({ name, conclusion: failure.conclusion, runId: runIdFromUrl(failure.url) });
|
|
139
|
+
}
|
|
140
|
+
return { checks };
|
|
141
|
+
}
|
|
142
|
+
|
|
102
143
|
/**
|
|
103
144
|
* Decide whether to re-run flaky REQUIRED checks. Fires ONLY when EVERY failing
|
|
104
|
-
* required check is infrastructural
|
|
105
|
-
*
|
|
106
|
-
* single real failure, an unclassifiable conclusion, a required-failing check
|
|
107
|
-
* with no matching `failures[]` entry, or a missing run id fails the WHOLE
|
|
108
|
-
* decision closed (never rerun a genuinely-broken PR, never loop on a real bug).
|
|
109
|
-
*
|
|
110
|
-
* @param {object} payload - the `--pull --json` payload.
|
|
111
|
-
* @returns {{ should: boolean, checks: Array<{name:string,conclusion:string,runId:string|null}>, runIds: string[], reason: string }}
|
|
145
|
+
* required check is infrastructural with a derivable run id, and NONE is a real
|
|
146
|
+
* failure or an unclassifiable signal.
|
|
112
147
|
*/
|
|
113
148
|
function decideRerun(payload) {
|
|
114
149
|
const empty = (reason) => ({ should: false, checks: [], runIds: [], reason });
|
|
@@ -119,28 +154,15 @@ function decideRerun(payload) {
|
|
|
119
154
|
const failingNames = Array.isArray(rc.failing) ? rc.failing : [];
|
|
120
155
|
if (failingNames.length === 0) return empty('no failing required checks');
|
|
121
156
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!conclByName.has(f.name)) {
|
|
128
|
-
conclByName.set(f.name, String(f.conclusion || '').toUpperCase());
|
|
129
|
-
urlByName.set(f.name, f.jobUrl || f.detailsUrl || null);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const picked = [];
|
|
134
|
-
for (const name of failingNames) {
|
|
135
|
-
const concl = conclByName.get(name);
|
|
136
|
-
if (!concl) return empty(`required check "${name}" has no known conclusion — cannot confirm flaky, fail closed`);
|
|
137
|
-
if (REAL_FAILURE_CONCLUSIONS.has(concl)) return empty(`required check "${name}" is a real failure (${concl}) — never rerun`);
|
|
138
|
-
if (!INFRA_CONCLUSIONS.has(concl)) return empty(`required check "${name}" conclusion ${concl} is not classified infrastructural — fail closed`);
|
|
139
|
-
picked.push({ name, conclusion: concl, runId: runIdFromUrl(urlByName.get(name)) });
|
|
140
|
-
}
|
|
157
|
+
const selection = selectRerunChecks(
|
|
158
|
+
failingNames,
|
|
159
|
+
indexFailures(Array.isArray(payload.failures) ? payload.failures : []),
|
|
160
|
+
);
|
|
161
|
+
if (selection.reason) return empty(selection.reason);
|
|
141
162
|
|
|
163
|
+
const picked = selection.checks;
|
|
142
164
|
const runIds = [...new Set(picked.map((p) => p.runId).filter(Boolean))];
|
|
143
|
-
if (
|
|
165
|
+
if (picked.some((item) => !item.runId)) return empty('no run id derivable from failure jobUrl — fail closed');
|
|
144
166
|
return {
|
|
145
167
|
should: true,
|
|
146
168
|
checks: picked,
|
|
@@ -164,6 +186,123 @@ function decideAutoActions(payload, opts = {}) {
|
|
|
164
186
|
};
|
|
165
187
|
}
|
|
166
188
|
|
|
189
|
+
function boundedPolicyValue(value, fallback, maximum) {
|
|
190
|
+
const selected = value ?? fallback;
|
|
191
|
+
return Number.isSafeInteger(selected) && selected > 0 && selected <= maximum ? selected : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeActionHistory(history) {
|
|
195
|
+
if (!Array.isArray(history)) return { ok: false, reason: 'action history is incomplete' };
|
|
196
|
+
const byKey = new Map();
|
|
197
|
+
for (const item of history) {
|
|
198
|
+
if (!item || typeof item.key !== 'string' || !item.key
|
|
199
|
+
|| !['pending', 'applied', 'failed'].includes(item.state)
|
|
200
|
+
|| !Number.isSafeInteger(item.attempts) || item.attempts < 0) {
|
|
201
|
+
return { ok: false, reason: 'action history is incomplete' };
|
|
202
|
+
}
|
|
203
|
+
const prior = byKey.get(item.key);
|
|
204
|
+
if (prior && (prior.state !== item.state || prior.attempts !== item.attempts)) {
|
|
205
|
+
return { ok: false, conflict: true, reason: `action history conflicts for ${item.key}` };
|
|
206
|
+
}
|
|
207
|
+
if (!prior) byKey.set(item.key, item);
|
|
208
|
+
}
|
|
209
|
+
return { ok: true, byKey };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function validateAutoActionAuthority(payload) {
|
|
213
|
+
if (!payload?.evidence || !Array.isArray(payload.evidence.unreadable)
|
|
214
|
+
|| typeof payload.evidence.tornRead !== 'boolean') {
|
|
215
|
+
return { ok: false, reason: 'verdict evidence is incomplete' };
|
|
216
|
+
}
|
|
217
|
+
if (payload.draft !== false) {
|
|
218
|
+
return { ok: false, reason: 'non-draft authority is incomplete' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const failuresByName = new Map();
|
|
222
|
+
for (const failure of Array.isArray(payload.failures) ? payload.failures : []) {
|
|
223
|
+
if (!failure || typeof failure.name !== 'string' || !failure.name) continue;
|
|
224
|
+
const evidence = {
|
|
225
|
+
conclusion: String(failure.conclusion || '').toUpperCase(),
|
|
226
|
+
url: failure.jobUrl || failure.detailsUrl || null,
|
|
227
|
+
};
|
|
228
|
+
const prior = failuresByName.get(failure.name);
|
|
229
|
+
if (prior && (prior.conclusion !== evidence.conclusion || prior.url !== evidence.url)) {
|
|
230
|
+
return { ok: false, conflict: true, reason: `required check evidence conflicts for ${failure.name}` };
|
|
231
|
+
}
|
|
232
|
+
if (!prior) failuresByName.set(failure.name, evidence);
|
|
233
|
+
}
|
|
234
|
+
return { ok: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildAutoActionCandidates(decision, subjectRevision) {
|
|
238
|
+
const candidates = [];
|
|
239
|
+
if (decision.updateBranch.should) {
|
|
240
|
+
candidates.push({ type: 'updateBranch', key: `updateBranch:${subjectRevision}`, subjectRevision });
|
|
241
|
+
}
|
|
242
|
+
if (decision.rerunFlaky.should) {
|
|
243
|
+
const runIds = [...decision.rerunFlaky.runIds].sort((left, right) => left.localeCompare(right));
|
|
244
|
+
for (const runId of runIds) {
|
|
245
|
+
candidates.push({
|
|
246
|
+
type: 'rerunFlaky',
|
|
247
|
+
key: `rerunFlaky:${subjectRevision}:${runId}`,
|
|
248
|
+
subjectRevision,
|
|
249
|
+
runId,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return candidates;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function selectBoundedActions(candidates, history, maxActions, maxAttempts) {
|
|
257
|
+
const actions = [];
|
|
258
|
+
for (const candidate of candidates) {
|
|
259
|
+
const prior = history.get(candidate.key);
|
|
260
|
+
if (prior && (prior.state === 'pending' || prior.state === 'applied' || prior.attempts >= maxAttempts)) continue;
|
|
261
|
+
actions.push({ ...candidate, attempt: prior ? prior.attempts + 1 : 1 });
|
|
262
|
+
if (actions.length === maxActions) break;
|
|
263
|
+
}
|
|
264
|
+
return actions;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Select a deterministic, per-head, bounded action/retry batch. */
|
|
268
|
+
function decideBoundedAutoActions(payload, opts = {}) {
|
|
269
|
+
const closed = (status, reason) => ({ status, reason, actions: [] });
|
|
270
|
+
const subjectRevision = payload?.headSha;
|
|
271
|
+
if (typeof subjectRevision !== 'string' || !/^[0-9a-f]{40}$/i.test(subjectRevision)) {
|
|
272
|
+
return closed(AUTO_ACTION_STATUS.INCOMPLETE, 'exact head authority is incomplete');
|
|
273
|
+
}
|
|
274
|
+
if (opts.expectedRevision != null && opts.expectedRevision !== subjectRevision) {
|
|
275
|
+
return closed(AUTO_ACTION_STATUS.STALE, 'payload head does not match expected revision');
|
|
276
|
+
}
|
|
277
|
+
const authority = validateAutoActionAuthority(payload);
|
|
278
|
+
if (!authority.ok) {
|
|
279
|
+
return closed(authority.conflict ? AUTO_ACTION_STATUS.CONFLICT : AUTO_ACTION_STATUS.INCOMPLETE, authority.reason);
|
|
280
|
+
}
|
|
281
|
+
const maxActions = boundedPolicyValue(opts.maxActions, 1, MAX_AUTO_ACTIONS);
|
|
282
|
+
const maxAttempts = boundedPolicyValue(opts.maxAttempts, 2, MAX_AUTO_ATTEMPTS);
|
|
283
|
+
if (maxActions == null || maxAttempts == null) {
|
|
284
|
+
return closed(AUTO_ACTION_STATUS.INCOMPLETE, 'auto-action policy exceeds bounded limits');
|
|
285
|
+
}
|
|
286
|
+
const history = normalizeActionHistory(opts.history || []);
|
|
287
|
+
if (!history.ok) {
|
|
288
|
+
return closed(history.conflict ? AUTO_ACTION_STATUS.CONFLICT : AUTO_ACTION_STATUS.INCOMPLETE, history.reason);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const decision = decideAutoActions(payload, opts);
|
|
292
|
+
if (decision.updateBranch.should && decision.rerunFlaky.should) {
|
|
293
|
+
return closed(AUTO_ACTION_STATUS.CONFLICT, 'payload authorizes conflicting update and rerun actions');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const candidates = buildAutoActionCandidates(decision, subjectRevision);
|
|
297
|
+
const actions = selectBoundedActions(candidates, history.byKey, maxActions, maxAttempts);
|
|
298
|
+
if (actions.length === 0) {
|
|
299
|
+
return closed(AUTO_ACTION_STATUS.UNCHANGED, candidates.length === 0
|
|
300
|
+
? 'no safe auto-action transition'
|
|
301
|
+
: 'identical actions are pending, applied, or retry-exhausted');
|
|
302
|
+
}
|
|
303
|
+
return { status: AUTO_ACTION_STATUS.PASS, reason: 'bounded auto-actions selected', actions };
|
|
304
|
+
}
|
|
305
|
+
|
|
167
306
|
module.exports = {
|
|
168
307
|
decideAutoActions,
|
|
169
308
|
decideUpdateBranch,
|
|
@@ -172,4 +311,6 @@ module.exports = {
|
|
|
172
311
|
isDegraded,
|
|
173
312
|
INFRA_CONCLUSIONS,
|
|
174
313
|
REAL_FAILURE_CONCLUSIONS,
|
|
314
|
+
AUTO_ACTION_STATUS,
|
|
315
|
+
decideBoundedAutoActions,
|
|
175
316
|
};
|
package/lib/pr-monitor/differ.js
CHANGED
|
@@ -14,6 +14,107 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const { EVENT_TYPES: T, makeEvent } = require('./events');
|
|
17
|
+
const {
|
|
18
|
+
VERDICT_STATES,
|
|
19
|
+
normalizePrNumber,
|
|
20
|
+
normalizeRepository,
|
|
21
|
+
normalizeSha,
|
|
22
|
+
object,
|
|
23
|
+
readArrayItems,
|
|
24
|
+
readDataProperty,
|
|
25
|
+
} = require('./verdict');
|
|
26
|
+
|
|
27
|
+
const CURRENT_VERDICT_STATES = new Set(Object.values(VERDICT_STATES));
|
|
28
|
+
|
|
29
|
+
function compareText(left, right) {
|
|
30
|
+
if (left < right) return -1;
|
|
31
|
+
if (left > right) return 1;
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizedReasonCodes(verdict) {
|
|
36
|
+
const reasons = readArrayItems(readDataProperty(verdict, 'reasons').value);
|
|
37
|
+
if (!reasons) return null;
|
|
38
|
+
const codes = [];
|
|
39
|
+
for (const entry of reasons) {
|
|
40
|
+
const code = readDataProperty(entry, 'code').value;
|
|
41
|
+
if (!object(entry) || typeof code !== 'string' || !code.trim()) return null;
|
|
42
|
+
codes.push(code.trim());
|
|
43
|
+
}
|
|
44
|
+
return [...new Set(codes)].sort(compareText);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeCurrentVerdict(verdict, { absent = false } = {}) {
|
|
48
|
+
if (absent && verdict == null) {
|
|
49
|
+
return {
|
|
50
|
+
state: null, repository: null, prNumber: null, headSha: null, baseSha: null, reasons: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const record = object(verdict);
|
|
54
|
+
const state = readDataProperty(record, 'state').value;
|
|
55
|
+
const repository = normalizeRepository(readDataProperty(record, 'repository').value);
|
|
56
|
+
const prNumber = normalizePrNumber(readDataProperty(record, 'prNumber').value);
|
|
57
|
+
const headSha = normalizeSha(readDataProperty(record, 'headSha').value);
|
|
58
|
+
const baseSha = normalizeSha(readDataProperty(record, 'baseSha').value);
|
|
59
|
+
const valid = record && CURRENT_VERDICT_STATES.has(state)
|
|
60
|
+
&& repository && prNumber && headSha && baseSha;
|
|
61
|
+
const reasons = valid ? normalizedReasonCodes(verdict) : null;
|
|
62
|
+
if (!valid || reasons === null
|
|
63
|
+
|| (state === VERDICT_STATES.MERGE_READY && reasons.length > 0)
|
|
64
|
+
|| (state !== VERDICT_STATES.MERGE_READY && reasons.length === 0)) {
|
|
65
|
+
return {
|
|
66
|
+
state: VERDICT_STATES.INCOMPLETE,
|
|
67
|
+
repository: null,
|
|
68
|
+
prNumber: null,
|
|
69
|
+
headSha: null,
|
|
70
|
+
baseSha: null,
|
|
71
|
+
reasons: ['malformed_verdict'],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
state,
|
|
76
|
+
repository,
|
|
77
|
+
prNumber,
|
|
78
|
+
headSha,
|
|
79
|
+
baseSha,
|
|
80
|
+
reasons,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function difference(left, right) {
|
|
85
|
+
const rightSet = new Set(right);
|
|
86
|
+
return left.filter((value) => !rightSet.has(value));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Deterministically describe a transition between exact-current-head verdicts. */
|
|
90
|
+
function diffVerdictEvidence(previous, next) {
|
|
91
|
+
const before = normalizeCurrentVerdict(previous, { absent: true });
|
|
92
|
+
const after = normalizeCurrentVerdict(next);
|
|
93
|
+
const addedReasons = difference(after.reasons, before.reasons);
|
|
94
|
+
const removedReasons = difference(before.reasons, after.reasons);
|
|
95
|
+
const headChanged = before.headSha !== after.headSha;
|
|
96
|
+
const baseChanged = before.baseSha !== after.baseSha;
|
|
97
|
+
const repositoryChanged = before.repository !== after.repository;
|
|
98
|
+
const prChanged = before.prNumber !== after.prNumber;
|
|
99
|
+
const stateChanged = before.state !== after.state;
|
|
100
|
+
return {
|
|
101
|
+
changed: headChanged || baseChanged || repositoryChanged || prChanged || stateChanged
|
|
102
|
+
|| addedReasons.length > 0 || removedReasons.length > 0,
|
|
103
|
+
headChanged,
|
|
104
|
+
stateChanged,
|
|
105
|
+
baseChanged,
|
|
106
|
+
repositoryChanged,
|
|
107
|
+
prChanged,
|
|
108
|
+
from: before.state,
|
|
109
|
+
to: after.state,
|
|
110
|
+
repository: after.repository,
|
|
111
|
+
prNumber: after.prNumber,
|
|
112
|
+
headSha: after.headSha,
|
|
113
|
+
baseSha: after.baseSha,
|
|
114
|
+
addedReasons,
|
|
115
|
+
removedReasons,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
17
118
|
|
|
18
119
|
/** Index an array of objects by a key field into a Map. */
|
|
19
120
|
function indexBy(list, field) {
|
|
@@ -44,9 +145,9 @@ function diffChecks(prev, next, out) {
|
|
|
44
145
|
const before = prevByName.get(String(c.name));
|
|
45
146
|
const wasFailed = before?.class === 'failed';
|
|
46
147
|
if (c.class === 'failed' && !wasFailed) {
|
|
47
|
-
out.push(makeEvent(T.CHECK_FAILED, `${c.name}:${next.headSha}`, { name: c.name }));
|
|
148
|
+
out.push(makeEvent(T.CHECK_FAILED, `${c.name}:${next.headSha}`, { name: c.displayName || c.name }));
|
|
48
149
|
} else if (c.class === 'green' && wasFailed) {
|
|
49
|
-
out.push(makeEvent(T.CHECK_RECOVERED, `${c.name}:${next.headSha}`, { name: c.name }));
|
|
150
|
+
out.push(makeEvent(T.CHECK_RECOVERED, `${c.name}:${next.headSha}`, { name: c.displayName || c.name }));
|
|
50
151
|
}
|
|
51
152
|
}
|
|
52
153
|
if (allGreen(next.checks) && !allGreen(prev.checks)) {
|
|
@@ -92,7 +193,9 @@ function diffReviews(prev, next, out) {
|
|
|
92
193
|
|| before.commitOid !== r.commitOid;
|
|
93
194
|
if (changed) {
|
|
94
195
|
out.push(makeEvent(T.REVIEW_SUBMITTED, `${r.author}:${r.commitOid || r.submittedAt}`, {
|
|
95
|
-
author: r.
|
|
196
|
+
author: r.displayAuthor || r.author,
|
|
197
|
+
verdict: r.state,
|
|
198
|
+
commitOid: r.displayCommitOid || r.commitOid || null,
|
|
96
199
|
}));
|
|
97
200
|
}
|
|
98
201
|
}
|
|
@@ -139,7 +242,9 @@ function diffDegraded(prev, next, out) {
|
|
|
139
242
|
const prevSurfaces = new Set((prev.degraded || []).map((d) => d.surface));
|
|
140
243
|
for (const d of (next.degraded || [])) {
|
|
141
244
|
if (!prevSurfaces.has(d.surface)) {
|
|
142
|
-
out.push(makeEvent(T.MONITOR_DEGRADED, d.surface, {
|
|
245
|
+
out.push(makeEvent(T.MONITOR_DEGRADED, d.surface, {
|
|
246
|
+
surface: d.displaySurface || d.surface, error: d.error || '',
|
|
247
|
+
}));
|
|
143
248
|
}
|
|
144
249
|
}
|
|
145
250
|
}
|
|
@@ -190,6 +295,7 @@ function diffSnapshots(prev, next) {
|
|
|
190
295
|
|
|
191
296
|
module.exports = {
|
|
192
297
|
diffSnapshots,
|
|
298
|
+
diffVerdictEvidence,
|
|
193
299
|
allGreen,
|
|
194
300
|
indexBy,
|
|
195
301
|
};
|
package/lib/pr-monitor/events.js
CHANGED
|
Binary file
|