forgedock 1.0.6 → 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.
@@ -9,6 +9,7 @@ argument-hint: [PR number to revert, or "last" for most recent deploy]
9
9
 
10
10
  **Config variables used by this command** (set in `forge.yaml`):
11
11
  - `{REPO_PATH}` ← `paths.root` — project repository root
12
+ - `{DEPLOY_WORKFLOW}` ← `deploy.workflow` (optional) — GitHub Actions workflow filename for fast-track deploys (e.g., `hotfix-deploy.yml`). When absent or empty, the workflow-trigger suggestion is omitted.
12
13
 
13
14
  You are the pipeline's emergency rollback system. When a shipped feature or fix causes production issues, this command creates a revert PR and fast-tracks it through the pipeline.
14
15
 
@@ -112,12 +113,29 @@ If conflicts cannot be auto-resolved, report to user and STOP.
112
113
 
113
114
  ### Step 2C: Verify the revert compiles
114
115
 
116
+ Read `forge.yaml → verification.commands` for project-specific tool commands:
117
+
115
118
  ```bash
116
- # Python files
117
- poetry -C services/api run python -m py_compile {reverted_python_files} 2>&1
119
+ # Python compile check (always runs — no config needed)
120
+ for f in {reverted_python_files}; do
121
+ python3 -m py_compile "$f" 2>&1
122
+ done
123
+
124
+ # Python format check (config-driven)
125
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
126
+ if [ -n "$PYTHON_FORMAT" ]; then
127
+ eval "$PYTHON_FORMAT" 2>&1 | head -20
128
+ else
129
+ echo "SKIPPED — python.format not configured in verification.commands"
130
+ fi
118
131
 
119
- # TypeScript files (if any)
120
- cd web && npx tsc --noEmit 2>&1 | head -20
132
+ # TypeScript typecheck (config-driven)
133
+ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
134
+ if [ -n "$TS_TYPECHECK" ]; then
135
+ eval "$TS_TYPECHECK" 2>&1 | head -20
136
+ else
137
+ echo "SKIPPED — typescript.typecheck not configured in verification.commands"
138
+ fi
121
139
  ```
122
140
 
123
141
  ---
@@ -160,10 +178,11 @@ PR_EOF
160
178
  ```bash
161
179
  gh pr merge {REVERT_PR} --merge
162
180
  ```
163
- - Then trigger hotfix deploy:
181
+ - Then trigger hotfix deploy (requires `deploy.workflow` set in `forge.yaml`):
164
182
  ```bash
165
183
  echo "Revert merged to main. CI/CD will deploy automatically."
166
- echo "For faster deploy, trigger hotfix: gh workflow run hotfix-deploy.yml --ref main -f services={affected} -f reason=\"Rollback PR #{PR_NUMBER}\""
184
+ echo "For faster deploy, trigger deploy workflow: gh workflow run {deploy.workflow} --ref main -f {deploy.workflow_inputs.services}={affected} -f {deploy.workflow_inputs.reason}=\"Rollback PR #{PR_NUMBER}\""
185
+ # If deploy.workflow is not configured in forge.yaml, CI/CD will deploy automatically on the next push to main.
167
186
  ```
168
187
 
169
188
  **If not urgent**:
@@ -88,31 +88,50 @@ Needs human review before proceeding to commit.
88
88
 
89
89
  ## Phase V2: Format and Verify
90
90
 
91
- Run after quality gate passes. Format checks are per-language:
91
+ Run after quality gate passes. All tool commands are read from `forge.yaml → verification.commands`; each step logs `SKIPPED — not configured` when the corresponding key is absent rather than silently passing:
92
92
 
93
93
  **Python**:
94
94
  ```bash
95
95
  cd {WORKTREE_PATH}
96
- black {PYTHON_FILES}
97
- isort {PYTHON_FILES}
96
+
97
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
98
+ if [ -n "$PYTHON_FORMAT" ]; then
99
+ eval "$PYTHON_FORMAT" 2>&1
100
+ else
101
+ echo "SKIPPED — python.format not configured in verification.commands"
102
+ fi
103
+
104
+ # Compile check always runs for Python files (no config needed — catches syntax errors)
98
105
  python -m py_compile {PYTHON_FILES}
99
106
  ```
100
107
  Failures in `py_compile` are BLOCKING — fix before continuing.
