@metasession.co/devaudit-cli 0.3.7 → 0.3.9
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 +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +33 -3
- package/sdlc/files/_common/scripts/close-out-contract.sh +34 -0
- package/sdlc/files/_common/scripts/derive-release-version.sh +18 -0
- package/sdlc/files/_common/scripts/derive-release-version.test.sh +57 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +119 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +363 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +593 -0
- package/sdlc/files/_common/scripts/validate-test-summary.sh +37 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +87 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +16 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +7 -4
- package/sdlc/files/ci/check-release-approval.yml.template +80 -21
- package/sdlc/files/ci/ci-status-fallback.yml.template +3 -0
- package/sdlc/files/ci/ci.yml.template +24 -33
- package/sdlc/files/ci/close-out-release.yml.template +67 -11
- package/sdlc/files/ci/compliance-evidence.yml.template +32 -16
- package/sdlc/files/ci/quality-gates-provenance.yml.template +62 -0
- package/sdlc/files/stacks/node/hooks/pre-push +58 -8
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +22 -2
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +20 -6
|
@@ -179,6 +179,12 @@ make_fixture "$D6" "REQ-006" << 'EOF'
|
|
|
179
179
|
| ---------------- | ------------ | ------------------------------------------ |
|
|
180
180
|
| E2E Tests | SKIPPED | API-only change, no UI surface (operator-approved) |
|
|
181
181
|
|
|
182
|
+
## Accepted skips
|
|
183
|
+
|
|
184
|
+
| Spec | Test | REQ/AC | Classification | Resolution attempted | Approved by | Rationale |
|
|
185
|
+
|------|------|--------|---------------|---------------------|-------------|----------|
|
|
186
|
+
| e2e/foo.spec.ts | "AC1: bar dialog" | REQ-006/AC1 | intentional_non_applicability | N/A — API-only change | operator | API-only change, no UI surface |
|
|
187
|
+
|
|
182
188
|
**Final assessment:** All gates passed. E2E skipped — API-only change.
|
|
183
189
|
EOF
|
|
184
190
|
EXIT6=$(run_validator "$D6")
|
|
@@ -259,6 +265,87 @@ EOF
|
|
|
259
265
|
EXIT11=$(run_validator "$D11")
|
|
260
266
|
assert_exit "summary with NOT_NEEDED but no rationale — should fail" 1 "$EXIT11"
|
|
261
267
|
|
|
268
|
+
# --- Case 12: SKIPPED with ## Accepted skips section and valid rows — should pass (devaudit-installer#279) ---
|
|
269
|
+
D12="$WORK/case12"
|
|
270
|
+
make_fixture "$D12" "REQ-012" << 'EOF'
|
|
271
|
+
# Test Execution Summary — REQ-012
|
|
272
|
+
|
|
273
|
+
## Gate Results
|
|
274
|
+
|
|
275
|
+
| Gate | Result | Details |
|
|
276
|
+
| ---------------- | ------------ | ------------------------------------------ |
|
|
277
|
+
| E2E Tests | SKIPPED | API-only change, no UI surface (operator-approved) |
|
|
278
|
+
|
|
279
|
+
## Accepted skips
|
|
280
|
+
|
|
281
|
+
| Spec | Test | REQ/AC | Classification | Resolution attempted | Approved by | Rationale |
|
|
282
|
+
|------|------|--------|---------------|---------------------|-------------|----------|
|
|
283
|
+
| e2e/foo.spec.ts | "AC3: bar dialog" | REQ-012/AC3 | intentional_non_applicability | N/A — API-only change | operator | API-only change, no UI surface |
|
|
284
|
+
|
|
285
|
+
**Final assessment:** All gates passed. E2E skipped — API-only change.
|
|
286
|
+
EOF
|
|
287
|
+
EXIT12=$(run_validator "$D12")
|
|
288
|
+
assert_exit "SKIPPED with ## Accepted skips section and valid rows — should pass" 0 "$EXIT12"
|
|
289
|
+
|
|
290
|
+
# --- Case 13: SKIPPED without ## Accepted skips section — should fail (devaudit-installer#279) ---
|
|
291
|
+
D13="$WORK/case13"
|
|
292
|
+
make_fixture "$D13" "REQ-013" << 'EOF'
|
|
293
|
+
# Test Execution Summary — REQ-013
|
|
294
|
+
|
|
295
|
+
## Gate Results
|
|
296
|
+
|
|
297
|
+
| Gate | Result | Details |
|
|
298
|
+
| ---------------- | ------------ | ------------------------------------------ |
|
|
299
|
+
| E2E Tests | SKIPPED | API-only change, no UI surface (operator-approved) |
|
|
300
|
+
|
|
301
|
+
**Final assessment:** All gates passed. E2E skipped — API-only change.
|
|
302
|
+
EOF
|
|
303
|
+
EXIT13=$(run_validator "$D13")
|
|
304
|
+
assert_exit "SKIPPED without ## Accepted skips section — should fail" 1 "$EXIT13"
|
|
305
|
+
|
|
306
|
+
# --- Case 14: SKIPPED with ## Accepted skips but no table rows — should fail (devaudit-installer#279) ---
|
|
307
|
+
D14="$WORK/case14"
|
|
308
|
+
make_fixture "$D14" "REQ-014" << 'EOF'
|
|
309
|
+
# Test Execution Summary — REQ-014
|
|
310
|
+
|
|
311
|
+
## Gate Results
|
|
312
|
+
|
|
313
|
+
| Gate | Result | Details |
|
|
314
|
+
| ---------------- | ------------ | ------------------------------------------ |
|
|
315
|
+
| E2E Tests | SKIPPED | API-only change, no UI surface (operator-approved) |
|
|
316
|
+
|
|
317
|
+
## Accepted skips
|
|
318
|
+
|
|
319
|
+
| Spec | Test | REQ/AC | Classification | Resolution attempted | Approved by | Rationale |
|
|
320
|
+
|------|------|--------|---------------|---------------------|-------------|----------|
|
|
321
|
+
|
|
322
|
+
**Final assessment:** All gates passed. E2E skipped — API-only change.
|
|
323
|
+
EOF
|
|
324
|
+
EXIT14=$(run_validator "$D14")
|
|
325
|
+
assert_exit "SKIPPED with ## Accepted skips but no table rows — should fail" 1 "$EXIT14"
|
|
326
|
+
|
|
327
|
+
# --- Case 15: SKIPPED with accepted-skip rows missing Approved by — should fail (devaudit-installer#279) ---
|
|
328
|
+
D15="$WORK/case15"
|
|
329
|
+
make_fixture "$D15" "REQ-015" << 'EOF'
|
|
330
|
+
# Test Execution Summary — REQ-015
|
|
331
|
+
|
|
332
|
+
## Gate Results
|
|
333
|
+
|
|
334
|
+
| Gate | Result | Details |
|
|
335
|
+
| ---------------- | ------------ | ------------------------------------------ |
|
|
336
|
+
| E2E Tests | SKIPPED | API-only change, no UI surface (operator-approved) |
|
|
337
|
+
|
|
338
|
+
## Accepted skips
|
|
339
|
+
|
|
340
|
+
| Spec | Test | REQ/AC | Classification | Resolution attempted | Approved by | Rationale |
|
|
341
|
+
|------|------|--------|---------------|---------------------|-------------|----------|
|
|
342
|
+
| e2e/foo.spec.ts | "AC3: bar dialog" | REQ-015/AC3 | intentional_non_applicability | N/A — API-only change | | API-only change, no UI surface |
|
|
343
|
+
|
|
344
|
+
**Final assessment:** All gates passed. E2E skipped — API-only change.
|
|
345
|
+
EOF
|
|
346
|
+
EXIT15=$(run_validator "$D15")
|
|
347
|
+
assert_exit "SKIPPED with accepted-skip rows missing Approved by — should fail" 1 "$EXIT15"
|
|
348
|
+
|
|
262
349
|
echo ""
|
|
263
350
|
echo "=== Summary: $PASS pass / $FAIL fail ==="
|
|
264
351
|
|
|
@@ -300,6 +300,21 @@ Then bucket each failure:
|
|
|
300
300
|
|
|
301
301
|
The classification is: "Is the AC correct but untested/buggy? → defect. Is the AC itself wrong/missing? → requirements gap."
|
|
302
302
|
|
|
303
|
+
**Then triage skipped tests (devaudit-installer#279).** After failure triage and missed-requirements check, inspect all skipped tests from the machine-readable Playwright reporter output (JSON reporter). For each skipped test:
|
|
304
|
+
|
|
305
|
+
1. **Extract the best available reason** from: `test.skipReason`, annotations, `test.skip(...)` / `testInfo.skip(...)` descriptions, setup-project failures, or helper/guard messages. Do not hard-code to one field — use the richest available source.
|
|
306
|
+
2. **Classify each skip into one of:**
|
|
307
|
+
- `environment_gap` — browser not installed, database down, port conflict, missing system dependency
|
|
308
|
+
- `precondition_gap` — seed data missing, auth fixture expired, test account locked
|
|
309
|
+
- `intentional_non_applicability` — API-only change with no UI surface, feature flag disabled for this environment
|
|
310
|
+
- `obsolete_or_quarantined` — test references removed functionality, spec is stale
|
|
311
|
+
3. **Fix `environment_gap` and `precondition_gap` first** — these are never acceptable skip reasons. Resolve the underlying issue, rerun the focused tests, then rerun the full suite.
|
|
312
|
+
4. **Only remaining `intentional_non_applicability` or unresolved skips** may be surfaced to the operator for approval. Each accepted skip must have an operator-approved rationale.
|
|
313
|
+
5. **A skipped test never proves an acceptance criterion.** If a skipped test is the only coverage for an in-scope AC, that AC is unverified until a passing test proves it.
|
|
314
|
+
6. **Do not write a `PASSED` E2E sentinel while unresolved or unapproved skips remain.**
|
|
315
|
+
|
|
316
|
+
Record all accepted skips in the `## Accepted skips` section of `test-execution-summary.md` (see Stage 3 blueprint).
|
|
317
|
+
|
|
303
318
|
### Phase 7 — Regression-pack handoff
|
|
304
319
|
|
|
305
320
|
After Phase 6 succeeds — green run, all ACs proved, defects filed for anything missing — the new spec(s) you authored move into the project's regression pack. There is **no separate graduation step**. The pack is defined as:
|
|
@@ -422,6 +437,7 @@ Wrap up with a summary the user can drop into the PR or ticket:
|
|
|
422
437
|
- Tests added — count, with a list.
|
|
423
438
|
- Tests updated — count.
|
|
424
439
|
- Tests deleted — count, with rationale.
|
|
440
|
+
- Tests skipped — count, classification, resolution status, and operator-approved rationale for any accepted skip (devaudit-installer#279).
|
|
425
441
|
- Suite result — passing, failing, flaky.
|
|
426
442
|
- Defects filed — count, with links.
|
|
427
443
|
- Requirements gap reports — count, with details (devaudit-installer#212 Gap 4). These are ACs classified as "impossible to test" or "missing AC" — returned to `sdlc-implementer` for the requirements gap flow, not filed as defects.
|
|
@@ -209,7 +209,7 @@ NEXT: Done — close issue + retire feature branch (sdlc-implementer halts)
|
|
|
209
209
|
Lead every substantive turn with a driver tag on the **first line**, then the two-line LAST/NEXT shape so the operator can `Ctrl-F NEXT:` in the chat transcript to find the current pointer without re-reading:
|
|
210
210
|
|
|
211
211
|
```
|
|
212
|
-
[
|
|
212
|
+
[Skill driving] — or — [Native agent driving] — or — [Operator driving] — or — [Blocked]
|
|
213
213
|
|
|
214
214
|
**LAST:** <one sentence>
|
|
215
215
|
**NEXT:** <one sentence with actor>
|
|
@@ -217,9 +217,10 @@ Lead every substantive turn with a driver tag on the **first line**, then the tw
|
|
|
217
217
|
|
|
218
218
|
The driver tag is mandatory and comes **before** the LAST/NEXT lines:
|
|
219
219
|
|
|
220
|
-
- **`[
|
|
221
|
-
- **`[
|
|
222
|
-
- **`[
|
|
220
|
+
- **`[Skill driving]`** — a skill (e.g. `sdlc-implementer`) is auto-continuing; no human action needed right now. The operator can look away.
|
|
221
|
+
- **`[Native agent driving]`** — the native AI agent (Cursor, Windsurf, Claude Code, Gemini CLI, etc.) is executing steps without a skill invocation. The operator can look away but should monitor more closely since no skill protocol is governing phase transitions.
|
|
222
|
+
- **`[Operator driving]`** — the skill/agent has halted; the human must do something (review, approve, merge, answer a question). The NEXT line states the specific action needed.
|
|
223
|
+
- **`[Blocked]`** — something failed and the skill/agent cannot proceed. State the blocker and the operator action needed to unblock.
|
|
223
224
|
|
|
224
225
|
The tag reflects the **final** state of the response — if the skill was driving but hits a halt mid-turn, the tag is `[Operator driving]` or `[Blocked]`.
|
|
225
226
|
|
|
@@ -587,6 +588,8 @@ Reached only on the **tracked** route from Phase 0 (the issue is already fetched
|
|
|
587
588
|
|
|
588
589
|
**Release Approval Gate retry (devaudit-installer#211 Gap 17).** If the Release Approval Gate check fails on the PR and the portal approval was already given (API sync delay, stale cache), retry logic: re-run the Release Approval Gate workflow up to 3 times with 30-second intervals (`gh workflow run check-release-approval.yml` or trigger via `workflow_dispatch`). If it still fails after 3 retries: halt — "Release Approval Gate still failing after 3 retries. Portal may show approval but the gate can't verify it. Operator action — check the portal release status manually, verify the API key is valid, and re-run the workflow from GitHub Actions if needed."
|
|
589
590
|
|
|
591
|
+
**Auto-refresh on UAT approval (devaudit#562, devaudit-installer#283).** When the portal's UAT approval is granted, the portal automatically sends a `repository_dispatch('release-approved')` event to the consuming project's repo. This triggers `check-release-approval.yml` to re-run without manual intervention. The retry logic above is still needed for edge cases (API sync delays, network issues), but the common path is now fully automated. If the auto-refresh doesn't fire (e.g. portal GitHub trigger misconfigured), the manual retry sequence above remains the fallback.
|
|
592
|
+
|
|
590
593
|
### Phase 5 — Finalise or change-request loop (SDLC stage 5)
|
|
591
594
|
|
|
592
595
|
Invoked separately by the user after UAT activity on the portal. Trigger: "resume REQ-XXX", "REQ-XXX UAT done", or just re-firing the skill on the same issue.
|
|
@@ -22,6 +22,14 @@ on:
|
|
|
22
22
|
pull_request:
|
|
23
23
|
branches: [main]
|
|
24
24
|
workflow_dispatch:
|
|
25
|
+
repository_dispatch:
|
|
26
|
+
types: [release-approved]
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
checks: write
|
|
31
|
+
pull-requests: write
|
|
32
|
+
issues: write
|
|
25
33
|
|
|
26
34
|
jobs:
|
|
27
35
|
check-approval:
|
|
@@ -43,7 +51,11 @@ jobs:
|
|
|
43
51
|
# produces the same release identity here (#81 — release-identity
|
|
44
52
|
# coherence; was hardcoded to `v$(date +%Y.%m.%d)` which never
|
|
45
53
|
# matched the `REQ-XXX` records the uploaders wrote).
|
|
46
|
-
|
|
54
|
+
#
|
|
55
|
+
# devaudit#283 — repository_dispatch events don't have a
|
|
56
|
+
# pull_request payload. Fall back to the develop branch ref so
|
|
57
|
+
# the gate can resolve the release and post a status check.
|
|
58
|
+
ref: ${{ github.event.pull_request.head.sha || 'develop' }}
|
|
47
59
|
fetch-depth: 0
|
|
48
60
|
|
|
49
61
|
- name: Resolve DevAudit base URL
|
|
@@ -149,13 +161,22 @@ jobs:
|
|
|
149
161
|
id: release
|
|
150
162
|
if: env.BOOTSTRAP_MODE != 'true'
|
|
151
163
|
run: |
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
# devaudit#283 — repository_dispatch('release-approved') events
|
|
165
|
+
# carry the release version in client_payload. Use it directly
|
|
166
|
+
# when available to avoid re-deriving from git history.
|
|
167
|
+
if [ "${{ github.event_name }}" = "repository_dispatch" ] && [ -n "${{ github.event.client_payload.release }}" ]; then
|
|
168
|
+
LOOKUP_PREFIX="${{ github.event.client_payload.release }}"
|
|
169
|
+
echo "Using release version from repository_dispatch payload: ${LOOKUP_PREFIX}"
|
|
170
|
+
else
|
|
171
|
+
# Derive the same release prefix the uploaders write to. The script
|
|
172
|
+
# priority is: subject `[REQ-XXX]` → body `Ref: REQ-XXX` → body
|
|
173
|
+
# `[REQ-XXX]` (merge-commit case) → bare-date fallback. Reusing it
|
|
174
|
+
# here makes the gate and the uploaders agree on release identity
|
|
175
|
+
# (#81). The 8 scenarios are covered by derive-release-version.test.sh.
|
|
176
|
+
chmod +x scripts/derive-release-version.sh 2>/dev/null || true
|
|
177
|
+
LOOKUP_PREFIX=$(bash scripts/derive-release-version.sh)
|
|
178
|
+
echo "Derived release prefix from git history: ${LOOKUP_PREFIX}"
|
|
179
|
+
fi
|
|
159
180
|
echo "Resolving release for prefix: ${LOOKUP_PREFIX}"
|
|
160
181
|
RESOLVE_URL="${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${LOOKUP_PREFIX}"
|
|
161
182
|
# Retry: register-release in ci.yml may still be racing with us.
|
|
@@ -234,21 +255,59 @@ jobs:
|
|
|
234
255
|
echo "::warning::Approved SHA (${APPROVED_SHA:0:8}) differs from PR HEAD (${PR_HEAD_SHA:0:8}). Code may have changed after approval."
|
|
235
256
|
fi
|
|
236
257
|
|
|
237
|
-
- name: Update PR
|
|
238
|
-
if: env.BOOTSTRAP_MODE != 'true' && github.event_name == 'workflow_dispatch'
|
|
258
|
+
- name: Update PR check run (workflow_dispatch / repository_dispatch re-trigger)
|
|
259
|
+
if: env.BOOTSTRAP_MODE != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch')
|
|
239
260
|
env:
|
|
240
261
|
GH_TOKEN: ${{ github.token }}
|
|
241
262
|
run: |
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
TARGET_SHA="${{ github.event.client_payload.approved_sha || '' }}"
|
|
264
|
+
if [ -z "$TARGET_SHA" ]; then
|
|
265
|
+
TARGET_SHA="${{ steps.release.outputs.approved_sha }}"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
RELEASE_PR="${{ github.event.client_payload.release_pr || '' }}"
|
|
269
|
+
if [ -z "$TARGET_SHA" ] && [ -n "$RELEASE_PR" ] && [ "$RELEASE_PR" != "null" ]; then
|
|
270
|
+
TARGET_SHA=$(gh pr view "$RELEASE_PR" --json headRefOid --jq '.headRefOid // empty' 2>/dev/null || true)
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
if [ -z "$TARGET_SHA" ]; then
|
|
274
|
+
PR_DATA=$(gh pr list --base main --json number,headRefOid,updatedAt --limit 20 2>/dev/null || echo '[]')
|
|
275
|
+
TARGET_SHA=$(echo "$PR_DATA" | jq -r 'sort_by(.updatedAt) | reverse | .[0].headRefOid // empty' 2>/dev/null || true)
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
if [ -z "$TARGET_SHA" ]; then
|
|
279
|
+
echo "::error::Unable to resolve PR head SHA for DevAudit Release Approval check refresh. Portal dispatch should include approved_sha."
|
|
280
|
+
exit 1
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
CHECK_NAME="DevAudit Release Approval"
|
|
284
|
+
DETAILS_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
285
|
+
COMPLETED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
286
|
+
EXISTING_ID=$(gh api \
|
|
287
|
+
"/repos/${{ github.repository }}/commits/${TARGET_SHA}/check-runs?name=DevAudit%20Release%20Approval" \
|
|
288
|
+
--jq '.check_runs[] | select(.name == "DevAudit Release Approval") | .id' \
|
|
289
|
+
| head -1 || true)
|
|
290
|
+
|
|
291
|
+
if [ -n "$EXISTING_ID" ]; then
|
|
292
|
+
gh api "/repos/${{ github.repository }}/check-runs/${EXISTING_ID}" \
|
|
293
|
+
-X PATCH \
|
|
294
|
+
-f status=completed \
|
|
295
|
+
-f conclusion=success \
|
|
296
|
+
-f completed_at="$COMPLETED_AT" \
|
|
297
|
+
-f details_url="$DETAILS_URL" \
|
|
298
|
+
-f "output[title]=DevAudit release approval confirmed" \
|
|
299
|
+
-f "output[summary]=Release ${{ steps.release.outputs.version }} is approved in DevAudit."
|
|
300
|
+
echo "Updated ${CHECK_NAME} check run ${EXISTING_ID} on ${TARGET_SHA}."
|
|
252
301
|
else
|
|
253
|
-
|
|
302
|
+
gh api "/repos/${{ github.repository }}/check-runs" \
|
|
303
|
+
-X POST \
|
|
304
|
+
-f name="$CHECK_NAME" \
|
|
305
|
+
-f head_sha="$TARGET_SHA" \
|
|
306
|
+
-f status=completed \
|
|
307
|
+
-f conclusion=success \
|
|
308
|
+
-f completed_at="$COMPLETED_AT" \
|
|
309
|
+
-f details_url="$DETAILS_URL" \
|
|
310
|
+
-f "output[title]=DevAudit release approval confirmed" \
|
|
311
|
+
-f "output[summary]=Release ${{ steps.release.outputs.version }} is approved in DevAudit."
|
|
312
|
+
echo "Created ${CHECK_NAME} check run on ${TARGET_SHA}."
|
|
254
313
|
fi
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
# `Quality Gates` status. Branch protection is satisfied without running the heavy gates
|
|
14
14
|
# on a commit that has no code in it.
|
|
15
15
|
#
|
|
16
|
+
# PRs to main are actively verified by quality-gates-provenance.yml, which checks
|
|
17
|
+
# for prior develop-side Quality Gates success on the same head SHA.
|
|
18
|
+
#
|
|
16
19
|
# Mixed commits (some docs + some code) trigger BOTH workflows. Same status name, both
|
|
17
20
|
# green = branch protection happy.
|
|
18
21
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# Single consolidated job — on a self-hosted runner, parallel jobs run
|
|
7
7
|
# sequentially anyway. One checkout + one cached npm ci = fast.
|
|
8
8
|
#
|
|
9
|
-
# PRs to develop run Quality Gates before integration. PRs to main
|
|
10
|
-
#
|
|
9
|
+
# PRs to develop run Quality Gates before integration. PRs to main are verified
|
|
10
|
+
# by quality-gates-provenance.yml (same-SHA provenance check, not heavy CI rerun).
|
|
11
11
|
# Compliance validation runs separately on PRs (compliance-validation.yml).
|
|
12
12
|
|
|
13
13
|
name: CI Pipeline
|
|
@@ -287,11 +287,16 @@ jobs:
|
|
|
287
287
|
# feature converges on one release record. See DevAudit #310.
|
|
288
288
|
chmod +x scripts/derive-release-version.sh 2>/dev/null || true
|
|
289
289
|
VERSION=$(./scripts/derive-release-version.sh)
|
|
290
|
+
if [ -z "$VERSION" ]; then
|
|
291
|
+
echo "version=skip" >> "$GITHUB_OUTPUT"
|
|
292
|
+
echo "Release registration skipped: close-out reconciliation detected."
|
|
293
|
+
exit 0
|
|
294
|
+
fi
|
|
290
295
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
291
296
|
echo "Release version: ${VERSION}"
|
|
292
297
|
|
|
293
298
|
- name: Check release exists (no creation — deferred to upload-evidence)
|
|
294
|
-
if: env.DEVAUDIT_BASE_URL != ''
|
|
299
|
+
if: env.DEVAUDIT_BASE_URL != '' && steps.version.outputs.version != 'skip'
|
|
295
300
|
run: |
|
|
296
301
|
# Release creation is deferred to the upload-evidence job (which
|
|
297
302
|
# runs after gates pass) so the portal never shows a release from
|
|
@@ -306,7 +311,7 @@ jobs:
|
|
|
306
311
|
--git-sha ${{ github.sha }} --branch ${{ github.ref_name }} || true
|
|
307
312
|
|
|
308
313
|
- name: Generate and upload bundled changes (REQ releases only)
|
|
309
|
-
if: env.DEVAUDIT_BASE_URL != ''
|
|
314
|
+
if: env.DEVAUDIT_BASE_URL != '' && steps.version.outputs.version != 'skip'
|
|
310
315
|
run: |
|
|
311
316
|
VERSION="${{ steps.version.outputs.version }}"
|
|
312
317
|
# Only generate bundled changes for REQ-tagged releases —
|
|
@@ -349,32 +354,20 @@ jobs:
|
|
|
349
354
|
run: |
|
|
350
355
|
if [ ! -f "compliance/RTM.md" ]; then exit 0; fi
|
|
351
356
|
|
|
352
|
-
# Per REQ row in RTM we look up: title (release
|
|
353
|
-
#
|
|
354
|
-
# MEDIUM / HIGH / CRITICAL token in the
|
|
355
|
-
# the portal renders gracefully with nulls.
|
|
356
|
-
# accepts either a bare string[] (legacy) or rich
|
|
357
|
+
# Per REQ row in RTM we look up: title (shared release metadata
|
|
358
|
+
# helper: **Requirement:** line, issue title, then H1 fallback), and
|
|
359
|
+
# risk_class (the first LOW / MEDIUM / HIGH / CRITICAL token in the
|
|
360
|
+
# row). Both are optional — the portal renders gracefully with nulls.
|
|
361
|
+
# The portal endpoint accepts either a bare string[] (legacy) or rich
|
|
362
|
+
# rows.
|
|
357
363
|
REQS_JSON='[]'
|
|
358
364
|
while IFS= read -r REQ; do
|
|
359
365
|
[ -z "$REQ" ] && continue
|
|
360
366
|
ROW=$(grep -m1 -E "^\| ${REQ} " compliance/RTM.md || true)
|
|
361
367
|
TITLE=""
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if [ -f "$FILE" ]; then
|
|
366
|
-
TITLE=$(grep -m1 '^# ' "$FILE" \
|
|
367
|
-
| sed -E 's/^# *(REQ-[0-9]+)?[[:space:]]*[—:-]?[[:space:]]*//')
|
|
368
|
-
break
|
|
369
|
-
fi
|
|
370
|
-
done
|
|
371
|
-
# Title fallback: first issue # found anywhere in the row
|
|
372
|
-
if [ -z "$TITLE" ] && [ -n "$ROW" ]; then
|
|
373
|
-
ISSUE_NUM=$(echo "$ROW" | grep -oE '#[0-9]+' | head -1 | tr -d '#')
|
|
374
|
-
if [ -n "$ISSUE_NUM" ]; then
|
|
375
|
-
TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq .title 2>/dev/null || true)
|
|
376
|
-
fi
|
|
377
|
-
fi
|
|
368
|
+
source scripts/extract-release-metadata.sh 2>/dev/null || true
|
|
369
|
+
extract_release_metadata "$REQ" 2>/dev/null || true
|
|
370
|
+
TITLE="$RELEASE_TITLE"
|
|
378
371
|
# Risk: first LOW/MEDIUM/HIGH/CRITICAL token in the row
|
|
379
372
|
RISK=""
|
|
380
373
|
if [ -n "$ROW" ]; then
|
|
@@ -680,18 +673,16 @@ jobs:
|
|
|
680
673
|
SHOT_TMP="$(mktemp -d)"
|
|
681
674
|
for REQ in "${SHOT_REQS[@]}"; do
|
|
682
675
|
# Per-REQ release metadata for the portal (no-clobbered on existing rows):
|
|
683
|
-
#
|
|
684
|
-
#
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
REQ_TITLE=$(grep -m1 '^# ' "compliance/pending-releases/RELEASE-TICKET-${REQ}.md" \
|
|
688
|
-
| sed -E 's/^# *(REQ-[0-9]+)?[[:space:]]*[—:-]?[[:space:]]*//')
|
|
689
|
-
fi
|
|
676
|
+
# Uses the shared extraction helper so both workflow paths produce
|
|
677
|
+
# the same title/summary for the same REQ. DevAudit-Installer#285.
|
|
678
|
+
source scripts/extract-release-metadata.sh 2>/dev/null || true
|
|
679
|
+
extract_release_metadata "$REQ" 2>/dev/null || true
|
|
690
680
|
REQ_CT=$(git log --grep "\[${REQ}\]\|Ref: ${REQ}" --pretty=%s -1 2>/dev/null \
|
|
691
681
|
| grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
|
|
692
682
|
| head -1 || true)
|
|
693
683
|
REQ_META=()
|
|
694
|
-
[ -n "$
|
|
684
|
+
[ -n "$RELEASE_TITLE" ] && REQ_META+=(--release-title "$RELEASE_TITLE")
|
|
685
|
+
[ -n "$RELEASE_SUMMARY" ] && REQ_META+=(--release-summary "$RELEASE_SUMMARY")
|
|
695
686
|
[ -n "$REQ_CT" ] && REQ_META+=(--change-type "$REQ_CT")
|
|
696
687
|
# devaudit-installer#147 — per-REQ scoped glob. ONLY this REQ's
|
|
697
688
|
# own screenshots/ directory; legacy folders for REQs not in
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Release close-out — reconcile the local compliance tree
|
|
2
|
-
# marked `released` on the DevAudit portal.
|
|
1
|
+
# Release close-out — reconcile the local compliance tree and sync main back to
|
|
2
|
+
# develop after a release is marked `released` on the DevAudit portal.
|
|
3
3
|
#
|
|
4
4
|
# Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
|
|
5
5
|
# Do not edit manually — re-run the CLI (`devaudit update`) to regenerate.
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
# release_pr }.
|
|
11
11
|
# - workflow_dispatch — manual catch-up for an already-released requirement.
|
|
12
12
|
#
|
|
13
|
-
# It
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# It merges origin/main into a branch from develop (devaudit#281), runs
|
|
14
|
+
# scripts/close-out-release.sh for the requirement (flip ticket + RTM, move
|
|
15
|
+
# ticket to approved-releases/), then opens one PR to develop containing both
|
|
16
|
+
# the main→develop sync and the compliance-tree close-out. The script is
|
|
17
|
+
# idempotent, so a no-op produces no PR.
|
|
16
18
|
|
|
17
19
|
name: Release Close-out
|
|
18
20
|
|
|
@@ -60,6 +62,25 @@ jobs:
|
|
|
60
62
|
BASE=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
|
|
61
63
|
echo "DEVAUDIT_BASE_URL=${BASE%/}" >> "$GITHUB_ENV"
|
|
62
64
|
|
|
65
|
+
- name: Merge main into develop branch
|
|
66
|
+
id: merge
|
|
67
|
+
run: |
|
|
68
|
+
# devaudit#281 — merge origin/main back into develop so the
|
|
69
|
+
# reconciliation PR carries both the released code and the
|
|
70
|
+
# compliance-tree close-out. Uses a non-interactive merge.
|
|
71
|
+
git config user.name "devaudit-bot"
|
|
72
|
+
git config user.email "devaudit-bot@users.noreply.github.com"
|
|
73
|
+
git fetch origin main
|
|
74
|
+
# Check if develop already contains main (nothing to merge)
|
|
75
|
+
if git merge-base --is-ancestor origin/main develop; then
|
|
76
|
+
echo "develop already contains origin/main — no merge needed."
|
|
77
|
+
echo "merged=false" >> "$GITHUB_OUTPUT"
|
|
78
|
+
else
|
|
79
|
+
git merge origin/main --no-edit --no-ff
|
|
80
|
+
echo "merged=true" >> "$GITHUB_OUTPUT"
|
|
81
|
+
echo "Merged origin/main into develop branch."
|
|
82
|
+
fi
|
|
83
|
+
|
|
63
84
|
- name: Run close-out
|
|
64
85
|
id: closeout
|
|
65
86
|
run: |
|
|
@@ -74,23 +95,58 @@ jobs:
|
|
|
74
95
|
echo "Nothing to close out (idempotent no-op) — no PR opened."
|
|
75
96
|
fi
|
|
76
97
|
|
|
98
|
+
- name: Close existing sync PRs for same REQ
|
|
99
|
+
if: steps.closeout.outputs.changed == 'true' || steps.merge.outputs.merged == 'true'
|
|
100
|
+
run: |
|
|
101
|
+
# devaudit#281 — if an older same-REQ sync PR is already open,
|
|
102
|
+
# close it as superseded instead of leaving duplicate sync PRs.
|
|
103
|
+
REQ="${{ steps.in.outputs.req }}"
|
|
104
|
+
# Look for open PRs targeting develop that mention this REQ in the title
|
|
105
|
+
EXISTING_PRS=$(gh pr list --base develop --state open --json number,title --jq '.[] | select(.title | test("'"${REQ}"'")) | .number' 2>/dev/null || true)
|
|
106
|
+
for PR_NUM in $EXISTING_PRS; do
|
|
107
|
+
echo "Closing existing PR #${PR_NUM} as superseded by automated close-out for ${REQ}"
|
|
108
|
+
gh pr close ${PR_NUM} --comment "Superseded by automated release close-out for ${REQ}. This PR is no longer needed — the reconciliation is handled by the Release Close-out workflow." 2>/dev/null || true
|
|
109
|
+
done
|
|
110
|
+
|
|
77
111
|
- name: Open close-out PR
|
|
78
|
-
if: steps.closeout.outputs.changed == 'true'
|
|
112
|
+
if: steps.closeout.outputs.changed == 'true' || steps.merge.outputs.merged == 'true'
|
|
79
113
|
run: |
|
|
80
114
|
REQ="${{ steps.in.outputs.req }}"
|
|
81
|
-
|
|
115
|
+
# Source the shared close-out contract for the branch prefix and marker
|
|
116
|
+
source scripts/close-out-contract.sh 2>/dev/null || true
|
|
117
|
+
BRANCH="${CLOSEOUT_BRANCH_PREFIX}${REQ}"
|
|
118
|
+
# Fallback if contract script not available
|
|
119
|
+
[ -z "$BRANCH" ] && BRANCH="chore/close-out-${REQ}"
|
|
82
120
|
git config user.name "devaudit-bot"
|
|
83
121
|
git config user.email "devaudit-bot@users.noreply.github.com"
|
|
84
122
|
git checkout -b "$BRANCH"
|
|
85
123
|
git add -A
|
|
86
|
-
|
|
124
|
+
# Build the close-out marker
|
|
125
|
+
MARKER=$(closeout_marker "${REQ}" 2>/dev/null || echo "Release-Closeout: ${REQ}")
|
|
126
|
+
git commit -m "docs(compliance): close out ${REQ} release — sync main + reconcile (RELEASED)
|
|
127
|
+
|
|
128
|
+
${MARKER}
|
|
87
129
|
|
|
88
130
|
Automated reconciliation after the DevAudit portal marked ${REQ} released.
|
|
89
|
-
|
|
131
|
+
Includes:
|
|
132
|
+
- main → develop sync
|
|
133
|
+
- release ticket moved to approved-releases/
|
|
134
|
+
- release ticket status → RELEASED
|
|
135
|
+
- RTM row → RELEASED
|
|
90
136
|
|
|
91
137
|
Ref: ${REQ}"
|
|
92
138
|
git push -u origin "$BRANCH" --force-with-lease
|
|
139
|
+
PR_BODY="Automated close-out — the DevAudit portal marked **${REQ}** \`released\`.
|
|
140
|
+
|
|
141
|
+
${MARKER}
|
|
142
|
+
|
|
143
|
+
This PR includes:
|
|
144
|
+
- **main → develop reconciliation** — merges released code back into develop
|
|
145
|
+
- **release ticket** moved to \`approved-releases/\`, status \`RELEASED\`
|
|
146
|
+
- **RTM row** flipped to \`RELEASED\`
|
|
147
|
+
|
|
148
|
+
Review + merge to develop."
|
|
93
149
|
gh pr create --base develop --head "$BRANCH" \
|
|
94
|
-
--title "docs(compliance): close out ${REQ}
|
|
95
|
-
--body "
|
|
150
|
+
--title "docs(compliance): close out ${REQ} — sync main + reconcile (RELEASED)" \
|
|
151
|
+
--body "$PR_BODY" \
|
|
96
152
|
|| echo "::warning::PR may already exist for ${BRANCH}."
|
|
@@ -129,6 +129,11 @@ jobs:
|
|
|
129
129
|
# code or docs. See DevAudit #310.
|
|
130
130
|
chmod +x scripts/derive-release-version.sh 2>/dev/null || true
|
|
131
131
|
VERSION=$(./scripts/derive-release-version.sh)
|
|
132
|
+
if [ -z "$VERSION" ]; then
|
|
133
|
+
echo "version=skip" >> "$GITHUB_OUTPUT"
|
|
134
|
+
echo "Compliance evidence upload skipped: close-out reconciliation detected."
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
132
137
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
133
138
|
echo "Release version: ${VERSION}"
|
|
134
139
|
|
|
@@ -143,10 +148,17 @@ jobs:
|
|
|
143
148
|
# merged, the next compliance-evidence.yml run uploads them as
|
|
144
149
|
# evidence and the matrix flips both items to ✓.
|
|
145
150
|
#
|
|
151
|
+
# devaudit#284 — release reconciliation merges (from close-out-release.yml)
|
|
152
|
+
# carry a `Release-Closeout: REQ-XXX` marker in the commit body.
|
|
153
|
+
# derive-release-version.sh detects this marker and emits an empty
|
|
154
|
+
# version, which this workflow converts to the explicit `skip` sentinel.
|
|
155
|
+
# Reconciliation pushes therefore never create housekeeping stubs and
|
|
156
|
+
# never attach fresh evidence to an already released tracked record.
|
|
157
|
+
#
|
|
146
158
|
# Mirrors the `incident-export.yml` pattern: auto-PR, operator fills
|
|
147
159
|
# in sign-off, merge to land.
|
|
148
160
|
- name: Auto-generate housekeeping stubs (if needed)
|
|
149
|
-
if: steps.resolve.outputs.skip != 'true'
|
|
161
|
+
if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
|
|
150
162
|
env:
|
|
151
163
|
# Use the workflow's GITHUB_TOKEN directly. The `permissions:`
|
|
152
164
|
# block on the job grants `contents: write` + `pull-requests:
|
|
@@ -226,7 +238,7 @@ jobs:
|
|
|
226
238
|
fi
|
|
227
239
|
|
|
228
240
|
- name: Upload compliance documents
|
|
229
|
-
if: steps.resolve.outputs.skip != 'true'
|
|
241
|
+
if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
|
|
230
242
|
run: |
|
|
231
243
|
chmod +x scripts/upload-evidence.sh 2>/dev/null || true
|
|
232
244
|
# Common flags WITHOUT --release. Each upload appends its OWN
|
|
@@ -428,19 +440,17 @@ jobs:
|
|
|
428
440
|
fi
|
|
429
441
|
rm -f "$AUDIT_LOG_FILE"
|
|
430
442
|
|
|
431
|
-
# Helper: emit
|
|
432
|
-
# REQ, derived from its
|
|
433
|
-
# commit attributed to that REQ. Empty pair when
|
|
443
|
+
# Helper: emit `--release-title …` `--release-summary …` `--change-type …`
|
|
444
|
+
# args for a given REQ, derived from its release-ticket metadata and
|
|
445
|
+
# the most recent commit attributed to that REQ. Empty pair when
|
|
446
|
+
# neither is available. Uses the shared extraction helper so both
|
|
447
|
+
# workflow paths produce the same title/summary for the same REQ.
|
|
448
|
+
# DevAudit-Installer#285.
|
|
434
449
|
req_meta_args() {
|
|
435
|
-
local REQ="$1"; local
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
TITLE=$(grep -m1 '^# ' "$FILE" \
|
|
440
|
-
| sed -E 's/^# *(REQ-[0-9]+)?[[:space:]]*[—:-]?[[:space:]]*//')
|
|
441
|
-
break
|
|
442
|
-
fi
|
|
443
|
-
done
|
|
450
|
+
local REQ="$1"; local CT=""
|
|
451
|
+
# Source the shared helper and extract canonical title + summary
|
|
452
|
+
source scripts/extract-release-metadata.sh 2>/dev/null || true
|
|
453
|
+
extract_release_metadata "$REQ" 2>/dev/null || true
|
|
444
454
|
CT=$(git log --grep "\[${REQ}\]\|Ref: ${REQ}" --pretty=%s -1 2>/dev/null \
|
|
445
455
|
| grep -oE '^(feat|fix|refactor|perf|chore|docs|ci|build|test|compliance|revert)' \
|
|
446
456
|
| head -1 || true)
|
|
@@ -448,7 +458,8 @@ jobs:
|
|
|
448
458
|
# trailing `[ … ] && cmd` whose test fails returns 1, and the
|
|
449
459
|
# function inherits that exit code; calling via $(req_meta_args …)
|
|
450
460
|
# then aborts the step silently (DevAudit-Installer#77).
|
|
451
|
-
if [ -n "$
|
|
461
|
+
if [ -n "$RELEASE_TITLE" ]; then printf -- '--release-title %q ' "$RELEASE_TITLE"; fi
|
|
462
|
+
if [ -n "$RELEASE_SUMMARY" ]; then printf -- '--release-summary %q ' "$RELEASE_SUMMARY"; fi
|
|
452
463
|
if [ -n "$CT" ]; then printf -- '--change-type %q ' "$CT"; fi
|
|
453
464
|
}
|
|
454
465
|
|
|
@@ -680,11 +691,16 @@ jobs:
|
|
|
680
691
|
run: |
|
|
681
692
|
chmod +x scripts/derive-release-version.sh 2>/dev/null || true
|
|
682
693
|
VERSION=$(./scripts/derive-release-version.sh)
|
|
694
|
+
if [ -z "$VERSION" ]; then
|
|
695
|
+
echo "version=skip" >> "$GITHUB_OUTPUT"
|
|
696
|
+
echo "E2E evidence upload skipped: close-out reconciliation detected."
|
|
697
|
+
exit 0
|
|
698
|
+
fi
|
|
683
699
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
684
700
|
echo "Resolved release version: ${VERSION}"
|
|
685
701
|
|
|
686
702
|
- name: Upload E2E Regression evidence to DevAudit
|
|
687
|
-
if: steps.resolve.outputs.skip != 'true'
|
|
703
|
+
if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
|
|
688
704
|
run: |
|
|
689
705
|
set -euo pipefail
|
|
690
706
|
DERIVED_RELEASE="${{ steps.version.outputs.version }}"
|