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.
- package/bin/forgedock.mjs +139 -35
- package/commands/autopilot.md +20 -6
- package/commands/cleanup.md +27 -12
- package/commands/deploy-info.md +36 -17
- package/commands/incident-response.md +14 -9
- package/commands/issue.md +8 -6
- package/commands/milestone.md +97 -48
- package/commands/orchestrate.md +108 -64
- package/commands/pipeline-health.md +8 -1
- package/commands/qa-sweep.md +14 -3
- package/commands/review-pr-agents.md +1 -1
- package/commands/review-pr-staging.md +80 -20
- package/commands/review-pr.md +232 -87
- package/commands/rollback.md +25 -6
- package/commands/work-on/build/validate.md +32 -13
- package/commands/work-on/decompose.md +2 -0
- package/commands/work-on-monolithic.md +27 -6
- package/commands/work-on.md +66 -22
- package/package.json +1 -1
- package/scripts/verify-route-registration.sh +65 -19
- package/scripts/verify-sops-chain.sh +22 -2
package/commands/rollback.md
CHANGED
|
@@ -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
|
|
117
|
-
|
|
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
|
|
120
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
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.
|
package/commands/work-on.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
| `
|
|
800
|
-
| `
|
|
801
|
-
| `
|
|
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
|
-
|
|
832
|
+
**Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*:
|
|
805
833
|
|
|
806
|
-
|
|
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
|
-
|
|
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
|
|
812
|
-
This var is operator-set
|
|
813
|
-
|
|
814
|
-
Verify with: docker exec {
|
|
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 {
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" |
|
|
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
|
|
97
|
+
MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
|
|
57
98
|
|
|
58
99
|
if [ -f "$MAIN_PY" ]; then
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
|
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 "
|
|
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
|
|
116
|
+
MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
|
|
75
117
|
|
|
76
118
|
if [ -f "$MAIN_PY" ]; then
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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 "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|