101
108
 
102
- **TypeScript (tsc)**:
109
+ **TypeScript**:
103
110
  ```bash
104
111
  cd {WORKTREE_PATH}
105
- prettier --write {TS_FILES}
106
- tsc --noEmit
107
- ```
108
- `tsc --noEmit` failures are BLOCKING — fix type errors before continuing.
109
112
 
110
- **TypeScript (satellite repos — MCP, n8n)**:
111
- ```bash
112
- cd {WORKTREE_PATH}
113
- npm run build
114
- prettier --write {TS_FILES}
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
+
117
+ if [ -n "$TS_FORMAT" ]; then
118
+ eval "$TS_FORMAT" 2>&1
119
+ else
120
+ echo "SKIPPED — typescript.format not configured in verification.commands"
121
+ fi
122
+
123
+ if [ -n "$TS_TYPECHECK" ]; then
124
+ eval "$TS_TYPECHECK" 2>&1
125
+ TS_EXIT=$?
126
+ elif [ -n "$TS_BUILD" ]; then
127
+ eval "$TS_BUILD" 2>&1 | tail -30
128
+ TS_EXIT=$?
129
+ else
130
+ echo "SKIPPED — typescript.typecheck and typescript.build not configured in verification.commands"
131
+ TS_EXIT=0
132
+ fi
115
133
  ```
134
+ Typecheck or build failures are BLOCKING — fix type errors before continuing.
116
135
 
117
136
  **Shell scripts**: Verify service interactions — read target middleware files, document what was verified in V4 summary.
118
137
 
@@ -40,6 +40,8 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
40
40
  - If FORGE:INVESTIGATOR comment is absent → EXIT with `DECOMPOSE_RESULT: status: BLOCKED`, blocker: "No investigation report found — run investigate first"
41
41
  - If investigation report has no Decomposition Assessment section, OR the assessment does not list any sub-issues → EXIT with `DECOMPOSE_RESULT: status: BLOCKED`, blocker: "Investigation report has no decomposition plan — re-run investigate with explicit decomposition scope"
42
42
 
43
+ > **Shared Scoping Convention**: This investigation gate (blocks without a `FORGE:INVESTIGATOR` comment) is the **reference pattern** for investigation-gated issue creation across the pipeline. `milestone.md` Step 4 and `orchestrate.md` MUST apply the same principle: read code and identify all affected call sites BEFORE writing any issue body. Sub-issue bodies created in Phase D3 MUST use the Pipeline Issue Template defined in `issue.md` Phase 3D — that template is the single canonical standard for all automated issue creation. <!-- Added: forge#293 -->
44
+
43
45
  Extract from investigation report:
44
46
  - Decomposition Assessment section: list of proposed sub-issues with titles and dependencies
45
47
  - Milestone (from issue metadata)
@@ -89,16 +89,20 @@ Mission: Validate whether the issue is real. Assume description is wrong until p
89
89
 
90
90
  **Domain file hints** (start search here):
91
91
 
92
- | Domain | Key files |
93
- |--------|-----------|
94
- | BILLING | `routers/billing.py`, `core/pricing.py`, `services/credit_service.py` |
95
- | SCRAPING | `unified_consumer.py`, `queues.py`, `domain_playbooks.json` |
92
+ Configure domain-to-file mappings for your project in `forge.yaml → review.key_paths`. The table below shows the standard layout — override any entry that doesn't match your codebase:
93
+
94
+ | Domain | Default key files (override via forge.yaml) |
95
+ |--------|---------------------------------------------|
96
+ | BILLING | `routers/billing.py`, `core/pricing.py`, `services/payment_service.py` |
97
+ | WORKER | `worker/main.py`, `worker/queues.py`, `worker/tasks.py` |
96
98
  | AUTH | `core/auth.py`, `routers/auth.py`, `dependencies.py` |
97
99
  | DATABASE | `infra/migrations/`, `models/`, `db/` |
98
100
  | FRONTEND | `web/src/app/`, `web/src/components/`, `web/src/lib/` |
99
- | CORTEX | `cortex_client.py`, `routers/cortex.py` |
101
+ | AI | `services/ai_client.py`, `routers/ai.py` |
100
102
  | INFRA | `.github/workflows/`, `docker-compose.yml`, `infra/traefik/` |
