forgedock 1.14.0 → 1.16.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/phases-args.test.mjs +11 -0
- package/bin/engine/phases.mjs +133 -15
- package/bin/engine/phases.test.mjs +24 -0
- package/bin/engine/runlog.mjs +10 -1
- package/bin/engine.mjs +43 -7
- package/bin/forgedock.mjs +1 -1
- package/commands/forge-stats.md +10 -0
- package/commands/issue.md +17 -2
- package/commands/orchestrate/phase-4-execution.md +10 -3
- package/commands/review-pr-staging.md +290 -30
- package/commands/review-pr.md +71 -22
- package/commands/work-on/build/implement.md +15 -1
- package/commands/work-on/build.md +7 -4
- package/commands/work-on/investigate.md +5 -3
- package/commands/work-on/remediate.md +215 -35
- package/docs/CONFIG.md +2 -2
- package/docs/RELEASE-NOTES.md +12 -0
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +6 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/check-spec-size.sh +102 -0
- package/scripts/check-spec-size.test.sh +85 -0
- package/scripts/classify-finding.sh +14 -7
- package/scripts/classify-finding.test.sh +9 -0
- package/scripts/forge-root.test.sh +41 -4
- package/scripts/remediate-approved-head.test.sh +43 -0
- package/scripts/remediation-marker-match.test.sh +6 -0
- package/scripts/resolve-deploy-gate.sh +51 -0
- package/scripts/resolve-deploy-gate.test.sh +59 -0
- package/scripts/review-pr-delta-wiring.test.sh +8 -0
- package/scripts/review-pr-domain-map.test.sh +10 -0
- package/scripts/review-pr-staging-scope.test.sh +127 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-spec-size.sh — PR spec-size budget (forge#3762).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Usage: check-spec-size.sh [base_ref] (default: $BASE_REF, else origin/staging)
|
|
7
|
+
# Env: PR_LABELS comma-separated PR label names (untrusted; grep-matched only)
|
|
8
|
+
# PR_BODY PR body text (untrusted; grep-matched only)
|
|
9
|
+
# GITHUB_STEP_SUMMARY summary file (stdout when unset)
|
|
10
|
+
# Exit: 0 pass/exempt, 1 budget violation, 2 usage/base-ref error (fail closed)
|
|
11
|
+
#
|
|
12
|
+
# Rules (against the PR base, compared in bytes):
|
|
13
|
+
# 1. Budgeted specs may not grow by more than 2% or more than 4096 bytes.
|
|
14
|
+
# 2. No commands/**/*.md file may gain a canonical block copy (per-file marker
|
|
15
|
+
# count may not rise). Count-based until the shared helper lands (#3493).
|
|
16
|
+
# Shrinking or equal size always passes.
|
|
17
|
+
# Exemption: `spec-size-exempt` label, or a `<!-- spec-size-exempt: <reason> -->`
|
|
18
|
+
# line with a non-empty reason in the PR body. Failures are reported, exit 0.
|
|
19
|
+
# Bash 3.2 portable: no assoc arrays, mapfile, grep -P.
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
BASE="${1:-${BASE_REF:-origin/staging}}"
|
|
23
|
+
PCT_NUM=2 # 2%
|
|
24
|
+
ABS_MAX=4096 # bytes
|
|
25
|
+
BUDGETED="commands/review-pr.md commands/review-pr-staging.md commands/work-on/remediate.md commands/work-on/review.md commands/work-on.md"
|
|
26
|
+
|
|
27
|
+
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
|
28
|
+
out() { printf '%s\n' "$*" >> "$SUMMARY"; }
|
|
29
|
+
|
|
30
|
+
if ! git rev-parse --verify -q "$BASE^{commit}" >/dev/null; then
|
|
31
|
+
echo "ERROR: base ref '$BASE' cannot be resolved (shallow checkout? need fetch-depth: 0)" >&2
|
|
32
|
+
exit 2
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# --- exemption -------------------------------------------------------------
|
|
36
|
+
EXEMPT_REASON=""
|
|
37
|
+
if printf '%s' "${PR_LABELS:-}" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -qxF 'spec-size-exempt'; then
|
|
38
|
+
EXEMPT_REASON="label spec-size-exempt"
|
|
39
|
+
else
|
|
40
|
+
R=$(printf '%s\n' "${PR_BODY:-}" | tr -d '\r' \
|
|
41
|
+
| sed -n 's/^[[:space:]]*<!--[[:space:]]*spec-size-exempt:[[:space:]]*\(.*\)-->[[:space:]]*$/\1/p' \
|
|
42
|
+
| head -n 1 | sed 's/[[:space:]]*$//')
|
|
43
|
+
[ -n "$R" ] && EXEMPT_REASON="PR body marker: $R"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
FAILURES=0
|
|
47
|
+
base_size() { git cat-file -s "$BASE:$1" 2>/dev/null || echo 0; }
|
|
48
|
+
MARKERS='FORGE_ROOT bootstrap \(canonical|TRUSTED_SCRIPT=""|HELPER_SCRIPT=""|mkdir -p "\$_SD".*# FENCE_BOOTSTRAP'
|
|
49
|
+
|
|
50
|
+
out "## Spec-size budget (base: \`$BASE\`)"
|
|
51
|
+
out ""
|
|
52
|
+
out "| File | Base | Head | Delta | Delta % | Status |"
|
|
53
|
+
out "|------|-----:|-----:|------:|--------:|--------|"
|
|
54
|
+
for f in $BUDGETED; do
|
|
55
|
+
[ -f "$f" ] || [ "$(base_size "$f")" != 0 ] || continue
|
|
56
|
+
b=$(base_size "$f")
|
|
57
|
+
h=0; [ -f "$f" ] && h=$(wc -c < "$f" | tr -d ' ')
|
|
58
|
+
g=$((h - b))
|
|
59
|
+
status="ok"
|
|
60
|
+
if [ "$g" -gt 0 ] && { [ $((g * 100)) -gt $((b * PCT_NUM)) ] || [ "$g" -gt "$ABS_MAX" ]; }; then
|
|
61
|
+
status="OVER BUDGET"; FAILURES=$((FAILURES + 1))
|
|
62
|
+
fi
|
|
63
|
+
if [ "$b" -gt 0 ]; then
|
|
64
|
+
p=$(awk -v g="$g" -v b="$b" 'BEGIN{printf "%+.2f%%", g*100/b}')
|
|
65
|
+
else p="n/a"; fi
|
|
66
|
+
out "| \`$f\` | $b | $h | $g | $p | $status |"
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
DUP_ROWS=""
|
|
70
|
+
# Every commands/**/*.md present at head (new files have base count 0).
|
|
71
|
+
for f in $(git ls-files 'commands/*.md' 'commands/**/*.md' | sort -u); do
|
|
72
|
+
[ -f "$f" ] || continue
|
|
73
|
+
hc=$(grep -cE "$MARKERS" "$f" 2>/dev/null || true); hc=${hc:-0}
|
|
74
|
+
bc=$(git show "$BASE:$f" 2>/dev/null | grep -cE "$MARKERS" 2>/dev/null || true); bc=${bc:-0}
|
|
75
|
+
if [ "$hc" -gt "$bc" ]; then
|
|
76
|
+
DUP_ROWS="$DUP_ROWS| \`$f\` | $bc | $hc | NEW CANONICAL BLOCK COPY |
|
|
77
|
+
"
|
|
78
|
+
FAILURES=$((FAILURES + 1))
|
|
79
|
+
fi
|
|
80
|
+
done
|
|
81
|
+
if [ -n "$DUP_ROWS" ]; then
|
|
82
|
+
out ""
|
|
83
|
+
out "### Canonical block copies added (source the shared helper instead)"
|
|
84
|
+
out ""
|
|
85
|
+
out "| File | Base markers | Head markers | Status |"
|
|
86
|
+
out "|------|-----:|-----:|--------|"
|
|
87
|
+
printf '%s' "$DUP_ROWS" >> "$SUMMARY"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
out ""
|
|
91
|
+
if [ "$FAILURES" -eq 0 ]; then
|
|
92
|
+
out "Result: PASS"
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
if [ -n "$EXEMPT_REASON" ]; then
|
|
96
|
+
out "Result: EXEMPT ($FAILURES violation(s) waived: $EXEMPT_REASON)"
|
|
97
|
+
echo "spec-size: $FAILURES violation(s) waived: $EXEMPT_REASON"
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
out "Result: FAIL ($FAILURES violation(s)). Shrink the spec, or add the spec-size-exempt label / a \`<!-- spec-size-exempt: <reason> -->\` line to the PR body."
|
|
101
|
+
echo "spec-size: FAIL ($FAILURES violation(s)); see job summary" >&2
|
|
102
|
+
exit 1
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-spec-size.test.sh — cases for scripts/check-spec-size.sh (forge#3762).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-spec-size.sh"
|
|
7
|
+
PASS=0; FAILN=0
|
|
8
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
9
|
+
ok() { PASS=$((PASS+1)); }
|
|
10
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
11
|
+
|
|
12
|
+
git init -q --bare "$TMP/origin.git"
|
|
13
|
+
git clone -q "$TMP/origin.git" "$TMP/w" 2>/dev/null
|
|
14
|
+
cd "$TMP/w" || exit 1
|
|
15
|
+
git config user.email t@example.com; git config user.name t
|
|
16
|
+
mkdir -p commands/work-on
|
|
17
|
+
gen() { awk -v n="$1" 'BEGIN{for(i=0;i<n;i++) print "line of spec text number " i}'; }
|
|
18
|
+
gen 4000 > commands/review-pr.md # ~110 KB: 2% ~ 2.2 KB < 4 KB cap
|
|
19
|
+
gen 20000 > commands/work-on.md # ~560 KB: 2% ~ 11 KB, so the 4 KB cap binds
|
|
20
|
+
gen 100 > commands/other.md
|
|
21
|
+
gen 100 > commands/work-on/remediate.md
|
|
22
|
+
git add -A; git commit -qm base; git branch -M staging; git push -q origin staging 2>/dev/null
|
|
23
|
+
git checkout -q -b feat
|
|
24
|
+
|
|
25
|
+
reset() { git reset -q --hard origin/staging; git clean -qfd; }
|
|
26
|
+
run() { # name want_rc [env...]
|
|
27
|
+
local name="$1" want="$2"; shift 2
|
|
28
|
+
local sum="$TMP/summary"; : > "$sum"
|
|
29
|
+
env GITHUB_STEP_SUMMARY="$sum" PR_LABELS="" PR_BODY="" "$@" bash "$S" origin/staging >"$TMP/out" 2>"$TMP/err"
|
|
30
|
+
local rc=$?
|
|
31
|
+
if [ "$rc" = "$want" ]; then ok; else bad "$name (rc=$rc want $want)"; cat "$TMP/err"; fi
|
|
32
|
+
}
|
|
33
|
+
has() { grep -qF "$2" "$TMP/summary" && ok || bad "$1 (summary lacks: $2)"; }
|
|
34
|
+
|
|
35
|
+
reset; run "no change passes" 0
|
|
36
|
+
has "summary shows delta table" "| File | Base | Head | Delta |"
|
|
37
|
+
|
|
38
|
+
reset; gen 100 >> commands/review-pr.md # +~2.8 KB on ~110 KB = ~2.5% (>2%, <4 KB)
|
|
39
|
+
run "over 2% fails" 1; has "over-budget flagged" "OVER BUDGET"
|
|
40
|
+
|
|
41
|
+
reset; gen 200 >> commands/work-on.md # +~5.6 KB (<2% of 560 KB, >4 KB)
|
|
42
|
+
run "over 4 KB fails" 1
|
|
43
|
+
|
|
44
|
+
reset; gen 5 >> commands/review-pr.md # small growth under both limits
|
|
45
|
+
run "small growth passes" 0
|
|
46
|
+
|
|
47
|
+
reset; gen 5000 >> commands/other.md # not a budgeted spec
|
|
48
|
+
run "non-budgeted growth passes" 0
|
|
49
|
+
|
|
50
|
+
reset; gen 100 > commands/review-pr.md # shrink
|
|
51
|
+
run "shrink passes" 0
|
|
52
|
+
|
|
53
|
+
reset; printf 'TRUSTED_SCRIPT=""\n' >> commands/other.md
|
|
54
|
+
run "new TRUSTED_SCRIPT copy fails" 1; has "dup flagged" "NEW CANONICAL BLOCK COPY"
|
|
55
|
+
|
|
56
|
+
reset; printf '_SD=x; mkdir -p "$_SD" # FENCE_BOOTSTRAP\n' >> commands/other.md
|
|
57
|
+
run "new FENCE_BOOTSTRAP anchor fails" 1
|
|
58
|
+
|
|
59
|
+
reset; printf 'echo hi # FENCE_BOOTSTRAP\n' >> commands/other.md
|
|
60
|
+
run "non-anchor FENCE_BOOTSTRAP tag passes" 0
|
|
61
|
+
|
|
62
|
+
reset; printf '# FORGE_ROOT bootstrap (canonical)\n' > commands/new-spec.md; git add -A
|
|
63
|
+
run "new spec file with block fails" 1
|
|
64
|
+
|
|
65
|
+
# Existing block, count unchanged or reduced, passes
|
|
66
|
+
reset; printf 'HELPER_SCRIPT=""\n' >> commands/other.md; git commit -qam x; git push -q origin HEAD:staging 2>/dev/null
|
|
67
|
+
git fetch -q origin staging
|
|
68
|
+
run "unchanged count passes" 0
|
|
69
|
+
: > commands/other.md; git add -A
|
|
70
|
+
run "reduced count passes" 0
|
|
71
|
+
|
|
72
|
+
# Exemption
|
|
73
|
+
reset; gen 100 >> commands/review-pr.md
|
|
74
|
+
run "exempt by label" 0 PR_LABELS="bug,spec-size-exempt"; has "label reported" "EXEMPT"
|
|
75
|
+
run "exempt by body marker" 0 PR_BODY=$'text\n<!-- spec-size-exempt: security fix -->\nmore'; has "reason reported" "security fix"
|
|
76
|
+
run "empty-reason marker not exempt" 1 PR_BODY='<!-- spec-size-exempt: -->'
|
|
77
|
+
run "label substring not exempt" 1 PR_LABELS="spec-size-exempt-not"
|
|
78
|
+
|
|
79
|
+
# Fail closed on unresolvable base
|
|
80
|
+
: > "$TMP/summary"
|
|
81
|
+
GITHUB_STEP_SUMMARY="$TMP/summary" bash "$S" origin/does-not-exist >/dev/null 2>&1; rc=$?
|
|
82
|
+
[ "$rc" = 2 ] && ok || bad "unresolved base fails closed (rc=$rc)"
|
|
83
|
+
|
|
84
|
+
echo "check-spec-size.test: pass=$PASS fail=$FAILN"
|
|
85
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -51,7 +51,9 @@
|
|
|
51
51
|
# earlier head of this PR or by a bundle's member PRs, one per line, columns
|
|
52
52
|
# file<TAB>line-start<TAB>line-end<TAB>title<TAB>prior-disposition<TAB>issue#.
|
|
53
53
|
# prior-disposition is `filed` (its issue is still open), `filed-closed` (issue closed:
|
|
54
|
-
# the row is IGNORED so a regressed defect is classified normally)
|
|
54
|
+
# the row is IGNORED so a regressed defect is classified normally), `note` (a recorded NOTE) or
|
|
55
|
+
# `unknown` (a reviewer-body row whose issue linkage failed: it is IGNORED for a MEDIUM
|
|
56
|
+
# CONFIRMED/LIKELY finding so it can never suppress one, and behaves like `note` otherwise).
|
|
55
57
|
# Line columns may be empty (NOTE-sourced rows carry no range). A finding re-flags a row
|
|
56
58
|
# when it is the same file AND ((line IoU >= 0.6 and >= 1 shared title token), or >= 3 shared title tokens). Output is
|
|
57
59
|
# `NOTE reflag-filed #N` (row has an issue number) or `NOTE reflag-note`; this gate never
|
|
@@ -180,7 +182,7 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
|
|
|
180
182
|
case "$_rls" in ''|*[!0-9]*) _rls=""; _rle="" ;; esac
|
|
181
183
|
case "$_rle" in ''|*[!0-9]*) _rle="$_rls" ;; esac
|
|
182
184
|
_dmap="$DIFF_MAP"; { [ -n "$_dmap" ] && [ -r "$_dmap" ]; } || _dmap=""
|
|
183
|
-
_reflag=$(awk -F '\t' -v file="$_rpath" -v ls="$_rls" -v le="$_rle" -v title="$TITLE" -v dmap="$_dmap" '
|
|
185
|
+
_reflag=$(awk -F '\t' -v file="$_rpath" -v ls="$_rls" -v le="$_rle" -v title="$TITLE" -v dmap="$_dmap" -v sev="$SEV" -v conf="$CONF" '
|
|
184
186
|
function toks(str, arr, n, i, parts, t, c) {
|
|
185
187
|
c = 0; str = tolower(str); gsub(/[^a-z0-9]+/, " ", str)
|
|
186
188
|
n = split(str, parts, " ")
|
|
@@ -202,18 +204,23 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
|
|
|
202
204
|
}
|
|
203
205
|
close(dmap)
|
|
204
206
|
}
|
|
207
|
+
# Generic words count toward the token set but never as topical similarity (#3719).
|
|
208
|
+
ns = split("the and with for from before after write read when that this into not are but can has have via use using does should may all any only also than then over under without within while where which what its it was were been being each both one two", sw, " ")
|
|
209
|
+
for (i = 1; i <= ns; i++) stop[sw[i]] = 1
|
|
205
210
|
ntitle = toks(title, ft)
|
|
206
211
|
}
|
|
207
212
|
{
|
|
208
213
|
pf = $1; sub(/^\.\//, "", pf)
|
|
209
214
|
if (pf != file) next
|
|
210
215
|
if ($5 == "filed-closed") next
|
|
216
|
+
if ($5 == "unknown" && sev == "MEDIUM" && (conf == "CONFIRMED" || conf == "LIKELY")) next
|
|
211
217
|
hit = 0
|
|
212
|
-
delete pt; toks($4, pt); shared = 0
|
|
213
|
-
for (t in pt) if (t in ft) shared++
|
|
218
|
+
delete pt; toks($4, pt); shared = 0; cshared = 0
|
|
219
|
+
for (t in pt) if (t in ft) { shared++; if (!(t in stop)) cshared++ }
|
|
214
220
|
# Line overlap alone is not a match (an unrelated finding can land on the same line): it also
|
|
215
|
-
# needs at least one shared title token. Title-only matching
|
|
216
|
-
|
|
221
|
+
# needs at least one shared non-stopword title token. Title-only matching needs >= 3 shared
|
|
222
|
+
# tokens, of which >= 2 are non-stopword (generic words never count as topical similarity).
|
|
223
|
+
if (cshared >= 1 && ls != "" && $2 ~ /^[0-9]+$/) {
|
|
217
224
|
ps = $2 + 0; pe = ($3 ~ /^[0-9]+$/) ? $3 + 0 : ps
|
|
218
225
|
if (pe < ps) pe = ps
|
|
219
226
|
d = shift(pf, ps, pe); ps += d; pe += d
|
|
@@ -222,7 +229,7 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
|
|
|
222
229
|
uni = (pe - ps + 1) + (le - ls + 1) - inter
|
|
223
230
|
if (uni > 0 && inter / uni >= 0.6) hit = 1
|
|
224
231
|
}
|
|
225
|
-
if (!hit && ntitle >= 3 && shared >= 3) hit = 1
|
|
232
|
+
if (!hit && ntitle >= 3 && shared >= 3 && cshared >= 2) hit = 1
|
|
226
233
|
if (!hit) next
|
|
227
234
|
if ($5 == "filed" && $6 ~ /^#?[0-9]+$/) { n = $6; sub(/^#/, "", n); print "NOTE reflag-filed #" n; found = 1; exit }
|
|
228
235
|
if (!note) note = "NOTE reflag-note"
|
|
@@ -166,6 +166,7 @@ DMAP=$(mktemp "${TMPDIR:-/tmp}/classify-finding-dmap.XXXXXX")
|
|
|
166
166
|
printf 'src/api.py\t10\t20\tmissing retry on timeout handler\tfiled\t123\n'
|
|
167
167
|
printf 'src/closed.py\t5\t15\tstale cache entry never evicted\tfiled-closed\t77\n'
|
|
168
168
|
printf 'src/notes.py\t\t\tduplicated predicate counted twice\tnote\t\n'
|
|
169
|
+
printf 'src/unlinked.py\t30\t40\tunvalidated redirect target in login handler\tunknown\t\n'
|
|
169
170
|
} > "$PRIOR"
|
|
170
171
|
PF=(--prior-findings "$PRIOR")
|
|
171
172
|
M=(--severity MEDIUM --confidence CONFIRMED)
|
|
@@ -182,9 +183,17 @@ expect_out "NOTE reflag-note" "NOTE row with empty line columns matches by title
|
|
|
182
183
|
expect ISSUE "same line, different topic MEDIUM CONFIRMED is not swallowed (SEC-1)" -- "${M[@]}" --agent security "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "SQL injection via unsanitized token"
|
|
183
184
|
expect ISSUE "same line with no title at all is not swallowed" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50
|
|
184
185
|
expect_out "NOTE reflag-note" "same line, same topic (one shared token) still NOTE" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "Lock handling is racy"
|
|
186
|
+
expect ISSUE "same line, only a stopword-class word shared is not swallowed (#3719)" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "Path traversal in the write target"
|
|
187
|
+
expect ISSUE "far line, only generic words shared is not swallowed (#3719)" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 300 --title "Command injection before write with the lock"
|
|
185
188
|
expect ISSUE "non-overlapping same-file finding is classified normally" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 200 --line-end 210 --title "missing bounds check"
|
|
186
189
|
expect ISSUE "two shared tokens are not enough" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 200 --title "lock released late"
|
|
187
190
|
expect ISSUE "same title on a different file is classified normally" -- "${M[@]}" "${PF[@]}" --file src/other.sh --line-start 45 --title "lock released before write completes"
|
|
191
|
+
expect ISSUE "unknown row does not demote MEDIUM CONFIRMED (unlinked prior)" -- "${M[@]}" "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
192
|
+
expect ISSUE "unknown row does not demote MEDIUM CONFIRMED by line IoU" -- "${M[@]}" "${PF[@]}" --file src/unlinked.py --line-start 32 --line-end 40 --title "redirect target unchecked"
|
|
193
|
+
expect ISSUE "unknown row does not demote MEDIUM LIKELY" -- --severity MEDIUM --confidence LIKELY "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
194
|
+
expect_out "NOTE reflag-note" "unknown row still demotes LOW" -- --severity LOW --confidence CONFIRMED "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
195
|
+
expect_out "NOTE reflag-note" "unknown row still demotes MEDIUM POSSIBLE" -- --severity MEDIUM --confidence POSSIBLE "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
196
|
+
expect ISSUE "HIGH unaffected by unknown row" -- --severity HIGH --confidence CONFIRMED "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
|
|
188
197
|
expect_out "NOTE reflag-filed #123" "re-flag of an already-filed issue links it" -- "${M[@]}" "${PF[@]}" --file src/api.py --line-start 12 --line-end 18 --title "retry x"
|
|
189
198
|
expect_out "NOTE reflag-filed #123" "filed re-flag by title tokens" -- "${M[@]}" "${PF[@]}" --file src/api.py --line-start 99 --title "Missing retry on timeout"
|
|
190
199
|
expect ISSUE "filed-closed row is ignored (regression not swallowed)" -- "${M[@]}" "${PF[@]}" --file src/closed.py --line-start 5 --line-end 15 --title "stale cache entry never evicted"
|
|
@@ -13,7 +13,7 @@ ok() { PASS=$((PASS+1)); }
|
|
|
13
13
|
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
14
|
expect() { [ "$2" = "$3" ] && ok || bad "$1 (got '$3' want '$2')"; }
|
|
15
15
|
|
|
16
|
-
SITES="commands/work-on.md commands/work-on/build/validate.md commands/quality-gate.md commands/work-on/review.md commands/work-on/investigate.md commands/work-on/decompose.md commands/work-on/build.md commands/work-on/close.md commands/review-pr.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
|
|
16
|
+
SITES="commands/work-on.md commands/work-on/build/validate.md commands/quality-gate.md commands/work-on/review.md commands/work-on/investigate.md commands/work-on/decompose.md commands/work-on/build.md commands/work-on/close.md commands/review-pr.md commands/work-on/remediate.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
|
|
17
17
|
|
|
18
18
|
# extract the bootstrap block (comment line .. closing top-level fi), indentation stripped
|
|
19
19
|
# Block ends at the first line (after the marker) that starts with `fi` at the marker's own indentation.
|
|
@@ -111,6 +111,23 @@ else
|
|
|
111
111
|
echo "SKIP: symlink cases (this platform cannot create symlinks)"; SKIPPED=$((SKIPPED+1))
|
|
112
112
|
fi
|
|
113
113
|
|
|
114
|
+
# forge#3758 (hgate is a fixture of its own: the loop runs once per shell and h14 is reused below with other versions): deploy-gate script resolves on a plugin-only install (cache only, no symlink, no FORGE_* env).
|
|
115
|
+
# The gate-script line is taken verbatim from the spec so the test exercises exactly what ships.
|
|
116
|
+
mkscripts "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0"; : > "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh"
|
|
117
|
+
grep -hE '^[[:space:]]*GATE_SCRIPT=""; \[ -n "\$FORGE_ROOT" \]' "$ROOT/commands/work-on/remediate.md" "$ROOT/commands/review-pr.md" | sed 's/^[[:space:]]*//' | sort -u > "$T/gs"
|
|
118
|
+
expect "gate-script resolver line is identical across sites" 1 "$(wc -l < "$T/gs" | tr -d ' ')"
|
|
119
|
+
gate_run() { ( cd "$T/consumer" && env -i PATH="$PATH" HOME="$1" "$SH" -c "$(cat "$T/canon"); $(cat "$T/gs"); printf %s \"\$GATE_SCRIPT\"" ); }
|
|
120
|
+
expect "plugin-only install resolves resolve-deploy-gate.sh" "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh" "$(gate_run "$T/hgate")"
|
|
121
|
+
expect "unresolved gate script stays empty (strict)" "" "$(gate_run "$T/empty")"
|
|
122
|
+
# cwd/consumer repo is never a candidate even when it carries the gate script
|
|
123
|
+
: > "$T/consumer/scripts/resolve-deploy-gate.sh"
|
|
124
|
+
expect "consumer repo gate script never used" "" "$(gate_run "$T/empty")"
|
|
125
|
+
rm -f "$T/consumer/scripts/resolve-deploy-gate.sh"
|
|
126
|
+
# no spec iterates CLAUDE_PLUGIN_ROOT as an env var for the gate script
|
|
127
|
+
for f in commands/work-on/remediate.md commands/review-pr.md; do
|
|
128
|
+
grep -qF '"${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}"' "$ROOT/$f" && bad "env-style CLAUDE_PLUGIN_ROOT gate loop in $f" || ok
|
|
129
|
+
done
|
|
130
|
+
|
|
114
131
|
# newest cached version wins (1.10.0 must beat 1.9.0)
|
|
115
132
|
for v in 1.9.0 1.10.0 1.2.0; do mkscripts "$T/h3/.claude/plugins/cache/forgedock/forgedock/$v"; done
|
|
116
133
|
expect "newest cached version wins" "$T/h3/.claude/plugins/cache/forgedock/forgedock/1.10.0" "$(run "$T/h3")"
|
|
@@ -325,6 +342,8 @@ hr_check_block() { # label file var script
|
|
|
325
342
|
hr_check_block review-pr "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
|
|
326
343
|
hr_check_block review-pr "$HR_CLASSIFY" PRIORITY_SCRIPT severity-to-priority.sh
|
|
327
344
|
hr_check_block review-pr "$HR_CLASSIFY" CODE_INDEX_SCRIPT code-index.sh
|
|
345
|
+
hr_check_block review-pr "$HR_CLASSIFY" DEDUP_SCRIPT issue-dedup.sh
|
|
346
|
+
hr_check_block issue "$ROOT/commands/issue.md" DEDUP_SCRIPT issue-dedup.sh
|
|
328
347
|
hr_check_block review-pr-staging "$HR_STG" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
|
|
329
348
|
hr_check_block review-pr-staging "$HR_STG" PRIORITY_SCRIPT severity-to-priority.sh
|
|
330
349
|
extract_helper_block "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm.sh"
|
|
@@ -334,6 +353,9 @@ extract_helper_block "$HR_STG" PRIORITY_SCRIPT > "$T/hr_pr_stg.sh"
|
|
|
334
353
|
# only the variable-name-specific text may differ, and review-pr/review-pr-staging must agree
|
|
335
354
|
cmp -s "$T/hr_dm.sh" "$T/hr_dm_stg.sh" && ok || bad 'DERIVE_MILESTONE_SCRIPT resolver differs between review-pr and review-pr-staging'
|
|
336
355
|
cmp -s "$T/hr_pr.sh" "$T/hr_pr_stg.sh" && ok || bad 'PRIORITY_SCRIPT resolver differs between review-pr and review-pr-staging'
|
|
356
|
+
extract_helper_block "$HR_CLASSIFY" DEDUP_SCRIPT > "$T/hr_dd.sh"
|
|
357
|
+
extract_helper_block "$ROOT/commands/issue.md" DEDUP_SCRIPT > "$T/hr_dd_issue.sh"
|
|
358
|
+
cmp -s "$T/hr_dd.sh" "$T/hr_dd_issue.sh" && ok || bad 'DEDUP_SCRIPT resolver differs between review-pr and issue'
|
|
337
359
|
# no bare cwd-relative helper call in either review spec
|
|
338
360
|
if grep -nE 'bash scripts/(derive-finding-milestone|severity-to-priority)\.sh' "$HR_CLASSIFY" "$HR_STG" >/dev/null 2>&1; then bad 'review spec still calls a helper by a bare scripts/ path'; else ok; fi
|
|
339
361
|
|
|
@@ -342,10 +364,25 @@ if grep -nE 'bash scripts/(derive-finding-milestone|severity-to-priority)\.sh' "
|
|
|
342
364
|
# design-bench.md, design-render-critique-loop.md, pipeline-health/phase-2-metrics.md, phase-3-dependency.md:
|
|
343
365
|
# node scripts/*.mjs run inside the ForgeDock repo itself or printed as user hints (not run under a PR checkout cwd).
|
|
344
366
|
# work-on/investigate.md, work-on/build/architect.md: {REPO_PATH}/scripts/code-index.sh, deferred to #3670.
|
|
367
|
+
# orchestrate/phase-3-dependency.md: scripts/worktree-lifecycle.sh lines sit in a roadmap/proposal fence, not executed.
|
|
345
368
|
# deploy-info.md: read-only grep of decrypt-secrets.sh, never executed.
|
|
346
|
-
SCRIPTS_ALLOW='commands/(design-bench|design-render-critique-loop|deploy-info|pipeline-health/phase-2-metrics|pipeline-health/phase-3-dependency|work-on/investigate|work-on/build/architect)\.md'
|
|
347
|
-
|
|
348
|
-
|
|
369
|
+
SCRIPTS_ALLOW='commands/(design-bench|design-render-critique-loop|deploy-info|pipeline-health/phase-2-metrics|pipeline-health/phase-3-dependency|orchestrate/phase-3-dependency|work-on/investigate|work-on/build/architect)\.md'
|
|
370
|
+
# Executed forms: a bash/sh/source word, a command substitution `$(scripts/x.sh`, or a line-start/after-;&&| direct `scripts/x.sh` / `./scripts/x.sh`.
|
|
371
|
+
GUARD_RE='(^|[^A-Za-z0-9_-])(bash|sh|source) +(\./)?scripts/[A-Za-z0-9_.-]+|REPO_PATH\}?/scripts/|\$\( *(\./)?scripts/[A-Za-z0-9_.-]+\.sh|(^|:[0-9]+:|[;&(] *)[[:space:]]*(\./)?scripts/[A-Za-z0-9_.-]+\.sh'
|
|
372
|
+
guard_scan() { # dir -> offending file:line:text rows (comment lines are prose)
|
|
373
|
+
grep -rnE "$GUARD_RE" "$1" 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' || true
|
|
374
|
+
}
|
|
375
|
+
guard_hits=$(guard_scan "$ROOT/commands" | sed "s#^$ROOT/##" | grep -vE "^($SCRIPTS_ALLOW):" || true)
|
|
376
|
+
# Negative fixtures (#3679): a bare direct call must be flagged; backticked prose and comments must not.
|
|
377
|
+
mkdir -p "$T/guardfx/bad1" "$T/guardfx/bad2" "$T/guardfx/bad3" "$T/guardfx/good"
|
|
378
|
+
printf 'X=$(scripts/foo.sh a b)\n' > "$T/guardfx/bad1/x.md"
|
|
379
|
+
printf 'scripts/foo.sh arg\n' > "$T/guardfx/bad2/x.md"
|
|
380
|
+
printf 'ok && ./scripts/foo.sh\n' > "$T/guardfx/bad3/x.md"
|
|
381
|
+
printf 'See `scripts/foo.sh` for details, forwarded to scripts/foo.sh --exclude.\n# $(scripts/foo.sh) in a comment\n' > "$T/guardfx/good/x.md"
|
|
382
|
+
for f in bad1 bad2 bad3; do
|
|
383
|
+
[ -n "$(guard_scan "$T/guardfx/$f")" ] && ok || bad "widened guard missed direct execution fixture $f"
|
|
384
|
+
done
|
|
385
|
+
[ -z "$(guard_scan "$T/guardfx/good")" ] && ok || bad 'widened guard flags prose/comment fixture'
|
|
349
386
|
if [ -n "$guard_hits" ]; then bad "commands/ has a cwd-relative or REPO_PATH helper call outside the allowlist: $(printf '%s' "$guard_hits" | head -5 | tr '\n' '|')"; else ok; fi
|
|
350
387
|
|
|
351
388
|
# Behavior (#3596): planted cwd copies of the two review helpers must never be selected.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# remediate-approved-head.test.sh — the M7 CI-gated merge is bound to APPROVED_HEAD on every path (forge#3686)
|
|
3
|
+
# Extracts the guard from commands/work-on/remediate.md and exercises it. No network. Usage: bash scripts/remediate-approved-head.test.sh
|
|
4
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
10
|
+
SPEC="$ROOT/commands/work-on/remediate.md"
|
|
11
|
+
|
|
12
|
+
PASS=0; FAILN=0
|
|
13
|
+
ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
14
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
15
|
+
eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
16
|
+
|
|
17
|
+
grep -qE 'GATED_HEAD" != "\$\{?APPROVED_HEAD' "$SPEC" && ok "spec compares GATED_HEAD to APPROVED_HEAD" || bad "spec compares GATED_HEAD to APPROVED_HEAD"
|
|
18
|
+
grep -qF 'if [ "${RESUME_AT_M7:-false}" = "true" ] && [ "$CI_GATE_RC" -eq 0 ]' "$SPEC" && bad "guard is no longer resume-only" || ok "guard is no longer resume-only"
|
|
19
|
+
|
|
20
|
+
# Extract the guard fence body (from the scratch reload through the closing fi) and run it per case.
|
|
21
|
+
GUARD=$(awk '/^_SD="\$\{FORGE_SCRATCHPAD/ && !g {g=1} g {print} g && /^fi$/ {exit}' "$SPEC")
|
|
22
|
+
[ -n "$GUARD" ] && ok "guard block extracted" || bad "guard block extracted"
|
|
23
|
+
|
|
24
|
+
# run_guard <persisted head or NONE> <GATED_HEAD> <CI_GATE_RC> <RESUME_AT_M7>
|
|
25
|
+
run_guard() {
|
|
26
|
+
local d; d=$(mktemp -d)
|
|
27
|
+
[ "$1" = NONE ] || printf '%s\n' "$1" > "$d/remediate_approved_head_7"
|
|
28
|
+
FORGE_SCRATCHPAD="$d" GATED_HEAD="$2" CI_GATE_RC="$3" RESUME_AT_M7="$4" CI_GATE_OUT="" \
|
|
29
|
+
bash -c "${GUARD//\{PR_NUMBER\}/7}"$'\nprintf %s "$CI_GATE_RC"'
|
|
30
|
+
rm -rf "$d"
|
|
31
|
+
}
|
|
32
|
+
A=$(printf 'a%.0s' {1..40}); B=$(printf 'b%.0s' {1..40})
|
|
33
|
+
for r in true false; do
|
|
34
|
+
eq "match merges (resume=$r)" "$(run_guard "$A" "$A" 0 $r)" 0
|
|
35
|
+
eq "mismatch refuses (resume=$r)" "$(run_guard "$A" "$B" 0 $r)" 4
|
|
36
|
+
eq "missing approved head refuses (resume=$r)" "$(run_guard NONE "$A" 0 $r)" 4
|
|
37
|
+
eq "malformed approved head refuses (resume=$r)" "$(run_guard "zz" "$A" 0 $r)" 4
|
|
38
|
+
eq "empty gated head refuses (resume=$r)" "$(run_guard "$A" "" 0 $r)" 4
|
|
39
|
+
eq "nonzero rc is unchanged (resume=$r)" "$(run_guard "$A" "$B" 3 $r)" 3
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
echo "passed=$PASS failed=$FAILN"
|
|
43
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -28,6 +28,8 @@ INTERIM=$'<!-- FORGE:REMEDIATION -->\n## Remediation (in progress)'
|
|
|
28
28
|
QUOTED_REVIEW=$'## Review\nThe prior run posted FORGE:REMEDIATION:COMPLETE on this PR.\n<!-- FORGE:REMEDIATION:COMPLETE -->'
|
|
29
29
|
QUOTED_WORK_ORDER=$'<!-- FORGE:INPR_FIX: head=abc1234 -->\nquoting <!-- FORGE:REMEDIATION -->\n<!-- FORGE:REMEDIATION:COMPLETE -->'
|
|
30
30
|
|
|
31
|
+
HELD=$'<!-- FORGE:REMEDIATION_HELD -->\n**Re-gate outcome**: HELD-AWAITING-MERGE'
|
|
32
|
+
eq "held-resume record matches neither COMPLETE nor TRAIL (forge#3685)" "$(complete_n "[$(c NONE Bot b "$HELD")]")/$(trail_n "[$(c NONE Bot b "$HELD")]")" "0/0"
|
|
31
33
|
eq "genuine M8 trail by bot matches COMPLETE" "$(complete_n "[$(c NONE Bot 'app[bot]' "$REAL")]")" 1
|
|
32
34
|
eq "review comment quoting COMPLETE mid-body does not match" "$(complete_n "[$(c OWNER User u "$QUOTED_REVIEW")]")" 0
|
|
33
35
|
eq "INPR_FIX work order quoting the markers does not match COMPLETE" "$(complete_n "[$(c NONE Bot b "$QUOTED_WORK_ORDER")]")" 0
|
|
@@ -47,5 +49,9 @@ for f in commands/work-on/remediate.md commands/work-on.md commands/orchestrate/
|
|
|
47
49
|
done
|
|
48
50
|
grep -qF "count '$COMPLETE_RE'" "$ROOT/commands/work-on/remediate.md" && ok "remediate.md uses the tested COMPLETE regex" || bad "remediate.md COMPLETE regex drifted from this test"
|
|
49
51
|
|
|
52
|
+
grep -qF 'FORGE:REMEDIATION_HELD' "$ROOT/commands/work-on/remediate.md" && ok "remediate.md documents FORGE:REMEDIATION_HELD" || bad "remediate.md lacks FORGE:REMEDIATION_HELD"
|
|
53
|
+
! grep -qE 'PR_OPEN_STATE = .?MERGED.? → EXIT .REMEDIATE_RESULT: status: ALREADY_DONE. \(nothing to remediate' "$ROOT/commands/work-on/remediate.md" && ok "M0 has no unconditional merged ALREADY_DONE exit" || bad "M0 still exits ALREADY_DONE unconditionally on MERGED"
|
|
54
|
+
grep -qiE 'merged.*(skip|exclusive|do not fall through)' "$ROOT/commands/work-on/remediate.md" && ok "M7 merged-first branch is exclusive" || bad "M7 merged-first exclusivity missing"
|
|
55
|
+
|
|
50
56
|
echo "passed=$PASS failed=$FAILN"
|
|
51
57
|
[ "$FAILN" -eq 0 ]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# resolve-deploy-gate.sh — trusted deploy-gate resolver (forge#3753)
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Usage: resolve-deploy-gate.sh <base> <owner/repo>
|
|
7
|
+
#
|
|
8
|
+
# Prints exactly one line: `true` (deploy gate, strict human bar) or `false`
|
|
9
|
+
# (fast lane). Always exits 0; every failure path prints `true` (fail closed).
|
|
10
|
+
#
|
|
11
|
+
# A base is the deploy gate when it is empty/`null`, the literal `main` (hard
|
|
12
|
+
# floor), the repository's GitHub default branch (`default_branch`), or the
|
|
13
|
+
# `branches.default` of the default branch's COMMITTED forge.yaml
|
|
14
|
+
# (`origin/<default>:forge.yaml`, falling back to the contents API) or of an
|
|
15
|
+
# explicit trusted config (FORGE_TRUSTED_CONFIG, orchestrator-supplied).
|
|
16
|
+
#
|
|
17
|
+
# It NEVER reads forge.yaml from $PWD: the PR worktree is author-controlled, so
|
|
18
|
+
# a PR could otherwise redefine which branch counts as the deploy gate.
|
|
19
|
+
# Config sources can only ADD gate branches; main and the GitHub default are
|
|
20
|
+
# unconditional, so no source relaxes the bar below the literal-main floor.
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
BASE="${1:-}"
|
|
24
|
+
REPO="${2:-}"
|
|
25
|
+
|
|
26
|
+
emit() { printf '%s\n' "$1"; exit 0; }
|
|
27
|
+
|
|
28
|
+
{ [ -z "$BASE" ] || [ "$BASE" = "null" ] || [ "$BASE" = "main" ]; } && emit true
|
|
29
|
+
[ -n "$REPO" ] || emit true
|
|
30
|
+
|
|
31
|
+
GH_DEFAULT=$(gh api "repos/${REPO}" --jq .default_branch 2>/dev/null || echo "")
|
|
32
|
+
{ [ -z "$GH_DEFAULT" ] || [ "$GH_DEFAULT" = "null" ]; } && emit true
|
|
33
|
+
[ "$BASE" = "$GH_DEFAULT" ] && emit true
|
|
34
|
+
|
|
35
|
+
_cfg_default() { yq '.branches.default // ""' 2>/dev/null || true; }
|
|
36
|
+
|
|
37
|
+
CFG_DEFAULT=""
|
|
38
|
+
# Committed default-branch config only (a ref, never the working tree).
|
|
39
|
+
CFG_DEFAULT=$(git show "origin/${GH_DEFAULT}:forge.yaml" 2>/dev/null | _cfg_default)
|
|
40
|
+
if [ -z "$CFG_DEFAULT" ]; then
|
|
41
|
+
CFG_DEFAULT=$(gh api "repos/${REPO}/contents/forge.yaml?ref=${GH_DEFAULT}" --jq .content 2>/dev/null \
|
|
42
|
+
| base64 -d 2>/dev/null | _cfg_default)
|
|
43
|
+
fi
|
|
44
|
+
[ -n "$CFG_DEFAULT" ] && [ "$CFG_DEFAULT" != "null" ] && [ "$BASE" = "$CFG_DEFAULT" ] && emit true
|
|
45
|
+
|
|
46
|
+
if [ -n "${FORGE_TRUSTED_CONFIG:-}" ] && [ -f "$FORGE_TRUSTED_CONFIG" ]; then
|
|
47
|
+
TRUSTED=$(_cfg_default < "$FORGE_TRUSTED_CONFIG")
|
|
48
|
+
[ -n "$TRUSTED" ] && [ "$TRUSTED" != "null" ] && [ "$BASE" = "$TRUSTED" ] && emit true
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
emit false
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# resolve-deploy-gate.test.sh — cases for scripts/resolve-deploy-gate.sh (forge#3753).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resolve-deploy-gate.sh"
|
|
7
|
+
PASS=0; FAILN=0
|
|
8
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
9
|
+
ok() { PASS=$((PASS+1)); }
|
|
10
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
11
|
+
|
|
12
|
+
command -v yq >/dev/null 2>&1 || { echo "SKIP: yq not installed"; exit 0; }
|
|
13
|
+
|
|
14
|
+
# Stub gh: GH_DEFAULT_STUB drives `repos/<r> --jq .default_branch`; contents API unused (fails).
|
|
15
|
+
mkdir -p "$TMP/bin"
|
|
16
|
+
cat > "$TMP/bin/gh" <<'EOF'
|
|
17
|
+
#!/usr/bin/env bash
|
|
18
|
+
case "$*" in
|
|
19
|
+
*"--jq .default_branch"*) [ -n "${GH_FAIL:-}" ] && exit 1; printf '%s\n' "${GH_DEFAULT_STUB:-}" ;;
|
|
20
|
+
*) exit 1 ;;
|
|
21
|
+
esac
|
|
22
|
+
EOF
|
|
23
|
+
chmod +x "$TMP/bin/gh"
|
|
24
|
+
|
|
25
|
+
# Repo fixture: origin/<default> committed forge.yaml, plus a PR worktree dir with its own forge.yaml.
|
|
26
|
+
ORIGIN="$TMP/origin.git"; REPO="$TMP/repo"
|
|
27
|
+
git init -q --bare "$ORIGIN"
|
|
28
|
+
git init -q "$REPO"; git -C "$REPO" config user.email t@t; git -C "$REPO" config user.name t
|
|
29
|
+
printf 'branches:\n default: develop\n' > "$REPO/forge.yaml"
|
|
30
|
+
git -C "$REPO" add forge.yaml; git -C "$REPO" commit -qm init
|
|
31
|
+
git -C "$REPO" branch -M trunk
|
|
32
|
+
git -C "$REPO" remote add origin "$ORIGIN"; git -C "$REPO" push -q origin trunk 2>/dev/null
|
|
33
|
+
git -C "$REPO" fetch -q origin
|
|
34
|
+
# PR worktree content: attacker redefines the deploy gate.
|
|
35
|
+
printf 'branches:\n default: release\n' > "$REPO/forge.yaml"
|
|
36
|
+
|
|
37
|
+
run() { # base default_stub [env...]
|
|
38
|
+
local base="$1" def="$2"; shift 2
|
|
39
|
+
(cd "$REPO" && env PATH="$TMP/bin:$PATH" GH_DEFAULT_STUB="$def" "$@" bash "$S" "$base" owner/repo)
|
|
40
|
+
}
|
|
41
|
+
expect() { [ "$1" = "$2" ] && ok || bad "$3: got '$1' want '$2'"; }
|
|
42
|
+
|
|
43
|
+
expect "$(run main trunk)" true "main with worktree release"
|
|
44
|
+
expect "$(run release trunk)" false "worktree-declared branch is not a gate"
|
|
45
|
+
expect "$(run staging trunk)" false "staging is fast lane"
|
|
46
|
+
expect "$(run trunk trunk)" true "github default"
|
|
47
|
+
expect "$(run master master)" true "github default master"
|
|
48
|
+
expect "$(run develop trunk)" true "committed origin config default"
|
|
49
|
+
expect "$(run '' trunk)" true "empty base"
|
|
50
|
+
expect "$(run null trunk)" true "null base"
|
|
51
|
+
expect "$(run staging '')" true "empty gh default fails closed"
|
|
52
|
+
expect "$(run staging trunk GH_FAIL=1)" true "gh failure fails closed"
|
|
53
|
+
expect "$(run main '' GH_FAIL=1)" true "main on gh failure"
|
|
54
|
+
printf 'branches:\n default: prod\n' > "$TMP/trusted.yaml"
|
|
55
|
+
expect "$(run prod trunk FORGE_TRUSTED_CONFIG="$TMP/trusted.yaml")" true "trusted config default"
|
|
56
|
+
expect "$(run prod trunk)" false "prod without trusted config"
|
|
57
|
+
|
|
58
|
+
echo "resolve-deploy-gate.test.sh: $PASS passed, $FAILN failed"
|
|
59
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -120,8 +120,16 @@ expect "round 2 narrows to security by default" "security" "$(roster 2 'security
|
|
|
120
120
|
expect "round 2 adds a path-triggered domain" "security auth" "$(roster 2 'security auth billing' $'README.md\nsrc/auth/login.py')"
|
|
121
121
|
expect "round 3 cap holds" "security" "$(roster 3 'security auth billing database' 'docs/notes.txt')"
|
|
122
122
|
expect "claim/lease paths trigger concurrency" "security concurrency" "$(roster 2 'security' 'engine/claim-lease.txt')"
|
|
123
|
+
export REPO_PATH="$ROOT" # ForgeDock itself: the persona file exists
|
|
123
124
|
expect "commands/** triggers spec-cli" "security spec-cli" "$(roster 2 'security' 'commands/foo.md')"
|
|
124
125
|
expect "bin/** triggers spec-cli" "security spec-cli" "$(roster 2 'security' 'bin/foo.mjs')"
|
|
126
|
+
# consumer repo layout: no persona file, so no spec-cli
|
|
127
|
+
mkdir -p "$TMP/consumer/bin" "$TMP/consumer/commands"
|
|
128
|
+
export REPO_PATH="$TMP/consumer"
|
|
129
|
+
expect "consumer bin/ gets no spec-cli" "security" "$(roster 2 'security' 'bin/foo.mjs')"
|
|
130
|
+
expect "consumer commands/ gets no spec-cli" "security" "$(roster 2 'security' 'commands/foo.md')"
|
|
131
|
+
expect "consumer with another trigger keeps it, no spec-cli" "security auth" "$(roster 2 'security' $'bin/foo.mjs\nsrc/auth/login.py')"
|
|
132
|
+
unset REPO_PATH
|
|
125
133
|
expect "non-numeric rounds fail to the 3B roster" "security auth" "$(roster 'x' 'security auth' 'README.md')"
|
|
126
134
|
|
|
127
135
|
# ---- 3. mode resolution through the real fence ----------------------------------------------------------------------
|
|
@@ -83,5 +83,15 @@ done
|
|
|
83
83
|
# Test 6: the protocol defines the marker domain through the mapping
|
|
84
84
|
if grep -qF 'marker domain' "$PERSONAS/protocols.md"; then ok; else bad "protocols.md does not define the marker domain"; fi
|
|
85
85
|
|
|
86
|
+
# Test 7: the roster regexes in delta_roster must equal the DOMAIN_FILES_* regexes, and every domain needs an empty-slice fallback
|
|
87
|
+
for pair in AUTH:auth BILLING:billing CONCURRENCY:concurrency DATABASE:database INFRA:infra SCRAPING:scraper FRONTEND:frontend API:api; do
|
|
88
|
+
var=${pair%%:*}; dom=${pair#*:}
|
|
89
|
+
a=$(grep -E "^DOMAIN_FILES_$var=" "$SPEC" | head -n 1 | sed -n 's/.*grep -iE "\([^"]*\)".*/\1/p')
|
|
90
|
+
b=$(grep -F "&& out=\"\$out $dom\"" "$SPEC" | grep 'grep -qiE' | head -n 1 | sed -n 's/.*grep -qiE "\([^"]*\)".*/\1/p')
|
|
91
|
+
if [ -z "$a" ] || [ -z "$b" ]; then bad "could not extract $var pattern (DOMAIN_FILES='$a' delta_roster='$b')"
|
|
92
|
+
elif [ "$a" = "$b" ]; then ok; else bad "$var regex drifted: DOMAIN_FILES='$a' delta_roster='$b'"; fi
|
|
93
|
+
if grep -qF "[ -z \"\$DOMAIN_FILES_$var\" ] && DOMAIN_FILES_$var=\"\$FILES\"" "$SPEC"; then ok; else bad "no empty-slice fallback for DOMAIN_FILES_$var"; fi
|
|
94
|
+
done
|
|
95
|
+
|
|
86
96
|
echo "review-pr-domain-map: $pass passed, $fail failed"
|
|
87
97
|
[ "$fail" -eq 0 ]
|