forgedock 1.0.21 → 1.0.22
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 +134 -80
- package/bin/demo.mjs +386 -0
- package/bin/forgedock.mjs +357 -30
- package/bin/init-detect.mjs +95 -0
- package/bin/runner.mjs +780 -0
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +6 -2
- package/commands/forge-stats.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +320 -26
- package/commands/quality-gate.md +91 -49
- package/commands/review-pr-agents.md +62 -38
- package/commands/review-pr.md +33 -2
- 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/validate.md +16 -7
- package/commands/work-on/close.md +129 -1
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on.md +226 -8
- 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 +36 -14
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -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 +5 -1
- package/templates/forge.yaml.minimal +44 -0
package/commands/quality-gate.md
CHANGED
|
@@ -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
|
|
@@ -144,34 +149,40 @@ grep -lE "asyncio\.shield|asyncio\.wait_for|Task\.cancel" {CHANGED_FILES} 2>/dev
|
|
|
144
149
|
grep -lE "^\w+ = (\{\}|\[\]|set\(\)|Lock\(\)|defaultdict|Counter)" {CHANGED_FILES} 2>/dev/null | grep -qE '\.py$' && DOMAINS="$DOMAINS STATE"
|
|
145
150
|
|
|
146
151
|
# Check for capacity constants in worker/infra/browser Python files
|
|
147
|
-
|
|
152
|
+
while IFS= read -r f; do
|
|
153
|
+
[ -z "$f" ] && continue
|
|
148
154
|
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
|
|
149
|
-
done
|
|
155
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'services/worker/|infra/|browser/')
|
|
150
156
|
|
|
151
157
|
# Check for env var usage in changed files
|
|
152
158
|
grep -lE "os\.getenv|process\.env" {CHANGED_FILES} 2>/dev/null && DOMAINS="$DOMAINS DEPLOY"
|
|
153
159
|
|
|
154
160
|
# Check for new app.* imports added outside try: blocks in Python files
|
|
155
|
-
|
|
161
|
+
while IFS= read -r f; do
|
|
162
|
+
[ -z "$f" ] && continue
|
|
156
163
|
NEW_IMPORTS=$(git diff HEAD -- "$f" 2>/dev/null | grep -E '^\+\s*(from app\.|import app\.)' | grep -v '^+++')
|
|
157
164
|
if [ -n "$NEW_IMPORTS" ]; then
|
|
158
165
|
DOMAINS="$DOMAINS IMPORT_RESOLUTION"
|
|
159
166
|
break
|
|
160
167
|
fi
|
|
161
|
-
done
|
|
168
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
162
169
|
|
|
163
170
|
# Check for runtime UID changes in Dockerfiles or entrypoint scripts
|
|
164
|
-
|
|
171
|
+
while IFS= read -r f; do
|
|
172
|
+
[ -z "$f" ] && continue
|
|
165
173
|
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
|
|
174
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -iE 'Dockerfile|entrypoint.*\.sh')
|
|
167
175
|
|
|
168
176
|
# Check for gate condition narrowing in router Python files (condition removal = fix may be incomplete across siblings)
|
|
169
|
-
|
|
177
|
+
while IFS= read -r f; do
|
|
178
|
+
[ -z "$f" ] && continue
|
|
170
179
|
git diff HEAD -- "$f" 2>/dev/null | grep -E '^-.*\bif\b.*\bor\b' | grep -v '^---' | grep -q . && DOMAINS="$DOMAINS ROUTER_BUG" && break
|
|
171
|
-
done
|
|
180
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'router')
|
|
172
181
|
|
|
173
182
|
# Check for external tool config files (structural schema validation advisory)
|
|
174
|
-
|
|
183
|
+
# {CHANGED_FILES} is space-separated (see array-split note above) — reuse the
|
|
184
|
+
# same CHANGED_FILES_ARR built above instead of re-splitting.
|
|
185
|
+
for f in "${CHANGED_FILES_ARR[@]}"; do
|
|
175
186
|
case "$f" in
|
|
176
187
|
traefik/*|infra/nginx/*|k8s/*|terraform/*)
|
|
177
188
|
DOMAINS="$DOMAINS CONFIG_SCHEMA" && break ;;
|
|
@@ -228,7 +239,11 @@ For every changed file, check:
|
|
|
228
239
|
|
|
229
240
|
```bash
|
|
230
241
|
# Quick automated checks
|
|
231
|
-
|
|
242
|
+
# {CHANGED_FILES} is a space-separated argument — split explicitly on IFS=' '
|
|
243
|
+
# into an array instead of a bare `for f in {CHANGED_FILES}` (see Step 1.5 for
|
|
244
|
+
# the full rationale; this is a separate bash block so the array is rebuilt here).
|
|
245
|
+
IFS=' ' read -ra CHANGED_FILES_ARR <<< "{CHANGED_FILES}"
|
|
246
|
+
for f in "${CHANGED_FILES_ARR[@]}"; do
|
|
232
247
|
# f-string SQL
|
|
233
248
|
grep -nE "f['\"].*SELECT|f['\"].*INSERT|f['\"].*UPDATE|f['\"].*DELETE|f['\"].*WHERE" "$f" 2>/dev/null && echo "SEC: f-string SQL in $f"
|
|
234
249
|
# Hardcoded secrets
|
|
@@ -256,10 +271,11 @@ For new/modified endpoints:
|
|
|
256
271
|
3. Does the endpoint check resource ownership (`user_id`) before returning data?
|
|
257
272
|
|
|
258
273
|
```bash
|
|
259
|
-
|
|
274
|
+
while IFS= read -r f; do
|
|
275
|
+
[ -z "$f" ] && continue
|
|
260
276
|
grep -nE "CurrentUser|SessionUser|Depends\(get_" "$f" 2>/dev/null
|
|
261
277
|
grep -nE "@router\.(get|post|put|delete)" "$f" 2>/dev/null
|
|
262
|
-
done
|
|
278
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E 'router|route')
|
|
263
279
|
```
|
|
264
280
|
|
|
265
281
|
### 2C: Deploy chain (when new env vars are introduced)
|
|
@@ -286,11 +302,12 @@ For each `.sql` file:
|
|
|
286
302
|
5. **Migration number collision**: Check against origin branch
|
|
287
303
|
|
|
288
304
|
```bash
|
|
289
|
-
|
|
305
|
+
while IFS= read -r f; do
|
|
306
|
+
[ -z "$f" ] && continue
|
|
290
307
|
grep -nE "ADD COLUMN.*NOT NULL" "$f" | grep -v "DEFAULT" && echo "DB: NOT NULL without DEFAULT in $f"
|
|
291
308
|
grep -nE "DROP (TABLE|COLUMN|INDEX)" "$f" | grep -v "IF EXISTS" && echo "DB: DROP without IF EXISTS in $f"
|
|
292
309
|
grep -nE "SELECT.*FROM" "$f" | grep -v "LIMIT" | grep -v "WHERE.*created_at" && echo "DB: possibly unbounded query in $f"
|
|
293
|
-
done
|
|
310
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep '\.sql$')
|
|
294
311
|
|
|
295
312
|
# Migration prefix collision scan (Deploy Gate — HIGH)
|
|
296
313
|
# Runs whenever any SQL file changed. Scans the full infra/migrations/ tree for duplicate
|
|
@@ -330,7 +347,8 @@ For each `.tsx`/`.ts` client component:
|
|
|
330
347
|
7. **Rules of Hooks**: Do function components avoid early returns before hook calls?
|
|
331
348
|
|
|
332
349
|
```bash
|
|
333
|
-
|
|
350
|
+
while IFS= read -r f; do
|
|
351
|
+
[ -z "$f" ] && continue
|
|
334
352
|
# Check for useEffect without cleanup (basic heuristic)
|
|
335
353
|
grep -c "useEffect" "$f" 2>/dev/null
|
|
336
354
|
grep -c "return.*cleanup\|return.*\(\) =>" "$f" 2>/dev/null
|
|
@@ -370,15 +388,16 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.tsx?$' | grep -v 'rou
|
|
|
370
388
|
}
|
|
371
389
|
in_component && /\buse[A-Z]/ { found_hook=1 }
|
|
372
390
|
' FILENAME="$f" "$f" 2>/dev/null
|
|
373
|
-
done
|
|
391
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.tsx?$' | grep -v 'route\.ts')
|
|
374
392
|
```
|
|
375
393
|
|
|
376
394
|
### 2F: Frontend proxy wiring (client-side fetch calls)
|
|
377
395
|
|
|
378
396
|
```bash
|
|
379
|
-
|
|
397
|
+
while IFS= read -r f; do
|
|
398
|
+
[ -z "$f" ] && continue
|
|
380
399
|
grep -nE '(fetch|useSWR|apiFetch)\s*[(<]\s*[`"'"'"']/api/v1/' "$f" 2>/dev/null && echo "PROXY: direct /api/v1/ call in $f — must use /api/ proxy"
|
|
381
|
-
done
|
|
400
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.(tsx?|jsx?)$' | grep -v 'route\.ts$')
|
|
382
401
|
```
|
|
383
402
|
|
|
384
403
|
### 2G: Cross-service integration (shell scripts with curl/wget)
|
|
@@ -413,7 +432,8 @@ The script `verify-host-headers.sh` checks for the `FORGE_INTERNAL_PATTERNS` env
|
|
|
413
432
|
**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
433
|
|
|
415
434
|
```bash
|
|
416
|
-
|
|
435
|
+
while IFS= read -r f; do
|
|
436
|
+
[ -z "$f" ] && continue
|
|
417
437
|
# Only check files that contain DB connection tools
|
|
418
438
|
if grep -qE "PGPASSWORD|createdb|psql|pg_dump|pg_restore" "$f" 2>/dev/null; then
|
|
419
439
|
# Look for new/modified lines with hardcoded localhost in variable assignments
|
|
@@ -428,7 +448,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.sh$'); do
|
|
|
428
448
|
echo "$HARDCODED"
|
|
429
449
|
fi
|
|
430
450
|
fi
|
|
431
|
-
done
|
|
451
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.sh$')
|
|
432
452
|
```
|
|
433
453
|
|
|
434
454
|
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 +514,8 @@ Report each conflict as **HIGH** — a shadowed native command is a silent user-
|
|
|
494
514
|
**Severity**: MEDIUM (flags for review, not auto-blocking). The review agent's concurrency auditor verifies with full context.
|
|
495
515
|
|
|
496
516
|
```bash
|
|
497
|
-
|
|
517
|
+
while IFS= read -r f; do
|
|
518
|
+
[ -z "$f" ] && continue
|
|
498
519
|
# asyncio.shield usage — outer await may be reached with pending CancelledError
|
|
499
520
|
grep -nE "asyncio\.shield" "$f" 2>/dev/null && \
|
|
500
521
|
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 +530,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$'); do
|
|
|
509
530
|
sed -n "$((LINENO+1)),$((LINENO+5))p" "$f" | grep -q "await" && \
|
|
510
531
|
echo "ASYNC: await in except/finally block at $f:$LINENO — may fail if CancelledError is pending"
|
|
511
532
|
done
|
|
512
|
-
done
|
|
533
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
513
534
|
```
|
|
514
535
|
|
|
515
536
|
|
|
@@ -532,7 +553,8 @@ For each new module-level state variable in the diff:
|
|
|
532
553
|
```
|
|
533
554
|
|
|
534
555
|
```bash
|
|
535
|
-
|
|
556
|
+
while IFS= read -r f; do
|
|
557
|
+
[ -z "$f" ] && continue
|
|
536
558
|
# Find new module-level state variables introduced in the diff
|
|
537
559
|
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
560
|
for new_var in $NEW_VARS; do
|
|
@@ -549,7 +571,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$'); do
|
|
|
549
571
|
done
|
|
550
572
|
done
|
|
551
573
|
done
|
|
552
|
-
done
|
|
574
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
553
575
|
```
|
|
554
576
|
|
|
555
577
|
**Severity**: MEDIUM — missing a mutation site is usually a latent bug, not an immediate crash, but it causes incorrect behavior under concurrent load.
|
|
@@ -567,11 +589,12 @@ For each changed file in the anti-detection/scraping domains:
|
|
|
567
589
|
**1. Identify modified string identifier sets** (frozensets, tuples, or lists of string literals used for detection):
|
|
568
590
|
|
|
569
591
|
```bash
|
|
570
|
-
|
|
592
|
+
while IFS= read -r f; do
|
|
593
|
+
[ -z "$f" ] && continue
|
|
571
594
|
echo "=== String sets in $f ==="
|
|
572
595
|
# Find named constants that are frozenset/tuple/list of strings
|
|
573
596
|
grep -nE "^[A-Z_]+ = (frozenset|tuple|set)\(\[?['\"]|^[A-Z_]+ = \(['\"]|^[A-Z_]+ = \[['\"]" "$f" 2>/dev/null
|
|
574
|
-
done
|
|
597
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_detection/|consumers/|browser/|shared/detection/')
|
|
575
598
|
```
|
|
576
599
|
|
|
577
600
|
**2. For each string in a modified set, check if it appears in sibling detection files:**
|
|
@@ -579,7 +602,8 @@ done
|
|
|
579
602
|
```bash
|
|
580
603
|
SERVICE_DIR=$(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E 'anti_detection/|consumers/|browser/' | head -1 | grep -oE '^services/[^/]+/[^/]+' || echo "services/worker/worker")
|
|
581
604
|
|
|
582
|
-
|
|
605
|
+
while IFS= read -r f; do
|
|
606
|
+
[ -z "$f" ] && continue
|
|
583
607
|
# Extract string values from detection sets in this file
|
|
584
608
|
STRINGS=$(grep -oE "'[a-z_][a-z0-9_-]*'" "$f" 2>/dev/null | tr -d "'" | sort -u)
|
|
585
609
|
for s in $STRINGS; do
|
|
@@ -587,13 +611,14 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_
|
|
|
587
611
|
FOUND=$(grep -rl "\"$s\"\|'$s'" $SERVICE_DIR/ 2>/dev/null | grep -v "^$f$" | grep '\.py$' | head -1)
|
|
588
612
|
[ -z "$FOUND" ] && echo "STR: '$s' in $f appears NOWHERE else in $SERVICE_DIR — possible typo or dead entry"
|
|
589
613
|
done
|
|
590
|
-
done
|
|
614
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_detection/|consumers/|browser/|shared/detection/')
|
|
591
615
|
```
|
|
592
616
|
|
|
593
617
|
**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
618
|
|
|
595
619
|
```bash
|
|
596
|
-
|
|
620
|
+
while IFS= read -r f; do
|
|
621
|
+
[ -z "$f" ] && continue
|
|
597
622
|
echo "=== Comment/code drift in $f ==="
|
|
598
623
|
# Extract string literals from actual sets (remove quotes)
|
|
599
624
|
SET_STRINGS=$(grep -oE "frozenset\(\[([^]]+)\]\)" "$f" 2>/dev/null | grep -oE "'[^']+'" | tr -d "'" | sort -u)
|
|
@@ -605,20 +630,25 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_
|
|
|
605
630
|
for ref in $COMMENT_REFS; do
|
|
606
631
|
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
632
|
done
|
|
608
|
-
done
|
|
633
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_detection/|consumers/|browser/|shared/detection/')
|
|
609
634
|
```
|
|
610
635
|
|
|
611
636
|
**4. Cross-reference sibling sets in related modules** (the primary inter-file check):
|
|
612
637
|
|
|
613
638
|
```bash
|
|
614
639
|
# Find all detection-related Python files in the same service
|
|
615
|
-
|
|
640
|
+
while IFS= read -r f; do
|
|
641
|
+
[ -z "$f" ] && continue
|
|
616
642
|
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
643
|
|
|
618
644
|
# Extract detection set names from the changed file
|
|
619
645
|
SET_NAMES=$(grep -oE "^[A-Z_]+ = (frozenset|tuple|set)" "$f" 2>/dev/null | grep -oE "^[A-Z_]+")
|
|
620
646
|
|
|
621
|
-
|
|
647
|
+
# $SIBLINGS is one file path per line (`find`) — herestring (not a piped
|
|
648
|
+
# `| while read`, which would run the loop body in a subshell and discard
|
|
649
|
+
# any accumulator set inside it) preserves newline-safety for paths with spaces.
|
|
650
|
+
while IFS= read -r sib; do
|
|
651
|
+
[ -z "$sib" ] && continue
|
|
622
652
|
# Extract strings from sibling detection sets
|
|
623
653
|
SIB_STRINGS=$(grep -oE "'[a-z_][a-z0-9_-]*'" "$sib" 2>/dev/null | tr -d "'" | sort -u)
|
|
624
654
|
|
|
@@ -629,8 +659,8 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_
|
|
|
629
659
|
echo "$SIB_STRINGS" | grep -q "^${s}$" || \
|
|
630
660
|
echo "STR: '$s' in $f not found in sibling file $sib — verify if cross-module consistency is required"
|
|
631
661
|
done
|
|
632
|
-
done
|
|
633
|
-
done
|
|
662
|
+
done <<< "$SIBLINGS"
|
|
663
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'anti_detection/|consumers/|browser/')
|
|
634
664
|
```
|
|
635
665
|
|
|
636
666
|
**Flag as findings**:
|
|
@@ -649,7 +679,8 @@ done
|
|
|
649
679
|
For each new or modified capacity constant in the diff:
|
|
650
680
|
|
|
651
681
|
```bash
|
|
652
|
-
|
|
682
|
+
while IFS= read -r f; do
|
|
683
|
+
[ -z "$f" ] && continue
|
|
653
684
|
# Find new capacity constant assignments introduced by this diff
|
|
654
685
|
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
686
|
for cap_var in $NEW_CAPS; do
|
|
@@ -667,7 +698,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'servi
|
|
|
667
698
|
git log --all -15 --oneline --grep="memory" 2>/dev/null | head -5
|
|
668
699
|
git log --all -15 --oneline --grep="$(echo $keywords | cut -d' ' -f1)" 2>/dev/null | head -5
|
|
669
700
|
done
|
|
670
|
-
done
|
|
701
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'services/worker/|infra/|browser/')
|
|
671
702
|
```
|
|
672
703
|
|
|
673
704
|
**Flag as findings**:
|
|
@@ -685,7 +716,8 @@ done
|
|
|
685
716
|
**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
717
|
|
|
687
718
|
```bash
|
|
688
|
-
|
|
719
|
+
while IFS= read -r f; do
|
|
720
|
+
[ -z "$f" ] && continue
|
|
689
721
|
if grep -q "appleboy/ssh-action" "$f"; then
|
|
690
722
|
TEMPLATE_HITS=$(grep -n "{{" "$f")
|
|
691
723
|
if [ -n "$TEMPLATE_HITS" ]; then
|
|
@@ -702,7 +734,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.github/workflows/.*\.
|
|
|
702
734
|
echo " docker ps --format json | jq -r '.Status'"
|
|
703
735
|
fi
|
|
704
736
|
fi
|
|
705
|
-
done
|
|
737
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.github/workflows/.*\.yml$')
|
|
706
738
|
```
|
|
707
739
|
|
|
708
740
|
Report any hit as **CONFIRMED HIGH** — the step will exit 1 before reaching the remote shell.
|
|
@@ -718,7 +750,8 @@ Go template directives in `appleboy/ssh-action` `script:` blocks are preprocesse
|
|
|
718
750
|
```bash
|
|
719
751
|
PR_BASE="${PR_BASE:-staging}"
|
|
720
752
|
|
|
721
|
-
|
|
753
|
+
while IFS= read -r f; do
|
|
754
|
+
[ -z "$f" ] && continue
|
|
722
755
|
# Extract new app.* import statements added in the diff
|
|
723
756
|
# Only consider lines added (starting with +), skip diff headers (+++)
|
|
724
757
|
NEW_IMPORTS=$(git diff HEAD -- "$f" 2>/dev/null | grep -E '^\+\s*(from app\.|import app\.)' | grep -v '^+++' | sed 's/^+//')
|
|
@@ -752,7 +785,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$'); do
|
|
|
752
785
|
fi
|
|
753
786
|
fi
|
|
754
787
|
done
|
|
755
|
-
done
|
|
788
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
756
789
|
```
|
|
757
790
|
|
|
758
791
|
**Flag as findings**:
|
|
@@ -775,20 +808,24 @@ A builder agent working in a milestone-branch worktree has access to milestone-o
|
|
|
775
808
|
# Step 1: Identify the service directory from the changed Dockerfile/entrypoint path
|
|
776
809
|
# e.g. services/worker/Dockerfile → service root = services/worker/
|
|
777
810
|
SERVICE_DIR=""
|
|
778
|
-
|
|
811
|
+
while IFS= read -r f; do
|
|
812
|
+
[ -z "$f" ] && continue
|
|
779
813
|
SERVICE_DIR=$(dirname "$f")
|
|
780
814
|
break
|
|
781
|
-
done
|
|
815
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -iE 'Dockerfile|entrypoint.*\.sh')
|
|
782
816
|
|
|
783
817
|
if [ -n "$SERVICE_DIR" ]; then
|
|
784
818
|
# Step 2: Find volume mount points for this service in any docker-compose*.yml
|
|
785
819
|
COMPOSE_FILES=$(find . -maxdepth 3 -name "docker-compose*.yml" 2>/dev/null | sort)
|
|
786
820
|
VOLUME_MOUNTS=""
|
|
787
|
-
|
|
821
|
+
# $COMPOSE_FILES is one path per line (`find`) — herestring, not a piped
|
|
822
|
+
# `| while read`, so VOLUME_MOUNTS remains visible after the loop exits.
|
|
823
|
+
while IFS= read -r cf; do
|
|
824
|
+
[ -z "$cf" ] && continue
|
|
788
825
|
# Extract named volume mount points (lines like: - volume_name:/container/path)
|
|
789
826
|
MOUNTS=$(grep -oE '[a-z_-]+:/[^: ]+' "$cf" 2>/dev/null | grep -v '^#' | cut -d: -f2)
|
|
790
827
|
VOLUME_MOUNTS="$VOLUME_MOUNTS $MOUNTS"
|
|
791
|
-
done
|
|
828
|
+
done <<< "$COMPOSE_FILES"
|
|
792
829
|
|
|
793
830
|
# Step 3: Grep the service directory for filesystem write operations
|
|
794
831
|
WRITE_OPS=$(grep -rn "mkdir\|write_bytes\|open(.*['\"]w\|makedirs\|shutil\.copy\|shutil\.move" \
|
|
@@ -818,7 +855,8 @@ fi
|
|
|
818
855
|
```bash
|
|
819
856
|
PR_BASE="${PR_BASE:-staging}"
|
|
820
857
|
|
|
821
|
-
|
|
858
|
+
while IFS= read -r f; do
|
|
859
|
+
[ -z "$f" ] && continue
|
|
822
860
|
# Extract removed condition lines from the diff (lines starting with -)
|
|
823
861
|
REMOVED_CONDITIONS=$(git diff HEAD -- "$f" 2>/dev/null | grep -E '^-.*\bif\b' | grep -v '^---' | sed 's/^-//' | sed 's/^\s*//' | head -5)
|
|
824
862
|
|
|
@@ -841,7 +879,7 @@ for f in $(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'route
|
|
|
841
879
|
fi
|
|
842
880
|
done
|
|
843
881
|
fi
|
|
844
|
-
done
|
|
882
|
+
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$' | grep -E 'router')
|
|
845
883
|
```
|
|
846
884
|
|
|
847
885
|
**Flag as findings**:
|
|
@@ -872,7 +910,11 @@ if [ -n "$TOOL_FILES" ]; then
|
|
|
872
910
|
WORKFLOW_DIR="{WORKTREE_PATH}/.github/workflows"
|
|
873
911
|
CI_MISSING=""
|
|
874
912
|
|
|
875
|
-
|
|
913
|
+
# $TOOL_FILES is one path per line (already newline-split above via `tr`) —
|
|
914
|
+
# herestring, not a piped `| while read`, so CI_MISSING remains visible
|
|
915
|
+
# after the loop exits.
|
|
916
|
+
while IFS= read -r f; do
|
|
917
|
+
[ -z "$f" ] && continue
|
|
876
918
|
case "$f" in
|
|
877
919
|
traefik/*)
|
|
878
920
|
grep -rlE "traefik.*validate|traefik.*check" "$WORKFLOW_DIR" 2>/dev/null | grep -q . || \
|
|
@@ -892,7 +934,7 @@ if [ -n "$TOOL_FILES" ]; then
|
|
|
892
934
|
CI_MISSING="$CI_MISSING terraform"
|
|
893
935
|
;;
|
|
894
936
|
esac
|
|
895
|
-
done
|
|
937
|
+
done <<< "$TOOL_FILES"
|
|
896
938
|
|
|
897
939
|
# Deduplicate
|
|
898
940
|
CI_MISSING=$(echo "$CI_MISSING" | tr ' ' '\n' | sort -u | grep -v '^$' | tr '\n' ' ')
|