forgedock 1.29.1 → 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 +5 -2
- package/bin/engine/phases.mjs +20 -3
- 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 +453 -66
- package/bin/engine.mjs +50 -38
- 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 +356 -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 +278 -22
- package/bin/lean/conformance.test.mjs +251 -5
- package/bin/lean/context.mjs +133 -55
- package/bin/lean/context.test.mjs +175 -9
- 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 +55 -19
- package/bin/lean/enumerating.mjs +189 -0
- package/bin/lean/enumerating.test.mjs +97 -0
- 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 +799 -127
- package/bin/lean/lean.test.mjs +1749 -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 +341 -99
- 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 +180 -0
- package/bin/lean/precheck.test.mjs +205 -0
- package/bin/lean/prefilter.mjs +180 -9
- package/bin/lean/prefilter.test.mjs +175 -3
- package/bin/lean/preflight.mjs +238 -0
- package/bin/lean/preflight.test.mjs +108 -0
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +134 -12
- package/bin/lean/recovery.test.mjs +75 -0
- 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 +385 -0
- package/bin/lean/sequence.test.mjs +373 -0
- package/bin/lean/testmap.mjs +43 -6
- package/bin/lean/testmap.test.mjs +38 -1
- 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 +77 -10
- package/bin/lean/verify.test.mjs +74 -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 +6 -2
- 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 +88 -6
- package/docs/OUTCOME-CODES.md +7 -4
- package/docs/RELEASE-NOTES.md +10 -0
- 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/acceptance-red-green.sh +13 -4
- package/scripts/acceptance-red-green.test.sh +14 -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/test-map.sh +63 -17
- package/scripts/test-map.test.sh +55 -0
- package/scripts/trusted-comments.test.sh +1 -1
|
@@ -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
|
|
@@ -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 ]
|