forgedock 1.29.0 → 1.29.1
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-cli.mjs +12 -6
- package/bin/lean/context.mjs +27 -4
- package/bin/lean/context.test.mjs +40 -12
- package/bin/lean/lean.mjs +23 -18
- package/bin/lean/lean.test.mjs +31 -3
- package/bin/lean/orchestrate.mjs +41 -11
- package/bin/lean/prefilter.mjs +79 -14
- package/bin/lean/prefilter.test.mjs +88 -2
- package/bin/lean/recovery.test.mjs +196 -7
- package/bin/lean/render.mjs +13 -0
- package/bin/lean/testmap.mjs +17 -0
- package/bin/lean/testmap.test.mjs +16 -1
- package/commands/deploy-pr.md +5 -4
- package/commands/orchestrate.md +4 -3
- package/commands/review-pr-staging.md +6 -1
- package/docs/OUTCOME-CODES.md +1 -1
- package/package.json +1 -1
- package/scripts/code-index.sh +33 -33
- package/scripts/code-index.test.sh +21 -2
- package/scripts/fixtures/test-map/go/.github/workflows/ci.yml +2 -0
- package/scripts/repo-facts.test.sh +5 -5
- package/scripts/review-prepare.sh +28 -6
- package/scripts/review-prepare.test.sh +22 -2
- package/scripts/test-map.sh +139 -33
- package/scripts/test-map.test.sh +195 -7
- package/scripts/verify-phase-trail.test.sh +44 -44
- package/scripts/wait-ci-green.sh +1 -1
- package/scripts/wait-ci-green.test.sh +4 -0
package/scripts/test-map.sh
CHANGED
|
@@ -17,11 +17,16 @@
|
|
|
17
17
|
# `ruff check .` (through `uv run` / `poetry run` when that lockfile exists), `**/*.py`
|
|
18
18
|
# go.mod (dir D) → `go test ./...`, `go vet ./...` in D, `D/**/*.go`, `D/go.mod`, `D/go.sum`
|
|
19
19
|
# Makefile / GNUmakefile targets test/lint/check/typecheck/vet → `make <target>`, `**`
|
|
20
|
-
# .github/workflows/*.yml|*.yaml `run:` steps (one command per line of a `|` block, a `>` block is one command
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
20
|
+
# .github/workflows/*.yml|*.yaml `run:` steps (one command per line of a `|` block, a `>` block is one command).
|
|
21
|
+
# The engine runs these commands before a PR, so a step is mapped only when its whole
|
|
22
|
+
# command is an allowlisted check (forge#4300): `<pm> test`, `<pm> run <package.json check script>` (also `test:*`
|
|
23
|
+
# style names, `pnpm -r`, `npm --prefix DIR`), `make <check target>`, `go test|vet|build`, `cargo test|clippy|check`,
|
|
24
|
+
# gradle/maven with check tasks/goals only, `[bundle exec] rspec`, `python manage.py test`, `python -m unittest`,
|
|
25
|
+
# `tox`/`nox` (not a publish/release/deploy/upload env), `deno|bun test`, `bash|sh <path>.test.sh` or a known
|
|
26
|
+
# test/lint/typecheck tool (forge#4315); everything else is dropped, and job and step names are never consulted. One leading `cd DIR &&` / `pushd DIR &&` becomes the entry cwd; any other chaining,
|
|
27
|
+
# substitution, `${{ }}`, `$GITHUB_*`, redirect or `<(...)`/`>(...)`, a non-check make target, or a fixer form
|
|
28
|
+
# (`--fix`, `--write`, `-w`, `black`/`ruff format`/`gofmt`/`prettier` without their check flag) drops the step (forge#4316). Globs from the tool (python/go/shell/js, under the cwd), else the
|
|
29
|
+
# workflow's `paths:` filter, else `DIR/**` for a cwd; a workflow entry never gets a bare `**`.
|
|
25
30
|
# Exit: 0 ok | 2 usage or bad repo path. bash 3.2 portable.
|
|
26
31
|
set -uo pipefail
|
|
27
32
|
|
|
@@ -53,6 +58,38 @@ fi
|
|
|
53
58
|
US="$(printf '\037')"
|
|
54
59
|
row() { printf '%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" >> "$T/rows"; }
|
|
55
60
|
JS_EXT="js jsx ts tsx mjs cjs"
|
|
61
|
+
# check recognisers shared by package.json scripts / Makefile targets and the workflow allowlist (forge#4300)
|
|
62
|
+
PKG_SCRIPTS="test|lint|typecheck|type-check|check|tsc"
|
|
63
|
+
MAKE_TARGETS="test|lint|check|typecheck|vet"
|
|
64
|
+
# forge#4315: `test:unit`, `lint:css` ... are the same check under a sub-name
|
|
65
|
+
# (#4314: only a closed set of suffixes; fixers `lint:fix`, watchers, e2e/integration (need services), deploy-ish and
|
|
66
|
+
# snapshot-updating scripts never run pre-PR.) $2 (optional) is the script's command: `format` is a check only when
|
|
67
|
+
# that command is a check form.
|
|
68
|
+
PKG_DENY='fix|watch|write|update|dev|serve|start|e2e|integration|deploy|release|publish|snapshot|-u'
|
|
69
|
+
PKG_SUFFIXES="unit ci node types type ts js static check"
|
|
70
|
+
pkg_check_script() {
|
|
71
|
+
[ -n "${1:-}" ] || return 1
|
|
72
|
+
case "$1" in *[!A-Za-z0-9:_.-]*) return 1 ;; esac
|
|
73
|
+
b=${1%%:*}
|
|
74
|
+
case "|$PKG_SCRIPTS|" in *"|$b|"*) ;; *) return 1 ;; esac
|
|
75
|
+
[ "$b" = "$1" ] && return 0
|
|
76
|
+
printf '%s' "$1" | grep -qiE "$PKG_DENY" && return 1
|
|
77
|
+
rest=${1#*:}; [ -n "$rest" ] || return 1
|
|
78
|
+
oldifs=$IFS; IFS=:; set -f
|
|
79
|
+
for seg in $rest; do
|
|
80
|
+
ok=0
|
|
81
|
+
case " $PKG_SUFFIXES " in *" $seg "*) [ -n "$seg" ] && ok=1 ;; esac
|
|
82
|
+
if [ "$ok" = 0 ] && [ "$seg" = format ]; then
|
|
83
|
+
case " ${2:-} " in *" --check "*|*" -c "*|*" --list-different "*|*" -l "*) ok=1 ;; esac
|
|
84
|
+
fi
|
|
85
|
+
[ "$ok" = 1 ] || { IFS=$oldifs; set +f; return 1; }
|
|
86
|
+
done
|
|
87
|
+
IFS=$oldifs; set +f
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
make_check_target() { case "|$MAKE_TARGETS|" in *"|$1|"*) [ -n "$1" ] ;; *) return 1 ;; esac; }
|
|
91
|
+
has_word() { k=$1; shift; for w in "$@"; do [ "$w" = "$k" ] && return 0; done; return 1; }
|
|
92
|
+
safe_dir() { case "$1" in ""|/*|*[!A-Za-z0-9._/-]*|..|../*|*/..|*/../*) return 1 ;; esac; return 0; }
|
|
56
93
|
# globs for the extensions in $2 under dir $1 ("." = repo root)
|
|
57
94
|
ext_globs() {
|
|
58
95
|
g=""; pre=""; [ "$1" = "." ] || pre="$1/"
|
|
@@ -70,10 +107,10 @@ grep -E '(^|/)package\.json$' "$T/files" | grep -v -E '(^|/)node_modules/' | sor
|
|
|
70
107
|
globs="$(ext_globs "$d" "$JS_EXT")$US$pj"
|
|
71
108
|
tag=""
|
|
72
109
|
if [ -n "$(ls "$d"/vitest.config.* "$d"/jest.config.* 2>/dev/null)" ] || jq -e 'has("jest")' "$pj" >/dev/null 2>&1; then tag=" (vitest/jest config)"; fi
|
|
73
|
-
jq -r '(.scripts // {}) | to_entries[] | select(.value | type == "string") | .key' "$pj" 2>/dev/null | while IFS= read -r s; do
|
|
110
|
+
jq -r '(.scripts // {}) | to_entries[] | select(.value | type == "string") | .key + "\t" + .value' "$pj" 2>/dev/null | while IFS="$(printf '\t')" read -r s sv; do
|
|
74
111
|
case "$s" in
|
|
75
112
|
test) row "$pj#scripts.$s$tag" "$d" "$PM run $s" "$globs" ;;
|
|
76
|
-
|
|
113
|
+
*) pkg_check_script "$s" "$sv" && row "$pj#scripts.$s" "$d" "$PM run $s" "$globs" ;;
|
|
77
114
|
esac
|
|
78
115
|
done
|
|
79
116
|
done
|
|
@@ -105,20 +142,20 @@ done
|
|
|
105
142
|
# ---- Makefile ----
|
|
106
143
|
for mf in Makefile GNUmakefile; do
|
|
107
144
|
[ -f "$mf" ] || continue
|
|
108
|
-
grep -oE
|
|
145
|
+
grep -oE "^($MAKE_TARGETS)[[:space:]]*:" "$mf" | sed -E 's/[[:space:]]*:$//' | sort -u | while IFS= read -r t; do
|
|
109
146
|
row "$mf#$t" "." "make $t" "**"
|
|
110
147
|
done
|
|
111
148
|
done
|
|
112
149
|
|
|
113
150
|
# ---- CI workflow run: steps ----
|
|
114
|
-
# Emits: job US
|
|
151
|
+
# Emits: job US command (US = \037), one per command line; and P TAB path for each `paths:` filter item
|
|
115
152
|
# before `jobs:`. A block ends at the first non-blank line not indented past the `run` key (sibling keys such as env:).
|
|
116
153
|
wf_extract() {
|
|
117
154
|
awk '
|
|
118
155
|
function indent(s) { match(s, /^ */); return RLENGTH }
|
|
119
156
|
function unq(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); if (s ~ /^".*"$/ || s ~ /^\x27.*\x27$/) s = substr(s, 2, length(s) - 2); return s }
|
|
120
|
-
function emit(c) { c = unq(c); if (c != "") print job "\037"
|
|
121
|
-
BEGIN { injobs = 0; jind = -1; block = 0; inpaths = 0; job = ""
|
|
157
|
+
function emit(c) { c = unq(c); if (c != "") print job "\037" c }
|
|
158
|
+
BEGIN { injobs = 0; jind = -1; block = 0; inpaths = 0; job = "" }
|
|
122
159
|
{
|
|
123
160
|
line = $0; sub(/\r$/, "", line)
|
|
124
161
|
if (block) {
|
|
@@ -143,13 +180,7 @@ wf_extract() {
|
|
|
143
180
|
next
|
|
144
181
|
}
|
|
145
182
|
if (line ~ /^[^ \t#]/) { injobs = 0; next }
|
|
146
|
-
if (line ~ /^ +[A-Za-z0-9_-]+:[ \t]*$/ && (jind < 0 || indent(line) == jind)) { jind = indent(line); job = line; sub(/^ +/, "", job); sub(/:.*$/, "", job);
|
|
147
|
-
if (line ~ /^[ \t]*-[ \t]/) sname = ""
|
|
148
|
-
if (line ~ /^[ \t]*(-[ \t]+)?name:/) {
|
|
149
|
-
nm = line; sub(/^[ \t]*(-[ \t]+)?name:[ \t]*/, "", nm); sub(/[ \t]+#.*$/, "", nm)
|
|
150
|
-
if (line !~ /^[ \t]*-[ \t]/ && indent(line) == jind + 2) jname = unq(nm); else sname = unq(nm)
|
|
151
|
-
next
|
|
152
|
-
}
|
|
183
|
+
if (line ~ /^ +[A-Za-z0-9_-]+:[ \t]*$/ && (jind < 0 || indent(line) == jind)) { jind = indent(line); job = line; sub(/^ +/, "", job); sub(/:.*$/, "", job); next }
|
|
153
184
|
if (line ~ /^[ \t]*(-[ \t]+)?run:/) {
|
|
154
185
|
rest = line; sub(/^[ \t]*(-[ \t]+)?run:[ \t]*/, "", rest); sub(/[ \t]+#.*$/, "", rest)
|
|
155
186
|
# bind = the column of the `run` key: step keys after it (env:, shell:) sit there and end the block
|
|
@@ -160,20 +191,93 @@ wf_extract() {
|
|
|
160
191
|
END { if (block && pend != "") emit(pend) }
|
|
161
192
|
' "$1"
|
|
162
193
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
# forge#4300: allowlist. 0 when the whole command $1 is a recognised check; sets CK_CWD (one leading `cd|pushd DIR &&`,
|
|
195
|
+
# else ".") and CK_CMD (the command without that prefix). Names never matter: a mutating command is dropped whatever its
|
|
196
|
+
# step is called, and a check is mapped whatever it is called.
|
|
197
|
+
check_cmd() {
|
|
198
|
+
c=$1; CK_CWD=.; CK_CMD=
|
|
199
|
+
# shellcheck disable=SC2016 # literal ${{ and $( patterns
|
|
200
|
+
# forge#4316: runner-file writes, redirects and process substitution are CI plumbing, never a local check
|
|
201
|
+
case "$c" in *'${{'*|*'`'*|*'$('*|*'$GITHUB_'*|*'<'*|*'>'*) return 1 ;; esac
|
|
202
|
+
case "$c" in
|
|
203
|
+
"cd "*" && "*|"pushd "*" && "*)
|
|
204
|
+
d=${c#* }; d=${d%%" && "*}; c=${c#*" && "}
|
|
205
|
+
case "$d" in ""|/*|*[!A-Za-z0-9._/-]*|..|../*|*/..|*/../*) return 1 ;; esac
|
|
206
|
+
d=${d%/}; [ -n "$d" ] && [ "$d" != . ] && CK_CWD=$d ;;
|
|
207
|
+
esac
|
|
208
|
+
c=${c%" && popd"}
|
|
209
|
+
case "$c" in *';'*|*'|'*|*'&'*) return 1 ;; esac
|
|
210
|
+
CK_CMD=$c
|
|
211
|
+
set -f
|
|
212
|
+
# shellcheck disable=SC2086 # split into words on purpose; set -f stops glob expansion
|
|
213
|
+
set -- $c
|
|
214
|
+
set +f
|
|
215
|
+
while [ $# -gt 0 ]; do case "$1" in [A-Za-z_]*=*) shift ;; *) break ;; esac; done
|
|
216
|
+
case "${1:-} ${2:-}" in
|
|
217
|
+
"uv run"|"poetry run"|"pnpm exec"|"yarn exec"|"python -m"|"python3 -m"|"bundle exec") shift 2 ;;
|
|
218
|
+
"npx "*|"bunx "*) shift ;;
|
|
219
|
+
esac
|
|
220
|
+
# forge#4316: a fixer rewrites the tree; only its read-only check form is a check
|
|
221
|
+
for w in "$@"; do case "$w" in --fix|--fix=*|--write|-w) return 1 ;; esac; done
|
|
222
|
+
case "${1:-} ${2:-}" in
|
|
223
|
+
"black "*|"ruff format") has_word --check "$@" || return 1 ;;
|
|
224
|
+
"gofmt "*) has_word -l "$@" || has_word -d "$@" || return 1 ;;
|
|
225
|
+
"prettier "*) has_word --check "$@" || has_word -c "$@" || has_word --list-different "$@" || has_word -l "$@" || return 1 ;;
|
|
226
|
+
esac
|
|
227
|
+
case "${1:-}" in
|
|
228
|
+
npm|pnpm|yarn|bun)
|
|
229
|
+
pm=$1; shift
|
|
230
|
+
case "$pm ${1:-}" in # forge#4315: pnpm -r test, npm --prefix DIR test
|
|
231
|
+
"pnpm -r"|"pnpm --recursive") shift ;;
|
|
232
|
+
"npm --prefix") [ $# -ge 2 ] && safe_dir "$2" || return 1; shift 2 ;;
|
|
233
|
+
esac
|
|
234
|
+
[ "${1:-}" = test ] && return 0
|
|
235
|
+
[ "${1:-}" = run ] && pkg_check_script "${2:-}" && return 0
|
|
236
|
+
case "$pm" in yarn|pnpm) pkg_check_script "${1:-}" && return 0 ;; esac
|
|
237
|
+
return 1 ;;
|
|
238
|
+
cargo) case "${2:-}" in test|clippy|check) return 0 ;; esac; return 1 ;;
|
|
239
|
+
./gradlew|gradlew|gradle|./mvnw|mvnw|mvn)
|
|
240
|
+
# every non-flag word is a check task/goal (`:mod:test` qualified for gradle); publish/deploy/install never pass
|
|
241
|
+
case "$1" in *mvn*) goals="test|verify|validate" ;; *) goals="test|check|lint" ;; esac
|
|
242
|
+
shift; nt=0
|
|
243
|
+
for w in "$@"; do
|
|
244
|
+
case "$w" in -*) continue ;; esac
|
|
245
|
+
t=${w##*:}
|
|
246
|
+
case "|$goals|" in *"|$t|"*) [ -n "$t" ] && nt=$((nt+1)) ;; *) return 1 ;; esac
|
|
247
|
+
done
|
|
248
|
+
[ "$nt" -gt 0 ]; return ;;
|
|
249
|
+
rspec|unittest) return 0 ;;
|
|
250
|
+
python|python3) [ "${2:-}" = manage.py ] && [ "${3:-}" = test ]; return ;;
|
|
251
|
+
tox|nox)
|
|
252
|
+
shift; prev=""
|
|
253
|
+
for w in "$@"; do
|
|
254
|
+
case "$prev" in -e|-s|--env|--session|--sessions) case ",$w," in *publish*|*release*|*deploy*|*upload*) return 1 ;; esac ;; esac
|
|
255
|
+
case "$w" in -e*|-s*) case "${w#-?}" in *publish*|*release*|*deploy*|*upload*) return 1 ;; esac ;; esac
|
|
256
|
+
prev=$w
|
|
257
|
+
done
|
|
258
|
+
return 0 ;;
|
|
259
|
+
deno) [ "${2:-}" = test ]; return ;;
|
|
260
|
+
bash|sh) case "${2:-}" in *.test.sh) safe_dir "$2"; return ;; esac; return 1 ;;
|
|
261
|
+
make)
|
|
262
|
+
# forge#4316: every target must be a check (`make test deploy` is dropped); VAR=value and flags are not targets
|
|
263
|
+
shift; nt=0
|
|
264
|
+
for w in "$@"; do case "$w" in -*|*=*) continue ;; esac; make_check_target "$w" || return 1; nt=$((nt+1)); done
|
|
265
|
+
[ "$nt" -gt 0 ]; return ;;
|
|
266
|
+
go) case "${2:-}" in test|vet|build) return 0 ;; esac; return 1 ;;
|
|
267
|
+
node) [ "${2:-}" = --test ]; return ;;
|
|
268
|
+
eslint|tsc|prettier|vitest|jest|pytest|ruff|flake8|pylint|mypy|black|golangci-lint|gofmt|staticcheck|shellcheck) return 0 ;;
|
|
167
269
|
esac
|
|
168
270
|
return 1
|
|
169
271
|
}
|
|
170
|
-
# forge#4297: deploy, publish, release, push and secret-using steps are never check commands
|
|
171
|
-
deploy_step() {
|
|
172
|
-
grep -qiE 'deploy|publish|release|push|secret' <<<"$*"
|
|
173
|
-
}
|
|
174
272
|
tool_globs() {
|
|
175
273
|
c=" $1 "
|
|
176
274
|
case "$c" in
|
|
275
|
+
*\ cargo\ *) printf '%s' "**/*.rs$US**/Cargo.toml${US}Cargo.lock"; return ;;
|
|
276
|
+
*gradlew\ *|*\ gradle\ *|*mvnw\ *|*\ mvn\ *) printf '%s' "$(ext_globs . "java kt kts gradle groovy scala")$US**/pom.xml"; return ;;
|
|
277
|
+
*[\ /]rspec\ *) printf '**/*.rb'; return ;;
|
|
278
|
+
*\ manage.py\ test\ *|*\ unittest\ *|*\ tox\ *|*\ nox\ *) printf '**/*.py'; return ;;
|
|
279
|
+
*\ npm\ --prefix\ *) d=${1#*--prefix }; d=${d%% *}; ext_globs "$d" "$JS_EXT"; return ;;
|
|
280
|
+
*\ deno\ *|*\ bun\ *|*\ bunx\ *) ext_globs . "$JS_EXT"; return ;;
|
|
177
281
|
*[\ /]ruff\ *|*[\ /]flake8\ *|*[\ /]pylint\ *|*[\ /]mypy\ *|*[\ /]black\ *|*[\ /]pytest\ *) printf '**/*.py'; return ;;
|
|
178
282
|
*[\ /]golangci-lint\ *|*[\ /]gofmt\ *|*[\ /]staticcheck\ *|*\ go\ *) printf '**/*.go'; return ;;
|
|
179
283
|
*[\ /]shellcheck\ *|*\ bash\ *.sh\ *|*\ sh\ *.sh\ *) printf '**/*.sh'; return ;;
|
|
@@ -185,13 +289,14 @@ grep -E '^\.github/workflows/[^/]+\.(yml|yaml)$' "$T/files" | sort | while IFS=
|
|
|
185
289
|
[ -f "$wf" ] || continue
|
|
186
290
|
wf_extract "$wf" > "$T/wf"
|
|
187
291
|
paths=$(awk -F'\t' '$1 == "P" { printf "%s\037", $2 }' "$T/wf"); paths=${paths%"$US"}
|
|
188
|
-
awk -F'\t' '$1 != "P"' "$T/wf" | while IFS="$
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
g
|
|
292
|
+
awk -F'\t' '$1 != "P"' "$T/wf" | while IFS="$US" read -r job cmd; do
|
|
293
|
+
check_cmd "$cmd" || continue
|
|
294
|
+
g=$(tool_globs "$CK_CMD")
|
|
295
|
+
[ -n "$g" ] && [ "$CK_CWD" != . ] && g="$CK_CWD/${g//$US/$US$CK_CWD/}"
|
|
192
296
|
[ -n "$g" ] || g="$paths"
|
|
193
|
-
[ -n "$g" ] || g="
|
|
194
|
-
|
|
297
|
+
[ -n "$g" ] || { [ "$CK_CWD" != . ] && g="$CK_CWD/**"; }
|
|
298
|
+
[ -n "$g" ] || continue
|
|
299
|
+
row "$wf#jobs.$job" "$CK_CWD" "$CK_CMD" "$g"
|
|
195
300
|
done
|
|
196
301
|
done
|
|
197
302
|
|
|
@@ -207,5 +312,6 @@ MAP=$(jq -R -s -c '
|
|
|
207
312
|
if [ "$STDOUT" = 1 ]; then printf '%s\n' "$MAP"; exit 0; fi
|
|
208
313
|
[ -n "$OUT" ] || OUT=".forge/test-map.json"
|
|
209
314
|
mkdir -p "$(dirname "$OUT")" || die "cannot create $(dirname "$OUT")"
|
|
210
|
-
printf '%s\n' "$MAP" > "$OUT.tmp.$$"
|
|
315
|
+
printf '%s\n' "$MAP" > "$OUT.tmp.$$" || die "cannot write $OUT"
|
|
316
|
+
mv "$OUT.tmp.$$" "$OUT" || die "cannot write $OUT"
|
|
211
317
|
echo "$OUT"
|
package/scripts/test-map.test.sh
CHANGED
|
@@ -15,11 +15,13 @@ eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
|
|
|
15
15
|
|
|
16
16
|
# copy a fixture into a fresh git repo and build its map
|
|
17
17
|
setup() {
|
|
18
|
-
rm -rf "$TMP/$1"; cp -R "$FIX/$1" "$TMP/$1"; cd "$TMP/$1" || exit 1
|
|
18
|
+
rm -rf "${TMP:?}/$1"; cp -R "$FIX/$1" "$TMP/$1"; cd "$TMP/$1" || exit 1
|
|
19
19
|
git init -q -b main; git config user.email t@example.com; git config user.name t; git config commit.gpgsign false
|
|
20
20
|
git add -A; git commit -qm init
|
|
21
21
|
OUTP="$(bash "$S")"; RC=$?
|
|
22
22
|
}
|
|
23
|
+
# forge#4300: workflow-derived entries of the map JSON in $2 carry no bare `**` glob
|
|
24
|
+
nostar() { eq "$1 no bare ** on workflow entries" "$(printf '%s' "$2" | jq '[.entries[] | select(.source | startswith(".github/workflows/")) | .globs[] | select(. == "**")] | length')" 0; }
|
|
23
25
|
# selected commands for changed files, sorted, joined by |
|
|
24
26
|
sel() { node "$SEL" --map .forge/test-map.json --select "$@" | LC_ALL=C sort | paste -sd'|' -; }
|
|
25
27
|
|
|
@@ -44,13 +46,15 @@ eq "python non-py" "$(sel uv.lock)" ""
|
|
|
44
46
|
# ---- 3. go: go.mod, Makefile and CI workflow steps; CI-only lint step; install lines skipped ----
|
|
45
47
|
setup go
|
|
46
48
|
eq "go rc" "$RC" 0
|
|
47
|
-
eq "go file" "$(sel main.go)" "
|
|
48
|
-
eq "nested go file" "$(sel internal/util/util.go)" "
|
|
49
|
+
eq "go file" "$(sel main.go)" "go test ./...|go vet ./...|golangci-lint run ./...|make test|make vet"
|
|
50
|
+
eq "nested go file" "$(sel internal/util/util.go)" "go test ./...|go vet ./...|golangci-lint run ./...|make test|make vet"
|
|
51
|
+
eq "unrecognised ci script dropped" "$(jq '[.entries[] | select(.command | test("check-docs"))] | length' .forge/test-map.json)" 0
|
|
49
52
|
eq "ci-only lint source" "$(jq -r '.entries[] | select(.command == "golangci-lint run ./...") | .source' .forge/test-map.json)" ".github/workflows/ci.yml#jobs.lint"
|
|
50
53
|
eq "install line skipped" "$(jq '[.entries[] | select(.command | test("mod download"))] | length' .forge/test-map.json)" 0
|
|
51
|
-
eq "paths filter fallback" "$(jq -c '.entries[] | select(.command == "
|
|
54
|
+
eq "paths filter fallback" "$(jq -c '.entries[] | select(.command == "make vet") | .globs' .forge/test-map.json)" '["**.go","go.mod"]'
|
|
52
55
|
eq "docs file" "$(sel README.md)" "make test"
|
|
53
56
|
eq "go.sum" "$(sel go.sum)" "go test ./...|go vet ./...|make test"
|
|
57
|
+
nostar go "$(cat .forge/test-map.json)"
|
|
54
58
|
|
|
55
59
|
# ---- 4. regenerable: a rerun is byte-identical; --stdout writes nothing; --output ----
|
|
56
60
|
cp .forge/test-map.json "$TMP/first.json"
|
|
@@ -84,8 +88,11 @@ jobs:
|
|
|
84
88
|
EOF
|
|
85
89
|
git add -A
|
|
86
90
|
OUTS="$(bash "$S" --stdout)"
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
# forge#4300: ./custom-check and `bash scripts/verify.sh` are not allowlisted checks, so they are dropped
|
|
92
|
+
# forge#4316: `ruff check --fix` rewrites the tree, so it is dropped too
|
|
93
|
+
eq "wf commands" "$(printf '%s' "$OUTS" | jq -r '[.entries[].command] | sort | join("|")')" "shellcheck scripts/*.sh"
|
|
94
|
+
eq "wf globs" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | {(.command): .globs}] | add')" '{"shellcheck scripts/*.sh":["**/*.sh"]}'
|
|
95
|
+
nostar wf "$OUTS"
|
|
89
96
|
|
|
90
97
|
# ---- 5b. forge#4297: `- run: |` then env:/shell: ends the block; deploy/publish/release/push/secret steps dropped ----
|
|
91
98
|
mkdir -p "$TMP/wf2/.github/workflows"; cd "$TMP/wf2" || exit 1
|
|
@@ -130,6 +137,56 @@ EOF
|
|
|
130
137
|
git add -A
|
|
131
138
|
OUTS="$(bash "$S" --stdout)"
|
|
132
139
|
eq "wf2 commands" "$(printf '%s' "$OUTS" | jq -r '[.entries[].command] | sort | join("|")')" "npm run lint|npm test|tsc --noEmit"
|
|
140
|
+
nostar wf2 "$OUTS"
|
|
141
|
+
|
|
142
|
+
# ---- 5d. forge#4300: allowlist, not a name/substring denylist; names never decide; cd/pushd prefix becomes the cwd ----
|
|
143
|
+
mkdir -p "$TMP/wf3/.github/workflows"; cd "$TMP/wf3" || exit 1
|
|
144
|
+
git init -q -b main
|
|
145
|
+
cat > .github/workflows/ci.yml <<'EOF'
|
|
146
|
+
on: pull_request
|
|
147
|
+
jobs:
|
|
148
|
+
release-tests:
|
|
149
|
+
runs-on: ubuntu-latest
|
|
150
|
+
steps:
|
|
151
|
+
- run: npm test
|
|
152
|
+
- run: pytest tests/test_release.py
|
|
153
|
+
- run: pushd sub && make test
|
|
154
|
+
- run: npm run lint
|
|
155
|
+
name: Deploy
|
|
156
|
+
- name: Run tests
|
|
157
|
+
uses: some/action@v1
|
|
158
|
+
with:
|
|
159
|
+
name: release
|
|
160
|
+
- name: Typecheck
|
|
161
|
+
with:
|
|
162
|
+
name: release
|
|
163
|
+
run: npm run typecheck
|
|
164
|
+
ship:
|
|
165
|
+
runs-on: ubuntu-latest
|
|
166
|
+
steps:
|
|
167
|
+
- run: twine upload dist/*
|
|
168
|
+
- run: terraform apply -auto-approve
|
|
169
|
+
- run: kubectl apply -f k8s/
|
|
170
|
+
- run: ./deploy.sh
|
|
171
|
+
name: test
|
|
172
|
+
- name: Tests
|
|
173
|
+
with:
|
|
174
|
+
name: test
|
|
175
|
+
run: ./publish.sh
|
|
176
|
+
- run: pytest && twine upload dist/*
|
|
177
|
+
- run: cd ../other && make test
|
|
178
|
+
- run: npm run build
|
|
179
|
+
- run: npm test -- --token=${{ secrets.NPM_TOKEN }}
|
|
180
|
+
EOF
|
|
181
|
+
git add -A
|
|
182
|
+
OUTS="$(bash "$S" --stdout)"
|
|
183
|
+
eq "wf3 commands" "$(printf '%s' "$OUTS" | jq -r '[.entries[].command] | sort | join("|")')" "make test|npm run lint|npm run typecheck|npm test|pytest tests/test_release.py"
|
|
184
|
+
eq "wf3 release-tests job kept" "$(printf '%s' "$OUTS" | jq -r '[.entries[].source] | unique | join("|")')" ".github/workflows/ci.yml#jobs.release-tests"
|
|
185
|
+
eq "wf3 pushd cwd + globs" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | select(.command == "make test") | {cwd, globs}]')" '[{"cwd":"sub","globs":["sub/**"]}]'
|
|
186
|
+
eq "wf3 no mutating command" "$(printf '%s' "$OUTS" | jq '[.entries[] | select(.command | test("twine|terraform|kubectl|deploy|publish|build|secrets"))] | length')" 0
|
|
187
|
+
nostar wf3 "$OUTS"
|
|
188
|
+
printf '%s' "$OUTS" > "$TMP/wf3-map.json"
|
|
189
|
+
eq "wf3 pushd entry runs in its dir" "$(node "$SEL" --map "$TMP/wf3-map.json" --select sub/Makefile)" "cd sub && make test"
|
|
133
190
|
|
|
134
191
|
# ---- 5c. forge#4298: a package dir with a space and a non-ASCII name keeps whole globs; workflow paths with spaces ----
|
|
135
192
|
mkdir -p "$TMP/sp/web app" "$TMP/sp/pkg/ü/src" "$TMP/sp/.github/workflows"; cd "$TMP/sp" || exit 1
|
|
@@ -148,15 +205,146 @@ jobs:
|
|
|
148
205
|
runs-on: ubuntu-latest
|
|
149
206
|
steps:
|
|
150
207
|
- run: ./build-docs.sh
|
|
208
|
+
- run: make check
|
|
151
209
|
EOF
|
|
152
210
|
git add -A
|
|
153
211
|
OUTS="$(bash "$S" --stdout)"
|
|
154
212
|
eq "spaced package globs" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | select(.cwd == "web app") | .globs[]] | map(select(endswith("package.json") or endswith(".js")))')" '["web app/**/*.js","web app/package.json"]'
|
|
155
213
|
eq "non-ASCII package dir" "$(printf '%s' "$OUTS" | jq -r '[.entries[] | select(.command == "npm run lint") | .cwd] | join("|")')" "pkg/ü"
|
|
156
|
-
eq "spaced workflow path filter" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | select(.command == "
|
|
214
|
+
eq "spaced workflow path filter" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | select(.command == "make check") | .globs]')" '[["docs site/**"]]'
|
|
215
|
+
nostar sp "$OUTS"
|
|
216
|
+
eq "unrecognised docs script dropped" "$(printf '%s' "$OUTS" | jq '[.entries[] | select(.command == "./build-docs.sh")] | length')" 0
|
|
157
217
|
printf '%s' "$OUTS" > "$TMP/sp-map.json"
|
|
158
218
|
eq "spaced changed file selects its package test" "$(node "$SEL" --map "$TMP/sp-map.json" --select "web app/src/x.js")" "cd 'web app' && npm run test"
|
|
159
219
|
|
|
220
|
+
# ---- 5e. forge#4315: real runners per ecosystem are mapped with their globs; mutating siblings stay dropped ----
|
|
221
|
+
# wf_case NAME: the step commands on stdin become .github/workflows/ci.yml of a fresh repo; OUTS = its map
|
|
222
|
+
wf_case() {
|
|
223
|
+
mkdir -p "$TMP/$1/.github/workflows"; cd "$TMP/$1" || exit 1
|
|
224
|
+
git init -q -b main
|
|
225
|
+
{ printf 'on: pull_request\njobs:\n ci:\n runs-on: ubuntu-latest\n steps:\n'; sed 's/^/ - run: /'; } > .github/workflows/ci.yml
|
|
226
|
+
git add -A
|
|
227
|
+
OUTS="$(bash "$S" --stdout)"
|
|
228
|
+
nostar "$1" "$OUTS"
|
|
229
|
+
}
|
|
230
|
+
cmds() { printf '%s' "$OUTS" | jq -r '[.entries[].command] | sort | join("|")'; }
|
|
231
|
+
globs_of() { printf '%s' "$OUTS" | jq -r --arg c "$1" '[.entries[] | select(.command == $c) | .globs[]] | join(",")'; }
|
|
232
|
+
|
|
233
|
+
wf_case rust <<'WF'
|
|
234
|
+
cargo test --workspace
|
|
235
|
+
cargo clippy -- -D warnings
|
|
236
|
+
cargo publish
|
|
237
|
+
cargo build --release
|
|
238
|
+
WF
|
|
239
|
+
eq "rust commands" "$(cmds)" "cargo clippy -- -D warnings|cargo test --workspace"
|
|
240
|
+
eq "rust globs" "$(globs_of "cargo test --workspace")" "**/*.rs,**/Cargo.toml,Cargo.lock"
|
|
241
|
+
|
|
242
|
+
wf_case jvm <<'WF'
|
|
243
|
+
./gradlew test
|
|
244
|
+
./gradlew :app:check --no-daemon
|
|
245
|
+
mvn -B test
|
|
246
|
+
./gradlew publish
|
|
247
|
+
./gradlew test publish
|
|
248
|
+
mvn deploy
|
|
249
|
+
mvn install
|
|
250
|
+
WF
|
|
251
|
+
eq "jvm commands" "$(cmds)" "./gradlew :app:check --no-daemon|./gradlew test|mvn -B test"
|
|
252
|
+
eq "jvm globs" "$(globs_of "mvn -B test")" "**/*.java,**/*.kt,**/*.kts,**/*.gradle,**/*.groovy,**/*.scala,**/pom.xml"
|
|
253
|
+
|
|
254
|
+
wf_case ruby <<'WF'
|
|
255
|
+
bundle exec rspec
|
|
256
|
+
rspec spec/models
|
|
257
|
+
bundle exec rake release
|
|
258
|
+
WF
|
|
259
|
+
eq "ruby commands" "$(cmds)" "bundle exec rspec|rspec spec/models"
|
|
260
|
+
eq "ruby globs" "$(globs_of "bundle exec rspec")" "**/*.rb"
|
|
261
|
+
|
|
262
|
+
wf_case pyrun <<'WF'
|
|
263
|
+
python manage.py test
|
|
264
|
+
python -m unittest discover
|
|
265
|
+
tox
|
|
266
|
+
nox -s lint
|
|
267
|
+
tox -e publish
|
|
268
|
+
nox -s release
|
|
269
|
+
python manage.py migrate
|
|
270
|
+
WF
|
|
271
|
+
eq "python runner commands" "$(cmds)" "nox -s lint|python -m unittest discover|python manage.py test|tox"
|
|
272
|
+
eq "python runner globs" "$(globs_of "tox")" "**/*.py"
|
|
273
|
+
|
|
274
|
+
wf_case js <<'WF'
|
|
275
|
+
deno test
|
|
276
|
+
bun test
|
|
277
|
+
pnpm -r test
|
|
278
|
+
npm --prefix web test
|
|
279
|
+
npm run test:unit
|
|
280
|
+
deno deploy
|
|
281
|
+
npm run deploy:prod
|
|
282
|
+
npm --prefix ../x test
|
|
283
|
+
WF
|
|
284
|
+
eq "js runner commands" "$(cmds)" "bun test|deno test|npm --prefix web test|npm run test:unit|pnpm -r test"
|
|
285
|
+
eq "bun globs" "$(globs_of "bun test")" "**/*.js,**/*.jsx,**/*.ts,**/*.tsx,**/*.mjs,**/*.cjs"
|
|
286
|
+
eq "npm --prefix globs" "$(globs_of "npm --prefix web test")" "web/**/*.js,web/**/*.jsx,web/**/*.ts,web/**/*.tsx,web/**/*.mjs,web/**/*.cjs"
|
|
287
|
+
|
|
288
|
+
# ---- 5f. forge#4314: package.json script suffix allowlist (fixers/watchers/e2e/deploy never run pre-PR) ----
|
|
289
|
+
mkdir -p "$TMP/sfx"; cd "$TMP/sfx" || exit 1
|
|
290
|
+
git init -q -b main
|
|
291
|
+
cat > package.json <<'JSON'
|
|
292
|
+
{"name":"s","scripts":{"test":"x","test:unit":"x","test:ci":"x","test:node":"x","test:types":"x","lint:ts":"x","lint:js":"x","check:static":"x","check:types":"x","tsc":"tsc --noEmit","typecheck":"tsc","lint:check":"x","test:unit:ci":"x","lint:format":"prettier --check .","check:format":"prettier --write .",
|
|
293
|
+
"lint:fix":"eslint --fix .","check:fix":"x","test:watch":"x","test:e2e":"x","test:integration":"x","test:deploy":"x","test:update":"x","test:snapshot":"x","lint:css":"x","test:unit:watch":"x","test:-u":"x","lint:write":"x","test:dev":"x","check:release":"x"}}
|
|
294
|
+
JSON
|
|
295
|
+
git add -A
|
|
296
|
+
SFX="$(bash "$S" --stdout | jq -r '[.entries[].source | sub("^package.json#scripts\\.";"")] | sort | join("|")')"
|
|
297
|
+
eq "script suffix allowlist" "$SFX" "check:static|check:types|lint:check|lint:format|lint:js|lint:ts|test|test:ci|test:node|test:types|test:unit|test:unit:ci|tsc|typecheck"
|
|
298
|
+
|
|
299
|
+
wf_case sfxwf <<'WF'
|
|
300
|
+
npm run lint:fix
|
|
301
|
+
npm run check:fix
|
|
302
|
+
pnpm test:watch
|
|
303
|
+
yarn test:e2e
|
|
304
|
+
bun run test:integration
|
|
305
|
+
npm run test:deploy
|
|
306
|
+
pnpm run lint:fix
|
|
307
|
+
npm run test:unit
|
|
308
|
+
pnpm lint:ts
|
|
309
|
+
yarn check:types
|
|
310
|
+
bun run test:ci
|
|
311
|
+
WF
|
|
312
|
+
eq "workflow suffix commands" "$(cmds)" "bun run test:ci|npm run test:unit|pnpm lint:ts|yarn check:types"
|
|
313
|
+
|
|
314
|
+
wf_case shell <<'WF'
|
|
315
|
+
bash scripts/a.test.sh
|
|
316
|
+
sh tests/b.test.sh
|
|
317
|
+
bash scripts/deploy.sh
|
|
318
|
+
bash scripts/verify.sh
|
|
319
|
+
WF
|
|
320
|
+
eq "shell commands" "$(cmds)" "bash scripts/a.test.sh|sh tests/b.test.sh"
|
|
321
|
+
eq "shell globs" "$(globs_of "bash scripts/a.test.sh")" "**/*.sh"
|
|
322
|
+
|
|
323
|
+
# ---- 5f. forge#4316: redirects, process substitution, $GITHUB_*, extra make targets and fixer forms are dropped ----
|
|
324
|
+
# (make needs a cwd for its globs: a workflow entry never gets a bare **)
|
|
325
|
+
wf_case unsafe <<'WF'
|
|
326
|
+
pytest > out.txt
|
|
327
|
+
pytest 2>&1
|
|
328
|
+
diff <(a) <(b)
|
|
329
|
+
tee >(x)
|
|
330
|
+
cd sub && make test deploy
|
|
331
|
+
eslint --fix .
|
|
332
|
+
prettier --write .
|
|
333
|
+
gofmt -w .
|
|
334
|
+
black .
|
|
335
|
+
ruff format .
|
|
336
|
+
cargo clippy --fix
|
|
337
|
+
pytest --junitxml=$GITHUB_WORKSPACE/r.xml
|
|
338
|
+
cd sub && make test lint
|
|
339
|
+
cd sub && make test V=1
|
|
340
|
+
black --check .
|
|
341
|
+
ruff format --check .
|
|
342
|
+
prettier --check .
|
|
343
|
+
gofmt -l .
|
|
344
|
+
eslint .
|
|
345
|
+
WF
|
|
346
|
+
eq "unsafe forms dropped, check forms kept" "$(cmds)" "black --check .|eslint .|gofmt -l .|make test V=1|make test lint|prettier --check .|ruff format --check ."
|
|
347
|
+
|
|
160
348
|
# ---- 6. usage ----
|
|
161
349
|
bash "$S" --repo-path "$TMP/missing" >/dev/null 2>&1; eq "bad repo path" "$?" 2
|
|
162
350
|
bash "$S" --nope >/dev/null 2>&1; eq "unknown option" "$?" 2
|