101
103
 
104
+ If `forge.yaml → review.key_paths` is present, use those mappings instead of the defaults above.
105
+
102
106
  **Steps**: Check right branch → read domain files → verify claims → git blame → determine root cause → identify affected files → validate proposed fixes against full system stack.
103
107
 
104
108
  **Fix-Approach Validation**: If issue proposes a fix, don't adopt it as spec. Trace through target system's middleware, auth, routing.
@@ -210,7 +214,24 @@ Fix HIGH/MEDIUM findings. Max 2 iterations. Skip for 1-file config/docs edits.
210
214
  All client-side fetch/useSWR/apiFetch must use `/api/...` proxy routes, NEVER `/api/v1/...` directly.
211
215
 
212
216
  ### 3I: Deployment completeness check (MANDATORY)
213
- New env vars must be in: `.env.example` + SOPS `prod.enc.yaml` + `decrypt-secrets.sh` ENV_MAPPING + `env_validation.py` (if API).
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.`
214
235
 
215
236
  ### 3J: Commit
216
237
  Conventional prefix (fix/feat/refactor/docs). Reference #{NUMBER} in the commit message.
@@ -747,25 +747,50 @@ if iteration == max_iterations AND not PASS:
747
747
 
748
748
  ### 3H: Format and verify
749
749
 
750
+ All tool commands are read from `forge.yaml → verification.commands`. When a key is absent, the step logs `SKIPPED — not configured in verification.commands` and continues rather than silently passing.
751
+
750
752
  **Python**:
751
753
  ```bash
752
754
  cd {WORKTREE_PATH}
753
- black {PYTHON_FILES} && isort {PYTHON_FILES} && python -m py_compile {PYTHON_FILES}
755
+
756
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
757
+ if [ -n "$PYTHON_FORMAT" ]; then
758
+ eval "$PYTHON_FORMAT" 2>&1
759
+ else
760
+ echo "SKIPPED — python.format not configured in verification.commands"
761
+ fi
762
+
763
+ # Compile check always runs (no config needed — catches syntax errors)
764
+ python -m py_compile {PYTHON_FILES}
754
765
  ```
755
766
  `py_compile` failures are BLOCKING.
756
767
 
757
- **TypeScript (primary repo — has tsconfig.json)**:
768
+ **TypeScript**:
758
769
  ```bash
759
770
  cd {WORKTREE_PATH}
