forgedock 1.30.0 → 1.31.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 +362 -33
- package/bin/engine.mjs +47 -37
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- 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 +294 -1
- 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 +116 -43
- package/bin/lean/context.test.mjs +139 -4
- 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 +37 -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 +650 -124
- package/bin/lean/lean.test.mjs +1450 -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/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +261 -38
- 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 +113 -1
- package/bin/lean/prefilter.mjs +159 -5
- package/bin/lean/prefilter.test.mjs +152 -3
- 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 +101 -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.test.mjs +127 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +149 -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 +319 -0
- package/bin/stats/efficiency.test.mjs +233 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +69 -6
- package/bin/stats/local-source.mjs +86 -3
- package/bin/stats/query.mjs +48 -0
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +224 -5
- 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 +53 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- 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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# the PR checkout, a SPEC_CLI persona file, forge.yaml paths.root) as data.
|
|
10
10
|
#
|
|
11
11
|
# Usage: review-prepare.sh --pr N --repo owner/name [--merge-issue N] [--thorough true|false] [--out FILE] [--out-dir DIR]
|
|
12
|
-
# [--checkout-pr] [--no-delta] [--build-gate block|advisory]
|
|
12
|
+
# [--checkout-pr] [--no-delta] [--delta-from SHA] [--build-gate block|advisory]
|
|
13
13
|
# --pr PR number (numeric, required) --repo owner/name (required)
|
|
14
14
|
# --merge-issue issue the PR closes, from --issue (numeric; non-numeric is ignored)
|
|
15
15
|
# --thorough true restores the full-union roster --out write the context JSON here (default: stdout)
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
# --no-delta skip the FULL/DELTA/BASE_SYNC_ONLY resolver: the review mode is always FULL (a staging bundle review always reviews the
|
|
24
24
|
# whole bundle), without widening the roster to the full union the way --thorough does; it also enables the pinned
|
|
25
25
|
# local-git diff fallback when `gh pr diff` fails (e.g. HTTP 406 over 20,000 lines), which a milestone->staging PR gets without it
|
|
26
|
+
# --delta-from explicit DELTA from a caller-supplied earlier reviewed head (forge#4360, `forgedock review-staging --delta`): skips the
|
|
27
|
+
# resolver (even with --no-delta), narrows diff, slices and roster to SHA..review sha (the roster is the domains the delta
|
|
28
|
+
# files touch). BLOCKED when SHA is not a full commit id in the checkout, equals or is not an ancestor of the review sha,
|
|
29
|
+
# or has no trusted FORGE:REVIEW-AGENT body with `Reviewed-SHA: SHA` (no earlier pass to credit). An empty file delta
|
|
30
|
+
# is FULL. Not combinable with --thorough true (exit 2).
|
|
26
31
|
# Env: FORGE_CONFIG (forge.yaml path; default: the main checkout's forge.yaml, then the git top-level one),
|
|
27
32
|
# REVIEW_PREPARE_SLEEP (seconds between mergeability retries, default 5),
|
|
28
33
|
# REVIEW_PREPARE_INDEX_TIMEOUT (seconds the code-index build may take, default 120; on timeout or failure the slices say
|
|
@@ -61,10 +66,10 @@
|
|
|
61
66
|
|
|
62
67
|
set -u
|
|
63
68
|
|
|
64
|
-
PR_NUMBER=""; REPO=""; MERGE_ISSUE=""; THOROUGH="false"; OUT=""; OUT_DIR=""; CHECKOUT_PR="false"; NO_DELTA="false"; BUILD_GATE="block"
|
|
69
|
+
PR_NUMBER=""; REPO=""; MERGE_ISSUE=""; THOROUGH="false"; OUT=""; OUT_DIR=""; CHECKOUT_PR="false"; NO_DELTA="false"; BUILD_GATE="block"; DELTA_FROM_OPT=""
|
|
65
70
|
while [ $# -gt 0 ]; do
|
|
66
71
|
case "$1" in
|
|
67
|
-
--pr|--repo|--merge-issue|--thorough|--out|--out-dir)
|
|
72
|
+
--pr|--repo|--merge-issue|--thorough|--out|--out-dir|--delta-from)
|
|
68
73
|
[ $# -ge 2 ] || { echo "review-prepare: $1 requires a value" >&2; exit 2; } ;;
|
|
69
74
|
esac
|
|
70
75
|
case "$1" in
|
|
@@ -76,6 +81,7 @@ while [ $# -gt 0 ]; do
|
|
|
76
81
|
--out-dir) OUT_DIR="${2:-}"; shift 2 ;;
|
|
77
82
|
--checkout-pr) CHECKOUT_PR="true"; shift ;;
|
|
78
83
|
--no-delta) NO_DELTA="true"; shift ;;
|
|
84
|
+
--delta-from) DELTA_FROM_OPT="${2:-}"; [ -n "$DELTA_FROM_OPT" ] || DELTA_FROM_OPT="-"; shift 2 ;;
|
|
79
85
|
--build-gate) case "${2:-}" in block|advisory) BUILD_GATE="$2" ;; *) echo "review-prepare: --build-gate must be block or advisory" >&2; exit 2 ;; esac; shift 2 ;;
|
|
80
86
|
*) echo "review-prepare: unknown argument: $1" >&2; exit 2 ;;
|
|
81
87
|
esac
|
|
@@ -85,6 +91,7 @@ case "$REPO" in */*) ;; *) echo "review-prepare: --repo must be owner/name" >&2;
|
|
|
85
91
|
case "$REPO" in *[!A-Za-z0-9._/-]*|*/*/*|/*|*/) echo "review-prepare: invalid --repo" >&2; exit 2 ;; esac
|
|
86
92
|
case "$MERGE_ISSUE" in *[!0-9]*) MERGE_ISSUE="" ;; esac
|
|
87
93
|
[ "$THOROUGH" = "true" ] || THOROUGH="false"
|
|
94
|
+
if [ -n "$DELTA_FROM_OPT" ] && [ "$THOROUGH" = "true" ]; then echo "review-prepare: --delta-from cannot be combined with --thorough true" >&2; exit 2; fi
|
|
88
95
|
|
|
89
96
|
# --- trusted helper resolution (install roots only; never cwd) ---
|
|
90
97
|
# shellcheck source=/dev/null # the resolver lib is found at runtime in the trusted install root
|
|
@@ -125,7 +132,7 @@ cleanup() { restore_ref; rm -rf "$TMP_DIR"; }
|
|
|
125
132
|
trap cleanup EXIT
|
|
126
133
|
|
|
127
134
|
# --- Phase 1A: fetch ---
|
|
128
|
-
PR_INFO=$(gh pr view "$PR_NUMBER" -R "$REPO" --json number,title,author,baseRefName,headRefName,headRefOid,isCrossRepository,additions,deletions,changedFiles 2>/dev/null) || PR_INFO=""
|
|
135
|
+
PR_INFO=$(gh pr view "$PR_NUMBER" -R "$REPO" --json number,title,author,baseRefName,headRefName,headRefOid,isCrossRepository,additions,deletions,changedFiles,labels 2>/dev/null) || PR_INFO=""
|
|
129
136
|
printf '%s' "$PR_INFO" | jq -e . >/dev/null 2>&1 || PR_INFO=""
|
|
130
137
|
if [ -z "$PR_INFO" ]; then block "gh pr view failed for #${PR_NUMBER}"; fi
|
|
131
138
|
pj() { printf '%s' "$PR_INFO" | jq -r "$1 // empty" 2>/dev/null; }
|
|
@@ -452,7 +459,53 @@ delta_roster() {
|
|
|
452
459
|
printf '%s' "$out"
|
|
453
460
|
}
|
|
454
461
|
REVIEW_MODE="FULL"; DELTA_FROM=""; CARRIED_FROM=""; FULL_ROUNDS=0; DELTA_FILES=""
|
|
455
|
-
if [
|
|
462
|
+
if [ -n "$DELTA_FROM_OPT" ]; then
|
|
463
|
+
# forge#4360: explicit delta. Every check fails closed (BLOCKED); only an empty file delta widens to FULL (below).
|
|
464
|
+
_dtop="${TOP:-.}"
|
|
465
|
+
case "$DELTA_FROM_OPT" in *[!0-9a-f]*|"") _dok=false ;; *) { [ "${#DELTA_FROM_OPT}" -eq 40 ] || [ "${#DELTA_FROM_OPT}" -eq 64 ]; } && _dok=true || _dok=false ;; esac
|
|
466
|
+
if [ "$_dok" != "true" ]; then block "--delta-from is not a full commit id: $(printf '%s' "$DELTA_FROM_OPT" | tr -cd '0-9A-Za-z' | cut -c1-64)"
|
|
467
|
+
elif [ "$DELTA_FROM_OPT" = "$REVIEW_SHA" ]; then block "--delta-from ${DELTA_FROM_OPT} is the review head: nothing to review"
|
|
468
|
+
elif ! git -C "$_dtop" cat-file -e "${DELTA_FROM_OPT}^{commit}" 2>/dev/null; then block "--delta-from ${DELTA_FROM_OPT} is not a commit in this checkout (fetch it first)"
|
|
469
|
+
elif ! git -C "$_dtop" merge-base --is-ancestor "$DELTA_FROM_OPT" "$REVIEW_SHA" 2>/dev/null; then block "--delta-from ${DELTA_FROM_OPT} is not an ancestor of ${REVIEW_SHA} (force-push or rebase): run a full review"
|
|
470
|
+
elif printf '%s' "$PR_INFO" | jq -e '[.labels[]?.name] | index("review-degraded") != null' >/dev/null 2>&1; then block "--delta-from ${DELTA_FROM_OPT}: the PR is labelled review-degraded (an earlier panel was incomplete): run a full review"
|
|
471
|
+
else
|
|
472
|
+
# forge#4471: the earlier pass must be complete and non-degraded, as scripts/review-delta.sh requires of a checkpoint:
|
|
473
|
+
# an agent body with a findings block for <from>, a post-panel-guard proof naming <from> (staging summary, synthesized
|
|
474
|
+
# block, or a # PR Review Summary whose Agents count matches) and no degraded marker after its first agent body.
|
|
475
|
+
_dv="error"
|
|
476
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$COMMENTS_JSON" ]; then
|
|
477
|
+
_db=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies '^<!-- 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) \
|
|
478
|
+
&& _dv=$(printf '%s\n' "$_db" | jq -s -r --arg f "$DELTA_FROM_OPT" '
|
|
479
|
+
def shas($t; $re): [ $t | scan($re) | .[0] ];
|
|
480
|
+
def one($a): if ($a | length) == 1 then $a[0] else null end;
|
|
481
|
+
def rsha: "(?:^|\\n)Reviewed-SHA: ([0-9a-f]{40}|[0-9a-f]{64})\\r?(?:\\n|$)";
|
|
482
|
+
to_entries as $E
|
|
483
|
+
| [ $E[] | select((.value | test("\\A<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->")) and (.value | test("<!-- REVIEW-FINDINGS-START -->")))
|
|
484
|
+
| select(one(shas(.value; rsha)) == $f)
|
|
485
|
+
| {i: .key, d: (.value | capture("\\A<!-- FORGE:REVIEW-AGENT:(?<d>[a-z0-9-]+) -->").d)} ] as $AG
|
|
486
|
+
| if ($AG | length) == 0 then "no-agent" else
|
|
487
|
+
($AG | map(.i) | min) as $first | ($AG | map(.d) | unique | length) as $nd
|
|
488
|
+
| ([ $E[] | .value as $t | select(
|
|
489
|
+
(($t | test("\\A<!-- FORGE:GATE_(?:PASS|FAILURE) -->\\r?\\n## Staging Review:"))
|
|
490
|
+
and one(shas($t; "\\*\\*Head\\*\\*: `([0-9a-f]{40}|[0-9a-f]{64})`")) == $f)
|
|
491
|
+
or (($t | startswith("<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->")) and one(shas($t; rsha)) == $f)
|
|
492
|
+
or (($t | test("# PR Review Summary"))
|
|
493
|
+
and one(shas($t; "\\*\\*Reviewed commit\\*\\*: `?([0-9a-f]{40}|[0-9a-f]{64})`?(?:[ |\\r\\n]|$)")) == $f
|
|
494
|
+
and one(shas($t; "\\*\\*Agents\\*\\*: ([0-9]+)")) == ($nd | tostring))) ] | length) as $proof
|
|
495
|
+
| ([ $E[] | select(.key > $first)
|
|
496
|
+
| select(.value | test("<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity|<!-- FORGE:REVIEW_DEGRADED|<!-- FORGE:REVIEW_BLOCKED")) ] | length) as $deg
|
|
497
|
+
| if $deg > 0 then "degraded" elif $proof == 0 then "no-proof" else "ok" end
|
|
498
|
+
end' 2>/dev/null) || _dv="error"
|
|
499
|
+
fi
|
|
500
|
+
case "$_dv" in
|
|
501
|
+
ok) REVIEW_MODE="DELTA"; DELTA_FROM="$DELTA_FROM_OPT" ;;
|
|
502
|
+
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" ;;
|
|
503
|
+
no-proof) 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): run a full review" ;;
|
|
504
|
+
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" ;;
|
|
505
|
+
*) block "--delta-from ${DELTA_FROM_OPT}: could not read the trusted review comments, no earlier pass to credit: run a full review" ;;
|
|
506
|
+
esac
|
|
507
|
+
fi
|
|
508
|
+
elif [ "$THOROUGH" != "true" ] && [ "$IS_MILESTONE_TO_STAGING" != "true" ] && [ "$NO_DELTA" != "true" ]; then
|
|
456
509
|
read -r RD_KIND RD_ARG RD_ROUNDS <<< "$(review_delta_mode)"
|
|
457
510
|
FULL_ROUNDS="${RD_ROUNDS:-0}"
|
|
458
511
|
case "$RD_KIND" in
|
|
@@ -463,7 +516,21 @@ fi
|
|
|
463
516
|
if [ "$REVIEW_MODE" = "DELTA" ]; then
|
|
464
517
|
DELTA_FILES=$(git -C "${TOP:-.}" -c core.quotepath=off diff --name-only --no-renames "${DELTA_FROM}" "${REVIEW_SHA}" 2>/dev/null) || DELTA_FILES=""
|
|
465
518
|
if [ -z "$DELTA_FILES" ]; then REVIEW_MODE="FULL"; DELTA_FROM=""
|
|
466
|
-
else
|
|
519
|
+
else
|
|
520
|
+
# an explicit --delta-from always narrows the roster to the domains the delta touches (no full-rounds gate)
|
|
521
|
+
_drounds="$FULL_ROUNDS"
|
|
522
|
+
# forge#4471: an explicit delta that changes the review specs/config cannot credit the earlier pass (same list as
|
|
523
|
+
# scripts/review-delta.sh, which answers FULL there; review-prepare.test.sh checks the two lists match)
|
|
524
|
+
if [ -n "$DELTA_FROM_OPT" ]; then
|
|
525
|
+
while IFS= read -r _df; do
|
|
526
|
+
case "$_df" in
|
|
527
|
+
forge.yaml|commands/review-pr*.md|commands/review-pr-agents/*|scripts/review-delta.sh|scripts/trusted-comments.sh|scripts/verify-phase-trail.sh|docs/spec/review-protocol.md|scripts/classify-finding.sh) block "--delta-from ${DELTA_FROM}: the delta changes ${_df} (review spec/config), the earlier pass cannot be credited: run a full review"; break ;;
|
|
528
|
+
esac
|
|
529
|
+
done <<< "$DELTA_FILES"
|
|
530
|
+
fi
|
|
531
|
+
[ -n "$DELTA_FROM_OPT" ] && { _drounds=2; ROSTER_REASON="delta ${DELTA_FROM:0:7}..${REVIEW_SHA:0:7}: domains the delta touches"; }
|
|
532
|
+
SELECTED_AGENTS=$(canonical_roster "$(delta_roster "$_drounds" "$SELECTED_AGENTS" "$DELTA_FILES")")
|
|
533
|
+
fi
|
|
467
534
|
fi
|
|
468
535
|
[ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ] && SELECTED_AGENTS=""
|
|
469
536
|
|
|
@@ -199,6 +199,91 @@ DELTA_KIND=DELTA prep "$WORK/root-delta" --thorough true; eq "thorough never goe
|
|
|
199
199
|
setup feat/x staging "$FIXFILES" "$DIFF_AUTH_BILL" "notasha"
|
|
200
200
|
DELTA_KIND=DELTA prep "$WORK/root-delta"; eq "unvalidated sha is FULL" .delta.mode FULL
|
|
201
201
|
|
|
202
|
+
# --- 5b. explicit --delta-from (forge#4360: forgedock review-staging --delta): no resolver involved ---
|
|
203
|
+
cmt() { # cmt <body> [association]: one comment object
|
|
204
|
+
jq -n --arg b "$1" --arg a "${2:-OWNER}" '{body: $b, author_association: $a, user: {login: "r", type: "User"}}'
|
|
205
|
+
}
|
|
206
|
+
agent_body() { printf '<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: %s\n<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->' "$1"; }
|
|
207
|
+
# shellcheck disable=SC2016 # the backticks are literal markdown
|
|
208
|
+
staging_summary() { printf '<!-- FORGE:GATE_FAILURE -->\n## Staging Review: NEEDS FIXES FIRST\n\n**Head**: `%s`\n\n<!-- FORGE:GATE_FAILURE:TYPE=staging-review|BUNDLE= -->' "$1"; }
|
|
209
|
+
comments_of() { jq -s . > "$FIX/comments.json"; } # comments_of < <cmt ...>...
|
|
210
|
+
earlier_pass() { # earlier_pass <sha> [association]: a complete earlier pass (agent body + post-guard staging summary)
|
|
211
|
+
{ cmt "$(agent_body "$1")" "${2:-OWNER}"; cmt "$(staging_summary "$1")" "${2:-OWNER}"; } | comments_of
|
|
212
|
+
}
|
|
213
|
+
git -C "$REPO_DIR" checkout -q -b pr-empty pr-branch; git -C "$REPO_DIR" commit -q --allow-empty -m C; # own branch: pr-branch stays at B (pinned-fallback tests below)
|
|
214
|
+
SHA_C=$(git -C "$REPO_DIR" rev-parse HEAD)
|
|
215
|
+
git -C "$REPO_DIR" checkout -q -b side main; echo d > "$REPO_DIR/d.txt"; git -C "$REPO_DIR" add -A; git -C "$REPO_DIR" commit -qm D
|
|
216
|
+
SHA_D=$(git -C "$REPO_DIR" rev-parse HEAD); git -C "$REPO_DIR" checkout -q main
|
|
217
|
+
setup staging main "$FIXFILES" "$DIFF_AUTH_BILL"; earlier_pass "$SHA_A"
|
|
218
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"
|
|
219
|
+
rc_is "--delta-from ok" 0; eq "--delta-from mode" .delta.mode DELTA; eq "--delta-from from" .delta.from "$SHA_A"
|
|
220
|
+
eq "--delta-from files" '.delta.files | join(" ")' "services/api/auth.py"
|
|
221
|
+
eq "--delta-from roster is the touched domains, even before 2 full rounds" '.roster.agents | join(" ")' "security auth api"
|
|
222
|
+
eq "--delta-from reason names the delta" '.roster.reason | startswith("delta ")' true
|
|
223
|
+
eq "--delta-from slices narrow to the delta" '.slices.security.paths | join(" ")' "services/api/auth.py"
|
|
224
|
+
eq "--delta-from full_files stays whole" '.full_files | length' 2
|
|
225
|
+
prep "$WORK/root-nodelta" --no-delta; eq "--no-delta without --delta-from stays FULL" .delta.mode FULL
|
|
226
|
+
earlier_pass "$SHA_A" NONE
|
|
227
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "untrusted earlier pass" 3
|
|
228
|
+
eq "untrusted earlier pass blocker" '.blockers | join(" ") | test("no earlier pass to credit")' true
|
|
229
|
+
earlier_pass "$SHA_B"
|
|
230
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "no earlier pass at the from sha" 3
|
|
231
|
+
earlier_pass "$SHA_D"
|
|
232
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_D"; rc_is "non-ancestor from sha" 3
|
|
233
|
+
eq "non-ancestor blocker" '.blockers | join(" ") | test("not an ancestor")' true
|
|
234
|
+
earlier_pass "$SHA_B"
|
|
235
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "from sha == head" 3
|
|
236
|
+
eq "head blocker" '.blockers | join(" ") | test("is the review head")' true
|
|
237
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "0123456789012345678901234567890123456789"; rc_is "unknown commit" 3
|
|
238
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "abc123"; rc_is "short sha" 3
|
|
239
|
+
setup staging main "$FIXFILES" "$DIFF_AUTH_BILL" "$SHA_C"; earlier_pass "$SHA_B"
|
|
240
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "empty delta" 0; eq "empty delta is FULL" .delta.mode FULL
|
|
241
|
+
prep "$WORK/root-nodelta" --delta-from "$SHA_B" --thorough true; rc_is "--delta-from with --thorough" 2
|
|
242
|
+
# forge#4471: the earlier pass must be complete and non-degraded, and the delta must not change review specs/config
|
|
243
|
+
setup staging main "$FIXFILES" "$DIFF_AUTH_BILL"
|
|
244
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->
|
|
245
|
+
panel degraded: 1 of 4 reviewers completed"; } | comments_of
|
|
246
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "degraded earlier pass blocks --delta-from" 3
|
|
247
|
+
eq "degraded earlier pass blocker" '.blockers | join(" ") | test("earlier pass is degraded")' true
|
|
248
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")"; cmt "<!-- FORGE:REVIEW_DEGRADED -->"; } | comments_of
|
|
249
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "REVIEW_DEGRADED after a summarised pass blocks" 3
|
|
250
|
+
{ cmt "<!-- FORGE:REVIEW_DEGRADED -->"; cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")"; } | comments_of
|
|
251
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "degraded marker before the earlier pass does not disqualify it" 0
|
|
252
|
+
{ cmt "$(agent_body "$SHA_A")"; } | comments_of
|
|
253
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "partial earlier pass (no post-guard proof) blocks" 3
|
|
254
|
+
eq "partial earlier pass blocker" '.blockers | join(" ") | test("no trusted proof that the earlier panel completed")' true
|
|
255
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")" NONE; } | comments_of
|
|
256
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "untrusted staging summary is no proof" 3
|
|
257
|
+
{ cmt "$(staging_summary "$SHA_B")"; cmt "$(agent_body "$SHA_A")"; } | comments_of
|
|
258
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "summary for another head is no proof" 3
|
|
259
|
+
{ cmt "<!-- FORGE:REVIEW-AGENT:security -->
|
|
260
|
+
Reviewed-SHA: $SHA_A
|
|
261
|
+
|
|
262
|
+
no findings block"; cmt "$(staging_summary "$SHA_A")"; } | comments_of
|
|
263
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "agent body without a findings block blocks" 3
|
|
264
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->
|
|
265
|
+
Reviewed-SHA: $SHA_A
|
|
266
|
+
<!-- REVIEW-FINDINGS-SYNTHESIZED-END -->"; } | comments_of
|
|
267
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "synthesized block is a post-guard proof" 0
|
|
268
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "# PR Review Summary
|
|
269
|
+
**Reviewed commit**: \`$SHA_A\`
|
|
270
|
+
**Agents**: 2"; } | comments_of
|
|
271
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "PR Review Summary with an Agents count above the domains blocks" 3
|
|
272
|
+
earlier_pass "$SHA_A"
|
|
273
|
+
sed 's/"changedFiles":1}/"changedFiles":1,"labels":[{"name":"review-degraded"}]}/' "$FIX/prinfo.json" > "$FIX/prinfo.tmp" && mv "$FIX/prinfo.tmp" "$FIX/prinfo.json"
|
|
274
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "review-degraded label blocks --delta-from" 3
|
|
275
|
+
eq "review-degraded label blocker" '.blockers | join(" ") | test("labelled review-degraded")' true
|
|
276
|
+
for _sf in forge.yaml scripts/trusted-comments.sh; do
|
|
277
|
+
git -C "$REPO_DIR" checkout -q -b "pr-spec-$(basename "$_sf")" pr-branch; mkdir -p "$REPO_DIR/$(dirname "$_sf")"; echo x > "$REPO_DIR/$_sf"
|
|
278
|
+
git -C "$REPO_DIR" add -A; git -C "$REPO_DIR" commit -qm "touch $_sf"; _sh=$(git -C "$REPO_DIR" rev-parse HEAD); git -C "$REPO_DIR" checkout -q main
|
|
279
|
+
setup staging main "$FIXFILES" "$DIFF_AUTH_BILL" "$_sh"; earlier_pass "$SHA_B"
|
|
280
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "delta touching $_sf blocks --delta-from" 3
|
|
281
|
+
eq "delta touching $_sf blocker" '.blockers | join(" ") | test("review spec/config")' true
|
|
282
|
+
done
|
|
283
|
+
# the spec/config list must stay identical to the resolver's FULL list
|
|
284
|
+
_pl() { grep -oE 'forge\.yaml\|commands/review-pr[^)]*\)' "$1" | head -n 1; }
|
|
285
|
+
if [ -n "$(_pl "$ROOT/scripts/review-delta.sh")" ] && [ "$(_pl "$ROOT/scripts/review-prepare.sh")" = "$(_pl "$ROOT/scripts/review-delta.sh")" ]; then ok; else bad "review-prepare.sh spec/config list drifted from scripts/review-delta.sh"; fi
|
|
286
|
+
|
|
202
287
|
# --- 6. full-build lane: empty trusted config is BLOCKED, never a silent skip ---
|
|
203
288
|
TS_DIFF='diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
|
|
204
289
|
+x'
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-staging-post.sh — deterministic post-dispatch checks of the staging->main review (forge#4359): the incomplete-panel guard,
|
|
3
|
+
# Phase 6.75 agent delivery verification and the Phase 6.5 test-gate verdict handling, 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-post.sh delivery --repo R --pr N --sd DIR [--head SHA]
|
|
8
|
+
# review-staging-post.sh test-gate --repo R --pr N --sd DIR --output FILE --prs-file F
|
|
9
|
+
# [--default BRANCH] [--config forge.yaml] [--posture blocking|advisory] [--override-phrase TEXT]
|
|
10
|
+
# delivery counts trusted, current-head FORGE:REVIEW-AGENT comments (findings block required) for every domain listed in
|
|
11
|
+
# DIR/<N>_launched_review_agents. A shortfall labels the PR review-degraded + needs-human and posts
|
|
12
|
+
# FORGE:GATE_FAILURE:TYPE=review-panel-integrity; an empty/unreadable list or a missing domain is a delivery failure.
|
|
13
|
+
# Exit 0 = every launched reviewer delivered, 1 = not.
|
|
14
|
+
# test-gate reads the saved /test-gate output FILE (the last FORGE:TEST_GATE:RESULT= marker; an absent marker stays empty and
|
|
15
|
+
# fails closed as BLOCK, never SKIP), applies the trusted override, the posture and the FORGE:GATE_PASS /
|
|
16
|
+
# FORGE:GATE_FAILURE test-gate comments, and persists DIR/<N>_test_gate_verdict and _reason.
|
|
17
|
+
# Exit 0 = continue, 1 = RESULT: BLOCK DEPLOY.
|
|
18
|
+
# Exit: 2 on a usage error.
|
|
19
|
+
#
|
|
20
|
+
# The `# BEGIN <name>` ... `# END <name>` blocks below are the spec's former fences, copied without their forge_resolve locator
|
|
21
|
+
# stubs (this script resolves helpers once, below). Helpers (trusted-comments.sh) resolve through forge_resolve from trusted
|
|
22
|
+
# install roots only, never the working directory or the repo under review.
|
|
23
|
+
set -u
|
|
24
|
+
|
|
25
|
+
CMD="${1:-}"; [ $# -gt 0 ] && shift
|
|
26
|
+
GH_REPO=""; PR_NUMBER=""; SD=""; HEAD_ARG=""; OUTPUT_FILE=""; PRS_FILE=""; CONFIG_ARG=""; POSTURE_ARG=""; PHRASE_ARG=""; DEFAULT_BRANCH="main"
|
|
27
|
+
while [ $# -gt 0 ]; do
|
|
28
|
+
[ $# -ge 2 ] || { echo "review-staging-post: $1 requires a value" >&2; exit 2; }
|
|
29
|
+
case "$1" in
|
|
30
|
+
--repo) GH_REPO="$2" ;;
|
|
31
|
+
--pr) PR_NUMBER="$2" ;;
|
|
32
|
+
--sd) SD="$2" ;;
|
|
33
|
+
--head) HEAD_ARG="$2" ;;
|
|
34
|
+
--output) OUTPUT_FILE="$2" ;;
|
|
35
|
+
--prs-file) PRS_FILE="$2" ;;
|
|
36
|
+
--config) CONFIG_ARG="$2" ;;
|
|
37
|
+
--posture) POSTURE_ARG="$2" ;;
|
|
38
|
+
--override-phrase) PHRASE_ARG="$2" ;;
|
|
39
|
+
--default) DEFAULT_BRANCH="$2" ;;
|
|
40
|
+
*) echo "review-staging-post: unknown argument: $1" >&2; exit 2 ;;
|
|
41
|
+
esac
|
|
42
|
+
shift 2
|
|
43
|
+
done
|
|
44
|
+
case "$CMD" in delivery|test-gate) ;; *) echo "review-staging-post: unknown subcommand '${CMD}' (delivery|test-gate)" >&2; exit 2 ;; esac
|
|
45
|
+
case "$PR_NUMBER" in ''|*[!0-9]*) echo "review-staging-post: --pr must be numeric" >&2; exit 2 ;; esac
|
|
46
|
+
case "$GH_REPO" in *[!A-Za-z0-9._/-]*|*/*/*|/*|*/|'') echo "review-staging-post: invalid --repo" >&2; exit 2 ;; */*) ;; *) echo "review-staging-post: --repo must be owner/name" >&2; exit 2 ;; esac
|
|
47
|
+
[ -n "$SD" ] || { echo "review-staging-post: --sd is required" >&2; exit 2; }
|
|
48
|
+
mkdir -p "$SD" || exit 2
|
|
49
|
+
export FORGE_SCRATCHPAD="$SD"
|
|
50
|
+
_SD="$SD"
|
|
51
|
+
# A malformed --head is dropped: the blocks re-resolve it and refuse on empty (fail closed).
|
|
52
|
+
REVIEW_SHA_FULL="$HEAD_ARG"
|
|
53
|
+
case "$REVIEW_SHA_FULL" in *[!0-9a-f]*|"") REVIEW_SHA_FULL="" ;; *) [ "${#REVIEW_SHA_FULL}" -eq 40 ] || [ "${#REVIEW_SHA_FULL}" -eq 64 ] || REVIEW_SHA_FULL="" ;; esac
|
|
54
|
+
unset LAUNCHED_REVIEW_AGENTS SELECTED_AGENT_COUNT TEST_GATE_VERDICT TEST_GATE_REASON
|
|
55
|
+
|
|
56
|
+
# --- trusted helper resolution (install roots only; never cwd) ---
|
|
57
|
+
# shellcheck source=/dev/null # the resolver lib is found at runtime in the trusted install root
|
|
58
|
+
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() { :; }; }
|
|
59
|
+
|
|
60
|
+
if [ "$CMD" = delivery ]; then
|
|
61
|
+
echo "=== Incomplete-panel guard and Phase 6.75: verify agent delivery ==="
|
|
62
|
+
# BEGIN panel-guard
|
|
63
|
+
# Each Bash call is a fresh shell: re-resolve the full head SHA if it is not set, and refuse when it is not a commit id
|
|
64
|
+
# (an empty SHA would make the Reviewed-SHA match accept any head).
|
|
65
|
+
[ -n "${REVIEW_SHA_FULL:-}" ] || REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
66
|
+
case "$REVIEW_SHA_FULL" in *[!0-9a-f]*|"") REVIEW_SHA_FULL="" ;; *) [ "${#REVIEW_SHA_FULL}" -eq 40 ] || [ "${#REVIEW_SHA_FULL}" -eq 64 ] || REVIEW_SHA_FULL="" ;; esac
|
|
67
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
68
|
+
ACTUAL_AGENT_COUNT=0
|
|
69
|
+
if [ -n "$REVIEW_SHA_FULL" ] && [ -n "$TRUSTED_SCRIPT" ]; then
|
|
70
|
+
# Only TRUSTED, current-head reviewer comments count, over every page of comments. Unique domains, so a duplicate does not inflate the count.
|
|
71
|
+
ACTUAL_AGENT_COUNT=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
|
|
72
|
+
| bash "$TRUSTED_SCRIPT" bodies "^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)" 2>/dev/null \
|
|
73
|
+
| jq -r 'scan("^<!-- FORGE:REVIEW-AGENT:([a-z0-9-]+) -->") | .[0]' 2>/dev/null | sort -u | grep -c . || echo 0)
|
|
74
|
+
fi
|
|
75
|
+
case "$ACTUAL_AGENT_COUNT" in ''|*[!0-9]*) ACTUAL_AGENT_COUNT=0 ;; esac # fail closed: unreadable or untrusted counts as not posted
|
|
76
|
+
# Fresh shell: SELECTED_AGENT_COUNT is not carried over. Reload it from the PR-keyed launched-agent file, then fail closed when it is empty, non-integer or zero (a staging review always selects at least one reviewer).
|
|
77
|
+
[ -n "${SELECTED_AGENT_COUNT:-}" ] || { [ -n "${PR_NUMBER:-}" ] && [ -r "$_SD/${PR_NUMBER}_launched_review_agents" ] && SELECTED_AGENT_COUNT=$(grep -E '^[a-z0-9-]+$' "$_SD/${PR_NUMBER}_launched_review_agents" | sort -u | grep -c .); }
|
|
78
|
+
SELECTED_COUNT_INVALID=false
|
|
79
|
+
case "${SELECTED_AGENT_COUNT:-}" in ''|*[!0-9]*|0) SELECTED_COUNT_INVALID=true; SELECTED_AGENT_COUNT=0 ;; esac
|
|
80
|
+
if [ "${DISPATCH_FAILED:-}" = "true" ] || [ "$SELECTED_COUNT_INVALID" = "true" ] || [ "$ACTUAL_AGENT_COUNT" -lt "$SELECTED_AGENT_COUNT" ]; then
|
|
81
|
+
gh label create "review-degraded" --color "E4E669" --description "PR review panel was incomplete; re-review required before deployment. Managed by ForgeDock." --force -R "$GH_REPO" 2>/dev/null || true
|
|
82
|
+
gh pr edit "$PR_NUMBER" -R "$GH_REPO" --add-label "review-degraded" --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
83
|
+
gh pr comment "$PR_NUMBER" -R "$GH_REPO" --body "<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->
|
|
84
|
+
## Deploy Review Blocked: Incomplete Isolated Review Panel
|
|
85
|
+
|
|
86
|
+
**Selected isolated reviewers**: ${SELECTED_AGENT_COUNT}
|
|
87
|
+
**Completed isolated reviewers**: ${ACTUAL_AGENT_COUNT}
|
|
88
|
+
|
|
89
|
+
Re-run the full staging review in a fresh session before deployment."
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
# END panel-guard
|
|
93
|
+
# BEGIN delivery-check
|
|
94
|
+
# Reload the launched-agent list persisted at dispatch (validated: lowercase marker domains only).
|
|
95
|
+
[ -n "${LAUNCHED_REVIEW_AGENTS:-}" ] || { [ -r "$_SD/${PR_NUMBER}_launched_review_agents" ] && LAUNCHED_REVIEW_AGENTS=$(grep -E '^[a-z0-9-]+$' "$_SD/${PR_NUMBER}_launched_review_agents" | sort -u | tr '\n' ' '); }
|
|
96
|
+
# Fail closed: a staging review always launches at least the Phase 2 material-change agent, so an empty list means lost state or skipped bookkeeping, never "nothing to verify".
|
|
97
|
+
if [ -z "${PR_NUMBER:-}" ] || [ -z "${LAUNCHED_REVIEW_AGENTS:-}" ]; then
|
|
98
|
+
echo "REVIEW DELIVERY FAILURE: launched-agent list is empty or unreadable (PR_NUMBER='${PR_NUMBER:-}')"
|
|
99
|
+
echo "Recover the launched reviewer domains from the PR's FORGE:REVIEW-AGENT comments, re-append them to the launched-agent file, and rerun."
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
MISSING_AGENT_COMMENTS=""
|
|
103
|
+
# Each Bash call is a fresh shell: re-resolve the full head SHA if it is not set, and refuse when it is not a commit id
|
|
104
|
+
# (an empty SHA would make the Reviewed-SHA match accept any head).
|
|
105
|
+
[ -n "${REVIEW_SHA_FULL:-}" ] || REVIEW_SHA_FULL=$(gh pr view "$PR_NUMBER" -R "$GH_REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
106
|
+
case "$REVIEW_SHA_FULL" in *[!0-9a-f]*|"") REVIEW_SHA_FULL="" ;; *) [ "${#REVIEW_SHA_FULL}" -eq 40 ] || [ "${#REVIEW_SHA_FULL}" -eq 64 ] || REVIEW_SHA_FULL="" ;; esac
|
|
107
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
108
|
+
for AGENT_DOMAIN in $LAUNCHED_REVIEW_AGENTS; do
|
|
109
|
+
AGENT_COMMENT_COUNT=0
|
|
110
|
+
if [ -n "$REVIEW_SHA_FULL" ] && [ -n "$TRUSTED_SCRIPT" ]; then
|
|
111
|
+
AGENT_COMMENT_COUNT=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null \
|
|
112
|
+
| bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->[\\s\\S]*(^|\\n)Reviewed-SHA:[ ]${REVIEW_SHA_FULL}(\\r?\\n|$)" 2>/dev/null || echo 0)
|
|
113
|
+
fi
|
|
114
|
+
case "$AGENT_COMMENT_COUNT" in ''|*[!0-9]*) AGENT_COMMENT_COUNT=0 ;; esac # fail closed: counts as missing
|
|
115
|
+
if [ "${AGENT_COMMENT_COUNT:-0}" -lt 1 ]; then
|
|
116
|
+
MISSING_AGENT_COMMENTS="${MISSING_AGENT_COMMENTS} ${AGENT_DOMAIN}"
|
|
117
|
+
fi
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
if [ -n "$MISSING_AGENT_COMMENTS" ]; then
|
|
121
|
+
echo "REVIEW DELIVERY FAILURE: missing findings comment(s) for:${MISSING_AGENT_COMMENTS}"
|
|
122
|
+
echo "A comment without a REVIEW-FINDINGS-START block counts as missing. Re-dispatch the missing agent once with the note: structured findings block required. A second miss is a degraded panel. Recover each agent's returned verdict, findings, and durable body path before rerunning review."
|
|
123
|
+
exit 1
|
|
124
|
+
fi
|
|
125
|
+
# END delivery-check
|
|
126
|
+
echo "DELIVERY: OK (${ACTUAL_AGENT_COUNT}/${SELECTED_AGENT_COUNT} launched reviewers posted a findings block for head ${REVIEW_SHA_FULL})"
|
|
127
|
+
exit 0
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# test-gate
|
|
131
|
+
case "$POSTURE_ARG" in ''|blocking|advisory) ;; *) echo "review-staging-post: --posture must be blocking or advisory" >&2; exit 2 ;; esac
|
|
132
|
+
[ -n "$PRS_FILE" ] || { echo "review-staging-post: --prs-file is required" >&2; exit 2; }
|
|
133
|
+
[ -r "$PRS_FILE" ] || { echo "review-staging-post: --prs-file $PRS_FILE is not readable (run the pre-dispatch first)" >&2; exit 2; }
|
|
134
|
+
[ -n "$OUTPUT_FILE" ] || { echo "review-staging-post: --output is required (the saved /test-gate output)" >&2; exit 2; }
|
|
135
|
+
ALL_PR_NUMBERS=$(grep -E '^[0-9]+$' "$PRS_FILE")
|
|
136
|
+
CONFIG_FILE="${CONFIG_ARG:-${FORGE_CONFIG:-forge.yaml}}"
|
|
137
|
+
GH_FLAG="-R $GH_REPO"
|
|
138
|
+
GATE_POSTURE="$POSTURE_ARG"
|
|
139
|
+
[ -n "$GATE_POSTURE" ] || GATE_POSTURE=$(yq '.verification.test_gate.posture // "blocking"' "$CONFIG_FILE" 2>/dev/null || echo "blocking")
|
|
140
|
+
case "$GATE_POSTURE" in advisory) ;; *) GATE_POSTURE=blocking ;; esac # unreadable or unknown is the blocking default
|
|
141
|
+
OVERRIDE_PHRASE="$PHRASE_ARG"
|
|
142
|
+
[ -n "$OVERRIDE_PHRASE" ] || OVERRIDE_PHRASE=$(yq '.verification.test_gate.override_phrase // "OVERRIDE: shipping with test failures —"' "$CONFIG_FILE" 2>/dev/null || echo "OVERRIDE: shipping with test failures —")
|
|
143
|
+
[ -n "$OVERRIDE_PHRASE" ] || OVERRIDE_PHRASE="OVERRIDE: shipping with test failures —"
|
|
144
|
+
echo "=== Phase 6.5: Runtime Test Gate ==="
|
|
145
|
+
echo "Bundle PRs: $(echo $ALL_PR_NUMBERS | tr '\n' ' ')"
|
|
146
|
+
echo "Posture: ${GATE_POSTURE}"
|
|
147
|
+
# Reset any stale gate result from a prior run (files are PR-keyed)
|
|
148
|
+
rm -f "${_SD:?}/${PR_NUMBER:?}_test_gate_verdict" "${_SD:?}/${PR_NUMBER:?}_test_gate_reason"
|
|
149
|
+
TEST_GATE_VERDICT="SKIP"
|
|
150
|
+
TEST_GATE_REASON="Phase 6.5 not yet run"
|
|
151
|
+
GATE_OUTPUT=""
|
|
152
|
+
if [ -r "$OUTPUT_FILE" ]; then GATE_OUTPUT=$(cat "$OUTPUT_FILE"); else echo "WARNING: /test-gate output $OUTPUT_FILE unreadable: treated as a missing result"; fi
|
|
153
|
+
# BEGIN test-gate-extract
|
|
154
|
+
# Extract machine-readable verdict from Skill output. An absent/invalid marker stays EMPTY (never coerced to SKIP):
|
|
155
|
+
# the verdict-handling fence treats an empty verdict as missing/invalid and fails closed under blocking posture.
|
|
156
|
+
TEST_GATE_VERDICT=$(echo "$GATE_OUTPUT" | grep -oE 'FORGE:TEST_GATE:RESULT=(BLOCK|PASS|SKIP)' | sed 's/^FORGE:TEST_GATE:RESULT=//' | tail -1)
|
|
157
|
+
[ -n "$TEST_GATE_VERDICT" ] || TEST_GATE_REASON="gate result missing/invalid (no FORGE:TEST_GATE:RESULT marker in /test-gate output)"
|
|
158
|
+
# END test-gate-extract
|
|
159
|
+
echo "Test-gate verdict: ${TEST_GATE_VERDICT}"
|
|
160
|
+
printf '%s\n' "$TEST_GATE_VERDICT" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "$TEST_GATE_REASON" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
161
|
+
# BEGIN test-gate-verdict
|
|
162
|
+
# Reload the Phase 6.5 gate result persisted by its producer (validated: only a known verdict reaches a gate decision).
|
|
163
|
+
[ -n "${TEST_GATE_VERDICT:-}" ] || { [ -r "$_SD/${PR_NUMBER}_test_gate_verdict" ] && TEST_GATE_VERDICT=$(sed -n -E '1{/^(BLOCK|PASS|SKIP)$/p;q}' "$_SD/${PR_NUMBER}_test_gate_verdict"); }
|
|
164
|
+
[ -n "${TEST_GATE_REASON:-}" ] || { [ -r "$_SD/${PR_NUMBER}_test_gate_reason" ] && TEST_GATE_REASON=$(sed -n 1p "$_SD/${PR_NUMBER}_test_gate_reason" | tr -d '\r\000-\037' | cut -c1-400); }
|
|
165
|
+
# Fail closed: a missing, empty, or unrecognised verdict is never a SKIP. Normalize it to BLOCK so the BLOCK arm applies
|
|
166
|
+
# the override check, advisory/blocking posture, and exit 1; GATE_VERDICT_INVALID makes the reasons say missing/invalid.
|
|
167
|
+
GATE_VERDICT_INVALID=0
|
|
168
|
+
case "${TEST_GATE_VERDICT:-}" in
|
|
169
|
+
BLOCK|PASS|SKIP) ;;
|
|
170
|
+
*)
|
|
171
|
+
GATE_VERDICT_INVALID=1
|
|
172
|
+
if [ -r "$_SD/${PR_NUMBER}_test_gate_verdict" ]; then _GV_WHY="persisted verdict file present but empty/invalid"; else _GV_WHY="persisted verdict file missing"; fi
|
|
173
|
+
echo "⚠️ Test gate: gate result missing/invalid (${_GV_WHY}) — treating as BLOCK, not SKIP."
|
|
174
|
+
TEST_GATE_VERDICT="BLOCK"
|
|
175
|
+
;;
|
|
176
|
+
esac
|
|
177
|
+
case "$TEST_GATE_VERDICT" in
|
|
178
|
+
|
|
179
|
+
SKIP)
|
|
180
|
+
echo "ℹ️ Test gate: SKIP — no executable changes or tests not configured."
|
|
181
|
+
echo " This gap will appear in the Phase 8 summary. No deploy impact."
|
|
182
|
+
TEST_GATE_REASON="SKIP — no runtime tests ran (docs-only bundle, no integration tests configured, or manual-only criteria)"
|
|
183
|
+
;;
|
|
184
|
+
|
|
185
|
+
PASS)
|
|
186
|
+
echo "✅ Test gate: PASS — all test clusters passed. Deploy may proceed."
|
|
187
|
+
TEST_GATE_REASON="PASS — all automated test clusters passed"
|
|
188
|
+
# Post FORGE:GATE_PASS for symmetric observability
|
|
189
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
190
|
+
gh pr comment "$PR_NUMBER" ${GH_FLAG} --body "<!-- FORGE:GATE_PASS -->
|
|
191
|
+
## Deploy Gate: PASSED
|
|
192
|
+
|
|
193
|
+
**Gate**: test-gate (runtime acceptance criteria)
|
|
194
|
+
**Result**: PASS — all test clusters passed. Deploy may proceed.
|
|
195
|
+
**Bundle PRs**: ${ALL_PR_NUMBERS}
|
|
196
|
+
**Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
197
|
+
|
|
198
|
+
<!-- FORGE:GATE_PASS:TYPE=test-gate|BUNDLE=$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs) -->" 2>/dev/null || true
|
|
199
|
+
fi
|
|
200
|
+
;;
|
|
201
|
+
|
|
202
|
+
BLOCK)
|
|
203
|
+
# Check for override comment on the staging→main PR (mirrors Phase 0A pattern)
|
|
204
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
205
|
+
# Trusted authors only, all comment pages, anchored; fail closed (no override) if the script or comments are
|
|
206
|
+
# unreadable (#3492). OVERRIDE_PHRASE comes from forge.yaml, so regex-escape it before using it as a pattern.
|
|
207
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
208
|
+
TG_SCRIPT="$TRUSTED_SCRIPT"
|
|
209
|
+
TG_COMMENTS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null) || TG_COMMENTS=""
|
|
210
|
+
TG_OVERRIDE=0; OVERRIDE_REASON="(reason not captured)"
|
|
211
|
+
if [ -n "$TG_SCRIPT" ] && [ -n "$TG_COMMENTS" ]; then
|
|
212
|
+
TG_RE="^$(printf '%s' "$OVERRIDE_PHRASE" | sed 's/[][\\.^$*+?(){}|\/]/\\&/g')"
|
|
213
|
+
TG_OVERRIDE=$(printf '%s' "$TG_COMMENTS" | bash "$TG_SCRIPT" count "$TG_RE" 2>/dev/null || echo 0)
|
|
214
|
+
[ "${TG_OVERRIDE:-0}" -gt 0 ] && OVERRIDE_REASON=$(printf '%s' "$TG_COMMENTS" | bash "$TG_SCRIPT" bodies "$TG_RE" 2>/dev/null | tail -1 | jq -r '.' 2>/dev/null || echo "(reason not captured)")
|
|
215
|
+
fi
|
|
216
|
+
else
|
|
217
|
+
TG_OVERRIDE=0
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
if [ "${TG_OVERRIDE:-0}" -gt 0 ]; then
|
|
221
|
+
echo "⚠️ Test gate: BLOCK — but override comment detected on PR #${PR_NUMBER}."
|
|
222
|
+
echo " Override: ${OVERRIDE_REASON}"
|
|
223
|
+
echo " Proceeding with deploy. Override is logged in Phase 8 summary."
|
|
224
|
+
TEST_GATE_VERDICT="PASS"
|
|
225
|
+
TEST_GATE_REASON="BLOCK downgraded to PASS by override: ${OVERRIDE_REASON}"
|
|
226
|
+
[ "$GATE_VERDICT_INVALID" = 1 ] && TEST_GATE_REASON="BLOCK (gate result missing/invalid) downgraded to PASS by override: ${OVERRIDE_REASON}"
|
|
227
|
+
|
|
228
|
+
elif [ "$GATE_POSTURE" = "advisory" ]; then
|
|
229
|
+
echo "⚠️ Test gate: BLOCK (advisory posture) — runtime failures detected but deploy is NOT prevented."
|
|
230
|
+
echo " Switch verification.test_gate.posture to 'blocking' in forge.yaml to enforce this gate."
|
|
231
|
+
TEST_GATE_REASON="BLOCK (advisory) — runtime failures detected; deploy allowed by advisory posture"
|
|
232
|
+
[ "$GATE_VERDICT_INVALID" = 1 ] && TEST_GATE_REASON="BLOCK (advisory) — gate result missing/invalid; deploy allowed by advisory posture"
|
|
233
|
+
|
|
234
|
+
else
|
|
235
|
+
# blocking posture (default) — STOP
|
|
236
|
+
echo "⛔ DEPLOY BLOCKED — /test-gate returned BLOCK verdict."
|
|
237
|
+
[ "$GATE_VERDICT_INVALID" = 1 ] && echo " (gate result missing/invalid — failing closed; re-run /test-gate or post the override comment)"
|
|
238
|
+
echo ""
|
|
239
|
+
echo "Runtime failures were detected in the staging→${DEFAULT_BRANCH} bundle."
|
|
240
|
+
echo "The failures were batch-introduced (not pre-existing on ${DEFAULT_BRANCH} baseline)."
|
|
241
|
+
echo ""
|
|
242
|
+
echo "Options:"
|
|
243
|
+
echo " 1. Fix the failing tests and merge fixes to staging before retrying the deploy."
|
|
244
|
+
echo " 2. Post a comment on this PR starting with \"${OVERRIDE_PHRASE} <reason>\" to bypass this gate."
|
|
245
|
+
echo " 3. Set verification.test_gate.posture: advisory in forge.yaml to downgrade to a warning."
|
|
246
|
+
echo ""
|
|
247
|
+
echo "RESULT: BLOCK DEPLOY"
|
|
248
|
+
|
|
249
|
+
# Post structured FORGE:GATE_FAILURE comment for pipeline-health tracking
|
|
250
|
+
GATE_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
251
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
252
|
+
gh pr comment "$PR_NUMBER" ${GH_FLAG} --body "<!-- FORGE:GATE_FAILURE -->
|
|
253
|
+
## Deploy Gate: BLOCKED
|
|
254
|
+
|
|
255
|
+
**Gate**: test-gate
|
|
256
|
+
**Timestamp**: ${GATE_TIMESTAMP}
|
|
257
|
+
**Bundle PRs**: $(echo $ALL_PR_NUMBERS | tr '\n' ' ')
|
|
258
|
+
**Posture**: ${GATE_POSTURE}
|
|
259
|
+
|
|
260
|
+
### What Happened
|
|
261
|
+
|
|
262
|
+
\`/test-gate\` detected runtime failures in the staging→\`${DEFAULT_BRANCH}\` bundle that were NOT present on the \`${DEFAULT_BRANCH}\` baseline. These are batch-introduced regressions. (If the gate result was missing/invalid, no verdict could be read and the gate failed closed; re-run \`/test-gate\`.)
|
|
263
|
+
|
|
264
|
+
\`/test-gate\` has filed \`test-failure\` issues for each failing cluster — check the issue tracker for details.
|
|
265
|
+
|
|
266
|
+
### Resolution
|
|
267
|
+
|
|
268
|
+
1. Fix the failing tests and merge fixes to staging.
|
|
269
|
+
2. Retry the staging→\`${DEFAULT_BRANCH}\` deploy.
|
|
270
|
+
|
|
271
|
+
To override (ship known failures with documented reason), post a comment containing:
|
|
272
|
+
\`${OVERRIDE_PHRASE} <reason>\`
|
|
273
|
+
|
|
274
|
+
<!-- FORGE:GATE_FAILURE:TYPE=test-gate|BUNDLE=$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs) -->" 2>/dev/null || true
|
|
275
|
+
fi
|
|
276
|
+
_GV_REASON="BLOCK (blocking posture, no override)"; [ "$GATE_VERDICT_INVALID" = 1 ] && _GV_REASON="BLOCK (gate result missing/invalid; blocking posture, no override)"
|
|
277
|
+
printf '%s\n' "$TEST_GATE_VERDICT" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "$_GV_REASON" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
278
|
+
exit 1
|
|
279
|
+
fi
|
|
280
|
+
;;
|
|
281
|
+
|
|
282
|
+
*)
|
|
283
|
+
# Unreachable (normalized above); fail closed regardless — never SKIP.
|
|
284
|
+
echo "⛔ Test gate: gate result missing/invalid ('${TEST_GATE_VERDICT}') — failing closed."
|
|
285
|
+
printf '%s\n' "BLOCK" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "BLOCK (gate result missing/invalid)" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
286
|
+
exit 1
|
|
287
|
+
;;
|
|
288
|
+
|
|
289
|
+
esac
|
|
290
|
+
# Persist the gate result: every Bash call is a fresh shell, and the verdict-handling fence and Phase 8 read it back (validated on reload).
|
|
291
|
+
printf '%s\n' "$TEST_GATE_VERDICT" > "$_SD/${PR_NUMBER}_test_gate_verdict"; printf '%s\n' "$TEST_GATE_REASON" > "$_SD/${PR_NUMBER}_test_gate_reason"
|
|
292
|
+
# END test-gate-verdict
|
|
293
|
+
exit 0
|