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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-staging-post.test.sh — cases for scripts/review-staging-post.sh (forge#4359): the incomplete-panel guard, Phase 6.75
|
|
3
|
+
# agent delivery and the Phase 6.5 test-gate verdict handling (formerly extracted from commands/review-pr-staging.md fences).
|
|
4
|
+
# gh is a PATH stub reading per-case fixtures; trust decisions go through the real scripts/trusted-comments.sh.
|
|
5
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
POST="$ROOT/scripts/review-staging-post.sh"
|
|
10
|
+
SPEC="$ROOT/commands/review-pr-staging.md"
|
|
11
|
+
PASS=0; FAILN=0
|
|
12
|
+
ok() { PASS=$((PASS+1)); }
|
|
13
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
|
+
command -v jq >/dev/null 2>&1 || { echo "review-staging-post.test.sh: jq missing, skipped"; exit 0; }
|
|
15
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
16
|
+
export FORGEDOCK_HOME="$ROOT" # resolve helpers from this checkout, not an ambient install
|
|
17
|
+
HEAD=0123456789abcdef0123456789abcdef01234567
|
|
18
|
+
OTHER=fedcba9876543210fedcba9876543210fedcba98
|
|
19
|
+
blk() { awk -v n="$1" '$0 == "# BEGIN " n {f=1; next} $0 == "# END " n {f=0} f' "$POST"; }
|
|
20
|
+
|
|
21
|
+
# (1) the fences now live only in the script: the blocks exist, the spec no longer carries them
|
|
22
|
+
for b in panel-guard delivery-check test-gate-extract test-gate-verdict; do
|
|
23
|
+
[ -n "$(blk "$b")" ] && ok || bad "block $b missing from review-staging-post.sh"
|
|
24
|
+
done
|
|
25
|
+
if grep -qE 'ACTUAL_AGENT_COUNT=\$\(gh api|MISSING_AGENT_COMMENTS=""|GATE_VERDICT_INVALID=0' "$SPEC"; then bad "the spec must not carry a copy of the panel-guard, delivery or verdict-handling fence"; else ok; fi
|
|
26
|
+
# 6.75 carries the findings-block lookahead; the 6.75 empty-list guard precedes the per-domain loop
|
|
27
|
+
blk delivery-check | grep -qF 'count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->' && ok || bad "6.75 count must require the findings block"
|
|
28
|
+
G=$(blk delivery-check | grep -n 'launched-agent list is empty or unreadable' | head -1 | cut -d: -f1); L=$(blk delivery-check | grep -n '^for AGENT_DOMAIN in' | head -1 | cut -d: -f1)
|
|
29
|
+
[ -n "$G" ] && [ -n "$L" ] && [ "$G" -lt "$L" ] && ok || bad "6.75 empty-list guard must precede the for AGENT_DOMAIN loop"
|
|
30
|
+
# the producer never coerces an absent marker to SKIP; the catch-all arm never sets SKIP
|
|
31
|
+
if blk test-gate-extract | grep -q 'tail -1 || echo "SKIP"'; then bad "producer must not coerce an absent marker to SKIP"; else ok; fi
|
|
32
|
+
if blk test-gate-verdict | awk '/^ \*\)/,/;;/' | grep -q 'TEST_GATE_VERDICT="SKIP"'; then bad "catch-all arm must not set TEST_GATE_VERDICT=SKIP"; else ok; fi
|
|
33
|
+
|
|
34
|
+
# (2) usage errors
|
|
35
|
+
bash "$POST" bogus --repo o/r --pr 1 --sd "$TMP/u" >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unknown subcommand must exit 2"
|
|
36
|
+
bash "$POST" delivery --repo o/r --pr x --sd "$TMP/u" >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "non-numeric --pr must exit 2"
|
|
37
|
+
bash "$POST" delivery --repo o/r --pr 1 >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "missing --sd must exit 2"
|
|
38
|
+
bash "$POST" test-gate --repo o/r --pr 1 --sd "$TMP/u" --output x --prs-file "$TMP/none" >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unreadable --prs-file must exit 2"
|
|
39
|
+
bash "$POST" test-gate --repo o/r --pr 1 --sd "$TMP/u" --output x --prs-file /dev/null --posture lax >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unknown --posture must exit 2"
|
|
40
|
+
|
|
41
|
+
# gh stub: $FIX/comments.json is the PR's comment list; posted bodies and edits go to $FIX/posted / $FIX/edits
|
|
42
|
+
mkdir -p "$TMP/bin"
|
|
43
|
+
cat > "$TMP/bin/gh" <<'STUB'
|
|
44
|
+
#!/usr/bin/env bash
|
|
45
|
+
F="$FIX"; a="$*"
|
|
46
|
+
case "$1 $2" in
|
|
47
|
+
"pr comment") while [ $# -gt 0 ]; do [ "$1" = --body ] && printf '%s\n@@\n' "$2" >> "$F/posted"; shift; done; exit 0 ;;
|
|
48
|
+
"pr edit") printf '%s\n' "$a" >> "$F/edits"; exit 0 ;;
|
|
49
|
+
"label create") exit 0 ;;
|
|
50
|
+
"pr view") [ -f "$F/head" ] && { cat "$F/head"; exit 0; }; exit 1 ;;
|
|
51
|
+
esac
|
|
52
|
+
if [ "$1" = api ]; then case "$a" in *"/comments"*) cat "$F/comments.json" 2>/dev/null || exit 1; exit 0 ;; esac; fi
|
|
53
|
+
echo "gh stub: unhandled: $a" >&2; exit 1
|
|
54
|
+
STUB
|
|
55
|
+
printf '#!/bin/sh\nexit 1\n' > "$TMP/bin/yq" # no yq: defaults apply
|
|
56
|
+
chmod +x "$TMP/bin/gh" "$TMP/bin/yq"
|
|
57
|
+
export PATH="$TMP/bin:$PATH"
|
|
58
|
+
newfix() { FIX="$TMP/fix.$1"; rm -rf "$FIX"; mkdir -p "$FIX/sd"; export FIX; }
|
|
59
|
+
FB=$'<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->'
|
|
60
|
+
cm() { jq -n --arg b "$1" --arg t "${2:-Bot}" '{body:$b, author_association:"NONE", user:{type:$t, login:"x"}}'; }
|
|
61
|
+
agent() { printf '<!-- FORGE:REVIEW-AGENT:%s -->\nReviewed-SHA: %s\n\n%s' "$1" "${2:-$HEAD}" "${3-$FB}"; }
|
|
62
|
+
comments() { jq -s '.' > "$FIX/comments.json"; }
|
|
63
|
+
deliver() { bash "$POST" delivery --repo o/r --pr 42 --sd "$FIX/sd" --head "${1:-$HEAD}" 2>&1; echo "rc=$?"; }
|
|
64
|
+
|
|
65
|
+
# (3) delivery: a complete panel passes and posts nothing
|
|
66
|
+
newfix full; printf 'security\nmaterial-change\n' > "$FIX/sd/42_launched_review_agents"
|
|
67
|
+
{ cm "$(agent security)"; cm "$(agent material-change)"; } | comments
|
|
68
|
+
OUT=$(deliver); case "$OUT" in *"DELIVERY: OK (2/2"*"rc=0") ok ;; *) bad "complete panel must pass (got: $OUT)" ;; esac
|
|
69
|
+
[ ! -s "$FIX/posted" ] && ok || bad "a complete panel must post nothing"
|
|
70
|
+
# a duplicate domain line does not inflate the selected count
|
|
71
|
+
printf 'security\nsecurity\nmaterial-change\n' > "$FIX/sd/42_launched_review_agents"
|
|
72
|
+
OUT=$(deliver); case "$OUT" in *"rc=0") ok ;; *) bad "duplicate launched lines must count once (got: $OUT)" ;; esac
|
|
73
|
+
|
|
74
|
+
# (4) shortfalls: missing domain, block-less comment, untrusted author, other head -> panel-integrity failure
|
|
75
|
+
for c in missing noblock untrusted otherhead; do
|
|
76
|
+
newfix "$c"; printf 'security\nmaterial-change\n' > "$FIX/sd/42_launched_review_agents"
|
|
77
|
+
case "$c" in
|
|
78
|
+
missing) cm "$(agent security)" | comments ;;
|
|
79
|
+
noblock) { cm "$(agent security)"; cm "$(agent material-change "$HEAD" 'Verdict: APPROVE, no findings.')"; } | comments ;;
|
|
80
|
+
untrusted) { cm "$(agent security)"; cm "$(agent material-change)" User; } | comments ;;
|
|
81
|
+
otherhead) { cm "$(agent security)"; cm "$(agent material-change "$OTHER")"; } | comments ;;
|
|
82
|
+
esac
|
|
83
|
+
OUT=$(deliver)
|
|
84
|
+
case "$OUT" in *"rc=1") ok ;; *) bad "$c: delivery must fail (got: $OUT)" ;; esac
|
|
85
|
+
if [ "$c" = noblock ]; then
|
|
86
|
+
case "$OUT" in *"missing findings comment(s) for: material-change"*) ok ;; *) bad "noblock: 6.75 must name the block-less domain (got: $OUT)" ;; esac
|
|
87
|
+
else
|
|
88
|
+
grep -qF '<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->' "$FIX/posted" 2>/dev/null && ok || bad "$c: shortfall must post GATE_FAILURE:TYPE=review-panel-integrity"
|
|
89
|
+
grep -qF 'review-degraded' "$FIX/edits" 2>/dev/null && ok || bad "$c: shortfall must label the PR review-degraded"
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
# empty / absent launched list fails closed
|
|
93
|
+
newfix empty; : > "$FIX/sd/42_launched_review_agents"; cm "$(agent security)" | comments
|
|
94
|
+
OUT=$(deliver); case "$OUT" in *"rc=1") ok ;; *) bad "empty launched list must fail (got: $OUT)" ;; esac
|
|
95
|
+
newfix none; cm "$(agent security)" | comments
|
|
96
|
+
OUT=$(deliver); case "$OUT" in *"rc=1") ok ;; *) bad "absent launched list must fail (got: $OUT)" ;; esac
|
|
97
|
+
# unreadable comments count as 0 delivered
|
|
98
|
+
newfix unreadable; printf 'security\n' > "$FIX/sd/42_launched_review_agents"
|
|
99
|
+
OUT=$(deliver); case "$OUT" in *"rc=1") ok ;; *) bad "unreadable comments must fail closed (got: $OUT)" ;; esac
|
|
100
|
+
# a malformed --head with no resolvable head refuses
|
|
101
|
+
newfix badhead; printf 'security\n' > "$FIX/sd/42_launched_review_agents"; cm "$(agent security)" | comments
|
|
102
|
+
OUT=$(deliver "n/a"); case "$OUT" in *"rc=1") ok ;; *) bad "an unresolvable head must fail closed (got: $OUT)" ;; esac
|
|
103
|
+
# ...but a malformed --head is re-resolved from gh when it can be
|
|
104
|
+
printf '%s\n' "$HEAD" > "$FIX/head"
|
|
105
|
+
OUT=$(deliver "n/a"); case "$OUT" in *"rc=0") ok ;; *) bad "a malformed --head must be re-resolved from gh (got: $OUT)" ;; esac
|
|
106
|
+
|
|
107
|
+
# (5) test gate
|
|
108
|
+
tg() { bash "$POST" test-gate --repo o/r --pr 42 --sd "$FIX/sd" --output "$FIX/out" --prs-file "$FIX/prs" "$@" 2>&1; echo "rc=$?"; }
|
|
109
|
+
tfix() { newfix "tg$1"; printf '7\n8\n' > "$FIX/prs"; echo '[]' > "$FIX/comments.json"; }
|
|
110
|
+
v() { sed -n 1p "$FIX/sd/42_test_gate_verdict"; }
|
|
111
|
+
tfix skip; printf 'noise\nFORGE:TEST_GATE:RESULT=SKIP\n' > "$FIX/out"
|
|
112
|
+
OUT=$(tg); case "$OUT" in *"rc=0") ok ;; *) bad "SKIP must continue (got: $OUT)" ;; esac
|
|
113
|
+
[ "$(v)" = SKIP ] && ok || bad "SKIP must be persisted (got $(v))"
|
|
114
|
+
[ ! -s "$FIX/posted" ] && ok || bad "SKIP posts no gate marker from the test gate"
|
|
115
|
+
tfix pass; printf 'FORGE:TEST_GATE:RESULT=BLOCK\nFORGE:TEST_GATE:RESULT=PASS\n' > "$FIX/out"
|
|
116
|
+
OUT=$(tg); case "$OUT" in *"rc=0") ok ;; *) bad "PASS must continue (got: $OUT)" ;; esac
|
|
117
|
+
[ "$(v)" = PASS ] && ok || bad "the last marker wins (got $(v))"
|
|
118
|
+
grep -qF '<!-- FORGE:GATE_PASS:TYPE=test-gate|BUNDLE=7 8 -->' "$FIX/posted" && ok || bad "PASS must post GATE_PASS:TYPE=test-gate with the bundle"
|
|
119
|
+
for m in missing nofile; do
|
|
120
|
+
tfix "$m"; [ "$m" = missing ] && printf 'all good, no marker\n' > "$FIX/out"
|
|
121
|
+
OUT=$(tg); case "$OUT" in *"RESULT: BLOCK DEPLOY"*"rc=1") ok ;; *) bad "$m marker must fail closed as BLOCK (got: $OUT)" ;; esac
|
|
122
|
+
[ "$(v)" = BLOCK ] && ok || bad "$m: persisted verdict must be BLOCK, never SKIP (got $(v))"
|
|
123
|
+
grep -qF '<!-- FORGE:GATE_FAILURE:TYPE=test-gate|BUNDLE=7 8 -->' "$FIX/posted" && ok || bad "$m: blocking BLOCK must post GATE_FAILURE"
|
|
124
|
+
done
|
|
125
|
+
tfix advisory; printf 'FORGE:TEST_GATE:RESULT=BLOCK\n' > "$FIX/out"
|
|
126
|
+
OUT=$(tg --posture advisory); case "$OUT" in *"rc=0") ok ;; *) bad "advisory BLOCK must continue (got: $OUT)" ;; esac
|
|
127
|
+
grep -q 'advisory' "$FIX/sd/42_test_gate_reason" && ok || bad "advisory reason must be persisted"
|
|
128
|
+
tfix override; printf 'FORGE:TEST_GATE:RESULT=BLOCK\n' > "$FIX/out"
|
|
129
|
+
cm 'OVERRIDE: shipping with test failures — flaky infra, tracked in #9' | comments
|
|
130
|
+
OUT=$(tg); case "$OUT" in *"rc=0") ok ;; *) bad "a trusted override must downgrade BLOCK (got: $OUT)" ;; esac
|
|
131
|
+
[ "$(v)" = PASS ] && grep -q 'downgraded to PASS by override' "$FIX/sd/42_test_gate_reason" && ok || bad "override must persist PASS with the reason"
|
|
132
|
+
tfix untrustedov; printf 'FORGE:TEST_GATE:RESULT=BLOCK\n' > "$FIX/out"
|
|
133
|
+
cm 'OVERRIDE: shipping with test failures — me' User | comments
|
|
134
|
+
OUT=$(tg); case "$OUT" in *"rc=1") ok ;; *) bad "an untrusted override must not downgrade BLOCK (got: $OUT)" ;; esac
|
|
135
|
+
tfix phrase; printf 'FORGE:TEST_GATE:RESULT=BLOCK\n' > "$FIX/out"
|
|
136
|
+
cm 'SHIP IT (a.b) because' | comments
|
|
137
|
+
OUT=$(tg --override-phrase 'SHIP IT (a.b)'); case "$OUT" in *"rc=0") ok ;; *) bad "a configured override phrase must be honoured literally (got: $OUT)" ;; esac
|
|
138
|
+
# stale verdict from a prior run is reset before reading
|
|
139
|
+
tfix stale; printf 'PASS\n' > "$FIX/sd/42_test_gate_verdict"; printf 'nothing\n' > "$FIX/out"
|
|
140
|
+
OUT=$(tg); [ "$(v)" = BLOCK ] && ok || bad "a stale persisted PASS must not survive a missing marker (got $(v))"
|
|
141
|
+
|
|
142
|
+
echo "review-staging-post.test.sh: passed=$PASS failed=$FAILN"
|
|
143
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-staging-pre.sh — deterministic pre-dispatch of the staging->main review (forge#4358): the Phase 0A bundle PR set,
|
|
3
|
+
# the 0A open review-finding gate and the 0B.1 bundle review scope, moved out of commands/review-pr-staging.md.
|
|
4
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#
|
|
7
|
+
# Usage: review-staging-pre.sh bundle --repo R --default D --staging S --pr N --sd DIR
|
|
8
|
+
# review-staging-pre.sh gate --repo R --pr N --prs-file F
|
|
9
|
+
# review-staging-pre.sh scope --repo R --default D --staging S --pr N --prs-file F --sd DIR
|
|
10
|
+
# bundle fetches D and S, prints the bundle PR list and writes DIR/<N>_bundle-prs.txt (same file the spec persists).
|
|
11
|
+
# Exit 1 when the fetch fails or the bundle PR set is empty (fail closed: never an empty bundle to gate).
|
|
12
|
+
# gate open review-finding gate: posts FORGE:GATE_FAILURE / FORGE:GATE_PASS on PR N (bodies unchanged from the spec).
|
|
13
|
+
# Exit 0 = pass (or trusted override, last line "RESULT: PASS"), 1 = blocked. An empty --prs-file or any failed
|
|
14
|
+
# gh read blocks (TYPE=gate-read-failure, never waived by an override): no GATE_PASS without real reads (forge#4470).
|
|
15
|
+
# scope 0B.1: writes DIR/<N>_scope.env (printf %q, read by 7A), DIR/<N>_bundle-pr-files.txt and DIR/<N>_scope.json
|
|
16
|
+
# {scope_mode, reviewed_pr_count, reviewed_commits, unreviewed_commits, unreviewed_files, cross_pr_files,
|
|
17
|
+
# deploy_wiring_files}; prints the one-line SCOPE_MODE summary. Every failed read widens scope to full.
|
|
18
|
+
# Exit: 2 on a usage error.
|
|
19
|
+
#
|
|
20
|
+
# The `# BEGIN <name>` ... `# END <name>` blocks below are byte-identical copies of the spec's blocks ({GH_REPO} -> $GH_REPO);
|
|
21
|
+
# scripts/review-staging-pre.test.sh fails when they drift. Helpers (trusted-comments.sh, review-delta.sh) resolve through
|
|
22
|
+
# forge_resolve from trusted install roots only, never the working directory or the repo under review.
|
|
23
|
+
# Bash 3.2 portable except where the copied spec code already uses grep -P (0A Step 1).
|
|
24
|
+
set -u
|
|
25
|
+
|
|
26
|
+
CMD="${1:-}"; [ $# -gt 0 ] && shift
|
|
27
|
+
GH_REPO=""; DEFAULT_BRANCH=""; STAGING_BRANCH=""; PR_NUMBER=""; PRS_FILE=""; SD=""
|
|
28
|
+
while [ $# -gt 0 ]; do
|
|
29
|
+
[ $# -ge 2 ] || { echo "review-staging-pre: $1 requires a value" >&2; exit 2; }
|
|
30
|
+
case "$1" in
|
|
31
|
+
--repo) GH_REPO="$2" ;;
|
|
32
|
+
--default) DEFAULT_BRANCH="$2" ;;
|
|
33
|
+
--staging) STAGING_BRANCH="$2" ;;
|
|
34
|
+
--pr) PR_NUMBER="$2" ;;
|
|
35
|
+
--prs-file) PRS_FILE="$2" ;;
|
|
36
|
+
--sd) SD="$2" ;;
|
|
37
|
+
*) echo "review-staging-pre: unknown argument: $1" >&2; exit 2 ;;
|
|
38
|
+
esac
|
|
39
|
+
shift 2
|
|
40
|
+
done
|
|
41
|
+
case "$CMD" in bundle|gate|scope) ;; *) echo "review-staging-pre: unknown subcommand '${CMD}' (bundle|gate|scope)" >&2; exit 2 ;; esac
|
|
42
|
+
case "$PR_NUMBER" in ''|*[!0-9]*) echo "review-staging-pre: --pr must be numeric" >&2; exit 2 ;; esac
|
|
43
|
+
case "$GH_REPO" in *[!A-Za-z0-9._/-]*|*/*/*|/*|*/|'') echo "review-staging-pre: invalid --repo" >&2; exit 2 ;; */*) ;; *) echo "review-staging-pre: --repo must be owner/name" >&2; exit 2 ;; esac
|
|
44
|
+
if [ "$CMD" != gate ]; then
|
|
45
|
+
for _b in "$DEFAULT_BRANCH" "$STAGING_BRANCH"; do
|
|
46
|
+
case "$_b" in ''|-*|*[!A-Za-z0-9._/-]*) echo "review-staging-pre: --default/--staging must be branch names" >&2; exit 2 ;; esac
|
|
47
|
+
done
|
|
48
|
+
[ -n "$SD" ] || { echo "review-staging-pre: --sd is required" >&2; exit 2; }
|
|
49
|
+
mkdir -p "$SD" || exit 2
|
|
50
|
+
export FORGE_SCRATCHPAD="$SD"
|
|
51
|
+
fi
|
|
52
|
+
_SD="${SD:-${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}}"
|
|
53
|
+
ALL_PR_NUMBERS=""
|
|
54
|
+
if [ "$CMD" != bundle ]; then
|
|
55
|
+
[ -n "$PRS_FILE" ] || { echo "review-staging-pre: --prs-file is required" >&2; exit 2; }
|
|
56
|
+
[ -r "$PRS_FILE" ] || { echo "review-staging-pre: --prs-file $PRS_FILE is not readable (run bundle first)" >&2; exit 2; }
|
|
57
|
+
ALL_PR_NUMBERS=$(grep -E '^[0-9]+$' "$PRS_FILE")
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# --- trusted helper resolution (install roots only; never cwd) ---
|
|
61
|
+
# shellcheck source=/dev/null # the resolver lib is found at runtime in the trusted install root
|
|
62
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; _SELF_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd -P)"; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" "${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/* "$_SELF_ROOT"; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
63
|
+
|
|
64
|
+
if [ "$CMD" = bundle ]; then
|
|
65
|
+
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH || { echo "review-staging-pre: git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH failed: bundle PR set unknown (fail closed)" >&2; exit 1; }
|
|
66
|
+
|
|
67
|
+
# Step 1: Find all PR numbers in the staging→main bundle
|
|
68
|
+
# These are the PRs whose commits are included in staging but not yet in main
|
|
69
|
+
BUNDLE_PRS=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --oneline \
|
|
70
|
+
| grep -oE '#[0-9]+' \
|
|
71
|
+
| sort -u \
|
|
72
|
+
| tr -d '#')
|
|
73
|
+
|
|
74
|
+
# Also extract PR numbers from merge commit subjects (most reliable)
|
|
75
|
+
MERGE_PRS=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --merges --oneline \
|
|
76
|
+
| sed -n 's/.*pull request #\([0-9][0-9]*\).*/\1/p' \
|
|
77
|
+
| sort -u)
|
|
78
|
+
|
|
79
|
+
# BEGIN bundle-pr-set
|
|
80
|
+
# Conventional commit subjects cite issues too ("fix(x): ... (#123)"), so a subject-only number is kept only when it is not a confirmed issue.
|
|
81
|
+
# Merge-subject numbers are PRs by construction; an unclassifiable number is kept (fail open: it may be a PR). forge#4127
|
|
82
|
+
# BEGIN bundle-pr-kind
|
|
83
|
+
# Prints "pr" or "issue" for issue number ${2} in repo ${1}; prints nothing and returns 1 when the read fails or is malformed.
|
|
84
|
+
bundle_pr_kind() { local k; k=$(gh api "repos/${1}/issues/${2}" --jq 'if .pull_request then "pr" else "issue" end' 2>/dev/null) || return 1; case "$k" in pr|issue) printf '%s' "$k" ;; *) return 1 ;; esac; }
|
|
85
|
+
# END bundle-pr-kind
|
|
86
|
+
SUBJECT_PRS=""; SKIPPED_ISSUES=""
|
|
87
|
+
for n in $BUNDLE_PRS; do
|
|
88
|
+
case " $(echo $MERGE_PRS) " in *" $n "*) continue ;; esac
|
|
89
|
+
if [ "$(bundle_pr_kind "$GH_REPO" "$n")" = issue ]; then SKIPPED_ISSUES="$SKIPPED_ISSUES $n"; else SUBJECT_PRS="$SUBJECT_PRS $n"; fi
|
|
90
|
+
done
|
|
91
|
+
ALL_PR_NUMBERS=$(echo "$SUBJECT_PRS $MERGE_PRS" | tr ' ' '\n' | sort -u | grep -E '^[0-9]+$')
|
|
92
|
+
# END bundle-pr-set
|
|
93
|
+
|
|
94
|
+
echo "PRs in staging→main bundle: $(echo $ALL_PR_NUMBERS | tr '\n' ' ')"
|
|
95
|
+
[ -z "$SKIPPED_ISSUES" ] || echo "Issue numbers cited in commit subjects (not bundle PRs, skipped):$SKIPPED_ISSUES"
|
|
96
|
+
# Persist for the 0B.1 fence (a later Bash call is a fresh shell); keyed by the PR number resolved in Phase -1.
|
|
97
|
+
if [ -n "${PR_NUMBER:-}" ]; then
|
|
98
|
+
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"; mkdir -p "$_SD"
|
|
99
|
+
printf '%s\n' "$ALL_PR_NUMBERS" > "$_SD/${PR_NUMBER}_bundle-prs.txt"
|
|
100
|
+
fi
|
|
101
|
+
[ -n "$ALL_PR_NUMBERS" ] || { echo "review-staging-pre: the staging→main bundle PR set is empty: nothing to gate (fail closed)" >&2; exit 1; }
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
if [ "$CMD" = gate ]; then
|
|
106
|
+
|
|
107
|
+
# Step 2: For each PR in the bundle, check for open review-finding issues and degraded panels
|
|
108
|
+
if [ -z "$ALL_PR_NUMBERS" ]; then
|
|
109
|
+
echo "⛔ DEPLOY BLOCKED — the bundle PR list ($PRS_FILE) is empty: nothing was checked, so the gate cannot pass."
|
|
110
|
+
echo "RESULT: BLOCK DEPLOY"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
BLOCKING_FINDINGS=""
|
|
114
|
+
DEGRADED_REVIEWS=""
|
|
115
|
+
READ_FAILURES=""
|
|
116
|
+
for pr_num in $ALL_PR_NUMBERS; do
|
|
117
|
+
# Capture each gh read and test its own status (as the scope step does): a failed read is never "not degraded" / "no findings".
|
|
118
|
+
IS_REVIEW_DEGRADED=$(gh pr view "$pr_num" -R $GH_REPO --json labels \
|
|
119
|
+
--jq '[.labels[].name] | any(. == "review-degraded")' 2>/dev/null) || IS_REVIEW_DEGRADED=unreadable
|
|
120
|
+
case "$IS_REVIEW_DEGRADED" in true|false) ;; *) READ_FAILURES="$READ_FAILURES #${pr_num}(labels)" ;; esac
|
|
121
|
+
if [ "$IS_REVIEW_DEGRADED" = "true" ]; then
|
|
122
|
+
DEGRADED_REVIEWS="${DEGRADED_REVIEWS}
|
|
123
|
+
**PR #${pr_num}** has a \`review-degraded\` label and requires a full fresh-context re-review."
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Search for open review-finding issues that reference this PR. The search is only a coarse
|
|
127
|
+
# candidate fetch (full-text, tokenized: it also hits hidden markers and longer numbers), so
|
|
128
|
+
# narrow it with the anchored FINDING_PR_FILTER: title suffix `PR #N)` or a `**Source**: PR #N` line.
|
|
129
|
+
FINDING_PR_FILTER='[.[] | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
130
|
+
OPEN_FINDINGS=""
|
|
131
|
+
if _RAW=$(gh issue list -R $GH_REPO \
|
|
132
|
+
--label "review-finding" \
|
|
133
|
+
--state open \
|
|
134
|
+
--search "PR #${pr_num}" \
|
|
135
|
+
--limit 20 \
|
|
136
|
+
--json number,title,body 2>/dev/null) && printf '%s' "$_RAW" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
|
137
|
+
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
|
|
138
|
+
else
|
|
139
|
+
READ_FAILURES="$READ_FAILURES #${pr_num}(findings)"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
if [ -n "$OPEN_FINDINGS" ]; then
|
|
143
|
+
BLOCKING_FINDINGS="${BLOCKING_FINDINGS}
|
|
144
|
+
**PR #${pr_num}** has open review findings:
|
|
145
|
+
${OPEN_FINDINGS}"
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
|
|
149
|
+
# An unreadable review state is not a known finding either: block before any override is consulted (forge#4470).
|
|
150
|
+
if [ -n "$READ_FAILURES" ]; then
|
|
151
|
+
echo "⛔ DEPLOY BLOCKED — could not read review state for bundle PR(s):${READ_FAILURES}"
|
|
152
|
+
echo "Re-run the gate once GitHub reads succeed."
|
|
153
|
+
echo "RESULT: BLOCK DEPLOY"
|
|
154
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
155
|
+
gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
156
|
+
## Deploy Gate: BLOCKED
|
|
157
|
+
|
|
158
|
+
**Gate**: open-review-finding (read failure)
|
|
159
|
+
**Unreadable**:${READ_FAILURES}
|
|
160
|
+
|
|
161
|
+
The degraded-label or open review-finding reads failed, so the gate cannot certify the bundle. Re-run the staging review once GitHub reads succeed.
|
|
162
|
+
|
|
163
|
+
<!-- FORGE:GATE_FAILURE:TYPE=gate-read-failure -->" 2>/dev/null || true
|
|
164
|
+
fi
|
|
165
|
+
exit 1
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# A degraded panel is a review-integrity failure, not a finding that an override may waive.
|
|
169
|
+
if [ -n "$DEGRADED_REVIEWS" ]; then
|
|
170
|
+
echo "⛔ DEPLOY BLOCKED — An included PR has an incomplete isolated review panel."
|
|
171
|
+
echo "$DEGRADED_REVIEWS"
|
|
172
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
173
|
+
gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
174
|
+
## Deploy Gate: BLOCKED
|
|
175
|
+
|
|
176
|
+
**Gate**: review-panel-integrity
|
|
177
|
+
|
|
178
|
+
### Degraded Review Panels
|
|
179
|
+
|
|
180
|
+
${DEGRADED_REVIEWS}
|
|
181
|
+
|
|
182
|
+
Re-run the complete review panel in a fresh session before deployment.
|
|
183
|
+
|
|
184
|
+
<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->" 2>/dev/null || true
|
|
185
|
+
fi
|
|
186
|
+
exit 1
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# Step 3: Block deploy if open findings exist (unless human override present)
|
|
190
|
+
if [ -n "$BLOCKING_FINDINGS" ]; then
|
|
191
|
+
# Check for human override comment on the staging→main PR
|
|
192
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
193
|
+
# Trusted authors only (scripts/trusted-comments.sh: OWNER/MEMBER/COLLABORATOR or a Bot), all comment pages,
|
|
194
|
+
# marker anchored at the start of the body. Fail closed: unresolvable script or unreadable comments = no override (#3492).
|
|
195
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
196
|
+
OVR_SCRIPT="$TRUSTED_SCRIPT"
|
|
197
|
+
OVR_COMMENTS=$(gh api --paginate "repos/$GH_REPO/issues/${PR_NUMBER}/comments" 2>/dev/null) || OVR_COMMENTS=""
|
|
198
|
+
if [ -n "$OVR_SCRIPT" ] && [ -n "$OVR_COMMENTS" ]; then
|
|
199
|
+
OVERRIDE=$(printf '%s' "$OVR_COMMENTS" | bash "$OVR_SCRIPT" count '^OVERRIDE: shipping with open findings' 2>/dev/null || echo 0)
|
|
200
|
+
else
|
|
201
|
+
OVERRIDE=0
|
|
202
|
+
fi
|
|
203
|
+
else
|
|
204
|
+
OVERRIDE=0
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
if [ "${OVERRIDE:-0}" -eq 0 ]; then
|
|
208
|
+
echo "⛔ DEPLOY BLOCKED — Open review-finding issues exist for PRs in this bundle."
|
|
209
|
+
echo ""
|
|
210
|
+
echo "$BLOCKING_FINDINGS"
|
|
211
|
+
echo ""
|
|
212
|
+
echo "Options:"
|
|
213
|
+
echo " 1. Wait for the open findings to be fixed and merged to staging first."
|
|
214
|
+
echo " 2. Post a comment on this PR starting with \"OVERRIDE: shipping with open findings — <reason>\" to bypass this gate."
|
|
215
|
+
echo ""
|
|
216
|
+
echo "This gate exists to prevent deploying commits with known unfixed review findings."
|
|
217
|
+
echo "RESULT: BLOCK DEPLOY"
|
|
218
|
+
|
|
219
|
+
# Post structured FORGE:GATE_FAILURE comment for pipeline-health tracking
|
|
220
|
+
FINDING_COUNT=$(echo "$BLOCKING_FINDINGS" | grep -c '^\s*- #' || echo "unknown")
|
|
221
|
+
GATE_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
222
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
223
|
+
gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_FAILURE -->
|
|
224
|
+
## Deploy Gate: BLOCKED
|
|
225
|
+
|
|
226
|
+
**Gate**: open-review-finding
|
|
227
|
+
**Timestamp**: ${GATE_TIMESTAMP}
|
|
228
|
+
**Blocking findings**: ${FINDING_COUNT}
|
|
229
|
+
|
|
230
|
+
### Open Review-Finding Issues
|
|
231
|
+
|
|
232
|
+
${BLOCKING_FINDINGS}
|
|
233
|
+
|
|
234
|
+
### Resolution
|
|
235
|
+
|
|
236
|
+
Fix the open findings above and merge fixes to staging before retrying the staging→main deploy.
|
|
237
|
+
To override (ship known issues with documented reason), post a comment starting with:
|
|
238
|
+
\`OVERRIDE: shipping with open findings — <reason>\`
|
|
239
|
+
|
|
240
|
+
<!-- FORGE:GATE_FAILURE:TYPE=open-review-finding|FINDINGS=${FINDING_COUNT} -->" 2>/dev/null || true
|
|
241
|
+
fi
|
|
242
|
+
exit 1
|
|
243
|
+
else
|
|
244
|
+
echo "⚠️ Open review findings exist but human override detected — proceeding with deploy."
|
|
245
|
+
echo "$BLOCKING_FINDINGS"
|
|
246
|
+
echo "Override comment found on PR #${PR_NUMBER}. Continuing."
|
|
247
|
+
fi
|
|
248
|
+
else
|
|
249
|
+
echo "✅ Open review-finding gate: PASSED — no open findings for PRs in this bundle."
|
|
250
|
+
# Post FORGE:GATE_PASS for symmetric observability — bypass is indistinguishable from clean pass without this
|
|
251
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
252
|
+
gh pr comment "$PR_NUMBER" -R $GH_REPO --body "<!-- FORGE:GATE_PASS -->
|
|
253
|
+
## Deploy Gate: PASSED
|
|
254
|
+
|
|
255
|
+
**Gate**: open-review-finding check
|
|
256
|
+
**Result**: PASS — no open \`review-finding\` issues exist for PRs in this bundle.
|
|
257
|
+
**Bundle PRs**: ${ALL_PR_NUMBERS}
|
|
258
|
+
**Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
259
|
+
|
|
260
|
+
<!-- FORGE:GATE_PASS:TYPE=open-review-finding -->" 2>/dev/null || true
|
|
261
|
+
fi
|
|
262
|
+
fi
|
|
263
|
+
echo "RESULT: PASS"
|
|
264
|
+
exit 0
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
# scope
|
|
268
|
+
SCOPE_MODE=full; REVIEWED_COMMITS=""; REVIEWED_MERGES=""; DEPLOY_WIRING_FILES=""; UNREVIEWED_COMMITS=""; UNREVIEWED_FILES=""; _LOG=""; CROSS_PR_FILES=""; REVIEWED_PR_COUNT=0
|
|
269
|
+
_PF="$_SD/${PR_NUMBER}_bundle-pr-files.txt"; : > "$_PF"
|
|
270
|
+
[ -n "${PR_NUMBER:-}" ] && [ -n "${GH_REPO:-}" ] || echo "SCOPE_MODE=full: scoping skipped (PR_NUMBER or GH_REPO empty after reload)"
|
|
271
|
+
[ -n "${ALL_PR_NUMBERS:-}" ] || echo "SCOPE_MODE=full: scoping skipped (ALL_PR_NUMBERS empty: Phase 0A did not persist a bundle list)"
|
|
272
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
273
|
+
[ -n "$TRUSTED_SCRIPT" ] || echo "SCOPE_MODE=full: scoping skipped (trusted-comments.sh not resolvable)"
|
|
274
|
+
REVIEW_DELTA_SCRIPT=$(forge_resolve review-delta.sh)
|
|
275
|
+
[ -n "$REVIEW_DELTA_SCRIPT" ] || echo "delta credit off: review-delta.sh not resolvable (stale-head PR commits stay UNREVIEWED)"
|
|
276
|
+
# BEGIN bundle-delta-credit
|
|
277
|
+
is_sha() { case "${1}" in *[!0-9a-f]*|"") return 1 ;; esac; [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ]; }
|
|
278
|
+
# Partial credit for a PR whose head moved after its last complete review. Only the shared resolver (scripts/review-delta.sh) decides; nothing is re-derived here.
|
|
279
|
+
# Needs pr _HEAD _J _CM _SD GH_REPO TRUSTED_SCRIPT REVIEW_DELTA_SCRIPT in scope. Sets DELTA_COMMITS (space-separated PR commit oids already covered by a trusted complete panel) and DELTA_FULL (1 = whole PR covered by a trusted carried-forward base sync).
|
|
280
|
+
# Every failure (script unresolved, FULL, error, malformed output, force-push, untrusted or absent carried record, git read failure) leaves DELTA_COMMITS empty and DELTA_FULL=0: those commits stay UNREVIEWED.
|
|
281
|
+
bundle_delta_credit() {
|
|
282
|
+
local base labels top cf out l1 kind arg from to oid rc n
|
|
283
|
+
DELTA_COMMITS=""; DELTA_FULL=0
|
|
284
|
+
[ -n "$REVIEW_DELTA_SCRIPT" ] || return 0
|
|
285
|
+
base=$(printf '%s' "$_J" | jq -r '.baseRefName // empty') || return 0
|
|
286
|
+
labels=$(printf '%s' "$_J" | jq -r '[.labels[].name] | join(",")') || return 0
|
|
287
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || return 0
|
|
288
|
+
[ -n "$base" ] && [ -n "$top" ] || return 0
|
|
289
|
+
cf=$(mktemp "$_SD/delta-comments.XXXXXX") || return 0
|
|
290
|
+
printf '%s' "$_CM" > "$cf" || { rm -f "$cf"; return 0; }
|
|
291
|
+
out=$(bash "$REVIEW_DELTA_SCRIPT" --pr "$pr" --head "$_HEAD" --base "$base" --trusted-script "$TRUSTED_SCRIPT" --comments-file "$cf" --labels "$labels" --repo "$GH_REPO" --repo-path "$top" 2>/dev/null) || out=""
|
|
292
|
+
rm -f "$cf"
|
|
293
|
+
l1=$(printf '%s\n' "$out" | sed -n 1p)
|
|
294
|
+
kind="${l1%% *}"; arg="${l1#* }"
|
|
295
|
+
case "$kind" in
|
|
296
|
+
DELTA)
|
|
297
|
+
from="${arg%%..*}"; to="${arg##*..}"
|
|
298
|
+
is_sha "$from" && [ "$to" = "$_HEAD" ] && [ "$from" != "$to" ] || return 0
|
|
299
|
+
for oid in $(printf '%s' "$_J" | jq -r '.commits[].oid'); do
|
|
300
|
+
git merge-base --is-ancestor "$oid" "$from" 2>/dev/null; rc=$?
|
|
301
|
+
[ "$rc" -eq 0 ] && DELTA_COMMITS="$DELTA_COMMITS $oid" # rc 1 (not covered) and rc >1 (error) both stay UNREVIEWED
|
|
302
|
+
done ;;
|
|
303
|
+
BASE_SYNC_ONLY)
|
|
304
|
+
is_sha "$arg" && [ "$arg" != "$_HEAD" ] || return 0
|
|
305
|
+
# A marker alone never counts: a TRUSTED FORGE:REVIEW-CARRIED record for this head must name the same reviewed sha (shape owned by review-pr.md carry_verified_from).
|
|
306
|
+
n=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "\\A(?=[\\s\\S]*\\nReviewed-SHA: ${_HEAD}\\r?(\\n|$))(?=[\\s\\S]*\\nCarried-From-SHA: ${arg}\\r?(\\n|$))<!-- FORGE:REVIEW-CARRIED -->" 2>/dev/null) || return 0
|
|
307
|
+
case "$n" in ''|*[!0-9]*) return 0 ;; esac
|
|
308
|
+
[ "$n" -ge 1 ] && DELTA_FULL=1 ;;
|
|
309
|
+
esac
|
|
310
|
+
return 0
|
|
311
|
+
}
|
|
312
|
+
# END bundle-delta-credit
|
|
313
|
+
# BEGIN bundle-pr-kind
|
|
314
|
+
# Prints "pr" or "issue" for issue number ${2} in repo ${1}; prints nothing and returns 1 when the read fails or is malformed.
|
|
315
|
+
bundle_pr_kind() { local k; k=$(gh api "repos/${1}/issues/${2}" --jq 'if .pull_request then "pr" else "issue" end' 2>/dev/null) || return 1; case "$k" in pr|issue) printf '%s' "$k" ;; *) return 1 ;; esac; }
|
|
316
|
+
# END bundle-pr-kind
|
|
317
|
+
# BEGIN bundle-scope-loop
|
|
318
|
+
_WHY=""
|
|
319
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "${ALL_PR_NUMBERS:-}" ] && [ -n "${PR_NUMBER:-}" ] && [ -n "${GH_REPO:-}" ]; then
|
|
320
|
+
SCOPE_MODE=scoped
|
|
321
|
+
for pr in $ALL_PR_NUMBERS; do
|
|
322
|
+
# A number 0A could not classify may be an issue (subjects cite issues): a confirmed issue is skipped, only a genuine read failure falls open (forge#4127).
|
|
323
|
+
_J=$(gh pr view "$pr" -R $GH_REPO --json headRefOid,mergeCommit,commits,state,baseRefName,labels 2>/dev/null) || {
|
|
324
|
+
if [ "$(bundle_pr_kind "$GH_REPO" "$pr")" = issue ]; then echo "0B.1: #$pr is an issue, not a bundle PR: skipped"; continue; fi
|
|
325
|
+
_WHY="gh pr view failed and #$pr is not a confirmed issue"; SCOPE_MODE=full; break; }
|
|
326
|
+
_HEAD=$(printf '%s' "$_J" | jq -r '.headRefOid')
|
|
327
|
+
case "$_HEAD" in *[!0-9a-f]*|"") SCOPE_MODE=full; break ;; esac # a malformed head id would make the sha match accept any comment (fail open): review everything
|
|
328
|
+
[ "${#_HEAD}" -eq 40 ] || [ "${#_HEAD}" -eq 64 ] || { SCOPE_MODE=full; break; }
|
|
329
|
+
_HEAD7=$(printf '%s' "$_HEAD" | cut -c1-7)
|
|
330
|
+
# Capture each gh read into a variable FIRST and test its own exit status: in a pipeline `||` only sees the last stage (no pipefail),
|
|
331
|
+
# so a failed gh read piped into another command would silently look like "no comments" / "no files".
|
|
332
|
+
_CM=$(gh api --paginate "repos/$GH_REPO/issues/$pr/comments" 2>/dev/null) || { SCOPE_MODE=full; break; }
|
|
333
|
+
_FILES=$(gh pr diff "$pr" -R $GH_REPO --name-only 2>/dev/null) || { SCOPE_MODE=full; break; }
|
|
334
|
+
# Reviewed = trusted FORGE:REVIEW_ROUTE (a real review mode) whose sha is the PR's FINAL head, plus at least one review-agent comment
|
|
335
|
+
# carrying the full-length `Reviewed-SHA: <head>` line and a findings block (same predicate as review-pr.md Phase 5; a 7-char substring or a comment with no findings block never counts), and no degraded/blocked review marker. A PR pushed to after its review has a stale sha: the exact-head gate gives it no credit, and only the delta branch below (shared resolver `scripts/review-delta.sh`) may credit the commits an earlier trusted complete panel covered or a trusted `FORGE:REVIEW-CARRIED` base sync carried; everything else stays UNREVIEWED.
|
|
336
|
+
_ROUTE=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^<!-- FORGE:REVIEW_ROUTE mode=(?!spec-evolution-blocked)[^ ]+ spec=review-pr.md sha=${_HEAD7} -->") || { SCOPE_MODE=full; break; }
|
|
337
|
+
_AGENT=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${_HEAD}(\\r?\\n|$)") || { SCOPE_MODE=full; break; }
|
|
338
|
+
_DEGRADED=$(printf '%s' "$_CM" | bash "$TRUSTED_SCRIPT" count "^<!-- FORGE:(REVIEW_DEGRADED|REVIEW_BLOCKED)") || { SCOPE_MODE=full; break; }
|
|
339
|
+
case "$_ROUTE$_AGENT$_DEGRADED" in ''|*[!0-9]*) SCOPE_MODE=full; break ;; esac
|
|
340
|
+
if [ "$_ROUTE" -ge 1 ] && [ "$_AGENT" -ge 1 ] && [ "$_DEGRADED" -eq 0 ] && [ "$(printf '%s' "$_J" | jq '.commits|length')" -lt 100 ]; then
|
|
341
|
+
REVIEWED_PR_COUNT=$((REVIEWED_PR_COUNT+1))
|
|
342
|
+
REVIEWED_COMMITS="$REVIEWED_COMMITS $(printf '%s' "$_J" | jq -r '[.commits[].oid, .mergeCommit.oid // empty] | join(" ")')"
|
|
343
|
+
REVIEWED_MERGES="$REVIEWED_MERGES $(printf '%s' "$_J" | jq -r '.mergeCommit.oid // empty')"
|
|
344
|
+
elif [ "$_DEGRADED" -eq 0 ] && [ "$(printf '%s' "$_J" | jq '.commits|length')" -lt 100 ]; then
|
|
345
|
+
# Stale head: the exact-head gate failed, so ask the shared resolver how much of the PR an earlier trusted complete panel covered. Additive only: it can move commits out of UNREVIEWED, never into it.
|
|
346
|
+
bundle_delta_credit
|
|
347
|
+
if [ "$DELTA_FULL" -eq 1 ]; then
|
|
348
|
+
REVIEWED_PR_COUNT=$((REVIEWED_PR_COUNT+1))
|
|
349
|
+
REVIEWED_COMMITS="$REVIEWED_COMMITS $(printf '%s' "$_J" | jq -r '[.commits[].oid, .mergeCommit.oid // empty] | join(" ")')"
|
|
350
|
+
REVIEWED_MERGES="$REVIEWED_MERGES $(printf '%s' "$_J" | jq -r '.mergeCommit.oid // empty')"
|
|
351
|
+
elif [ -n "$DELTA_COMMITS" ]; then
|
|
352
|
+
REVIEWED_COMMITS="$REVIEWED_COMMITS$DELTA_COMMITS" # the PR's merge commit is not credited: it contains the post-review commits
|
|
353
|
+
fi
|
|
354
|
+
fi
|
|
355
|
+
printf '%s\n' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"
|
|
356
|
+
done
|
|
357
|
+
[ "$SCOPE_MODE" = scoped ] || echo "SCOPE_MODE=full: scoping fell open at bundle PR #$pr (${_WHY:-a gh read or marker count failed or returned malformed data})"
|
|
358
|
+
fi
|
|
359
|
+
# END bundle-scope-loop
|
|
360
|
+
if [ "$SCOPE_MODE" = "scoped" ]; then
|
|
361
|
+
# Cross-PR interactions: files touched by 2+ bundle PRs.
|
|
362
|
+
CROSS_PR_FILES=$(cut -d' ' -f2- "$_PF" | sort | uniq -d)
|
|
363
|
+
# Unreviewed commits: on staging, not in main, and not a commit of any reviewed PR (direct pushes, fast-lane fixes, post-review pushes).
|
|
364
|
+
# Merge commits are included: a merge that is not a reviewed PR's own merge commit may carry conflict-resolution content nobody reviewed.
|
|
365
|
+
_LOG=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --format=%H) || SCOPE_MODE=full
|
|
366
|
+
fi
|
|
367
|
+
if [ "$SCOPE_MODE" = "scoped" ]; then
|
|
368
|
+
for c in $_LOG; do
|
|
369
|
+
case " $REVIEWED_COMMITS " in *" $c "*) continue ;; esac
|
|
370
|
+
UNREVIEWED_COMMITS="$UNREVIEWED_COMMITS $c"
|
|
371
|
+
done
|
|
372
|
+
# Files of every unreviewed commit (--cc limits a merge commit to its conflict-resolution content), so 7B can test file membership.
|
|
373
|
+
# Each git read is captured and its own status tested (a failure here must widen scope, never narrow it).
|
|
374
|
+
UNREVIEWED_FILES=""
|
|
375
|
+
for c in $UNREVIEWED_COMMITS; do
|
|
376
|
+
_DT=$(git diff-tree --no-commit-id --name-only -r "$c") || { SCOPE_MODE=full; break; }
|
|
377
|
+
_DC=$(git diff-tree --no-commit-id --name-only -r --cc "$c") || { SCOPE_MODE=full; break; }
|
|
378
|
+
UNREVIEWED_FILES="$UNREVIEWED_FILES"$'\n'"$_DT"$'\n'"$_DC"
|
|
379
|
+
done
|
|
380
|
+
# A reviewed PR's own merge commit may carry conflict-resolution content its review never saw: count its --cc files as unreviewed too.
|
|
381
|
+
for c in $REVIEWED_MERGES; do
|
|
382
|
+
_DC=$(git diff-tree --no-commit-id --name-only -r --cc "$c") || { SCOPE_MODE=full; break; }
|
|
383
|
+
UNREVIEWED_FILES="$UNREVIEWED_FILES"$'\n'"$_DC"
|
|
384
|
+
done
|
|
385
|
+
UNREVIEWED_FILES=$(printf '%s\n' "$UNREVIEWED_FILES" | sed '/^$/d' | sort -u)
|
|
386
|
+
fi
|
|
387
|
+
# Deploy/runtime readiness files are always in scope: migrations, env, workflows, compose/infra, dependency manifests.
|
|
388
|
+
_DW=$(git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --name-only) || { SCOPE_MODE=full; _DW=""; }
|
|
389
|
+
DEPLOY_WIRING_FILES=$(printf '%s\n' "$_DW" | grep -E '(^|/)(migrations?/|\.github/workflows/|infra/|deploy/|docker-compose[^/]*\.ya?ml$|\.env[^/]*$|Dockerfile[^/]*$|package(-lock)?\.json$|pnpm-lock\.yaml$|yarn\.lock$|requirements[^/]*\.txt$|pyproject\.toml$|poetry\.lock$|go\.(mod|sum)$|Cargo\.(toml|lock)$)' || true)
|
|
390
|
+
# Persist the results for 7A (a later Bash call is a fresh shell). %q keeps multi-line file lists intact.
|
|
391
|
+
printf 'SCOPE_MODE=%q\nCROSS_PR_FILES=%q\nUNREVIEWED_FILES=%q\nDEPLOY_WIRING_FILES=%q\n' "$SCOPE_MODE" "${CROSS_PR_FILES:-}" "${UNREVIEWED_FILES:-}" "${DEPLOY_WIRING_FILES:-}" > "$_SD/${PR_NUMBER}_scope.env"
|
|
392
|
+
echo "SCOPE_MODE=$SCOPE_MODE reviewed_prs=$REVIEWED_PR_COUNT unreviewed_commits=$(echo $UNREVIEWED_COMMITS | wc -w) unreviewed_files=$(echo "$UNREVIEWED_FILES" | grep -c .) cross_pr_files=$(echo "$CROSS_PR_FILES" | grep -c .)"
|
|
393
|
+
# JSON twin of scope.env for the CLI: commit lists are space separated, file lists newline separated.
|
|
394
|
+
_jw() { printf '%s\n' "${1:-}" | tr ' ' '\n' | sed '/^$/d' | jq -R . | jq -s .; }
|
|
395
|
+
_jn() { printf '%s\n' "${1:-}" | sed '/^$/d' | jq -R . | jq -s .; }
|
|
396
|
+
jq -n --arg mode "$SCOPE_MODE" --argjson n "${REVIEWED_PR_COUNT:-0}" \
|
|
397
|
+
--argjson rc "$(_jw "$REVIEWED_COMMITS")" --argjson uc "$(_jw "$UNREVIEWED_COMMITS")" \
|
|
398
|
+
--argjson uf "$(_jn "${UNREVIEWED_FILES:-}")" --argjson cf "$(_jn "${CROSS_PR_FILES:-}")" --argjson dw "$(_jn "${DEPLOY_WIRING_FILES:-}")" \
|
|
399
|
+
'{scope_mode: $mode, reviewed_pr_count: $n, reviewed_commits: $rc, unreviewed_commits: $uc, unreviewed_files: $uf, cross_pr_files: $cf, deploy_wiring_files: $dw}' \
|
|
400
|
+
> "$_SD/${PR_NUMBER}_scope.json" || { echo "review-staging-pre: could not write ${PR_NUMBER}_scope.json" >&2; exit 1; }
|
|
401
|
+
exit 0
|