760
- prettier --write {TS_FILES} && tsc --noEmit
761
- ```
762
- `tsc --noEmit` failures are BLOCKING.
763
771
 
764
- **TypeScript (satellite repos)**:
765
- ```bash
766
- cd {WORKTREE_PATH}
767
- npm run build && prettier --write {TS_FILES}
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
+
776
+ if [ -n "$TS_FORMAT" ]; then
777
+ eval "$TS_FORMAT" 2>&1
778
+ else
779
+ echo "SKIPPED — typescript.format not configured in verification.commands"
780
+ fi
781
+
782
+ if [ -n "$TS_TYPECHECK" ]; then
783
+ eval "$TS_TYPECHECK" 2>&1
784
+ TS_EXIT=$?
785
+ elif [ -n "$TS_BUILD" ]; then
786
+ eval "$TS_BUILD" 2>&1 | tail -30
787
+ TS_EXIT=$?
788
+ else
789
+ echo "SKIPPED — typescript.typecheck and typescript.build not configured in verification.commands"
790
+ TS_EXIT=0
791
+ fi
768
792
  ```
793
+ Typecheck or build failures are BLOCKING.
769
794
 
770
795
  ### 3I: Frontend proxy wiring check (MANDATORY)
771
796
 
@@ -791,27 +816,44 @@ Advisory only — does not block build. Check for lambda/callable in connect_arg
791
816
 
792
817
  Skip if no new env vars introduced.
793
818
 
819
+ **Config variables used by this phase** (set in `forge.yaml`):
820
+ - `{deploy.secrets_backend}` — secrets delivery method (`sops`, `aws-sm`, `vault`, `ci-env`, `none`). When absent or not `sops`, SOPS-specific checks below are skipped with an explicit log message.
821
+ - `{verification.services[name].container}` — container name for post-deploy verification. Resolved by matching the service name; falls back to `{service}` (bare name) when not configured.
822
+
794
823
  For each new env var, verify present in ALL required locations:
795
824
 
796
825
  | Location | Required for |
797
826
  |----------|-------------|
798
827
  | `.env.example` | All new vars |
799
- | `infra/secrets/prod.enc.yaml` (SOPS) | Production secrets |
800
- | `infra/decrypt-secrets.sh` ENV_MAPPING | All vars in deploy chain |
801
- | `app/env_validation.py` | API service vars |
802
- | `docker-compose.prod.yml` | Vars needing explicit injection |
828
+ | Secrets backend (see `deploy.secrets_backend`) | Secret vars — skip if backend is `none` or unset |
829
+ | `app/env_validation.py` | API service vars (if project has one) |
830
+ | `docker-compose.prod.yml` | Vars needing explicit injection (if project uses Docker Compose) |
803
831
 
804
- Deploy chain: SOPS → `decrypt-secrets.sh` (ENV_MAPPING) → `.env.secrets` → `merge-env-secrets.sh` → `.env.production` → docker-compose `env_file`.
832
+ **Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*:
805
833
 
806
- **Operator-set var classification** *(trigger: new env var is NOT present in `infra/secrets/prod.enc.yaml`)*: <!-- Added: forge#380 -->
834
+ If the project uses SOPS, verify the new var is present in all SOPS chain locations:
835
+ - `infra/secrets/prod.enc.yaml` — SOPS-encrypted secret store
836
+ - `infra/decrypt-secrets.sh` ENV_MAPPING — maps SOPS key to env var name
837
+ - Deploy chain: SOPS → `decrypt-secrets.sh` (ENV_MAPPING) → `.env.secrets` → `merge-env-secrets.sh` → `.env.production` → docker-compose `env_file`
807
838
 
808
- Some env vars are operator-set (non-secret, not sourced from SOPS) — they must be manually added to `.env.production` on the production server. The SOPS deploy chain does not carry them. When a new env var has no entry in `prod.enc.yaml`, classify it as operator-set and add a **HARD BLOCKER** item to the Testing Checklist:
839
+ If `deploy.secrets_backend` is absent or not `sops`, skip these checks and log:
840
+ > `SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.`
841
+
842
+ **Operator-set var classification** *(trigger: new env var is NOT in the configured secrets backend)*: <!-- Added: forge#380 -->
843
+
844
+ Some env vars are operator-set (non-secret, not sourced from the secrets backend) — they must be manually added to the runtime environment on the production server. When a new env var has no entry in the secrets backend, classify it as operator-set and add a **HARD BLOCKER** item to the Testing Checklist.
845
+
846
+ Resolve the container name for the verification command:
847
+ 1. Look up the service in `forge.yaml → verification.services[]` by name — use the `container` field if present.
848
+ 2. If no matching entry, fall back to the bare service name: `{service}` (no suffix).
809
849
 
810
850
  ```
811
- - [ ] HARD BLOCKER: Add {VAR_NAME} to .env.production on the production server.
812
- This var is operator-set (not in SOPS) — it does NOT flow through the automated
813
- deploy chain. It must be added manually before or after deploy.
814
- Verify with: docker exec {service}-blue env | grep {VAR_NAME}
851
+ - [ ] HARD BLOCKER: Add {VAR_NAME} to the runtime environment on the production server.
852
+ This var is operator-set — it does NOT flow through the automated secrets chain.
853
+ It must be added manually before or after deploy.
854
+ Verify with: docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
855
+ (CONTAINER_NAME resolved from verification.services[{service}].container in forge.yaml,
856
+ or bare service name if not configured)
815
857
  ```
816
858
 
817
859
  **`env_file` re-read warning** *(trigger: any new env var added to `.env.production` path)*:
@@ -822,11 +864,13 @@ Add this warning to the Testing Checklist whenever a new env var is introduced (
822
864
 
823
865
  **Post-deploy in-container verification** *(trigger: any new env var)*:
824
866
 
825
- Add the following to the Testing Checklist so the deployer can confirm delivery after deploy:
867
+ Add the following to the Testing Checklist so the deployer can confirm delivery after deploy.
868
+
869
+ Resolve `{CONTAINER_NAME}` from `forge.yaml → verification.services[{service}].container`; use `{service}` (bare) if the field is absent.
826
870
 
827
871
  ```bash
