forgedock 1.28.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/outcome-codes.mjs +29 -0
- package/bin/engine-cli.mjs +77 -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 +356 -0
- package/bin/lean/context.test.mjs +307 -0
- package/bin/lean/effects.mjs +19 -1
- package/bin/lean/lean.mjs +178 -37
- package/bin/lean/lean.test.mjs +441 -7
- package/bin/lean/orchestrate.mjs +112 -5
- package/bin/lean/prefilter.mjs +81 -16
- package/bin/lean/prefilter.test.mjs +91 -2
- package/bin/lean/prompts.mjs +22 -7
- package/bin/lean/recovery.test.mjs +419 -0
- package/bin/lean/render.mjs +38 -5
- package/bin/lean/testmap.mjs +101 -0
- package/bin/lean/testmap.test.mjs +73 -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/deploy-pr.md +5 -4
- package/commands/orchestrate.md +54 -7
- package/commands/review-pr-staging.md +6 -1
- 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/code-index.sh +33 -33
- package/scripts/code-index.test.sh +21 -2
- package/scripts/fixtures/test-map/go/.github/workflows/ci.yml +25 -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/review-prepare.sh +28 -6
- package/scripts/review-prepare.test.sh +22 -2
- package/scripts/test-map.sh +317 -0
- package/scripts/test-map.test.sh +353 -0
- package/scripts/verify-phase-trail.test.sh +44 -44
- package/scripts/wait-ci-green.sh +10 -4
- package/scripts/wait-ci-green.test.sh +13 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# repo-facts.test.sh — cases for scripts/repo-facts.sh (forge#4272).
|
|
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)/repo-facts.sh"
|
|
7
|
+
PASS=0; FAILN=0
|
|
8
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/repo-facts-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
|
|
9
|
+
ok() { PASS=$((PASS+1)); }
|
|
10
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
11
|
+
eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
|
|
12
|
+
q() { printf '%s' "$OUT" | jq -c "$1"; }
|
|
13
|
+
|
|
14
|
+
new_repo() {
|
|
15
|
+
rm -rf "${TMP:?}/$1"; mkdir -p "$TMP/$1"; cd "$TMP/$1" || exit 1
|
|
16
|
+
git init -q -b main; git config user.email t@example.com; git config user.name t; git config commit.gpgsign false
|
|
17
|
+
}
|
|
18
|
+
commit() { git add -A; git commit -qm "$1"; }
|
|
19
|
+
lines() { i=0; : > "$1"; while [ $i -lt "$2" ]; do echo "l$i" >> "$1"; i=$((i+1)); done; }
|
|
20
|
+
|
|
21
|
+
# ---- 1. pins, slow suites, package edge, boundary config, hotspot ----
|
|
22
|
+
new_repo full
|
|
23
|
+
mkdir -p packages/a packages/b .github/workflows src
|
|
24
|
+
cat > package.json <<'EOF'
|
|
25
|
+
{"name":"root","private":true,"packageManager":"pnpm@9.1.0","workspaces":["packages/*"],
|
|
26
|
+
"scripts":{"test":"vitest run","lint":"eslint .","test:e2e":"playwright test"}}
|
|
27
|
+
EOF
|
|
28
|
+
echo "20.11.1" > .nvmrc
|
|
29
|
+
echo '{"name":"@x/a","dependencies":{"@x/b":"workspace:*","left-pad":"1.0.0"}}' > packages/a/package.json
|
|
30
|
+
echo '{"name":"@x/b","scripts":{"test":"vitest run"}}' > packages/b/package.json
|
|
31
|
+
echo "module.exports = { rules: { 'boundaries/element-types': ['error'] } };" > .eslintrc.js
|
|
32
|
+
echo "module.exports = { rules: { semi: 'error' } };" > packages/b/.eslintrc.js
|
|
33
|
+
cat > .github/workflows/ci.yml <<'EOF'
|
|
34
|
+
on: push
|
|
35
|
+
jobs:
|
|
36
|
+
db:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
services:
|
|
39
|
+
postgres:
|
|
40
|
+
image: postgres
|
|
41
|
+
steps:
|
|
42
|
+
- run: pnpm run test
|
|
43
|
+
long:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
timeout-minutes: 45
|
|
46
|
+
steps:
|
|
47
|
+
- run: pytest tests/slow
|
|
48
|
+
quick:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
timeout-minutes: 5
|
|
51
|
+
steps:
|
|
52
|
+
- run: ruff check .
|
|
53
|
+
EOF
|
|
54
|
+
lines src/big.ts 2100; commit c1
|
|
55
|
+
echo x >> src/big.ts; lines src/small.ts 10; commit c2
|
|
56
|
+
OUT="$(bash "$S" --stdout)"; rc=$?
|
|
57
|
+
eq "rc" "$rc" 0
|
|
58
|
+
eq "version" "$(q '.version')" 1
|
|
59
|
+
eq "pm pin" "$(q '.toolchain.pm')" '{"name":"pnpm","version":"9.1.0","source":"package.json packageManager"}'
|
|
60
|
+
eq "node pin" "$(q '.toolchain.node')" '{"spec":"20.11.1","source":".nvmrc"}'
|
|
61
|
+
eq "packages" "$(q '.layer_rules.packages')" '[{"name":"@x/a","dir":"packages/a"},{"name":"@x/b","dir":"packages/b"}]'
|
|
62
|
+
eq "edge" "$(q '.layer_rules.edges')" '[{"from":"@x/a","to":"@x/b","kind":"dependencies","manifest":"packages/a/package.json"}]'
|
|
63
|
+
eq "boundary configs" "$(q '.layer_rules.boundary_configs')" '[".eslintrc.js"]'
|
|
64
|
+
eq "services job" "$(q '[.slow_suites[] | select(.command == "pnpm run test") | .why]')" '["CI job .github/workflows/ci.yml#jobs.db uses services"]'
|
|
65
|
+
eq "timeout job" "$(q '[.slow_suites[] | select(.command == "pytest tests/slow") | .why]')" '["CI job .github/workflows/ci.yml#jobs.long sets timeout-minutes 45"]'
|
|
66
|
+
eq "quick job not slow" "$(q '[.slow_suites[] | select(.command == "ruff check .")] | length')" 0
|
|
67
|
+
eq "hotspot" "$(q '.hotspots')" '[{"path":"src/big.ts","lines":2101,"commits_90d":2,"hotspot":4202}]'
|
|
68
|
+
|
|
69
|
+
# ---- 2. a cached test map is read: its e2e command is a slow suite ----
|
|
70
|
+
mkdir -p .forge
|
|
71
|
+
echo '{"version":1,"entries":[{"globs":["e2e/**"],"command":"pnpm exec playwright test","cwd":".","source":"package.json#scripts.test:e2e"}]}' > .forge/test-map.json
|
|
72
|
+
OUT="$(bash "$S" --stdout)"
|
|
73
|
+
eq "map e2e" "$(q '.slow_suites')" '[{"command":"pnpm exec playwright test","source":"package.json#scripts.test:e2e","why":"command names playwright"}]'
|
|
74
|
+
|
|
75
|
+
# ---- 3. default output file, rerun stable ----
|
|
76
|
+
P="$(bash "$S")"; eq "output path" "$P" ".forge/repo-facts.json"
|
|
77
|
+
cp .forge/repo-facts.json "$TMP/first.json"; bash "$S" >/dev/null
|
|
78
|
+
if cmp -s .forge/repo-facts.json "$TMP/first.json"; then ok; else bad "rerun not identical"; fi
|
|
79
|
+
|
|
80
|
+
# ---- 4. a repo with none of these: empty sections, exit 0 ----
|
|
81
|
+
new_repo bare
|
|
82
|
+
echo hi > README.md; commit init
|
|
83
|
+
OUT="$(bash "$S" --stdout)"; rc=$?
|
|
84
|
+
eq "bare rc" "$rc" 0
|
|
85
|
+
eq "bare" "$(q '[.toolchain.pm, .toolchain.node, .slow_suites, .layer_rules, .hotspots]')" '[null,null,[],{"packages":[],"edges":[],"boundary_configs":[]},[]]'
|
|
86
|
+
new_repo empty
|
|
87
|
+
OUT="$(bash "$S" --stdout)"; eq "no commits rc" "$?" 0
|
|
88
|
+
eq "no commits" "$(q '.hotspots')" '[]'
|
|
89
|
+
|
|
90
|
+
# ---- 4b. #4298: a big boundary config matching early (pipefail + grep -q SIGPIPE) and non-ASCII / spaced paths ----
|
|
91
|
+
new_repo paths
|
|
92
|
+
mkdir -p "packages/é b" "apps/ü"
|
|
93
|
+
echo '{"name":"root","private":true,"workspaces":["packages/*"]}' > package.json
|
|
94
|
+
echo '{"name":"@x/c"}' > "packages/é b/package.json"
|
|
95
|
+
{ echo "export default [{ rules: { 'boundaries/element-types': ['error'] } }];"; i=0; while [ $i -lt 20000 ]; do echo "// padding line $i to overflow the pipe buffer ......................................"; i=$((i+1)); done; } > "apps/ü/eslint.config.js"
|
|
96
|
+
commit init
|
|
97
|
+
OUT="$(bash "$S" --stdout)"; eq "paths rc" "$?" 0
|
|
98
|
+
eq "big boundary config in a non-ASCII dir" "$(q '.layer_rules.boundary_configs')" '["apps/ü/eslint.config.js"]'
|
|
99
|
+
eq "spaced non-ASCII package dir" "$(q '.layer_rules.packages')" '[{"name":"@x/c","dir":"packages/é b"}]'
|
|
100
|
+
|
|
101
|
+
# ---- 5. usage ----
|
|
102
|
+
bash "$S" --repo-path "$TMP/missing" >/dev/null 2>&1; eq "bad repo path" "$?" 2
|
|
103
|
+
bash "$S" --bogus >/dev/null 2>&1; eq "unknown option" "$?" 2
|
|
104
|
+
|
|
105
|
+
echo "repo-facts.test.sh: $PASS passed, $FAILN failed"
|
|
106
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -87,15 +87,23 @@ case "$MERGE_ISSUE" in *[!0-9]*) MERGE_ISSUE="" ;; esac
|
|
|
87
87
|
[ "$THOROUGH" = "true" ] || THOROUGH="false"
|
|
88
88
|
|
|
89
89
|
# --- trusted helper resolution (install roots only; never cwd) ---
|
|
90
|
+
# shellcheck source=/dev/null # the resolver lib is found at runtime in the trusted install root
|
|
90
91
|
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; _SELF_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd -P)"; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" "${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/* "$_SELF_ROOT"; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
91
92
|
|
|
92
93
|
ROSTER_SCRIPT=$(forge_resolve review-roster.sh)
|
|
93
94
|
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
95
|
+
# shellcheck disable=SC2034 # read by lib/review-delta-mode.sh (sourced below)
|
|
94
96
|
REVIEW_DELTA_SCRIPT=$(forge_resolve review-delta.sh)
|
|
95
97
|
CODE_INDEX_SCRIPT=$(forge_resolve code-index.sh)
|
|
96
|
-
_l=$(forge_resolve lib/pr-issue-number.sh)
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
_l=$(forge_resolve lib/pr-issue-number.sh)
|
|
99
|
+
# shellcheck source=/dev/null # resolved at runtime from the trusted install root
|
|
100
|
+
if [ -n "$_l" ]; then . "$_l"; else pr_issue_number() { return 0; }; fi
|
|
101
|
+
_l=$(forge_resolve lib/review-template-resolve.sh)
|
|
102
|
+
# shellcheck source=/dev/null # resolved at runtime from the trusted install root
|
|
103
|
+
if [ -n "$_l" ]; then . "$_l"; else review_template_resolve() { TEMPLATE_BASE=""; TEMPLATE_SOURCE="none"; MONOLITHIC_CATALOG=""; }; fi
|
|
104
|
+
_l=$(forge_resolve lib/review-delta-mode.sh)
|
|
105
|
+
# shellcheck source=/dev/null disable=SC2329 # resolved at runtime; the fallback stubs mirror the lib's API
|
|
106
|
+
if [ -n "$_l" ]; then . "$_l"; else review_delta_mode() { echo "FULL - 0"; }; carry_verified_from() { :; }; fi
|
|
99
107
|
|
|
100
108
|
# --- state, scratch and the ref-restore guard ---
|
|
101
109
|
BLOCKERS=""
|
|
@@ -105,12 +113,14 @@ mkdir -p "$OUT_DIR" 2>/dev/null || { echo "review-prepare: cannot create $OUT_DI
|
|
|
105
113
|
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/review-prepare.XXXXXX") || exit 2
|
|
106
114
|
ORIG_REF=""; CHECKED_OUT=""
|
|
107
115
|
# restore_ref: put the checkout back exactly where it was. -f so a dirty review checkout can never wedge the restore.
|
|
116
|
+
# shellcheck disable=SC2329 # called from cleanup (EXIT trap) and the checkout paths
|
|
108
117
|
restore_ref() {
|
|
109
118
|
if [ -n "$CHECKED_OUT" ] && [ -n "$ORIG_REF" ]; then
|
|
110
119
|
git checkout -f "$ORIG_REF" >/dev/null 2>&1 || echo "review-prepare: WARNING could not restore $ORIG_REF" >&2
|
|
111
120
|
fi
|
|
112
121
|
CHECKED_OUT=""
|
|
113
122
|
}
|
|
123
|
+
# shellcheck disable=SC2329 # invoked by the EXIT trap
|
|
114
124
|
cleanup() { restore_ref; rm -rf "$TMP_DIR"; }
|
|
115
125
|
trap cleanup EXIT
|
|
116
126
|
|
|
@@ -144,6 +154,7 @@ if [ -z "$BLOCKERS" ]; then
|
|
|
144
154
|
# Pinned (forge#3935): same-repo PRs only (a fork's branch name resolves to origin's own branch), and the fetched tip must equal REVIEW_SHA; the diff is taken against REVIEW_SHA, never the branch name.
|
|
145
155
|
if { [ "$NO_DELTA" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; } && [ -n "$BASE" ] && [ -n "$HEAD" ] && [ "$(printf '%s' "$PR_INFO" | jq -r .isCrossRepository 2>/dev/null)" = "false" ] && printf '%s' "$REVIEW_SHA" | grep -qE '^[0-9a-f]{40}$' \
|
|
146
156
|
&& git fetch -q origin "$BASE" "$HEAD" 2>/dev/null && [ "$(git rev-parse -q --verify "refs/remotes/origin/${HEAD}^{commit}" 2>/dev/null)" = "$REVIEW_SHA" ]; then
|
|
157
|
+
# shellcheck disable=SC2015 # either assignment failing resets both, by design
|
|
147
158
|
FILES=$(git diff --name-only "origin/${BASE}...${REVIEW_SHA}" 2>/dev/null) && DIFF=$(git diff "origin/${BASE}...${REVIEW_SHA}" 2>/dev/null) || { FILES=""; DIFF=""; }
|
|
148
159
|
fi
|
|
149
160
|
[ -n "$FILES" ] || block "gh pr diff failed"
|
|
@@ -420,7 +431,9 @@ canonical_roster() { local out="" a d; for a in $1; do d=$(agent_domain "$a"); c
|
|
|
420
431
|
SELECTED_AGENTS=$(canonical_roster "$SELECTED_AGENTS")
|
|
421
432
|
|
|
422
433
|
# --- Phase 3C: review mode (FULL | DELTA | BASE_SYNC_ONLY) ---
|
|
434
|
+
# shellcheck disable=SC2034 # both read by lib/review-delta-mode.sh
|
|
423
435
|
BASE_BRANCH="$BASE"
|
|
436
|
+
# shellcheck disable=SC2034
|
|
424
437
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
425
438
|
delta_roster() {
|
|
426
439
|
local rounds="${1}" full="${2}" files="${3}" out="security" root
|
|
@@ -473,11 +486,20 @@ if [ "$REVIEW_MODE" != "BASE_SYNC_ONLY" ]; then
|
|
|
473
486
|
_ito="${REVIEW_PREPARE_INDEX_TIMEOUT:-120}"; case "$_ito" in ''|*[!0-9]*|0) _ito=120 ;; esac
|
|
474
487
|
_tbin=$(command -v timeout || command -v gtimeout || true)
|
|
475
488
|
echo "review-prepare: building code index (limit ${_ito}s)" >&2
|
|
476
|
-
|
|
477
|
-
|
|
489
|
+
# forge#4235: keep the build's stderr so a failure is diagnosable (its tail is echoed below); stdout stays discarded.
|
|
490
|
+
_ierr=$(mktemp "$OUT_DIR/.code-index-err-XXXXXX" 2>/dev/null) || _ierr=/dev/null
|
|
491
|
+
if [ -n "$_tbin" ]; then "$_tbin" "$_ito" bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" >/dev/null 2>"$_ierr"; _irc=$?
|
|
492
|
+
else bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" >/dev/null 2>"$_ierr"; _irc=$?; fi
|
|
478
493
|
if [ "$_irc" -eq 0 ]; then INDEX_OK=1
|
|
479
494
|
elif [ "$_irc" -eq 124 ]; then echo "review-prepare: code index timed out after ${_ito}s — skipping index slices (agents grep instead)" >&2
|
|
480
|
-
else
|
|
495
|
+
else
|
|
496
|
+
echo "review-prepare: code index failed (exit $_irc) — skipping index slices (agents grep instead)" >&2
|
|
497
|
+
# forge#4316: the tail can name repo files; show the repo root as <repo>, strip control chars, cap each line
|
|
498
|
+
[ -s "$_ierr" ] && tail -n 20 "$_ierr" | tr -d '\000-\010\013-\037\177' \
|
|
499
|
+
| awk -v r="$REPO_PATH" '{ l = $0; if (r != "") { o = ""; while ((i = index(l, r)) > 0) { o = o substr(l, 1, i - 1) "<repo>"; l = substr(l, i + length(r)) } l = o l } print substr(l, 1, 200) }' \
|
|
500
|
+
| sed 's/^/review-prepare: code-index: /' >&2
|
|
501
|
+
fi
|
|
502
|
+
[ "$_ierr" = /dev/null ] || rm -f "$_ierr"
|
|
481
503
|
fi
|
|
482
504
|
build_index_slice() {
|
|
483
505
|
local domain="${1}" files
|
|
@@ -109,7 +109,9 @@ setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'CONFLICTING|DIRTY\n' > "
|
|
|
109
109
|
prep "$ROOT"; eq "conflicting" .merge_health.mergeable CONFLICTING; eq "conflicting state" .merge_health.state DIRTY; eq "no retry on CONFLICTING" .merge_health.retries 0
|
|
110
110
|
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'UNKNOWN|UNKNOWN\n' > "$FIX/merge.seq"
|
|
111
111
|
prep "$ROOT"; eq "unknown stays unknown after 3 retries" .merge_health.retries 3; eq "still UNKNOWN" .merge_health.mergeable UNKNOWN
|
|
112
|
-
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
112
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
113
|
+
# shellcheck disable=SC2016 # the literal $(rm -rf) is the hostile payload under test
|
|
114
|
+
printf 'garbage|$(rm -rf)\n' > "$FIX/merge.seq"
|
|
113
115
|
prep "$ROOT"; eq "garbage state normalized" .merge_health.state UNKNOWN
|
|
114
116
|
setup feat/x staging "README.md" "$DIFF_PLAIN"; echo 0 > "$FIX/prior"
|
|
115
117
|
prep "$ROOT"; eq "first-time contributor" .pr.first_time_contributor true
|
|
@@ -234,6 +236,7 @@ CFG_OVERRIDE="$WORK/emptyroot.yaml" prep "$WORK/root-notpl"
|
|
|
234
236
|
rc_is "no template source" 3; eq "templates none" .templates.source none
|
|
235
237
|
|
|
236
238
|
# --- 8. issue number extraction and trusted contract ---
|
|
239
|
+
# shellcheck source=/dev/null
|
|
237
240
|
. "$ROOT/scripts/lib/pr-issue-number.sh"
|
|
238
241
|
chk() { local got; got=$(pr_issue_number_of "$2"); if [ "$got" = "$3" ]; then ok; else bad "$1: got '$got', want '$3'"; fi; }
|
|
239
242
|
chk "Closes" "Closes #12" 12
|
|
@@ -271,6 +274,7 @@ setup feat/x staging "README.md" "$DIFF_PLAIN" "00000000000000000000000000000000
|
|
|
271
274
|
prep "$ROOT" --checkout-pr
|
|
272
275
|
rc_is "unknown sha cannot be checked out" 3
|
|
273
276
|
if [ "$(git -C "$REPO_DIR" symbolic-ref --short HEAD 2>/dev/null)" = "main" ]; then ok; else bad "failed checkout moved HEAD"; fi
|
|
277
|
+
# shellcheck disable=SC2016 # matches the literal $ORIG_REF in the script source
|
|
274
278
|
if grep -q 'checkout -f "\$ORIG_REF"' "$ROOT/scripts/review-prepare.sh"; then ok; else bad "restore_ref must use git checkout -f"; fi
|
|
275
279
|
|
|
276
280
|
# --- 10. fail closed on gh and bad arguments ---
|
|
@@ -317,8 +321,24 @@ eq "a hostile cwd roster script is never used" '.roster.agents | index("evil") !
|
|
|
317
321
|
# --- code index build is bounded and visible (forge#4126) ---
|
|
318
322
|
setup feat/x staging "services/api/auth.py" "$DIFF_AUTH_BILL"
|
|
319
323
|
prep "$ROOT"
|
|
320
|
-
eq "real index fills the auth slice" '.index_slices.auth | startswith("Domain files (auth):")' true
|
|
324
|
+
_f0=$fail; eq "real index fills the auth slice" '.index_slices.auth | startswith("Domain files (auth):")' true
|
|
325
|
+
# forge#4235: on failure show review-prepare's stderr (it carries the index build's exit code and stderr tail)
|
|
326
|
+
[ "$fail" -eq "$_f0" ] || sed 's/^/ stderr: /' "$WORK/stderr"
|
|
321
327
|
if grep -q "review-prepare: building code index" "$WORK/stderr"; then ok; else bad "index build must print a progress line on stderr"; fi
|
|
328
|
+
# a failing build reports its exit code and stderr tail, and the slices fall back to grep
|
|
329
|
+
mkroot "$WORK/root-failindex" "" "" templates
|
|
330
|
+
# forge#4316: a line naming a repo file, carrying an ESC and over 200 chars is echoed sanitised and capped
|
|
331
|
+
# shellcheck disable=SC2016 # $2 is the fake index script's own argument
|
|
332
|
+
printf '#!/usr/bin/env bash\necho "index-boom-marker" >&2\nprintf "parse error in %%s/secret/file.py \\033[31m%%0300d\\n" "$2" 0 >&2\nexit 3\n' > "$WORK/root-failindex/scripts/code-index.sh"
|
|
333
|
+
prep "$WORK/root-failindex"
|
|
334
|
+
rc_is "failing index build" 0
|
|
335
|
+
if grep -q "code index failed (exit 3)" "$WORK/stderr"; then ok; else bad "index failure must report the build's exit code on stderr"; fi
|
|
336
|
+
if grep -q "review-prepare: code-index: index-boom-marker" "$WORK/stderr"; then ok; else bad "index failure must echo the build's stderr tail: $(head -c 300 "$WORK/stderr")"; fi
|
|
337
|
+
_ix=$(grep "review-prepare: code-index: parse error" "$WORK/stderr")
|
|
338
|
+
case "$_ix" in "review-prepare: code-index: parse error in <repo>/secret/file.py [31m0"*) ok ;; *) bad "index stderr must show the repo root as <repo> without control chars: $(printf '%s' "$_ix" | head -c 120 | od -c | head -3)" ;; esac
|
|
339
|
+
if [ "${#_ix}" -le $((28 + 200)) ]; then ok; else bad "index stderr line must be capped at 200 chars, got ${#_ix}"; fi
|
|
340
|
+
eq "failed index falls back to grep slices" .index_slices.auth "Code index not available — agent will use grep exploration"
|
|
341
|
+
if ls "$WORK/out"/.code-index-err-* >/dev/null 2>&1; then bad "index stderr temp file must be removed"; else ok; fi
|
|
322
342
|
if command -v timeout >/dev/null 2>&1 || command -v gtimeout >/dev/null 2>&1; then
|
|
323
343
|
mkroot "$WORK/root-slowindex" "" "" templates
|
|
324
344
|
printf '#!/usr/bin/env bash\nsleep 30\n' > "$WORK/root-slowindex/scripts/code-index.sh"
|
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
# 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 `**`.
|
|
30
|
+
# Exit: 0 ok | 2 usage or bad repo path. bash 3.2 portable.
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
die() { echo "test-map: $*" >&2; exit 2; }
|
|
34
|
+
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
35
|
+
REPO="."; OUT=""; STDOUT=0
|
|
36
|
+
while [ $# -gt 0 ]; do
|
|
37
|
+
case "$1" in
|
|
38
|
+
--repo-path) [ $# -ge 2 ] && [ -n "$2" ] || die "usage: test-map.sh [--repo-path DIR] [--output FILE | --stdout]"; REPO="$2"; shift 2 ;;
|
|
39
|
+
--output) [ $# -ge 2 ] && [ -n "$2" ] || die "usage: test-map.sh [--repo-path DIR] [--output FILE | --stdout]"; OUT="$2"; shift 2 ;;
|
|
40
|
+
--stdout) STDOUT=1; shift ;;
|
|
41
|
+
*) die "unknown option: $1" ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
[ -d "$REPO" ] || die "repo path not found: $REPO"
|
|
45
|
+
case "$OUT" in ""|/*) ;; *) OUT="$PWD/$OUT" ;; esac
|
|
46
|
+
cd "$REPO" || die "cannot enter $REPO"
|
|
47
|
+
|
|
48
|
+
T=$(mktemp -d "${TMPDIR:-/tmp}/test-map.XXXXXX") || die "mktemp failed"
|
|
49
|
+
trap 'rm -rf "$T"' EXIT
|
|
50
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
51
|
+
# -z and quotepath=off (#4298): non-ASCII and spaced paths come out verbatim, never C-quoted
|
|
52
|
+
git -c core.quotepath=off ls-files -z --cached --others --exclude-standard | tr '\0' '\n' > "$T/files"
|
|
53
|
+
else
|
|
54
|
+
find . -type f -not -path './.git/*' | sed 's|^\./||' | sort > "$T/files"
|
|
55
|
+
fi
|
|
56
|
+
: > "$T/rows"
|
|
57
|
+
# row: source TAB cwd TAB command TAB globs separated by US (\037, #4298: a glob may contain spaces)
|
|
58
|
+
US="$(printf '\037')"
|
|
59
|
+
row() { printf '%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" >> "$T/rows"; }
|
|
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; }
|
|
93
|
+
# globs for the extensions in $2 under dir $1 ("." = repo root)
|
|
94
|
+
ext_globs() {
|
|
95
|
+
g=""; pre=""; [ "$1" = "." ] || pre="$1/"
|
|
96
|
+
for e in $2; do g="$g$US${pre}**/*.$e"; done
|
|
97
|
+
printf '%s' "${g#"$US"}"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ---- package.json scripts ----
|
|
101
|
+
PM=npm
|
|
102
|
+
[ -f yarn.lock ] && PM=yarn
|
|
103
|
+
[ -f pnpm-lock.yaml ] && PM=pnpm
|
|
104
|
+
grep -E '(^|/)package\.json$' "$T/files" | grep -v -E '(^|/)node_modules/' | sort | while IFS= read -r pj; do
|
|
105
|
+
[ -f "$pj" ] || continue
|
|
106
|
+
d=$(dirname "$pj")
|
|
107
|
+
globs="$(ext_globs "$d" "$JS_EXT")$US$pj"
|
|
108
|
+
tag=""
|
|
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
|
|
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
|
|
111
|
+
case "$s" in
|
|
112
|
+
test) row "$pj#scripts.$s$tag" "$d" "$PM run $s" "$globs" ;;
|
|
113
|
+
*) pkg_check_script "$s" "$sv" && row "$pj#scripts.$s" "$d" "$PM run $s" "$globs" ;;
|
|
114
|
+
esac
|
|
115
|
+
done
|
|
116
|
+
done
|
|
117
|
+
|
|
118
|
+
# ---- python ----
|
|
119
|
+
RUN=""
|
|
120
|
+
[ -f poetry.lock ] && RUN="poetry run "
|
|
121
|
+
[ -f uv.lock ] && RUN="uv run "
|
|
122
|
+
PY_SRC=""
|
|
123
|
+
if [ -f pyproject.toml ] && grep -qE '^\[tool\.pytest' pyproject.toml; then PY_SRC="pyproject.toml#tool.pytest"
|
|
124
|
+
elif [ -f pytest.ini ]; then PY_SRC="pytest.ini"
|
|
125
|
+
elif [ -f setup.cfg ] && grep -qE '^\[tool:pytest\]' setup.cfg; then PY_SRC="setup.cfg#tool:pytest"
|
|
126
|
+
elif grep -qE '(^|/)conftest\.py$' "$T/files"; then PY_SRC="conftest.py"; fi
|
|
127
|
+
[ -n "$PY_SRC" ] && row "$PY_SRC" "." "${RUN}pytest" "**/*.py$US**/conftest.py"
|
|
128
|
+
RUFF_SRC=""
|
|
129
|
+
if [ -f pyproject.toml ] && grep -qE '^\[tool\.ruff' pyproject.toml; then RUFF_SRC="pyproject.toml#tool.ruff"
|
|
130
|
+
elif [ -f ruff.toml ]; then RUFF_SRC="ruff.toml"
|
|
131
|
+
elif [ -f .ruff.toml ]; then RUFF_SRC=".ruff.toml"; fi
|
|
132
|
+
[ -n "$RUFF_SRC" ] && row "$RUFF_SRC" "." "${RUN}ruff check ." "**/*.py"
|
|
133
|
+
|
|
134
|
+
# ---- go ----
|
|
135
|
+
grep -E '(^|/)go\.mod$' "$T/files" | grep -v -E '(^|/)vendor/' | sort | while IFS= read -r gm; do
|
|
136
|
+
d=$(dirname "$gm"); pre=""; [ "$d" = "." ] || pre="$d/"
|
|
137
|
+
globs="${pre}**/*.go$US${pre}go.mod$US${pre}go.sum"
|
|
138
|
+
row "$gm" "$d" "go test ./..." "$globs"
|
|
139
|
+
row "$gm" "$d" "go vet ./..." "$globs"
|
|
140
|
+
done
|
|
141
|
+
|
|
142
|
+
# ---- Makefile ----
|
|
143
|
+
for mf in Makefile GNUmakefile; do
|
|
144
|
+
[ -f "$mf" ] || continue
|
|
145
|
+
grep -oE "^($MAKE_TARGETS)[[:space:]]*:" "$mf" | sed -E 's/[[:space:]]*:$//' | sort -u | while IFS= read -r t; do
|
|
146
|
+
row "$mf#$t" "." "make $t" "**"
|
|
147
|
+
done
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
# ---- CI workflow run: steps ----
|
|
151
|
+
# Emits: job US command (US = \037), one per command line; and P TAB path for each `paths:` filter item
|
|
152
|
+
# before `jobs:`. A block ends at the first non-blank line not indented past the `run` key (sibling keys such as env:).
|
|
153
|
+
wf_extract() {
|
|
154
|
+
awk '
|
|
155
|
+
function indent(s) { match(s, /^ */); return RLENGTH }
|
|
156
|
+
function unq(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); if (s ~ /^".*"$/ || s ~ /^\x27.*\x27$/) s = substr(s, 2, length(s) - 2); return s }
|
|
157
|
+
function emit(c) { c = unq(c); if (c != "") print job "\037" c }
|
|
158
|
+
BEGIN { injobs = 0; jind = -1; block = 0; inpaths = 0; job = "" }
|
|
159
|
+
{
|
|
160
|
+
line = $0; sub(/\r$/, "", line)
|
|
161
|
+
if (block) {
|
|
162
|
+
if (line ~ /^[ \t]*$/) next
|
|
163
|
+
if (indent(line) > bind) {
|
|
164
|
+
l = line; sub(/^ +/, "", l)
|
|
165
|
+
if (fold) { pend = (pend == "" ? l : pend " " l); next }
|
|
166
|
+
if (pend != "") { l = pend " " l; pend = "" }
|
|
167
|
+
if (l ~ /\\$/) { pend = substr(l, 1, length(l) - 1); sub(/[ \t]+$/, "", pend); next }
|
|
168
|
+
emit(l); next
|
|
169
|
+
}
|
|
170
|
+
if (pend != "") { emit(pend); pend = "" }
|
|
171
|
+
block = 0
|
|
172
|
+
}
|
|
173
|
+
if (line ~ /^jobs:[ \t]*$/) { injobs = 1; inpaths = 0; next }
|
|
174
|
+
if (!injobs) {
|
|
175
|
+
if (line ~ /^[ \t]*paths:[ \t]*$/) { inpaths = 1; pind = indent(line); next }
|
|
176
|
+
if (inpaths) {
|
|
177
|
+
if (line ~ /^[ \t]*-[ \t]/ && indent(line) >= pind) { p = line; sub(/^[ \t]*-[ \t]*/, "", p); sub(/[ \t]+#.*$/, "", p); print "P\t" unq(p); next }
|
|
178
|
+
if (line !~ /^[ \t]*$/ && line !~ /^[ \t]*#/) inpaths = 0
|
|
179
|
+
}
|
|
180
|
+
next
|
|
181
|
+
}
|
|
182
|
+
if (line ~ /^[^ \t#]/) { injobs = 0; next }
|
|
183
|
+
if (line ~ /^ +[A-Za-z0-9_-]+:[ \t]*$/ && (jind < 0 || indent(line) == jind)) { jind = indent(line); job = line; sub(/^ +/, "", job); sub(/:.*$/, "", job); next }
|
|
184
|
+
if (line ~ /^[ \t]*(-[ \t]+)?run:/) {
|
|
185
|
+
rest = line; sub(/^[ \t]*(-[ \t]+)?run:[ \t]*/, "", rest); sub(/[ \t]+#.*$/, "", rest)
|
|
186
|
+
# bind = the column of the `run` key: step keys after it (env:, shell:) sit there and end the block
|
|
187
|
+
if (rest ~ /^[|>][-+0-9]*$/) { block = 1; pend = ""; fold = (rest ~ /^>/); bind = match(line, /run:/) - 1; next }
|
|
188
|
+
emit(rest)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
END { if (block && pend != "") emit(pend) }
|
|
192
|
+
' "$1"
|
|
193
|
+
}
|
|
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 ;;
|
|
269
|
+
esac
|
|
270
|
+
return 1
|
|
271
|
+
}
|
|
272
|
+
tool_globs() {
|
|
273
|
+
c=" $1 "
|
|
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 ;;
|
|
281
|
+
*[\ /]ruff\ *|*[\ /]flake8\ *|*[\ /]pylint\ *|*[\ /]mypy\ *|*[\ /]black\ *|*[\ /]pytest\ *) printf '**/*.py'; return ;;
|
|
282
|
+
*[\ /]golangci-lint\ *|*[\ /]gofmt\ *|*[\ /]staticcheck\ *|*\ go\ *) printf '**/*.go'; return ;;
|
|
283
|
+
*[\ /]shellcheck\ *|*\ bash\ *.sh\ *|*\ sh\ *.sh\ *) printf '**/*.sh'; return ;;
|
|
284
|
+
*[\ /]eslint\ *|*[\ /]tsc\ *|*[\ /]prettier\ *|*[\ /]vitest\ *|*[\ /]jest\ *|*\ node\ --test*|*\ npm\ *|*\ pnpm\ *|*\ yarn\ *|*\ npx\ *) ext_globs . "$JS_EXT"; return ;;
|
|
285
|
+
esac
|
|
286
|
+
printf ''
|
|
287
|
+
}
|
|
288
|
+
grep -E '^\.github/workflows/[^/]+\.(yml|yaml)$' "$T/files" | sort | while IFS= read -r wf; do
|
|
289
|
+
[ -f "$wf" ] || continue
|
|
290
|
+
wf_extract "$wf" > "$T/wf"
|
|
291
|
+
paths=$(awk -F'\t' '$1 == "P" { printf "%s\037", $2 }' "$T/wf"); paths=${paths%"$US"}
|
|
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/}"
|
|
296
|
+
[ -n "$g" ] || g="$paths"
|
|
297
|
+
[ -n "$g" ] || { [ "$CK_CWD" != . ] && g="$CK_CWD/**"; }
|
|
298
|
+
[ -n "$g" ] || continue
|
|
299
|
+
row "$wf#jobs.$job" "$CK_CWD" "$CK_CMD" "$g"
|
|
300
|
+
done
|
|
301
|
+
done
|
|
302
|
+
|
|
303
|
+
MAP=$(jq -R -s -c '
|
|
304
|
+
[split("\n")[] | select(length > 0) | split("\t") | {source: .[0], cwd: .[1], command: .[2], globs: (.[3] | split("\u001f") | map(select(length > 0)))}]
|
|
305
|
+
| sort_by(.source, .command)
|
|
306
|
+
| reduce .[] as $e ({order: [], by: {}};
|
|
307
|
+
([$e.command, $e.cwd] | tojson) as $k
|
|
308
|
+
| if .by[$k] then .by[$k].globs += ($e.globs - .by[$k].globs)
|
|
309
|
+
else .order += [$k] | .by[$k] = {globs: $e.globs, command: $e.command, cwd: $e.cwd, source: $e.source} end)
|
|
310
|
+
| {version: 1, entries: [.by[.order[]]]}' < "$T/rows") || die "jq failed"
|
|
311
|
+
|
|
312
|
+
if [ "$STDOUT" = 1 ]; then printf '%s\n' "$MAP"; exit 0; fi
|
|
313
|
+
[ -n "$OUT" ] || OUT=".forge/test-map.json"
|
|
314
|
+
mkdir -p "$(dirname "$OUT")" || die "cannot create $(dirname "$OUT")"
|
|
315
|
+
printf '%s\n' "$MAP" > "$OUT.tmp.$$" || die "cannot write $OUT"
|
|
316
|
+
mv "$OUT.tmp.$$" "$OUT" || die "cannot write $OUT"
|
|
317
|
+
echo "$OUT"
|