forgedock 1.46.0 → 1.48.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 +2 -1
- package/bin/engine-cli.mjs +22 -8
- package/bin/lean/conformance.e2e.test.mjs +20 -0
- package/bin/lean/conformance.mjs +99 -9
- package/bin/lean/conformance.test.mjs +121 -4
- package/bin/lean/context.mjs +108 -6
- package/bin/lean/context.test.mjs +84 -2
- package/bin/lean/coupled.mjs +38 -0
- package/bin/lean/coupled.test.mjs +57 -0
- package/bin/lean/decision-engine.mjs +5 -1
- package/bin/lean/decision-engine.test.mjs +21 -0
- package/bin/lean/drift.mjs +3 -2
- package/bin/lean/drift.test.mjs +23 -1
- package/bin/lean/effects.mjs +3 -2
- package/bin/lean/execution.mjs +41 -8
- package/bin/lean/execution.test.mjs +72 -2
- package/bin/lean/integration.mjs +2 -2
- package/bin/lean/integration.test.mjs +22 -1
- package/bin/lean/issuewatch.mjs +134 -37
- package/bin/lean/issuewatch.test.mjs +123 -1
- package/bin/lean/lane.mjs +13 -0
- package/bin/lean/lane.test.mjs +28 -1
- package/bin/lean/lean.mjs +60 -12
- package/bin/lean/lean.test.mjs +80 -0
- package/bin/lean/limits.mjs +6 -2
- package/bin/lean/limits.test.mjs +86 -0
- package/bin/lean/milestonesync.mjs +2 -2
- package/bin/lean/milestonesync.test.mjs +22 -0
- package/bin/lean/orchestrate.mjs +79 -9
- package/bin/lean/probe.mjs +4 -2
- package/bin/lean/probe.test.mjs +4 -0
- package/bin/lean/prompts.mjs +18 -11
- package/bin/lean/rotator.mjs +96 -0
- package/bin/lean/rotator.test.mjs +143 -0
- package/bin/lean/routecheck.mjs +48 -0
- package/bin/lean/routecheck.test.mjs +69 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +9 -0
- package/bin/lean/sandbox-remote.mjs +4 -2
- package/bin/lean/verify.mjs +2 -1
- package/bin/review-staging.mjs +20 -3
- package/commands/deploy-info.md +4 -4
- package/commands/issue.md +6 -1
- package/commands/review-pr-agents/frontend.md +1 -0
- package/commands/review-pr-agents/infra.md +2 -0
- package/commands/review-pr-staging.md +2 -0
- package/commands/work-on/build/context.md +7 -1
- package/commands/work-on/investigate.md +15 -3
- package/docs/CONFIG.md +21 -2
- package/docs/ISSUE-WATCH.md +22 -1
- package/docs/OUTCOME-CODES.md +2 -1
- package/package.json +1 -1
- package/scripts/file-deferred-followups.sh +125 -28
- package/scripts/file-deferred-followups.test.sh +49 -0
- package/scripts/issue-create.sh +7 -0
- package/scripts/issue-create.test.sh +13 -0
- package/scripts/issue-dedup.sh +5 -4
- package/scripts/issue-dedup.test.sh +15 -0
- package/scripts/owner-route-parity.test.sh +32 -0
- package/scripts/plan-validate-live.mjs +18 -6
- package/scripts/prior-attempts.mjs +72 -0
- package/scripts/prior-attempts.test.mjs +52 -0
- package/scripts/review-finalize.sh +35 -4
- package/scripts/review-finalize.test.sh +10 -0
- package/scripts/review-prepare.sh +16 -4
- package/scripts/review-prepare.test.sh +8 -0
- package/scripts/review-roster.sh +3 -1
- package/scripts/review-roster.test.sh +30 -2
- package/scripts/review-staging-pre.sh +8 -6
- package/scripts/review-staging-pre.test.sh +11 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
/**
|
|
5
|
+
* scripts/prior-attempts.mjs — prior attempts on an issue's subject (#5085). Deterministic, no LLM.
|
|
6
|
+
*
|
|
7
|
+
* Extracts the subject terms (error/outcome codes, symbols, title words) from a title and body, searches closed issues
|
|
8
|
+
* and merged PRs by those terms (GitHub search) plus the whole git history (`git log --grep`), whatever files they
|
|
9
|
+
* touched, and prints a `## Prior Attempts` table. Same search as the context pack's subject rows (bin/lean/context.mjs).
|
|
10
|
+
* Used by /issue (filing time) and as the live fallback when the Forge Ledger recall index is absent.
|
|
11
|
+
*
|
|
12
|
+
* Usage: node scripts/prior-attempts.mjs --repo OWNER/NAME --title T [--body-file F] [--issue N] [--base REF] [--json]
|
|
13
|
+
* Exit: 0 table printed (also when nothing is found), 1 a search failed (stderr says which), 2 usage error.
|
|
14
|
+
*/
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { subjectHistory } from "../bin/lean/context.mjs";
|
|
19
|
+
|
|
20
|
+
const USAGE = "usage: prior-attempts.mjs --repo OWNER/NAME --title T [--body-file F] [--issue N] [--base REF] [--json]";
|
|
21
|
+
|
|
22
|
+
const KEYS = { "--repo": "repo", "--title": "title", "--body-file": "bodyFile", "--issue": "issue", "--base": "base" };
|
|
23
|
+
|
|
24
|
+
export function parseArgs(argv) {
|
|
25
|
+
const o = { json: false };
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
const a = argv[i];
|
|
28
|
+
if (a === "--json") o.json = true;
|
|
29
|
+
else if (a === "-h" || a === "--help") o.help = true;
|
|
30
|
+
else if (Object.hasOwn(KEYS, a)) {
|
|
31
|
+
if (i + 1 >= argv.length) throw new Error(`${a} needs a value`);
|
|
32
|
+
o[KEYS[a]] = argv[++i];
|
|
33
|
+
} else throw new Error(`unknown argument ${a}`);
|
|
34
|
+
}
|
|
35
|
+
return o;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Backslashes first, then pipes: a title ending in "\" must not turn the cell's "\|" back into a column break (CodeQL js/incomplete-sanitization).
|
|
39
|
+
const cell = (s) => String(s ?? "").replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
40
|
+
const shipped = (r) => (r.status === "merged" ? "yes" : r.status === "not_planned" || r.status === "closed unmerged" ? "no" : "closed");
|
|
41
|
+
|
|
42
|
+
/** The `## Prior Attempts` markdown for a subjectHistory result; columns 2 and 4 are left for the drafter. */
|
|
43
|
+
export function renderPriorAttempts({ terms = [], rows = [] } = {}) {
|
|
44
|
+
const out = ["## Prior Attempts", ""];
|
|
45
|
+
if (!rows.length) out.push(`None found (terms: ${terms.length ? terms.join(", ") : "none"}).`);
|
|
46
|
+
else {
|
|
47
|
+
out.push("| Issue/PR | What it changed | Shipped? | Why it didn't hold |", "|---|---|---|---|");
|
|
48
|
+
for (const r of rows) out.push(`| #${r.number} ${cell(r.title)} (${r.kind}, ${r.status}) | ? | ${shipped(r)} | ? |`);
|
|
49
|
+
out.push("", `Search terms: ${terms.join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
return out.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const run = (cmd) => (args, opts = {}) => new Promise((resolve, reject) =>
|
|
55
|
+
execFile(cmd, args, { maxBuffer: 16 * 1024 * 1024, ...opts }, (e, stdout, stderr) => (e ? reject(new Error(String(stderr || e.message).trim())) : resolve(stdout))));
|
|
56
|
+
|
|
57
|
+
/** CLI body; `io` {gh, git, read, out, err} is injectable for tests. Returns the exit code. */
|
|
58
|
+
export async function main(argv, io = {}) {
|
|
59
|
+
const out = io.out ?? ((s) => process.stdout.write(`${s}\n`)), err = io.err ?? ((s) => process.stderr.write(`${s}\n`));
|
|
60
|
+
let o;
|
|
61
|
+
try { o = parseArgs(argv); } catch (e) { err(`${e.message}\n${USAGE}`); return 2; }
|
|
62
|
+
if (o.help) { out(USAGE); return 0; }
|
|
63
|
+
if (!o.repo || !/^[\w.-]+\/[\w.-]+$/.test(o.repo) || !o.title) { err(USAGE); return 2; }
|
|
64
|
+
let body = "";
|
|
65
|
+
if (o.bodyFile) { try { body = (io.read ?? ((p) => readFileSync(p, "utf8")))(o.bodyFile); } catch (e) { err(`cannot read --body-file: ${e.message}`); return 2; } }
|
|
66
|
+
const r = await subjectHistory({ io: { gh: io.gh ?? run("gh"), git: io.git ?? run("git") }, repo: o.repo, issue: o.issue ? Number(o.issue) : null, title: o.title, body, base: o.base ?? "HEAD" });
|
|
67
|
+
out(o.json ? JSON.stringify(r) : renderPriorAttempts(r));
|
|
68
|
+
for (const e of r.errors) err(`WARNING: ${e}`);
|
|
69
|
+
return r.errors.length ? 1 : 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
import { describe, it } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { main, parseArgs, renderPriorAttempts } from "./prior-attempts.mjs";
|
|
6
|
+
|
|
7
|
+
const cap = () => { const o = { out: [], err: [] }; return { o, io: { out: (s) => o.out.push(s), err: (s) => o.err.push(s) } }; };
|
|
8
|
+
const item = (n, title, extra = {}) => JSON.stringify({ number: n, title, state_reason: "completed", pr: false, merged: false, ...extra });
|
|
9
|
+
|
|
10
|
+
describe("#5085: prior-attempts.mjs", () => {
|
|
11
|
+
it("prints a ## Prior Attempts table from closed issues and merged PRs found by subject terms, not files", async () => {
|
|
12
|
+
const { o, io } = cap();
|
|
13
|
+
const queries = [];
|
|
14
|
+
const gh = async (a) => { const q = a.find((x) => String(x).startsWith("q=")); queries.push(q);
|
|
15
|
+
return q.includes('"tls.bypass_misclassified"') ? [item(34683, "TLS bypass counted as block"), item(35204, "Retry TLS | bypass", { pr: true, merged: true }), item(34935, "won't fix", { state_reason: "not_planned" })].join("\n") : ""; };
|
|
16
|
+
const git = async () => `\x1e${"a".repeat(40)}\x1fMerge pull request #34735 from o/x\x1fClassify tls.bypass_misclassified as success\n`;
|
|
17
|
+
const code = await main(["--repo", "o/r", "--title", "fix: TLS bypass misclassified", "--body-file", "b.md", "--issue", "38355"], { ...io, gh, git, read: () => "Ends as `tls.bypass_misclassified` in src/a.ts" });
|
|
18
|
+
assert.equal(code, 0, o.err.join("\n"));
|
|
19
|
+
const t = o.out.join("\n");
|
|
20
|
+
assert.match(t, /^## Prior Attempts\n\n\| Issue\/PR \| What it changed \| Shipped\? \| Why it didn't hold \|/);
|
|
21
|
+
assert.ok(t.includes("| #35204 Retry TLS \\| bypass (PR, merged) | ? | yes | ? |"), t);
|
|
22
|
+
assert.ok(t.includes("| #34935 won't fix (issue, not_planned) | ? | no | ? |"), t);
|
|
23
|
+
assert.ok(t.includes("| #34683 TLS bypass counted as block (issue, completed) | ? | closed | ? |"), t);
|
|
24
|
+
assert.ok(t.includes("| #34735 Classify tls.bypass_misclassified as success (PR, merged) | ? | yes | ? |"), t);
|
|
25
|
+
assert.ok(t.includes("Search terms: tls.bypass_misclassified, bypass, misclassified"), t);
|
|
26
|
+
assert.ok(queries.every((q) => q.startsWith("q=repo:o/r is:closed ")) && !queries.some((q) => q.includes("src/a.ts")));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("nothing found prints the heading and the terms; a failed search exits 1 with a warning", async () => {
|
|
30
|
+
assert.equal(renderPriorAttempts({ terms: ["x.y"], rows: [] }), "## Prior Attempts\n\nNone found (terms: x.y).");
|
|
31
|
+
const { o, io } = cap();
|
|
32
|
+
assert.equal(await main(["--repo", "o/r", "--title", "Thin content billing"], { ...io, gh: async () => { throw new Error("HTTP 403"); }, git: async () => "" }), 1);
|
|
33
|
+
assert.ok(o.out[0].startsWith("## Prior Attempts") && o.err.some((e) => e.startsWith("WARNING: attempts: subject search")));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("a title with a backslash or pipe stays inside its table cell (CodeQL js/incomplete-sanitization, release review #5103)", () => {
|
|
37
|
+
const t = renderPriorAttempts({ terms: ["x"], rows: [{ number: 7, title: "path a\\|b ends in \\", kind: "issue", status: "merged" }] });
|
|
38
|
+
const row = t.split("\n").find((l) => l.startsWith("| #7"));
|
|
39
|
+
assert.ok(row.includes("a\\\\\\|b ends in \\\\ "), row);
|
|
40
|
+
assert.equal(row.split(/(?<!\\)\|/).length, 6, row);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("usage errors exit 2; --json prints the raw rows", async () => {
|
|
44
|
+
const { io } = cap();
|
|
45
|
+
assert.equal(await main(["--title", "x"], io), 2);
|
|
46
|
+
assert.equal(await main(["--repo", "o/r", "--bogus"], io), 2);
|
|
47
|
+
assert.throws(() => parseArgs(["--repo"]), /needs a value/);
|
|
48
|
+
const c = cap();
|
|
49
|
+
assert.equal(await main(["--repo", "o/r", "--title", "Thin content billing", "--json"], { ...c.io, gh: async () => item(9, "thin"), git: async () => "" }), 0);
|
|
50
|
+
assert.deepEqual(JSON.parse(c.o.out[0]).rows.map((r) => r.number), [9]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -266,7 +266,7 @@ do_extract() {
|
|
|
266
266
|
# carried head's, and only a VERIFIED carry record (trusted, same head, resolver agrees) selects that SHA. DELTA: reviewers
|
|
267
267
|
# saw only the delta, so the earlier fully reviewed head's findings are read too (they must still reach the Phase 8 gates);
|
|
268
268
|
# the 6B.4 head-SHA and provenance gates then drop any whose cited line is gone from the current head.
|
|
269
|
-
PRIMARY_SHA="$REVIEW_SHA"; EARLIER_SHA=""
|
|
269
|
+
PRIMARY_SHA="$REVIEW_SHA"; EARLIER_SHA=""; PRIMARY_FINDINGS=""
|
|
270
270
|
if [ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ]; then
|
|
271
271
|
CARRIED_FROM=$(carry_verified_from); [ -n "$CARRIED_FROM" ] && PRIMARY_SHA="$CARRIED_FROM"
|
|
272
272
|
fi
|
|
@@ -280,6 +280,8 @@ do_extract() {
|
|
|
280
280
|
if [ -n "$EARLIER_SHA" ]; then
|
|
281
281
|
EARLIER_BODIES=$(bodies_at "$EARLIER_SHA") && [ -n "$EARLIER_BODIES" ] \
|
|
282
282
|
|| { echo "REVIEW_RESULT: status: BLOCKED, blocker: earlier reviewed head ${EARLIER_SHA} findings unreadable (fail closed)"; exit 3; }
|
|
283
|
+
# forge#5081: the findings the delta-head reviewers themselves raised; an earlier-pass finding absent from them is "earlier only".
|
|
284
|
+
PRIMARY_FINDINGS=$(printf '%s\n' "$SOURCE_BODIES" | jq -r 'scan("<!-- FINDING:(.+?) -->") | .[0]' 2>/dev/null)
|
|
283
285
|
SOURCE_BODIES=$(printf '%s\n%s' "$SOURCE_BODIES" "$EARLIER_BODIES")
|
|
284
286
|
CARRIER_BODIES=$(printf '%s\n%s' "$CARRIER_BODIES" "$(agent_bodies_at "$EARLIER_SHA")")
|
|
285
287
|
fi
|
|
@@ -385,10 +387,18 @@ do_extract() {
|
|
|
385
387
|
| tr '[:upper:]' '[:lower:]' | sed -E 's/.*(fails|regresses).*/\1/')
|
|
386
388
|
TEXT_FILE="$WORKDIR/text.$N_EXTRACTED"
|
|
387
389
|
{ printf '%s\n%s\n' "$F_TITLE" "$F_FILE"; cat "$SECTION_FILE" 2>/dev/null; } > "$TEXT_FILE"
|
|
390
|
+
# DELTA: earlier_only = raised only at the earlier head (not by the delta reviewers, not re-raised on the same file within 5 lines).
|
|
391
|
+
EARLIER_ONLY=false
|
|
392
|
+
if [ -n "$EARLIER_SHA" ] && ! printf '%s\n' "$PRIMARY_FINDINGS" | grep -qxF -- "${F_ID}|${F_CONF}|${F_SEV}|${F_LOC}|${F_TITLE}" \
|
|
393
|
+
&& ! printf '%s\n' "$PRIMARY_FINDINGS" | awk -F'|' -v f="$F_FILE" -v l="${L1:-0}" '{ loc = $4; i = match(loc, /:[0-9]+(-[0-9]+)?$/)
|
|
394
|
+
if (i) { file = substr(loc, 1, i - 1); split(substr(loc, i + 1), b, "-"); st = b[1] + 0 } else { file = loc; st = 0 }
|
|
395
|
+
if (file == f && (l + 0 == 0 || st == 0 || (st >= l - 5 && st <= l + 5))) found = 1 } END { exit !found }'; then
|
|
396
|
+
EARLIER_ONLY=true
|
|
397
|
+
fi
|
|
388
398
|
jq -n -c --arg id "$F_ID" --arg c "$F_CONF" --arg s "$F_SEV" --arg loc "$F_LOC" --arg file "$F_FILE" --arg l1 "$L1" --arg l2 "$L2" \
|
|
389
|
-
--arg title "$F_TITLE" --arg agent "$AGENT" --arg scen "$SCENARIO" --arg fe "$FIX_EFF" --rawfile text "$TEXT_FILE" \
|
|
399
|
+
--arg title "$F_TITLE" --arg agent "$AGENT" --arg scen "$SCENARIO" --arg fe "$FIX_EFF" --rawfile text "$TEXT_FILE" --argjson eo "$EARLIER_ONLY" \
|
|
390
400
|
'{id: $id, confidence: $c, severity: $s, loc: $loc, file: $file, line_start: ($l1 | tonumber? // null), line_end: ($l2 | tonumber? // null),
|
|
391
|
-
title: $title, agent: $agent, scenario: $scen, fix_efficacy: $fe, text: $text}' >> "$RAW"
|
|
401
|
+
title: $title, agent: $agent, scenario: $scen, fix_efficacy: $fe, text: $text} + (if $eo then {earlier_only: true} else {} end)' >> "$RAW"
|
|
392
402
|
done <<EOF
|
|
393
403
|
$FINDINGS
|
|
394
404
|
EOF
|
|
@@ -934,6 +944,21 @@ do_file_prepare() {
|
|
|
934
944
|
}
|
|
935
945
|
|
|
936
946
|
OUT_ITEMS="$WORKDIR/items.jsonl"; : > "$OUT_ITEMS"
|
|
947
|
+
# forge#5081: DELTA: the old-side line ranges (file, start, count) the delta rewrote or deleted; an earlier-only finding inside one was fixed there.
|
|
948
|
+
DELTA_OLD_RANGES="$WORKDIR/delta-old-ranges"; : > "$DELTA_OLD_RANGES"
|
|
949
|
+
if [ "$REVIEW_MODE" = "DELTA" ] && is_sha "$DELTA_FROM" && is_sha "$REVIEW_SHA"; then
|
|
950
|
+
_dtop=$(git rev-parse --show-toplevel 2>/dev/null) || _dtop=""
|
|
951
|
+
if [ -n "$_dtop" ]; then
|
|
952
|
+
{ git -C "$_dtop" diff -U0 --no-color --no-renames "$DELTA_FROM" "$REVIEW_SHA" 2>/dev/null || true; } | while IFS= read -r _l; do
|
|
953
|
+
case "$_l" in
|
|
954
|
+
"--- a/"*) _f="${_l#--- a/}" ;;
|
|
955
|
+
"@@ "*) _h="${_l#@@ -}"; _o="${_h%% *}"
|
|
956
|
+
case "$_o" in *,*) _oc="${_o#*,}" ;; *) _oc=1 ;; esac
|
|
957
|
+
printf '%s\t%s\t%s\n' "$_f" "${_o%%,*}" "$_oc" ;;
|
|
958
|
+
esac
|
|
959
|
+
done > "$DELTA_OLD_RANGES" || : > "$DELTA_OLD_RANGES"
|
|
960
|
+
fi
|
|
961
|
+
fi
|
|
937
962
|
TOTAL=$(jq '.findings | length' "$IN_FILE"); I=0; N_FILE=0
|
|
938
963
|
while [ "$I" -lt "$TOTAL" ]; do
|
|
939
964
|
F_ROUTE=$(fj .route); F_CLASS=$(fj .class)
|
|
@@ -954,7 +979,13 @@ do_file_prepare() {
|
|
|
954
979
|
else
|
|
955
980
|
DEDUP_RESULT=""; DEDUP_EXIT=0
|
|
956
981
|
fi
|
|
957
|
-
|
|
982
|
+
F_EARLIER_ONLY=$(fj .earlier_only)
|
|
983
|
+
if [ "$F_EARLIER_ONLY" = "true" ] && [ "$F_LINE" -gt 0 ] && [ -s "$DELTA_OLD_RANGES" ] \
|
|
984
|
+
&& awk -F'\t' -v f="$F_FILE" -v l="$F_LINE" '$1 == f && $3 + 0 > 0 && l >= $2 + 0 && l <= $2 + $3 - 1 { found = 1 } END { exit !found }' "$DELTA_OLD_RANGES"; then
|
|
985
|
+
# The delta rewrote the cited lines and its reviewers did not re-raise the finding: fixed in the delta, never re-filed.
|
|
986
|
+
ACTION="skip-fixed-in-delta"; NOTE="fixed in delta ${DELTA_FROM:0:7}..${REVIEW_SHA:0:7}"
|
|
987
|
+
echo "FIXED-IN-DELTA: ${F_FILE}:${F_LINE} was changed by the delta and not re-raised"
|
|
988
|
+
elif [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
958
989
|
ACTION="skip-dedup"; NOTE="$DEDUP_RESULT"; DEDUP_OF=$(printf '%s' "$DEDUP_RESULT" | sed -n 's/[^0-9]*\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
959
990
|
echo "DEDUP: Skipping — $DEDUP_RESULT"
|
|
960
991
|
elif [ "$DEDUP_EXIT" -eq 2 ]; then
|
|
@@ -772,6 +772,16 @@ mkfind K-4 CONFIRMED HIGH c.py 5 "Renamed defect"; putfind 1
|
|
|
772
772
|
jq '.findings |= map(. + {class: "ISSUE", disposition: "ISSUE x"})' "$WORK/out/findings.json" > "$WORK/out/classified.json"
|
|
773
773
|
run "$GOOD" file-prepare
|
|
774
774
|
want "delta mode: Source line marks own closed issue" '.items[0] | [.action, (.dedup_of | tostring)] | join(",")' worklist.json "skip-resolved,63"
|
|
775
|
+
# forge#5081: a finding raised only at the earlier head whose cited lines the delta rewrote is fixed in the delta, never re-filed
|
|
776
|
+
echo "changed" > "$REPO_DIR/c.py"; git -C "$REPO_DIR" commit -qam delta; SHA2=$(git -C "$REPO_DIR" rev-parse HEAD); git -C "$REPO_DIR" reset -q --hard "$SHA"
|
|
777
|
+
fp_setup; SHA_OVERRIDE="$SHA2" mkctx 1 DELTA "$SHA" 12
|
|
778
|
+
echo '[]' > "$FIX/issues-open.json"; echo '[]' > "$FIX/issues-closed.json"
|
|
779
|
+
mkfind K-5 CONFIRMED HIGH c.py 5 "Earlier-only defect"; mkfind K-6 CONFIRMED HIGH c.py 5 "Re-raised defect"; putfind 2
|
|
780
|
+
jq '.findings |= map(. + {class: "ISSUE", disposition: "ISSUE x"} + (if .id == "K-5" then {earlier_only: true} else {} end))' "$WORK/out/findings.json" > "$WORK/out/classified.json"
|
|
781
|
+
run "$GOOD" file-prepare
|
|
782
|
+
want "delta mode: earlier-only finding on rewritten lines is fixed in delta" '.items[] | select(.id == "K-5") | .action' worklist.json "skip-fixed-in-delta"
|
|
783
|
+
want "delta mode: a finding the delta reviewers re-raised is still filed" '.items[] | select(.id == "K-6") | .action' worklist.json "file"
|
|
784
|
+
SHA_OVERRIDE=""
|
|
775
785
|
# the dedup helper receives the title without the shared suffix
|
|
776
786
|
DEDUP_LOG="$WORK/dedup-args"; : > "$DEDUP_LOG"
|
|
777
787
|
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$1" >> "%s"\nexit 0\n' "$DEDUP_LOG" > "$GOOD/scripts/issue-dedup.sh"
|
|
@@ -498,15 +498,15 @@ if [ -n "$DELTA_FROM_OPT" ]; then
|
|
|
498
498
|
[ -n "$_dl" ] || _dl="[]"
|
|
499
499
|
fi
|
|
500
500
|
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$COMMENTS_JSON" ]; then
|
|
501
|
-
_db=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies '
|
|
501
|
+
_db=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies '^(?:[^\n]*\r?\n){0,4}<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->|^<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->|# PR Review Summary|^<!-- FORGE:GATE_(PASS|FAILURE) -->|<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity|<!-- FORGE:REVIEW_DEGRADED|<!-- FORGE:REVIEW_BLOCKED' 2>/dev/null) \
|
|
502
502
|
&& _dv=$(printf '%s\n' "$_db" | jq -s -r --arg f "$DELTA_FROM_OPT" --argjson L "$_dl" '
|
|
503
503
|
def shas($t; $re): [ $t | scan($re) | .[0] ];
|
|
504
504
|
def one($a): if ($a | length) == 1 then $a[0] else null end;
|
|
505
505
|
def rsha: "(?:^|\\n)Reviewed-SHA: ([0-9a-f]{40}|[0-9a-f]{64})\\r?(?:\\n|$)";
|
|
506
506
|
to_entries as $E
|
|
507
|
-
| [ $E[] | select((.value | test("\\A<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->")) and (.value | test("<!-- REVIEW-FINDINGS-START -->")))
|
|
507
|
+
| [ $E[] | select((.value | test("\\A(?:[^\\n]*\\r?\\n){0,4}<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->")) and (.value | test("<!-- REVIEW-FINDINGS-START -->")))
|
|
508
508
|
| select(one(shas(.value; rsha)) == $f)
|
|
509
|
-
| {i: .key, d: (.value | capture("\\A<!-- FORGE:REVIEW-AGENT:(?<d>[a-z0-9-]+) -->").d)} ] as $AG
|
|
509
|
+
| {i: .key, d: (.value | capture("\\A(?:[^\\n]*\\r?\\n){0,4}<!-- FORGE:REVIEW-AGENT:(?<d>[a-z0-9-]+) -->").d)} ] as $AG
|
|
510
510
|
| if ($AG | length) == 0 then "no-agent" else
|
|
511
511
|
($AG | map(.i) | min) as $first | ($AG | map(.d) | unique | length) as $nd
|
|
512
512
|
| ([ $E[] | .value as $t | select(
|
|
@@ -526,7 +526,19 @@ if [ -n "$DELTA_FROM_OPT" ]; then
|
|
|
526
526
|
case "$_dv" in
|
|
527
527
|
ok) REVIEW_MODE="DELTA"; DELTA_FROM="$DELTA_FROM_OPT" ;;
|
|
528
528
|
no-agent) block "--delta-from ${DELTA_FROM_OPT}: no trusted FORGE:REVIEW-AGENT comment with a findings block for that head, no earlier pass to credit: run a full review" ;;
|
|
529
|
-
no-proof)
|
|
529
|
+
no-proof)
|
|
530
|
+
# forge#5081: name what lacked a valid delivery (launched domains with none, comments whose marker or findings block is malformed)
|
|
531
|
+
_hint=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies '<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->' 2>/dev/null \
|
|
532
|
+
| jq -s -r --arg f "$DELTA_FROM_OPT" --argjson L "$_dl" '
|
|
533
|
+
def vstart: test("\\A(?:[^\\n]*\\r?\\n){0,4}<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->");
|
|
534
|
+
[ .[] | {d: capture("<!-- FORGE:REVIEW-AGENT:(?<d>[a-z0-9-]+) -->").d, start: vstart, blk: test("<!-- REVIEW-FINDINGS-START -->"), at: test("Reviewed-SHA: " + $f)}
|
|
535
|
+
| .ok = (.start and .blk and .at) ] as $A
|
|
536
|
+
| ([ $A[] | select(.ok) | .d ] | unique) as $ok
|
|
537
|
+
| [ $L[] | select(. as $d | $ok | index($d) | not) ] as $miss
|
|
538
|
+
| [ $A[] | select(.at and (.ok | not)) | "\(.d) (" + (if .start | not then "REVIEW-AGENT marker not within the first 5 lines" elif .blk | not then "no findings block" else "no usable Reviewed-SHA" end) + ")" ] as $mal
|
|
539
|
+
| (if ($miss | length) > 0 then "; launched domains without a valid delivery at that head: " + ($miss | join(", ")) else "" end)
|
|
540
|
+
+ (if ($mal | length) > 0 then "; malformed reviewer comment(s): " + ($mal | join(", ")) else "" end)' 2>/dev/null) || _hint=""
|
|
541
|
+
block "--delta-from ${DELTA_FROM_OPT}: no trusted proof that the earlier panel completed (staging summary, synthesized block or PR Review Summary for that head, nor every launched domain delivered at it)${_hint}: run a full review" ;;
|
|
530
542
|
degraded) block "--delta-from ${DELTA_FROM_OPT}: the earlier pass is degraded (review-panel-integrity / REVIEW_DEGRADED / REVIEW_BLOCKED after its agents): run a full review" ;;
|
|
531
543
|
*) block "--delta-from ${DELTA_FROM_OPT}: could not read the trusted review comments, no earlier pass to credit: run a full review" ;;
|
|
532
544
|
esac
|
|
@@ -250,6 +250,14 @@ prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "untrusted ear
|
|
|
250
250
|
eq "untrusted earlier pass blocker" '.blockers | join(" ") | test("no earlier pass to credit")' true
|
|
251
251
|
earlier_pass "$SHA_B"
|
|
252
252
|
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "no earlier pass at the from sha" 3
|
|
253
|
+
# forge#5081: a reviewer body that opens with a heading before the marker still counts as delivered
|
|
254
|
+
{ cmt "$(printf '## Security review\n\n%s' "$(agent_body "$SHA_A")")"; cmt "$(staging_summary "$SHA_A")"; } | comments_of
|
|
255
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "marker not on line 1 is accepted" 0; eq "marker-not-on-line-1 mode" .delta.mode DELTA
|
|
256
|
+
# ...and a malformed comment (marker buried past the first lines) is named in the blocker
|
|
257
|
+
late_body() { printf 'a\nb\nc\nd\ne\nf\n<!-- FORGE:REVIEW-AGENT:frontend -->\nReviewed-SHA: %s\n<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->' "$1"; }
|
|
258
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "$(late_body "$SHA_A")"; } | comments_of
|
|
259
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "no proof with a malformed reviewer comment" 3
|
|
260
|
+
eq "blocker names the malformed comment" '.blockers | join(" ") | test("frontend \\(REVIEW-AGENT marker not within the first 5 lines\\)")' true
|
|
253
261
|
earlier_pass "$SHA_D"
|
|
254
262
|
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_D"; rc_is "non-ancestor from sha" 3
|
|
255
263
|
eq "non-ancestor blocker" '.blockers | join(" ") | test("not an ancestor")' true
|
package/scripts/review-roster.sh
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
# exact value EXTENDED is STANDARD. An extended roster prints an extra DEPTH=extended line.
|
|
29
29
|
# Path triggers escalate in EVERY tier except full (which already runs the union): CONTRACT HIGH_RISK, cross-critical
|
|
30
30
|
# pairs, churn hot-spot, danger zone, bin/engine* (Concurrency + spec-cli), bin/ (spec-cli), auth/session/trust-filter
|
|
31
|
-
# files (Auth: auth*, not author*; oauth, jwt, login, password, permission, rbac, acl, token(s), api_key(s), credential, deps.py), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API).
|
|
31
|
+
# files (Auth: auth*, not author*; oauth, jwt, login, password, permission, rbac, acl, token(s), api_key(s), credential, deps.py), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API). web/app dirs, .tsx/.jsx, next.config.*, middleware.ts (Frontend; forge#5077).
|
|
32
32
|
# Auth and Infra are path-gated: a diff-text score alone never selects them. Security is a hard floor in every tier.
|
|
33
33
|
# spec-cli is added only with --spec-cli true (the persona file exists in the repo under review); in the standard tier it
|
|
34
34
|
# also covers commands/**. Bash 3.2 portable (no mapfile, associative arrays, grep -P).
|
|
@@ -198,8 +198,10 @@ if printf '%s\n' "$AUTH_FILES" | grep -qiE '(^|[/_.-])(auth([^o]|o[^r]|$)|author
|
|
|
198
198
|
add_agent Auth; trig auth-path
|
|
199
199
|
fi
|
|
200
200
|
hit '^\.github/|(^|/)Dockerfile|docker-compose|(^|/)(docker-)?compose[^/]*\.ya?ml$|(^|/)deploy[^/]*\.sh$|(^|/)docker-entrypoint[^/]*$|(^|/)(deploy|infra|traefik|nginx|k8s|helm|terraform)/|\.tf$' && { add_agent Infra; trig infra-path; }
|
|
201
|
+
hit '(^|/)app/(.*/)?route\.[cm]?[jt]sx?$|(^|/)pages/api/' && { add_agent Infra; trig route-handler; } # forge#5087: a new HTTP route needs its ingress confirmed
|
|
201
202
|
hit 'migration|\.sql$' && { add_agent Database; trig migration-path; }
|
|
202
203
|
hit '^sdk/|openapi.*\.json$|openapi-versions/' && { add_agent API; trig sdk-path; }
|
|
204
|
+
hit '(^|/)(web|app)/|\.(tsx|jsx)$|(^|/)next\.config\.[a-z]+$|(^|/)middleware\.(ts|js)$' && { add_agent Frontend; trig frontend-path; }
|
|
203
205
|
hit '^bin/engine' && { add_agent Concurrency; trig bin-engine; }
|
|
204
206
|
if [ "$SPEC_CLI" = "true" ]; then
|
|
205
207
|
hit '^bin/' && { add_agent spec-cli; trig bin; }
|
|
@@ -164,6 +164,15 @@ run "$(printf 'README.md\nscripts/deploy.sh')" --scores "$SC_QUIET" --changed-li
|
|
|
164
164
|
expect_has "infra-path-multi-file-list" infra
|
|
165
165
|
run "Scripts/Deploy.SH" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
166
166
|
expect_has "infra-path-case-insensitive" infra
|
|
167
|
+
# forge#5087: a route-handler diff pulls the Infra reviewer even when no infra file changed
|
|
168
|
+
for f in "web/src/app/(site)/api/v1/agent/signup/pending/route.ts" "web/src/pages/api/health.ts" "app/route.js"; do
|
|
169
|
+
run "$f" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
170
|
+
expect_has "route-handler-$f" infra
|
|
171
|
+
done
|
|
172
|
+
for f in web/src/app/page.tsx web/src/app/dashboard/layout.tsx web/src/lib/route.ts; do
|
|
173
|
+
run "$f" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
174
|
+
expect_not "no-route-handler-$f" infra
|
|
175
|
+
done
|
|
167
176
|
run "db/migrations/0042_add.sql" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
168
177
|
expect_has "migration path" database
|
|
169
178
|
run "sdk/node/src/index.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
@@ -182,8 +191,8 @@ expect_has "billing score in medium tier" billing; expect_has "billing score in
|
|
|
182
191
|
run "web/src/app/a.tsx
|
|
183
192
|
web/src/b.tsx" --scores "FRONTEND=3 API=2 BILLING=1 DATABASE=1" --changed-lines 300
|
|
184
193
|
expect_tier "standard" standard; expect_has "standard" frontend; expect_has "standard" api; expect_not "standard top-2 only" billing
|
|
185
|
-
run "
|
|
186
|
-
|
|
194
|
+
run "svc/a.py
|
|
195
|
+
svc/b.py" --scores "FRONTEND=3 API=2" --changed-lines 100
|
|
187
196
|
expect_sel "medium has no baseline" "security"
|
|
188
197
|
run "svc/x.py" --scores "FRONTEND=3" --changed-lines ""
|
|
189
198
|
expect_tier "unknown size" standard; expect_has "unknown size" frontend
|
|
@@ -250,5 +259,24 @@ expect_tier "EXTENDED lifts fix-pr to standard" standard
|
|
|
250
259
|
run "svc/a.py" --scores "$SC_TOP" --changed-lines 400 --thorough true --review-depth EXTENDED
|
|
251
260
|
expect_tier "full stays full" full
|
|
252
261
|
|
|
262
|
+
# forge#5077: Frontend path trigger escalates in every tier except full; a bundle selects every path-triggered domain
|
|
263
|
+
for n in 30 100 300; do
|
|
264
|
+
run "web/src/app/(site)/not-found.tsx" --scores "$SC_QUIET" --changed-lines "$n" --changed-files 1
|
|
265
|
+
expect_has "frontend path trigger at $n lines" frontend
|
|
266
|
+
done
|
|
267
|
+
for f in next.config.mjs middleware.ts app/page.jsx; do
|
|
268
|
+
run "$f" --scores "$SC_QUIET" --changed-lines 30 --changed-files 1; expect_has "frontend path $f" frontend
|
|
269
|
+
done
|
|
270
|
+
run "web/src/app/(site)/not-found.tsx
|
|
271
|
+
svc/auth/login.py
|
|
272
|
+
.github/workflows/ci.yml
|
|
273
|
+
db/migrations/001.sql
|
|
274
|
+
sdk/client.py" --scores "FRONTEND=0 API=0 BILLING=2 DATABASE=2" --changed-lines 700000 --changed-files 5
|
|
275
|
+
expect_tier "staging bundle" standard
|
|
276
|
+
for d in frontend auth infra database api; do expect_has "bundle union" "$d"; done
|
|
277
|
+
run "docs/web-notes.md
|
|
278
|
+
svc/a.py" --scores "$SC_QUIET" --changed-lines 30 --changed-files 2
|
|
279
|
+
expect_not "no frontend path" frontend
|
|
280
|
+
|
|
253
281
|
echo "review-roster: $pass passed, $fail failed"
|
|
254
282
|
[ "$fail" -eq 0 ]
|
|
@@ -72,7 +72,7 @@ gh_retry() {
|
|
|
72
72
|
# One review-finding search fetch; fails (so gh_retry retries) on a gh error or non-array output.
|
|
73
73
|
fetch_findings() {
|
|
74
74
|
local raw
|
|
75
|
-
raw=$(gh issue list -R "$GH_REPO" --label "review-finding" --state open --search "PR #${1}" --limit 20 --json number,title,body 2>/dev/null) || return 1
|
|
75
|
+
raw=$(gh issue list -R "$GH_REPO" --label "review-finding" --state open --search "PR #${1}" --limit 20 --json number,title,body,labels 2>/dev/null) || return 1
|
|
76
76
|
printf '%s' "$raw" | jq -e 'type == "array"' >/dev/null 2>&1 || return 1
|
|
77
77
|
printf '%s' "$raw"
|
|
78
78
|
}
|
|
@@ -156,7 +156,9 @@ for pr_num in $ALL_PR_NUMBERS; do
|
|
|
156
156
|
# Search for open review-finding issues that reference this PR. The search is only a coarse
|
|
157
157
|
# candidate fetch (full-text, tokenized: it also hits hidden markers and longer numbers), so
|
|
158
158
|
# narrow it with the anchored FINDING_PR_FILTER: title suffix `PR #N)` or a `**Source**: PR #N` line.
|
|
159
|
-
|
|
159
|
+
# Only a blocking finding gates: labelled `blocking`, priority:P0/P1, or carrying no priority:* label (unknown severity fails closed).
|
|
160
|
+
# A priority:P2/P3 follow-up (NOTE-class, POSSIBLE, over review.max_filed_findings) never blocks the bundle.
|
|
161
|
+
FINDING_PR_FILTER='[.[] | select(([.labels[]?.name] | (index("blocking") != null) or (index("priority:P0") != null) or (index("priority:P1") != null) or (map(select(startswith("priority:"))) | length == 0))) | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
160
162
|
OPEN_FINDINGS=""
|
|
161
163
|
if _RAW=$(gh_retry --search fetch_findings "$pr_num"); then
|
|
162
164
|
OPEN_FINDINGS=$(printf '%s' "$_RAW" | jq -r --arg n "${pr_num}" "$FINDING_PR_FILTER" 2>/dev/null) || READ_FAILURES="$READ_FAILURES #${pr_num}(findings)" # uses FINDING_PR_FILTER
|
|
@@ -177,7 +179,7 @@ if [ -n "$READ_FAILURES" ]; then
|
|
|
177
179
|
echo "Re-run the gate once GitHub reads succeed."
|
|
178
180
|
echo "RESULT: BLOCK DEPLOY"
|
|
179
181
|
if [ -n "$PR_NUMBER" ]; then
|
|
180
|
-
gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
182
|
+
gh_retry gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
181
183
|
## Deploy Gate: BLOCKED
|
|
182
184
|
|
|
183
185
|
**Gate**: open-review-finding (read failure)
|
|
@@ -195,7 +197,7 @@ if [ -n "$DEGRADED_REVIEWS" ]; then
|
|
|
195
197
|
echo "⛔ DEPLOY BLOCKED — An included PR has an incomplete isolated review panel."
|
|
196
198
|
echo "$DEGRADED_REVIEWS"
|
|
197
199
|
if [ -n "$PR_NUMBER" ]; then
|
|
198
|
-
gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
200
|
+
gh_retry gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
199
201
|
## Deploy Gate: BLOCKED
|
|
200
202
|
|
|
201
203
|
**Gate**: review-panel-integrity
|
|
@@ -245,7 +247,7 @@ if [ -n "$BLOCKING_FINDINGS" ]; then
|
|
|
245
247
|
FINDING_COUNT=$(echo "$BLOCKING_FINDINGS" | grep -c '^\s*- #' || echo "unknown")
|
|
246
248
|
GATE_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
247
249
|
if [ -n "$PR_NUMBER" ]; then
|
|
248
|
-
gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
250
|
+
gh_retry gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
249
251
|
## Deploy Gate: BLOCKED
|
|
250
252
|
|
|
251
253
|
**Gate**: open-review-finding
|
|
@@ -274,7 +276,7 @@ else
|
|
|
274
276
|
echo "✅ Open review-finding gate: PASSED — no open findings for PRs in this bundle."
|
|
275
277
|
# Post FORGE:GATE_PASS for symmetric observability — bypass is indistinguishable from clean pass without this
|
|
276
278
|
if [ -n "$PR_NUMBER" ]; then
|
|
277
|
-
gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_PASS -->
|
|
279
|
+
gh_retry gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_PASS -->
|
|
278
280
|
## Deploy Gate: PASSED
|
|
279
281
|
|
|
280
282
|
**Gate**: open-review-finding check
|
|
@@ -39,7 +39,7 @@ case "$a" in "pr view"*"--json labels"*) [ -f "$F/fail-labels" ] && exit 1 ;; "i
|
|
|
39
39
|
flaky() { [ -f "$F/flaky-$1" ] || return 0; local c; c=$(cat "$F/count-$1" 2>/dev/null || echo 0); echo $((c+1)) > "$F/count-$1"; [ "$c" -lt "$(cat "$F/flaky-$1")" ] && exit 1; return 0; }
|
|
40
40
|
case "$a" in "pr view"*"--json labels"*) flaky labels ;; "issue list"*) flaky issues ;; "api repos/o/r/issues/"*) case "$a" in *"/comments"*) ;; *) flaky kind ;; esac ;; esac
|
|
41
41
|
case "$1 $2" in
|
|
42
|
-
"pr comment") while [ $# -gt 0 ]; do [ "$1" = --body ] && { printf '%s\n@@\n' "$2" >> "$F/posted"; }; shift; done; exit 0 ;;
|
|
42
|
+
"pr comment") flaky comment; while [ $# -gt 0 ]; do [ "$1" = --body ] && { printf '%s\n@@\n' "$2" >> "$F/posted"; }; shift; done; exit 0 ;;
|
|
43
43
|
"pr diff") [ -f "$F/files-$3" ] && { cat "$F/files-$3"; exit 0; }; exit 1 ;;
|
|
44
44
|
"pr view")
|
|
45
45
|
case "$a" in *"--json labels"*) [ -f "$F/degraded-$3" ] && echo true || echo false; exit 0 ;; esac
|
|
@@ -99,6 +99,16 @@ case "$OUT" in *"RESULT: BLOCK DEPLOY"*) ok ;; *) bad "blocked gate must print R
|
|
|
99
99
|
grep -q 'FORGE:GATE_FAILURE:TYPE=open-review-finding|FINDINGS=1' "$FIX/posted" && ok || bad "blocked gate must post the GATE_FAILURE marker with FINDINGS=1"
|
|
100
100
|
newfix unanchored; jq -n '[{number:41,title:"bug (PR #55)",body:"x"}]' > "$FIX/findings-5.json"
|
|
101
101
|
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; [ $? -eq 0 ] && ok || bad "a finding for another PR (#55) must not block #5"
|
|
102
|
+
# #5081: only blocking findings gate; a priority:P2/P3 follow-up does not, a priority:P1 or `blocking` one does
|
|
103
|
+
newfix nonblocking; jq -n '[{number:41,title:"nit (PR #5)",body:"x",labels:[{name:"review-finding"},{name:"priority:P3"}]}]' > "$FIX/findings-5.json"
|
|
104
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; [ $? -eq 0 ] && ok || bad "a priority:P3 review-finding must not block"
|
|
105
|
+
newfix blockp1; jq -n '[{number:41,title:"bug (PR #5)",body:"x",labels:[{name:"review-finding"},{name:"priority:P1"}]}]' > "$FIX/findings-5.json"
|
|
106
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; [ $? -eq 1 ] && ok || bad "a priority:P1 review-finding must block"
|
|
107
|
+
newfix blocklabel; jq -n '[{number:41,title:"bug (PR #5)",body:"x",labels:[{name:"review-finding"},{name:"priority:P3"},{name:"blocking"}]}]' > "$FIX/findings-5.json"
|
|
108
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; [ $? -eq 1 ] && ok || bad "a finding labelled blocking must block"
|
|
109
|
+
# #5081: a transient gh pr comment failure is retried, not dropped
|
|
110
|
+
newfix commentflaky; echo 2 > "$FIX/flaky-comment"
|
|
111
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; grep -qx '<!-- FORGE:GATE_PASS:TYPE=open-review-finding -->' "$FIX/posted" && ok || bad "a transient gh pr comment failure must be retried until the GATE_PASS lands"
|
|
102
112
|
newfix override; jq -n '[{number:41,title:"bug (PR #5)",body:"x"}]' > "$FIX/findings-5.json"
|
|
103
113
|
jq -n '[{body:"OVERRIDE: shipping with open findings — known",author_association:"OWNER",user:{type:"User",login:"o"}}]' > "$FIX/comments-9.json"
|
|
104
114
|
OUT=$(run gate --repo o/r --pr 9 --prs-file "$TMP/prs" 2>&1); RC=$?
|