828
872
  # Verify env var reached the running container (run post-deploy)
829
- docker exec {service}-blue env | grep {VAR_NAME}
873
+ docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
830
874
  # Expected: {VAR_NAME}={value}
831
875
  # If blank: container was not recreated — run: docker compose up --no-deps --force-recreate {service}
832
876
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "GitHub as a knowledge graph for AI agents. Autonomous dev pipeline for Claude Code.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -5,6 +5,18 @@
5
5
  # changed_files_file: path to a file listing changed files (one per line)
6
6
  # repo_root: path to the repository root
7
7
  #
8
+ # Layout path overrides (env vars — override defaults for non-AlterLab layouts):
9
+ # FORGE_PAGES_ROOT Root of Next.js App Router pages dir (default: web/src/app)
10
+ # FORGE_API_ROUTERS_DIR Dir containing FastAPI router .py files (default: services/api/app/routers)
11
+ # FORGE_API_MAIN Path to API app entrypoint, relative to repo root (default: services/api/app/main.py)
12
+ # FORGE_API_MIDDLEWARE_DIR Dir containing API middleware .py files (default: services/api/app/middleware)
13
+ # FORGE_API_SERVICES_DIR Root of the backend API service dir (default: services/api)
14
+ # FORGE_WORKER_DIR Root of the background worker service dir (default: services/worker)
15
+ # FORGE_COMPONENTS_DIR Root of shared frontend component library (default: web/src/components)
16
+ #
17
+ # Set these from forge.yaml review.layout fields before invoking, e.g.:
18
+ # export FORGE_PAGES_ROOT=$(yq '.review.layout.pages // "web/src/app"' forge.yaml)
19
+ #
8
20
  # Output: Structured findings (one per line, prefixed with severity)
9
21
  # BLOCKING: <message> — must fix before merge
10
22
  # WARNING: <message> — likely issue, verify manually
@@ -23,17 +35,39 @@ else
23
35
  FILES=$(cat "$CHANGED_FILES")
24
36
  fi
25
37
 
38
+ # Layout path configuration — read from env vars, fall back to AlterLab defaults
39
+ FORGE_PAGES_ROOT="${FORGE_PAGES_ROOT:-web/src/app}"
40
+ FORGE_API_ROUTERS_DIR="${FORGE_API_ROUTERS_DIR:-services/api/app/routers}"
41
+ FORGE_API_MAIN="${FORGE_API_MAIN:-services/api/app/main.py}"
42
+ FORGE_API_MIDDLEWARE_DIR="${FORGE_API_MIDDLEWARE_DIR:-services/api/app/middleware}"
43
+ FORGE_API_SERVICES_DIR="${FORGE_API_SERVICES_DIR:-services/api}"
44
+ FORGE_WORKER_DIR="${FORGE_WORKER_DIR:-services/worker}"
45
+ FORGE_COMPONENTS_DIR="${FORGE_COMPONENTS_DIR:-web/src/components}"
46
+
26
47
  BLOCKING=0
27
48
  WARNINGS=0
28
49
 
29
50
  # --- Next.js Route Handler registration check ---
30
51
  while read -r f; do
31
- URL_PATH=$(echo "$f" | sed 's|^web/src/app||; s|/route\.ts$||; s|\[.*\]|*|g')
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
32
65
  ROUTE_SEGMENT=$(echo "$URL_PATH" | sed 's|/api/v1/||; s|/.*||')
33
66
 
34
67
  # Check if next.config.js has a rewrite that might shadow this route
35
68
  if [ -f "$REPO_ROOT/web/next.config.js" ]; then
36
- if grep -q "$ROUTE_SEGMENT" "$REPO_ROOT/web/next.config.js" 2>/dev/null; then
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
37
71
  echo "WARNING: Route handler $f ($URL_PATH) — next.config.js references '$ROUTE_SEGMENT', may shadow this route"
38
72
  WARNINGS=$((WARNINGS + 1))
39
73
  else
@@ -43,55 +77,65 @@ while read -r f; do
43
77
 
44
78
  # Check if nginx routes this path to Next.js or directly to backend
45
79
  if [ -f "$REPO_ROOT/infra/nginx/nginx.conf" ]; then
