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