forgedock 1.26.0 → 1.28.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/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +38 -2
- package/bin/engine/telemetry-seam.mjs +41 -6
- package/bin/engine/telemetry-seam.test.mjs +35 -0
- package/bin/engine-cli.mjs +616 -54
- package/bin/engine.mjs +13 -5
- package/bin/forgedock.mjs +169 -39
- package/bin/gc.mjs +580 -0
- package/bin/init-detect.mjs +68 -3
- package/bin/init-enrich.mjs +3 -0
- package/bin/init-repo.mjs +213 -0
- package/bin/journey.mjs +278 -43
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +24 -5
- package/bin/lean/batch.test.mjs +54 -4
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +235 -37
- package/bin/lean/conformance.test.mjs +143 -5
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +178 -16
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +877 -151
- package/bin/lean/lean.test.mjs +1798 -28
- package/bin/lean/ledger.mjs +92 -0
- package/bin/lean/ledger.test.mjs +100 -0
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +321 -19
- package/bin/lean/prompts.mjs +125 -17
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +25 -6
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +124 -0
- package/bin/lean/verify.test.mjs +118 -0
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +14 -2
- package/bin/stats/query.mjs +72 -0
- package/bin/stats/stats.test.mjs +54 -0
- package/commands/adopt.md +3 -0
- package/commands/audit.md +5 -8
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +125 -6
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/review-pr.md +2 -2
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +227 -3
- package/docs/OUTCOME-CODES.md +11 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +321 -0
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +125 -0
- package/scripts/check-devdocs-freshness.test.sh +130 -0
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +16 -5
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +14 -2
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/northstar-docs.test.sh +68 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/review-prepare.sh +11 -9
- package/scripts/review-prepare.test.sh +14 -0
- package/scripts/structure-delta.sh +155 -0
- package/scripts/structure-delta.test.sh +136 -0
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-devdocs-freshness.test.sh — fixture PRs for scripts/check-devdocs-freshness.sh (forge#4185).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-devdocs-freshness.sh"
|
|
7
|
+
PASS=0; FAILN=0
|
|
8
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
9
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
10
|
+
|
|
11
|
+
git init -q --bare "$TMP/origin.git"
|
|
12
|
+
git clone -q "$TMP/origin.git" "$TMP/w" 2>/dev/null
|
|
13
|
+
cd "$TMP/w" || exit 1
|
|
14
|
+
git config user.email t@example.com; git config user.name t
|
|
15
|
+
mkdir -p bin/lean scripts/fixtures devdocs/modules devdocs/decisions
|
|
16
|
+
printf 'export const x = 1;\n' > bin/lean/lean.mjs
|
|
17
|
+
printf 'test\n' > bin/lean/lean.test.mjs
|
|
18
|
+
printf '#!/bin/sh\necho foo\n' > scripts/foo.sh
|
|
19
|
+
printf 'fixture\n' > scripts/fixtures/f.txt
|
|
20
|
+
printf '# lean\n' > devdocs/modules/lean.md
|
|
21
|
+
printf '# runner\n' > devdocs/modules/runner.md
|
|
22
|
+
: > devdocs/decisions/.gitkeep
|
|
23
|
+
cat > devdocs/index.yaml <<'EOF'
|
|
24
|
+
version: 2
|
|
25
|
+
domains:
|
|
26
|
+
lean:
|
|
27
|
+
docs:
|
|
28
|
+
- path: modules/lean.md
|
|
29
|
+
modules:
|
|
30
|
+
- name: runner
|
|
31
|
+
glob: "bin/runner*"
|
|
32
|
+
path: modules/runner.md
|
|
33
|
+
authority: required
|
|
34
|
+
|
|
35
|
+
- name: lean
|
|
36
|
+
glob: "bin/lean/*"
|
|
37
|
+
path: modules/lean.md
|
|
38
|
+
authority: required
|
|
39
|
+
|
|
40
|
+
always_load: []
|
|
41
|
+
EOF
|
|
42
|
+
cat > NORTHSTAR.md <<'EOF'
|
|
43
|
+
# North Star
|
|
44
|
+
|
|
45
|
+
## Non-negotiables
|
|
46
|
+
|
|
47
|
+
1. Deterministic first.
|
|
48
|
+
|
|
49
|
+
## Runtime map
|
|
50
|
+
|
|
51
|
+
| Path | Role |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `bin/lean/*` | lean engine |
|
|
54
|
+
|
|
55
|
+
## Anti-goals
|
|
56
|
+
|
|
57
|
+
- More agents.
|
|
58
|
+
EOF
|
|
59
|
+
git add -A; git commit -qm base; git branch -M staging; git push -q origin staging 2>/dev/null
|
|
60
|
+
git checkout -q -b feat
|
|
61
|
+
|
|
62
|
+
reset() { git reset -q --hard origin/staging; git clean -qfd; }
|
|
63
|
+
commit() { git add -A; git commit -qm change; }
|
|
64
|
+
run() { # name want_rc [env...]
|
|
65
|
+
local name="$1" want="$2"; shift 2
|
|
66
|
+
env PR_BODY="" "$@" bash "$S" origin/staging >"$TMP/out" 2>&1
|
|
67
|
+
local rc=$?
|
|
68
|
+
if [ "$rc" = "$want" ]; then PASS=$((PASS+1)); else bad "$name: rc=$rc want=$want"; cat "$TMP/out"; fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
reset; printf 'export const x = 2;\n' > bin/lean/lean.mjs; commit
|
|
72
|
+
run "bin/lean/lean.mjs without docs fails" 1
|
|
73
|
+
grep -q 'bin/lean/lean.mjs' "$TMP/out" || bad "failure output names bin/lean/lean.mjs"
|
|
74
|
+
|
|
75
|
+
reset; printf 'export const x = 2;\n' > bin/lean/lean.mjs; printf '# lean\nnew entry\n' > devdocs/modules/lean.md; commit
|
|
76
|
+
run "bin/lean/lean.mjs + dossier passes" 0
|
|
77
|
+
|
|
78
|
+
reset; printf 'export const x = 2;\n' > bin/lean/lean.mjs; printf '# runner\nedit\n' > devdocs/modules/runner.md; commit
|
|
79
|
+
run "bin/lean/lean.mjs + unrelated dossier fails" 1
|
|
80
|
+
|
|
81
|
+
reset; printf 'export const x = 2;\n' > bin/lean/lean.mjs; commit
|
|
82
|
+
run "exemption line passes" 0 PR_BODY="Summary
|
|
83
|
+
docs-not-needed: refactor only, no behaviour change"
|
|
84
|
+
run "CRLF exemption line passes" 0 PR_BODY="$(printf 'Summary\r\ndocs-not-needed: typo fix\r\n')"
|
|
85
|
+
run "empty exemption reason fails" 1 PR_BODY="docs-not-needed: "
|
|
86
|
+
run "inline mention is not an exemption" 1 PR_BODY="we said docs-not-needed: no"
|
|
87
|
+
printf 'docs-not-needed: via file\n' > "$TMP/body"
|
|
88
|
+
env PR_BODY="" bash "$S" origin/staging --body-file "$TMP/body" >"$TMP/out" 2>&1 && PASS=$((PASS+1)) || bad "--body-file exemption passes"
|
|
89
|
+
|
|
90
|
+
reset; printf 'test2\n' > bin/lean/lean.test.mjs; printf 'fixture2\n' > scripts/fixtures/f.txt; printf 'x\n' > scripts/new.test.sh; commit
|
|
91
|
+
run "test-only and fixture changes pass" 0
|
|
92
|
+
|
|
93
|
+
reset; printf '#!/bin/sh\necho bar\n' > scripts/foo.sh; commit
|
|
94
|
+
run "scripts/foo.sh without docs fails" 1
|
|
95
|
+
|
|
96
|
+
reset; printf '#!/bin/sh\necho bar\n' > scripts/foo.sh; printf '# ADR\n' > devdocs/decisions/4185-x.md; commit
|
|
97
|
+
run "scripts/foo.sh + new decision passes" 0
|
|
98
|
+
|
|
99
|
+
reset; printf '# ADR\n' > devdocs/decisions/4185-x.md; git add -A; git commit -qm adr; git push -q origin HEAD:staging 2>/dev/null
|
|
100
|
+
git fetch -q origin; reset; printf '#!/bin/sh\necho bar\n' > scripts/foo.sh; printf '# ADR\nedited\n' > devdocs/decisions/4185-x.md; commit
|
|
101
|
+
run "editing an existing decision does not count" 1
|
|
102
|
+
|
|
103
|
+
reset; printf '#!/bin/sh\necho bar\n' > scripts/foo.sh; sed -i.bak 's/Deterministic first./Deterministic first, always./' NORTHSTAR.md; rm -f NORTHSTAR.md.bak; commit
|
|
104
|
+
run "NORTHSTAR edit outside Runtime map fails" 1
|
|
105
|
+
|
|
106
|
+
reset; printf '#!/bin/sh\necho bar\n' > scripts/foo.sh; sed -i.bak 's/| lean engine |/| lean engine (canonical) |/' NORTHSTAR.md; rm -f NORTHSTAR.md.bak; commit
|
|
107
|
+
run "NORTHSTAR edit inside Runtime map passes" 0
|
|
108
|
+
|
|
109
|
+
reset; printf '#!/bin/sh\necho bar\n' > scripts/foo.sh; sed -i.bak '/| `bin\/lean\/\*` | lean engine |/d' NORTHSTAR.md; rm -f NORTHSTAR.md.bak; commit
|
|
110
|
+
run "NORTHSTAR row deleted inside Runtime map passes" 0
|
|
111
|
+
|
|
112
|
+
reset; printf 'readme\n' > README.md; commit
|
|
113
|
+
run "no relevant changes pass" 0
|
|
114
|
+
|
|
115
|
+
reset
|
|
116
|
+
env PR_BODY="" bash "$S" no/such-ref >"$TMP/out" 2>&1; [ $? = 2 ] && PASS=$((PASS+1)) || bad "unknown base ref exits 2"
|
|
117
|
+
env bash "$S" >"$TMP/out" 2>&1; [ $? = 2 ] && PASS=$((PASS+1)) || bad "missing base ref and no upstream exits 2"
|
|
118
|
+
|
|
119
|
+
# #4220: without a base ref the branch's upstream is the base (CI always passes it explicitly).
|
|
120
|
+
git branch -q --set-upstream-to=origin/staging
|
|
121
|
+
printf '#!/bin/sh\necho bar\n' > scripts/foo.sh; commit
|
|
122
|
+
env PR_BODY="" bash "$S" >"$TMP/out" 2>&1; [ $? = 1 ] && PASS=$((PASS+1)) || bad "no base ref: upstream diff, docs missing fails"
|
|
123
|
+
printf 'docs-not-needed: fixture\n' > "$TMP/body"
|
|
124
|
+
env PR_BODY="" bash "$S" --body-file "$TMP/body" >"$TMP/out" 2>&1; [ $? = 0 ] && PASS=$((PASS+1)) || bad "no base ref + --body-file uses the upstream"
|
|
125
|
+
printf '# ADR\n' > devdocs/decisions/4220-x.md; commit
|
|
126
|
+
env PR_BODY="" bash "$S" >"$TMP/out" 2>&1; [ $? = 0 ] && PASS=$((PASS+1)) || bad "no base ref: upstream diff with a new decision passes"
|
|
127
|
+
git branch -q --unset-upstream
|
|
128
|
+
|
|
129
|
+
echo "check-devdocs-freshness.test.sh: $PASS passed, $FAILN failed"
|
|
130
|
+
[ "$FAILN" = 0 ]
|
|
@@ -91,7 +91,9 @@ MARKER_REGISTRY="
|
|
|
91
91
|
BENCH_SCORECARD
|
|
92
92
|
BLAST_RADIUS
|
|
93
93
|
BLOCKED_ON_HUMAN_MERGE
|
|
94
|
+
BOOTSTRAP_PLAN
|
|
94
95
|
BODY
|
|
96
|
+
BOOTSTRAP_PLAN
|
|
95
97
|
BUILDER
|
|
96
98
|
BUILD_BLOCKED
|
|
97
99
|
CALIBRATION_CHECK
|
|
@@ -106,6 +108,7 @@ MARKER_REGISTRY="
|
|
|
106
108
|
COORD_ISSUE
|
|
107
109
|
CRITIQUE
|
|
108
110
|
DECISION_RECORD
|
|
111
|
+
BOOTSTRAP_PLAN
|
|
109
112
|
DECOMPOSED
|
|
110
113
|
DECOMPOSE_BLOCKED
|
|
111
114
|
DEGRADED_REREVIEW
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# check-spec-size.sh — PR spec-size budget (forge#3762).
|
|
2
|
+
# check-spec-size.sh — PR spec-size budget for every command spec (forge#3762, #4164).
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
4
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
#
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
# Exit: 0 pass/exempt, 1 budget violation, 2 usage/base-ref error (fail closed)
|
|
14
14
|
#
|
|
15
15
|
# Rules (against the merge-base of HEAD and the PR base, compared in bytes):
|
|
16
|
-
# 1.
|
|
16
|
+
# 1. Every commands/**/*.md spec (head files plus files deleted at head) may not grow by more
|
|
17
|
+
# than 2% or more than 4096 bytes. A new spec (absent at the base) may be at most 4096 bytes.
|
|
18
|
+
# CEILINGS adds a per-file hard size cap (path:max_bytes) checked whatever the growth.
|
|
17
19
|
# 2. The NET count of canonical block markers across all of commands/**/*.md
|
|
18
20
|
# may not rise (moving a block between files, or replacing one with a stub,
|
|
19
21
|
# is allowed). Count-based until the shared helper lands (#3493).
|
|
@@ -25,8 +27,9 @@ set -uo pipefail
|
|
|
25
27
|
|
|
26
28
|
BASE="${1:-${BASE_REF:-origin/staging}}"
|
|
27
29
|
PCT_NUM=2 # 2%
|
|
28
|
-
ABS_MAX=4096 # bytes
|
|
29
|
-
|
|
30
|
+
ABS_MAX=4096 # bytes; also the size cap for a new spec
|
|
31
|
+
# Per-file hard ceilings, one path:max_bytes per line (a spec converted to a script stub stays small).
|
|
32
|
+
CEILINGS="commands/issue.md:15360"
|
|
30
33
|
|
|
31
34
|
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
|
32
35
|
out() { printf '%s\n' "$*" >> "$SUMMARY"; }
|
|
@@ -67,27 +70,42 @@ out "## Spec-size budget (base: \`$BASE\`)"
|
|
|
67
70
|
out ""
|
|
68
71
|
out "| File | Base | Head | Delta | Delta % | Status |"
|
|
69
72
|
out "|------|-----:|-----:|------:|--------:|--------|"
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
ceiling_of() { printf '%s\n' "$CEILINGS" | awk -F: -v f="$1" '$1 == f {print $2; exit}'; }
|
|
74
|
+
HEAD_FILES=$(git ls-files 'commands/*.md' | sort -u)
|
|
75
|
+
BASE_FILES=$(git ls-tree -r --name-only "$BASE" -- commands | grep -E '\.md$' | sort -u)
|
|
76
|
+
ALL_FILES=$(printf '%s\n%s\n' "$HEAD_FILES" "$BASE_FILES" | sort -u)
|
|
77
|
+
SPECS=0
|
|
78
|
+
for f in $ALL_FILES; do
|
|
79
|
+
[ -n "$f" ] || continue
|
|
80
|
+
SPECS=$((SPECS + 1))
|
|
72
81
|
b=$(base_size "$f")
|
|
73
82
|
h=0; [ -f "$f" ] && h=$(wc -c < "$f" | tr -d ' ')
|
|
74
83
|
g=$((h - b))
|
|
84
|
+
cap=$(ceiling_of "$f")
|
|
75
85
|
status="ok"
|
|
76
|
-
if [ "$
|
|
77
|
-
status="OVER BUDGET
|
|
86
|
+
if [ "$b" -eq 0 ]; then
|
|
87
|
+
[ "$h" -gt "$ABS_MAX" ] && status="OVER BUDGET (new spec > $ABS_MAX bytes)"
|
|
88
|
+
elif [ "$g" -gt 0 ] && { [ $((g * 100)) -gt $((b * PCT_NUM)) ] || [ "$g" -gt "$ABS_MAX" ]; }; then
|
|
89
|
+
status="OVER BUDGET"
|
|
90
|
+
fi
|
|
91
|
+
if [ -n "$cap" ] && [ "$h" -gt "$cap" ]; then
|
|
92
|
+
status="OVER CEILING ($cap bytes)"
|
|
78
93
|
fi
|
|
94
|
+
# Only changed or failing specs get a row; the table would otherwise list every spec.
|
|
95
|
+
[ "$g" -ne 0 ] || [ "$status" != "ok" ] || continue
|
|
96
|
+
[ "$status" = "ok" ] || FAILURES=$((FAILURES + 1))
|
|
79
97
|
if [ "$b" -gt 0 ]; then
|
|
80
98
|
p=$(awk -v g="$g" -v b="$b" 'BEGIN{printf "%+.2f%%", g*100/b}')
|
|
81
99
|
else p="n/a"; fi
|
|
82
100
|
out "| \`$f\` | $b | $h | $g | $p | $status |"
|
|
83
101
|
done
|
|
102
|
+
out ""
|
|
103
|
+
out "$SPECS spec(s) budgeted; unchanged specs within budget are not listed."
|
|
84
104
|
|
|
85
105
|
# Net marker total across every commands/**/*.md: head (working tree, incl. new
|
|
86
106
|
# files) vs base (incl. files deleted at head). Fail only when the total rises.
|
|
87
107
|
HEAD_TOTAL=0; BASE_TOTAL=0; MARKER_ROWS=""
|
|
88
|
-
|
|
89
|
-
BASE_FILES=$(git ls-tree -r --name-only "$BASE" -- commands | grep -E '\.md$' | sort -u)
|
|
90
|
-
for f in $(printf '%s\n%s\n' "$HEAD_FILES" "$BASE_FILES" | sort -u); do
|
|
108
|
+
for f in $ALL_FILES; do
|
|
91
109
|
[ -n "$f" ] || continue
|
|
92
110
|
hc=0; [ -f "$f" ] && hc=$(grep -cE "$MARKERS" "$f" 2>/dev/null || true); hc=${hc:-0}
|
|
93
111
|
bc=$(git show "$BASE:$f" 2>/dev/null | grep -cE "$MARKERS" 2>/dev/null || true); bc=${bc:-0}
|
|
@@ -45,8 +45,30 @@ run "over 4 KB fails" 1
|
|
|
45
45
|
reset; gen 5 >> commands/review-pr.md # small growth under both limits
|
|
46
46
|
run "small growth passes" 0
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
# Every spec is budgeted (#4164): top-level and nested specs outside the old review list
|
|
49
|
+
reset; gen 5000 >> commands/other.md
|
|
50
|
+
run "formerly unbudgeted spec growth fails" 1; has "other spec flagged" "| \`commands/other.md\`"
|
|
51
|
+
reset; gen 1000 > commands/work-on/nested.md; git add -A; git commit -qm nested; git push -q origin HEAD:staging 2>/dev/null; git fetch -q origin staging
|
|
52
|
+
gen 100 >> commands/work-on/nested.md
|
|
53
|
+
run "nested spec over 2% fails" 1
|
|
54
|
+
git reset -q --hard HEAD~1; git push -q -f origin HEAD:staging 2>/dev/null; git fetch -q origin staging; reset
|
|
55
|
+
reset; gen 2 >> commands/other.md
|
|
56
|
+
run "unbudgeted-before small growth passes" 0
|
|
57
|
+
|
|
58
|
+
# New specs: up to 4096 bytes
|
|
59
|
+
reset; gen 100 > commands/brand-new.md; git add -A # ~2.8 KB
|
|
60
|
+
run "new spec under 4 KB passes" 0
|
|
61
|
+
reset; gen 200 > commands/brand-new.md; git add -A # ~5.6 KB
|
|
62
|
+
run "new spec over 4 KB fails" 1; has "new spec flagged" "new spec >"
|
|
63
|
+
|
|
64
|
+
# Per-file ceiling: commands/issue.md may not exceed 15360 bytes whatever the growth
|
|
65
|
+
reset; head -c 15300 /dev/zero | tr '\0' 'x' > commands/issue.md; git add -A; git commit -qm issue; git push -q origin HEAD:staging 2>/dev/null; git fetch -q origin staging
|
|
66
|
+
printf '%050d' 0 >> commands/issue.md # 15350 bytes: +0.3%, under the cap
|
|
67
|
+
run "ceiling: under the cap passes" 0
|
|
68
|
+
printf '%050d' 0 >> commands/issue.md # 15400 bytes: +0.65% (within 2%/4 KB) but over the cap
|
|
69
|
+
run "ceiling: over the cap fails" 1; has "ceiling flagged" "OVER CEILING (15360 bytes)"
|
|
70
|
+
run "ceiling: exempt label waives" 0 PR_LABELS="spec-size-exempt"
|
|
71
|
+
git reset -q --hard HEAD~1; git push -q -f origin HEAD:staging 2>/dev/null; git fetch -q origin staging; reset
|
|
50
72
|
|
|
51
73
|
reset; gen 100 > commands/review-pr.md # shrink
|
|
52
74
|
run "shrink passes" 0
|
|
@@ -89,9 +111,10 @@ run "copy without removal still fails" 1
|
|
|
89
111
|
# Shared-helper stubs (forge#3778): the sourced-helper stub shape carries none of the markers, so converting an inlined block to a stub
|
|
90
112
|
# is a net reduction, while re-pasting the full block still fails.
|
|
91
113
|
STUB_LOC='type forge_resolve >/dev/null 2>&1 || for _r in "${FORGEDOCK_HOME:-}"; do . "$_r/scripts/lib/forge-resolve.sh"; done'
|
|
92
|
-
|
|
114
|
+
# (appended to the large work-on.md so the byte budget stays out of the marker cases)
|
|
115
|
+
reset; printf '%s\n' "$STUB_LOC" '. "$(forge_resolve lib/fence-bootstrap.sh)" || exit 1 # FENCE_BOOTSTRAP' 'HELPER_SCRIPT=$(forge_resolve x.sh)' >> commands/work-on.md
|
|
93
116
|
run "one-tag-per-stub design passes" 0
|
|
94
|
-
reset; : > commands/donor.md; printf '%s\n' "$STUB_LOC" 'HELPER_SCRIPT=$(forge_resolve x.sh)' >> commands/
|
|
117
|
+
reset; : > commands/donor.md; printf '%s\n' "$STUB_LOC" 'HELPER_SCRIPT=$(forge_resolve x.sh)' >> commands/work-on.md
|
|
95
118
|
run "block converted to a stub passes" 0
|
|
96
119
|
reset; printf '%s\n' "$STUB_LOC" 'HELPER_SCRIPT=""' >> commands/other.md
|
|
97
120
|
run "stub plus a re-pasted resolver block fails" 1
|
package/scripts/ci-local/lib.mjs
CHANGED
|
@@ -327,7 +327,7 @@ export function shellQuote(s) {
|
|
|
327
327
|
// An allowlist, not a blacklist: remote CI starts from a clean environment, so ambient operator state (agent
|
|
328
328
|
// credentials, install-root overrides, git hook variables such as GIT_DIR/GIT_INDEX_FILE, FORGE_*/CLAUDE_* settings)
|
|
329
329
|
// must not reach the jobs. Toolchain variables are passed through; credential-looking names are always dropped.
|
|
330
|
-
const ENV_EXACT = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LANGUAGE', 'TERM', 'TMPDIR', 'TMP', 'TEMP', 'TZ', 'COLORTERM', 'NO_COLOR', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'DOCKER_HOST', 'JAVA_HOME', 'GOPATH', 'GOROOT', 'GOFLAGS', 'GOCACHE', 'GOMODCACHE', 'GOPROXY', 'GOTOOLCHAIN', 'VIRTUAL_ENV', 'PYTHONPATH', 'PYTHONDONTWRITEBYTECODE', 'RUSTFLAGS', 'RUSTUP_HOME', 'SDKMAN_DIR', 'PNPM_HOME', 'BUN_INSTALL', 'NVM_DIR', 'NVM_BIN', 'NVM_INC', 'RBENV_ROOT', 'PYENV_ROOT', 'ASDF_DIR', 'ASDF_DATA_DIR', 'MISE_DATA_DIR', 'HOMEBREW_PREFIX', 'ComSpec', 'SystemRoot', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMFILES', 'MSYSTEM']);
|
|
330
|
+
const ENV_EXACT = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LANGUAGE', 'TERM', 'TMPDIR', 'TMP', 'TEMP', 'TZ', 'COLORTERM', 'NO_COLOR', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'DOCKER_HOST', 'JAVA_HOME', 'GOPATH', 'GOROOT', 'GOFLAGS', 'GOCACHE', 'GOMODCACHE', 'GOPROXY', 'GOTOOLCHAIN', 'VIRTUAL_ENV', 'PYTHONPATH', 'PYTHONDONTWRITEBYTECODE', 'RUSTFLAGS', 'RUSTUP_HOME', 'SDKMAN_DIR', 'PNPM_HOME', 'BUN_INSTALL', 'NVM_DIR', 'NVM_BIN', 'NVM_INC', 'RBENV_ROOT', 'PYENV_ROOT', 'ASDF_DIR', 'ASDF_DATA_DIR', 'MISE_DATA_DIR', 'HOMEBREW_PREFIX', 'ComSpec', 'SystemRoot', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMFILES', 'MSYSTEM', 'FORGE_HOST_PARALLELISM']);
|
|
331
331
|
const ENV_PREFIX = ['LC_', 'XDG_', 'CARGO_', 'MAVEN_', 'M2_', 'GRADLE_', 'DOTNET_', 'npm_config_', 'NPM_CONFIG_', 'YARN_', 'UV_', 'POETRY_', 'PIP_CACHE', 'FORGE_CI_LOCAL_'];
|
|
332
332
|
// Substring tokens are unambiguous; short ones (PASS, PAT, KEY, IDENT, CRED, SESSION, COOKIE) match only as whole
|
|
333
333
|
// name segments (or a trailing _KEY) so benign names such as FORGE_CI_LOCAL_PASSTHROUGH or PATH are kept.
|
|
@@ -347,6 +347,13 @@ export function scrubbedEnv(extra = {}) {
|
|
|
347
347
|
return Object.assign(env, extra);
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
// Host-derived worker cap a ForgeDock lane exports (#4182): never run more jobs/slots than FORGE_HOST_PARALLELISM.
|
|
351
|
+
export function capParallel(n, env = process.env) {
|
|
352
|
+
const s = String(env?.FORGE_HOST_PARALLELISM ?? '').trim();
|
|
353
|
+
const p = /^\d+$/.test(s) ? Number(s) : 0;
|
|
354
|
+
return p > 0 ? Math.min(n, p) : n;
|
|
355
|
+
}
|
|
356
|
+
|
|
350
357
|
export function homeForgeDir() {
|
|
351
358
|
return process.env.FORGE_CI_LOCAL_HOME || path.join(process.env.HOME || os.homedir(), '.forge');
|
|
352
359
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
import { describe, it } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { scrubbedEnv, capParallel } from "./lib.mjs";
|
|
6
|
+
|
|
7
|
+
describe("ci-local host parallelism (#4182)", () => {
|
|
8
|
+
it("scrubbedEnv passes FORGE_HOST_PARALLELISM through and still drops secrets", () => {
|
|
9
|
+
const keep = { FORGE_HOST_PARALLELISM: process.env.FORGE_HOST_PARALLELISM, FD_TEST_API_TOKEN: process.env.FD_TEST_API_TOKEN, FORGE_OTHER_SETTING: process.env.FORGE_OTHER_SETTING };
|
|
10
|
+
process.env.FORGE_HOST_PARALLELISM = "2"; process.env.FD_TEST_API_TOKEN = "s3cret"; process.env.FORGE_OTHER_SETTING = "x";
|
|
11
|
+
try {
|
|
12
|
+
const env = scrubbedEnv({});
|
|
13
|
+
assert.equal(env.FORGE_HOST_PARALLELISM, "2");
|
|
14
|
+
assert.equal(env.FD_TEST_API_TOKEN, undefined);
|
|
15
|
+
assert.equal(env.FORGE_OTHER_SETTING, undefined, "other FORGE_* settings stay out");
|
|
16
|
+
} finally {
|
|
17
|
+
for (const [k, v] of Object.entries(keep)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; }
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("capParallel caps slots/job parallelism at a positive FORGE_HOST_PARALLELISM, else leaves them alone", () => {
|
|
22
|
+
assert.equal(capParallel(4, { FORGE_HOST_PARALLELISM: "2" }), 2);
|
|
23
|
+
assert.equal(capParallel(1, { FORGE_HOST_PARALLELISM: "2" }), 1);
|
|
24
|
+
assert.equal(capParallel(4, {}), 4);
|
|
25
|
+
for (const bad of ["0", "abc", "-1", "", " "]) assert.equal(capParallel(4, { FORGE_HOST_PARALLELISM: bad }), 4, JSON.stringify(bad));
|
|
26
|
+
});
|
|
27
|
+
});
|
package/scripts/ci-local/run.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import net from 'node:net';
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import { run as runSync, commandExists, scrubbedEnv, homeForgeDir, sha256, fileHash, toolVersion, shellSplit } from './lib.mjs';
|
|
14
|
+
import { run as runSync, commandExists, scrubbedEnv, capParallel, homeForgeDir, sha256, fileHash, toolVersion, shellSplit } from './lib.mjs';
|
|
15
15
|
import { shellArgv } from './adapters/common.mjs';
|
|
16
16
|
import { checkPortability } from './adapters/portability.mjs';
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => { clean
|
|
|
25
25
|
// ---- machine-wide slot semaphore ----
|
|
26
26
|
function slotCount() {
|
|
27
27
|
const n = Number(process.env.FORGE_CI_LOCAL_SLOTS);
|
|
28
|
-
return n > 0 ? n : Math.max(2, Math.min(8, Math.floor(os.cpus().length / 2)));
|
|
28
|
+
return capParallel(n > 0 ? n : Math.max(2, Math.min(8, Math.floor(os.cpus().length / 2))));
|
|
29
29
|
}
|
|
30
30
|
function alive(pid) { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } }
|
|
31
31
|
|
|
@@ -117,6 +117,16 @@ class Tail {
|
|
|
117
117
|
async function portFree(port) { return new Promise((res) => { const s = net.createServer(); s.once('error', () => res(false)); s.listen(port, '127.0.0.1', () => s.close(() => res(true))); }); }
|
|
118
118
|
const HEALTH_FLAGS = new Set(['--health-cmd', '--health-interval', '--health-timeout', '--health-retries', '--health-start-period', '--shm-size']);
|
|
119
119
|
|
|
120
|
+
// forge#4181: ownership labels from the lane (FORGE_DOCKER_LABELS); a standalone run fills in defaults so it is labelled too.
|
|
121
|
+
const LABEL_KEYS = ['owner', 'batch', 'lane', 'issue', 'expires'];
|
|
122
|
+
export function dockerLabelArgs(env = process.env, now = Date.now()) {
|
|
123
|
+
const got = new Map();
|
|
124
|
+
for (const e of String(env.FORGE_DOCKER_LABELS || '').split(/\s+/)) { const m = /^forgedock\.([a-z]+)=([A-Za-z0-9_.-]+)$/.exec(e); if (m && !got.has(m[1])) got.set(m[1], m[2]); }
|
|
125
|
+
const dflt = { owner: '1', batch: 'adhoc', lane: `ci-local-${process.pid}`, issue: '0', expires: String(now + 6 * 3600 * 1000) };
|
|
126
|
+
for (const k of LABEL_KEYS) if (!got.has(k)) got.set(k, dflt[k]);
|
|
127
|
+
return [...got].flatMap(([k, v]) => ['--label', `forgedock.${k}=${v}`]);
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
async function startServices(job, log) {
|
|
121
131
|
const started = [];
|
|
122
132
|
const stop = () => { for (const n of started) runSync('docker', ['rm', '-f', n], { timeout: 30000 }); started.length = 0; };
|
|
@@ -128,11 +138,12 @@ async function startServices(job, log) {
|
|
|
128
138
|
if (!commandExists('docker') || runSync('docker', ['info'], { timeout: 20000 }).code !== 0) return { error: 'docker is not available', finish };
|
|
129
139
|
// forge#4040 (SEC-6): one docker network per job; nothing runs with --network host.
|
|
130
140
|
net = `forge-ci-net-${process.pid}-${job.id.replace(/[^A-Za-z0-9_.-]/g, '-')}`.slice(0, 120);
|
|
131
|
-
const
|
|
141
|
+
const labels = dockerLabelArgs();
|
|
142
|
+
const nr = runSync('docker', ['network', 'create', ...labels, net], { timeout: 30000 });
|
|
132
143
|
if (nr.code !== 0) { net = ''; return { error: `docker network create failed: ${(nr.err || nr.out).split('\n')[0]}`, finish }; }
|
|
133
144
|
for (const svc of job.services) {
|
|
134
145
|
const name = `forge-ci-${process.pid}-${job.id.replace(/[^A-Za-z0-9_.-]/g, '-')}-${svc.name}`.slice(0, 120);
|
|
135
|
-
const args = ['run', '-d', '--rm', '--name', name, '--network', net, '--network-alias', svc.name];
|
|
146
|
+
const args = ['run', '-d', '--rm', ...labels, '--name', name, '--network', net, '--network-alias', svc.name];
|
|
136
147
|
const hostPorts = [];
|
|
137
148
|
for (const p of svc.ports || []) {
|
|
138
149
|
const m = /^(?:(\d+):)?(\d+)(?:\/(tcp|udp))?$/.exec(String(p).trim());
|
|
@@ -361,7 +372,7 @@ export async function runPlan(plan, o = {}) {
|
|
|
361
372
|
// (DEGRADED) and a re-run resumes from the result cache.
|
|
362
373
|
const maxInvS = Number(process.env.FORGE_CI_LOCAL_MAX_INVOCATION_SECONDS) || 540;
|
|
363
374
|
const budgetS = Math.min(requestedS, maxInvS); const capped = requestedS > maxInvS;
|
|
364
|
-
const maxParallel = o.maxParallel || plan.config.max_parallel || Number(process.env.FORGE_CI_LOCAL_MAX_PARALLEL) || 4;
|
|
375
|
+
const maxParallel = capParallel(o.maxParallel || plan.config.max_parallel || Number(process.env.FORGE_CI_LOCAL_MAX_PARALLEL) || 4);
|
|
365
376
|
const tmpRoot = fs.mkdtempSync(path.join(process.env.TMPDIR || os.tmpdir(), 'forge-ci-local.'));
|
|
366
377
|
cleanups.add(() => { try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ } });
|
|
367
378
|
const ctx = {
|
|
@@ -342,6 +342,7 @@ export function scopeCommand(cmd, cwdRel, sctx, depth = 0) {
|
|
|
342
342
|
|
|
343
343
|
let r = null;
|
|
344
344
|
if (LINTERS[tool] && !(LINTERS[tool].sub && !LINTERS[tool].sub.has(rest[1]))) r = scopeLint(tool, rest, cmd, cwdRel, sctx, join);
|
|
345
|
+
else if (tool === 'tsc') r = scopeTsc(rest, cmd, cwdRel, sctx, join);
|
|
345
346
|
else if (tool === 'jest') r = scopeJest(rest, cmd, cwdRel, sctx, join);
|
|
346
347
|
else if (tool === 'vitest') r = scopeVitest(rest, cmd, cwdRel, sctx, join);
|
|
347
348
|
else if (tool === 'pytest' || tool === 'py.test' || (/^python[0-9.]*$/.test(tool) && rest[1] === '-m' && rest[2] === 'pytest')) r = scopeRunner('pytest', tool === 'pytest' || tool === 'py.test' ? rest.slice(1) : rest.slice(3), tool === 'pytest' || tool === 'py.test' ? [rest[0]] : rest.slice(0, 3), cmd, cwdRel, sctx, join);
|
|
@@ -507,6 +508,87 @@ function scopeMaven(rest, cmd, cwdRel, sctx, join) {
|
|
|
507
508
|
return scoped(join([...rest, '-pl', [...mods].sort().join(','), '-amd']), mods.size, null, 'maven-affected');
|
|
508
509
|
}
|
|
509
510
|
|
|
511
|
+
// ---- TypeScript compiler: the owning tsconfig project(s) of the changed files and of the files that reference them ----
|
|
512
|
+
const TS_EXT = set('ts tsx mts cts js jsx mjs cjs');
|
|
513
|
+
const TSC_VALUE = set('-p --project -t --target -m --module --outDir --outFile --rootDir --lib --jsx --tsBuildInfoFile --declarationDir --moduleResolution --baseUrl --types --typeRoots --locale --generateTrace --maxNodeModuleJsDepth --moduleDetection --verbatimModuleSyntax');
|
|
514
|
+
|
|
515
|
+
// tsconfig.json is JSON with comments and trailing commas; null when unreadable.
|
|
516
|
+
function readTsconfig(sctx, dir) {
|
|
517
|
+
sctx.cache.tsconfig = sctx.cache.tsconfig || new Map();
|
|
518
|
+
if (sctx.cache.tsconfig.has(dir)) return sctx.cache.tsconfig.get(dir);
|
|
519
|
+
let cfg = null;
|
|
520
|
+
try {
|
|
521
|
+
const raw = fs.readFileSync(path.join(sctx.repo, dir, 'tsconfig.json'), 'utf8');
|
|
522
|
+
const noComments = raw.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (m, q) => q || '');
|
|
523
|
+
cfg = JSON.parse(noComments.replace(/,(\s*[}\]])/g, '$1'));
|
|
524
|
+
} catch { cfg = null; }
|
|
525
|
+
sctx.cache.tsconfig.set(dir, cfg);
|
|
526
|
+
return cfg;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// The directory of the nearest tsconfig.json above `file` ('.' for the repo root), or '' when none owns it.
|
|
530
|
+
export function tsProjectOf(sctx, file) {
|
|
531
|
+
for (let d = px.dirname(file); ; d = px.dirname(d)) {
|
|
532
|
+
if (sctx.present.has(d === '.' ? 'tsconfig.json' : `${d}/tsconfig.json`)) return d;
|
|
533
|
+
if (d === '.' || d === '/') return '';
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// A project `tsc -b` can build: composite itself, or listed in another tsconfig's `references`.
|
|
538
|
+
function tsBuildable(sctx, dir) {
|
|
539
|
+
const cfg = readTsconfig(sctx, dir);
|
|
540
|
+
if (cfg && cfg.compilerOptions && cfg.compilerOptions.composite === true) return true;
|
|
541
|
+
if (!sctx.cache.tsRefs) {
|
|
542
|
+
sctx.cache.tsRefs = new Set();
|
|
543
|
+
for (const f of sctx.tracked) {
|
|
544
|
+
if (px.basename(f) !== 'tsconfig.json' || !sctx.present.has(f)) continue;
|
|
545
|
+
const from = px.dirname(f); const c = readTsconfig(sctx, from);
|
|
546
|
+
for (const r of (c && Array.isArray(c.references) ? c.references : [])) {
|
|
547
|
+
if (!r || typeof r.path !== 'string') continue;
|
|
548
|
+
const to = px.normalize(px.join(from, r.path));
|
|
549
|
+
sctx.cache.tsRefs.add(to.endsWith('.json') ? px.dirname(to) : to);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return sctx.cache.tsRefs.has(dir);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function scopeTsc(rest, cmd, cwdRel, sctx, join) {
|
|
557
|
+
const flags = []; let project = ''; let build = false; const buildTargets = [];
|
|
558
|
+
for (let i = 1; i < rest.length; i++) {
|
|
559
|
+
const t = rest[i];
|
|
560
|
+
if (t === '-p' || t === '--project') { if (i + 1 >= rest.length) return full(cmd, '', { selector: 'unscoped:tsc-args' }); project = rest[++i]; continue; }
|
|
561
|
+
if (t.startsWith('--project=')) { project = t.slice(10); continue; }
|
|
562
|
+
if (t === '-b' || t === '--build') { build = true; continue; }
|
|
563
|
+
if (t === '-w' || t === '--watch') return full(cmd, '', { selector: 'unscoped:tsc-watch' });
|
|
564
|
+
if (t.startsWith('-')) { flags.push(t); if (TSC_VALUE.has(t) && i + 1 < rest.length) flags.push(rest[++i]); continue; }
|
|
565
|
+
if (!build) return full(cmd, '', { selector: 'unscoped:tsc-explicit-files' });
|
|
566
|
+
buildTargets.push(t);
|
|
567
|
+
}
|
|
568
|
+
if (/[$`*?]/.test(project) || buildTargets.some((t) => /[$`*?]/.test(t))) return full(cmd, '', { selector: 'unscoped:complex-target' });
|
|
569
|
+
// The project this command checks: -p X, the single -b target, else the tsconfig in its cwd.
|
|
570
|
+
const targetOf = (t) => { const r = rel(cwdRel, t); return r.endsWith('.json') ? px.dirname(r) : r; };
|
|
571
|
+
if (buildTargets.length > 1) return full(cmd, '', { selector: 'unscoped:explicit-targets' });
|
|
572
|
+
const target = project ? targetOf(project) : buildTargets.length ? targetOf(buildTargets[0]) : rel(cwdRel, '.');
|
|
573
|
+
if (sctx.widen) return full(cmd, sctx.widen, { selector: 'tsc-project' });
|
|
574
|
+
const un = unmappedFile(sctx.changed, TS_EXT);
|
|
575
|
+
if (un) return full(cmd, `unknown-language: ${un}`, { selector: 'tsc-project' });
|
|
576
|
+
const r = reach(sctx);
|
|
577
|
+
if (r.widen) return full(cmd, r.widen, { selector: 'tsc-project' });
|
|
578
|
+
const tsFiles = [...r.files].filter((f) => TS_EXT.has(extOf(f)));
|
|
579
|
+
if (!tsFiles.length) return skipNo(0, null, 'tsc-project', 'no changed TS/JS file');
|
|
580
|
+
const owners = dedupe(tsFiles.map((f) => tsProjectOf(sctx, f)).filter(Boolean))
|
|
581
|
+
.filter((d) => target === '.' || d === target || d.startsWith(target + '/')).sort();
|
|
582
|
+
if (!owners.length) return skipNo(0, null, 'tsc-project', `no changed TS/JS file in ${target}`);
|
|
583
|
+
// A command already aimed at one non-root project is that project's check: keep it as is.
|
|
584
|
+
if (target !== '.') return scoped(cmd, owners.length, null, 'tsc-project', { projects: owners });
|
|
585
|
+
if (owners.includes('.')) return full(cmd, 'root-project', { selector: 'tsc-project', projects: owners });
|
|
586
|
+
if (owners.length === 1) return scoped(join([rest[0], build ? '-b' : '-p', fromCwd(cwdRel, owners[0]), ...flags]), 1, null, 'tsc-project', { projects: owners });
|
|
587
|
+
if (!owners.every((d) => tsBuildable(sctx, d))) return full(cmd, 'multi-project', { selector: 'tsc-project', projects: owners });
|
|
588
|
+
// Build mode rejects --noEmit; composite projects emit their declarations as the repo's own build does.
|
|
589
|
+
return scoped(join([rest[0], '-b', ...owners.map((d) => fromCwd(cwdRel, d)), ...flags.filter((f) => f !== '--noEmit')]), owners.length, null, 'tsc-project', { projects: owners });
|
|
590
|
+
}
|
|
591
|
+
|
|
510
592
|
function scopeMono(tool, rest, cmd, sctx, join) {
|
|
511
593
|
if (sctx.widen) return full(cmd, sctx.widen, { selector: `${tool}-affected` });
|
|
512
594
|
const base = sctx.from || sctx.baseRef;
|
package/scripts/ci-local.test.sh
CHANGED
|
@@ -432,7 +432,7 @@ SHIM
|
|
|
432
432
|
chmod +x "$T/bin/docker"
|
|
433
433
|
export FAKE_DOCKER_LOG="$T/docker.log"
|
|
434
434
|
: > "$FAKE_DOCKER_LOG"
|
|
435
|
-
OUT=$(PATH="$T/bin:$PATH" bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
435
|
+
OUT=$(env -u FORGE_DOCKER_LABELS PATH="$T/bin:$PATH" bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
436
436
|
eq "a service job runs when docker is available" 0 "$RC"
|
|
437
437
|
LOG=$(cat "$FAKE_DOCKER_LOG")
|
|
438
438
|
has "container is started with the same image" "$LOG" "postgres:16"
|
|
@@ -442,6 +442,18 @@ has "health options are passed through" "$LOG" "--health-cmd"
|
|
|
442
442
|
hasnt "options off the safe list are dropped" "$LOG" "--privileged"
|
|
443
443
|
has "container is named per run" "$LOG" "forge-ci-"
|
|
444
444
|
has "container is torn down" "$LOG" "rm -f"
|
|
445
|
+
# forge#4181: the network and every container carry the forgedock.* ownership labels; standalone runs get defaults
|
|
446
|
+
RUNL=$(grep '^run -d' <<<"$LOG"); NETL=$(grep '^network create' <<<"$LOG")
|
|
447
|
+
for k in owner=1 batch=adhoc lane=ci-local- issue=0 expires=; do has "4181: container label forgedock.$k" "$RUNL" "--label forgedock.$k"; has "4181: network label forgedock.$k" "$NETL" "--label forgedock.$k"; done
|
|
448
|
+
has "4181: container keeps --rm" "$RUNL" "--rm"
|
|
449
|
+
: > "$FAKE_DOCKER_LOG"
|
|
450
|
+
OUT=$(FORGE_DOCKER_LABELS="forgedock.owner=1 forgedock.batch=b9 forgedock.lane=lean-42 forgedock.issue=42 forgedock.expires=1792434600394 other=x forgedock.bad=a;b" PATH="$T/bin:$PATH" bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
451
|
+
eq "4181: a labelled run passes" 0 "$RC"
|
|
452
|
+
RUNL=$(grep '^run -d' "$FAKE_DOCKER_LOG"); NETL=$(grep '^network create' "$FAKE_DOCKER_LOG")
|
|
453
|
+
for l in batch=b9 lane=lean-42 issue=42 expires=1792434600394; do has "4181: container gets lane label $l" "$RUNL" "--label forgedock.$l"; has "4181: network gets lane label $l" "$NETL" "--label forgedock.$l"; done
|
|
454
|
+
hasnt "4181: non-forgedock entries are dropped" "$RUNL" "other=x"
|
|
455
|
+
hasnt "4181: malformed entries are dropped" "$RUNL" "forgedock.bad"
|
|
456
|
+
hasnt "4181: env labels replace the defaults" "$RUNL" "batch=adhoc"
|
|
445
457
|
: > "$FAKE_DOCKER_LOG"
|
|
446
458
|
OUT=$(FAKE_DOCKER_INFO_RC=1 PATH="$T/bin:$PATH" bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
447
459
|
eq "docker absent defers the service job (exit 0)" 0 "$RC"
|
|
@@ -776,7 +788,7 @@ OUT=$(PATH="$T/bin:$PATH" bash "$S" run --repo "$r" --base main --base-branch ma
|
|
|
776
788
|
LOG=$(cat "$FAKE_DOCKER_LOG")
|
|
777
789
|
eq "SEC-6: the circleci service job runs" 0 "$RC"
|
|
778
790
|
hasnt "SEC-6: no --network host" "$LOG" "--network host"
|
|
779
|
-
has "SEC-6: a per-job docker network is created" "$
|
|
791
|
+
has "SEC-6: a per-job docker network is created" "$(grep '^network create' <<<"$LOG")" "forge-ci-net-"
|
|
780
792
|
has "SEC-6: the container joins it" "$LOG" "--network forge-ci-net-"
|
|
781
793
|
has "SEC-6: only the declared/default port is published, on 127.0.0.1" "$LOG" "-p 127.0.0.1:54331:5432"
|
|
782
794
|
has "SEC-6: the network is removed afterwards" "$LOG" "network rm forge-ci-net-"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .c import value
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from . import c
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from . import a
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def step():
|
|
10
|
+
from .a import run # function-local: not a module-level edge
|
|
11
|
+
return c.value + (0 if run else 1)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from .a import run as _run_alias # noqa: E402 module-level a <-> b cycle
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
value = 1
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Fixture module for symbol-index.test.sh. SECRET_DOCSTRING_MARKER"""
|
|
2
|
+
import os
|
|
3
|
+
from collections import OrderedDict as OD
|
|
4
|
+
|
|
5
|
+
__all__ = ["Greeter", "greet", "fetch", "LIMIT"]
|
|
6
|
+
|
|
7
|
+
LIMIT = 10
|
|
8
|
+
_cache = OD()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _normalise(name):
|
|
12
|
+
return name.strip().lower()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def greet(name):
|
|
16
|
+
return "hi " + _normalise(name)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def fetch(key):
|
|
20
|
+
return _cache.get(key, LIMIT)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def helper():
|
|
24
|
+
return os.getcwd()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Greeter:
|
|
28
|
+
"""SECRET_CLASS_DOC_MARKER"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, name):
|
|
31
|
+
self.name = name
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def text(self):
|
|
35
|
+
return greet(self.name)
|
|
36
|
+
|
|
37
|
+
def _shout(self):
|
|
38
|
+
return self.text.upper()
|
|
39
|
+
|
|
40
|
+
def shout(self):
|
|
41
|
+
return self._shout()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const LIMIT = 10;
|
|
4
|
+
|
|
5
|
+
export function load(path: string): string {
|
|
6
|
+
return readFileSync(path, "utf8").slice(0, LIMIT);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Loader {
|
|
10
|
+
read(path: string): string {
|
|
11
|
+
return load(path);
|
|
12
|
+
}
|
|
13
|
+
}
|