46
- if grep -q "location.*$ROUTE_SEGMENT" "$REPO_ROOT/infra/nginx/nginx.conf" 2>/dev/null; then
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
47
84
  echo "WARNING: Route handler $f ($URL_PATH) — nginx.conf has a location block for '$ROUTE_SEGMENT', may bypass Next.js"
48
85
  WARNINGS=$((WARNINGS + 1))
49
86
  fi
50
87
  fi
51
- done < <(echo "$FILES" | grep -E "^web/src/app/api/.*route\.ts$") || true
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
52
93
 
53
94
  # --- Python API Router registration check ---
54
95
  while read -r f; do
55
96
  ROUTER_NAME=$(basename "$f" .py)
56
- MAIN_PY="$REPO_ROOT/services/api/app/main.py"
97
+ MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
57
98
 
58
99
  if [ -f "$MAIN_PY" ]; then
59
- if grep -q "$ROUTER_NAME" "$MAIN_PY" 2>/dev/null; then
60
- echo "OK: API Router '$ROUTER_NAME' ($f) — registered in main.py"
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
102
+ echo "OK: API Router '$ROUTER_NAME' ($f) — registered in $(basename "$MAIN_PY")"
61
103
  else
62
- echo "BLOCKING: API Router '$ROUTER_NAME' ($f) — NOT found in main.py. Router will not be reachable."
104
+ echo "BLOCKING: API Router '$ROUTER_NAME' ($f) — NOT found in $(basename "$MAIN_PY"). Router will not be reachable."
63
105
  BLOCKING=$((BLOCKING + 1))
64
106
  fi
65
107
  else
66
- echo "WARNING: Cannot verify router registration — main.py not found at $MAIN_PY"
108
+ echo "WARNING: Cannot verify router registration — API main not found at $MAIN_PY"
67
109
  WARNINGS=$((WARNINGS + 1))
68
110
  fi
69
- done < <(echo "$FILES" | grep -E "^services/api/app/routers/.*\.py$") || true
111
+ done < <(echo "$FILES" | grep -E "^${FORGE_API_ROUTERS_DIR}/.*\\.py$") || true
70
112
 
71
113
  # --- Python Middleware registration check ---
72
114
  while read -r f; do
73
115
  MIDDLEWARE_NAME=$(basename "$f" .py)
74
- MAIN_PY="$REPO_ROOT/services/api/app/main.py"
116
+ MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
75
117
 
76
118
  if [ -f "$MAIN_PY" ]; then
77
- if grep -q "$MIDDLEWARE_NAME" "$MAIN_PY" 2>/dev/null; then
78
- echo "OK: Middleware '$MIDDLEWARE_NAME' ($f) — registered in main.py"
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
121
+ echo "OK: Middleware '$MIDDLEWARE_NAME' ($f) — registered in $(basename "$MAIN_PY")"
79
122
  else
80
- echo "WARNING: Middleware '$MIDDLEWARE_NAME' ($f) — not found in main.py. May not be active."
123
+ echo "WARNING: Middleware '$MIDDLEWARE_NAME' ($f) — not found in $(basename "$MAIN_PY"). May not be active."
81
124
  WARNINGS=$((WARNINGS + 1))
82
125
  fi
83
126
  fi
84
- done < <(echo "$FILES" | grep -E "^services/api/app/middleware/.*\.py$") || true
127
+ done < <(echo "$FILES" | grep -E "^${FORGE_API_MIDDLEWARE_DIR}/.*\\.py$") || true
85
128
 
86
129
  # --- Shared module import check ---
87
130
  while read -r f; do
88
131
  MODULE_NAME=$(basename "$f" .py)
89
132
 
90
- IMPORTERS=$(grep -rl "$MODULE_NAME" "$REPO_ROOT/services/api/" "$REPO_ROOT/services/worker/" 2>/dev/null | head -5)
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)
91
135
  if [ -n "$IMPORTERS" ]; then
92
136
  echo "OK: Shared module '$MODULE_NAME' ($f) — imported by services"
93
137
  else
94
- echo "WARNING: Shared module '$MODULE_NAME' ($f) — no imports found in services/api/ or services/worker/"
138
+ echo "WARNING: Shared module '$MODULE_NAME' ($f) — no imports found in ${FORGE_API_SERVICES_DIR}/ or ${FORGE_WORKER_DIR}/"
95
139
  WARNINGS=$((WARNINGS + 1))
