forgedock 1.0.21 → 1.1.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/README.md +227 -174
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +386 -0
- package/bin/engine/phases.mjs +148 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +27 -0
- package/bin/engine-cli.mjs +50 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +693 -1393
- package/bin/init-detect.mjs +95 -0
- package/bin/journey.mjs +783 -0
- package/bin/runner.mjs +847 -0
- package/bin/settings-hook.mjs +112 -0
- package/bin/tui.mjs +6 -3
- package/commands/autopilot.md +4 -4
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +11 -5
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +2 -1
- package/commands/issue.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +346 -34
- package/commands/quality-gate.md +168 -80
- package/commands/review-pr-agents.md +69 -45
- package/commands/review-pr.md +54 -6
- package/commands/scope.md +11 -3
- package/commands/security-audit.md +24 -16
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +47 -3
- package/commands/work-on/build/context.md +36 -5
- package/commands/work-on/build/implement.md +16 -4
- package/commands/work-on/build/validate.md +54 -8
- package/commands/work-on/build.md +27 -9
- package/commands/work-on/close.md +131 -3
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on/review.md +24 -0
- package/commands/work-on-monolithic.md +1 -0
- package/commands/work-on.md +271 -11
- package/docs/BRAND.md +6 -2
- package/docs/CONFIG.md +39 -34
- package/docs/site/.vitepress/config.mjs +85 -0
- package/docs/site/annotations-explained.md +55 -0
- package/docs/site/command-learning-path.md +104 -0
- package/docs/site/command-reference.md +3 -0
- package/docs/site/getting-started.md +42 -13
- package/docs/site/index.md +1 -1
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/examples/forgedock-demo/README.md +113 -0
- package/examples/forgedock-demo/bootstrap.sh +125 -0
- package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
- package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
- package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
- package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
- package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
- package/examples/forgedock-demo/labels.json +21 -0
- package/examples/forgedock-demo/package.json +16 -0
- package/examples/forgedock-demo/scripts/smoke.js +61 -0
- package/examples/forgedock-demo/src/auth.js +28 -0
- package/examples/forgedock-demo/src/db.js +66 -0
- package/examples/forgedock-demo/src/routes/notes.js +64 -0
- package/examples/forgedock-demo/src/server.js +79 -0
- package/package.json +13 -3
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- package/templates/forge.yaml.minimal +44 -0
package/commands/quality-gate.md
CHANGED
|
@@ -39,10 +39,10 @@ This quality gate uses **domain detection** to adapt to your project's actual te
|
|
|
39
39
|
| FRONTEND | `.tsx`/`.ts` files in client component paths |
|
|
40
40
|
| PROXY | Client components using `fetch`/`useSWR`/`apiFetch` |
|
|
41
41
|
| SHELL | `.sh` files or scripts using `curl`/`wget` |
|
|
42
|
-
| STRING_SETS | Python files in anti-detection or browser automation paths |
|
|
42
|
+
| STRING_SETS | *(scraping pack — opt-in)* Python files in anti-detection or browser automation paths. Enable via `forge.yaml → quality_gate.optional_domains: [STRING_SETS]` |
|
|
43
43
|
| CONCURRENCY | Python files using `asyncio.shield`, `asyncio.wait_for`, or `Task.cancel` |
|
|
44
44
|
| STATE | Python files with new module-level mutable variable assignments |
|
|
45
|
-
| CAPACITY | Worker/infra Python files with new size/limit/threshold constants |
|
|
45
|
+
| CAPACITY | *(scraping pack — opt-in)* Worker/infra Python files with new size/limit/threshold constants. Enable via `forge.yaml → quality_gate.optional_domains: [CAPACITY]` |
|
|
46
46
|
| WORKFLOW | `.yml` files in `.github/workflows/` |
|
|
47
47
|
| IMPORT_RESOLUTION | Python files with new `from app.*` imports added outside `try:` blocks |
|
|
48
48
|
| INFRA | Dockerfiles or entrypoints with runtime UID changes |
|
|
@@ -92,10 +92,10 @@ Before running checks, classify the changed files into domains. This avoids runn
|
|
|
92
92
|
| `*.tsx`, `*.ts` in `web/src/` (excluding `route.ts`) | FRONTEND |
|
|
93
93
|
| `*.tsx`, `*.ts` client components with fetch/useSWR | PROXY |
|
|
94
94
|
| `*.sh` files or scripts with `curl`/`wget` | SHELL |
|
|
95
|
-
| `*.py` in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` | STRING_SETS |
|
|
95
|
+
| `*.py` in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` | STRING_SETS *(scraping pack — opt-in, see 2J)* |
|
|
96
96
|
| `*.py` containing `asyncio.shield`, `asyncio.wait_for`, or `Task.cancel` | CONCURRENCY |
|
|
97
97
|
| `*.py` files with new module-level `dict`, `set`, `Counter`, `Lock`, or `defaultdict` assignments | STATE |
|
|
98
|
-
| `*.py` in `services/worker/`, `infra/`, or `browser/` with new assignments to variables containing `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT` | CAPACITY |
|
|
98
|
+
| `*.py` in `services/worker/`, `infra/`, or `browser/` with new assignments to variables containing `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT` | CAPACITY *(scraping pack — opt-in, see 2K)* |
|
|
99
99
|
| `*.yml` in `.github/workflows/` | WORKFLOW |
|
|
100
100
|
| `*.py` with new `from app.` import statements added outside `try:` blocks | IMPORT_RESOLUTION |
|
|
101
101
|
| `Dockerfile*` or `entrypoint*.sh` with added/changed `USER`, `su-exec`, `gosu`, or `setuid` | INFRA |
|
|
@@ -109,7 +109,12 @@ Before running checks, classify the changed files into domains. This avoids runn
|
|
|
109
109
|
# Initialize domain flags
|
|
110
110
|
DOMAINS=""
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
# {CHANGED_FILES} is a space-separated argument (same contract as {AFFECTED_FILES}
|
|
113
|
+
# in architect.md) — split explicitly on IFS=' ' into an array instead of relying
|
|
114
|
+
# on a bare `for f in {CHANGED_FILES}`, which word-splits on the shell's default
|
|
115
|
+
# IFS (space, tab, AND newline) and would corrupt any path containing a space.
|
|
116
|
+
IFS=' ' read -ra CHANGED_FILES_ARR <<< "{CHANGED_FILES}"
|
|
117
|
+
for f in "${CHANGED_FILES_ARR[@]}"; do
|
|
113
118
|
case "$f" in
|
|
114
119
|
*.sql|*migrations/*) DOMAINS="$DOMAINS DATABASE" ;;
|
|
115
120
|
esac
|
|
@@ -126,7 +131,12 @@ for f in {CHANGED_FILES}; do
|
|
|
126
131
|
esac
|
|
127
132
|
case "$f" in
|
|
128
133
|
*anti_detection/*|*consumers/*|*browser/*|*shared/detection/*)
|
|
129
|
-
|
|
134
|
+
# STRING_SETS is a scraping-pack domain — opt-in via forge.yaml → quality_gate.optional_domains
|
|
135
|
+
# <!-- Updated: forge#1349 — not enabled by default; scraping-product-specific -->
|
|
136
|
+
if echo "$f" | grep -qE '\.py$'; then
|
|
137
|
+
_OPT_DOMAINS=$(grep -A5 'optional_domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null | grep -oE 'STRING_SETS' | head -1 || true)
|
|
138
|
+
[ -n "$_OPT_DOMAINS" ] && DOMAINS="$DOMAINS STRING_SETS"
|
|
139
|
+
fi
|
|
130
140
|
;;
|
|
131
141
|
esac
|
|
132
142
|
case "$f" in
|
|
@@ -144,34 +154,45 @@ grep -lE "asyncio\.shield|asyncio\.wait_for|Task\.cancel" {CHANGED_FILES} 2>/dev
|
|
|
144
154
|
grep -lE "^\w+ = (\{\}|\[\]|set\(\)|Lock\(\)|defaultdict|Counter)" {CHANGED_FILES} 2>/dev/null | grep -qE '\.py$' && DOMAINS="$DOMAINS STATE"
|
|
145
155
|
|
|
146
156
|
# Check for capacity constants in worker/infra/browser Python files
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
# CAPACITY is a scraping-pack domain — opt-in via forge.yaml → quality_gate.optional_domains
|
|
158
|
+
# <!-- Updated: forge#1349 — not enabled by default; scraping-product-specific -->
|
|
159
|
+
_OPT_CAPACITY=$(grep -A5 'optional_domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null | grep -oE 'CAPACITY' | head -1 || true)
|
|
160
|
+
if [ -n "$_OPT_CAPACITY" ]; then
|
|
161
|
+
while IFS= read -r f; do
|
|
162
|
+
[ -z "$f" ] && continue
|
|
163
|
+
git diff HEAD -- "$f" 2>/dev/null | grep -qE '^\+[A-Za-z_]\w*(MB|SIZE|MAX|LIMIT|THRESHOLD|TIMEOUT)\w*\s*=' && DOMAINS="$DOMAINS CAPACITY" && break
|
|
164
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'services/worker/|infra/|browser/')
|
|
165
|
+
fi
|
|
150
166
|
|
|
151
167
|
# Check for env var usage in changed files
|
|
152
168
|
grep -lE "os\.getenv|process\.env" {CHANGED_FILES} 2>/dev/null && DOMAINS="$DOMAINS DEPLOY"
|
|
153
169
|
|
|
154
170
|
# Check for new app.* imports added outside try: blocks in Python files
|
|
155
|
-
|
|
171
|
+
while IFS= read -r f; do
|
|
172
|
+
[ -z "$f" ] && continue
|
|
156
173
|
NEW_IMPORTS=$(git diff HEAD -- "$f" 2>/dev/null | grep -E '^\+\s*(from app\.|import app\.)' | grep -v '^+++')
|
|
157
174
|
if [ -n "$NEW_IMPORTS" ]; then
|
|
158
175
|
DOMAINS="$DOMAINS IMPORT_RESOLUTION"
|
|
159
176
|
break
|
|
160
177
|
fi
|
|
161
|
-
done
|
|
178
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
162
179
|
|
|
163
180
|
# Check for runtime UID changes in Dockerfiles or entrypoint scripts
|
|
164
|
-
|
|
181
|
+
while IFS= read -r f; do
|
|
182
|
+
[ -z "$f" ] && continue
|
|
165
183
|
git diff HEAD -- "$f" 2>/dev/null | grep -E '^\+.*(USER\s+[^0]|su-exec|gosu|setuid)' | grep -v '^+++' | grep -q . && DOMAINS="$DOMAINS INFRA" && break
|
|
166
|
-
done
|
|
184
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -iE 'Dockerfile|entrypoint.*\.sh')
|
|
167
185
|
|
|
168
186
|
# Check for gate condition narrowing in router Python files (condition removal = fix may be incomplete across siblings)
|
|
169
|
-
|
|
187
|
+
while IFS= read -r f; do
|
|
188
|
+
[ -z "$f" ] && continue
|
|
170
189
|
git diff HEAD -- "$f" 2>/dev/null | grep -E '^-.*\bif\b.*\bor\b' | grep -v '^---' | grep -q . && DOMAINS="$DOMAINS ROUTER_BUG" && break
|
|
171
|
-
done
|
|
190
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'router')
|
|
172
191
|
|
|
173
192
|
# Check for external tool config files (structural schema validation advisory)
|
|
174
|
-
|
|
193
|
+
# {CHANGED_FILES} is space-separated (see array-split note above) — reuse the
|
|
194
|
+
# same CHANGED_FILES_ARR built above instead of re-splitting.
|
|
195
|
+
for f in "${CHANGED_FILES_ARR[@]}"; do
|
|
175
196
|
case "$f" in
|
|
176
197
|
traefik/*|infra/nginx/*|k8s/*|terraform/*)
|
|
177
198
|
DOMAINS="$DOMAINS CONFIG_SCHEMA" && break ;;
|
|
@@ -228,7 +249,11 @@ For every changed file, check:
|
|
|
228
249
|
|
|
229
250
|
```bash
|
|
230
251
|
# Quick automated checks
|
|
231
|
-
|
|
252
|
+
# {CHANGED_FILES} is a space-separated argument — split explicitly on IFS=' '
|
|
253
|
+
# into an array instead of a bare `for f in {CHANGED_FILES}` (see Step 1.5 for
|
|
254
|
+
# the full rationale; this is a separate bash block so the array is rebuilt here).
|
|
255
|
+
IFS=' ' read -ra CHANGED_FILES_ARR <<< "{CHANGED_FILES}"
|
|
256
|
+
for f in "${CHANGED_FILES_ARR[@]}"; do
|
|
232
257
|
# f-string SQL
|
|
233
258
|
grep -nE "f['\"].*SELECT|f['\"].*INSERT|f['\"].*UPDATE|f['\"].*DELETE|f['\"].*WHERE" "$f" 2>/dev/null && echo "SEC: f-string SQL in $f"
|
|
234
259
|
# Hardcoded secrets
|
|
@@ -251,15 +276,28 @@ done
|
|
|
251
276
|
### 2B: Auth conventions (Python routers, TypeScript route handlers)
|
|
252
277
|
|
|
253
278
|
For new/modified endpoints:
|
|
254
|
-
1. Which auth dependency is used
|
|
255
|
-
2. Does the
|
|
256
|
-
|
|
279
|
+
1. Which auth dependency is used? Read `forge.yaml → review.domains.auth` for the configured dependency name(s). If absent, skip the naming check and fall back to generic resource-ownership verification only.
|
|
280
|
+
2. Does the endpoint check resource ownership (`user_id` or equivalent) before returning data?
|
|
281
|
+
|
|
282
|
+
Route-path-to-dependency convention (e.g. "dashboard routes must use X, public routes must use Y") is project-specific. Configure it in `forge.yaml → review.domains.auth` if your project uses one. Do not enforce a hard-coded convention universally. <!-- Updated: forge#1349 — removed hardcoded SessionUser/CurrentUser rule -->
|
|
257
283
|
|
|
258
284
|
```bash
|
|
259
|
-
|
|
260
|
-
|
|
285
|
+
# Read configured auth dependency names from forge.yaml (if present)
|
|
286
|
+
AUTH_DEPS=""
|
|
287
|
+
if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
|
|
288
|
+
AUTH_DEPS=$(grep -A5 'domains:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
|
|
289
|
+
| grep -A3 'auth:' | grep -oE '[A-Za-z_]+User|[A-Za-z_]+Auth|get_[a-z_]+' | sort -u | tr '\n' '|' | sed 's/|$//')
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
while IFS= read -r f; do
|
|
293
|
+
[ -z "$f" ] && continue
|
|
294
|
+
if [ -n "$AUTH_DEPS" ]; then
|
|
295
|
+
grep -nE "($AUTH_DEPS)|Depends\(get_" "$f" 2>/dev/null
|
|
296
|
+
else
|
|
297
|
+
grep -nE "Depends\(get_" "$f" 2>/dev/null
|
|
298
|
+
fi
|
|
261
299
|
grep -nE "@router\.(get|post|put|delete)" "$f" 2>/dev/null
|
|
262
|
-
done
|
|
300
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E 'router|route')
|
|
263
301
|
```
|
|
264
302
|
|
|
265
303
|
### 2C: Deploy chain (when new env vars are introduced)
|
|
@@ -270,8 +308,13 @@ if [ -n "$NEW_ENVS" ]; then
|
|
|
270
308
|
for var in $NEW_ENVS; do
|
|
271
309
|
# Check if it exists in .env.example
|
|
272
310
|
grep -q "$var" .env.example 2>/dev/null || echo "DEPLOY: $var not in .env.example"
|
|
273
|
-
# Check
|
|
274
|
-
|
|
311
|
+
# Check decrypt-secrets.sh ENV_MAPPING only when the SOPS mapping script exists.
|
|
312
|
+
# Emitting a finding for a file that doesn't exist in the repo produces false BLOCKINGs
|
|
313
|
+
# on non-SOPS repos. Use verify-sops-chain.sh for the full SOPS chain check instead.
|
|
314
|
+
# <!-- Updated: forge#1349 — gate on file presence -->
|
|
315
|
+
if [ -f "{WORKTREE_PATH}/scripts/decrypt-secrets.sh" ]; then
|
|
316
|
+
grep -q "$var" "{WORKTREE_PATH}/scripts/decrypt-secrets.sh" 2>/dev/null || echo "DEPLOY: $var not in decrypt-secrets.sh ENV_MAPPING"
|
|
317
|
+
fi
|
|
275
318
|
done
|
|
276
319
|
fi
|
|
277
320
|
```
|
|
@@ -286,34 +329,31 @@ For each `.sql` file:
|
|
|
286
329
|
5. **Migration number collision**: Check against origin branch
|
|
287
330
|
|
|
288
331
|
```bash
|
|
289
|
-
|
|
332
|
+
while IFS= read -r f; do
|
|
333
|
+
[ -z "$f" ] && continue
|
|
290
334
|
grep -nE "ADD COLUMN.*NOT NULL" "$f" | grep -v "DEFAULT" && echo "DB: NOT NULL without DEFAULT in $f"
|
|
291
335
|
grep -nE "DROP (TABLE|COLUMN|INDEX)" "$f" | grep -v "IF EXISTS" && echo "DB: DROP without IF EXISTS in $f"
|
|
292
336
|
grep -nE "SELECT.*FROM" "$f" | grep -v "LIMIT" | grep -v "WHERE.*created_at" && echo "DB: possibly unbounded query in $f"
|
|
293
|
-
done
|
|
337
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep '\.sql$')
|
|
294
338
|
|
|
295
339
|
# Migration prefix collision scan (Deploy Gate — HIGH)
|
|
296
|
-
# Runs whenever any SQL file changed. Scans the
|
|
297
|
-
# numeric prefixes
|
|
298
|
-
#
|
|
340
|
+
# Runs whenever any SQL file changed. Scans the configured migrations directory for
|
|
341
|
+
# duplicate numeric prefixes. Duplicate prefixes can cause ordering conflicts during deploy.
|
|
342
|
+
# <!-- Updated: forge#1349 — removed stale validate-migration-order.sh reference (script does not exist) -->
|
|
299
343
|
MIGRATIONS_DIR="{WORKTREE_PATH}/infra/migrations"
|
|
344
|
+
# Support configured migration directory from forge.yaml (if set)
|
|
345
|
+
if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
|
|
346
|
+
_CONFIGURED_MIGRATIONS=$(grep -E '^\s*migrations_dir:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
|
|
347
|
+
| head -1 | sed 's/.*migrations_dir:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
348
|
+
[ -n "$_CONFIGURED_MIGRATIONS" ] && MIGRATIONS_DIR="{WORKTREE_PATH}/${_CONFIGURED_MIGRATIONS}"
|
|
349
|
+
fi
|
|
300
350
|
if [ -d "$MIGRATIONS_DIR" ]; then
|
|
301
|
-
# Extract grandfathered prefixes from validate-migration-order.sh if it exists
|
|
302
|
-
GRANDFATHER_SCRIPT="{WORKTREE_PATH}/scripts/validate-migration-order.sh"
|
|
303
|
-
GRANDFATHERED=""
|
|
304
|
-
if [ -f "$GRANDFATHER_SCRIPT" ]; then
|
|
305
|
-
GRANDFATHERED=$(grep -oE '[0-9]{4}' "$GRANDFATHER_SCRIPT" | sort -u | tr '\n' ' ')
|
|
306
|
-
fi
|
|
307
351
|
# Find duplicate numeric prefixes across all migration files
|
|
308
352
|
DUPLICATE_PREFIXES=$(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null \
|
|
309
353
|
| sed 's|.*/\([0-9]*\)_.*|\1|' \
|
|
310
354
|
| sort | uniq -d)
|
|
311
355
|
for prefix in $DUPLICATE_PREFIXES; do
|
|
312
|
-
|
|
313
|
-
echo "DB: migration prefix $prefix is duplicated but grandfathered (INFO)"
|
|
314
|
-
else
|
|
315
|
-
echo "DB-COLLISION | HIGH | infra/migrations/ | Duplicate migration prefix $prefix is NOT grandfathered — will hard-fail deploy-production.yml validate-migration-order.sh gate. Classify as CRITICAL BLOCKER."
|
|
316
|
-
fi
|
|
356
|
+
echo "DB-COLLISION | HIGH | $MIGRATIONS_DIR | Duplicate migration prefix $prefix — ordering conflict will occur during deploy. Classify as CRITICAL BLOCKER."
|
|
317
357
|
done
|
|
318
358
|
fi
|
|
319
359
|
```
|
|
@@ -330,7 +370,8 @@ For each `.tsx`/`.ts` client component:
|
|
|
330
370
|
7. **Rules of Hooks**: Do function components avoid early returns before hook calls?
|
|
331
371
|
|
|
332
372
|
```bash
|
|
333
|
-
|
|
373
|
+
while IFS= read -r f; do
|
|
374
|
+
[ -z "$f" ] && continue
|
|
334
375
|
# Check for useEffect without cleanup (basic heuristic)
|
|
335
376
|
grep -c "useEffect" "$f" 2>/dev/null
|
|
336
377
|
grep -c "return.*cleanup\|return.*\(\) =>" "$f" 2>/dev/null
|
|
@@ -370,15 +411,35 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.tsx?$' | grep -v 'rou
|
|
|
370
411
|
}
|
|
371
412
|
in_component && /\buse[A-Z]/ { found_hook=1 }
|
|
372
413
|
' FILENAME="$f" "$f" 2>/dev/null
|
|
373
|
-
done
|
|
414
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.tsx?$' | grep -v 'route\.ts')
|
|
374
415
|
```
|
|
375
416
|
|
|
376
417
|
### 2F: Frontend proxy wiring (client-side fetch calls)
|
|
377
418
|
|
|
419
|
+
The direct-backend path prefix to flag is read from `forge.yaml → review.layout.direct_api_prefix`.
|
|
420
|
+
If absent, defaults to `/api/v1/` (backward compatible). This prevents false findings on projects
|
|
421
|
+
that use a different API path scheme. <!-- Updated: forge#1349 — gate on forge.yaml, not hardcoded prefix -->
|
|
422
|
+
|
|
378
423
|
```bash
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
424
|
+
# Read configured direct API prefix from forge.yaml, fall back to /api/v1/
|
|
425
|
+
DIRECT_API_PREFIX="/api/v1/"
|
|
426
|
+
if [ -f "{WORKTREE_PATH}/forge.yaml" ]; then
|
|
427
|
+
_CONFIGURED_PREFIX=$(grep -E '^\s*direct_api_prefix:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
|
|
428
|
+
| head -1 | sed 's/.*direct_api_prefix:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
429
|
+
# Also check under review.layout section
|
|
430
|
+
if [ -z "$_CONFIGURED_PREFIX" ]; then
|
|
431
|
+
_CONFIGURED_PREFIX=$(grep -A 10 'layout:' "{WORKTREE_PATH}/forge.yaml" 2>/dev/null \
|
|
432
|
+
| grep -E '^\s*direct_api_prefix:' \
|
|
433
|
+
| head -1 | sed 's/.*direct_api_prefix:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
434
|
+
fi
|
|
435
|
+
[ -n "$_CONFIGURED_PREFIX" ] && DIRECT_API_PREFIX="$_CONFIGURED_PREFIX"
|
|
436
|
+
fi
|
|
437
|
+
ESCAPED_PREFIX=$(printf '%s' "$DIRECT_API_PREFIX" | sed 's|/|\\/|g')
|
|
438
|
+
|
|
439
|
+
while IFS= read -r f; do
|
|
440
|
+
[ -z "$f" ] && continue
|
|
441
|
+
grep -nE "(fetch|useSWR|apiFetch)\\s*[(<]\\s*[\`\"']${ESCAPED_PREFIX}" "$f" 2>/dev/null && echo "PROXY: direct ${DIRECT_API_PREFIX} call in $f — must use /api/ proxy (configure review.layout.direct_api_prefix in forge.yaml to change the flagged prefix)"
|
|
442
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.(tsx?|jsx?)$' | grep -v 'route\.ts$')
|
|
382
443
|
```
|
|
383
444
|
|
|
384
445
|
### 2G: Cross-service integration (shell scripts with curl/wget)
|
|
@@ -413,7 +474,8 @@ The script `verify-host-headers.sh` checks for the `FORGE_INTERNAL_PATTERNS` env
|
|
|
413
474
|
**Why this matters**: Every DB connection function in a deploy script must use `${POSTGRES_HOST:-localhost}` (or equivalent env var interpolation), not a bare `"localhost"` string literal. A hardcoded `"localhost"` works in local development but fails in any environment where PostgreSQL runs on a separate host. The pattern is reliably grep-detectable. <!-- Added: forge#303 -->
|
|
414
475
|
|
|
415
476
|
```bash
|
|
416
|
-
|
|
477
|
+
while IFS= read -r f; do
|
|
478
|
+
[ -z "$f" ] && continue
|
|
417
479
|
# Only check files that contain DB connection tools
|
|
418
480
|
if grep -qE "PGPASSWORD|createdb|psql|pg_dump|pg_restore" "$f" 2>/dev/null; then
|
|
419
481
|
# Look for new/modified lines with hardcoded localhost in variable assignments
|
|
@@ -428,7 +490,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.sh$'); do
|
|
|
428
490
|
echo "$HARDCODED"
|
|
429
491
|
fi
|
|
430
492
|
fi
|
|
431
|
-
done
|
|
493
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.sh$')
|
|
432
494
|
```
|
|
433
495
|
|
|
434
496
|
Report any hit as **HIGH** — the DB connection will fail in any environment where PostgreSQL is not on `127.0.0.1:5432`.
|
|
@@ -494,7 +556,8 @@ Report each conflict as **HIGH** — a shadowed native command is a silent user-
|
|
|
494
556
|
**Severity**: MEDIUM (flags for review, not auto-blocking). The review agent's concurrency auditor verifies with full context.
|
|
495
557
|
|
|
496
558
|
```bash
|
|
497
|
-
|
|
559
|
+
while IFS= read -r f; do
|
|
560
|
+
[ -z "$f" ] && continue
|
|
498
561
|
# asyncio.shield usage — outer await may be reached with pending CancelledError
|
|
499
562
|
grep -nE "asyncio\.shield" "$f" 2>/dev/null && \
|
|
500
563
|
echo "ASYNC: asyncio.shield in $f — verify outer await isn't reachable with pending CancelledError. Prefer loop.create_task() for fire-and-forget."
|
|
@@ -509,7 +572,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$'); do
|
|
|
509
572
|
sed -n "$((LINENO+1)),$((LINENO+5))p" "$f" | grep -q "await" && \
|
|
510
573
|
echo "ASYNC: await in except/finally block at $f:$LINENO — may fail if CancelledError is pending"
|
|
511
574
|
done
|
|
512
|
-
done
|
|
575
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
513
576
|
```
|
|
514
577
|
|
|
515
578
|
|
|
@@ -532,7 +595,8 @@ For each new module-level state variable in the diff:
|
|
|
532
595
|
```
|
|
533
596
|
|
|
534
597
|
```bash
|
|
535
|
-
|
|
598
|
+
while IFS= read -r f; do
|
|
599
|
+
[ -z "$f" ] && continue
|
|
536
600
|
# Find new module-level state variables introduced in the diff
|
|
537
601
|
NEW_VARS=$(git diff HEAD -- "$f" 2>/dev/null | grep -E '^\+[A-Za-z_]\w* = (\{\}|\[\]|set\(\)|Lock\(\)|defaultdict|Counter)' | grep -oE '^[+][A-Za-z_]\w*' | tr -d '+' | sort -u)
|
|
538
602
|
for new_var in $NEW_VARS; do
|
|
@@ -549,16 +613,18 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$'); do
|
|
|
549
613
|
done
|
|
550
614
|
done
|
|
551
615
|
done
|
|
552
|
-
done
|
|
616
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
553
617
|
```
|
|
554
618
|
|
|
555
619
|
**Severity**: MEDIUM — missing a mutation site is usually a latent bug, not an immediate crash, but it causes incorrect behavior under concurrent load.
|
|
556
620
|
|
|
557
621
|
---
|
|
558
622
|
|
|
559
|
-
### 2J: String literal consistency (anti-detection/scraping code)
|
|
623
|
+
### 2J: String literal consistency (anti-detection/scraping code) *(Scraping Pack — opt-in)*
|
|
624
|
+
|
|
625
|
+
> **This domain is opt-in and disabled by default.** It is specific to scraping/anti-detection products and produces false findings on general repos. Enable it by adding `STRING_SETS` to `forge.yaml → quality_gate.optional_domains`. <!-- Added: forge#1349 -->
|
|
560
626
|
|
|
561
|
-
**Triggered when**: changed files include Python files in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` directories.
|
|
627
|
+
**Triggered when**: changed files include Python files in `anti_detection/`, `consumers/`, `browser/`, or `shared/detection/` directories AND `STRING_SETS` is in `forge.yaml → quality_gate.optional_domains`.
|
|
562
628
|
|
|
563
629
|
**Why this matters**: Detection keyword sets (e.g., `CHALLENGE_KEYWORDS`, `COMMON_COOKIES`) across related modules share a domain truth — if Akamai adds a new cookie marker, it must appear in ALL related sets. A typo or omission in one set silently breaks detection for that service path. This check catches both inter-file inconsistencies and intra-file comment/code drift.
|
|
564
630
|
|
|
@@ -567,11 +633,12 @@ For each changed file in the anti-detection/scraping domains:
|
|
|
567
633
|
**1. Identify modified string identifier sets** (frozensets, tuples, or lists of string literals used for detection):
|
|
568
634
|
|
|
569
635
|
```bash
|
|
570
|
-
|
|
636
|
+
while IFS= read -r f; do
|
|
637
|
+
[ -z "$f" ] && continue
|
|
571
638
|
echo "=== String sets in $f ==="
|
|
572
639
|
# Find named constants that are frozenset/tuple/list of strings
|
|
573
640
|
grep -nE "^[A-Z_]+ = (frozenset|tuple|set)\(\[?['\"]|^[A-Z_]+ = \(['\"]|^[A-Z_]+ = \[['\"]" "$f" 2>/dev/null
|
|
574
|
-
done
|
|
641
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_detection/|consumers/|browser/|shared/detection/')
|
|
575
642
|
```
|
|
576
643
|
|
|
577
644
|
**2. For each string in a modified set, check if it appears in sibling detection files:**
|
|
@@ -579,7 +646,8 @@ done
|
|
|
579
646
|
```bash
|
|
580
647
|
SERVICE_DIR=$(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E 'anti_detection/|consumers/|browser/' | head -1 | grep -oE '^services/[^/]+/[^/]+' || echo "services/worker/worker")
|
|
581
648
|
|
|
582
|
-
|
|
649
|
+
while IFS= read -r f; do
|
|
650
|
+
[ -z "$f" ] && continue
|
|
583
651
|
# Extract string values from detection sets in this file
|
|
584
652
|
STRINGS=$(grep -oE "'[a-z_][a-z0-9_-]*'" "$f" 2>/dev/null | tr -d "'" | sort -u)
|
|
585
653
|
for s in $STRINGS; do
|
|
@@ -587,13 +655,14 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_
|
|
|
587
655
|
FOUND=$(grep -rl "\"$s\"\|'$s'" $SERVICE_DIR/ 2>/dev/null | grep -v "^$f$" | grep '\.py$' | head -1)
|
|
588
656
|
[ -z "$FOUND" ] && echo "STR: '$s' in $f appears NOWHERE else in $SERVICE_DIR — possible typo or dead entry"
|
|
589
657
|
done
|
|
590
|
-
done
|
|
658
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_detection/|consumers/|browser/|shared/detection/')
|
|
591
659
|
```
|
|
592
660
|
|
|
593
661
|
**3. Check for identifiers mentioned in comments/docstrings of the SAME file that are NOT in the actual set** (catches the `abck` vs `_abck` class of bug):
|
|
594
662
|
|
|
595
663
|
```bash
|
|
596
|
-
|
|
664
|
+
while IFS= read -r f; do
|
|
665
|
+
[ -z "$f" ] && continue
|
|
597
666
|
echo "=== Comment/code drift in $f ==="
|
|
598
667
|
# Extract string literals from actual sets (remove quotes)
|
|
599
668
|
SET_STRINGS=$(grep -oE "frozenset\(\[([^]]+)\]\)" "$f" 2>/dev/null | grep -oE "'[^']+'" | tr -d "'" | sort -u)
|
|
@@ -605,20 +674,25 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_
|
|
|
605
674
|
for ref in $COMMENT_REFS; do
|
|
606
675
|
echo "$SET_STRINGS" | grep -q "^${ref}$" || echo "STR: '$ref' mentioned in comment in $f but NOT found in any string set — possible omission or typo"
|
|
607
676
|
done
|
|
608
|
-
done
|
|
677
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_detection/|consumers/|browser/|shared/detection/')
|
|
609
678
|
```
|
|
610
679
|
|
|
611
680
|
**4. Cross-reference sibling sets in related modules** (the primary inter-file check):
|
|
612
681
|
|
|
613
682
|
```bash
|
|
614
683
|
# Find all detection-related Python files in the same service
|
|
615
|
-
|
|
684
|
+
while IFS= read -r f; do
|
|
685
|
+
[ -z "$f" ] && continue
|
|
616
686
|
SIBLINGS=$(find $(dirname "$f")/../ -name "*.py" -path "*/anti_detection/*" -o -name "*.py" -path "*/consumers/*" -o -name "*.py" -path "*/browser/*" 2>/dev/null | grep -v "^$f$" | head -10)
|
|
617
687
|
|
|
618
688
|
# Extract detection set names from the changed file
|
|
619
689
|
SET_NAMES=$(grep -oE "^[A-Z_]+ = (frozenset|tuple|set)" "$f" 2>/dev/null | grep -oE "^[A-Z_]+")
|
|
620
690
|
|
|
621
|
-
|
|
691
|
+
# $SIBLINGS is one file path per line (`find`) — herestring (not a piped
|
|
692
|
+
# `| while read`, which would run the loop body in a subshell and discard
|
|
693
|
+
# any accumulator set inside it) preserves newline-safety for paths with spaces.
|
|
694
|
+
while IFS= read -r sib; do
|
|
695
|
+
[ -z "$sib" ] && continue
|
|
622
696
|
# Extract strings from sibling detection sets
|
|
623
697
|
SIB_STRINGS=$(grep -oE "'[a-z_][a-z0-9_-]*'" "$sib" 2>/dev/null | tr -d "'" | sort -u)
|
|
624
698
|
|
|
@@ -629,8 +703,8 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_
|
|
|
629
703
|
echo "$SIB_STRINGS" | grep -q "^${s}$" || \
|
|
630
704
|
echo "STR: '$s' in $f not found in sibling file $sib — verify if cross-module consistency is required"
|
|
631
705
|
done
|
|
632
|
-
done
|
|
633
|
-
done
|
|
706
|
+
done <<< "$SIBLINGS"
|
|
707
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_detection/|consumers/|browser/')
|
|
634
708
|
```
|
|
635
709
|
|
|
636
710
|
**Flag as findings**:
|
|
@@ -640,16 +714,19 @@ done
|
|
|
640
714
|
|
|
641
715
|
---
|
|
642
716
|
|
|
643
|
-
### 2K: Capacity constant validation (worker/infra code)
|
|
717
|
+
### 2K: Capacity constant validation (worker/infra code) *(Scraping Pack — opt-in)*
|
|
718
|
+
|
|
719
|
+
> **This domain is opt-in and disabled by default.** It is specific to scraping/browser-pool products and produces false findings on general repos. Enable it by adding `CAPACITY` to `forge.yaml → quality_gate.optional_domains`. <!-- Added: forge#1349 -->
|
|
644
720
|
|
|
645
|
-
**Triggered when**: changed Python files in `services/worker/`, `infra/`, or `browser/` introduce new assignments to variables whose names contain `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT`.
|
|
721
|
+
**Triggered when**: changed Python files in `services/worker/`, `infra/`, or `browser/` introduce new assignments to variables whose names contain `MB`, `SIZE`, `MAX`, `LIMIT`, `THRESHOLD`, or `TIMEOUT` AND `CAPACITY` is in `forge.yaml → quality_gate.optional_domains`.
|
|
646
722
|
|
|
647
723
|
**Why this matters**: Hardcoded capacity constants (e.g., `_BROWSER_ESTIMATED_MB = 200`) are design-time guesses that drift from production reality. When used in guard conditions (e.g., "5 × 200 MB = 1000 MB peak"), a wrong constant directly limits system throughput and may never be caught until a slot fails to spawn. A constant is correctly-typed and correctly-used but factually wrong — logic checks pass, the bug ships.
|
|
648
724
|
|
|
649
725
|
For each new or modified capacity constant in the diff:
|
|
650
726
|
|
|
651
727
|
```bash
|
|
652
|
-
|
|
728
|
+
while IFS= read -r f; do
|
|
729
|
+
[ -z "$f" ] && continue
|
|
653
730
|
# Find new capacity constant assignments introduced by this diff
|
|
654
731
|
NEW_CAPS=$(git diff HEAD -- "$f" 2>/dev/null | grep -E '^\+[A-Za-z_]\w*(MB|SIZE|MAX|LIMIT|THRESHOLD|TIMEOUT)\w*\s*=' | grep -oE '^[+][A-Za-z_]\w+' | tr -d '+' | sort -u)
|
|
655
732
|
for cap_var in $NEW_CAPS; do
|
|
@@ -667,7 +744,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'servi
|
|
|
667
744
|
git log --all -15 --oneline --grep="memory" 2>/dev/null | head -5
|
|
668
745
|
git log --all -15 --oneline --grep="$(echo $keywords | cut -d' ' -f1)" 2>/dev/null | head -5
|
|
669
746
|
done
|
|
670
|
-
done
|
|
747
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'services/worker/|infra/|browser/')
|
|
671
748
|
```
|
|
672
749
|
|
|
673
750
|
**Flag as findings**:
|
|
@@ -685,7 +762,8 @@ done
|
|
|
685
762
|
**Why this matters**: `appleboy/ssh-action` processes every `script:` field through Go's `text/template` engine **client-side, before SSH transmission**. Any `{{` in the script — including in comments, `docker ps --format` strings, and `docker inspect --format` strings — is interpreted as a Go template directive. If the expression does not resolve against the action's data context (which is an empty map for `appleboy/ssh-action`), the step exits with status 1 **before the script reaches the remote shell**. Shell error handlers (`|| fallback`, `2>/dev/null`, `set -e`) are completely bypassed. `continue-on-error: true` masks the failure silently, producing apparent deploy success with no remote shell output.
|
|
686
763
|
|
|
687
764
|
```bash
|
|
688
|
-
|
|
765
|
+
while IFS= read -r f; do
|
|
766
|
+
[ -z "$f" ] && continue
|
|
689
767
|
if grep -q "appleboy/ssh-action" "$f"; then
|
|
690
768
|
TEMPLATE_HITS=$(grep -n "{{" "$f")
|
|
691
769
|
if [ -n "$TEMPLATE_HITS" ]; then
|
|
@@ -702,7 +780,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.github/workflows/.*\.
|
|
|
702
780
|
echo " docker ps --format json | jq -r '.Status'"
|
|
703
781
|
fi
|
|
704
782
|
fi
|
|
705
|
-
done
|
|
783
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.github/workflows/.*\.yml$')
|
|
706
784
|
```
|
|
707
785
|
|
|
708
786
|
Report any hit as **CONFIRMED HIGH** — the step will exit 1 before reaching the remote shell.
|
|
@@ -718,7 +796,8 @@ Go template directives in `appleboy/ssh-action` `script:` blocks are preprocesse
|
|
|
718
796
|
```bash
|
|
719
797
|
PR_BASE="${PR_BASE:-staging}"
|
|
720
798
|
|
|
721
|
-
|
|
799
|
+
while IFS= read -r f; do
|
|
800
|
+
[ -z "$f" ] && continue
|
|
722
801
|
# Extract new app.* import statements added in the diff
|
|
723
802
|
# Only consider lines added (starting with +), skip diff headers (+++)
|
|
724
803
|
NEW_IMPORTS=$(git diff HEAD -- "$f" 2>/dev/null | grep -E '^\+\s*(from app\.|import app\.)' | grep -v '^+++' | sed 's/^+//')
|
|
@@ -752,7 +831,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$'); do
|
|
|
752
831
|
fi
|
|
753
832
|
fi
|
|
754
833
|
done
|
|
755
|
-
done
|
|
834
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
756
835
|
```
|
|
757
836
|
|
|
758
837
|
**Flag as findings**:
|
|
@@ -775,20 +854,24 @@ A builder agent working in a milestone-branch worktree has access to milestone-o
|
|
|
775
854
|
# Step 1: Identify the service directory from the changed Dockerfile/entrypoint path
|
|
776
855
|
# e.g. services/worker/Dockerfile → service root = services/worker/
|
|
777
856
|
SERVICE_DIR=""
|
|
778
|
-
|
|
857
|
+
while IFS= read -r f; do
|
|
858
|
+
[ -z "$f" ] && continue
|
|
779
859
|
SERVICE_DIR=$(dirname "$f")
|
|
780
860
|
break
|
|
781
|
-
done
|
|
861
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -iE 'Dockerfile|entrypoint.*\.sh')
|
|
782
862
|
|
|
783
863
|
if [ -n "$SERVICE_DIR" ]; then
|
|
784
864
|
# Step 2: Find volume mount points for this service in any docker-compose*.yml
|
|
785
865
|
COMPOSE_FILES=$(find . -maxdepth 3 -name "docker-compose*.yml" 2>/dev/null | sort)
|
|
786
866
|
VOLUME_MOUNTS=""
|
|
787
|
-
|
|
867
|
+
# $COMPOSE_FILES is one path per line (`find`) — herestring, not a piped
|
|
868
|
+
# `| while read`, so VOLUME_MOUNTS remains visible after the loop exits.
|
|
869
|
+
while IFS= read -r cf; do
|
|
870
|
+
[ -z "$cf" ] && continue
|
|
788
871
|
# Extract named volume mount points (lines like: - volume_name:/container/path)
|
|
789
872
|
MOUNTS=$(grep -oE '[a-z_-]+:/[^: ]+' "$cf" 2>/dev/null | grep -v '^#' | cut -d: -f2)
|
|
790
873
|
VOLUME_MOUNTS="$VOLUME_MOUNTS $MOUNTS"
|
|
791
|
-
done
|
|
874
|
+
done <<< "$COMPOSE_FILES"
|
|
792
875
|
|
|
793
876
|
# Step 3: Grep the service directory for filesystem write operations
|
|
794
877
|
WRITE_OPS=$(grep -rn "mkdir\|write_bytes\|open(.*['\"]w\|makedirs\|shutil\.copy\|shutil\.move" \
|
|
@@ -818,7 +901,8 @@ fi
|
|
|
818
901
|
```bash
|
|
819
902
|
PR_BASE="${PR_BASE:-staging}"
|
|
820
903
|
|
|
821
|
-
|
|
904
|
+
while IFS= read -r f; do
|
|
905
|
+
[ -z "$f" ] && continue
|
|
822
906
|
# Extract removed condition lines from the diff (lines starting with -)
|
|
823
907
|
REMOVED_CONDITIONS=$(git diff HEAD -- "$f" 2>/dev/null | grep -E '^-.*\bif\b' | grep -v '^---' | sed 's/^-//' | sed 's/^\s*//' | head -5)
|
|
824
908
|
|
|
@@ -841,7 +925,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'route
|
|
|
841
925
|
fi
|
|
842
926
|
done
|
|
843
927
|
fi
|
|
844
|
-
done
|
|
928
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'router')
|
|
845
929
|
```
|
|
846
930
|
|
|
847
931
|
**Flag as findings**:
|
|
@@ -872,7 +956,11 @@ if [ -n "$TOOL_FILES" ]; then
|
|
|
872
956
|
WORKFLOW_DIR="{WORKTREE_PATH}/.github/workflows"
|
|
873
957
|
CI_MISSING=""
|
|
874
958
|
|
|
875
|
-
|
|
959
|
+
# $TOOL_FILES is one path per line (already newline-split above via `tr`) —
|
|
960
|
+
# herestring, not a piped `| while read`, so CI_MISSING remains visible
|
|
961
|
+
# after the loop exits.
|
|
962
|
+
while IFS= read -r f; do
|
|
963
|
+
[ -z "$f" ] && continue
|
|
876
964
|
case "$f" in
|
|
877
965
|
traefik/*)
|
|
878
966
|
grep -rlE "traefik.*validate|traefik.*check" "$WORKFLOW_DIR" 2>/dev/null | grep -q . || \
|
|
@@ -892,7 +980,7 @@ if [ -n "$TOOL_FILES" ]; then
|
|
|
892
980
|
CI_MISSING="$CI_MISSING terraform"
|
|
893
981
|
;;
|
|
894
982
|
esac
|
|
895
|
-
done
|
|
983
|
+
done <<< "$TOOL_FILES"
|
|
896
984
|
|
|
897
985
|
# Deduplicate
|
|
898
986
|
CI_MISSING=$(echo "$CI_MISSING" | tr ' ' '\n' | sort -u | grep -v '^$' | tr '\n' ' ')
|