forgedock 1.28.0 → 1.29.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 +29 -0
- package/bin/engine-cli.mjs +71 -16
- package/bin/lean/batch.test.mjs +2 -2
- package/bin/lean/conformance.mjs +47 -2
- package/bin/lean/conformance.test.mjs +53 -1
- package/bin/lean/context.mjs +333 -0
- package/bin/lean/context.test.mjs +279 -0
- package/bin/lean/effects.mjs +19 -1
- package/bin/lean/lean.mjs +171 -35
- package/bin/lean/lean.test.mjs +413 -7
- package/bin/lean/orchestrate.mjs +80 -3
- package/bin/lean/prefilter.mjs +2 -2
- package/bin/lean/prefilter.test.mjs +3 -0
- package/bin/lean/prompts.mjs +22 -7
- package/bin/lean/recovery.test.mjs +230 -0
- package/bin/lean/render.mjs +25 -5
- package/bin/lean/testmap.mjs +84 -0
- package/bin/lean/testmap.test.mjs +58 -0
- package/bin/lean/verify.mjs +48 -3
- package/bin/lean/verify.test.mjs +81 -3
- package/bin/stats/index.mjs +12 -2
- package/bin/stats/local-source.mjs +13 -0
- package/bin/stats/query.mjs +27 -0
- package/bin/stats/stats.test.mjs +41 -2
- package/commands/orchestrate.md +52 -6
- package/docs/CONFIG.md +15 -0
- package/docs/OUTCOME-CODES.md +14 -0
- package/docs/RELEASE-NOTES.md +9 -0
- package/package.json +1 -1
- package/scripts/check-spec-markers.sh +1 -0
- package/scripts/fixtures/test-map/go/.github/workflows/ci.yml +23 -0
- package/scripts/fixtures/test-map/go/Makefile +5 -0
- package/scripts/fixtures/test-map/go/README.md +1 -0
- package/scripts/fixtures/test-map/go/go.mod +3 -0
- package/scripts/fixtures/test-map/go/internal/util/util.go +3 -0
- package/scripts/fixtures/test-map/go/main.go +3 -0
- package/scripts/fixtures/test-map/node/package.json +10 -0
- package/scripts/fixtures/test-map/node/packages/ui/package.json +4 -0
- package/scripts/fixtures/test-map/node/packages/ui/src/button.tsx +1 -0
- package/scripts/fixtures/test-map/node/pnpm-lock.yaml +1 -0
- package/scripts/fixtures/test-map/node/src/a.ts +1 -0
- package/scripts/fixtures/test-map/node/vitest.config.ts +1 -0
- package/scripts/fixtures/test-map/python/pkg/__init__.py +0 -0
- package/scripts/fixtures/test-map/python/pkg/mod.py +2 -0
- package/scripts/fixtures/test-map/python/pyproject.toml +9 -0
- package/scripts/fixtures/test-map/python/tests/test_mod.py +5 -0
- package/scripts/fixtures/test-map/python/uv.lock +1 -0
- package/scripts/repo-facts.sh +146 -0
- package/scripts/repo-facts.test.sh +106 -0
- package/scripts/test-map.sh +211 -0
- package/scripts/test-map.test.sh +165 -0
- package/scripts/wait-ci-green.sh +10 -4
- package/scripts/wait-ci-green.test.sh +9 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# test-map.sh — deterministic `file glob → check command` map for a repo (forge#4272). No LLM, no network, runs none of
|
|
6
|
+
# the repo's commands: git + jq + awk only (no yq). Codebase-agnostic: node, python, go, make and CI workflow steps.
|
|
7
|
+
#
|
|
8
|
+
# Usage: test-map.sh [--repo-path DIR] [--output FILE | --stdout]
|
|
9
|
+
# default output <repo>/.forge/test-map.json, rewritten on every run (a regenerable cache; deleting it restores the
|
|
10
|
+
# pre-PR step's `learned.test_commands` behaviour).
|
|
11
|
+
# Output: {"version":1,"entries":[{"globs":[...],"command","cwd","source"}]}, sorted by source then command; entries
|
|
12
|
+
# with the same (command, cwd) are merged (globs unioned, first source kept).
|
|
13
|
+
# Sources:
|
|
14
|
+
# package.json (root + every tracked one outside node_modules): scripts test/lint/typecheck/type-check/check →
|
|
15
|
+
# `<pm> run <script>` in the package dir (pm from the root lockfile), JS/TS globs under that dir
|
|
16
|
+
# pytest (pyproject [tool.pytest*], pytest.ini, setup.cfg [tool:pytest], conftest.py) and ruff config → `pytest`,
|
|
17
|
+
# `ruff check .` (through `uv run` / `poetry run` when that lockfile exists), `**/*.py`
|
|
18
|
+
# go.mod (dir D) → `go test ./...`, `go vet ./...` in D, `D/**/*.go`, `D/go.mod`, `D/go.sum`
|
|
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
|
+
# install/setup lines skipped): globs
|
|
22
|
+
# from the tool (python/go/shell/js), else the workflow's `paths:` filter, else `**`. A step whose job id, job
|
|
23
|
+
# name, step name or command matches deploy/publish/release/push/secret is never mapped (forge#4297): the engine
|
|
24
|
+
# runs these commands before a PR, so only check steps belong here.
|
|
25
|
+
# Exit: 0 ok | 2 usage or bad repo path. bash 3.2 portable.
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
die() { echo "test-map: $*" >&2; exit 2; }
|
|
29
|
+
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
30
|
+
REPO="."; OUT=""; STDOUT=0
|
|
31
|
+
while [ $# -gt 0 ]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--repo-path) [ $# -ge 2 ] && [ -n "$2" ] || die "usage: test-map.sh [--repo-path DIR] [--output FILE | --stdout]"; REPO="$2"; shift 2 ;;
|
|
34
|
+
--output) [ $# -ge 2 ] && [ -n "$2" ] || die "usage: test-map.sh [--repo-path DIR] [--output FILE | --stdout]"; OUT="$2"; shift 2 ;;
|
|
35
|
+
--stdout) STDOUT=1; shift ;;
|
|
36
|
+
*) die "unknown option: $1" ;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
[ -d "$REPO" ] || die "repo path not found: $REPO"
|
|
40
|
+
case "$OUT" in ""|/*) ;; *) OUT="$PWD/$OUT" ;; esac
|
|
41
|
+
cd "$REPO" || die "cannot enter $REPO"
|
|
42
|
+
|
|
43
|
+
T=$(mktemp -d "${TMPDIR:-/tmp}/test-map.XXXXXX") || die "mktemp failed"
|
|
44
|
+
trap 'rm -rf "$T"' EXIT
|
|
45
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
46
|
+
# -z and quotepath=off (#4298): non-ASCII and spaced paths come out verbatim, never C-quoted
|
|
47
|
+
git -c core.quotepath=off ls-files -z --cached --others --exclude-standard | tr '\0' '\n' > "$T/files"
|
|
48
|
+
else
|
|
49
|
+
find . -type f -not -path './.git/*' | sed 's|^\./||' | sort > "$T/files"
|
|
50
|
+
fi
|
|
51
|
+
: > "$T/rows"
|
|
52
|
+
# row: source TAB cwd TAB command TAB globs separated by US (\037, #4298: a glob may contain spaces)
|
|
53
|
+
US="$(printf '\037')"
|
|
54
|
+
row() { printf '%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" >> "$T/rows"; }
|
|
55
|
+
JS_EXT="js jsx ts tsx mjs cjs"
|
|
56
|
+
# globs for the extensions in $2 under dir $1 ("." = repo root)
|
|
57
|
+
ext_globs() {
|
|
58
|
+
g=""; pre=""; [ "$1" = "." ] || pre="$1/"
|
|
59
|
+
for e in $2; do g="$g$US${pre}**/*.$e"; done
|
|
60
|
+
printf '%s' "${g#"$US"}"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# ---- package.json scripts ----
|
|
64
|
+
PM=npm
|
|
65
|
+
[ -f yarn.lock ] && PM=yarn
|
|
66
|
+
[ -f pnpm-lock.yaml ] && PM=pnpm
|
|
67
|
+
grep -E '(^|/)package\.json$' "$T/files" | grep -v -E '(^|/)node_modules/' | sort | while IFS= read -r pj; do
|
|
68
|
+
[ -f "$pj" ] || continue
|
|
69
|
+
d=$(dirname "$pj")
|
|
70
|
+
globs="$(ext_globs "$d" "$JS_EXT")$US$pj"
|
|
71
|
+
tag=""
|
|
72
|
+
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
|
|
74
|
+
case "$s" in
|
|
75
|
+
test) row "$pj#scripts.$s$tag" "$d" "$PM run $s" "$globs" ;;
|
|
76
|
+
lint|typecheck|type-check|check) row "$pj#scripts.$s" "$d" "$PM run $s" "$globs" ;;
|
|
77
|
+
esac
|
|
78
|
+
done
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
# ---- python ----
|
|
82
|
+
RUN=""
|
|
83
|
+
[ -f poetry.lock ] && RUN="poetry run "
|
|
84
|
+
[ -f uv.lock ] && RUN="uv run "
|
|
85
|
+
PY_SRC=""
|
|
86
|
+
if [ -f pyproject.toml ] && grep -qE '^\[tool\.pytest' pyproject.toml; then PY_SRC="pyproject.toml#tool.pytest"
|
|
87
|
+
elif [ -f pytest.ini ]; then PY_SRC="pytest.ini"
|
|
88
|
+
elif [ -f setup.cfg ] && grep -qE '^\[tool:pytest\]' setup.cfg; then PY_SRC="setup.cfg#tool:pytest"
|
|
89
|
+
elif grep -qE '(^|/)conftest\.py$' "$T/files"; then PY_SRC="conftest.py"; fi
|
|
90
|
+
[ -n "$PY_SRC" ] && row "$PY_SRC" "." "${RUN}pytest" "**/*.py$US**/conftest.py"
|
|
91
|
+
RUFF_SRC=""
|
|
92
|
+
if [ -f pyproject.toml ] && grep -qE '^\[tool\.ruff' pyproject.toml; then RUFF_SRC="pyproject.toml#tool.ruff"
|
|
93
|
+
elif [ -f ruff.toml ]; then RUFF_SRC="ruff.toml"
|
|
94
|
+
elif [ -f .ruff.toml ]; then RUFF_SRC=".ruff.toml"; fi
|
|
95
|
+
[ -n "$RUFF_SRC" ] && row "$RUFF_SRC" "." "${RUN}ruff check ." "**/*.py"
|
|
96
|
+
|
|
97
|
+
# ---- go ----
|
|
98
|
+
grep -E '(^|/)go\.mod$' "$T/files" | grep -v -E '(^|/)vendor/' | sort | while IFS= read -r gm; do
|
|
99
|
+
d=$(dirname "$gm"); pre=""; [ "$d" = "." ] || pre="$d/"
|
|
100
|
+
globs="${pre}**/*.go$US${pre}go.mod$US${pre}go.sum"
|
|
101
|
+
row "$gm" "$d" "go test ./..." "$globs"
|
|
102
|
+
row "$gm" "$d" "go vet ./..." "$globs"
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
# ---- Makefile ----
|
|
106
|
+
for mf in Makefile GNUmakefile; do
|
|
107
|
+
[ -f "$mf" ] || continue
|
|
108
|
+
grep -oE '^(test|lint|check|typecheck|vet)[[:space:]]*:' "$mf" | sed -E 's/[[:space:]]*:$//' | sort -u | while IFS= read -r t; do
|
|
109
|
+
row "$mf#$t" "." "make $t" "**"
|
|
110
|
+
done
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
# ---- CI workflow run: steps ----
|
|
114
|
+
# Emits: job US jobname US stepname US command (US = \037, so empty names survive `read`), one per command line; and P TAB path for each `paths:` filter item
|
|
115
|
+
# before `jobs:`. A block ends at the first non-blank line not indented past the `run` key (sibling keys such as env:).
|
|
116
|
+
wf_extract() {
|
|
117
|
+
awk '
|
|
118
|
+
function indent(s) { match(s, /^ */); return RLENGTH }
|
|
119
|
+
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" jname "\037" sname "\037" c }
|
|
121
|
+
BEGIN { injobs = 0; jind = -1; block = 0; inpaths = 0; job = ""; jname = ""; sname = "" }
|
|
122
|
+
{
|
|
123
|
+
line = $0; sub(/\r$/, "", line)
|
|
124
|
+
if (block) {
|
|
125
|
+
if (line ~ /^[ \t]*$/) next
|
|
126
|
+
if (indent(line) > bind) {
|
|
127
|
+
l = line; sub(/^ +/, "", l)
|
|
128
|
+
if (fold) { pend = (pend == "" ? l : pend " " l); next }
|
|
129
|
+
if (pend != "") { l = pend " " l; pend = "" }
|
|
130
|
+
if (l ~ /\\$/) { pend = substr(l, 1, length(l) - 1); sub(/[ \t]+$/, "", pend); next }
|
|
131
|
+
emit(l); next
|
|
132
|
+
}
|
|
133
|
+
if (pend != "") { emit(pend); pend = "" }
|
|
134
|
+
block = 0
|
|
135
|
+
}
|
|
136
|
+
if (line ~ /^jobs:[ \t]*$/) { injobs = 1; inpaths = 0; next }
|
|
137
|
+
if (!injobs) {
|
|
138
|
+
if (line ~ /^[ \t]*paths:[ \t]*$/) { inpaths = 1; pind = indent(line); next }
|
|
139
|
+
if (inpaths) {
|
|
140
|
+
if (line ~ /^[ \t]*-[ \t]/ && indent(line) >= pind) { p = line; sub(/^[ \t]*-[ \t]*/, "", p); sub(/[ \t]+#.*$/, "", p); print "P\t" unq(p); next }
|
|
141
|
+
if (line !~ /^[ \t]*$/ && line !~ /^[ \t]*#/) inpaths = 0
|
|
142
|
+
}
|
|
143
|
+
next
|
|
144
|
+
}
|
|
145
|
+
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); jname = ""; sname = ""; next }
|
|
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
|
+
}
|
|
153
|
+
if (line ~ /^[ \t]*(-[ \t]+)?run:/) {
|
|
154
|
+
rest = line; sub(/^[ \t]*(-[ \t]+)?run:[ \t]*/, "", rest); sub(/[ \t]+#.*$/, "", rest)
|
|
155
|
+
# bind = the column of the `run` key: step keys after it (env:, shell:) sit there and end the block
|
|
156
|
+
if (rest ~ /^[|>][-+0-9]*$/) { block = 1; pend = ""; fold = (rest ~ /^>/); bind = match(line, /run:/) - 1; next }
|
|
157
|
+
emit(rest)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
END { if (block && pend != "") emit(pend) }
|
|
161
|
+
' "$1"
|
|
162
|
+
}
|
|
163
|
+
skip_cmd() {
|
|
164
|
+
case "$1" in
|
|
165
|
+
\#*|echo\ *|echo|cd\ *|export\ *|set\ *|if\ *|then|fi|else|for\ *|do|done|\}*|\{*|*'$GITHUB_'*|*'>> $GITHUB'*) return 0 ;;
|
|
166
|
+
npm\ ci*|npm\ install*|npm\ i|npm\ i\ *|pnpm\ install*|pnpm\ i|yarn|yarn\ install*|pip\ install*|pip3\ install*|python*\ -m\ pip\ install*|uv\ sync*|uv\ pip\ install*|poetry\ install*|go\ mod\ download*|apt-get*|sudo\ *|corepack\ *|brew\ *) return 0 ;;
|
|
167
|
+
esac
|
|
168
|
+
return 1
|
|
169
|
+
}
|
|
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
|
+
tool_globs() {
|
|
175
|
+
c=" $1 "
|
|
176
|
+
case "$c" in
|
|
177
|
+
*[\ /]ruff\ *|*[\ /]flake8\ *|*[\ /]pylint\ *|*[\ /]mypy\ *|*[\ /]black\ *|*[\ /]pytest\ *) printf '**/*.py'; return ;;
|
|
178
|
+
*[\ /]golangci-lint\ *|*[\ /]gofmt\ *|*[\ /]staticcheck\ *|*\ go\ *) printf '**/*.go'; return ;;
|
|
179
|
+
*[\ /]shellcheck\ *|*\ bash\ *.sh\ *|*\ sh\ *.sh\ *) printf '**/*.sh'; return ;;
|
|
180
|
+
*[\ /]eslint\ *|*[\ /]tsc\ *|*[\ /]prettier\ *|*[\ /]vitest\ *|*[\ /]jest\ *|*\ node\ --test*|*\ npm\ *|*\ pnpm\ *|*\ yarn\ *|*\ npx\ *) ext_globs . "$JS_EXT"; return ;;
|
|
181
|
+
esac
|
|
182
|
+
printf ''
|
|
183
|
+
}
|
|
184
|
+
grep -E '^\.github/workflows/[^/]+\.(yml|yaml)$' "$T/files" | sort | while IFS= read -r wf; do
|
|
185
|
+
[ -f "$wf" ] || continue
|
|
186
|
+
wf_extract "$wf" > "$T/wf"
|
|
187
|
+
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="$(printf '\037')" read -r job jname sname cmd; do
|
|
189
|
+
skip_cmd "$cmd" && continue
|
|
190
|
+
deploy_step "$job" "$jname" "$sname" "$cmd" && continue
|
|
191
|
+
g=$(tool_globs "$cmd")
|
|
192
|
+
[ -n "$g" ] || g="$paths"
|
|
193
|
+
[ -n "$g" ] || g="**"
|
|
194
|
+
row "$wf#jobs.$job" "." "$cmd" "$g"
|
|
195
|
+
done
|
|
196
|
+
done
|
|
197
|
+
|
|
198
|
+
MAP=$(jq -R -s -c '
|
|
199
|
+
[split("\n")[] | select(length > 0) | split("\t") | {source: .[0], cwd: .[1], command: .[2], globs: (.[3] | split("\u001f") | map(select(length > 0)))}]
|
|
200
|
+
| sort_by(.source, .command)
|
|
201
|
+
| reduce .[] as $e ({order: [], by: {}};
|
|
202
|
+
([$e.command, $e.cwd] | tojson) as $k
|
|
203
|
+
| if .by[$k] then .by[$k].globs += ($e.globs - .by[$k].globs)
|
|
204
|
+
else .order += [$k] | .by[$k] = {globs: $e.globs, command: $e.command, cwd: $e.cwd, source: $e.source} end)
|
|
205
|
+
| {version: 1, entries: [.by[.order[]]]}' < "$T/rows") || die "jq failed"
|
|
206
|
+
|
|
207
|
+
if [ "$STDOUT" = 1 ]; then printf '%s\n' "$MAP"; exit 0; fi
|
|
208
|
+
[ -n "$OUT" ] || OUT=".forge/test-map.json"
|
|
209
|
+
mkdir -p "$(dirname "$OUT")" || die "cannot create $(dirname "$OUT")"
|
|
210
|
+
printf '%s\n' "$MAP" > "$OUT.tmp.$$" && mv "$OUT.tmp.$$" "$OUT" || die "cannot write $OUT"
|
|
211
|
+
echo "$OUT"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-map.test.sh — cases for scripts/test-map.sh over the node, python and go fixtures (forge#4272).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
S="$HERE/test-map.sh"
|
|
8
|
+
SEL="$HERE/../bin/lean/testmap.mjs"
|
|
9
|
+
FIX="$HERE/fixtures/test-map"
|
|
10
|
+
PASS=0; FAILN=0
|
|
11
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/test-map-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
|
|
12
|
+
ok() { PASS=$((PASS+1)); }
|
|
13
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
14
|
+
eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
|
|
15
|
+
|
|
16
|
+
# copy a fixture into a fresh git repo and build its map
|
|
17
|
+
setup() {
|
|
18
|
+
rm -rf "$TMP/$1"; cp -R "$FIX/$1" "$TMP/$1"; cd "$TMP/$1" || exit 1
|
|
19
|
+
git init -q -b main; git config user.email t@example.com; git config user.name t; git config commit.gpgsign false
|
|
20
|
+
git add -A; git commit -qm init
|
|
21
|
+
OUTP="$(bash "$S")"; RC=$?
|
|
22
|
+
}
|
|
23
|
+
# selected commands for changed files, sorted, joined by |
|
|
24
|
+
sel() { node "$SEL" --map .forge/test-map.json --select "$@" | LC_ALL=C sort | paste -sd'|' -; }
|
|
25
|
+
|
|
26
|
+
# ---- 1. node: root scripts for root files, workspace scripts for package files ----
|
|
27
|
+
setup node
|
|
28
|
+
eq "node rc" "$RC" 0
|
|
29
|
+
eq "node output path" "$OUTP" ".forge/test-map.json"
|
|
30
|
+
eq "schema" "$(jq -c '[.version, (.entries | length > 0), (.entries | all(has("globs") and has("command") and has("cwd") and has("source")))]' .forge/test-map.json)" '[1,true,true]'
|
|
31
|
+
eq "non-check script ignored" "$(jq '[.entries[] | select(.command | test("build"))] | length' .forge/test-map.json)" 0
|
|
32
|
+
eq "root file" "$(sel src/a.ts)" "pnpm run lint|pnpm run test"
|
|
33
|
+
eq "package file" "$(sel packages/ui/src/button.tsx)" "cd packages/ui && pnpm run test|cd packages/ui && pnpm run typecheck|pnpm run lint|pnpm run test"
|
|
34
|
+
eq "vitest config noted" "$(jq -r '.entries[] | select(.cwd == "." and .command == "pnpm run test") | .source' .forge/test-map.json)" "package.json#scripts.test (vitest/jest config)"
|
|
35
|
+
eq "unrelated file" "$(sel README.md)" ""
|
|
36
|
+
|
|
37
|
+
# ---- 2. python: pytest + ruff through uv ----
|
|
38
|
+
setup python
|
|
39
|
+
eq "python rc" "$RC" 0
|
|
40
|
+
eq "python file" "$(sel pkg/mod.py)" "uv run pytest|uv run ruff check ."
|
|
41
|
+
eq "python test file" "$(sel tests/test_mod.py)" "uv run pytest|uv run ruff check ."
|
|
42
|
+
eq "python non-py" "$(sel uv.lock)" ""
|
|
43
|
+
|
|
44
|
+
# ---- 3. go: go.mod, Makefile and CI workflow steps; CI-only lint step; install lines skipped ----
|
|
45
|
+
setup go
|
|
46
|
+
eq "go rc" "$RC" 0
|
|
47
|
+
eq "go file" "$(sel main.go)" "./scripts/check-docs|go test ./...|go vet ./...|golangci-lint run ./...|make test"
|
|
48
|
+
eq "nested go file" "$(sel internal/util/util.go)" "./scripts/check-docs|go test ./...|go vet ./...|golangci-lint run ./...|make test"
|
|
49
|
+
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
|
+
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 == "./scripts/check-docs") | .globs' .forge/test-map.json)" '["**.go","go.mod"]'
|
|
52
|
+
eq "docs file" "$(sel README.md)" "make test"
|
|
53
|
+
eq "go.sum" "$(sel go.sum)" "go test ./...|go vet ./...|make test"
|
|
54
|
+
|
|
55
|
+
# ---- 4. regenerable: a rerun is byte-identical; --stdout writes nothing; --output ----
|
|
56
|
+
cp .forge/test-map.json "$TMP/first.json"
|
|
57
|
+
bash "$S" >/dev/null
|
|
58
|
+
if cmp -s .forge/test-map.json "$TMP/first.json"; then ok; else bad "rerun not byte-identical"; fi
|
|
59
|
+
rm -rf .forge; OUTS="$(bash "$S" --stdout)"
|
|
60
|
+
eq "stdout same map" "$OUTS" "$(cat "$TMP/first.json")"
|
|
61
|
+
eq "stdout writes nothing" "$([ -e .forge ] && echo yes || echo no)" no
|
|
62
|
+
bash "$S" --repo-path "$TMP/go" --output "$TMP/out/map.json" >/dev/null
|
|
63
|
+
if cmp -s "$TMP/out/map.json" "$TMP/first.json"; then ok; else bad "--output differs"; fi
|
|
64
|
+
|
|
65
|
+
# ---- 5. block runs, continuation lines, setup lines, workflow without paths ----
|
|
66
|
+
mkdir -p "$TMP/wf/.github/workflows"; cd "$TMP/wf" || exit 1
|
|
67
|
+
git init -q -b main
|
|
68
|
+
cat > .github/workflows/lint.yaml <<'EOF'
|
|
69
|
+
on: push
|
|
70
|
+
jobs:
|
|
71
|
+
checks:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- run: npm ci
|
|
75
|
+
- run: "shellcheck scripts/*.sh"
|
|
76
|
+
- run: >-
|
|
77
|
+
ruff check
|
|
78
|
+
--fix
|
|
79
|
+
- run: |
|
|
80
|
+
echo hi
|
|
81
|
+
bash scripts/verify.sh \
|
|
82
|
+
--strict
|
|
83
|
+
./custom-check
|
|
84
|
+
EOF
|
|
85
|
+
git add -A
|
|
86
|
+
OUTS="$(bash "$S" --stdout)"
|
|
87
|
+
eq "wf commands" "$(printf '%s' "$OUTS" | jq -r '[.entries[].command] | sort | join("|")')" "./custom-check|bash scripts/verify.sh --strict|ruff check --fix|shellcheck scripts/*.sh"
|
|
88
|
+
eq "wf globs" "$(printf '%s' "$OUTS" | jq -c '[.entries[] | {(.command): .globs}] | add')" '{"./custom-check":["**"],"bash scripts/verify.sh --strict":["**/*.sh"],"ruff check --fix":["**/*.py"],"shellcheck scripts/*.sh":["**/*.sh"]}'
|
|
89
|
+
|
|
90
|
+
# ---- 5b. forge#4297: `- run: |` then env:/shell: ends the block; deploy/publish/release/push/secret steps dropped ----
|
|
91
|
+
mkdir -p "$TMP/wf2/.github/workflows"; cd "$TMP/wf2" || exit 1
|
|
92
|
+
git init -q -b main
|
|
93
|
+
cat > .github/workflows/ci.yml <<'EOF'
|
|
94
|
+
on:
|
|
95
|
+
push:
|
|
96
|
+
branches: [main]
|
|
97
|
+
jobs:
|
|
98
|
+
test:
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
steps:
|
|
101
|
+
- run: |
|
|
102
|
+
npm test
|
|
103
|
+
env:
|
|
104
|
+
CI: true
|
|
105
|
+
shell: bash
|
|
106
|
+
- name: Lint
|
|
107
|
+
run: |
|
|
108
|
+
npm run lint
|
|
109
|
+
working-directory: .
|
|
110
|
+
- name: Publish to npm
|
|
111
|
+
run: npm run ship
|
|
112
|
+
- run: ./scripts/release.sh
|
|
113
|
+
- run: git push origin HEAD
|
|
114
|
+
- run: curl -H "token ${{ secrets.TOKEN }}" https://x
|
|
115
|
+
- run: >-
|
|
116
|
+
tsc
|
|
117
|
+
--noEmit
|
|
118
|
+
shell: bash
|
|
119
|
+
deploy:
|
|
120
|
+
name: Ship it
|
|
121
|
+
runs-on: ubuntu-latest
|
|
122
|
+
steps:
|
|
123
|
+
- run: ./ship.sh
|
|
124
|
+
site:
|
|
125
|
+
name: Deploy site
|
|
126
|
+
runs-on: ubuntu-latest
|
|
127
|
+
steps:
|
|
128
|
+
- run: ./build-site.sh
|
|
129
|
+
EOF
|
|
130
|
+
git add -A
|
|
131
|
+
OUTS="$(bash "$S" --stdout)"
|
|
132
|
+
eq "wf2 commands" "$(printf '%s' "$OUTS" | jq -r '[.entries[].command] | sort | join("|")')" "npm run lint|npm test|tsc --noEmit"
|
|
133
|
+
|
|
134
|
+
# ---- 5c. forge#4298: a package dir with a space and a non-ASCII name keeps whole globs; workflow paths with spaces ----
|
|
135
|
+
mkdir -p "$TMP/sp/web app" "$TMP/sp/pkg/ü/src" "$TMP/sp/.github/workflows"; cd "$TMP/sp" || exit 1
|
|
136
|
+
git init -q -b main
|
|
137
|
+
echo '{"name":"root","private":true}' > package.json
|
|
138
|
+
echo '{"name":"web","scripts":{"test":"vitest run"}}' > "web app/package.json"
|
|
139
|
+
echo '{"name":"u","scripts":{"lint":"eslint ."}}' > "pkg/ü/package.json"
|
|
140
|
+
echo 'x' > "pkg/ü/src/a.ts"
|
|
141
|
+
cat > .github/workflows/ci.yml <<'EOF'
|
|
142
|
+
on:
|
|
143
|
+
push:
|
|
144
|
+
paths:
|
|
145
|
+
- "docs site/**"
|
|
146
|
+
jobs:
|
|
147
|
+
docs:
|
|
148
|
+
runs-on: ubuntu-latest
|
|
149
|
+
steps:
|
|
150
|
+
- run: ./build-docs.sh
|
|
151
|
+
EOF
|
|
152
|
+
git add -A
|
|
153
|
+
OUTS="$(bash "$S" --stdout)"
|
|
154
|
+
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
|
+
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 == "./build-docs.sh") | .globs]')" '[["docs site/**"]]'
|
|
157
|
+
printf '%s' "$OUTS" > "$TMP/sp-map.json"
|
|
158
|
+
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
|
+
|
|
160
|
+
# ---- 6. usage ----
|
|
161
|
+
bash "$S" --repo-path "$TMP/missing" >/dev/null 2>&1; eq "bad repo path" "$?" 2
|
|
162
|
+
bash "$S" --nope >/dev/null 2>&1; eq "unknown option" "$?" 2
|
|
163
|
+
|
|
164
|
+
echo "test-map.test.sh: $PASS passed, $FAILN failed"
|
|
165
|
+
[ "$FAILN" -eq 0 ]
|
package/scripts/wait-ci-green.sh
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
#
|
|
5
5
|
# wait-ci-green.sh — CI gate every autonomous merge must pass first.
|
|
6
6
|
#
|
|
7
|
-
# Usage: wait-ci-green.sh <pr-number> -R <owner/repo> [--timeout SECONDS] [--interval SECONDS]
|
|
7
|
+
# Usage: wait-ci-green.sh <pr-number> -R <owner/repo> [--timeout SECONDS] [--interval SECONDS] [--ignore-check NAME]...
|
|
8
|
+
#
|
|
9
|
+
# --ignore-check NAME (repeatable): drop checks with exactly that name before deciding. `forgedock/verify` (forge#4270,
|
|
10
|
+
# the lean engine's report-only gate summary) is always ignored, so every caller of this gate treats it the same
|
|
11
|
+
# (forge#4298); requiring it is the repository's branch-protection choice.
|
|
8
12
|
#
|
|
9
13
|
# Waits until every check on the PR has finished, then decides. Fails closed: a merge may
|
|
10
14
|
# proceed ONLY on exit 0. Field test: pipeline agents merged PRs to staging while checks were
|
|
@@ -40,17 +44,18 @@ set -uo pipefail
|
|
|
40
44
|
_TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
|
|
41
45
|
type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
|
|
42
46
|
|
|
43
|
-
PR=""; REPO=""
|
|
47
|
+
PR=""; REPO=""; IGNORE='["forgedock/verify"]'
|
|
44
48
|
TIMEOUT="${FORGE_CI_TIMEOUT:-540}"; INTERVAL="${FORGE_CI_INTERVAL:-20}"; SETTLE="${FORGE_CI_SETTLE:-45}"
|
|
45
49
|
GRACE="${FORGE_CI_NO_CHECKS_GRACE:-120}"; REQUIRE="${FORGE_CI_REQUIRE_CHECKS:-0}"
|
|
46
|
-
usage() { echo "CI_GATE: ERROR"; echo "CI_GATE_HEAD: unknown"; echo "usage: wait-ci-green.sh <pr> -R <owner/repo> [--timeout S] [--interval S]" >&2; exit 2; }
|
|
50
|
+
usage() { echo "CI_GATE: ERROR"; echo "CI_GATE_HEAD: unknown"; echo "usage: wait-ci-green.sh <pr> -R <owner/repo> [--timeout S] [--interval S] [--ignore-check NAME]..." >&2; exit 2; }
|
|
47
51
|
while [ $# -gt 0 ]; do
|
|
48
52
|
case "$1" in
|
|
49
53
|
-R) [ $# -ge 2 ] || usage; REPO="$2"; shift 2 ;;
|
|
50
54
|
-R*) REPO="${1#-R}"; REPO="${REPO# }"; shift ;;
|
|
51
55
|
--timeout) [ $# -ge 2 ] || usage; TIMEOUT="$2"; shift 2 ;;
|
|
52
56
|
--interval) [ $# -ge 2 ] || usage; INTERVAL="$2"; shift 2 ;;
|
|
53
|
-
-
|
|
57
|
+
--ignore-check) [ $# -ge 2 ] || usage; IGNORE="$(jq -c --arg n "$2" '. + [$n]' <<<"$IGNORE")"; shift 2 ;;
|
|
58
|
+
-h|--help) sed -n '5,37p' "$0"; exit 0 ;;
|
|
54
59
|
*) [ -z "$PR" ] || usage; PR="$1"; shift ;;
|
|
55
60
|
esac
|
|
56
61
|
done
|
|
@@ -132,6 +137,7 @@ while :; do
|
|
|
132
137
|
fi
|
|
133
138
|
fi
|
|
134
139
|
errors=0; limited=0; transient=0
|
|
140
|
+
[ "$IGNORE" = '[]' ] || JSON="$(printf '%s' "$JSON" | jq -c --argjson ig "$IGNORE" '[.[] | select(.name as $n | $ig | index($n) | not)]')"
|
|
135
141
|
cur="$(head_sha)"
|
|
136
142
|
if [ -n "$cur" ] && [ "$cur" != "$START_SHA" ]; then emit ERROR; echo "PR head moved during the wait ($START_SHA -> $cur); re-review the new head"; exit 2; fi
|
|
137
143
|
total=$(printf '%s' "$JSON" | jq 'length')
|
|
@@ -55,6 +55,15 @@ expect "pending then pass" "0|CI_GATE: PASS" "$(run "$(mk pend '[{"name":"a","bu
|
|
|
55
55
|
expect "pending then fail" "1|CI_GATE: FAIL" "$(run "$(mk pendfail '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"fail"}]')")"
|
|
56
56
|
expect "pending past timeout" "3|CI_GATE: TIMEOUT" "$(run "$(mk slow '[{"name":"a","bucket":"pending"}]')" --timeout 2)"
|
|
57
57
|
expect "fail wins over pending" "1|CI_GATE: FAIL" "$(run "$(mk mixed '[{"name":"a","bucket":"pending"},{"name":"b","bucket":"fail"}]')")"
|
|
58
|
+
# forge#4270: --ignore-check drops the named check (the lane's own report-only forgedock/verify) before deciding.
|
|
59
|
+
OWN='[{"name":"a","bucket":"pass"},{"name":"forgedock/verify","bucket":"fail"}]'
|
|
60
|
+
# forge#4298: forgedock/verify is ignored by default, so every caller (legacy engine, review-finalize, spec gates) agrees.
|
|
61
|
+
expect "own verify check failing is ignored by default" "0|CI_GATE: PASS" "$(run "$(mk ownfail "$OWN")")"
|
|
62
|
+
expect "default ignore keeps other failures" "1|CI_GATE: FAIL" "$(run "$(mk owndef '[{"name":"b","bucket":"fail"},{"name":"forgedock/verify","bucket":"fail"}]')")"
|
|
63
|
+
expect "usage text lists --ignore-check" "1" "$(bash "$GATE" 2>&1 >/dev/null | grep -c -- '--ignore-check NAME')"
|
|
64
|
+
expect "--ignore-check drops the named failing check" "0|CI_GATE: PASS" "$(run "$(mk ownign "$OWN")" --ignore-check forgedock/verify)"
|
|
65
|
+
expect "--ignore-check keeps other failures" "1|CI_GATE: FAIL" "$(run "$(mk ownother '[{"name":"b","bucket":"fail"},{"name":"forgedock/verify","bucket":"pass"}]')" --ignore-check forgedock/verify)"
|
|
66
|
+
expect "--ignore-check without a name is a usage error" "2|CI_GATE: ERROR" "$(run "$(mk ownusage "$P")" --ignore-check)"
|
|
58
67
|
expect "transient outage then pass" "0|CI_GATE: PASS" "$(run "$(mk blip ERR "$P")")"
|
|
59
68
|
expect "persistent unclassified failure is an ERROR" "2|CI_GATE: ERROR" "$(run "$(mk down ERRX)" --timeout 60)"
|
|
60
69
|
# Rate-limit / 5xx classification: retried, never an ERROR; a window that only saw them ends RATE_LIMITED (rc 4).
|