forgedock 1.0.6 → 1.0.7

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=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
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=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
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=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
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=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
114
+ TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
115
+ TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
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.
@@ -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=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
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=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
773
+ TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
774
+ TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
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.7",
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,12 +35,21 @@ 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
+ URL_PATH=$(echo "$f" | sed "s|^${FORGE_PAGES_ROOT}||; s|/route\\.ts\$||; s|\\[.*\\]|*|g")
32
53
  ROUTE_SEGMENT=$(echo "$URL_PATH" | sed 's|/api/v1/||; s|/.*||')
33
54
 
34
55
  # Check if next.config.js has a rewrite that might shadow this route
@@ -48,50 +69,50 @@ while read -r f; do
48
69
  WARNINGS=$((WARNINGS + 1))
49
70
  fi
50
71
  fi
51
- done < <(echo "$FILES" | grep -E "^web/src/app/api/.*route\.ts$") || true
72
+ done < <(echo "$FILES" | grep -E "^${FORGE_PAGES_ROOT}/api/.*route\\.ts$") || true
52
73
 
53
74
  # --- Python API Router registration check ---
54
75
  while read -r f; do
55
76
  ROUTER_NAME=$(basename "$f" .py)
56
- MAIN_PY="$REPO_ROOT/services/api/app/main.py"
77
+ MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
57
78
 
58
79
  if [ -f "$MAIN_PY" ]; then
59
80
  if grep -q "$ROUTER_NAME" "$MAIN_PY" 2>/dev/null; then
60
- echo "OK: API Router '$ROUTER_NAME' ($f) — registered in main.py"
81
+ echo "OK: API Router '$ROUTER_NAME' ($f) — registered in $(basename "$MAIN_PY")"
61
82
  else
62
- echo "BLOCKING: API Router '$ROUTER_NAME' ($f) — NOT found in main.py. Router will not be reachable."
83
+ echo "BLOCKING: API Router '$ROUTER_NAME' ($f) — NOT found in $(basename "$MAIN_PY"). Router will not be reachable."
63
84
  BLOCKING=$((BLOCKING + 1))
64
85
  fi
65
86
  else
66
- echo "WARNING: Cannot verify router registration — main.py not found at $MAIN_PY"
87
+ echo "WARNING: Cannot verify router registration — API main not found at $MAIN_PY"
67
88
  WARNINGS=$((WARNINGS + 1))
68
89
  fi
69
- done < <(echo "$FILES" | grep -E "^services/api/app/routers/.*\.py$") || true
90
+ done < <(echo "$FILES" | grep -E "^${FORGE_API_ROUTERS_DIR}/.*\\.py$") || true
70
91
 
71
92
  # --- Python Middleware registration check ---
72
93
  while read -r f; do
73
94
  MIDDLEWARE_NAME=$(basename "$f" .py)
74
- MAIN_PY="$REPO_ROOT/services/api/app/main.py"
95
+ MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
75
96
 
76
97
  if [ -f "$MAIN_PY" ]; then
77
98
  if grep -q "$MIDDLEWARE_NAME" "$MAIN_PY" 2>/dev/null; then
78
- echo "OK: Middleware '$MIDDLEWARE_NAME' ($f) — registered in main.py"
99
+ echo "OK: Middleware '$MIDDLEWARE_NAME' ($f) — registered in $(basename "$MAIN_PY")"
79
100
  else
80
- echo "WARNING: Middleware '$MIDDLEWARE_NAME' ($f) — not found in main.py. May not be active."
101
+ echo "WARNING: Middleware '$MIDDLEWARE_NAME' ($f) — not found in $(basename "$MAIN_PY"). May not be active."
81
102
  WARNINGS=$((WARNINGS + 1))
82
103
  fi
83
104
  fi
84
- done < <(echo "$FILES" | grep -E "^services/api/app/middleware/.*\.py$") || true
105
+ done < <(echo "$FILES" | grep -E "^${FORGE_API_MIDDLEWARE_DIR}/.*\\.py$") || true
85
106
 
86
107
  # --- Shared module import check ---
87
108
  while read -r f; do
88
109
  MODULE_NAME=$(basename "$f" .py)
89
110
 
90
- IMPORTERS=$(grep -rl "$MODULE_NAME" "$REPO_ROOT/services/api/" "$REPO_ROOT/services/worker/" 2>/dev/null | head -5)
111
+ IMPORTERS=$(grep -rl "$MODULE_NAME" "$REPO_ROOT/${FORGE_API_SERVICES_DIR}/" "$REPO_ROOT/${FORGE_WORKER_DIR}/" 2>/dev/null | head -5)
91
112
  if [ -n "$IMPORTERS" ]; then
92
113
  echo "OK: Shared module '$MODULE_NAME' ($f) — imported by services"
93
114
  else
94
- echo "WARNING: Shared module '$MODULE_NAME' ($f) — no imports found in services/api/ or services/worker/"
115
+ echo "WARNING: Shared module '$MODULE_NAME' ($f) — no imports found in ${FORGE_API_SERVICES_DIR}/ or ${FORGE_WORKER_DIR}/"
95
116
  WARNINGS=$((WARNINGS + 1))
96
117
  fi
97
118
  done < <(echo "$FILES" | grep -E "^shared/.*\.py$") || true
@@ -100,14 +121,14 @@ done < <(echo "$FILES" | grep -E "^shared/.*\.py$") || true
100
121
  while read -r f; do
101
122
  COMPONENT_NAME=$(basename "$f" .tsx)
102
123
 
103
- IMPORTERS=$(grep -rl "$COMPONENT_NAME" "$REPO_ROOT/web/src/app/" "$REPO_ROOT/web/src/components/" 2>/dev/null | grep -v "$f" | head -3)
124
+ IMPORTERS=$(grep -rl "$COMPONENT_NAME" "$REPO_ROOT/${FORGE_PAGES_ROOT}/" "$REPO_ROOT/${FORGE_COMPONENTS_DIR}/" 2>/dev/null | grep -v "$f" | head -3)
104
125
  if [ -n "$IMPORTERS" ]; then
105
126
  echo "OK: Component '$COMPONENT_NAME' ($f) — imported by other files"
106
127
  else
107
128
  echo "WARNING: Component '$COMPONENT_NAME' ($f) — no imports found. May be unused or new."
108
129
  WARNINGS=$((WARNINGS + 1))
109
130
  fi
110
- done < <(echo "$FILES" | grep -E "^web/src/components/.*\.tsx$") || true
131
+ done < <(echo "$FILES" | grep -E "^${FORGE_COMPONENTS_DIR}/.*\\.tsx$") || true
111
132
 
112
133
  # --- Exit code ---
113
134
  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