forgedock 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/forgedock.mjs +40 -2
- package/commands/autopilot.md +6 -0
- package/commands/deploy-info.md +8 -8
- package/commands/orchestrate.md +3 -3
- package/commands/review-pr-staging.md +21 -21
- package/commands/review-pr.md +17 -17
- package/commands/rollback.md +2 -2
- package/commands/work-on/build/validate.md +4 -4
- package/commands/work-on-monolithic.md +18 -1
- package/commands/work-on.md +4 -4
- package/package.json +1 -1
- package/scripts/verify-route-registration.sh +33 -8
- package/scripts/verify-sops-chain.sh +4 -2
package/bin/forgedock.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "path";
|
|
14
14
|
import {
|
|
15
15
|
mkdir,
|
|
16
|
+
rm,
|
|
16
17
|
symlink,
|
|
17
18
|
copyFile,
|
|
18
19
|
readlink,
|
|
@@ -3653,14 +3654,51 @@ async function docsInit() {
|
|
|
3653
3654
|
console.log(` Files: ${templateFiles.length} seed files`);
|
|
3654
3655
|
console.log("");
|
|
3655
3656
|
|
|
3656
|
-
|
|
3657
|
+
// Use resolvedTarget (canonical path) rather than the raw targetDir string. The
|
|
3658
|
+
// containment check above already resolved symlinks and validated the path; using the
|
|
3659
|
+
// canonical form here closes the TOCTOU window to the narrowest possible interval
|
|
3660
|
+
// and ensures mkdir does not follow a symlink that was NOT present when we checked.
|
|
3661
|
+
//
|
|
3662
|
+
// TOCTOU residual: a narrow window remains between the realpath check and this mkdir
|
|
3663
|
+
// on POSIX systems — a symlink planted at an intermediate path component during that
|
|
3664
|
+
// window could cause mkdir to create directories outside the project boundary.
|
|
3665
|
+
// Full elimination requires O_NOFOLLOW semantics (kernel-level, not available from
|
|
3666
|
+
// Node.js user-space). We mitigate with a post-create re-validation below (SEC-5).
|
|
3667
|
+
await mkdir(resolvedTarget, { recursive: true });
|
|
3668
|
+
|
|
3669
|
+
// SEC-5: Post-create re-validation — detect symlink planted during the TOCTOU window.
|
|
3670
|
+
// After mkdir succeeds, call realpath() on the now-existing directory to get its
|
|
3671
|
+
// canonical on-disk location. If that location lies outside the project root a
|
|
3672
|
+
// symlink was planted between the containment check and the mkdir call. In that case
|
|
3673
|
+
// we remove the directory tree that was created (wherever it landed) and abort.
|
|
3674
|
+
try {
|
|
3675
|
+
const postMkdirReal = await realpath(resolvedTarget);
|
|
3676
|
+
if (postMkdirReal !== realCwd && !postMkdirReal.startsWith(realCwd + sep)) {
|
|
3677
|
+
await rm(postMkdirReal, { recursive: true, force: true }).catch(() => {});
|
|
3678
|
+
console.log(
|
|
3679
|
+
`${RED}Error: devdocs directory escaped the project boundary after creation (TOCTOU).${RESET}`,
|
|
3680
|
+
);
|
|
3681
|
+
console.log(` Created at: ${postMkdirReal}`);
|
|
3682
|
+
console.log(` Project: ${realCwd}`);
|
|
3683
|
+
console.log(
|
|
3684
|
+
` A symlink was likely planted at an intermediate path component between the`,
|
|
3685
|
+
);
|
|
3686
|
+
console.log(
|
|
3687
|
+
` containment check and the mkdir call. The created directory has been removed.`,
|
|
3688
|
+
);
|
|
3689
|
+
process.exit(1);
|
|
3690
|
+
}
|
|
3691
|
+
} catch {
|
|
3692
|
+
// realpath() failure after a successful mkdir is unexpected; proceed — the directory
|
|
3693
|
+
// was created by us and the normal containment check already passed.
|
|
3694
|
+
}
|
|
3657
3695
|
|
|
3658
3696
|
let copied = 0;
|
|
3659
3697
|
let skipped = 0;
|
|
3660
3698
|
|
|
3661
3699
|
for (const srcFile of templateFiles) {
|
|
3662
3700
|
const rel = relative(templatesDir, srcFile);
|
|
3663
|
-
const destFile = join(
|
|
3701
|
+
const destFile = join(resolvedTarget, rel);
|
|
3664
3702
|
const destDir = dirname(destFile);
|
|
3665
3703
|
|
|
3666
3704
|
await mkdir(destDir, { recursive: true });
|
package/commands/autopilot.md
CHANGED
|
@@ -13,6 +13,12 @@ argument-hint: [--fix | --recon-only | --fix --limit 5 | --dry-run]
|
|
|
13
13
|
- `{EMEMO_PATH}` ← `services.ememo_path` (optional) — path on production server to open eMemo files
|
|
14
14
|
- `{BILLING_ENABLED}` ← `billing.enabled` (optional, default `false`) — set to `true` to enable Stripe data collection in the Analytics Snapshot agent
|
|
15
15
|
|
|
16
|
+
Resolve `{BILLING_ENABLED}` from `forge.yaml` before executing any phase:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
BILLING_ENABLED=$(yq '.billing.enabled // false' "$(git rev-parse --show-toplevel)/forge.yaml" 2>/dev/null || echo 'false')
|
|
20
|
+
```
|
|
21
|
+
|
|
16
22
|
You are an autonomous improvement engine for this project. Your job is to **find what's wrong, create trackable issues, and optionally fix the highest-impact ones** — all in a single cycle. Every cycle leaves the platform measurably better than before.
|
|
17
23
|
|
|
18
24
|
**This is designed to run repeatedly.** Each cycle builds on the last — issues created in cycle N get fixed in cycle N+1. The platform compounds improvements over time.
|
package/commands/deploy-info.md
CHANGED
|
@@ -156,9 +156,9 @@ done
|
|
|
156
156
|
git diff --stat origin/$TARGET..origin/$SOURCE
|
|
157
157
|
|
|
158
158
|
# Read layout paths from forge.yaml, fall back to AlterLab defaults
|
|
159
|
-
LAYOUT_API=$(yq '.review.layout.api // "services/api"'
|
|
160
|
-
LAYOUT_WORKER=$(yq '.review.layout.worker // "services/worker"'
|
|
161
|
-
LAYOUT_PAGES=$(yq '.review.layout.pages // "web/src/app"'
|
|
159
|
+
LAYOUT_API=$(yq '.review.layout.api // "services/api"' forge.yaml 2>/dev/null || echo "services/api")
|
|
160
|
+
LAYOUT_WORKER=$(yq '.review.layout.worker // "services/worker"' forge.yaml 2>/dev/null || echo "services/worker")
|
|
161
|
+
LAYOUT_PAGES=$(yq '.review.layout.pages // "web/src/app"' forge.yaml 2>/dev/null || echo "web/src/app")
|
|
162
162
|
# Derive web root from pages root (strip trailing /src/app or similar suffix if present)
|
|
163
163
|
LAYOUT_WEB=$(echo "$LAYOUT_PAGES" | sed 's|/src/app$||; s|/src$||')
|
|
164
164
|
|
|
@@ -180,7 +180,7 @@ done | sort
|
|
|
180
180
|
|
|
181
181
|
```bash
|
|
182
182
|
# Read migrations path from forge.yaml, fall back to infra/migrations
|
|
183
|
-
LAYOUT_MIGRATIONS=$(yq '.review.layout.migrations // "infra/migrations"'
|
|
183
|
+
LAYOUT_MIGRATIONS=$(yq '.review.layout.migrations // "infra/migrations"' forge.yaml 2>/dev/null || echo "infra/migrations")
|
|
184
184
|
|
|
185
185
|
# Are there new migrations?
|
|
186
186
|
MIGRATIONS=$(git diff --name-only origin/$TARGET..origin/$SOURCE | grep "^${LAYOUT_MIGRATIONS}/" | sort)
|
|
@@ -220,18 +220,18 @@ Analyze the deploy payload and flag risks:
|
|
|
220
220
|
# Check for new env vars that might not be in the configured secrets backend
|
|
221
221
|
NEW_ENV=$(git diff origin/$TARGET..origin/$SOURCE -- '.env.example' | grep "^+" | grep -v "^+++" | grep -v "^#")
|
|
222
222
|
if [ -n "$NEW_ENV" ]; then
|
|
223
|
-
echo "⚠️ NEW ENVIRONMENT VARIABLES — verify they exist in your secrets backend ({
|
|
223
|
+
echo "⚠️ NEW ENVIRONMENT VARIABLES — verify they exist in your secrets backend ({DEPLOY_SECRETS_BACKEND}):"
|
|
224
224
|
echo "$NEW_ENV"
|
|
225
225
|
# Cross-check with SOPS chain (only when deploy.secrets_backend == "sops")
|
|
226
226
|
# If your project uses a different backend, skip this block and verify manually.
|
|
227
|
-
if [ "{
|
|
227
|
+
if [ "{DEPLOY_SECRETS_BACKEND}" = "sops" ]; then
|
|
228
228
|
for var in $(echo "$NEW_ENV" | grep -oP '^\+\K[A-Z_]+'); do
|
|
229
229
|
if ! grep -q "$var" scripts/decrypt-secrets.sh 2>/dev/null; then
|
|
230
230
|
echo " ❌ $var NOT in decrypt-secrets.sh ENV_MAPPING"
|
|
231
231
|
fi
|
|
232
232
|
done
|
|
233
233
|
else
|
|
234
|
-
echo " ℹ️ SOPS chain check skipped — deploy.secrets_backend is '{
|
|
234
|
+
echo " ℹ️ SOPS chain check skipped — deploy.secrets_backend is '{DEPLOY_SECRETS_BACKEND}'. Verify new vars in your configured backend."
|
|
235
235
|
fi
|
|
236
236
|
fi
|
|
237
237
|
```
|
|
@@ -249,7 +249,7 @@ Generate a checklist based on what's in the deploy:
|
|
|
249
249
|
- [ ] All PRs in this deploy have been reviewed
|
|
250
250
|
- [ ] No open `needs-human` issues blocking deploy
|
|
251
251
|
- [ ] {If migrations} Database backup verified
|
|
252
|
-
- [ ] {If new env vars} Secrets backend ({
|
|
252
|
+
- [ ] {If new env vars} Secrets backend ({DEPLOY_SECRETS_BACKEND}) updated with new vars; verify via your configured secrets chain
|
|
253
253
|
- [ ] {If traefik changes} Routing tested locally
|
|
254
254
|
|
|
255
255
|
### Deploy Command
|
package/commands/orchestrate.md
CHANGED
|
@@ -824,9 +824,9 @@ All tool commands are read from `forge.yaml → verification.commands`; each ste
|
|
|
824
824
|
|
|
825
825
|
```bash
|
|
826
826
|
# Read toolchain commands from forge.yaml
|
|
827
|
-
TS_TYPECHECK=$(
|
|
828
|
-
TS_BUILD=$(
|
|
829
|
-
PYTHON_FORMAT=$(
|
|
827
|
+
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
828
|
+
TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
|
|
829
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
830
830
|
|
|
831
831
|
# Check if this is a milestone batch with TypeScript changes
|
|
832
832
|
MILESTONE_BRANCH="milestone/{milestone_slug}"
|
|
@@ -14,6 +14,22 @@ Performs comprehensive review of `staging` before merging to `main`. Handles lar
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## Config Resolution
|
|
18
|
+
|
|
19
|
+
Read `forge.yaml` to resolve branch names before running any commands:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
23
|
+
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
|
|
24
|
+
GH_FLAG="-R $GH_REPO"
|
|
25
|
+
DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
|
|
26
|
+
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
All `$DEFAULT_BRANCH` and `$STAGING_BRANCH` references below are populated from `forge.yaml`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
17
33
|
## Evidence-Based Review Protocol (ALL Agents)
|
|
18
34
|
|
|
19
35
|
### Diff-First Approach
|
|
@@ -70,22 +86,6 @@ Include ALL findings (CONFIRMED, LIKELY, POSSIBLE). One line per finding, sequen
|
|
|
70
86
|
|
|
71
87
|
---
|
|
72
88
|
|
|
73
|
-
## Config Resolution
|
|
74
|
-
|
|
75
|
-
Read `forge.yaml` to resolve branch names before running any commands:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
79
|
-
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
|
|
80
|
-
GH_FLAG="-R $GH_REPO"
|
|
81
|
-
DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
|
|
82
|
-
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
All `$DEFAULT_BRANCH` and `$STAGING_BRANCH` references below are populated from `forge.yaml`.
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
89
|
## Phase 0A: Open Review-Finding Gate (BLOCKING — runs before scope analysis)
|
|
90
90
|
|
|
91
91
|
**Purpose**: Prevent deploying commits that have known, unfixed review findings. The review system catches bugs before merging; this gate ensures the merge path acts on that information.
|
|
@@ -213,8 +213,8 @@ Create review chunks by priority: Billing/Pricing (CRITICAL) → Security/Auth (
|
|
|
213
213
|
Read `forge.yaml → verification.commands.python` for project-specific tool commands:
|
|
214
214
|
|
|
215
215
|
```bash
|
|
216
|
-
PYTHON_FORMAT=$(
|
|
217
|
-
PYTHON_LINT=$(
|
|
216
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
217
|
+
PYTHON_LINT=$(yq '.verification.commands.python.lint // ""' forge.yaml 2>/dev/null || echo '')
|
|
218
218
|
|
|
219
219
|
if [ -n "$PYTHON_FORMAT" ]; then
|
|
220
220
|
eval "$PYTHON_FORMAT" 2>&1 | head -30
|
|
@@ -234,8 +234,8 @@ fi
|
|
|
234
234
|
Read `forge.yaml → verification.commands.typescript` for project-specific tool commands:
|
|
235
235
|
|
|
236
236
|
```bash
|
|
237
|
-
TS_TYPECHECK=$(
|
|
238
|
-
TS_BUILD=$(
|
|
237
|
+
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
238
|
+
TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
|
|
239
239
|
|
|
240
240
|
if [ -n "$TS_TYPECHECK" ]; then
|
|
241
241
|
eval "$TS_TYPECHECK" 2>&1
|
|
@@ -261,7 +261,7 @@ Build failure is BLOCKING — deploy WILL fail. Typecheck alone misses SSG/prere
|
|
|
261
261
|
Read `forge.yaml → verification.commands.python.test`:
|
|
262
262
|
|
|
263
263
|
```bash
|
|
264
|
-
PYTHON_TEST=$(
|
|
264
|
+
PYTHON_TEST=$(yq '.verification.commands.python.test // ""' forge.yaml 2>/dev/null || echo '')
|
|
265
265
|
|
|
266
266
|
if [ -n "$PYTHON_TEST" ]; then
|
|
267
267
|
eval "$PYTHON_TEST" 2>&1 | tail -50
|
package/commands/review-pr.md
CHANGED
|
@@ -161,9 +161,9 @@ Read `forge.yaml → verification.commands.python` for project-specific tool com
|
|
|
161
161
|
|
|
162
162
|
```bash
|
|
163
163
|
# Read toolchain commands from forge.yaml
|
|
164
|
-
PYTHON_FORMAT=$(
|
|
165
|
-
PYTHON_LINT=$(
|
|
166
|
-
PYTHON_TYPECHECK=$(
|
|
164
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
165
|
+
PYTHON_LINT=$(yq '.verification.commands.python.lint // ""' forge.yaml 2>/dev/null || echo '')
|
|
166
|
+
PYTHON_TYPECHECK=$(yq '.verification.commands.python.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
167
167
|
|
|
168
168
|
# Run format check
|
|
169
169
|
if [ -n "$PYTHON_FORMAT" ]; then
|
|
@@ -187,7 +187,7 @@ else
|
|
|
187
187
|
fi
|
|
188
188
|
|
|
189
189
|
# Always run compile check on changed Python files (fast, language-universal)
|
|
190
|
-
|
|
190
|
+
gh pr diff $ARGUMENTS --name-only | grep '\.py$' | while IFS= read -r f; do
|
|
191
191
|
python3 -m py_compile "$f" 2>&1
|
|
192
192
|
done
|
|
193
193
|
```
|
|
@@ -197,9 +197,9 @@ done
|
|
|
197
197
|
Read `forge.yaml → verification.commands.typescript` for project-specific tool commands:
|
|
198
198
|
|
|
199
199
|
```bash
|
|
200
|
-
TS_FORMAT=$(
|
|
201
|
-
TS_LINT=$(
|
|
202
|
-
TS_TYPECHECK=$(
|
|
200
|
+
TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
201
|
+
TS_LINT=$(yq '.verification.commands.typescript.lint // ""' forge.yaml 2>/dev/null || echo '')
|
|
202
|
+
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
203
203
|
|
|
204
204
|
# Run format check
|
|
205
205
|
if [ -n "$TS_FORMAT" ]; then
|
|
@@ -243,7 +243,7 @@ gh pr diff $ARGUMENTS | grep -oE "['\"][A-Za-z0-9+/=]{40,}['\"]" | head -10
|
|
|
243
243
|
|
|
244
244
|
### 2F: SQL Migration Validation (if *.sql changed)
|
|
245
245
|
```bash
|
|
246
|
-
|
|
246
|
+
gh pr diff $ARGUMENTS --name-only | grep "\.sql$" | while IFS= read -r sql_file; do
|
|
247
247
|
grep -E "FOR UPDATE" "$sql_file" | grep -qE "(SUM|COUNT|AVG|MIN|MAX)\s*\(" && echo "ERROR: FOR UPDATE with aggregate"
|
|
248
248
|
grep -qE "DROP (TABLE|COLUMN|INDEX)" "$sql_file" && ! grep -qE "IF EXISTS" "$sql_file" && echo "WARNING: DROP without IF EXISTS"
|
|
249
249
|
grep -qE "ALTER TABLE.*ADD COLUMN.*NOT NULL" "$sql_file" && ! grep -qE "DEFAULT" "$sql_file" && echo "WARNING: NOT NULL without DEFAULT"
|
|
@@ -263,7 +263,7 @@ Read `forge.yaml → verification.commands.{lang}.test` for the project's test c
|
|
|
263
263
|
```bash
|
|
264
264
|
# Check each configured language for a test command
|
|
265
265
|
for lang in python typescript go rust; do
|
|
266
|
-
TEST_CMD=$(
|
|
266
|
+
TEST_CMD=$(yq ".verification.commands.${lang}.test // \"\"" forge.yaml 2>/dev/null || echo '')
|
|
267
267
|
if [ -n "$TEST_CMD" ]; then
|
|
268
268
|
echo "=== Running tests (${lang}): ${TEST_CMD} ==="
|
|
269
269
|
eval "$TEST_CMD" 2>&1 | tail -30
|
|
@@ -273,8 +273,8 @@ for lang in python typescript go rust; do
|
|
|
273
273
|
done
|
|
274
274
|
|
|
275
275
|
# If no test commands were configured, log explicitly
|
|
276
|
-
PYTHON_TEST=$(
|
|
277
|
-
TS_TEST=$(
|
|
276
|
+
PYTHON_TEST=$(yq '.verification.commands.python.test // ""' forge.yaml 2>/dev/null || echo '')
|
|
277
|
+
TS_TEST=$(yq '.verification.commands.typescript.test // ""' forge.yaml 2>/dev/null || echo '')
|
|
278
278
|
if [ -z "$PYTHON_TEST" ] && [ -z "$TS_TEST" ]; then
|
|
279
279
|
echo "SKIPPED — no test commands configured in verification.commands"
|
|
280
280
|
fi
|
|
@@ -303,8 +303,8 @@ Read `forge.yaml → verification.commands.typescript.typecheck` and `.build`:
|
|
|
303
303
|
```bash
|
|
304
304
|
gh pr checkout $ARGUMENTS --detach 2>/dev/null
|
|
305
305
|
|
|
306
|
-
TS_TYPECHECK=$(
|
|
307
|
-
TS_BUILD=$(
|
|
306
|
+
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
307
|
+
TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
|
|
308
308
|
|
|
309
309
|
if [ -n "$TS_TYPECHECK" ]; then
|
|
310
310
|
eval "$TS_TYPECHECK" 2>&1
|
|
@@ -337,10 +337,10 @@ Read `forge.yaml → verification.commands.python.format` and `.build`:
|
|
|
337
337
|
gh pr checkout $ARGUMENTS --detach 2>/dev/null
|
|
338
338
|
|
|
339
339
|
# Compile-check all changed Python files (language-universal — no config needed)
|
|
340
|
-
|
|
340
|
+
echo "$CHANGED_FILES" | grep '\.py$' | while IFS= read -r f; do python3 -m py_compile "$f" 2>&1; done
|
|
341
341
|
|
|
342
342
|
if [ "$REQUIRES_FULL_BUILD" = "true" ]; then
|
|
343
|
-
PYTHON_FORMAT=$(
|
|
343
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
344
344
|
if [ -n "$PYTHON_FORMAT" ]; then
|
|
345
345
|
eval "$PYTHON_FORMAT" 2>&1
|
|
346
346
|
else
|
|
@@ -495,7 +495,7 @@ fi
|
|
|
495
495
|
# Python scoping hazard check — local imports that shadow module-level names
|
|
496
496
|
# A local `import X` makes X a local variable for the ENTIRE function scope.
|
|
497
497
|
# Any reference to X ABOVE the local import will crash with UnboundLocalError.
|
|
498
|
-
|
|
498
|
+
echo "$CHANGED_FILES" | grep -E '\.py$' | while IFS= read -r f; do
|
|
499
499
|
echo "=== Python Scoping Check: $f ==="
|
|
500
500
|
# Find function-scoped imports (indented import statements)
|
|
501
501
|
grep -nE "^\s+import [a-z]" "$f" 2>/dev/null | while read line; do
|
|
@@ -509,7 +509,7 @@ for f in $(echo "$CHANGED_FILES" | grep -E '\.py$'); do
|
|
|
509
509
|
done
|
|
510
510
|
|
|
511
511
|
# Config file assumption check (baked into Docker image vs volume-mounted)
|
|
512
|
-
|
|
512
|
+
echo "$CHANGED_FILES" | grep -E "config/.*\.(json|yaml|yml)$" | while IFS= read -r f; do
|
|
513
513
|
echo "=== Config File: $f ==="
|
|
514
514
|
grep -n "$(dirname $f)" docker-compose.yml 2>/dev/null || echo "WARNING: Config dir may not be mounted — changes may require --build"
|
|
515
515
|
grep -n "$(dirname $f)" services/*/Dockerfile 2>/dev/null || true
|
package/commands/rollback.md
CHANGED
|
@@ -122,7 +122,7 @@ for f in {reverted_python_files}; do
|
|
|
122
122
|
done
|
|
123
123
|
|
|
124
124
|
# Python format check (config-driven)
|
|
125
|
-
PYTHON_FORMAT=$(
|
|
125
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
126
126
|
if [ -n "$PYTHON_FORMAT" ]; then
|
|
127
127
|
eval "$PYTHON_FORMAT" 2>&1 | head -20
|
|
128
128
|
else
|
|
@@ -130,7 +130,7 @@ else
|
|
|
130
130
|
fi
|
|
131
131
|
|
|
132
132
|
# TypeScript typecheck (config-driven)
|
|
133
|
-
TS_TYPECHECK=$(
|
|
133
|
+
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
134
134
|
if [ -n "$TS_TYPECHECK" ]; then
|
|
135
135
|
eval "$TS_TYPECHECK" 2>&1 | head -20
|
|
136
136
|
else
|
|
@@ -94,7 +94,7 @@ Run after quality gate passes. All tool commands are read from `forge.yaml → v
|
|
|
94
94
|
```bash
|
|
95
95
|
cd {WORKTREE_PATH}
|
|
96
96
|
|
|
97
|
-
PYTHON_FORMAT=$(
|
|
97
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
98
98
|
if [ -n "$PYTHON_FORMAT" ]; then
|
|
99
99
|
eval "$PYTHON_FORMAT" 2>&1
|
|
100
100
|
else
|
|
@@ -110,9 +110,9 @@ Failures in `py_compile` are BLOCKING — fix before continuing.
|
|
|
110
110
|
```bash
|
|
111
111
|
cd {WORKTREE_PATH}
|
|
112
112
|
|
|
113
|
-
TS_FORMAT=$(
|
|
114
|
-
TS_TYPECHECK=$(
|
|
115
|
-
TS_BUILD=$(
|
|
113
|
+
TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
114
|
+
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
115
|
+
TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
|
|
116
116
|
|
|
117
117
|
if [ -n "$TS_FORMAT" ]; then
|
|
118
118
|
eval "$TS_FORMAT" 2>&1
|
|
@@ -214,7 +214,24 @@ Fix HIGH/MEDIUM findings. Max 2 iterations. Skip for 1-file config/docs edits.
|
|
|
214
214
|
All client-side fetch/useSWR/apiFetch must use `/api/...` proxy routes, NEVER `/api/v1/...` directly.
|
|
215
215
|
|
|
216
216
|
### 3I: Deployment completeness check (MANDATORY)
|
|
217
|
-
|
|
217
|
+
Skip if no new env vars introduced.
|
|
218
|
+
|
|
219
|
+
For each new env var, verify present in ALL required locations:
|
|
220
|
+
|
|
221
|
+
| Location | Required for |
|
|
222
|
+
|----------|-------------|
|
|
223
|
+
| `.env.example` | All new vars |
|
|
224
|
+
| Secrets backend (see `deploy.secrets_backend`) | Secret vars — skip if backend is `none` or unset |
|
|
225
|
+
| `app/env_validation.py` | API service vars (if project has one) |
|
|
226
|
+
|
|
227
|
+
**Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*:
|
|
228
|
+
|
|
229
|
+
If the project uses SOPS, verify the new var is present in all SOPS chain locations:
|
|
230
|
+
- `infra/secrets/prod.enc.yaml` — SOPS-encrypted secret store
|
|
231
|
+
- `infra/decrypt-secrets.sh` ENV_MAPPING — maps SOPS key to env var name
|
|
232
|
+
|
|
233
|
+
If `deploy.secrets_backend` is absent or not `sops`, skip these checks and log:
|
|
234
|
+
> `SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.`
|
|
218
235
|
|
|
219
236
|
### 3J: Commit
|
|
220
237
|
Conventional prefix (fix/feat/refactor/docs). Reference #{NUMBER} in the commit message.
|
package/commands/work-on.md
CHANGED
|
@@ -753,7 +753,7 @@ All tool commands are read from `forge.yaml → verification.commands`. When a k
|
|
|
753
753
|
```bash
|
|
754
754
|
cd {WORKTREE_PATH}
|
|
755
755
|
|
|
756
|
-
PYTHON_FORMAT=$(
|
|
756
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
757
757
|
if [ -n "$PYTHON_FORMAT" ]; then
|
|
758
758
|
eval "$PYTHON_FORMAT" 2>&1
|
|
759
759
|
else
|
|
@@ -769,9 +769,9 @@ python -m py_compile {PYTHON_FILES}
|
|
|
769
769
|
```bash
|
|
770
770
|
cd {WORKTREE_PATH}
|
|
771
771
|
|
|
772
|
-
TS_FORMAT=$(
|
|
773
|
-
TS_TYPECHECK=$(
|
|
774
|
-
TS_BUILD=$(
|
|
772
|
+
TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
773
|
+
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
774
|
+
TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
|
|
775
775
|
|
|
776
776
|
if [ -n "$TS_FORMAT" ]; then
|
|
777
777
|
eval "$TS_FORMAT" 2>&1
|
package/package.json
CHANGED
|
@@ -49,12 +49,25 @@ WARNINGS=0
|
|
|
49
49
|
|
|
50
50
|
# --- Next.js Route Handler registration check ---
|
|
51
51
|
while read -r f; do
|
|
52
|
-
|
|
52
|
+
# Use shell parameter expansion instead of sed to avoid interpreting FORGE_PAGES_ROOT
|
|
53
|
+
# as a sed pattern (values containing | or other metacharacters break the substitution).
|
|
54
|
+
stripped="${f#${FORGE_PAGES_ROOT}/}"
|
|
55
|
+
stripped="${stripped%/route.ts}"
|
|
56
|
+
# Replace dynamic segments [param] with * using a loop (no regex needed)
|
|
57
|
+
URL_PATH=""
|
|
58
|
+
IFS='/' read -ra SEGMENTS <<< "$stripped"
|
|
59
|
+
for seg in "${SEGMENTS[@]}"; do
|
|
60
|
+
case "$seg" in
|
|
61
|
+
\[*\]) URL_PATH="${URL_PATH}/*" ;;
|
|
62
|
+
*) URL_PATH="${URL_PATH}/${seg}" ;;
|
|
63
|
+
esac
|
|
64
|
+
done
|
|
53
65
|
ROUTE_SEGMENT=$(echo "$URL_PATH" | sed 's|/api/v1/||; s|/.*||')
|
|
54
66
|
|
|
55
67
|
# Check if next.config.js has a rewrite that might shadow this route
|
|
56
68
|
if [ -f "$REPO_ROOT/web/next.config.js" ]; then
|
|
57
|
-
|
|
69
|
+
# Use -F (fixed string) so ROUTE_SEGMENT is not interpreted as a regex pattern
|
|
70
|
+
if grep -qF "$ROUTE_SEGMENT" "$REPO_ROOT/web/next.config.js" 2>/dev/null; then
|
|
58
71
|
echo "WARNING: Route handler $f ($URL_PATH) — next.config.js references '$ROUTE_SEGMENT', may shadow this route"
|
|
59
72
|
WARNINGS=$((WARNINGS + 1))
|
|
60
73
|
else
|
|
@@ -64,12 +77,19 @@ while read -r f; do
|
|
|
64
77
|
|
|
65
78
|
# Check if nginx routes this path to Next.js or directly to backend
|
|
66
79
|
if [ -f "$REPO_ROOT/infra/nginx/nginx.conf" ]; then
|
|
67
|
-
|
|
80
|
+
# Check for location blocks mentioning ROUTE_SEGMENT — use two -F searches to avoid
|
|
81
|
+
# embedding the variable in a regex: first find lines with "location", then filter
|
|
82
|
+
# for the literal segment. This avoids ROUTE_SEGMENT being interpreted as a BRE.
|
|
83
|
+
if grep -F "location" "$REPO_ROOT/infra/nginx/nginx.conf" 2>/dev/null | grep -qF "$ROUTE_SEGMENT"; then
|
|
68
84
|
echo "WARNING: Route handler $f ($URL_PATH) — nginx.conf has a location block for '$ROUTE_SEGMENT', may bypass Next.js"
|
|
69
85
|
WARNINGS=$((WARNINGS + 1))
|
|
70
86
|
fi
|
|
71
87
|
fi
|
|
72
|
-
done < <(echo "$FILES" |
|
|
88
|
+
done < <(echo "$FILES" | while IFS= read -r line; do
|
|
89
|
+
case "$line" in
|
|
90
|
+
"${FORGE_PAGES_ROOT}/api/"*"route.ts") echo "$line" ;;
|
|
91
|
+
esac
|
|
92
|
+
done) || true
|
|
73
93
|
|
|
74
94
|
# --- Python API Router registration check ---
|
|
75
95
|
while read -r f; do
|
|
@@ -77,7 +97,8 @@ while read -r f; do
|
|
|
77
97
|
MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
|
|
78
98
|
|
|
79
99
|
if [ -f "$MAIN_PY" ]; then
|
|
80
|
-
|
|
100
|
+
# Use -F (fixed string) so ROUTER_NAME is not interpreted as a regex pattern
|
|
101
|
+
if grep -qF "$ROUTER_NAME" "$MAIN_PY" 2>/dev/null; then
|
|
81
102
|
echo "OK: API Router '$ROUTER_NAME' ($f) — registered in $(basename "$MAIN_PY")"
|
|
82
103
|
else
|
|
83
104
|
echo "BLOCKING: API Router '$ROUTER_NAME' ($f) — NOT found in $(basename "$MAIN_PY"). Router will not be reachable."
|
|
@@ -95,7 +116,8 @@ while read -r f; do
|
|
|
95
116
|
MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
|
|
96
117
|
|
|
97
118
|
if [ -f "$MAIN_PY" ]; then
|
|
98
|
-
|
|
119
|
+
# Use -F (fixed string) so MIDDLEWARE_NAME is not interpreted as a regex pattern
|
|
120
|
+
if grep -qF "$MIDDLEWARE_NAME" "$MAIN_PY" 2>/dev/null; then
|
|
99
121
|
echo "OK: Middleware '$MIDDLEWARE_NAME' ($f) — registered in $(basename "$MAIN_PY")"
|
|
100
122
|
else
|
|
101
123
|
echo "WARNING: Middleware '$MIDDLEWARE_NAME' ($f) — not found in $(basename "$MAIN_PY"). May not be active."
|
|
@@ -108,7 +130,8 @@ done < <(echo "$FILES" | grep -E "^${FORGE_API_MIDDLEWARE_DIR}/.*\\.py$") || tru
|
|
|
108
130
|
while read -r f; do
|
|
109
131
|
MODULE_NAME=$(basename "$f" .py)
|
|
110
132
|
|
|
111
|
-
|
|
133
|
+
# Use -F (fixed string) so MODULE_NAME is not interpreted as a regex pattern
|
|
134
|
+
IMPORTERS=$(grep -rlF "$MODULE_NAME" "$REPO_ROOT/${FORGE_API_SERVICES_DIR}/" "$REPO_ROOT/${FORGE_WORKER_DIR}/" 2>/dev/null | head -5)
|
|
112
135
|
if [ -n "$IMPORTERS" ]; then
|
|
113
136
|
echo "OK: Shared module '$MODULE_NAME' ($f) — imported by services"
|
|
114
137
|
else
|
|
@@ -121,7 +144,9 @@ done < <(echo "$FILES" | grep -E "^shared/.*\.py$") || true
|
|
|
121
144
|
while read -r f; do
|
|
122
145
|
COMPONENT_NAME=$(basename "$f" .tsx)
|
|
123
146
|
|
|
124
|
-
|
|
147
|
+
# Use -F (fixed string) for both the component name search and the self-exclusion
|
|
148
|
+
# to prevent file paths and component names from being interpreted as regex patterns
|
|
149
|
+
IMPORTERS=$(grep -rlF "$COMPONENT_NAME" "$REPO_ROOT/${FORGE_PAGES_ROOT}/" "$REPO_ROOT/${FORGE_COMPONENTS_DIR}/" 2>/dev/null | grep -vF "$f" | head -3)
|
|
125
150
|
if [ -n "$IMPORTERS" ]; then
|
|
126
151
|
echo "OK: Component '$COMPONENT_NAME' ($f) — imported by other files"
|
|
127
152
|
else
|
|
@@ -80,7 +80,9 @@ if [ -n "$NEW_MAPPINGS" ]; then
|
|
|
80
80
|
|
|
81
81
|
# For each new mapping, verify the SOPS path structure exists
|
|
82
82
|
# (We can't decrypt SOPS to verify values, but we can check the mapping format is valid)
|
|
83
|
-
|
|
83
|
+
# Use process substitution instead of a pipe so WARNINGS increments persist in the parent shell.
|
|
84
|
+
# (echo ... | while read creates a subshell — counter changes are lost on subshell exit.)
|
|
85
|
+
while IFS= read -r mapping; do
|
|
84
86
|
VAR_NAME=$(echo "$mapping" | grep -oE '^"[A-Z_]+"' | tr -d '"')
|
|
85
87
|
SOPS_SECTION=$(echo "$mapping" | grep -oE '\("([a-z_]+)"' | head -1 | tr -d '("')
|
|
86
88
|
SOPS_KEY=$(echo "$mapping" | grep -oE ', "([a-z_]+)"\)' | tr -d ', ")')
|
|
@@ -91,7 +93,7 @@ if [ -n "$NEW_MAPPINGS" ]; then
|
|
|
91
93
|
echo "WARNING: Malformed ENV_MAPPING entry: $mapping"
|
|
92
94
|
WARNINGS=$((WARNINGS + 1))
|
|
93
95
|
fi
|
|
94
|
-
done
|
|
96
|
+
done < <(echo "$NEW_MAPPINGS")
|
|
95
97
|
else
|
|
96
98
|
echo "OK: No new ENV_MAPPING entries in diff"
|
|
97
99
|
fi
|