forgedock 1.0.17 → 1.0.19
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/README.md +34 -1
- package/bin/cross-publish.mjs +344 -0
- package/bin/forgedock.mjs +229 -17
- package/commands/autopilot.md +77 -0
- package/commands/design-bench.md +176 -0
- package/commands/design-render-critique-loop.md +103 -0
- package/commands/design.md +259 -0
- package/commands/issue.md +7 -3
- package/commands/orchestrate.md +121 -0
- package/commands/{resume.md → pipeline-resume.md} +4 -4
- package/commands/{status.md → pipeline-status.md} +1 -1
- package/commands/qa-sweep.md +37 -2
- package/commands/quality-gate.md +57 -0
- package/commands/review-pr-staging.md +158 -2
- package/commands/test-gate.md +929 -0
- package/commands/work-on/build/implement.md +5 -3
- package/commands/work-on.md +53 -3
- package/docs/CI.md +177 -0
- package/docs/FORGE-PROTOCOL.md +324 -0
- package/docs/articles/b-vs-c-baseline.md +210 -0
- package/docs/articles/full-abc-benchmark.md +205 -0
- package/docs/articles/selective-spec-loading-token-savings.md +140 -0
- package/docs/design/abc-benchmark.md +195 -0
- package/docs/design/design-architect-rationale.md +123 -0
- package/docs/design/design-memory.md +61 -0
- package/docs/design/design-spec-schema.md +248 -0
- package/docs/design/design-system-lint.md +150 -0
- package/docs/design/divergent-generation.md +106 -0
- package/docs/design/effects-appropriateness.md +133 -0
- package/docs/design/fixtures/briefs/README.md +47 -0
- package/docs/design/fixtures/briefs/cadence.md +39 -0
- package/docs/design/fixtures/briefs/plume.md +39 -0
- package/docs/design/fixtures/briefs/slipstream.md +39 -0
- package/docs/design/fixtures/briefs/tender.md +39 -0
- package/docs/design/fixtures/briefs/voltage.md +39 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
- package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
- package/docs/design/foundations/brutalist.css +294 -0
- package/docs/design/foundations/editorial.css +300 -0
- package/docs/design/foundations/minimal-luxury.css +295 -0
- package/docs/design/foundations/technical.css +293 -0
- package/docs/design/foundations/warm-photo.css +299 -0
- package/docs/design/reference-corpus.md +1726 -0
- package/docs/design/render-critique-loop.md +57 -0
- package/docs/spec-graph-schema.md +397 -0
- package/package.json +1 -1
- package/scripts/bench-scorecard.mjs +239 -0
- package/scripts/build-spec-graph.mjs +509 -0
- package/scripts/check-native-conflicts.sh +200 -0
- package/scripts/classify-lane.sh +0 -7
- package/scripts/design-system-lint.mjs +993 -0
- package/scripts/graph-query.sh +486 -0
- package/scripts/transition-label.sh +10 -2
- package/scripts/validate-spec-graph.sh +423 -0
- package/templates/workflows/forgedock-review.yml +96 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Deterministic deploy-gate — verify a staging→main bundle's acceptance criteria against running code before deploy
|
|
3
|
+
argument-hint: [--prs "<N1 N2 ...>"] [--base <branch>]
|
|
4
|
+
allowed-tools: Task, Bash, Read, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
7
|
+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
8
|
+
|
|
9
|
+
# /test-gate — Deterministic Deploy-Gate Testing
|
|
10
|
+
|
|
11
|
+
**Input**: `$ARGUMENTS` — optional `--prs "<space-separated PR numbers>"` and `--base <branch>`.
|
|
12
|
+
|
|
13
|
+
Verifies a staging→main bundle's acceptance criteria against running code before deploy. Called by `/review-pr-staging` (Phase 6.5) with the bundle PRs, or run standalone when `--prs` is absent (computes the bundle itself). Returns a machine-readable BLOCK / PASS / SKIP verdict for the caller to consume.
|
|
14
|
+
|
|
15
|
+
**Agent model policy**: Default `model: "sonnet"`. If Sonnet is rate-limited, fall back to `model: "opus"`.
|
|
16
|
+
|
|
17
|
+
**Posture**: Blocking-with-override at the staging→main boundary (mirrors the Phase 0A open-finding gate in `/review-pr-staging`). Posture is config-driven via `verification.test_gate`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Config Resolution
|
|
22
|
+
|
|
23
|
+
Read `forge.yaml` **before** running any commands. All variable references below are populated from this block.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
27
|
+
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
|
|
28
|
+
GH_FLAG="-R $GH_REPO"
|
|
29
|
+
REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
|
|
30
|
+
DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
|
|
31
|
+
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
|
|
32
|
+
|
|
33
|
+
# Test-gate config (all fields optional — defaults shown)
|
|
34
|
+
INTEGRATION_TESTS=$(yq '.verification.integration_tests // []' "$CONFIG_FILE")
|
|
35
|
+
TEST_SERVICES=$(yq '.verification.test_services // {}' "$CONFIG_FILE")
|
|
36
|
+
GATE_POSTURE=$(yq '.verification.test_gate.posture // "blocking"' "$CONFIG_FILE")
|
|
37
|
+
OVERRIDE_PHRASE=$(yq '.verification.test_gate.override_phrase // "OVERRIDE: shipping with test failures —"' "$CONFIG_FILE")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Input Parsing
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
BUNDLE_PRS=""
|
|
46
|
+
BASE_BRANCH="${DEFAULT_BRANCH}"
|
|
47
|
+
|
|
48
|
+
# Parse --prs and --base from $ARGUMENTS
|
|
49
|
+
while [[ "$@" ]]; do
|
|
50
|
+
case "$1" in
|
|
51
|
+
--prs) shift; BUNDLE_PRS="$1";;
|
|
52
|
+
--base) shift; BASE_BRANCH="$1";;
|
|
53
|
+
esac
|
|
54
|
+
shift
|
|
55
|
+
done
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If `--prs` is absent, Phase 0 computes the bundle.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Missing-Config Guard (MANDATORY — runs before all phases)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# If integration_tests is empty/absent, exit ADVISORY — never crash
|
|
66
|
+
TEST_COUNT=$(yq '.verification.integration_tests | length' "$CONFIG_FILE" 2>/dev/null || echo 0)
|
|
67
|
+
if [ "${TEST_COUNT:-0}" -eq 0 ]; then
|
|
68
|
+
echo "ADVISORY: verification.integration_tests is not configured in $CONFIG_FILE."
|
|
69
|
+
echo "No tests to run. Emitting SKIP verdict."
|
|
70
|
+
echo ""
|
|
71
|
+
echo "To enable /test-gate, add to forge.yaml:"
|
|
72
|
+
echo " verification:"
|
|
73
|
+
echo " integration_tests:"
|
|
74
|
+
echo " - cluster: \"api\""
|
|
75
|
+
echo " command: \"pytest tests/integration/ -q --tb=short\""
|
|
76
|
+
echo " working_dir: \".\""
|
|
77
|
+
echo " test_services:"
|
|
78
|
+
echo " api: \"your-api-container-name\""
|
|
79
|
+
echo " test_gate:"
|
|
80
|
+
echo " posture: \"blocking\""
|
|
81
|
+
echo " override_phrase: \"OVERRIDE: shipping with test failures —\""
|
|
82
|
+
# Emit structured SKIP verdict and exit
|
|
83
|
+
echo "<!-- FORGE:TEST_GATE:SKIP|reason=no-tests-configured -->"
|
|
84
|
+
echo "<!-- FORGE:TEST_GATE:RESULT=SKIP -->"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Phase 0: Triage — Test-or-Skip
|
|
92
|
+
|
|
93
|
+
**Purpose**: Decide test-or-skip before any provisioning. Three ordered checks:
|
|
94
|
+
1. **0A** — no PRs in bundle → SKIP
|
|
95
|
+
2. **0B** — no executable file changes → SKIP (docs/config/markdown-only)
|
|
96
|
+
3. **0C** — no runtime-testable acceptance criteria → SKIP (all-manual or no criteria)
|
|
97
|
+
|
|
98
|
+
Every SKIP emits both a machine-readable verdict (`FORGE:TEST_GATE:RESULT=SKIP`) and a logged reason annotation (`FORGE:TEST_GATE:SKIP|reason=...`) so the caller and pipeline-health see a deliberate skip, not a silent gap.
|
|
99
|
+
|
|
100
|
+
### 0A: Resolve bundle PRs
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git fetch origin ${DEFAULT_BRANCH} ${STAGING_BRANCH} 2>/dev/null
|
|
104
|
+
|
|
105
|
+
if [ -n "$BUNDLE_PRS" ]; then
|
|
106
|
+
# Caller passed PR numbers explicitly (standard case when invoked by review-pr-staging)
|
|
107
|
+
echo "Bundle PRs (from --prs): $BUNDLE_PRS"
|
|
108
|
+
else
|
|
109
|
+
# Standalone invocation: compute bundle from staging→main diff
|
|
110
|
+
BUNDLE_PRS=$(git log origin/${DEFAULT_BRANCH}..origin/${STAGING_BRANCH} --merges --oneline \
|
|
111
|
+
| grep -oP '(?<=pull request #)\d+' \
|
|
112
|
+
| sort -u \
|
|
113
|
+
| tr '\n' ' ')
|
|
114
|
+
echo "Bundle PRs (computed from ${STAGING_BRANCH}→${DEFAULT_BRANCH}): $BUNDLE_PRS"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
if [ -z "$BUNDLE_PRS" ]; then
|
|
118
|
+
echo "No PRs found in bundle. Emitting SKIP verdict."
|
|
119
|
+
echo "<!-- FORGE:TEST_GATE:SKIP|reason=no-bundle-prs -->"
|
|
120
|
+
echo "<!-- FORGE:TEST_GATE:RESULT=SKIP -->"
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 0B: Detect executable changes
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
BUNDLE_DIFF=$(git diff origin/${BASE_BRANCH}...origin/${STAGING_BRANCH} --name-only 2>/dev/null)
|
|
129
|
+
|
|
130
|
+
EXECUTABLE_FILES=$(echo "$BUNDLE_DIFF" | grep -vE '\.(md|txt|yaml|yml|json|toml|lock|ini|cfg|env\.example|gitignore)$' | grep -v '^docs/' | grep -v '^\.github/' || true)
|
|
131
|
+
|
|
132
|
+
if [ -z "$EXECUTABLE_FILES" ]; then
|
|
133
|
+
echo "Bundle contains only documentation/config changes. No runtime testing required."
|
|
134
|
+
echo "Files changed:"
|
|
135
|
+
echo "$BUNDLE_DIFF"
|
|
136
|
+
echo "RESULT: SKIP (no executable changes in bundle)"
|
|
137
|
+
echo "<!-- FORGE:TEST_GATE:SKIP|reason=no-executable-changes -->"
|
|
138
|
+
echo "<!-- FORGE:TEST_GATE:RESULT=SKIP -->"
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
echo "Executable files changed in bundle:"
|
|
143
|
+
echo "$EXECUTABLE_FILES"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 0C: Criteria pre-check — test-or-skip before provisioning
|
|
147
|
+
|
|
148
|
+
Check whether any solved issue in the bundle carries at least one runtime-testable acceptance criterion (`[type:api]`, `[type:unit]`, or `[type:e2e]`, or an unannotated criterion that does not match `[type:manual]`). Skip provisioning entirely if all criteria are manual-only or no criteria exist.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
echo "=== Phase 0C: Criteria pre-check ==="
|
|
152
|
+
|
|
153
|
+
TRIAGE_HAS_TESTABLE_CRITERIA=false
|
|
154
|
+
TRIAGE_ALL_ISSUES=""
|
|
155
|
+
TRIAGE_CRITERIA_COUNT=0
|
|
156
|
+
TRIAGE_MANUAL_COUNT=0
|
|
157
|
+
|
|
158
|
+
for pr_num in $BUNDLE_PRS; do
|
|
159
|
+
# Extract closing issue references from PR body and GitHub API
|
|
160
|
+
PR_BODY=$(gh pr view "$pr_num" ${GH_FLAG} --json body --jq '.body' 2>/dev/null || echo "")
|
|
161
|
+
CLOSED_ISSUES=$(echo "$PR_BODY" | grep -iP '(closes|fixes|resolves)\s+#\d+' | grep -oP '#\d+' | tr -d '#' || true)
|
|
162
|
+
LINKED_ISSUES=$(gh pr view "$pr_num" ${GH_FLAG} --json closingIssuesReferences \
|
|
163
|
+
--jq '.closingIssuesReferences[].number' 2>/dev/null || true)
|
|
164
|
+
ALL_ISSUES_FOR_PR=$(echo "${CLOSED_ISSUES} ${LINKED_ISSUES}" | tr ' ' '\n' | sort -u | grep -E '^[0-9]+$' || true)
|
|
165
|
+
TRIAGE_ALL_ISSUES="${TRIAGE_ALL_ISSUES} ${ALL_ISSUES_FOR_PR}"
|
|
166
|
+
|
|
167
|
+
for issue_num in $ALL_ISSUES_FOR_PR; do
|
|
168
|
+
CRITERIA=$(gh issue view "$issue_num" ${GH_FLAG} --json body \
|
|
169
|
+
--jq '.body' 2>/dev/null \
|
|
170
|
+
| awk '/^## Acceptance Criteria/{found=1; next} /^## /{if(found) exit} found{print}' \
|
|
171
|
+
| grep -E '^- \[' || true)
|
|
172
|
+
|
|
173
|
+
if [ -z "$CRITERIA" ]; then
|
|
174
|
+
continue
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# Check for any non-manual criterion — exit early as soon as one is found
|
|
178
|
+
while IFS= read -r criterion; do
|
|
179
|
+
[ -z "$criterion" ] && continue
|
|
180
|
+
TRIAGE_CRITERIA_COUNT=$((TRIAGE_CRITERIA_COUNT + 1))
|
|
181
|
+
if echo "$criterion" | grep -qP '\[type:(api|unit|e2e)\]'; then
|
|
182
|
+
# Explicit automated annotation found
|
|
183
|
+
TRIAGE_HAS_TESTABLE_CRITERIA=true
|
|
184
|
+
echo " Testable criterion found in issue #${issue_num}: ${criterion}"
|
|
185
|
+
break 3 # Break out of criterion loop, issue loop, and PR loop
|
|
186
|
+
elif echo "$criterion" | grep -qP '\[type:manual\]'; then
|
|
187
|
+
TRIAGE_MANUAL_COUNT=$((TRIAGE_MANUAL_COUNT + 1))
|
|
188
|
+
else
|
|
189
|
+
# Unannotated criterion — apply same regex heuristics as Phase 2
|
|
190
|
+
if echo "$criterion" | grep -qP '(endpoint|request|response|status\s+\d{3}|curl|API|HTTP|unit|function|return|assert|throws|browser|click|navigate|render|page|user flow)'; then
|
|
191
|
+
TRIAGE_HAS_TESTABLE_CRITERIA=true
|
|
192
|
+
echo " Testable criterion found (inferred) in issue #${issue_num}: ${criterion}"
|
|
193
|
+
break 3 # Break out of all loops
|
|
194
|
+
else
|
|
195
|
+
# No type signal — treat as manual for triage purposes
|
|
196
|
+
TRIAGE_MANUAL_COUNT=$((TRIAGE_MANUAL_COUNT + 1))
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
done <<< "$CRITERIA"
|
|
200
|
+
done
|
|
201
|
+
done
|
|
202
|
+
|
|
203
|
+
TRIAGE_ISSUE_COUNT=$(echo "$TRIAGE_ALL_ISSUES" | tr ' ' '\n' | sort -u | grep -cE '^[0-9]+$' || echo 0)
|
|
204
|
+
echo "Triage summary: ${TRIAGE_ISSUE_COUNT} solved issue(s), ${TRIAGE_CRITERIA_COUNT} criterion/criteria found, ${TRIAGE_MANUAL_COUNT} manual"
|
|
205
|
+
|
|
206
|
+
if [ "$TRIAGE_HAS_TESTABLE_CRITERIA" = "false" ]; then
|
|
207
|
+
if [ "${TRIAGE_CRITERIA_COUNT}" -eq 0 ]; then
|
|
208
|
+
SKIP_REASON="no-acceptance-criteria"
|
|
209
|
+
echo "No acceptance criteria found in any solved issue. No automated tests to run."
|
|
210
|
+
else
|
|
211
|
+
SKIP_REASON="no-testable-criteria"
|
|
212
|
+
echo "All ${TRIAGE_CRITERIA_COUNT} acceptance criteria are manual-only. No automated tests to run."
|
|
213
|
+
fi
|
|
214
|
+
echo "Emitting SKIP verdict (triage: ${SKIP_REASON})."
|
|
215
|
+
echo "<!-- FORGE:TEST_GATE:SKIP|reason=${SKIP_REASON} -->"
|
|
216
|
+
echo "<!-- FORGE:TEST_GATE:RESULT=SKIP -->"
|
|
217
|
+
exit 0
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
echo "Runtime-testable criteria confirmed. Proceeding with provisioning."
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Phase 1: Collate — Bundle PRs → Solved Issues → Acceptance Criteria
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
echo "=== Phase 1: Collating acceptance criteria from bundle ==="
|
|
229
|
+
|
|
230
|
+
COLLATED_CRITERIA=""
|
|
231
|
+
SOLVED_ISSUES=""
|
|
232
|
+
|
|
233
|
+
for pr_num in $BUNDLE_PRS; do
|
|
234
|
+
# Get PR body to extract closing references
|
|
235
|
+
PR_BODY=$(gh pr view "$pr_num" ${GH_FLAG} --json body --jq '.body' 2>/dev/null || echo "")
|
|
236
|
+
|
|
237
|
+
# Extract issue numbers from Closes/Fixes references (case-insensitive)
|
|
238
|
+
CLOSED_ISSUES=$(echo "$PR_BODY" | grep -iP '(closes|fixes|resolves)\s+#\d+' | grep -oP '#\d+' | tr -d '#' || true)
|
|
239
|
+
|
|
240
|
+
# Also extract from linked issues via GitHub API
|
|
241
|
+
LINKED_ISSUES=$(gh pr view "$pr_num" ${GH_FLAG} --json closingIssuesReferences \
|
|
242
|
+
--jq '.closingIssuesReferences[].number' 2>/dev/null || true)
|
|
243
|
+
|
|
244
|
+
ALL_ISSUES=$(echo "${CLOSED_ISSUES} ${LINKED_ISSUES}" | tr ' ' '\n' | sort -u | grep -E '^[0-9]+$' || true)
|
|
245
|
+
|
|
246
|
+
for issue_num in $ALL_ISSUES; do
|
|
247
|
+
SOLVED_ISSUES="${SOLVED_ISSUES} ${issue_num}"
|
|
248
|
+
|
|
249
|
+
# Extract Acceptance Criteria section from issue body
|
|
250
|
+
CRITERIA=$(gh issue view "$issue_num" ${GH_FLAG} --json body \
|
|
251
|
+
--jq '.body' 2>/dev/null \
|
|
252
|
+
| awk '/^## Acceptance Criteria/{found=1; next} /^## /{if(found) exit} found{print}' \
|
|
253
|
+
| grep -E '^- \[' || true)
|
|
254
|
+
|
|
255
|
+
if [ -n "$CRITERIA" ]; then
|
|
256
|
+
COLLATED_CRITERIA="${COLLATED_CRITERIA}
|
|
257
|
+
### Issue #${issue_num} (PR #${pr_num})
|
|
258
|
+
${CRITERIA}"
|
|
259
|
+
fi
|
|
260
|
+
done
|
|
261
|
+
done
|
|
262
|
+
|
|
263
|
+
echo "Solved issues: $(echo $SOLVED_ISSUES | tr ' ' '\n' | sort -u | tr '\n' ' ')"
|
|
264
|
+
echo ""
|
|
265
|
+
echo "Collated Acceptance Criteria:"
|
|
266
|
+
echo "$COLLATED_CRITERIA"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Phase 2: Classify — Bucket Criteria by Test Type
|
|
272
|
+
|
|
273
|
+
Bucket each acceptance criterion by `[type:api|unit|e2e|manual]` annotation. Regex fallback when unannotated.
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
echo "=== Phase 2: Classifying criteria by test type ==="
|
|
277
|
+
|
|
278
|
+
API_CRITERIA=""
|
|
279
|
+
UNIT_CRITERIA=""
|
|
280
|
+
E2E_CRITERIA=""
|
|
281
|
+
MANUAL_CRITERIA=""
|
|
282
|
+
UNCLASSIFIED=""
|
|
283
|
+
|
|
284
|
+
while IFS= read -r line; do
|
|
285
|
+
if echo "$line" | grep -qP '\[type:api\]'; then
|
|
286
|
+
API_CRITERIA="${API_CRITERIA}\n${line}"
|
|
287
|
+
elif echo "$line" | grep -qP '\[type:unit\]'; then
|
|
288
|
+
UNIT_CRITERIA="${UNIT_CRITERIA}\n${line}"
|
|
289
|
+
elif echo "$line" | grep -qP '\[type:e2e\]'; then
|
|
290
|
+
E2E_CRITERIA="${E2E_CRITERIA}\n${line}"
|
|
291
|
+
elif echo "$line" | grep -qP '\[type:manual\]'; then
|
|
292
|
+
MANUAL_CRITERIA="${MANUAL_CRITERIA}\n${line}"
|
|
293
|
+
elif echo "$line" | grep -qP '(endpoint|request|response|status\s+\d{3}|curl|API|HTTP)'; then
|
|
294
|
+
API_CRITERIA="${API_CRITERIA}\n${line} [inferred:api]"
|
|
295
|
+
elif echo "$line" | grep -qP '(unit|function|return|assert|throws)'; then
|
|
296
|
+
UNIT_CRITERIA="${UNIT_CRITERIA}\n${line} [inferred:unit]"
|
|
297
|
+
elif echo "$line" | grep -qP '(browser|click|navigate|render|page|user flow)'; then
|
|
298
|
+
E2E_CRITERIA="${E2E_CRITERIA}\n${line} [inferred:e2e]"
|
|
299
|
+
elif echo "$line" | grep -qP '^-\s+\['; then
|
|
300
|
+
# Has content but no type signal — treat as manual
|
|
301
|
+
MANUAL_CRITERIA="${MANUAL_CRITERIA}\n${line} [inferred:manual]"
|
|
302
|
+
fi
|
|
303
|
+
done <<< "$(echo -e "$COLLATED_CRITERIA")"
|
|
304
|
+
|
|
305
|
+
echo "API criteria: $(echo -e "$API_CRITERIA" | grep -c '^\-' || echo 0)"
|
|
306
|
+
echo "Unit criteria: $(echo -e "$UNIT_CRITERIA" | grep -c '^\-' || echo 0)"
|
|
307
|
+
echo "E2E criteria: $(echo -e "$E2E_CRITERIA" | grep -c '^\-' || echo 0)"
|
|
308
|
+
echo "Manual criteria: $(echo -e "$MANUAL_CRITERIA" | grep -c '^\-' || echo 0)"
|
|
309
|
+
|
|
310
|
+
AUTOMATED_CRITERIA="${API_CRITERIA}${UNIT_CRITERIA}${E2E_CRITERIA}"
|
|
311
|
+
if [ -z "$(echo -e "$AUTOMATED_CRITERIA" | grep -E '^-')" ]; then
|
|
312
|
+
echo "All criteria are manual — no automated test clusters to run."
|
|
313
|
+
echo "Emitting SKIP verdict (manual-only bundle — defense-in-depth check after Phase 0C)."
|
|
314
|
+
echo "<!-- FORGE:TEST_GATE:SKIP|reason=manual-only-criteria -->"
|
|
315
|
+
echo "<!-- FORGE:TEST_GATE:RESULT=SKIP -->"
|
|
316
|
+
exit 0
|
|
317
|
+
fi
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Phase 3: Provision — Bring Up Test Services
|
|
323
|
+
|
|
324
|
+
For each cluster in `verification.integration_tests`, verify the mapped container is running. CLI-only projects (no `test_services` mapping) skip this phase.
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
echo "=== Phase 3: Provisioning test services ==="
|
|
328
|
+
|
|
329
|
+
PROVISION_FAILURES=""
|
|
330
|
+
CLUSTERS=$(yq '.verification.integration_tests[].cluster' "$CONFIG_FILE" 2>/dev/null | sort -u)
|
|
331
|
+
|
|
332
|
+
for cluster in $CLUSTERS; do
|
|
333
|
+
# Look up container name for this cluster
|
|
334
|
+
CONTAINER=$(yq ".verification.test_services.${cluster} // \"\"" "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
335
|
+
|
|
336
|
+
if [ -z "$CONTAINER" ] || [ "$CONTAINER" = "null" ]; then
|
|
337
|
+
echo "ADVISORY: No container mapped for cluster '${cluster}' in verification.test_services."
|
|
338
|
+
echo " Running in CLI-only mode — skipping container health check for '${cluster}'."
|
|
339
|
+
continue
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Check if container is running
|
|
343
|
+
CONTAINER_STATE=$(docker inspect --format '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo "false")
|
|
344
|
+
if [ "$CONTAINER_STATE" != "true" ]; then
|
|
345
|
+
echo "WARNING: Container '${CONTAINER}' (cluster: ${cluster}) is not running."
|
|
346
|
+
PROVISION_FAILURES="${PROVISION_FAILURES}\n- Cluster '${cluster}': container '${CONTAINER}' not running"
|
|
347
|
+
else
|
|
348
|
+
echo "OK: Container '${CONTAINER}' (cluster: ${cluster}) is healthy."
|
|
349
|
+
fi
|
|
350
|
+
done
|
|
351
|
+
|
|
352
|
+
if [ -n "$PROVISION_FAILURES" ]; then
|
|
353
|
+
echo ""
|
|
354
|
+
echo "Provision warnings (advisory — continuing with available clusters):"
|
|
355
|
+
echo -e "$PROVISION_FAILURES"
|
|
356
|
+
fi
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Phase 4: Fan Out — Execute Test Clusters
|
|
362
|
+
|
|
363
|
+
Spawn a narrow-band test subagent per cluster. Each executes its configured command and captures pass/fail output. Flaky-retry: up to 2 retries per cluster on non-deterministic failures.
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
echo "=== Phase 4: Fanning out test clusters ==="
|
|
367
|
+
|
|
368
|
+
CLUSTER_RESULTS=""
|
|
369
|
+
CLUSTER_COUNT=$(yq '.verification.integration_tests | length' "$CONFIG_FILE" 2>/dev/null || echo 0)
|
|
370
|
+
|
|
371
|
+
for i in $(seq 0 $((CLUSTER_COUNT - 1))); do
|
|
372
|
+
CLUSTER=$(yq ".verification.integration_tests[${i}].cluster" "$CONFIG_FILE")
|
|
373
|
+
CMD=$(yq ".verification.integration_tests[${i}].command" "$CONFIG_FILE")
|
|
374
|
+
WORKING_DIR=$(yq ".verification.integration_tests[${i}].working_dir // \".\"" "$CONFIG_FILE")
|
|
375
|
+
|
|
376
|
+
echo "Running cluster '${CLUSTER}': ${CMD}"
|
|
377
|
+
|
|
378
|
+
RETRY=0
|
|
379
|
+
MAX_RETRIES=2
|
|
380
|
+
CLUSTER_EXIT=1
|
|
381
|
+
|
|
382
|
+
while [ $RETRY -le $MAX_RETRIES ]; do
|
|
383
|
+
if (cd "${REPO_PATH}/${WORKING_DIR}" && eval "$CMD") 2>&1; then
|
|
384
|
+
CLUSTER_EXIT=0
|
|
385
|
+
break
|
|
386
|
+
else
|
|
387
|
+
CLUSTER_EXIT=$?
|
|
388
|
+
RETRY=$((RETRY + 1))
|
|
389
|
+
if [ $RETRY -le $MAX_RETRIES ]; then
|
|
390
|
+
echo " Cluster '${CLUSTER}' failed (exit ${CLUSTER_EXIT}) — retry ${RETRY}/${MAX_RETRIES}..."
|
|
391
|
+
fi
|
|
392
|
+
fi
|
|
393
|
+
done
|
|
394
|
+
|
|
395
|
+
if [ "$CLUSTER_EXIT" -eq 0 ]; then
|
|
396
|
+
CLUSTER_RESULTS="${CLUSTER_RESULTS}\n- ${CLUSTER}: PASS"
|
|
397
|
+
echo " PASS: cluster '${CLUSTER}'"
|
|
398
|
+
else
|
|
399
|
+
CLUSTER_RESULTS="${CLUSTER_RESULTS}\n- ${CLUSTER}: FAIL (after ${MAX_RETRIES} retries)"
|
|
400
|
+
echo " FAIL: cluster '${CLUSTER}'"
|
|
401
|
+
fi
|
|
402
|
+
done
|
|
403
|
+
|
|
404
|
+
echo ""
|
|
405
|
+
echo "Cluster results:"
|
|
406
|
+
echo -e "$CLUSTER_RESULTS"
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Phase 5: Baseline Comparison + Test-Failure Issue Creation
|
|
412
|
+
|
|
413
|
+
Capture a pre-batch baseline from the base branch (or previous run) to distinguish batch-introduced failures from pre-existing ones. File `test-failure` issues for batch-introduced failures only.
|
|
414
|
+
|
|
415
|
+
### 5A: Capture baseline
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
echo "=== Phase 5A: Capturing baseline from base branch (${BASE_BRANCH}) ==="
|
|
419
|
+
|
|
420
|
+
BASELINE_RESULTS=""
|
|
421
|
+
|
|
422
|
+
# Run the same tests against the base branch state to establish baseline
|
|
423
|
+
# If base-branch tests also fail, those are pre-existing failures — not batch-introduced
|
|
424
|
+
git stash 2>/dev/null || true
|
|
425
|
+
git checkout "origin/${BASE_BRANCH}" -- . 2>/dev/null || true
|
|
426
|
+
|
|
427
|
+
for i in $(seq 0 $((CLUSTER_COUNT - 1))); do
|
|
428
|
+
CLUSTER=$(yq ".verification.integration_tests[${i}].cluster" "$CONFIG_FILE")
|
|
429
|
+
CMD=$(yq ".verification.integration_tests[${i}].command" "$CONFIG_FILE")
|
|
430
|
+
WORKING_DIR=$(yq ".verification.integration_tests[${i}].working_dir // \".\"" "$CONFIG_FILE")
|
|
431
|
+
|
|
432
|
+
if (cd "${REPO_PATH}/${WORKING_DIR}" && eval "$CMD") 2>&1; then
|
|
433
|
+
BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: PASS"
|
|
434
|
+
else
|
|
435
|
+
BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: FAIL (pre-existing)"
|
|
436
|
+
fi
|
|
437
|
+
done
|
|
438
|
+
|
|
439
|
+
# Restore working state
|
|
440
|
+
git checkout HEAD -- . 2>/dev/null || true
|
|
441
|
+
|
|
442
|
+
echo "Baseline results:"
|
|
443
|
+
echo -e "$BASELINE_RESULTS"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### 5B: Identify batch-introduced failures
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
echo "=== Phase 5B: Identifying batch-introduced failures ==="
|
|
450
|
+
|
|
451
|
+
BATCH_FAILURES=""
|
|
452
|
+
|
|
453
|
+
while IFS= read -r result_line; do
|
|
454
|
+
CLUSTER=$(echo "$result_line" | grep -oP '^- \K[^:]+')
|
|
455
|
+
STATUS=$(echo "$result_line" | grep -oP ':\s+\K.+')
|
|
456
|
+
|
|
457
|
+
if echo "$STATUS" | grep -q "^FAIL"; then
|
|
458
|
+
# Check if this cluster also failed in baseline
|
|
459
|
+
BASELINE_STATUS=$(echo -e "$BASELINE_RESULTS" | grep "^- ${CLUSTER}:" | grep -oP ':\s+\K.+' || echo "PASS")
|
|
460
|
+
if echo "$BASELINE_STATUS" | grep -q "^PASS"; then
|
|
461
|
+
# This cluster passed in baseline but fails in the bundle — batch-introduced failure
|
|
462
|
+
BATCH_FAILURES="${BATCH_FAILURES}\n${CLUSTER}"
|
|
463
|
+
echo "BATCH-INTRODUCED FAILURE: cluster '${CLUSTER}' (baseline: PASS → bundle: FAIL)"
|
|
464
|
+
else
|
|
465
|
+
echo "PRE-EXISTING FAILURE: cluster '${CLUSTER}' (baseline: FAIL → bundle: FAIL — not this batch)"
|
|
466
|
+
fi
|
|
467
|
+
fi
|
|
468
|
+
done <<< "$(echo -e "$CLUSTER_RESULTS")"
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### 5C: File test-failure issues for batch-introduced failures
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
echo "=== Phase 5C: Filing test-failure issues ==="
|
|
475
|
+
|
|
476
|
+
if [ -z "$(echo -e "$BATCH_FAILURES" | grep -E '\S')" ]; then
|
|
477
|
+
echo "No batch-introduced failures — skipping issue creation."
|
|
478
|
+
else
|
|
479
|
+
# Ensure test-failure label exists
|
|
480
|
+
gh label create "test-failure" --color "B60205" \
|
|
481
|
+
--description "Runtime test failure introduced by a bundle of PRs. Managed by ForgeDock." \
|
|
482
|
+
--force ${GH_FLAG} 2>/dev/null || true
|
|
483
|
+
|
|
484
|
+
while IFS= read -r cluster; do
|
|
485
|
+
[ -z "$cluster" ] && continue
|
|
486
|
+
|
|
487
|
+
# Duplicate prevention: check for open test-failure issues for this cluster
|
|
488
|
+
EXISTING=$(gh issue list ${GH_FLAG} \
|
|
489
|
+
--label "test-failure" \
|
|
490
|
+
--state open \
|
|
491
|
+
--search "test-gate cluster ${cluster}" \
|
|
492
|
+
--limit 5 \
|
|
493
|
+
--json number,title \
|
|
494
|
+
--jq '.[0].number // empty' 2>/dev/null || true)
|
|
495
|
+
|
|
496
|
+
if [ -n "$EXISTING" ]; then
|
|
497
|
+
echo "Skipping issue creation — open test-failure issue already exists: #${EXISTING} (cluster: ${cluster})"
|
|
498
|
+
continue
|
|
499
|
+
fi
|
|
500
|
+
|
|
501
|
+
# Identify source issues for this cluster's PRs
|
|
502
|
+
SOURCE_ISSUES_LIST=""
|
|
503
|
+
for issue_num in $(echo "$SOLVED_ISSUES" | tr ' ' '\n' | sort -u | grep -E '^[0-9]+$'); do
|
|
504
|
+
SOURCE_ISSUES_LIST="${SOURCE_ISSUES_LIST}\n- #${issue_num}"
|
|
505
|
+
done
|
|
506
|
+
|
|
507
|
+
ISSUE_NUM=$(gh issue create ${GH_FLAG} \
|
|
508
|
+
--title "fix: test-gate FAIL — cluster '${cluster}' broken by staging→${DEFAULT_BRANCH} bundle" \
|
|
509
|
+
--label "test-failure,priority:P1" \
|
|
510
|
+
--body "$(cat <<ISSUE_EOF
|
|
511
|
+
## Problem
|
|
512
|
+
|
|
513
|
+
The \`/test-gate\` command detected a runtime failure in cluster \`${cluster}\` that was introduced by the staging→${DEFAULT_BRANCH} bundle. The failure did not exist on \`${BASE_BRANCH}\` (baseline PASS), confirming it is batch-introduced, not pre-existing.
|
|
514
|
+
|
|
515
|
+
## Root Cause (if known)
|
|
516
|
+
|
|
517
|
+
Root cause unknown — investigation needed. The test command for cluster \`${cluster}\` failed after 2 retries.
|
|
518
|
+
|
|
519
|
+
## Affected Files
|
|
520
|
+
|
|
521
|
+
Files to be identified during investigation. Start with changes in the bundle PRs: $(echo $BUNDLE_PRS | tr ' ' ', ' | sed 's/,\s*$//').
|
|
522
|
+
|
|
523
|
+
## Acceptance Criteria
|
|
524
|
+
|
|
525
|
+
- [ ] Root cause of cluster \`${cluster}\` failure identified
|
|
526
|
+
- [ ] Fix implemented and cluster passes in isolation
|
|
527
|
+
- [ ] Baseline comparison confirms no regression in \`${BASE_BRANCH}\`
|
|
528
|
+
- [ ] /test-gate emits PASS verdict for this bundle after fix
|
|
529
|
+
|
|
530
|
+
## Context
|
|
531
|
+
|
|
532
|
+
**Detected by**: \`/test-gate\` — Phase 5 baseline comparison
|
|
533
|
+
**Bundle PRs**: ${BUNDLE_PRS}
|
|
534
|
+
**Base branch**: \`${BASE_BRANCH}\`
|
|
535
|
+
**Cluster**: \`${cluster}\`
|
|
536
|
+
**Baseline result**: PASS (cluster was healthy on \`${BASE_BRANCH}\`)
|
|
537
|
+
**Bundle result**: FAIL (cluster broken after applying bundle)
|
|
538
|
+
|
|
539
|
+
## Source Issues
|
|
540
|
+
|
|
541
|
+
The following issues were solved by this bundle:
|
|
542
|
+
$(echo -e "$SOURCE_ISSUES_LIST")
|
|
543
|
+
|
|
544
|
+
## Reproduction Steps
|
|
545
|
+
|
|
546
|
+
1. Check out \`origin/${STAGING_BRANCH}\`
|
|
547
|
+
2. Run the cluster test command from \`forge.yaml\`:
|
|
548
|
+
\`\`\`bash
|
|
549
|
+
$(yq ".verification.integration_tests[] | select(.cluster == \"${cluster}\") | .command" "$CONFIG_FILE")
|
|
550
|
+
\`\`\`
|
|
551
|
+
3. Observe failure. Compare against \`${BASE_BRANCH}\` baseline:
|
|
552
|
+
\`\`\`bash
|
|
553
|
+
git checkout origin/${BASE_BRANCH} -- .
|
|
554
|
+
$(yq ".verification.integration_tests[] | select(.cluster == \"${cluster}\") | .command" "$CONFIG_FILE")
|
|
555
|
+
\`\`\`
|
|
556
|
+
ISSUE_EOF
|
|
557
|
+
)" --json number --jq '.number' 2>/dev/null || echo "FAILED")
|
|
558
|
+
|
|
559
|
+
if [ "$ISSUE_NUM" != "FAILED" ] && [ -n "$ISSUE_NUM" ]; then
|
|
560
|
+
echo "Filed test-failure issue #${ISSUE_NUM} for cluster '${cluster}'"
|
|
561
|
+
else
|
|
562
|
+
echo "WARNING: Failed to create test-failure issue for cluster '${cluster}'"
|
|
563
|
+
fi
|
|
564
|
+
done <<< "$(echo -e "$BATCH_FAILURES")"
|
|
565
|
+
fi
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Phase 6: Criteria-Adequacy Check
|
|
571
|
+
|
|
572
|
+
Reconcile the executed test plan against each solved issue's acceptance criteria. For each solved issue, map individual acceptance criteria to the clusters that ran. Flag uncovered criteria (runtime-testable, no cluster maps to them) and untestable-as-written criteria (vague oracle, no deterministic verification) as distinct finding classes. Coverage gaps contribute to the BLOCK verdict.
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
echo "=== Phase 6: Criteria-adequacy check ==="
|
|
576
|
+
|
|
577
|
+
# Initialize adequacy state — all variables must be set before Phase 7 reads them
|
|
578
|
+
COVERAGE_BLOCK=false
|
|
579
|
+
UNCOVERED_BLOCK_COUNT=0
|
|
580
|
+
ADEQUACY_COVERED_LIST=""
|
|
581
|
+
ADEQUACY_UNCOVERED_LIST=""
|
|
582
|
+
ADEQUACY_UNTESTABLE_LIST=""
|
|
583
|
+
ADEQUACY_MANUAL_LIST=""
|
|
584
|
+
|
|
585
|
+
# Build a flat list of cluster names that passed, for mapping lookup
|
|
586
|
+
PASSING_CLUSTERS=$(echo -e "$CLUSTER_RESULTS" | grep ": PASS" | grep -oP '^- \K[^:]+' | tr '\n' ' ' || true)
|
|
587
|
+
ALL_CLUSTERS=$(yq '.verification.integration_tests[].cluster' "$CONFIG_FILE" 2>/dev/null | tr '\n' ' ' || true)
|
|
588
|
+
|
|
589
|
+
echo "Passing clusters: ${PASSING_CLUSTERS:-none}"
|
|
590
|
+
echo "All configured clusters: ${ALL_CLUSTERS:-none}"
|
|
591
|
+
echo ""
|
|
592
|
+
|
|
593
|
+
# Helper: classify a single criterion line into one of four buckets:
|
|
594
|
+
# covered, uncovered, untestable, manual
|
|
595
|
+
# Arguments: $1 = criterion text, $2 = source_issue, $3 = source_pr
|
|
596
|
+
classify_criterion() {
|
|
597
|
+
local criterion="$1"
|
|
598
|
+
local source_issue="$2"
|
|
599
|
+
local source_pr="$3"
|
|
600
|
+
|
|
601
|
+
# --- Step 1: Detect explicitly manual criteria ---
|
|
602
|
+
if echo "$criterion" | grep -qP '\[type:manual\]'; then
|
|
603
|
+
ADEQUACY_MANUAL_LIST="${ADEQUACY_MANUAL_LIST}\n [manual] Issue #${source_issue} (PR #${source_pr}): ${criterion}"
|
|
604
|
+
return
|
|
605
|
+
fi
|
|
606
|
+
|
|
607
|
+
# --- Step 2: Detect untestable-as-written criteria ---
|
|
608
|
+
# Vague oracles: subjective language, pure process requirements, non-deterministic assertions
|
|
609
|
+
if echo "$criterion" | grep -qiP '(should feel|must be appropriate|is good|as needed|where applicable|adequacy findings are filed|filed as tracked|flow back through|human validation|manual review|out-of-band|up to the reviewer|at the discretion|be reasonable|be sufficient|when possible|if applicable)'; then
|
|
610
|
+
ADEQUACY_UNTESTABLE_LIST="${ADEQUACY_UNTESTABLE_LIST}\n [untestable-as-written] Issue #${source_issue} (PR #${source_pr}): ${criterion}"
|
|
611
|
+
return
|
|
612
|
+
fi
|
|
613
|
+
|
|
614
|
+
# --- Step 3: Determine if criterion is runtime-testable ---
|
|
615
|
+
local is_automated=false
|
|
616
|
+
local inferred_type=""
|
|
617
|
+
|
|
618
|
+
if echo "$criterion" | grep -qP '\[type:(api|unit|e2e)\]'; then
|
|
619
|
+
is_automated=true
|
|
620
|
+
inferred_type=$(echo "$criterion" | grep -oP '\[type:\K(api|unit|e2e)' || echo "automated")
|
|
621
|
+
elif echo "$criterion" | grep -qP '(endpoint|request|response|status\s+\d{3}|curl|API|HTTP)'; then
|
|
622
|
+
is_automated=true
|
|
623
|
+
inferred_type="api"
|
|
624
|
+
elif echo "$criterion" | grep -qP '(unit|function|return|assert|throws)'; then
|
|
625
|
+
is_automated=true
|
|
626
|
+
inferred_type="unit"
|
|
627
|
+
elif echo "$criterion" | grep -qP '(browser|click|navigate|render|page|user flow)'; then
|
|
628
|
+
is_automated=true
|
|
629
|
+
inferred_type="e2e"
|
|
630
|
+
fi
|
|
631
|
+
|
|
632
|
+
if [ "$is_automated" = "false" ]; then
|
|
633
|
+
# No automation signal — treat as manual (inferred)
|
|
634
|
+
ADEQUACY_MANUAL_LIST="${ADEQUACY_MANUAL_LIST}\n [manual/inferred] Issue #${source_issue} (PR #${source_pr}): ${criterion}"
|
|
635
|
+
return
|
|
636
|
+
fi
|
|
637
|
+
|
|
638
|
+
# --- Step 4: Map automated criterion to a cluster ---
|
|
639
|
+
# A criterion is "covered" if at least one passing cluster name or cluster type
|
|
640
|
+
# matches the criterion's inferred type or the criterion text.
|
|
641
|
+
local is_covered=false
|
|
642
|
+
|
|
643
|
+
for cluster in $PASSING_CLUSTERS; do
|
|
644
|
+
# Match: cluster name substring appears in criterion text, OR
|
|
645
|
+
# cluster name matches the inferred test type (api/unit/e2e)
|
|
646
|
+
if echo "$criterion" | grep -qiF "$cluster"; then
|
|
647
|
+
is_covered=true
|
|
648
|
+
break
|
|
649
|
+
fi
|
|
650
|
+
if [ "$inferred_type" = "$cluster" ]; then
|
|
651
|
+
is_covered=true
|
|
652
|
+
break
|
|
653
|
+
fi
|
|
654
|
+
# Broad type-level match: if a passing cluster name contains the type keyword
|
|
655
|
+
if echo "$cluster" | grep -qiF "$inferred_type"; then
|
|
656
|
+
is_covered=true
|
|
657
|
+
break
|
|
658
|
+
fi
|
|
659
|
+
done
|
|
660
|
+
|
|
661
|
+
if [ "$is_covered" = "true" ]; then
|
|
662
|
+
ADEQUACY_COVERED_LIST="${ADEQUACY_COVERED_LIST}\n [covered:${inferred_type}] Issue #${source_issue} (PR #${source_pr}): ${criterion}"
|
|
663
|
+
else
|
|
664
|
+
ADEQUACY_UNCOVERED_LIST="${ADEQUACY_UNCOVERED_LIST}\n [UNCOVERED:${inferred_type}] Issue #${source_issue} (PR #${source_pr}): ${criterion}"
|
|
665
|
+
UNCOVERED_BLOCK_COUNT=$((UNCOVERED_BLOCK_COUNT + 1))
|
|
666
|
+
fi
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
# --- Iterate COLLATED_CRITERIA by issue section ---
|
|
670
|
+
# COLLATED_CRITERIA format:
|
|
671
|
+
# ### Issue #N (PR #M)
|
|
672
|
+
# - [ ] criterion text
|
|
673
|
+
# - [x] completed criterion
|
|
674
|
+
CURRENT_ISSUE=""
|
|
675
|
+
CURRENT_PR=""
|
|
676
|
+
|
|
677
|
+
while IFS= read -r line; do
|
|
678
|
+
# Detect section header: ### Issue #N (PR #M)
|
|
679
|
+
if echo "$line" | grep -qP '^### Issue #\d+'; then
|
|
680
|
+
CURRENT_ISSUE=$(echo "$line" | grep -oP '(?<=Issue #)\d+' | head -1)
|
|
681
|
+
CURRENT_PR=$(echo "$line" | grep -oP '(?<=PR #)\d+' | head -1)
|
|
682
|
+
continue
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
# Process criterion lines (both checked and unchecked)
|
|
686
|
+
if echo "$line" | grep -qP '^- \['; then
|
|
687
|
+
[ -z "$CURRENT_ISSUE" ] && continue
|
|
688
|
+
classify_criterion "$line" "$CURRENT_ISSUE" "$CURRENT_PR"
|
|
689
|
+
fi
|
|
690
|
+
done <<< "$(echo -e "$COLLATED_CRITERIA")"
|
|
691
|
+
|
|
692
|
+
# --- Report adequacy findings ---
|
|
693
|
+
COVERED_COUNT=$(echo -e "$ADEQUACY_COVERED_LIST" | grep -c '^\s*\[covered' || echo 0)
|
|
694
|
+
UNCOVERED_COUNT=$(echo -e "$ADEQUACY_UNCOVERED_LIST" | grep -c '^\s*\[UNCOVERED' || echo 0)
|
|
695
|
+
UNTESTABLE_COUNT=$(echo -e "$ADEQUACY_UNTESTABLE_LIST" | grep -c '^\s*\[untestable' || echo 0)
|
|
696
|
+
MANUAL_COUNT=$(echo -e "$ADEQUACY_MANUAL_LIST" | grep -c '^\s*\[manual' || echo 0)
|
|
697
|
+
|
|
698
|
+
echo "Criteria-adequacy summary:"
|
|
699
|
+
echo " Covered (mapped to a passing cluster): ${COVERED_COUNT}"
|
|
700
|
+
echo " Uncovered (runtime-testable, no cluster maps): ${UNCOVERED_COUNT}"
|
|
701
|
+
echo " Untestable-as-written (vague oracle): ${UNTESTABLE_COUNT}"
|
|
702
|
+
echo " Manual (require human validation): ${MANUAL_COUNT}"
|
|
703
|
+
echo ""
|
|
704
|
+
|
|
705
|
+
if [ "${COVERED_COUNT}" -gt 0 ]; then
|
|
706
|
+
echo "Covered criteria:"
|
|
707
|
+
echo -e "$ADEQUACY_COVERED_LIST"
|
|
708
|
+
echo ""
|
|
709
|
+
fi
|
|
710
|
+
|
|
711
|
+
if [ "${UNCOVERED_COUNT}" -gt 0 ]; then
|
|
712
|
+
echo "UNCOVERED criteria (coverage gaps — no passing cluster maps to these):"
|
|
713
|
+
echo -e "$ADEQUACY_UNCOVERED_LIST"
|
|
714
|
+
echo ""
|
|
715
|
+
fi
|
|
716
|
+
|
|
717
|
+
if [ "${UNTESTABLE_COUNT}" -gt 0 ]; then
|
|
718
|
+
echo "Untestable-as-written criteria (flagged — not counted as failures):"
|
|
719
|
+
echo -e "$ADEQUACY_UNTESTABLE_LIST"
|
|
720
|
+
echo ""
|
|
721
|
+
fi
|
|
722
|
+
|
|
723
|
+
if [ "${MANUAL_COUNT}" -gt 0 ]; then
|
|
724
|
+
echo "Manual criteria (require human validation — not covered by /test-gate):"
|
|
725
|
+
echo -e "$ADEQUACY_MANUAL_LIST"
|
|
726
|
+
echo ""
|
|
727
|
+
fi
|
|
728
|
+
|
|
729
|
+
# --- Set COVERAGE_BLOCK if uncovered runtime-testable criteria exist ---
|
|
730
|
+
if [ "${UNCOVERED_BLOCK_COUNT}" -gt 0 ]; then
|
|
731
|
+
if [ "$GATE_POSTURE" = "blocking" ]; then
|
|
732
|
+
COVERAGE_BLOCK=true
|
|
733
|
+
echo "COVERAGE BLOCK: ${UNCOVERED_BLOCK_COUNT} runtime-testable criterion/criteria have no mapped test cluster."
|
|
734
|
+
echo "Coverage gaps cannot produce a clean PASS verdict (posture: blocking)."
|
|
735
|
+
else
|
|
736
|
+
echo "COVERAGE ADVISORY: ${UNCOVERED_BLOCK_COUNT} runtime-testable criterion/criteria have no mapped test cluster."
|
|
737
|
+
echo "(posture: advisory — surfaced but deploy is not blocked by coverage gaps)"
|
|
738
|
+
fi
|
|
739
|
+
fi
|
|
740
|
+
|
|
741
|
+
# --- File test-gap issues for uncovered runtime-testable criteria ---
|
|
742
|
+
if [ "${UNCOVERED_COUNT}" -gt 0 ]; then
|
|
743
|
+
# Ensure test-gap label exists
|
|
744
|
+
gh label create "test-gap" --color "E4E669" \
|
|
745
|
+
--description "Acceptance criterion not covered by any /test-gate cluster. Filed by ForgeDock." \
|
|
746
|
+
--force ${GH_FLAG} 2>/dev/null || true
|
|
747
|
+
|
|
748
|
+
while IFS= read -r gap_line; do
|
|
749
|
+
[ -z "$(echo "$gap_line" | grep -E '\S')" ] && continue
|
|
750
|
+
|
|
751
|
+
# Extract issue and criterion from gap line format: [UNCOVERED:type] Issue #N (PR #M): criterion
|
|
752
|
+
GAP_ISSUE=$(echo "$gap_line" | grep -oP '(?<=Issue #)\d+' | head -1)
|
|
753
|
+
GAP_PR=$(echo "$gap_line" | grep -oP '(?<=PR #)\d+' | head -1)
|
|
754
|
+
GAP_TYPE=$(echo "$gap_line" | grep -oP '(?<=UNCOVERED:)[^\]]+' | head -1)
|
|
755
|
+
GAP_CRITERION=$(echo "$gap_line" | grep -oP '(?<=: )- \[.+' | head -1 || echo "$gap_line")
|
|
756
|
+
|
|
757
|
+
[ -z "$GAP_ISSUE" ] && continue
|
|
758
|
+
|
|
759
|
+
# Duplicate prevention: check for open test-gap issues referencing this issue+criterion
|
|
760
|
+
EXISTING_GAP=$(gh issue list ${GH_FLAG} \
|
|
761
|
+
--label "test-gap" \
|
|
762
|
+
--state open \
|
|
763
|
+
--search "test-gap issue #${GAP_ISSUE}" \
|
|
764
|
+
--limit 5 \
|
|
765
|
+
--json number,title \
|
|
766
|
+
--jq '.[0].number // empty' 2>/dev/null || true)
|
|
767
|
+
|
|
768
|
+
if [ -n "$EXISTING_GAP" ]; then
|
|
769
|
+
echo "Skipping test-gap issue creation — open issue already exists: #${EXISTING_GAP} (source: #${GAP_ISSUE})"
|
|
770
|
+
continue
|
|
771
|
+
fi
|
|
772
|
+
|
|
773
|
+
GAP_ISSUE_NUM=$(gh issue create ${GH_FLAG} \
|
|
774
|
+
--title "fix: test-gap — uncovered criterion in issue #${GAP_ISSUE} (${GAP_TYPE})" \
|
|
775
|
+
--label "test-gap" \
|
|
776
|
+
--body "$(cat <<TGAP_EOF
|
|
777
|
+
## Problem
|
|
778
|
+
|
|
779
|
+
The \`/test-gate\` criteria-adequacy check (Phase 6) found a runtime-testable acceptance criterion in issue #${GAP_ISSUE} that is not covered by any configured test cluster. Coverage gaps cannot produce a clean PASS verdict — this criterion must be covered before the staging→${DEFAULT_BRANCH} bundle can pass the deploy gate.
|
|
780
|
+
|
|
781
|
+
## Root Cause (if known)
|
|
782
|
+
|
|
783
|
+
No test cluster in \`forge.yaml verification.integration_tests\` maps to this criterion. Either:
|
|
784
|
+
1. The test cluster that would cover this criterion does not exist yet (gap in test suite), or
|
|
785
|
+
2. The cluster name does not match the criterion's keywords (mapping heuristic miss).
|
|
786
|
+
|
|
787
|
+
## Affected Files
|
|
788
|
+
|
|
789
|
+
Files to be identified during investigation. Start with the test suite for the \`${GAP_TYPE}\` cluster.
|
|
790
|
+
|
|
791
|
+
## Acceptance Criteria
|
|
792
|
+
|
|
793
|
+
- [ ] A test cluster that covers this criterion is identified or created
|
|
794
|
+
- [ ] The criterion maps to the cluster in a /test-gate run
|
|
795
|
+
- [ ] /test-gate Phase 6 reports this criterion as covered (not uncovered) for this bundle
|
|
796
|
+
|
|
797
|
+
## Context
|
|
798
|
+
|
|
799
|
+
**Detected by**: \`/test-gate\` — Phase 6 criteria-adequacy check
|
|
800
|
+
**Source issue**: #${GAP_ISSUE}
|
|
801
|
+
**Source PR**: #${GAP_PR}
|
|
802
|
+
**Bundle PRs**: ${BUNDLE_PRS}
|
|
803
|
+
**Criterion type**: \`${GAP_TYPE}\`
|
|
804
|
+
**Uncovered criterion**:
|
|
805
|
+
\`\`\`
|
|
806
|
+
${GAP_CRITERION}
|
|
807
|
+
\`\`\`
|
|
808
|
+
TGAP_EOF
|
|
809
|
+
)" --json number --jq '.number' 2>/dev/null || echo "FAILED")
|
|
810
|
+
|
|
811
|
+
if [ "$GAP_ISSUE_NUM" != "FAILED" ] && [ -n "$GAP_ISSUE_NUM" ]; then
|
|
812
|
+
echo "Filed test-gap issue #${GAP_ISSUE_NUM} for uncovered criterion in #${GAP_ISSUE} (${GAP_TYPE})"
|
|
813
|
+
else
|
|
814
|
+
echo "WARNING: Failed to create test-gap issue for uncovered criterion in #${GAP_ISSUE}"
|
|
815
|
+
fi
|
|
816
|
+
done <<< "$(echo -e "$ADEQUACY_UNCOVERED_LIST")"
|
|
817
|
+
fi
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## Phase 7: Verdict — Emit Machine-Readable Result
|
|
822
|
+
|
|
823
|
+
Determine BLOCK / PASS / SKIP based on batch-introduced failures, cluster results, coverage gaps, and posture config.
|
|
824
|
+
|
|
825
|
+
```bash
|
|
826
|
+
echo "=== Phase 7: Emitting verdict ==="
|
|
827
|
+
|
|
828
|
+
BATCH_FAILURE_COUNT=$(echo -e "$BATCH_FAILURES" | grep -c '\S' || echo 0)
|
|
829
|
+
TOTAL_PASS=$(echo -e "$CLUSTER_RESULTS" | grep -c "PASS" || echo 0)
|
|
830
|
+
TOTAL_FAIL=$(echo -e "$CLUSTER_RESULTS" | grep -c "FAIL" || echo 0)
|
|
831
|
+
|
|
832
|
+
# COVERAGE_BLOCK is set by Phase 6 when uncovered runtime-testable criteria exist
|
|
833
|
+
# and posture is blocking. It is initialized to false if Phase 6 found no gaps.
|
|
834
|
+
COVERAGE_BLOCK="${COVERAGE_BLOCK:-false}"
|
|
835
|
+
UNCOVERED_BLOCK_COUNT="${UNCOVERED_BLOCK_COUNT:-0}"
|
|
836
|
+
|
|
837
|
+
if [ "${BATCH_FAILURE_COUNT}" -gt 0 ]; then
|
|
838
|
+
VERDICT="BLOCK"
|
|
839
|
+
VERDICT_REASON="${BATCH_FAILURE_COUNT} cluster(s) failed with batch-introduced regressions: $(echo -e "$BATCH_FAILURES" | tr '\n' ', ' | sed 's/,\s*$//')"
|
|
840
|
+
elif [ "$COVERAGE_BLOCK" = "true" ]; then
|
|
841
|
+
# Coverage gaps: runtime-testable criteria exist but no cluster maps to them.
|
|
842
|
+
# A gate that runs some tests but skips verifying specific criteria is not a clean pass.
|
|
843
|
+
VERDICT="BLOCK"
|
|
844
|
+
VERDICT_REASON="${UNCOVERED_BLOCK_COUNT} runtime-testable acceptance criterion/criteria not covered by any test cluster. Coverage gaps cannot produce a clean PASS verdict."
|
|
845
|
+
elif [ "${TOTAL_FAIL}" -gt 0 ]; then
|
|
846
|
+
# All failures are pre-existing (baseline also failed) — not blocking
|
|
847
|
+
VERDICT="PASS"
|
|
848
|
+
VERDICT_REASON="${TOTAL_FAIL} pre-existing failure(s) detected (also failed on ${BASE_BRANCH} baseline — not introduced by this batch). ${TOTAL_PASS} cluster(s) passed."
|
|
849
|
+
else
|
|
850
|
+
VERDICT="PASS"
|
|
851
|
+
VERDICT_REASON="All ${TOTAL_PASS} cluster(s) passed. No batch-introduced failures. All runtime-testable criteria covered."
|
|
852
|
+
fi
|
|
853
|
+
|
|
854
|
+
echo ""
|
|
855
|
+
echo "============================================="
|
|
856
|
+
echo " TEST GATE VERDICT: ${VERDICT}"
|
|
857
|
+
echo "============================================="
|
|
858
|
+
echo " Reason: ${VERDICT_REASON}"
|
|
859
|
+
echo " Posture: ${GATE_POSTURE}"
|
|
860
|
+
echo " Bundle PRs: ${BUNDLE_PRS}"
|
|
861
|
+
echo " Solved issues: $(echo $SOLVED_ISSUES | tr ' ' '\n' | sort -u | tr '\n' ' ')"
|
|
862
|
+
echo ""
|
|
863
|
+
echo "Cluster summary:"
|
|
864
|
+
echo -e "$CLUSTER_RESULTS"
|
|
865
|
+
echo ""
|
|
866
|
+
echo "Adequacy summary:"
|
|
867
|
+
echo " Covered: ${COVERED_COUNT:-0} | Uncovered: ${UNCOVERED_COUNT:-0} | Untestable-as-written: ${UNTESTABLE_COUNT:-0} | Manual: ${MANUAL_COUNT:-0}"
|
|
868
|
+
echo "============================================="
|
|
869
|
+
|
|
870
|
+
# Emit machine-readable verdict marker (consumed by review-pr-staging Phase 6.5)
|
|
871
|
+
echo ""
|
|
872
|
+
echo "<!-- FORGE:TEST_GATE:RESULT=${VERDICT} -->"
|
|
873
|
+
|
|
874
|
+
# Handle BLOCK verdict according to posture
|
|
875
|
+
if [ "$VERDICT" = "BLOCK" ]; then
|
|
876
|
+
if [ "$GATE_POSTURE" = "advisory" ]; then
|
|
877
|
+
echo ""
|
|
878
|
+
echo "ADVISORY posture: BLOCK verdict surfaced but deploy is NOT prevented."
|
|
879
|
+
echo "Switch to posture: blocking in forge.yaml to make this gate enforce."
|
|
880
|
+
echo ""
|
|
881
|
+
echo "TEST GATE: ADVISORY BLOCK"
|
|
882
|
+
else
|
|
883
|
+
# blocking posture (default)
|
|
884
|
+
echo ""
|
|
885
|
+
echo "BLOCKING posture: Deploy is prevented. To override, post a PR comment containing:"
|
|
886
|
+
echo " ${OVERRIDE_PHRASE} <reason>"
|
|
887
|
+
echo ""
|
|
888
|
+
echo "TEST GATE: BLOCK DEPLOY"
|
|
889
|
+
exit 1
|
|
890
|
+
fi
|
|
891
|
+
else
|
|
892
|
+
echo ""
|
|
893
|
+
echo "TEST GATE: ${VERDICT}"
|
|
894
|
+
fi
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## Override Escape Hatch
|
|
900
|
+
|
|
901
|
+
When a PR comment containing `verification.test_gate.override_phrase` (default: `OVERRIDE: shipping with test failures —`) is detected on the staging→main PR, the BLOCK verdict is downgraded and the deploy proceeds. Override detection is performed by the caller (`/review-pr-staging` Phase 6.5) using:
|
|
902
|
+
|
|
903
|
+
```bash
|
|
904
|
+
OVERRIDE=$(gh pr view "$PR_NUMBER" ${GH_FLAG} \
|
|
905
|
+
--json comments \
|
|
906
|
+
--jq "[.comments[].body | select(startswith(\"${OVERRIDE_PHRASE}\"))] | length" 2>/dev/null || echo 0)
|
|
907
|
+
|
|
908
|
+
if [ "${OVERRIDE:-0}" -gt 0 ]; then
|
|
909
|
+
echo "Override detected — downgrading BLOCK to PASS for this deploy."
|
|
910
|
+
echo "<!-- FORGE:TEST_GATE:RESULT=PASS -->"
|
|
911
|
+
fi
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
---
|
|
915
|
+
|
|
916
|
+
## Structured Verdict Reference
|
|
917
|
+
|
|
918
|
+
The caller (`/review-pr-staging`) reads the verdict via:
|
|
919
|
+
|
|
920
|
+
```bash
|
|
921
|
+
VERDICT=$(gh pr view "$PR_NUMBER" ${GH_FLAG} --json comments \
|
|
922
|
+
--jq '[.comments[].body | capture("FORGE:TEST_GATE:RESULT=(?<v>BLOCK|PASS|SKIP)").v // empty] | last // "SKIP"')
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
| Verdict | Meaning |
|
|
926
|
+
|---------|---------|
|
|
927
|
+
| `PASS` | All clusters passed (or all failures are pre-existing). Deploy may proceed. |
|
|
928
|
+
| `BLOCK` | Batch-introduced failures detected. Deploy blocked (blocking posture) or warned (advisory posture). |
|
|
929
|
+
| `SKIP` | No tests configured, no executable changes, or manual-only criteria. Deploy proceeds without test verification. |
|