96
140
  fi
97
141
  done < <(echo "$FILES" | grep -E "^shared/.*\.py$") || true
@@ -100,14 +144,16 @@ done < <(echo "$FILES" | grep -E "^shared/.*\.py$") || true
100
144
  while read -r f; do
101
145
  COMPONENT_NAME=$(basename "$f" .tsx)
102
146
 
103
- IMPORTERS=$(grep -rl "$COMPONENT_NAME" "$REPO_ROOT/web/src/app/" "$REPO_ROOT/web/src/components/" 2>/dev/null | grep -v "$f" | head -3)
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)
104
150
  if [ -n "$IMPORTERS" ]; then
105
151
  echo "OK: Component '$COMPONENT_NAME' ($f) — imported by other files"
106
152
  else
107
153
  echo "WARNING: Component '$COMPONENT_NAME' ($f) — no imports found. May be unused or new."
108
154
  WARNINGS=$((WARNINGS + 1))
109
155
  fi
110
- done < <(echo "$FILES" | grep -E "^web/src/components/.*\.tsx$") || true
156
+ done < <(echo "$FILES" | grep -E "^${FORGE_COMPONENTS_DIR}/.*\\.tsx$") || true
111
157
 
112
158
  # --- Exit code ---
113
159
  if [ "$BLOCKING" -gt 0 ]; then
@@ -24,6 +24,24 @@ DIFF_INPUT="${1:--}"
24
24
  CHANGED_FILES_INPUT="${2:--}"
25
25
  REPO_ROOT="${3:-.}"
26
26
 
27
+ # --- Secrets backend guard ---
28
+ # This script only applies when the project uses SOPS as its secrets backend.
29
+ # Read the configured backend from the FORGE_SECRETS_BACKEND env var (set by the
30
+ # calling pipeline) or fall back to parsing forge.yaml in the repo root.
31
+ #
32
+ # If the backend is not "sops" (or is unset), exit 0 with a skip message.
33
+ # This is a clean no-op — not a false-pass — so callers can distinguish
34
+ # "SOPS configured and passed" from "SOPS not in use on this project".
35
+ _SECRETS_BACKEND="${FORGE_SECRETS_BACKEND:-}"
36
+ if [ -z "$_SECRETS_BACKEND" ] && [ -f "$REPO_ROOT/forge.yaml" ]; then
37
+ _SECRETS_BACKEND=$(grep -E '^\s*secrets_backend:' "$REPO_ROOT/forge.yaml" \
38
+ | head -1 | sed 's/.*secrets_backend:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
39
+ fi
40
+ if [ "$_SECRETS_BACKEND" != "sops" ]; then
41
+ echo "SKIP: verify-sops-chain — secrets_backend is '${_SECRETS_BACKEND:-none}' (not sops). Configure deploy.secrets_backend: sops in forge.yaml to enable SOPS chain verification."
42
+ exit 0
43
+ fi
44
+
27
45
  if [ "$DIFF_INPUT" = "-" ]; then
28
46
  DIFF_CONTENT=$(cat)
29
47
  else
@@ -62,7 +80,9 @@ if [ -n "$NEW_MAPPINGS" ]; then
62
80
 
63
81
  # For each new mapping, verify the SOPS path structure exists
64
82
  # (We can't decrypt SOPS to verify values, but we can check the mapping format is valid)
65
- echo "$NEW_MAPPINGS" | while IFS= read -r mapping; do
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
66
86
  VAR_NAME=$(echo "$mapping" | grep -oE '^"[A-Z_]+"' | tr -d '"')
67
87
  SOPS_SECTION=$(echo "$mapping" | grep -oE '\("([a-z_]+)"' | head -1 | tr -d '("')
68
88
  SOPS_KEY=$(echo "$mapping" | grep -oE ', "([a-z_]+)"\)' | tr -d ', ")')
@@ -73,7 +93,7 @@ if [ -n "$NEW_MAPPINGS" ]; then
73
93
  echo "WARNING: Malformed ENV_MAPPING entry: $mapping"
74
94
  WARNINGS=$((WARNINGS + 1))
75
95
  fi
76
- done
96
+ done < <(echo "$NEW_MAPPINGS")
77
97
  else
78
98
  echo "OK: No new ENV_MAPPING entries in diff"
79
99
  fi