forgedock 1.18.0 → 1.19.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.
Files changed (116) hide show
  1. package/bin/engine/outcome-codes.mjs +13 -0
  2. package/bin/engine/phases.mjs +54 -4
  3. package/bin/engine/runlog.mjs +10 -2
  4. package/bin/engine/telemetry-seam.mjs +3 -0
  5. package/bin/engine/telemetry.test.mjs +3 -2
  6. package/bin/engine-cli.mjs +32 -2
  7. package/bin/engine.mjs +139 -7
  8. package/bin/runner.mjs +69 -9
  9. package/bin/stats/aggregate.mjs +7 -3
  10. package/bin/stats/cache.mjs +7 -3
  11. package/bin/stats/github-source.mjs +136 -28
  12. package/bin/stats/index.mjs +46 -9
  13. package/bin/stats/render.mjs +1 -1
  14. package/bin/stats/stats.test.mjs +201 -40
  15. package/commands/review-pr-staging.md +4 -4
  16. package/commands/review-pr.md +25 -15
  17. package/commands/work-on/build/architect.md +53 -0
  18. package/commands/work-on/build/implement.md +19 -0
  19. package/commands/work-on/build/validate.md +320 -28
  20. package/commands/work-on/build.md +66 -9
  21. package/commands/work-on/investigate.md +7 -0
  22. package/commands/work-on/remediate.md +32 -13
  23. package/commands/work-on/review.md +32 -19
  24. package/docs/CONFIG.md +89 -2
  25. package/docs/OUTCOME-CODES.md +5 -1
  26. package/docs/STATS.md +11 -4
  27. package/docs/TELEMETRY.md +5 -2
  28. package/docs/WORK-ON-RUNTIME.md +18 -0
  29. package/package.json +2 -2
  30. package/packages/protocol/src/types.js +1 -1
  31. package/scripts/acceptance-red-green.sh +90 -0
  32. package/scripts/acceptance-red-green.test.sh +74 -0
  33. package/scripts/cap-findings.test.sh +2 -0
  34. package/scripts/check-contract-scope.test.sh +2 -0
  35. package/scripts/check-edge-cases.sh +143 -0
  36. package/scripts/check-edge-cases.test.sh +175 -0
  37. package/scripts/check-spec-markers.sh +2 -0
  38. package/scripts/check-test-tmp-cleanup.sh +56 -0
  39. package/scripts/check-test-tmp-cleanup.test.sh +69 -0
  40. package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
  41. package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
  42. package/scripts/ci-local/adapters/buildkite.mjs +58 -0
  43. package/scripts/ci-local/adapters/circleci.mjs +101 -0
  44. package/scripts/ci-local/adapters/common.mjs +115 -0
  45. package/scripts/ci-local/adapters/explicit.mjs +21 -0
  46. package/scripts/ci-local/adapters/github-actions.mjs +349 -0
  47. package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
  48. package/scripts/ci-local/adapters/portability.mjs +104 -0
  49. package/scripts/ci-local/adapters/task-runners.mjs +67 -0
  50. package/scripts/ci-local/cli.mjs +50 -0
  51. package/scripts/ci-local/lib.mjs +371 -0
  52. package/scripts/ci-local/plan.mjs +186 -0
  53. package/scripts/ci-local/run.mjs +345 -0
  54. package/scripts/ci-local/scoping.mjs +508 -0
  55. package/scripts/ci-local.sh +97 -0
  56. package/scripts/ci-local.test.sh +919 -0
  57. package/scripts/classify-finding.sh +32 -6
  58. package/scripts/classify-finding.test.sh +23 -0
  59. package/scripts/classify-test-run.sh +69 -0
  60. package/scripts/classify-test-run.test.sh +90 -0
  61. package/scripts/close-parent-c3.test.sh +2 -0
  62. package/scripts/consumer-sweep.sh +569 -0
  63. package/scripts/consumer-sweep.test.sh +357 -0
  64. package/scripts/detect-append-registries.sh +106 -0
  65. package/scripts/detect-append-registries.test.sh +64 -0
  66. package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
  67. package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
  68. package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
  69. package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
  70. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
  71. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
  72. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
  73. package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
  74. package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
  75. package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
  76. package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
  77. package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
  78. package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
  79. package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
  80. package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
  81. package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
  82. package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
  83. package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
  84. package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
  85. package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
  86. package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
  87. package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
  88. package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
  89. package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
  90. package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
  91. package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
  92. package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
  93. package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
  94. package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
  95. package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
  96. package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
  97. package/scripts/forge-root.test.sh +1 -1
  98. package/scripts/lib/telemetry.sh +1 -0
  99. package/scripts/remediate-approved-head.test.sh +2 -0
  100. package/scripts/remediate-basesync.test.sh +1 -1
  101. package/scripts/review-acceptance-gate.sh +2 -2
  102. package/scripts/review-acceptance-gate.test.sh +20 -0
  103. package/scripts/review-delta.sh +8 -1
  104. package/scripts/review-delta.test.sh +19 -0
  105. package/scripts/review-finalize.sh +193 -41
  106. package/scripts/review-finalize.test.sh +211 -4
  107. package/scripts/review-pr-delta-wiring.test.sh +13 -0
  108. package/scripts/review-prepare.sh +5 -3
  109. package/scripts/review-prepare.test.sh +18 -2
  110. package/scripts/run-shell-tests.sh +5 -0
  111. package/scripts/telemetry.test.sh +2 -0
  112. package/scripts/trusted-comments.test.sh +2 -1
  113. package/scripts/verify-phase-trail.test.sh +8 -0
  114. package/scripts/wait-ci-green.sh +61 -9
  115. package/scripts/wait-ci-green.test.sh +41 -3
  116. package/scripts/with-run-tmpdir.sh +20 -0
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # Fixtures for scripts/consumer-sweep.sh. Run: bash scripts/consumer-sweep.test.sh (bash 3.2 compatible)
6
+ # Fixture repos are multi-ecosystem (py, ts, go, yaml, sh) and codebase-agnostic.
7
+
8
+ set -u
9
+ DIR=$(cd "$(dirname "$0")" && pwd)
10
+ S="$DIR/consumer-sweep.sh"
11
+ T=$(mktemp -d "${TMPDIR:-/tmp}/consumer-sweep-test.XXXXXX")
12
+ trap 'rm -rf "$T"' EXIT
13
+ pass=0; fail=0
14
+ OUT=""; RC=0
15
+
16
+ g() { git -C "$T/r" -c user.name=t -c user.email=t@example.com -c commit.gpgsign=false "$@" >/dev/null 2>&1; }
17
+ run() { OUT=$(bash "$S" --repo "$T/r" "$@" 2>"$T/err"); RC=$?; }
18
+ has() { # has <desc> <substring>
19
+ case "$OUT" in *"$2"*) pass=$((pass+1)) ;; *) fail=$((fail+1)); echo "FAIL: $1 (missing '$2')"; printf '%s\n' "$OUT" | sed 's/^/ /' ;; esac
20
+ }
21
+ lacks() { # lacks <desc> <substring>
22
+ case "$OUT" in *"$2"*) fail=$((fail+1)); echo "FAIL: $1 (unexpected '$2')"; printf '%s\n' "$OUT" | sed 's/^/ /' ;; *) pass=$((pass+1)) ;; esac
23
+ }
24
+ rc() { if [ "$RC" = "$1" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: $2 (rc=$RC, want $1)"; printf '%s\n' "$OUT" | sed 's/^/ /'; fi; }
25
+ reset() { g checkout -q -f base; g branch -q -D feat; g checkout -q -b feat; }
26
+
27
+ mkdir "$T/r"; git -C "$T/r" init -q -b base >/dev/null 2>&1 || { git -C "$T/r" init -q; g checkout -b base; }
28
+ mkdir -p "$T/r/svc" "$T/r/web" "$T/r/cmd" "$T/r/ci" "$T/r/.github/workflows" "$T/r/vendor" "$T/r/scripts"
29
+ cat > "$T/r/svc/core.py" <<'P'
30
+ def compute_total(items):
31
+ return sum(items)
32
+ TIMEOUT_SECONDS = 30
33
+ P
34
+ echo 'from svc.core import compute_total
35
+ print(compute_total([1]))' > "$T/r/svc/user_a.py"
36
+ echo 'import { compute_total } from "./bridge"
37
+ export const t = compute_total' > "$T/r/web/user_b.ts"
38
+ echo 'package cmd
39
+ // compute_total mirrors the python helper
40
+ func Run() {}' > "$T/r/cmd/main.go"
41
+ cat > "$T/r/ci/settings.yaml" <<'P'
42
+ retry_budget: 3
43
+ pinned_hash: "9f8e7d6c5b4a"
44
+ P
45
+ cat > "$T/r/.github/workflows/verify.yml" <<'P'
46
+ steps:
47
+ - run: test "$(sha256sum policy.txt)" = "9f8e7d6c5b4a"
48
+ P
49
+ cat > "$T/r/scripts/run.sh" <<'P'
50
+ #!/bin/sh
51
+ exec tool --output-dir "$OUT_DIRECTORY" --retry_budget
52
+ read_cfg() { echo retry_budget; }
53
+ parse() { case "$1" in
54
+ --output-dir) shift ;;
55
+ esac; }
56
+ P
57
+ echo 'compute_total vendored copy' > "$T/r/vendor/lib.txt"
58
+ echo 'x = TIMEOUT_SECONDS' > "$T/r/svc/user_c.py"
59
+ printf 'import os\nOUT = os.environ.get("OUT_DIRECTORY")\n' > "$T/r/svc/cfg.py"
60
+ mkdir -p "$T/r/db"
61
+ printf -- '-- see "obsolete_table_ref": dropped\nSELECT 1;\n' > "$T/r/db/schema.sql"
62
+ echo 'report reads obsolete_table_ref nightly' > "$T/r/db/report.txt"
63
+ echo 'docs: pass --output-dir and set OUT_DIRECTORY' > "$T/r/docs.txt"
64
+ g add -A; g commit -m base
65
+ g checkout -q -b feat
66
+
67
+ # (a) renamed function: the OLD name is swept across py, ts, go; the changed file is not a site
68
+ sed -i.bak 's/compute_total/compute_sum/' "$T/r/svc/core.py"; rm -f "$T/r/svc/core.py.bak"
69
+ g add -A; g commit -m rename
70
+ run --range base..feat
71
+ rc 0 "sweep exits 0"
72
+ has "renamed function: py consumer" "compute_total function svc/user_a.py:1"
73
+ has "renamed function: ts consumer" "compute_total function web/user_b.ts:1"
74
+ has "renamed function: go consumer" "compute_total function cmd/main.go:2"
75
+ lacks "changed file is not a site" "svc/core.py"
76
+ lacks "vendored path excluded by default" "vendor/lib.txt"
77
+
78
+ # (b) re-typed function: same name, changed signature line -> still swept
79
+ reset
80
+ printf 'def compute_total(items, scale):\n return sum(items) * scale\nTIMEOUT_SECONDS = 30\n' > "$T/r/svc/core.py"
81
+ g add -A; g commit -m retype
82
+ run --range base..feat
83
+ has "re-typed function swept by name" "compute_total function svc/user_a.py:1"
84
+
85
+ # (c) renamed config key + renamed constant
86
+ reset
87
+ printf 'retry_budget: 3\npinned_hash: "9f8e7d6c5b4a"\n' | sed 's/retry_budget/retry_limit/' > "$T/r/ci/settings.yaml"
88
+ sed -i.bak 's/TIMEOUT_SECONDS = 30/TIMEOUT_LIMIT = 30/' "$T/r/svc/core.py"; rm -f "$T/r/svc/core.py.bak"
89
+ git -C "$T/r" add -A; g commit -m key
90
+ run --range base..feat
91
+ has "changed config key swept (sh consumer)" "retry_budget key scripts/run.sh:"
92
+ has "renamed constant swept" "TIMEOUT_SECONDS const svc/user_c.py:1"
93
+
94
+ # (d) changed literal hash is found in a different workflow
95
+ reset
96
+ sed -i.bak 's/9f8e7d6c5b4a/1a2b3c4d5e6f/' "$T/r/ci/settings.yaml"; rm -f "$T/r/ci/settings.yaml.bak"
97
+ g add -A; g commit -m hash
98
+ run --range base..feat
99
+ has "old literal hash swept" "9f8e7d6c5b4a literal .github/workflows/verify.yml:2"
100
+ lacks "new hash is not swept (added side)" "1a2b3c4d5e6f"
101
+
102
+ # (d2) removed line that starts with "-- " (diff line "--- ...") is content, not a file header
103
+ reset
104
+ printf 'SELECT 1;\n' > "$T/r/db/schema.sql"
105
+ g add -A; g commit -m dropcomment
106
+ run --range base..feat
107
+ has "removed '-- ' line identifier swept" "obsolete_table_ref key db/report.txt:1"
108
+
109
+ # (e) CLI flag and env var on the removed side
110
+ reset
111
+ sed -i.bak -e 's/--output-dir "\$OUT_DIRECTORY"/--out "$OUT_DIR"/' -e 's/^ --output-dir)/ --out)/' "$T/r/scripts/run.sh"; rm -f "$T/r/scripts/run.sh.bak"
112
+ sed -i.bak 's/OUT_DIRECTORY/OUT_DIR/' "$T/r/svc/cfg.py"; rm -f "$T/r/svc/cfg.py.bak"
113
+ g add -A; g commit -m flag
114
+ run --range base..feat
115
+ has "flag swept without dashes" "output-dir flag docs.txt:1"
116
+ has "env var swept" "OUT_DIRECTORY env docs.txt:1"
117
+
118
+ # (f) pure additions are not swept
119
+ reset
120
+ echo 'def brand_new_helper(): pass' >> "$T/r/svc/core.py"; echo 'brand_new_helper' > "$T/r/svc/ref.txt"
121
+ g add -A; g commit -m add
122
+ run --range base..feat
123
+ rc 0 "pure addition exits 0"
124
+ lacks "pure addition not swept" "brand_new_helper"
125
+
126
+ # (g) noise bounds: short ids, stop-list words, cap + truncated row
127
+ reset
128
+ sed -i.bak 's/def compute_total(items):/def compute_total(items):\n # type value/' "$T/r/svc/core.py"; rm -f "$T/r/svc/core.py.bak"
129
+ printf 'def abc(): pass\ndef value(): pass\n' > "$T/r/svc/short.py"; g add -A; g commit -q -m short; g checkout -q -b feat2 2>/dev/null
130
+ git -C "$T/r" rm -q svc/short.py; g commit -m rmshort
131
+ run --range feat..feat2
132
+ lacks "3-char identifier skipped" "abc"
133
+ lacks "stop-list word skipped" "value"
134
+ reset
135
+ for i in 1 2 3 4 5 6; do echo "compute_total $i" > "$T/r/svc/many$i.txt"; done
136
+ g add -A; g commit -m many; g checkout -q -b feat3
137
+ sed -i.bak 's/compute_total/compute_sum/' "$T/r/svc/core.py"; rm -f "$T/r/svc/core.py.bak"; g add -A; g commit -m rename
138
+ run --range feat..feat3 --cap 2
139
+ rows=$(printf '%s\n' "$OUT" | grep -c '^compute_total function ')
140
+ if [ "$rows" = 2 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: cap 2 yields $rows rows"; fi
141
+ has "truncated row at cap" "compute_total truncated +"
142
+ g checkout -q -f base; g branch -q -D feat2 feat3
143
+
144
+ # (h) extra exclude glob
145
+ reset
146
+ sed -i.bak 's/compute_total/compute_sum/' "$T/r/svc/core.py"; rm -f "$T/r/svc/core.py.bak"; g add -A; g commit -m rename
147
+ run --range base..feat --exclude-glob 'web/'
148
+ lacks "configured directory glob excludes web/" "web/user_b.ts"
149
+ has "other consumers remain" "svc/user_a.py"
150
+
151
+ # (i) gate mode: unaddressed, then justified
152
+ printf 'CONSUMERS:\n- svc/user_a.py:1 unaffected: imports the alias only\n' > "$T/c1"
153
+ run --range base..feat --consumers "$T/c1"
154
+ rc 1 "gate fails with unaddressed sites"
155
+ has "unaddressed ts consumer named" "UNADDRESSED: compute_total web/user_b.ts:1"
156
+ has "unaddressed go consumer named" "UNADDRESSED: compute_total cmd/main.go:2"
157
+ lacks "justified site not reported" "svc/user_a.py"
158
+ printf 'CONSUMERS:\n- `svc/user_a.py:9` unaffected: alias\n* web/user_b.ts unaffected: re-export\n- cmd/main.go:2 unaffected: comment only\n- svc/user_c.py updated\n' > "$T/c2"
159
+ run --range base..feat --consumers "$T/c2"
160
+ rc 0 "gate passes when every site is justified"
161
+ has "OK line" "OK:"
162
+ printf 'CONSUMERS:\n- cmd/main.go unaffected:\n' > "$T/c3"
163
+ run --range base..feat --consumers "$T/c3"
164
+ rc 1 "reasonless justification does not count"
165
+
166
+ # (j) worktree mode: uncommitted + untracked changes form the change set
167
+ reset
168
+ sed -i.bak 's/compute_total/compute_sum/' "$T/r/svc/core.py"; rm -f "$T/r/svc/core.py.bak"
169
+ echo 'compute_sum(1)' > "$T/r/svc/user_a.py"
170
+ echo 'compute_total again' > "$T/r/svc/untracked_new.txt"
171
+ run --range base..WORKTREE
172
+ has "unstaged rename swept" "compute_total function web/user_b.ts:1"
173
+ lacks "modified file is in the change set" "svc/user_a.py"
174
+ lacks "untracked file is in the change set" "untracked_new.txt"
175
+ g checkout -q -f base; rm -f "$T/r/svc/untracked_new.txt"; g checkout -q -f feat
176
+
177
+ # (k) identifier mode
178
+ run --ident compute_total --ident retry_budget
179
+ rc 0 "ident mode exits 0"
180
+ has "ident mode lists consumers" "compute_total ident svc/user_a.py:1"
181
+ has "ident mode lists the definition too (no change set)" "svc/core.py"
182
+ run --ident abc
183
+ rc 2 "short --ident is a usage error"
184
+ run --ident '-e'
185
+ rc 2 "dash-leading --ident rejected"
186
+
187
+ # (l) hostile identifier text is data, never evaluated
188
+ reset
189
+ printf 'def "$(touch %s/pwned)"():\n' "$T" > /dev/null
190
+ printf '%s\n' "x = '\$(touch $T/pwned)'" '`touch '"$T"'/pwned2`' "key: ; touch $T/pwned3" > "$T/r/svc/hostile.txt"
191
+ g add -A; g commit -m hostile; g checkout -q -b feat4
192
+ git -C "$T/r" rm -q svc/hostile.txt; g commit -m rmhostile
193
+ run --range feat..feat4
194
+ rc 0 "hostile text sweeps cleanly"
195
+ if [ ! -e "$T/pwned" ] && [ ! -e "$T/pwned2" ] && [ ! -e "$T/pwned3" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: diff text was evaluated"; fi
196
+ g checkout -q -f base; g branch -q -D feat4
197
+
198
+ # (m) failure modes are exit 2, never a pass
199
+ run --range nosuchref..feat
200
+ rc 2 "unresolvable base is exit 2"
201
+ run --range base
202
+ rc 2 "malformed range is exit 2"
203
+ OUT=$(bash "$S" --repo "$T" --range base..feat 2>&1); RC=$?
204
+ rc 2 "non-repo is exit 2"
205
+ run --range base..feat --consumers "$T/missing"
206
+ rc 2 "missing consumers file is exit 2"
207
+ run --ident compute_total --consumers "$T/c1"
208
+ rc 2 "--consumers needs --range"
209
+ run
210
+ rc 2 "no mode is exit 2"
211
+
212
+ # (n) survival: an edit that keeps its identifiers flags nothing; prose words and version bumps flag nothing
213
+ reset
214
+ printf 'cfg = get_setting("alpha_key")\n' > "$T/r/svc/surv.py"
215
+ printf 'The tool MUST validate input.\n- `legacy_option` toggles the old path.\n' > "$T/r/docs-note.md"
216
+ printf '{\n "name": "pkg",\n "version": "1.2.3"\n}\n' > "$T/r/package.json"
217
+ printf 'MUST be set. alpha_key lives here. legacy_option is documented. 1.2.3 is pinned.\n' > "$T/r/svc/consumer_notes.txt"
218
+ g add -A; g commit -m survbase; g checkout -q -b feat5
219
+ printf 'cfg = get_setting("alpha_key", default=None)\n' > "$T/r/svc/surv.py"
220
+ printf 'The tool SHOULD validate input.\n- `legacy_option` toggles the old path.\n' > "$T/r/docs-note.md"
221
+ printf '{\n "name": "pkg",\n "version": "1.2.4"\n}\n' > "$T/r/package.json"
222
+ g add -A; g commit -m surv
223
+ run --range feat..feat5
224
+ rc 0 "survival sweep exits 0"
225
+ lacks "kept identifier is not swept" "alpha_key"
226
+ lacks "prose word change is not swept" "MUST"
227
+ lacks "version bump is not swept" "1.2.3"
228
+ lacks "kept backticked token is not swept" "legacy_option"
229
+ g checkout -q -b feat6
230
+ printf 'The tool SHOULD validate input.\n' > "$T/r/docs-note.md"
231
+ g add -A; g commit -m prose2
232
+ run --range feat5..feat6
233
+ has "removed documented prose option is swept" "legacy_option "
234
+ has "backticked token consumer found" "svc/consumer_notes.txt:1"
235
+ g checkout -q -f base; g branch -q -D feat5 feat6
236
+
237
+ # (o) a removed function whose name is still defined outside the change set is ambiguous, not flagged
238
+ reset
239
+ printf 'def helper_probe(x):\n return x\n' > "$T/r/svc/probe_a.py"
240
+ printf 'def helper_probe(y):\n return y\n' > "$T/r/svc/probe_b.py"
241
+ printf 'helper_probe(1)\n' > "$T/r/svc/probe_use.py"
242
+ g add -A; g commit -m probes; g checkout -q -b feat7
243
+ git -C "$T/r" rm -q svc/probe_a.py; g commit -m dropprobe
244
+ run --range feat..feat7
245
+ has "still-defined name is reported ambiguous" "helper_probe ambiguous"
246
+ lacks "ambiguous name has no site rows" "svc/probe_use.py"
247
+ g checkout -q -f base; g branch -q -D feat7
248
+
249
+ # (p) special-character paths can be justified (gate parser splits on the literal ' unaffected: ', not on characters)
250
+ reset
251
+ mkdir -p "$T/r/app/[id]" "$T/r/app/(auth)" "$T/r/docs"
252
+ echo 'compute_total(page)' > "$T/r/app/[id]/page.tsx"
253
+ echo 'compute_total(login)' > "$T/r/app/(auth)/login.tsx"
254
+ echo 'compute_total notes' > "$T/r/docs/my notes.md"
255
+ g add -A; g commit -m specialpaths; g checkout -q -b feat8
256
+ sed -i.bak 's/compute_total/compute_sum/' "$T/r/svc/core.py"; rm -f "$T/r/svc/core.py.bak"; g add -A; g commit -m rename
257
+ printf -- '- svc/user_a.py:1 unaffected: alias\n- web/user_b.ts:1 unaffected: re-export\n- cmd/main.go:2 unaffected: comment\n' > "$T/c4"
258
+ run --range feat..feat8 --consumers "$T/c4"
259
+ rc 1 "special-char paths stay unaddressed until justified"
260
+ has "bracket path reported" "UNADDRESSED: compute_total app/[id]/page.tsx:1"
261
+ has "paren path reported" "UNADDRESSED: compute_total app/(auth)/login.tsx:1"
262
+ has "space path reported" "UNADDRESSED: compute_total docs/my notes.md:1"
263
+ printf -- '%s\n' '- svc/user_a.py:1 unaffected: alias' '- web/user_b.ts:1 unaffected: re-export' '- cmd/main.go:2 unaffected: comment' \
264
+ '- app/[id]/page.tsx:12 unaffected: route param only' '- `app/(auth)/login.tsx:3` unaffected: login copy' '* docs/my notes.md:2 unaffected: prose' > "$T/c5"
265
+ run --range feat..feat8 --consumers "$T/c5"
266
+ rc 0 "special-char paths are accepted as justified"
267
+ has "gate OK" "OK:"
268
+ printf -- '%s\n' '- app/[id]/page.tsx unresolved: not checked' > "$T/c6"
269
+ run --range feat..feat8 --consumers "$T/c6"
270
+ rc 1 "an unresolved line never justifies a site"
271
+
272
+ # (q) advisory mode records sites but never fails
273
+ run --range feat..feat8 --mode advisory --consumers "$T/c4"
274
+ rc 0 "advisory mode exits 0 with unaddressed sites"
275
+ has "advisory prints ADVISORY lines" "ADVISORY: compute_total app/[id]/page.tsx:1"
276
+ lacks "advisory never prints UNADDRESSED" "UNADDRESSED"
277
+ run --range feat..feat8 --mode sideways
278
+ rc 2 "unknown --mode is exit 2"
279
+
280
+ # (r) budget: an exhausted budget stops the run with the advisory BUDGET_EXCEEDED result, never exit 1
281
+ run --range feat..feat8 --budget-seconds 0
282
+ rc 0 "zero budget exits 0 in sweep mode"
283
+ has "zero budget reports BUDGET_EXCEEDED" "BUDGET_EXCEEDED"
284
+ run --range feat..feat8 --budget-seconds 0 --consumers "$T/c6"
285
+ rc 0 "zero budget never fails the gate"
286
+ has "gate prints BUDGET_EXCEEDED" "BUDGET_EXCEEDED:"
287
+ run --range feat..feat8 --budget-seconds abc
288
+ rc 2 "non-numeric budget is exit 2"
289
+ g checkout -q -f base; g branch -q -D feat8
290
+
291
+ # (s) one grep per run (a PATH shim counts git grep calls) and a 3000-hit identifier completes fast
292
+ reset
293
+ mkdir -p "$T/r/bulk"
294
+ i=1; while [ "$i" -le 30 ]; do
295
+ j=1; : > "$T/r/bulk/b$i.txt"; while [ "$j" -le 100 ]; do echo "call heavy_symbol_x $j" >> "$T/r/bulk/b$i.txt"; j=$((j+1)); done
296
+ i=$((i+1))
297
+ done
298
+ printf 'def heavy_symbol_x():\n pass\ndef other_symbol_y():\n pass\ndef third_symbol_z():\n pass\n' > "$T/r/svc/heavy.py"
299
+ echo 'other_symbol_y third_symbol_z' > "$T/r/svc/other_use.txt"
300
+ g add -A; g commit -m bulk; g checkout -q -b feat9
301
+ printf 'def renamed_a():\n pass\ndef renamed_b():\n pass\ndef renamed_c():\n pass\n' > "$T/r/svc/heavy.py"
302
+ g add -A; g commit -m renamebulk
303
+ mkdir "$T/shim"; REALGIT=$(command -v git)
304
+ printf '#!/bin/sh\ncase " $* " in *" grep "*) echo x >> "%s/grepcount" ;; esac\nexec "%s" "$@"\n' "$T" "$REALGIT" > "$T/shim/git"; chmod +x "$T/shim/git"
305
+ : > "$T/grepcount"
306
+ t0=$SECONDS
307
+ OUT=$(PATH="$T/shim:$PATH" bash "$S" --repo "$T/r" --range feat..feat9 2>"$T/err"); RC=$?
308
+ elapsed=$((SECONDS - t0))
309
+ rc 0 "bulk sweep exits 0"
310
+ n=$(wc -l < "$T/grepcount" | tr -d ' ')
311
+ if [ "$n" = 1 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: expected exactly 1 git grep, saw $n"; fi
312
+ if [ "$elapsed" -lt 3 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: 3000-hit sweep took ${elapsed}s (want < 3s)"; fi
313
+ has "heavy identifier is capped" "heavy_symbol_x truncated +2975"
314
+ rows=$(printf '%s\n' "$OUT" | grep -c '^heavy_symbol_x function ')
315
+ if [ "$rows" = 25 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: heavy identifier rows=$rows (want 25)"; fi
316
+ has "sibling identifiers found with the same single grep" "other_symbol_y function svc/other_use.txt:1"
317
+ g checkout -q -f base; g branch -q -D feat9
318
+
319
+ # (t) --eval-history measures flagged sites per merge and prints mean and max (read-only)
320
+ mkdir "$T/h"
321
+ gh_() { git -C "$T/h" -c user.name=t -c user.email=t@example.com -c commit.gpgsign=false "$@" >/dev/null 2>&1; }
322
+ git -C "$T/h" init -q -b main >/dev/null 2>&1 || { git -C "$T/h" init -q; gh_ checkout -b main; }
323
+ printf 'def legacy_entry():\n pass\n' > "$T/h/lib.py"; echo 'legacy_entry()' > "$T/h/use.py"
324
+ gh_ add -A; gh_ commit -m init
325
+ gh_ checkout -q -b topic1; sed -i.bak 's/legacy_entry/modern_entry/' "$T/h/lib.py"; rm -f "$T/h/lib.py.bak"; gh_ add -A; gh_ commit -m t1
326
+ gh_ checkout -q main; gh_ merge --no-ff -m "Merge topic1" topic1
327
+ gh_ checkout -q -b topic2; echo 'brand_new = 1' > "$T/h/new.py"; gh_ add -A; gh_ commit -m t2
328
+ gh_ checkout -q main; gh_ merge --no-ff -m "Merge topic2" topic2
329
+ before=$(git -C "$T/h" rev-parse HEAD)
330
+ OUT=$(bash "$S" --repo "$T/h" --eval-history 5 2>"$T/err"); RC=$?
331
+ rc 0 "eval-history exits 0"
332
+ has "eval prints the flagged merge" "sites=1"
333
+ has "eval prints mean and max" "mean=0.50 max=1 prs=2"
334
+ has "eval reports the guard verdict" "guard=PASS"
335
+ if [ "$before" = "$(git -C "$T/h" rev-parse HEAD)" ] && [ -z "$(git -C "$T/h" status --short)" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: eval-history modified the repo"; fi
336
+ run --eval-history 2 --range base..feat
337
+ rc 2 "eval-history is exclusive with --range"
338
+
339
+ # forge#4037: a 10,000-line YAML rewrite finishes quickly (per-file token sets, not removed x added), and extraction runs
340
+ # under --budget-seconds: a spent budget reports BUDGET_EXCEEDED, advisory, exit 0.
341
+ mkdir "$T/y"; gy() { git -C "$T/y" -c user.name=t -c user.email=t@example.com -c commit.gpgsign=false "$@" >/dev/null 2>&1; }
342
+ git -C "$T/y" init -q -b base >/dev/null 2>&1
343
+ awk 'BEGIN{for(i=1;i<=10000;i++) printf " svc.old-key-%d: value_%d\n", i, i}' > "$T/y/big.yaml"
344
+ gy add -A; gy commit -m base; gy checkout -q -b feat
345
+ awk 'BEGIN{for(i=1;i<=10000;i++) printf " svc.new-key-%d: value_%d\n", i, i}' > "$T/y/big.yaml"
346
+ gy add -A; gy commit -m rewrite
347
+ start=$(date +%s)
348
+ OUT=$(bash "$S" --repo "$T/y" --range base..feat --budget-seconds 20 --mode advisory 2>"$T/err"); RC=$?
349
+ elapsed=$(( $(date +%s) - start ))
350
+ rc 0 "10k-line yaml rewrite exits 0"
351
+ if [ "$elapsed" -le 20 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: 10k-line yaml rewrite took ${elapsed}s"; fi
352
+ OUT=$(bash "$S" --repo "$T/y" --range base..feat --budget-seconds 0 --mode blocking 2>"$T/err"); RC=$?
353
+ rc 0 "zero budget is advisory exit 0"
354
+ has "zero budget reports BUDGET_EXCEEDED" "BUDGET_EXCEEDED"
355
+
356
+ echo "consumer-sweep.test.sh: $pass passed, $fail failed"
357
+ [ "$fail" -eq 0 ]
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # detect-append-registries.sh — find append-only registry files and prove a merge=union policy is safe (forge#3969).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ #
6
+ # A registry is a hand-maintained list that most PRs append to (test paths, cron targets). Two PRs appending at the
7
+ # same spot conflict textually although every line is independent, which costs a base-sync remediation round.
8
+ #
9
+ # Usage:
10
+ # detect-append-registries.sh --detect [--repo <dir>] [--since <git date>] [--min-commits N] [--ratio PCT] [--limit N]
11
+ # Rank files from `git log --numstat`: touched by >= N commits (default 5), >= PCT% of them add-only (default 80).
12
+ # Output, one per line: <touches> <add-only> <path> (sorted by touches, descending). Read-only.
13
+ # detect-append-registries.sh --check-union <path> [--repo <dir>]
14
+ # Prove in a throwaway repo (mktemp -d) that two branches appending different lines to <path> conflict without
15
+ # merge=union and merge cleanly with it, both lines surviving. Never touches the caller's repo or git config.
16
+ # Structured files (JSON/YAML/TOML/XML/lock files) are always rejected: union would corrupt ordering or structure.
17
+ # Exit: 0 detect ok / union safe, 1 union NOT safe, 2 usage error
18
+ # Bash 3.2 portable: no assoc arrays, mapfile, grep -P. Honours no FORGEDOCK_HOME (self-contained).
19
+ set -uo pipefail
20
+
21
+ usage() { sed -n '6,17p' "$0" >&2; exit 2; }
22
+ MODE=""; REPO="."; SINCE="90 days ago"; MIN=5; RATIO=80; LIMIT=20; TARGET=""
23
+ while [ $# -gt 0 ]; do
24
+ case "$1" in
25
+ --detect) MODE=detect ;;
26
+ --check-union) MODE=check; TARGET="${2-}"; [ $# -ge 2 ] || usage; shift ;;
27
+ --repo) REPO="${2-}"; [ $# -ge 2 ] || usage; shift ;;
28
+ --since) SINCE="${2-}"; [ $# -ge 2 ] || usage; shift ;;
29
+ --min-commits) MIN="${2-}"; [ $# -ge 2 ] || usage; shift ;;
30
+ --ratio) RATIO="${2-}"; [ $# -ge 2 ] || usage; shift ;;
31
+ --limit) LIMIT="${2-}"; [ $# -ge 2 ] || usage; shift ;;
32
+ *) usage ;;
33
+ esac
34
+ shift
35
+ done
36
+ [ -n "$MODE" ] || usage
37
+ for n in "$MIN" "$RATIO" "$LIMIT"; do case "$n" in ''|*[!0-9]*) echo "detect-append-registries: numeric option expected, got '$n'" >&2; exit 2 ;; esac; done
38
+
39
+ if [ "$MODE" = detect ]; then
40
+ git -C "$REPO" rev-parse --git-dir >/dev/null 2>&1 || { echo "detect-append-registries: not a git repo: $REPO" >&2; exit 2; }
41
+ # numstat: "<added>\t<deleted>\t<path>"; binaries show "-". Merge commits are skipped (--no-merges).
42
+ LOG=$(git -C "$REPO" log --no-merges --since="$SINCE" --numstat --format= 2>/dev/null) || LOG=""
43
+ printf '%s\n' "$LOG" | awk -F'\t' -v min="$MIN" -v ratio="$RATIO" '
44
+ NF >= 3 && $1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ {
45
+ p = $3; t[p]++; if ($2 == 0 && $1 > 0) a[p]++
46
+ }
47
+ END { for (p in t) if (t[p] >= min && (a[p] + 0) * 100 >= ratio * t[p]) printf "%d %d %s\n", t[p], a[p] + 0, p }
48
+ ' | sort -rn -k1,1 -k2,2 | head -n "$LIMIT"
49
+ exit 0
50
+ fi
51
+
52
+ # --- --check-union
53
+ [ -n "$TARGET" ] || usage
54
+ case "$TARGET" in /*|../*|*/../*|..) echo "detect-append-registries: path must be repo-relative: $TARGET" >&2; exit 2 ;; esac
55
+ case "$TARGET" in *[[:space:]]*|*'['*|*'*'*|*'?'*|*'\'*) echo "UNION UNSAFE: $TARGET (whitespace or glob characters in path)"; exit 1 ;; esac
56
+ case "$(printf '%s' "$TARGET" | tr 'A-Z' 'a-z')" in
57
+ *.json|*.jsonc|*.yml|*.yaml|*.toml|*.xml|*.lock|*lock.json|*.sum|*.csv|*.md)
58
+ echo "UNION UNSAFE: $TARGET is a structured or ordered file type (union can corrupt structure, ordering or uniqueness)"; exit 1 ;;
59
+ esac
60
+
61
+ TMP=$(mktemp -d) || { echo "detect-append-registries: mktemp failed" >&2; exit 2; }
62
+ trap 'rm -rf "$TMP"' EXIT
63
+ # hermetic git: no user config, hooks or signing; never reads or writes the caller's repo configuration
64
+ export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_NOSYSTEM=1
65
+ export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
66
+ W="$TMP/w"
67
+ g() { git -C "$W" -c commit.gpgsign=false -c core.hooksPath=/dev/null "$@"; }
68
+ git init -q "$W" 2>/dev/null || { echo "detect-append-registries: git init failed" >&2; exit 2; }
69
+ g checkout -q -b base 2>/dev/null
70
+ mkdir -p "$W/$(dirname "$TARGET")"
71
+ # seed with the real file's committed content when available, else a small stand-in list
72
+ if git -C "$REPO" cat-file -e "HEAD:$TARGET" 2>/dev/null; then
73
+ git -C "$REPO" show "HEAD:$TARGET" > "$W/$TARGET" 2>/dev/null
74
+ else
75
+ printf 'entry-1\nentry-2\nentry-3\n' > "$W/$TARGET"
76
+ fi
77
+ [ -s "$W/$TARGET" ] && [ -n "$(tail -c1 "$W/$TARGET")" ] && printf '\n' >> "$W/$TARGET"
78
+ g add -A >/dev/null; g commit -q -m seed >/dev/null
79
+
80
+ append_branch() { # name line
81
+ g checkout -q -b "$1" base 2>/dev/null
82
+ printf '%s\n' "$2" >> "$W/$TARGET"
83
+ g add -A >/dev/null; g commit -q -m "append $2" >/dev/null
84
+ }
85
+ append_branch side-a "forge-union-probe-a"
86
+ append_branch side-b "forge-union-probe-b"
87
+
88
+ # 1. without union the same-spot appends must conflict (otherwise the probe proves nothing)
89
+ g checkout -q side-a 2>/dev/null
90
+ if g merge -q --no-edit side-b >/dev/null 2>&1; then
91
+ echo "UNION UNSAFE: $TARGET does not conflict without union (probe is not meaningful)"; exit 1
92
+ fi
93
+ g merge --abort >/dev/null 2>&1
94
+
95
+ # 2. with union both lines must survive
96
+ g reset -q --hard side-a
97
+ printf '%s merge=union\n' "$TARGET" > "$W/.git/info/attributes"
98
+ if ! g merge -q --no-edit side-b >/dev/null 2>&1; then
99
+ echo "UNION UNSAFE: $TARGET still conflicts with merge=union"; exit 1
100
+ fi
101
+ if ! grep -qxF 'forge-union-probe-a' "$W/$TARGET" || ! grep -qxF 'forge-union-probe-b' "$W/$TARGET"; then
102
+ echo "UNION UNSAFE: $TARGET lost an appended line under merge=union"; exit 1
103
+ fi
104
+ echo "UNION SAFE: $TARGET (same-spot appends conflict without union, merge cleanly with it)"
105
+ echo "REMINDER: this proves the mechanism only. Add the .gitattributes entry only if every line in $TARGET is independent."
106
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # detect-append-registries.test.sh — fixture cases for scripts/detect-append-registries.sh (forge#3969).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ set -uo pipefail
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
+ DA="$ROOT/scripts/detect-append-registries.sh"
8
+ PASS=0; FAILN=0
9
+ ok() { PASS=$((PASS+1)); }
10
+ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
11
+ command -v git >/dev/null 2>&1 || { echo "detect-append-registries.test.sh: git missing, skipped"; exit 0; }
12
+ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
13
+ export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_NOSYSTEM=1
14
+ export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
15
+ W="$TMP/w"
16
+ g() { git -C "$W" -c commit.gpgsign=false -c core.hooksPath=/dev/null "$@"; }
17
+ git init -q "$W" 2>/dev/null; g checkout -q -b main 2>/dev/null
18
+ mkdir -p "$W/ci"
19
+ printf 'test-1\ntest-2\n' > "$W/ci/tests.list"; printf 'a: 1\n' > "$W/conf.yml"; printf 'x\n' > "$W/other.txt"
20
+ g add -A >/dev/null; g commit -q -m seed >/dev/null
21
+
22
+ # 1. two PRs appending to the same registry conflict without union and merge cleanly with it (ac-3 fixture)
23
+ g checkout -q -b pr1 main 2>/dev/null; printf 'test-pr1\n' >> "$W/ci/tests.list"; g add -A >/dev/null; g commit -q -m pr1 >/dev/null
24
+ g checkout -q -b pr2 main 2>/dev/null; printf 'test-pr2\n' >> "$W/ci/tests.list"; g add -A >/dev/null; g commit -q -m pr2 >/dev/null
25
+ g checkout -q pr1 2>/dev/null
26
+ if g merge -q --no-edit pr2 >/dev/null 2>&1; then bad "plain merge of two same-spot appends should conflict"; else ok; fi
27
+ g merge --abort >/dev/null 2>&1
28
+ printf 'ci/tests.list merge=union\n' > "$W/.git/info/attributes"
29
+ if g merge -q --no-edit pr2 >/dev/null 2>&1 && grep -qxF test-pr1 "$W/ci/tests.list" && grep -qxF test-pr2 "$W/ci/tests.list"; then ok; else bad "union merge of two same-spot appends should be clean with both lines"; fi
30
+ rm -f "$W/.git/info/attributes"
31
+
32
+ # 2. --check-union accepts a plain one-line-per-entry list, existing in the repo or not
33
+ out=$(bash "$DA" --check-union ci/tests.list --repo "$W" 2>&1); rc=$?
34
+ [ "$rc" -eq 0 ] && ok || bad "check-union ci/tests.list expected rc 0, got $rc ($out)"
35
+ out=$(bash "$DA" --check-union new/dir/registry.txt --repo "$W" 2>&1); rc=$?
36
+ [ "$rc" -eq 0 ] && ok || bad "check-union on a new path expected rc 0, got $rc ($out)"
37
+ # the probe never touches the caller's repo
38
+ [ -z "$(g status --porcelain)" ] && [ ! -e "$W/.git/info/attributes" ] && ok || bad "check-union modified the caller repo"
39
+
40
+ # 3. structured files are rejected
41
+ for f in conf.yml data.json pyproject.toml package-lock.json notes.md; do
42
+ out=$(bash "$DA" --check-union "$f" --repo "$W" 2>&1); rc=$?
43
+ [ "$rc" -eq 1 ] && ok || bad "check-union $f expected rc 1, got $rc"
44
+ done
45
+ # unsafe paths
46
+ out=$(bash "$DA" --check-union ../escape --repo "$W" 2>&1); rc=$?; [ "$rc" -eq 2 ] && ok || bad "parent path expected rc 2, got $rc"
47
+ out=$(bash "$DA" --check-union 'a b.txt' --repo "$W" 2>&1); rc=$?; [ "$rc" -eq 1 ] && ok || bad "whitespace path expected rc 1, got $rc"
48
+ out=$(bash "$DA" 2>&1); rc=$?; [ "$rc" -eq 2 ] && ok || bad "no mode expected rc 2, got $rc"
49
+
50
+ # 4. --detect ranks an add-only, high-churn file and ignores a file with deletions or low churn
51
+ i=0
52
+ while [ "$i" -lt 6 ]; do
53
+ i=$((i+1)); printf 'line-%s\n' "$i" >> "$W/ci/tests.list"
54
+ printf 'v%s\n' "$i" > "$W/other.txt" # rewritten each time: add + delete
55
+ g add -A >/dev/null; g commit -q -m "c$i" >/dev/null
56
+ done
57
+ out=$(bash "$DA" --detect --repo "$W" --min-commits 5 2>&1); rc=$?
58
+ [ "$rc" -eq 0 ] && ok || bad "detect expected rc 0, got $rc"
59
+ printf '%s\n' "$out" | grep -qF ' ci/tests.list' && ok || bad "detect should list ci/tests.list (got: $out)"
60
+ printf '%s\n' "$out" | grep -qF ' other.txt' && bad "detect must not list a rewritten file (got: $out)" || ok
61
+ printf '%s\n' "$out" | grep -qF ' conf.yml' && bad "detect must not list a low-churn file (got: $out)" || ok
62
+
63
+ echo "detect-append-registries.test.sh: $PASS passed, $FAILN failed"
64
+ [ "$FAILN" -eq 0 ]
@@ -0,0 +1,11 @@
1
+ pr:
2
+ branches:
3
+ include: [main]
4
+ pool:
5
+ vmImage: ubuntu-latest
6
+ variables:
7
+ greeting: hi
8
+ steps:
9
+ - script: echo "$(greeting) azure"
10
+ displayName: say
11
+ - task: SomeUnknownTask@1
@@ -0,0 +1,12 @@
1
+ image: atlassian/default-image:4
2
+ pipelines:
3
+ default:
4
+ - step:
5
+ name: Build and test
6
+ script:
7
+ - echo bitbucket-test
8
+ - step:
9
+ name: Deploy
10
+ deployment: production
11
+ script:
12
+ - echo deploy
@@ -0,0 +1,10 @@
1
+ steps:
2
+ - label: ":hammer: test"
3
+ command: echo bk-test
4
+ - wait
5
+ - label: "docker plugin"
6
+ command: echo bk-docker
7
+ plugins:
8
+ - docker#v5.0.0:
9
+ image: node:20
10
+ - block: ":rocket: release"
@@ -0,0 +1,29 @@
1
+ version: 2.1
2
+ jobs:
3
+ test:
4
+ docker:
5
+ - image: cimg/base:stable
6
+ steps:
7
+ - checkout
8
+ - run:
9
+ name: unit
10
+ command: echo circle-unit
11
+ secured:
12
+ docker:
13
+ - image: cimg/base:stable
14
+ steps:
15
+ - run: echo secured
16
+ deploy-prod:
17
+ docker:
18
+ - image: cimg/base:stable
19
+ steps:
20
+ - run: echo deploy
21
+ workflows:
22
+ main:
23
+ jobs:
24
+ - test
25
+ - secured:
26
+ context: org-secrets
27
+ requires: [test]
28
+ - deploy-prod:
29
+ requires: [test]