forgedock 1.30.0 → 1.32.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/bin/engine/outcome-codes.mjs +3 -2
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +409 -38
- package/bin/engine.mjs +47 -37
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/hooks/reread-nudge.mjs +65 -0
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +338 -2
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +150 -15
- package/bin/lean/conformance.test.mjs +175 -3
- package/bin/lean/context.mjs +128 -44
- package/bin/lean/context.test.mjs +157 -5
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +48 -15
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +712 -134
- package/bin/lean/lean.test.mjs +1626 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/lightlane.mjs +219 -0
- package/bin/lean/lightlane.test.mjs +237 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +303 -41
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +138 -13
- package/bin/lean/precheck.test.mjs +118 -1
- package/bin/lean/prefilter.mjs +220 -5
- package/bin/lean/prefilter.test.mjs +260 -4
- package/bin/lean/preflight.mjs +108 -3
- package/bin/lean/preflight.test.mjs +48 -1
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +104 -13
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.mjs +16 -1
- package/bin/lean/sched.test.mjs +175 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +170 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +296 -13
- package/bin/lean/sequence.test.mjs +263 -2
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +72 -10
- package/bin/lean/verify.test.mjs +68 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +335 -0
- package/bin/stats/efficiency.test.mjs +266 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +91 -6
- package/bin/stats/local-source.mjs +145 -8
- package/bin/stats/query.mjs +181 -3
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +403 -8
- package/commands/orchestrate.md +3 -1
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +80 -5
- package/docs/OUTCOME-CODES.md +2 -1
- package/docs/STATS.md +66 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/eval/corpus/lightlane-v1.json +55 -0
- package/docs/eval/lightlane-bench.md +91 -0
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/bench-lightlane.mjs +286 -0
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/trusted-comments.test.sh +1 -1
package/bin/lean/verify.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* failed jobs' log tails for the builder's fix loop. All GitHub effects go through the injected io.
|
|
7
7
|
*/
|
|
8
8
|
import { ciPollDelayMs } from "../engine.mjs";
|
|
9
|
+
import { parseE2e } from "./e2e.mjs";
|
|
9
10
|
|
|
10
11
|
export const VERIFY_MODES = Object.freeze(["local", "ci", "hybrid"]);
|
|
11
12
|
/** Changed files that never need the CI matrix (docs-only lanes skip CI verification). */
|
|
@@ -29,8 +30,10 @@ const OK = ["success", "neutral", "skipped"];
|
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* forge.yaml `verify:` section (block style; parseForgeYaml gives strings) → {mode, workflow, pathsInput, skipPaths,
|
|
32
|
-
* maxRounds, precheckTimeoutSeconds, precheckTotalSeconds, checkRun}. Invalid values fall back to the defaults; mode defaults to local (today's
|
|
33
|
-
* behaviour); checkRun (`check_run`, #4270) is true only for the literal `true
|
|
33
|
+
* maxRounds, precheckTimeoutSeconds, precheckTotalSeconds, checkRun, checks, postMerge, duplicateExports, e2e}. Invalid values fall back to the defaults; mode defaults to local (today's
|
|
34
|
+
* behaviour); checkRun (`check_run`, #4270) is true only for the literal `true`; checks (#4387) is the optional allow-list
|
|
35
|
+
* of check names that gate CI verification, or null; e2e is the e2e merge gate (#4403, e2e.mjs parseE2e). #4406: postMerge (`post_merge`) is the command run on
|
|
36
|
+
* the merged tree at base-sync before the push (null: the lane's scoped typecheck); duplicateExports is false only for the literal `false`.
|
|
34
37
|
*/
|
|
35
38
|
export function parseVerify(sec = {}) {
|
|
36
39
|
const s = sec && typeof sec === "object" ? sec : {};
|
|
@@ -47,7 +50,12 @@ export function parseVerify(sec = {}) {
|
|
|
47
50
|
const tt = Number(str(s.precheck_total_seconds));
|
|
48
51
|
const precheckTotalSeconds = str(s.precheck_total_seconds) !== "" && Number.isInteger(tt) && tt >= 1 && tt <= 7200 ? tt : DEFAULT_PRECHECK_TOTAL_SECONDS;
|
|
49
52
|
const checkRun = str(s.check_run).toLowerCase() === "true";
|
|
50
|
-
|
|
53
|
+
const rawChecks = Array.isArray(s.checks) ? s.checks : str(s.checks).split(",");
|
|
54
|
+
const names = rawChecks.map((x) => String(x ?? "").trim()).filter(Boolean);
|
|
55
|
+
const checks = names.length ? [...new Set(names)] : null;
|
|
56
|
+
const postMerge = str(s.post_merge) || null;
|
|
57
|
+
const duplicateExports = str(s.duplicate_exports).toLowerCase() !== "false";
|
|
58
|
+
return { mode, workflow, pathsInput, skipPaths, maxRounds, precheckTimeoutSeconds, precheckTotalSeconds, checkRun, checks, postMerge, duplicateExports, e2e: parseE2e(s) };
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
const globRe = (g) => new RegExp(`^${g.split("**").map((p) => p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")).join(".*")}$`);
|
|
@@ -83,15 +91,55 @@ export async function collectFailedLogs(io, failedChecks) {
|
|
|
83
91
|
}
|
|
84
92
|
return capLogs(out);
|
|
85
93
|
}
|
|
94
|
+
const RANK = { failure: 2, pending: 1, success: 0 };
|
|
95
|
+
/**
|
|
96
|
+
* #4387: one check per name at its latest attempt: the highest check-run id (ids grow monotonically, so a rerun or a later
|
|
97
|
+
* push/pull_request run of the same job wins). A name with any entry lacking a numeric id keeps its worst state.
|
|
98
|
+
*/
|
|
99
|
+
export function latestPerName(checks) {
|
|
100
|
+
const by = new Map();
|
|
101
|
+
for (const c of checks ?? []) { if (!by.has(c?.name)) by.set(c?.name, []); by.get(c?.name).push(c); }
|
|
102
|
+
return [...by.values()].map((xs) => xs.every((c) => Number.isFinite(Number(c?.id)) && c?.id !== null && c?.id !== "")
|
|
103
|
+
? xs.reduce((a, b) => (Number(b.id) > Number(a.id) ? b : a))
|
|
104
|
+
: xs.reduce((a, b) => ((RANK[b?.state] ?? 1) > (RANK[a?.state] ?? 1) ? b : a)));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* #4387: the check names that gate CI verification: forge.yaml `verify.checks`, else the base branch's required checks
|
|
108
|
+
* under `merge_wait: required`, else null (every check gates, as before). An unreadable or empty required set is null.
|
|
109
|
+
*/
|
|
110
|
+
export async function ciGate({ io, lane, mergeWait, verify }) {
|
|
111
|
+
if (verify?.checks?.length) return [...verify.checks];
|
|
112
|
+
if (mergeWait !== "required" || !io?.requiredChecks) return null;
|
|
113
|
+
let req = [];
|
|
114
|
+
try { req = (await io.requiredChecks(lane)) ?? []; } catch { req = []; }
|
|
115
|
+
return Array.isArray(req) && req.length ? req : null;
|
|
116
|
+
}
|
|
117
|
+
/** #4387: the `rerunOf` of the re-verify after a rerun of `v`'s failed jobs: [{name, id}] (commit checks) or {runId, attempt}. */
|
|
118
|
+
export const rerunTarget = (v) => (v?.failedChecks ? v.failedChecks.map((c) => ({ name: c.name, id: c.id ?? null })) : { runId: v?.runId ?? null, attempt: v?.attempt ?? null });
|
|
119
|
+
/**
|
|
120
|
+
* #4387: rerun the failed jobs of a red verification once (`io.rerunFailedJobs`, `gh run rerun <id> --failed`), so a
|
|
121
|
+
* no-commit fix round can tell a flake from a real failure. Returns {ok, runs, jobs, detail}; never throws.
|
|
122
|
+
*/
|
|
123
|
+
export async function rerunFailedOnce({ io, v }) {
|
|
124
|
+
const jobs = (v?.failures ?? []).map((f) => f.job);
|
|
125
|
+
const runs = v?.failedChecks ? [...new Set(v.failedChecks.map((c) => runIdOf(c.url)).filter(Boolean))] : v?.runId != null ? [String(v.runId)] : [];
|
|
126
|
+
if (!io?.rerunFailedJobs) return { ok: false, runs, jobs, detail: "no rerun seam" };
|
|
127
|
+
if (!runs.length) return { ok: false, runs, jobs, detail: "no Actions run to rerun" };
|
|
128
|
+
try { for (const id of runs) await io.rerunFailedJobs(id); }
|
|
129
|
+
catch (e) { return { ok: false, runs, jobs, detail: `rerun could not start: ${String(e?.stderr ?? e?.message ?? e).split("\n")[0].slice(0, 200)}` }; }
|
|
130
|
+
return { ok: true, runs, jobs, detail: `reran ${runs.length} run(s)` };
|
|
131
|
+
}
|
|
86
132
|
const sameSha = (a, b) => Boolean(a && b) && (String(a).startsWith(b) || String(b).startsWith(a));
|
|
87
133
|
|
|
88
134
|
/**
|
|
89
135
|
* One CI verification of `head` on `branch`. Returns {result: pass|fail|timeout|unavailable|skipped, failures, runUrl,
|
|
90
136
|
* detail, runId}. `push` is called before any CI call (never for a skipped lane); `exclude` lists run ids already judged,
|
|
91
137
|
* so a re-dispatch on the same head never reuses an old run; `onPoll` runs before each wait (lease renew, progress).
|
|
138
|
+
* #4387: `gate` (ciGate) limits which commit checks decide; each name counts at its latest attempt. `rerunOf`
|
|
139
|
+
* (rerunTarget) makes the re-verify after a rerun wait for newer attempts than the ones judged red.
|
|
92
140
|
*/
|
|
93
141
|
export async function runCiVerify({ io, branch, head, changedFiles, verify, ciMaxMinutes = 360, now = () => Date.now(), sleep, random = Math.random,
|
|
94
|
-
push = async () => {}, exclude = [], onPoll = async () => {}, graceMs = DEFAULT_GRACE_MS }) {
|
|
142
|
+
push = async () => {}, exclude = [], onPoll = async () => {}, graceMs = DEFAULT_GRACE_MS, gate = null, rerunOf = null }) {
|
|
95
143
|
const v = verify ?? parseVerify({});
|
|
96
144
|
const files = verifiableFiles(changedFiles);
|
|
97
145
|
if (skipsCi(files, v.skipPaths)) return { result: "skipped", failures: [], runUrl: null, detail: `all ${files.length} changed file(s) match skip_paths` };
|
|
@@ -105,27 +153,41 @@ export async function runCiVerify({ io, branch, head, changedFiles, verify, ciMa
|
|
|
105
153
|
catch (e) { return unavailable(`workflow ${v.workflow} could not be dispatched: ${String(e?.stderr ?? e?.message ?? e).split("\n")[0].slice(0, 200)}`); }
|
|
106
154
|
} else if (!io.commitChecks) return unavailable("no commit-checks seam");
|
|
107
155
|
const skip = new Set(exclude.map(String));
|
|
156
|
+
const rerunRun = rerunOf && !Array.isArray(rerunOf) && rerunOf.runId != null ? rerunOf : null;
|
|
157
|
+
if (rerunRun) skip.delete(String(rerunRun.runId));
|
|
158
|
+
const staleIds = new Map(Array.isArray(rerunOf) ? rerunOf.filter((c) => c?.id != null).map((c) => [c.name, Number(c.id)]) : []);
|
|
159
|
+
const gateSet = Array.isArray(gate) && gate.length ? new Set(gate) : null;
|
|
108
160
|
for (let i = 0; ; i++) {
|
|
109
161
|
if (v.workflow) {
|
|
110
162
|
let runs = [];
|
|
111
163
|
try { runs = (await io.workflowRuns({ workflow: v.workflow, branch })) ?? []; } catch { runs = []; }
|
|
112
164
|
const run = runs.find((r) => sameSha(r?.headSha, head) && !skip.has(String(r?.databaseId)));
|
|
113
|
-
|
|
165
|
+
const stale = run && rerunRun && String(run.databaseId) === String(rerunRun.runId) && !(Number(run.attempt) > Number(rerunRun.attempt ?? 0));
|
|
166
|
+
if (run && !stale && String(run.status).toLowerCase() === "completed") {
|
|
114
167
|
const c = String(run.conclusion ?? "").toLowerCase();
|
|
115
168
|
if (OK.includes(c)) return { result: "pass", failures: [], runUrl: run.url ?? null, runId: run.databaseId, detail: `${v.workflow} ${c}` };
|
|
116
169
|
let log = "";
|
|
117
170
|
try { log = String((await io.failedLogs?.(run.databaseId)) ?? ""); } catch { log = ""; }
|
|
118
|
-
return { result: "fail", failures: capLogs([{ job: run.name ?? v.workflow, url: run.url ?? null, log }]), runUrl: run.url ?? null, runId: run.databaseId, detail: `${v.workflow} ${c || "failed"}` };
|
|
171
|
+
return { result: "fail", failures: capLogs([{ job: run.name ?? v.workflow, url: run.url ?? null, log }]), runUrl: run.url ?? null, runId: run.databaseId, attempt: run.attempt ?? null, detail: `${v.workflow} ${c || "failed"}` };
|
|
119
172
|
}
|
|
120
173
|
if (!run && now() - startedAt >= graceMs) return unavailable(`no ${v.workflow} run for ${String(head).slice(0, 12)} appeared within ${Math.round(graceMs / 60000)} min`);
|
|
121
174
|
} else {
|
|
122
175
|
let checks = [];
|
|
123
176
|
try { checks = ((await io.commitChecks(head)) ?? []).filter((c) => c?.name !== VERIFY_CHECK_NAME); } catch { checks = []; }
|
|
124
177
|
if (!checks.length && now() - startedAt >= graceMs) return unavailable(`no checks reported for ${String(head).slice(0, 12)} within ${Math.round(graceMs / 60000)} min`);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
178
|
+
const latest = latestPerName(checks).map((c) => (staleIds.has(c.name) && !(Number(c.id) > staleIds.get(c.name)) ? { ...c, state: "pending" } : c));
|
|
179
|
+
// #4387: only gating checks decide; gate names not reported yet are waited on for the grace period, and with none reported every check gates.
|
|
180
|
+
const gating = gateSet ? latest.filter((c) => gateSet.has(c.name)) : [];
|
|
181
|
+
const relevant = gating.length ? gating : latest;
|
|
182
|
+
const missing = gateSet ? [...gateSet].filter((n) => !latest.some((c) => c.name === n)) : [];
|
|
183
|
+
const waitMissing = missing.length && now() - startedAt < graceMs;
|
|
184
|
+
if (relevant.length && !waitMissing && relevant.every((c) => c.state !== "pending")) {
|
|
185
|
+
const failed = relevant.filter((c) => c.state === "failure");
|
|
186
|
+
const ignored = gating.length ? latest.filter((c) => !gateSet.has(c.name) && c.state === "failure").map((c) => c.name) : [];
|
|
187
|
+
const note = ignored.length ? `; ignored non-gating: ${ignored.join(", ")}` : "";
|
|
188
|
+
if (!failed.length) return { result: "pass", failures: [], runUrl: null, detail: `${relevant.length} ${gating.length ? "gating " : ""}check(s) green${note}` };
|
|
189
|
+
return { result: "fail", failures: await collectFailedLogs(io, failed), failedChecks: failed.map((c) => ({ name: c.name, id: c.id ?? null, url: c.url ?? null })), runUrl: failed[0].url ?? null,
|
|
190
|
+
detail: `${failed.length} check(s) failed: ${failed.map((c) => c.name).join(", ")}${note}` };
|
|
129
191
|
}
|
|
130
192
|
}
|
|
131
193
|
if (now() - startedAt >= ciMaxMinutes * 60000) return { result: "timeout", failures: [], runUrl: null, detail: `CI still pending after ${ciMaxMinutes} min` };
|
package/bin/lean/verify.test.mjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
import { describe, it } from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
|
-
import { parseVerify, matchesGlob, skipsCi, capLogs, runCiVerify, collectFailedLogs, verifyReport, DEFAULT_SKIP_PATHS, LOG_CAP, VERIFY_CHECK_NAME, VERIFY_TEXT_CAP } from "./verify.mjs";
|
|
5
|
+
import { parseVerify, matchesGlob, skipsCi, capLogs, runCiVerify, collectFailedLogs, verifyReport, DEFAULT_SKIP_PATHS, LOG_CAP, VERIFY_CHECK_NAME, VERIFY_TEXT_CAP, latestPerName, ciGate, rerunFailedOnce, rerunTarget } from "./verify.mjs";
|
|
6
6
|
import { precheck } from "./conformance.mjs";
|
|
7
|
+
import { parseE2e } from "./e2e.mjs";
|
|
7
8
|
|
|
8
9
|
const HEAD = "abc123abc123abc123";
|
|
9
10
|
/** Fake io + clock: `runs` is a list of successive workflowRuns answers (the last repeats); `checks` likewise. */
|
|
@@ -22,15 +23,22 @@ function fake(o = {}) {
|
|
|
22
23
|
|
|
23
24
|
describe("verify-by-CI config (#4167)", () => {
|
|
24
25
|
it("parseVerify defaults to local and rejects invalid values", () => {
|
|
25
|
-
assert.deepEqual(parseVerify({}), { mode: "local", workflow: null, pathsInput: null, skipPaths: [...DEFAULT_SKIP_PATHS], maxRounds: 2, precheckTimeoutSeconds: 300, precheckTotalSeconds: 600, checkRun: false });
|
|
26
|
+
assert.deepEqual(parseVerify({}), { mode: "local", workflow: null, pathsInput: null, skipPaths: [...DEFAULT_SKIP_PATHS], maxRounds: 2, precheckTimeoutSeconds: 300, precheckTotalSeconds: 600, checkRun: false, checks: null, postMerge: null, duplicateExports: true, e2e: parseE2e({}) });
|
|
26
27
|
const v = parseVerify({ mode: "ci", workflow: ".github/workflows/verify.yml", paths_input: "changed_paths", skip_paths: "*.md, docs/** ,", max_rounds: "3" });
|
|
27
|
-
assert.deepEqual(v, { mode: "ci", workflow: ".github/workflows/verify.yml", pathsInput: "changed_paths", skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 300, precheckTotalSeconds: 600, checkRun: false });
|
|
28
|
+
assert.deepEqual(v, { mode: "ci", workflow: ".github/workflows/verify.yml", pathsInput: "changed_paths", skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 300, precheckTotalSeconds: 600, checkRun: false, checks: null, postMerge: null, duplicateExports: true, e2e: parseE2e({}) });
|
|
28
29
|
assert.equal(parseVerify({ mode: "remote" }).mode, "local");
|
|
29
30
|
assert.equal(parseVerify({ workflow: "x; rm -rf /" }).workflow, null);
|
|
30
31
|
assert.equal(parseVerify({ max_rounds: "-1" }).maxRounds, 2);
|
|
31
32
|
assert.equal(parseVerify({ max_rounds: "0" }).maxRounds, 0);
|
|
32
33
|
assert.equal(parseVerify({ mode: "hybrid" }).mode, "hybrid");
|
|
33
34
|
});
|
|
35
|
+
it("#4406 post_merge is a trimmed command or null; duplicate_exports is off only for the literal false", () => {
|
|
36
|
+
const v = parseVerify({ post_merge: " pnpm -r typecheck ", duplicate_exports: "false" });
|
|
37
|
+
assert.equal(v.postMerge, "pnpm -r typecheck");
|
|
38
|
+
assert.equal(v.duplicateExports, false);
|
|
39
|
+
assert.equal(parseVerify({ post_merge: " " }).postMerge, null);
|
|
40
|
+
assert.equal(parseVerify({ duplicate_exports: "no" }).duplicateExports, true);
|
|
41
|
+
});
|
|
34
42
|
it("globs: a pattern without / matches the basename anywhere; ** spans directories", () => {
|
|
35
43
|
assert.ok(matchesGlob("README.md", "*.md")); assert.ok(matchesGlob("pkg/a/CHANGELOG.md", "*.md"));
|
|
36
44
|
assert.ok(matchesGlob("docs/guide/x.png", "docs/**")); assert.ok(!matchesGlob("src/docs/x.ts", "docs/**"));
|
|
@@ -94,6 +102,63 @@ describe("runCiVerify (#4167)", () => {
|
|
|
94
102
|
});
|
|
95
103
|
});
|
|
96
104
|
|
|
105
|
+
describe("CI verification gate, latest attempt and flake rerun (#4387)", () => {
|
|
106
|
+
const CI = parseVerify({ mode: "ci" });
|
|
107
|
+
const run = (id) => `https://github.com/o/r/actions/runs/${id}/job/1`;
|
|
108
|
+
it("#4387 required checks gate: a red non-required storybook check passes when required lint and test are green", async () => {
|
|
109
|
+
const f = fake({ checks: [[{ name: "lint", state: "success", id: 1, url: run(1) }, { name: "test", state: "success", id: 2, url: run(1) }, { name: "storybook (web)", state: "failure", id: 3, url: run(2) }]] });
|
|
110
|
+
const r = await runCiVerify(f.args({ verify: CI, gate: ["lint", "test"] }));
|
|
111
|
+
assert.equal(r.result, "pass"); assert.match(r.detail, /ignored non-gating: storybook \(web\)/); assert.deepEqual(f.logsFor, []);
|
|
112
|
+
const g = fake({ checks: [[{ name: "lint", state: "failure", id: 1, url: run(1) }, { name: "test", state: "success", id: 2, url: run(1) }, { name: "storybook (web)", state: "failure", id: 3, url: run(2) }]] });
|
|
113
|
+
const red = await runCiVerify(g.args({ verify: CI, gate: ["lint", "test"] }));
|
|
114
|
+
assert.equal(red.result, "fail"); assert.deepEqual(red.failures.map((x) => x.job), ["lint"]);
|
|
115
|
+
assert.deepEqual(red.failedChecks, [{ name: "lint", id: 1, url: run(1) }]);
|
|
116
|
+
});
|
|
117
|
+
it("#4387 latest attempt per name: a later green run of a check counts as green; a later red one as red", async () => {
|
|
118
|
+
const f = fake({ checks: [[{ name: "test", state: "failure", id: 10, url: run(5) }, { name: "test", state: "success", id: 11, url: run(6) }, { name: "lint", state: "success", id: 9 }]] });
|
|
119
|
+
assert.equal((await runCiVerify(f.args({ verify: CI }))).result, "pass");
|
|
120
|
+
const g = fake({ checks: [[{ name: "test", state: "success", id: 10, url: run(5) }, { name: "test", state: "failure", id: 11, url: run(6) }]] });
|
|
121
|
+
assert.equal((await runCiVerify(g.args({ verify: CI }))).result, "fail");
|
|
122
|
+
// without ids the worst state of a name wins (unchanged conservative behaviour)
|
|
123
|
+
assert.deepEqual(latestPerName([{ name: "t", state: "failure" }, { name: "t", state: "success" }]).map((c) => c.state), ["failure"]);
|
|
124
|
+
});
|
|
125
|
+
it("#4387 a gate name not reported yet is waited on for the grace period; with none reported every check gates", async () => {
|
|
126
|
+
const f = fake({ checks: [[{ name: "lint", state: "success", id: 1 }], [{ name: "lint", state: "success", id: 1 }, { name: "test", state: "failure", id: 2, url: run(3) }]] });
|
|
127
|
+
assert.equal((await runCiVerify(f.args({ verify: CI, gate: ["lint", "test"] }))).result, "fail");
|
|
128
|
+
const g = fake({ checks: [[{ name: "storybook", state: "failure", id: 1, url: run(4) }]] });
|
|
129
|
+
const r = await runCiVerify(g.args({ verify: CI, gate: ["lint", "test"] }));
|
|
130
|
+
assert.equal(r.result, "fail"); assert.ok(g.clock >= 5 * 60000, "waited for the grace period first");
|
|
131
|
+
});
|
|
132
|
+
it("#4387 verify.checks overrides which checks gate CI verification", async () => {
|
|
133
|
+
const v = parseVerify({ mode: "ci", checks: "lint, test ," });
|
|
134
|
+
assert.deepEqual(v.checks, ["lint", "test"]);
|
|
135
|
+
assert.deepEqual(parseVerify({ checks: ["a", " b"] }).checks, ["a", "b"]);
|
|
136
|
+
const io = { requiredChecks: async () => ["build"] };
|
|
137
|
+
assert.deepEqual(await ciGate({ io, lane: "staging", mergeWait: "required", verify: v }), ["lint", "test"]);
|
|
138
|
+
assert.deepEqual(await ciGate({ io, lane: "staging", mergeWait: "required", verify: CI }), ["build"]);
|
|
139
|
+
assert.equal(await ciGate({ io, lane: "staging", mergeWait: "all", verify: CI }), null);
|
|
140
|
+
assert.equal(await ciGate({ io: { requiredChecks: async () => { throw new Error("404"); } }, lane: "staging", mergeWait: "required", verify: CI }), null);
|
|
141
|
+
const f = fake({ checks: [[{ name: "lint", state: "success", id: 1 }, { name: "test", state: "success", id: 2 }, { name: "build", state: "failure", id: 3, url: run(1) }]] });
|
|
142
|
+
assert.equal((await runCiVerify(f.args({ verify: v, gate: await ciGate({ io, lane: "staging", mergeWait: "required", verify: v }) }))).result, "pass");
|
|
143
|
+
});
|
|
144
|
+
it("#4387 rerunFailedOnce reruns each failed Actions run once; the re-verify waits for newer attempts", async () => {
|
|
145
|
+
const reran = [];
|
|
146
|
+
const v = { failures: [{ job: "test" }, { job: "types" }], failedChecks: [{ name: "test", id: 4, url: run(55) }, { name: "types", id: 5, url: run(55) }] };
|
|
147
|
+
const rr = await rerunFailedOnce({ io: { rerunFailedJobs: async (id) => { reran.push(id); } }, v });
|
|
148
|
+
assert.deepEqual([rr.ok, rr.runs, rr.jobs, reran], [true, ["55"], ["test", "types"], ["55"]]);
|
|
149
|
+
assert.equal((await rerunFailedOnce({ io: {}, v })).ok, false);
|
|
150
|
+
assert.match((await rerunFailedOnce({ io: { rerunFailedJobs: async () => { throw new Error("run too old"); } }, v })).detail, /run too old/);
|
|
151
|
+
const f = fake({ checks: [[{ name: "test", state: "failure", id: 4, url: run(55) }], [{ name: "test", state: "pending", id: 6 }], [{ name: "test", state: "success", id: 6 }]] });
|
|
152
|
+
const r = await runCiVerify(f.args({ verify: CI, rerunOf: rerunTarget({ failedChecks: [{ name: "test", id: 4 }] }) }));
|
|
153
|
+
assert.equal(r.result, "pass"); assert.ok(f.clock > 0, "the stale red attempt was not judged");
|
|
154
|
+
});
|
|
155
|
+
it("#4387 workflow mode: the re-verify waits for the same run's next attempt", async () => {
|
|
156
|
+
const f = fake({ runs: [[{ databaseId: 7, headSha: HEAD, status: "completed", conclusion: "failure", attempt: 1 }], [{ databaseId: 7, headSha: HEAD, status: "completed", conclusion: "success", attempt: 2 }]] });
|
|
157
|
+
const r = await runCiVerify(f.args({ exclude: [7], rerunOf: rerunTarget({ runId: 7, attempt: 1 }) }));
|
|
158
|
+
assert.equal(r.result, "pass"); assert.equal(r.runId, 7);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
97
162
|
describe("merge-time fix rounds and declared check commands (#4240)", () => {
|
|
98
163
|
it("collectFailedLogs fetches one log per Actions run and caps the total", async () => {
|
|
99
164
|
const fetched = [];
|