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
|
@@ -14,12 +14,28 @@ 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
|
|
20
36
|
```bash
|
|
21
|
-
git fetch origin
|
|
22
|
-
git diff origin
|
|
37
|
+
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
38
|
+
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH
|
|
23
39
|
```
|
|
24
40
|
|
|
25
41
|
### Dynamic Exploration
|
|
@@ -29,9 +45,9 @@ From each changed file, follow imports and function calls. Trace data flows acro
|
|
|
29
45
|
|
|
30
46
|
| Confidence | Criteria | Action |
|
|
31
47
|
|------------|----------|--------|
|
|
32
|
-
| CONFIRMED | Traced full code path, found specific proof | Report as P1 |
|
|
33
|
-
| LIKELY | Pattern suggests issue, mitigations might exist | Report as P2 |
|
|
34
|
-
| POSSIBLE | Suspicious but couldn't trace fully | Report as P3 |
|
|
48
|
+
| CONFIRMED | Traced full code path, found specific proof | Report as priority:P1 |
|
|
49
|
+
| LIKELY | Pattern suggests issue, mitigations might exist | Report as priority:P2 |
|
|
50
|
+
| POSSIBLE | Suspicious but couldn't trace fully | Report as priority:P3 |
|
|
35
51
|
| UNFOUNDED | Found correct handling | Do NOT report |
|
|
36
52
|
|
|
37
53
|
### Severity Decision Tree
|
|
@@ -77,17 +93,17 @@ Include ALL findings (CONFIRMED, LIKELY, POSSIBLE). One line per finding, sequen
|
|
|
77
93
|
**Why this matters**: Review findings are filed before the originating PR merges to staging. Without this gate, a staging→main bundle can include commits with known unfixed bugs — the review system caught the issue, but the deploy path ignored it. This gate closes the gap between issue discovery and deploy execution. <!-- Added: forge#303 -->
|
|
78
94
|
|
|
79
95
|
```bash
|
|
80
|
-
git fetch origin
|
|
96
|
+
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
81
97
|
|
|
82
98
|
# Step 1: Find all PR numbers in the staging→main bundle
|
|
83
99
|
# These are the PRs whose commits are included in staging but not yet in main
|
|
84
|
-
BUNDLE_PRS=$(git log origin
|
|
100
|
+
BUNDLE_PRS=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --oneline \
|
|
85
101
|
| grep -oP '#\d+' \
|
|
86
102
|
| sort -u \
|
|
87
103
|
| tr -d '#')
|
|
88
104
|
|
|
89
105
|
# Also extract PR numbers from merge commit subjects (most reliable)
|
|
90
|
-
MERGE_PRS=$(git log origin
|
|
106
|
+
MERGE_PRS=$(git log origin/$DEFAULT_BRANCH..origin/$STAGING_BRANCH --merges --oneline \
|
|
91
107
|
| grep -oP '(?<=pull request #)\d+' \
|
|
92
108
|
| sort -u)
|
|
93
109
|
|
|
@@ -178,10 +194,10 @@ If the gate exits with `RESULT: BLOCK DEPLOY` → **STOP**. Do NOT proceed to Ph
|
|
|
178
194
|
## Phase 0B: Scope Analysis
|
|
179
195
|
|
|
180
196
|
```bash
|
|
181
|
-
git fetch origin
|
|
182
|
-
git diff origin
|
|
183
|
-
git diff origin
|
|
184
|
-
git diff origin
|
|
197
|
+
git fetch origin $DEFAULT_BRANCH $STAGING_BRANCH
|
|
198
|
+
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --stat | tail -20
|
|
199
|
+
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --numstat | awk '{add+=$1; del+=$2} END {print "Added:", add, "Deleted:", del, "Total:", add+del}'
|
|
200
|
+
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH --name-only | sort | uniq
|
|
185
201
|
```
|
|
186
202
|
|
|
187
203
|
Categorize by service (API, Worker, Web, Shared, Infra). Identify high-risk files (billing, credits, pricing, auth, security, migration, scraper).
|
|
@@ -193,26 +209,70 @@ Create review chunks by priority: Billing/Pricing (CRITICAL) → Security/Auth (
|
|
|
193
209
|
## Phase 1: Automated Checks
|
|
194
210
|
|
|
195
211
|
### 1A: Python Linting
|
|
212
|
+
|
|
213
|
+
Read `forge.yaml → verification.commands.python` for project-specific tool commands:
|
|
214
|
+
|
|
196
215
|
```bash
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
|
|
219
|
+
if [ -n "$PYTHON_FORMAT" ]; then
|
|
220
|
+
eval "$PYTHON_FORMAT" 2>&1 | head -30
|
|
221
|
+
else
|
|
222
|
+
echo "SKIPPED — python.format not configured in verification.commands"
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
if [ -n "$PYTHON_LINT" ]; then
|
|
226
|
+
eval "$PYTHON_LINT" 2>&1 | head -30
|
|
227
|
+
else
|
|
228
|
+
echo "SKIPPED — python.lint not configured in verification.commands"
|
|
229
|
+
fi
|
|
199
230
|
```
|
|
200
231
|
|
|
201
232
|
### 1B: TypeScript Type-Check + Build (MANDATORY)
|
|
233
|
+
|
|
234
|
+
Read `forge.yaml → verification.commands.typescript` for project-specific tool commands:
|
|
235
|
+
|
|
202
236
|
```bash
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
|
|
240
|
+
if [ -n "$TS_TYPECHECK" ]; then
|
|
241
|
+
eval "$TS_TYPECHECK" 2>&1
|
|
242
|
+
TS_EXIT=$?
|
|
243
|
+
[ "$TS_EXIT" -ne 0 ] && echo "BLOCKING: typecheck failed — deploy WILL fail"
|
|
244
|
+
else
|
|
245
|
+
echo "SKIPPED — typescript.typecheck not configured in verification.commands"
|
|
246
|
+
TS_EXIT=0
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
if [ -n "$TS_BUILD" ]; then
|
|
250
|
+
eval "$TS_BUILD" 2>&1 | tail -50
|
|
251
|
+
BUILD_EXIT=$?
|
|
252
|
+
[ "$BUILD_EXIT" -ne 0 ] && echo "BLOCKING: build failed — deploy WILL fail"
|
|
253
|
+
else
|
|
254
|
+
echo "SKIPPED — typescript.build not configured in verification.commands"
|
|
255
|
+
fi
|
|
205
256
|
```
|
|
206
|
-
|
|
257
|
+
Build failure is BLOCKING — deploy WILL fail. Typecheck alone misses SSG/prerender failures — configure `typescript.build` in `verification.commands`.
|
|
207
258
|
|
|
208
259
|
### 1C: Python Tests
|
|
260
|
+
|
|
261
|
+
Read `forge.yaml → verification.commands.python.test`:
|
|
262
|
+
|
|
209
263
|
```bash
|
|
210
|
-
|
|
264
|
+
PYTHON_TEST=$(yq '.verification.commands.python.test // ""' forge.yaml 2>/dev/null || echo '')
|
|
265
|
+
|
|
266
|
+
if [ -n "$PYTHON_TEST" ]; then
|
|
267
|
+
eval "$PYTHON_TEST" 2>&1 | tail -50
|
|
268
|
+
else
|
|
269
|
+
echo "SKIPPED — python.test not configured in verification.commands"
|
|
270
|
+
fi
|
|
211
271
|
```
|
|
212
272
|
|
|
213
273
|
### 1D: Secrets Scan
|
|
214
274
|
```bash
|
|
215
|
-
git diff origin
|
|
275
|
+
git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH | grep -iE "(api[_-]?key|secret|password|token|credential)" | grep -vE "(#|//|\.example|placeholder)" | head -20
|
|
216
276
|
```
|
|
217
277
|
|
|
218
278
|
### 1E: CI Status Gate (BLOCKING)
|
|
@@ -304,7 +364,7 @@ Only assign milestone if reviewing a milestone/* branch. Plain staging reviews g
|
|
|
304
364
|
Check for open review-finding issues at same file:line → skip. Closed issues at same location → potential regression (elevate priority).
|
|
305
365
|
|
|
306
366
|
### 7F: Create Issues
|
|
307
|
-
Sequential creation. Title: `Staging Review: {summary} (staging → main)`. Labels: review-finding, needs-validation, staging-review, P1/P2/P3. Body includes: source branch context (`staging`), code context, evidence, validation checklist.
|
|
367
|
+
Sequential creation. Title: `Staging Review: {summary} (staging → main)`. Labels: review-finding, needs-validation, staging-review, priority:P1/priority:P2/priority:P3. Body includes: source branch context (`staging`), code context, evidence, validation checklist.
|
|
308
368
|
|
|
309
369
|
**For each finding** (that passes dedup), create issue:
|
|
310
370
|
```bash
|
package/commands/review-pr.md
CHANGED
|
@@ -156,27 +156,78 @@ Record: services touched, domains detected, PR scope (1-2 sentences), change cat
|
|
|
156
156
|
## Phase 2: Automated Checks (Run ALL in Parallel)
|
|
157
157
|
|
|
158
158
|
### 2A: Python Linting (if Python changed)
|
|
159
|
+
|
|
160
|
+
Read `forge.yaml → verification.commands.python` for project-specific tool commands:
|
|
161
|
+
|
|
159
162
|
```bash
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
# Read toolchain commands from forge.yaml
|
|
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
|
+
|
|
168
|
+
# Run format check
|
|
169
|
+
if [ -n "$PYTHON_FORMAT" ]; then
|
|
170
|
+
eval "$PYTHON_FORMAT" 2>&1 | head -30
|
|
171
|
+
else
|
|
172
|
+
echo "SKIPPED — python.format not configured in verification.commands"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Run lint
|
|
176
|
+
if [ -n "$PYTHON_LINT" ]; then
|
|
177
|
+
eval "$PYTHON_LINT" 2>&1 | head -30
|
|
178
|
+
else
|
|
179
|
+
echo "SKIPPED — python.lint not configured in verification.commands"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Run typecheck
|
|
183
|
+
if [ -n "$PYTHON_TYPECHECK" ]; then
|
|
184
|
+
eval "$PYTHON_TYPECHECK" 2>&1 | head -50
|
|
185
|
+
else
|
|
186
|
+
echo "SKIPPED — python.typecheck not configured in verification.commands"
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# Always run compile check on changed Python files (fast, language-universal)
|
|
190
|
+
gh pr diff $ARGUMENTS --name-only | grep '\.py$' | while IFS= read -r f; do
|
|
191
|
+
python3 -m py_compile "$f" 2>&1
|
|
192
|
+
done
|
|
165
193
|
```
|
|
166
|
-
**IMPORTANT**: Always `cd` into service dir and `poetry run black`. Never `pipx run black`.
|
|
167
194
|
|
|
168
|
-
### 2B: TypeScript/JS (if
|
|
195
|
+
### 2B: TypeScript/JS (if TypeScript/JS changed)
|
|
196
|
+
|
|
197
|
+
Read `forge.yaml → verification.commands.typescript` for project-specific tool commands:
|
|
198
|
+
|
|
169
199
|
```bash
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
|
|
204
|
+
# Run format check
|
|
205
|
+
if [ -n "$TS_FORMAT" ]; then
|
|
206
|
+
eval "$TS_FORMAT" 2>&1 | head -30
|
|
207
|
+
else
|
|
208
|
+
echo "SKIPPED — typescript.format not configured in verification.commands"
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# Run lint
|
|
212
|
+
if [ -n "$TS_LINT" ]; then
|
|
213
|
+
eval "$TS_LINT" 2>&1 | head -30
|
|
214
|
+
else
|
|
215
|
+
echo "SKIPPED — typescript.lint not configured in verification.commands"
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
# Run typecheck
|
|
219
|
+
if [ -n "$TS_TYPECHECK" ]; then
|
|
220
|
+
eval "$TS_TYPECHECK" 2>&1 | head -50
|
|
221
|
+
TS_TYPECHECK_EXIT=$?
|
|
222
|
+
else
|
|
223
|
+
echo "SKIPPED — typescript.typecheck not configured in verification.commands"
|
|
224
|
+
TS_TYPECHECK_EXIT=0
|
|
225
|
+
fi
|
|
173
226
|
```
|
|
174
227
|
|
|
175
228
|
### 2C: Static Type Checking (if Python changed)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
cd services/api && poetry run ruff check app/ --select=E,W,F,B,S --ignore=E501 2>&1 | head -30
|
|
179
|
-
```
|
|
229
|
+
|
|
230
|
+
Covered by `PYTHON_TYPECHECK` command in 2A above. If `verification.commands.python.typecheck` is unset, this step is explicitly skipped with a log line — it does not silently pass.
|
|
180
231
|
|
|
181
232
|
### 2D: Environment Variable Audit
|
|
182
233
|
```bash
|
|
@@ -192,7 +243,7 @@ gh pr diff $ARGUMENTS | grep -oE "['\"][A-Za-z0-9+/=]{40,}['\"]" | head -10
|
|
|
192
243
|
|
|
193
244
|
### 2F: SQL Migration Validation (if *.sql changed)
|
|
194
245
|
```bash
|
|
195
|
-
|
|
246
|
+
gh pr diff $ARGUMENTS --name-only | grep "\.sql$" | while IFS= read -r sql_file; do
|
|
196
247
|
grep -E "FOR UPDATE" "$sql_file" | grep -qE "(SUM|COUNT|AVG|MIN|MAX)\s*\(" && echo "ERROR: FOR UPDATE with aggregate"
|
|
197
248
|
grep -qE "DROP (TABLE|COLUMN|INDEX)" "$sql_file" && ! grep -qE "IF EXISTS" "$sql_file" && echo "WARNING: DROP without IF EXISTS"
|
|
198
249
|
grep -qE "ALTER TABLE.*ADD COLUMN.*NOT NULL" "$sql_file" && ! grep -qE "DEFAULT" "$sql_file" && echo "WARNING: NOT NULL without DEFAULT"
|
|
@@ -206,8 +257,27 @@ git diff origin/main...HEAD -- "**/package.json" | grep -E "^\+" | grep -v "^\+\
|
|
|
206
257
|
```
|
|
207
258
|
|
|
208
259
|
### 2H: Tests
|
|
260
|
+
|
|
261
|
+
Read `forge.yaml → verification.commands.{lang}.test` for the project's test command:
|
|
262
|
+
|
|
209
263
|
```bash
|
|
210
|
-
|
|
264
|
+
# Check each configured language for a test command
|
|
265
|
+
for lang in python typescript go rust; do
|
|
266
|
+
TEST_CMD=$(yq ".verification.commands.${lang}.test // \"\"" forge.yaml 2>/dev/null || echo '')
|
|
267
|
+
if [ -n "$TEST_CMD" ]; then
|
|
268
|
+
echo "=== Running tests (${lang}): ${TEST_CMD} ==="
|
|
269
|
+
eval "$TEST_CMD" 2>&1 | tail -30
|
|
270
|
+
TEST_EXIT=$?
|
|
271
|
+
[ "$TEST_EXIT" -ne 0 ] && echo "BLOCKING: ${lang} tests failed (exit $TEST_EXIT)"
|
|
272
|
+
fi
|
|
273
|
+
done
|
|
274
|
+
|
|
275
|
+
# If no test commands were configured, log explicitly
|
|
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
|
+
if [ -z "$PYTHON_TEST" ] && [ -z "$TS_TEST" ]; then
|
|
279
|
+
echo "SKIPPED — no test commands configured in verification.commands"
|
|
280
|
+
fi
|
|
211
281
|
```
|
|
212
282
|
**BLOCKING if tests fail.**
|
|
213
283
|
|
|
@@ -217,36 +287,67 @@ cd services/api && poetry run pytest tests/ -x -q --tb=short 2>&1 | tail -30
|
|
|
217
287
|
CHANGED_FILES=$(gh pr diff $ARGUMENTS --name-only)
|
|
218
288
|
HAS_TS=$(echo "$CHANGED_FILES" | grep -E '\.(tsx?|jsx?)$' | head -1)
|
|
219
289
|
HAS_PY=$(echo "$CHANGED_FILES" | grep -E '\.py$' | head -1)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
290
|
+
# Use POSIX-portable if/else (avoid bash-only [[ ]])
|
|
291
|
+
IS_STAGING_TO_MAIN="false"
|
|
292
|
+
if [ "$HEAD" = "staging" ] && [ "$BASE" = "main" ]; then IS_STAGING_TO_MAIN="true"; fi
|
|
293
|
+
IS_MILESTONE_TO_STAGING="false"
|
|
294
|
+
case "$HEAD" in milestone/*) if [ "$BASE" = "staging" ]; then IS_MILESTONE_TO_STAGING="true"; fi ;; esac
|
|
295
|
+
REQUIRES_FULL_BUILD="false"
|
|
296
|
+
if [ "$IS_STAGING_TO_MAIN" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; then REQUIRES_FULL_BUILD="true"; fi
|
|
223
297
|
```
|
|
224
298
|
|
|
225
299
|
**TypeScript files changed:**
|
|
300
|
+
|
|
301
|
+
Read `forge.yaml → verification.commands.typescript.typecheck` and `.build`:
|
|
302
|
+
|
|
226
303
|
```bash
|
|
227
304
|
gh pr checkout $ARGUMENTS --detach 2>/dev/null
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
305
|
+
|
|
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
|
+
|
|
309
|
+
if [ -n "$TS_TYPECHECK" ]; then
|
|
310
|
+
eval "$TS_TYPECHECK" 2>&1
|
|
311
|
+
TSC_EXIT=$?
|
|
312
|
+
else
|
|
313
|
+
echo "SKIPPED — typescript.typecheck not configured in verification.commands"
|
|
314
|
+
TSC_EXIT=0
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
if [ -n "$TS_BUILD" ] && { [ "$REQUIRES_FULL_BUILD" = "true" ] || [ "$TSC_EXIT" -eq 0 ]; }; then
|
|
318
|
+
eval "$TS_BUILD" 2>&1 | tail -30
|
|
232
319
|
BUILD_EXIT=$?
|
|
320
|
+
elif [ -z "$TS_BUILD" ]; then
|
|
321
|
+
echo "SKIPPED — typescript.build not configured in verification.commands"
|
|
233
322
|
fi
|
|
234
|
-
|
|
323
|
+
|
|
324
|
+
git checkout - 2>/dev/null
|
|
235
325
|
```
|
|
236
326
|
|
|
237
327
|
If `TSC_EXIT != 0`: **CONFIRMED blocking** — type errors.
|
|
238
|
-
If `BUILD_EXIT != 0`: **CONFIRMED blocking** —
|
|
328
|
+
If `BUILD_EXIT != 0`: **CONFIRMED blocking** — build/prerender failure.
|
|
239
329
|
|
|
240
|
-
**CRITICAL**:
|
|
330
|
+
**CRITICAL**: typecheck alone is NOT sufficient for staging→main or milestone→staging — configure `typescript.build` in `verification.commands` to catch SSG/prerender failures that typecheck misses.
|
|
241
331
|
|
|
242
332
|
**Python files changed:**
|
|
333
|
+
|
|
334
|
+
Read `forge.yaml → verification.commands.python.format` and `.build`:
|
|
335
|
+
|
|
243
336
|
```bash
|
|
244
337
|
gh pr checkout $ARGUMENTS --detach 2>/dev/null
|
|
245
|
-
|
|
338
|
+
|
|
339
|
+
# Compile-check all changed Python files (language-universal — no config needed)
|
|
340
|
+
echo "$CHANGED_FILES" | grep '\.py$' | while IFS= read -r f; do python3 -m py_compile "$f" 2>&1; done
|
|
341
|
+
|
|
246
342
|
if [ "$REQUIRES_FULL_BUILD" = "true" ]; then
|
|
247
|
-
|
|
248
|
-
|
|
343
|
+
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
344
|
+
if [ -n "$PYTHON_FORMAT" ]; then
|
|
345
|
+
eval "$PYTHON_FORMAT" 2>&1
|
|
346
|
+
else
|
|
347
|
+
echo "SKIPPED — python.format not configured in verification.commands (full-build format check skipped)"
|
|
348
|
+
fi
|
|
249
349
|
fi
|
|
350
|
+
|
|
250
351
|
git checkout - 2>/dev/null
|
|
251
352
|
```
|
|
252
353
|
|
|
@@ -258,9 +359,9 @@ Check whether the PR's actual changes match what the builder committed to in its
|
|
|
258
359
|
|
|
259
360
|
```bash
|
|
260
361
|
# Find the contract comment on the linked issue
|
|
261
|
-
ISSUE_NUM=$(gh pr view $ARGUMENTS --json body --jq '.body
|
|
362
|
+
ISSUE_NUM=$(gh pr view $ARGUMENTS --json body --jq '.body | gsub("(?s).*?(?:Closes #|#)(?<n>[0-9]+).*"; "\(.n)") // empty' 2>/dev/null | head -1)
|
|
262
363
|
if [ -n "$ISSUE_NUM" ]; then
|
|
263
|
-
CONTRACT_FILES=$(gh api repos/${REPO}/issues/${ISSUE_NUM}/comments --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | .[0].body' 2>/dev/null | grep -
|
|
364
|
+
CONTRACT_FILES=$(gh api repos/${REPO}/issues/${ISSUE_NUM}/comments --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | .[0].body' 2>/dev/null | grep -E '`[^`]+\.(py|tsx?|sql|sh|yml|yaml|json)`' | grep -oE '`[^`]+\.(py|tsx?|sql|sh|yml|yaml|json)`' | tr -d '`' | sort -u)
|
|
264
365
|
PR_FILES=$(gh pr diff $ARGUMENTS --name-only | sort -u)
|
|
265
366
|
|
|
266
367
|
# Files in PR but NOT in contract
|
|
@@ -289,17 +390,28 @@ For each changed file, identify its **activation path** — how does execution r
|
|
|
289
390
|
|
|
290
391
|
### Step 2.5A: Identify File Types and Their Registration Points
|
|
291
392
|
|
|
292
|
-
Map each changed file to its activation requirements
|
|
393
|
+
Map each changed file to its activation requirements.
|
|
394
|
+
|
|
395
|
+
**Layout path resolution** — Before applying the table below, read your project's layout from `forge.yaml → review.layout` and substitute the values into the pattern column. Defaults (used when the key is absent) match the AlterLab monorepo layout:
|
|
396
|
+
|
|
397
|
+
| `forge.yaml` key | Default | Used in table as |
|
|
398
|
+
|-----------------|---------|-----------------|
|
|
399
|
+
| `review.layout.pages` | `web/src/app` | `{PAGES_ROOT}` |
|
|
400
|
+
| `review.layout.api_routers` | `services/api/app/routers` | `{API_ROUTERS}` |
|
|
401
|
+
| `review.layout.api_main` | `services/api/app/main.py` | `{API_MAIN}` |
|
|
402
|
+
| `review.layout.api_middleware` | `services/api/app/middleware` | `{API_MIDDLEWARE}` |
|
|
403
|
+
| `review.layout.migrations` | `infra/migrations` | `{MIGRATIONS}` |
|
|
404
|
+
| `review.layout.worker` | `services/worker` | `{WORKER}` |
|
|
293
405
|
|
|
294
406
|
| Changed File Pattern | Assumption | Verification Target |
|
|
295
407
|
|---------------------|------------|---------------------|
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
408
|
+
| `{PAGES_ROOT}/api/**/*.ts` (Route Handler) | Requests reach this handler | Check `web/next.config.js` rewrites don't shadow it; check `infra/nginx/nginx.conf` routes path to Next.js |
|
|
409
|
+
| `{API_ROUTERS}/*.py` (API Router) | Router is registered in app | Check `{API_MAIN}` includes this router |
|
|
410
|
+
| `{API_MIDDLEWARE}/*.py` | Middleware is in the stack | Check `{API_MAIN}` middleware registration order |
|
|
411
|
+
| `{MIGRATIONS}/*.sql` | Migration runs on current schema | Check previous migration's end state matches assumptions |
|
|
300
412
|
| `shared/**/*.py` | Imported by consumer services | Verify import paths exist in api/worker; check Docker volume mounts |
|
|
301
|
-
| `
|
|
302
|
-
| Any file using `os.getenv("NEW_VAR")` | Env var is set at runtime | Check `docker-compose.yml`, `.env.example`, `
|
|
413
|
+
| `{WORKER}/**/*.py` (Consumer) | Queue consumer is registered | Check consumer registration in worker startup |
|
|
414
|
+
| Any file using `os.getenv("NEW_VAR")` | Env var is set at runtime | Check `docker-compose.yml`, `.env.example`, `{API_MAIN}` env validation module |
|
|
303
415
|
| `scripts/decrypt-secrets.sh` (ENV_MAPPING) | Secret reaches running container | Trace full chain: SOPS key → ENV_MAPPING → deploy workflow SCP target → merge script path → `docker-compose.prod.yml` env_file. See Step 2.5B SOPS deploy chain check. |
|
|
304
416
|
| `.secrets/prod.enc.yaml` | SOPS key maps to ENV_MAPPING | Verify key path in YAML matches the tuple in `decrypt-secrets.sh` ENV_MAPPING |
|
|
305
417
|
| `.github/workflows/deploy-production.yml` | Deploy paths are consistent | Verify SCP target + merge script `PROJECT` var resolve to same dir as `docker-compose.prod.yml` env_file |
|
|
@@ -317,55 +429,73 @@ Map each changed file to its activation requirements:
|
|
|
317
429
|
|
|
318
430
|
For each changed file, execute the relevant checks using the standalone verification scripts in `$FORGE_HOME/scripts/`. These scripts can also be run independently outside the review context (e.g., from `/quality-gate` or `/work-on` builder steps).
|
|
319
431
|
|
|
432
|
+
**Platform note**: The verify-*.sh scripts require bash and standard POSIX tools. On Windows without bash (Git Bash / WSL / MSYS2), these checks are skipped with an explicit message — the review continues without them.
|
|
433
|
+
|
|
320
434
|
```bash
|
|
321
435
|
CHANGED_FILES=$(gh pr diff $ARGUMENTS --name-only)
|
|
322
436
|
REPO_ROOT="." # Assumes cwd is the repo root
|
|
323
437
|
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
# --- Script-based checks (reusable, testable, deterministic) ---
|
|
331
|
-
# Each script exits 0 (pass), 1 (blocking findings), or 2 (warnings only).
|
|
332
|
-
# Output is structured: "BLOCKING: ...", "WARNING: ...", "OK: ..." per line.
|
|
333
|
-
|
|
334
|
-
# 1. Route/router/middleware/shared-module/component registration
|
|
335
|
-
echo "=== Running: verify-route-registration.sh ==="
|
|
336
|
-
$FORGE_HOME/scripts/verify-route-registration.sh "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
|
|
337
|
-
|
|
338
|
-
# 2. Environment variable wiring (checks .env.example, docker-compose, env_validation, SOPS mapping)
|
|
339
|
-
echo "=== Running: verify-env-vars.sh ==="
|
|
340
|
-
$FORGE_HOME/scripts/verify-env-vars.sh "$DIFF_TMP" "$REPO_ROOT" || true
|
|
341
|
-
|
|
342
|
-
# 3. Host headers in shell scripts + client-side proxy bypass check
|
|
343
|
-
# Read project-specific internal service patterns from forge.yaml (if present)
|
|
344
|
-
FORGE_INTERNAL_PATTERNS=""
|
|
345
|
-
if [ -f "$REPO_ROOT/forge.yaml" ]; then
|
|
346
|
-
FORGE_INTERNAL_PATTERNS=$(grep -A 999 'internal_service_patterns:' "$REPO_ROOT/forge.yaml" \
|
|
347
|
-
| grep -E '^\s*-\s+' \
|
|
348
|
-
| sed 's/^\s*-\s*//' \
|
|
349
|
-
| tr -d '"'"'" \
|
|
350
|
-
| paste -sd '|' -)
|
|
438
|
+
# --- Platform / bash capability guard ---
|
|
439
|
+
# The verify-*.sh scripts require bash. Detect availability before invoking.
|
|
440
|
+
# On Windows without Git Bash/WSL, skip gracefully rather than crash.
|
|
441
|
+
BASH_AVAILABLE=false
|
|
442
|
+
if command -v bash >/dev/null 2>&1 && bash -c 'echo ok' >/dev/null 2>&1; then
|
|
443
|
+
BASH_AVAILABLE=true
|
|
351
444
|
fi
|
|
352
|
-
export FORGE_INTERNAL_PATTERNS
|
|
353
|
-
echo "=== Running: verify-host-headers.sh ==="
|
|
354
|
-
$FORGE_HOME/scripts/verify-host-headers.sh "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
|
|
355
445
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
446
|
+
if [ "$BASH_AVAILABLE" = "true" ]; then
|
|
447
|
+
# Write changed files and diff to temp files for script consumption.
|
|
448
|
+
# Use PID-based names instead of mktemp for cross-platform compatibility.
|
|
449
|
+
CHANGED_FILES_TMP="/tmp/forge-review-changed-$$.tmp"
|
|
450
|
+
DIFF_TMP="/tmp/forge-review-diff-$$.tmp"
|
|
451
|
+
echo "$CHANGED_FILES" > "$CHANGED_FILES_TMP"
|
|
452
|
+
gh pr diff $ARGUMENTS > "$DIFF_TMP"
|
|
453
|
+
|
|
454
|
+
# --- Script-based checks (reusable, testable, deterministic) ---
|
|
455
|
+
# Each script exits 0 (pass), 1 (blocking findings), or 2 (warnings only).
|
|
456
|
+
# Output is structured: "BLOCKING: ...", "WARNING: ...", "OK: ..." per line.
|
|
457
|
+
|
|
458
|
+
# 1. Route/router/middleware/shared-module/component registration
|
|
459
|
+
echo "=== Running: verify-route-registration.sh ==="
|
|
460
|
+
bash "$FORGE_HOME/scripts/verify-route-registration.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
|
|
461
|
+
|
|
462
|
+
# 2. Environment variable wiring (checks .env.example, docker-compose, env_validation, SOPS mapping)
|
|
463
|
+
echo "=== Running: verify-env-vars.sh ==="
|
|
464
|
+
bash "$FORGE_HOME/scripts/verify-env-vars.sh" "$DIFF_TMP" "$REPO_ROOT" || true
|
|
465
|
+
|
|
466
|
+
# 3. Host headers in shell scripts + client-side proxy bypass check
|
|
467
|
+
# Read project-specific internal service patterns from forge.yaml (if present)
|
|
468
|
+
FORGE_INTERNAL_PATTERNS=""
|
|
469
|
+
if [ -f "$REPO_ROOT/forge.yaml" ]; then
|
|
470
|
+
FORGE_INTERNAL_PATTERNS=$(grep -A 999 'internal_service_patterns:' "$REPO_ROOT/forge.yaml" \
|
|
471
|
+
| grep -E '^\s*-\s+' \
|
|
472
|
+
| sed 's/^\s*-\s*//' \
|
|
473
|
+
| tr -d '"'"'" \
|
|
474
|
+
| awk 'NR>1{printf "|"}{printf $0}END{print ""}')
|
|
475
|
+
fi
|
|
476
|
+
export FORGE_INTERNAL_PATTERNS
|
|
477
|
+
echo "=== Running: verify-host-headers.sh ==="
|
|
478
|
+
bash "$FORGE_HOME/scripts/verify-host-headers.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
|
|
479
|
+
|
|
480
|
+
# 4. SOPS deploy chain (ENV_MAPPING consistency, deploy path drift, hotfix sync)
|
|
481
|
+
echo "=== Running: verify-sops-chain.sh ==="
|
|
482
|
+
bash "$FORGE_HOME/scripts/verify-sops-chain.sh" "$DIFF_TMP" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
|
|
359
483
|
|
|
360
|
-
# Cleanup temp files
|
|
361
|
-
rm -f "$CHANGED_FILES_TMP" "$DIFF_TMP"
|
|
484
|
+
# Cleanup temp files
|
|
485
|
+
rm -f "$CHANGED_FILES_TMP" "$DIFF_TMP"
|
|
486
|
+
else
|
|
487
|
+
echo "=== Phase 2.5B: verify-*.sh skipped — bash not available on this platform ==="
|
|
488
|
+
echo " The verify-*.sh scripts require bash (POSIX shell)."
|
|
489
|
+
echo " Install Git Bash (Windows) or WSL to enable these checks."
|
|
490
|
+
echo " The review continues — integration assumptions should be verified manually."
|
|
491
|
+
fi
|
|
362
492
|
|
|
363
493
|
# --- Inline checks (not yet extracted to scripts) ---
|
|
364
494
|
|
|
365
495
|
# Python scoping hazard check — local imports that shadow module-level names
|
|
366
496
|
# A local `import X` makes X a local variable for the ENTIRE function scope.
|
|
367
497
|
# Any reference to X ABOVE the local import will crash with UnboundLocalError.
|
|
368
|
-
|
|
498
|
+
echo "$CHANGED_FILES" | grep -E '\.py$' | while IFS= read -r f; do
|
|
369
499
|
echo "=== Python Scoping Check: $f ==="
|
|
370
500
|
# Find function-scoped imports (indented import statements)
|
|
371
501
|
grep -nE "^\s+import [a-z]" "$f" 2>/dev/null | while read line; do
|
|
@@ -379,7 +509,7 @@ for f in $(echo "$CHANGED_FILES" | grep -E '\.py$'); do
|
|
|
379
509
|
done
|
|
380
510
|
|
|
381
511
|
# Config file assumption check (baked into Docker image vs volume-mounted)
|
|
382
|
-
|
|
512
|
+
echo "$CHANGED_FILES" | grep -E "config/.*\.(json|yaml|yml)$" | while IFS= read -r f; do
|
|
383
513
|
echo "=== Config File: $f ==="
|
|
384
514
|
grep -n "$(dirname $f)" docker-compose.yml 2>/dev/null || echo "WARNING: Config dir may not be mounted — changes may require --build"
|
|
385
515
|
grep -n "$(dirname $f)" services/*/Dockerfile 2>/dev/null || true
|
|
@@ -396,7 +526,9 @@ done
|
|
|
396
526
|
# diff. Pre-existing drift is the most dangerous kind — it lurks until
|
|
397
527
|
# staging→main and then blocks the deploy.
|
|
398
528
|
WORKFLOW_FILES=$(echo "$CHANGED_FILES" | grep -E "^\.github/workflows/.*\.yml$" || true)
|
|
399
|
-
|
|
529
|
+
# Use POSIX-portable conditional (avoid bash-only [[ ]])
|
|
530
|
+
IS_STAGING_PR="false"
|
|
531
|
+
if [ "$HEAD" = "staging" ] && [ "$BASE" = "main" ]; then IS_STAGING_PR="true"; fi
|
|
400
532
|
|
|
401
533
|
if [ -n "$WORKFLOW_FILES" ] || [ "$IS_STAGING_PR" = "true" ]; then
|
|
402
534
|
echo "=== Sibling Workflow Drift Check (MANDATORY for staging→main) ==="
|
|
@@ -415,9 +547,14 @@ if [ -n "$WORKFLOW_FILES" ] || [ "$IS_STAGING_PR" = "true" ]; then
|
|
|
415
547
|
|
|
416
548
|
echo "--- Comparing '$JOB' job between ci.yml and deploy-production.yml ---"
|
|
417
549
|
|
|
418
|
-
# Extract env vars from ALL steps in the job (not just pytest)
|
|
419
|
-
|
|
420
|
-
|
|
550
|
+
# Extract env vars from ALL steps in the job (not just pytest).
|
|
551
|
+
# Flag-based awk avoids the range-collapse bug: /pat1/,/pat2/ collapses
|
|
552
|
+
# to a single line when the header (e.g. " test-api:") matches both
|
|
553
|
+
# patterns simultaneously. The flag form sets p=1 on the header line,
|
|
554
|
+
# prints body lines while p=1, and clears p when the next sibling job
|
|
555
|
+
# header (same indentation, lowercase start) is seen. <!-- Added: forge#310 -->
|
|
556
|
+
CI_ENVS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$CI_WF" 2>/dev/null | grep -E "PYTHONPATH|DATABASE_URL|REDIS_URL|TESTING" | sed 's/^ *//' | sort)
|
|
557
|
+
DEPLOY_ENVS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$DEPLOY_WF" 2>/dev/null | grep -E "PYTHONPATH|DATABASE_URL|REDIS_URL|TESTING" | sed 's/^ *//' | sort)
|
|
421
558
|
|
|
422
559
|
# Check for PYTHONPATH specifically — the exact var that caused the #11356 failure
|
|
423
560
|
CI_PYPATH=$(echo "$CI_ENVS" | grep "PYTHONPATH" || echo "(not set)")
|
|
@@ -430,16 +567,16 @@ if [ -n "$WORKFLOW_FILES" ] || [ "$IS_STAGING_PR" = "true" ]; then
|
|
|
430
567
|
fi
|
|
431
568
|
|
|
432
569
|
# Check for dependency installation steps that exist in one but not the other
|
|
433
|
-
CI_INSTALLS=$(
|
|
434
|
-
DEPLOY_INSTALLS=$(
|
|
570
|
+
CI_INSTALLS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$CI_WF" 2>/dev/null | grep -c "poetry install\|pip install\|npm install" || echo 0)
|
|
571
|
+
DEPLOY_INSTALLS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$DEPLOY_WF" 2>/dev/null | grep -c "poetry install\|pip install\|npm install" || echo 0)
|
|
435
572
|
if [ "$CI_INSTALLS" != "$DEPLOY_INSTALLS" ]; then
|
|
436
573
|
echo " WARNING: Different number of dependency install steps in '$JOB' — ci.yml has $CI_INSTALLS, deploy has $DEPLOY_INSTALLS"
|
|
437
574
|
echo " ACTION: Read both files and verify all dependencies needed by tests are installed in both workflows."
|
|
438
575
|
fi
|
|
439
576
|
|
|
440
577
|
# Check step names — if CI has a step that deploy doesn't, flag it
|
|
441
|
-
CI_STEPS=$(
|
|
442
|
-
DEPLOY_STEPS=$(
|
|
578
|
+
CI_STEPS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$CI_WF" 2>/dev/null | grep "- name:" | sed 's/.*- name: //' | sort)
|
|
579
|
+
DEPLOY_STEPS=$(awk -v pat="^ ${JOB}:" 'BEGIN{p=0} $0~pat{p=1; print; next} p && /^ [a-z]/{p=0} p{print}' "$DEPLOY_WF" 2>/dev/null | grep "- name:" | sed 's/.*- name: //' | sort)
|
|
443
580
|
MISSING_IN_DEPLOY=$(comm -23 <(echo "$CI_STEPS") <(echo "$DEPLOY_STEPS") 2>/dev/null || true)
|
|
444
581
|
if [ -n "$MISSING_IN_DEPLOY" ]; then
|
|
445
582
|
echo " WARNING: Steps in ci.yml '$JOB' missing from deploy-production.yml:"
|
|
@@ -568,7 +705,10 @@ gh api repos/{owner}/{repo}/issues/$ARGUMENTS/comments --jq '.[-10:] | .[].body[
|
|
|
568
705
|
**Skip if**: Only 1 agent OR total findings ≤ 3.
|
|
569
706
|
|
|
570
707
|
```bash
|
|
571
|
-
|
|
708
|
+
# Extract structured finding IDs from FINDING HTML comments.
|
|
709
|
+
# Uses jq's scan() (POSIX-portable, no grep -oP required).
|
|
710
|
+
ALL_FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
711
|
+
--jq '[.[].body | scan("<!-- FINDING:([^>]+) -->") | .[0]] | join("\n")')
|
|
572
712
|
AGENT_COUNT=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '[.[] | select(.body | test("REVIEW-FINDINGS-START"))] | length')
|
|
573
713
|
FINDING_COUNT=$(echo "$ALL_FINDINGS" | grep -c '.' || echo 0)
|
|
574
714
|
```
|
|
@@ -595,9 +735,13 @@ REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
|
|
|
595
735
|
HAS_SYNTHESIS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '.[].body' | grep -c 'REVIEW-FINDINGS-SYNTHESIZED-START' || echo 0)
|
|
596
736
|
|
|
597
737
|
if [ "$HAS_SYNTHESIS" -gt 0 ]; then
|
|
598
|
-
|
|
738
|
+
# Extract finding IDs from synthesized block using jq scan() — no grep -oP needed
|
|
739
|
+
FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
740
|
+
--jq '[.[] | select(.body | test("REVIEW-FINDINGS-SYNTHESIZED-START")) | .body | scan("<!-- FINDING:([^>]+) -->") | .[0]] | join("\n")')
|
|
599
741
|
else
|
|
600
|
-
|
|
742
|
+
# Extract finding IDs from all agent comments using jq scan() — portable, no grep -oP
|
|
743
|
+
FINDINGS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
744
|
+
--jq '[.[].body | scan("<!-- FINDING:([^>]+) -->") | .[0]] | join("\n")')
|
|
601
745
|
fi
|
|
602
746
|
```
|
|
603
747
|
|
|
@@ -924,7 +1068,8 @@ fi
|
|
|
924
1068
|
|
|
925
1069
|
```bash
|
|
926
1070
|
CURRENT_SHA=$(gh pr view $ARGUMENTS --json headRefOid --jq '.headRefOid')
|
|
927
|
-
REVIEW_IS_STALE
|
|
1071
|
+
REVIEW_IS_STALE="false"
|
|
1072
|
+
if [ "$CURRENT_SHA" != "$REVIEW_SHA" ]; then REVIEW_IS_STALE="true"; fi
|
|
928
1073
|
```
|
|
929
1074
|
|
|
930
1075
|
```bash
|