@metasession.co/devaudit-cli 0.3.10 → 0.3.12
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/dist/index.js +110 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/sdlc/HOST_ADAPTER.md +17 -0
- package/sdlc/SKILLS.md +8 -0
- package/sdlc/ai-rules/SDLC_RULES.md +27 -7
- package/sdlc/files/_common/0-project-setup.md +4 -4
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +38 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +26 -0
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +11 -0
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +4 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +31 -1
- package/sdlc/files/_common/scripts/upload-evidence.test.sh +119 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +153 -2
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +155 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +36 -1
- package/sdlc/files/ci/check-release-approval.yml.template +18 -28
- package/sdlc/files/ci/ci-status-fallback.yml.template +4 -0
- package/sdlc/files/ci/ci.yml.template +13 -7
- package/sdlc/files/ci/compliance-evidence.yml.template +30 -29
- package/sdlc/files/hosts/_schema/adapter.schema.json +31 -0
- package/sdlc/files/hosts/railway/adapter.json +8 -1
- package/sdlc/package.json +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +68 -0
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metasession.co/devaudit-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "^0.8.2",
|
|
36
|
-
"@metasession.co/devaudit-plugin-sdk": "^0.3.
|
|
36
|
+
"@metasession.co/devaudit-plugin-sdk": "^0.3.12",
|
|
37
37
|
"ajv": "^8.20.0",
|
|
38
38
|
"commander": "^12.1.0",
|
|
39
39
|
"consola": "^3.2.3",
|
package/sdlc/HOST_ADAPTER.md
CHANGED
|
@@ -76,6 +76,17 @@ Sync may warn if these are absent; it's a soft check (the CI workflow is the har
|
|
|
76
76
|
| `config_keys.optional` | string[] | Keys consumers may define (e.g. an optional database service). |
|
|
77
77
|
| `config_keys.defaults` | object | Fallback values when a key is absent. |
|
|
78
78
|
|
|
79
|
+
### Runtime contract
|
|
80
|
+
|
|
81
|
+
| Field | Type | Purpose |
|
|
82
|
+
| ----------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
|
|
83
|
+
| `runtime_contract.preferred_web_runtime` | string | Operator-facing guidance for the expected production web runtime shape on this host. |
|
|
84
|
+
| `runtime_contract.forbid_typescript_runtime` | boolean | Whether long-lived production web processes on this host should forbid `tsx`/`ts-node` style execution. |
|
|
85
|
+
| `runtime_contract.prefer_standalone_output` | boolean | Whether frameworks with a standalone/minimal runtime output should prefer it on this host. |
|
|
86
|
+
| `runtime_contract.scheduler_placement` | string | Operator-facing guidance for where scheduled/background jobs should run on this host. |
|
|
87
|
+
|
|
88
|
+
This field is not consumed by templates yet. It exists so host-level runtime-efficiency rules are versioned and schema-validated instead of living as tribal knowledge in issues and comments.
|
|
89
|
+
|
|
79
90
|
### Notes
|
|
80
91
|
|
|
81
92
|
| Field | Type | Purpose |
|
|
@@ -103,6 +114,12 @@ See the step-by-step walkthrough: **[docs/adding-a-host.md](../docs/adding-a-hos
|
|
|
103
114
|
"wait_for_deploy": "for i in $(seq 1 30); do flyctl status --app \"${APP_NAME}\" --json | jq -e '.Deployment.Status == \"successful\"' && break; sleep 10; done",
|
|
104
115
|
"required_secrets": ["FLY_API_TOKEN", "DEVAUDIT_API_KEY"],
|
|
105
116
|
"required_env": ["APP_NAME"],
|
|
117
|
+
"runtime_contract": {
|
|
118
|
+
"preferred_web_runtime": "compiled JavaScript or framework standalone output",
|
|
119
|
+
"forbid_typescript_runtime": true,
|
|
120
|
+
"prefer_standalone_output": true,
|
|
121
|
+
"scheduler_placement": "scheduled jobs should run in a separate worker or platform cron, not in the web process"
|
|
122
|
+
},
|
|
106
123
|
"notes": [
|
|
107
124
|
"Production URL is resolved at deploy time by flyctl — no GitHub Secret to maintain.",
|
|
108
125
|
"Deploy step runs `flyctl deploy --remote-only` in the post-deploy workflow; FLY_API_TOKEN is the auth."
|
package/sdlc/SKILLS.md
CHANGED
|
@@ -112,6 +112,14 @@ node scripts/validate-adapter.cjs sdlc/files/_common/skills/<name>/SKILL.md
|
|
|
112
112
|
|
|
113
113
|
These delegations are hard contracts — the orchestrator's SKILL.md fails review if it inlines a specialist skill's procedure. The invocation pattern is documented in [docs/adding-a-skill.md §Orchestrator skills](../docs/adding-a-skill.md#orchestrator-skills-calling-other-skills).
|
|
114
114
|
|
|
115
|
+
Phase 4 is now more than a one-shot handoff note. When a release PR is blocked or waiting, `sdlc-implementer` can hand execution to the bundled watcher:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
node SDLC/bin/devaudit-sdlc.js --watch-pr=<number> --repo <owner/name> --once
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
or, without `--once`, a bounded poll loop that persists retry state in `.sdlc-pr-watch.json`, re-runs likely flaky workflows, and re-runs the Release Approval Gate when the portal is already approved but GitHub has not yet converged. That makes "blocked PR handling" an executable part of the orchestration contract rather than a prose-only reminder.
|
|
122
|
+
|
|
115
123
|
`sdlc-implementer` is **not** used for trivial / housekeeping changes (docs, formatting, dependency bumps, CI tweaks) — those skip the requirement and the ceremony. See [the change-type matrix](../docs/change-workflows.md) and the [trivial-change walkthrough](./files/_common/implementing-an-sdlc-issue.md#trivial-change-walkthrough).
|
|
116
124
|
|
|
117
125
|
### The SoT-alignment skill family
|
|
@@ -412,6 +412,16 @@ When the user says implementation is done, or when all acceptance criteria from
|
|
|
412
412
|
|
|
413
413
|
After gates pass, create `compliance/evidence/REQ-XXX/test-execution-summary.md` documenting gate results, test changes, and coverage against the test plan. Include links to evidence locations in DevAudit.
|
|
414
414
|
|
|
415
|
+
If `compliance/pending-releases/BUNDLED-CHANGES-REQ-XXX.md` exists, add a `## Bundled Release Context` section to `test-execution-summary.md` with this minimum structure:
|
|
416
|
+
- `**Core tracked release:**`
|
|
417
|
+
- `**Absorbed predecessor releases:**`
|
|
418
|
+
- `**Absorbed non-release work:**`
|
|
419
|
+
- `**Why bundled here:**`
|
|
420
|
+
- `**Evidence impact:**`
|
|
421
|
+
- `**Reviewer impact:**`
|
|
422
|
+
- `**Security / risk impact:**`
|
|
423
|
+
- `**Reference:**`
|
|
424
|
+
|
|
415
425
|
Tell the user: **"All gates passed. Let me generate the test execution summary."**
|
|
416
426
|
|
|
417
427
|
### Step 2: Upload binary/JSON evidence to DevAudit
|
|
@@ -457,6 +467,8 @@ Dependency audit: 0 vulnerabilities
|
|
|
457
467
|
Evidence uploaded to DevAudit project: [PROJECT_SLUG]
|
|
458
468
|
```
|
|
459
469
|
|
|
470
|
+
If `BUNDLED-CHANGES-REQ-XXX.md` exists, add the same structured bundled-release fields to `security-summary.md` and to `ai-use-note.md` (when present), and add `## Bundled Changes` or `## Absorbed Predecessor Releases` to the release ticket with the same minimum fields. The GitHub PR body is supporting context, not the primary audit record.
|
|
471
|
+
|
|
460
472
|
Verify these also exist in git:
|
|
461
473
|
- `compliance/evidence/REQ-XXX/test-scope.md` (from planning)
|
|
462
474
|
- `compliance/evidence/REQ-XXX/implementation-plan.md` (MEDIUM/HIGH risk — from implementation plan step)
|
|
@@ -484,9 +496,9 @@ When creating the PR, include:
|
|
|
484
496
|
- A **"Test Changes"** section listing test files added/modified, what they cover, and what's NOT covered
|
|
485
497
|
- A **"Where to Find Test Results"** section pointing reviewers to: CI status icons on commits, automated E2E comment, DevAudit portal link, and compliance evidence files in the PR
|
|
486
498
|
|
|
487
|
-
### Step 6: Commit compliance markdown
|
|
499
|
+
### Step 6: Commit compliance markdown and push immediately
|
|
488
500
|
|
|
489
|
-
Commit
|
|
501
|
+
Commit the compliance markdown and **push immediately**. The compliance-only push is cheap, and it is the authoritative trigger for the Compliance Evidence Upload workflow. Pushing now surfaces destination/configuration failures before you waste time on later review steps.
|
|
490
502
|
|
|
491
503
|
```bash
|
|
492
504
|
# ONLY commit markdown — binary/JSON evidence is in DevAudit
|
|
@@ -508,7 +520,15 @@ Co-Authored-By: [AI tool tag]"
|
|
|
508
520
|
|
|
509
521
|
**NEVER `git add` JSON, TXT, HTML, PNG, or JPG evidence files. They belong in DevAudit.**
|
|
510
522
|
|
|
511
|
-
### Step 7:
|
|
523
|
+
### Step 7: Wait for the Compliance Evidence Upload workflow
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
gh run watch --workflow "Compliance Evidence Upload"
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
If it fails, fix the upload/configuration problem before proceeding. The portal's Test Reports gate stays red until the authoritative `develop` upload lands, even if `test-execution-summary.md` already exists on a feature branch.
|
|
530
|
+
|
|
531
|
+
### Step 8: WAIT CHECKPOINT — UAT Verification (if UAT configured)
|
|
512
532
|
|
|
513
533
|
If the project has a UAT environment that auto-deploys from `develop`, verify the change works on UAT before creating a PR.
|
|
514
534
|
|
|
@@ -547,17 +567,17 @@ Co-Authored-By: [AI tool tag]"
|
|
|
547
567
|
|
|
548
568
|
**If UAT fails:** Fix on `develop`, re-run local gates, push, and repeat. Do NOT create a PR until UAT is green.
|
|
549
569
|
|
|
550
|
-
### Step
|
|
570
|
+
### Step 9: Push any UAT-result follow-up commit
|
|
551
571
|
|
|
552
|
-
|
|
572
|
+
If Step 8 added a UAT verification note to `security-summary.md`, push that follow-up commit now.
|
|
553
573
|
|
|
554
574
|
```bash
|
|
555
575
|
git push origin develop
|
|
556
576
|
```
|
|
557
577
|
|
|
558
|
-
Tell the user: **"UAT verification passed. Compliance
|
|
578
|
+
Tell the user: **"UAT verification passed. Compliance evidence is on the portal. Next step: create a PR from develop to main."**
|
|
559
579
|
|
|
560
|
-
### Step
|
|
580
|
+
### Step 10: Verify release exists in DevAudit
|
|
561
581
|
|
|
562
582
|
CI auto-creates releases when uploading evidence (using `--create-release-if-missing`). After pushing, verify the release appears in DevAudit:
|
|
563
583
|
|
|
@@ -22,11 +22,11 @@ This guide configures a new project so that the five pipeline workflows can run
|
|
|
22
22
|
|
|
23
23
|
## Fast path (recommended): `devaudit install`
|
|
24
24
|
|
|
25
|
-
The DevAudit CLI (`@metasession.co/devaudit-cli`) automates almost all of this guide. It writes `sdlc-config.json`, creates the DevAudit project, issues an API key, sets the GitHub secrets and variables, installs the hook framework, configures branch protection, and syncs the framework templates (CI workflows, hooks, scripts)
|
|
25
|
+
The DevAudit CLI (`@metasession.co/devaudit-cli`) automates almost all of this guide. It writes `sdlc-config.json`, creates the DevAudit project, issues an API key, sets the GitHub secrets and variables, installs the hook framework, configures branch protection, and syncs the framework templates (CI workflows, hooks, scripts). The command starts in seconds; the full operator onboarding flow usually takes about 5-10 minutes.
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
npm install -g @metasession.co/devaudit-cli # requires Node >= 22
|
|
29
|
-
devaudit auth login # paste your PAT (issued at https://devaudit.
|
|
29
|
+
devaudit auth login # paste your PAT (issued at https://devaudit.ai/settings/tokens)
|
|
30
30
|
devaudit install ../path/to/this-project
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -112,8 +112,8 @@ cat > compliance/RTM.md << 'EOF'
|
|
|
112
112
|
|
|
113
113
|
## Part B: Change Request Traceability
|
|
114
114
|
|
|
115
|
-
| REQ-ID | Issue | Risk | Evidence | Status |
|
|
116
|
-
|
|
115
|
+
| REQ-ID | Issue | Risk | Evidence | Status | PR | Reviewer | AI-tool |
|
|
116
|
+
|--------|-------|------|----------|--------|-----|----------|---------|
|
|
117
117
|
EOF
|
|
118
118
|
|
|
119
119
|
# Commit
|
|
@@ -26,6 +26,21 @@
|
|
|
26
26
|
|
|
27
27
|
set -euo pipefail
|
|
28
28
|
|
|
29
|
+
find_bundled_changes_file() {
|
|
30
|
+
local req_id="$1"
|
|
31
|
+
local candidate=""
|
|
32
|
+
for candidate in \
|
|
33
|
+
"compliance/pending-releases/BUNDLED-CHANGES-${req_id}.md" \
|
|
34
|
+
"compliance/approved-releases/BUNDLED-CHANGES-${req_id}.md" \
|
|
35
|
+
"compliance/superseded-releases/BUNDLED-CHANGES-${req_id}.md"; do
|
|
36
|
+
if [ -f "$candidate" ]; then
|
|
37
|
+
printf '%s' "$candidate"
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
extract_release_metadata() {
|
|
30
45
|
local req_id="$1"
|
|
31
46
|
RELEASE_TITLE=""
|
|
@@ -116,4 +131,27 @@ extract_release_metadata() {
|
|
|
116
131
|
;;
|
|
117
132
|
esac
|
|
118
133
|
fi
|
|
134
|
+
|
|
135
|
+
# If the tracked release has a generated bundled-changes artefact,
|
|
136
|
+
# surface that fact in the release summary so the portal release row and
|
|
137
|
+
# workflow metadata don't rely on the GitHub PR body as the only bundle
|
|
138
|
+
# narrative. DevAudit-Installer#344.
|
|
139
|
+
local bundled_file=""
|
|
140
|
+
bundled_file=$(find_bundled_changes_file "$req_id" 2>/dev/null || true)
|
|
141
|
+
if [ -n "$bundled_file" ]; then
|
|
142
|
+
local bundled_note
|
|
143
|
+
bundled_note="Bundled release context: see \`${bundled_file}\`."
|
|
144
|
+
if [ -n "$RELEASE_SUMMARY" ]; then
|
|
145
|
+
case "$RELEASE_SUMMARY" in
|
|
146
|
+
*"Bundled release context:"*) ;;
|
|
147
|
+
*)
|
|
148
|
+
RELEASE_SUMMARY="${RELEASE_SUMMARY}
|
|
149
|
+
|
|
150
|
+
${bundled_note}"
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
153
|
+
else
|
|
154
|
+
RELEASE_SUMMARY="$bundled_note"
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
119
157
|
}
|
|
@@ -358,6 +358,32 @@ assert_eq "Title extracted" "Feature fifteen" "$RELEASE_TITLE"
|
|
|
358
358
|
assert_empty "Whitespace-only summary cleared" "$RELEASE_SUMMARY"
|
|
359
359
|
echo ""
|
|
360
360
|
|
|
361
|
+
# --- Test 17: Bundled changes file appends bundle note to summary ---
|
|
362
|
+
echo "--- Test 17: Bundled changes note appended ---"
|
|
363
|
+
make_fixture "$WORK/test17"
|
|
364
|
+
cat > "compliance/pending-releases/RELEASE-TICKET-REQ-017.md" <<'TICKET'
|
|
365
|
+
# Release Ticket — REQ-017
|
|
366
|
+
|
|
367
|
+
**Requirement:** REQ-017 — Feature seventeen
|
|
368
|
+
|
|
369
|
+
## Summary
|
|
370
|
+
Core tracked change summary.
|
|
371
|
+
TICKET
|
|
372
|
+
cat > "compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md" <<'BUNDLE'
|
|
373
|
+
## Bundled Changes
|
|
374
|
+
|
|
375
|
+
- `abc1234` chore: sync templates
|
|
376
|
+
BUNDLE
|
|
377
|
+
|
|
378
|
+
source "$HELPER"
|
|
379
|
+
extract_release_metadata "REQ-017"
|
|
380
|
+
assert_eq "Bundled note appended to summary" \
|
|
381
|
+
"Core tracked change summary.
|
|
382
|
+
|
|
383
|
+
Bundled release context: see \`compliance/pending-releases/BUNDLED-CHANGES-REQ-017.md\`." \
|
|
384
|
+
"$RELEASE_SUMMARY"
|
|
385
|
+
echo ""
|
|
386
|
+
|
|
361
387
|
echo ""
|
|
362
388
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
363
389
|
exit $FAIL
|
|
@@ -78,6 +78,17 @@ COUNT=$(echo "$BUNDLED" | wc -l | tr -d ' ')
|
|
|
78
78
|
# Generate the markdown summary.
|
|
79
79
|
echo "## Bundled Changes"
|
|
80
80
|
echo ""
|
|
81
|
+
echo "- **Core tracked release:** \`${VERSION}\`"
|
|
82
|
+
echo "- **Absorbed predecessor releases:** None detected automatically"
|
|
83
|
+
echo "- **Absorbed non-release work:** Housekeeping commits since \`${SINCE_REF}\` listed below"
|
|
84
|
+
echo "- **Why bundled here:** These housekeeping commits were not individually released; the next tracked release is the truthful approval record for the full state of develop."
|
|
85
|
+
echo "- **Evidence impact:** Gate evidence for \`${VERSION}\` covers the full state of develop at CI time, including the absorbed housekeeping commits listed below."
|
|
86
|
+
echo "- **Reviewer impact:** Approval scope includes the core tracked change plus the absorbed non-release work listed below."
|
|
87
|
+
echo "- **Security / risk impact:** No additional security/risk impact identified automatically; reviewer must confirm in the canonical release artifacts."
|
|
88
|
+
echo "- **Reference:** commit range \`${SINCE_REF}..HEAD\`"
|
|
89
|
+
echo ""
|
|
90
|
+
echo "### Absorbed Non-Release Work"
|
|
91
|
+
echo ""
|
|
81
92
|
echo "The following ${COUNT} housekeeping commit(s) were absorbed into release \`${VERSION}\` since \`${SINCE_REF}\`:"
|
|
82
93
|
echo ""
|
|
83
94
|
echo "$BUNDLED" | while IFS=$'\t' read -r SHA SUBJECT; do
|
|
@@ -89,6 +89,10 @@ assert_not_contains "feat commit excluded" "feat: add booking widget" "$OUTPUT"
|
|
|
89
89
|
assert_not_contains "fix commit excluded" "fix: resolve null pointer" "$OUTPUT"
|
|
90
90
|
assert_contains "header present" "## Bundled Changes" "$OUTPUT"
|
|
91
91
|
assert_contains "version in header" "REQ-042" "$OUTPUT"
|
|
92
|
+
assert_contains "core tracked release field present" "**Core tracked release:**" "$OUTPUT"
|
|
93
|
+
assert_contains "absorbed non-release work field present" "**Absorbed non-release work:**" "$OUTPUT"
|
|
94
|
+
assert_contains "reviewer impact field present" "**Reviewer impact:**" "$OUTPUT"
|
|
95
|
+
assert_contains "reference field present" "**Reference:**" "$OUTPUT"
|
|
92
96
|
echo
|
|
93
97
|
|
|
94
98
|
# ─── Test 2: No housekeeping commits ────────────────────────────────
|
|
@@ -98,6 +98,8 @@ CHANGE_TYPE=""
|
|
|
98
98
|
GATE_STATUS=""
|
|
99
99
|
SDLC_STAGE=""
|
|
100
100
|
TEST_CYCLE=""
|
|
101
|
+
SENTINEL_CONTENT=""
|
|
102
|
+
COMMIT_TIMESTAMP=""
|
|
101
103
|
# Repeatable `--meta-key key=value` accumulator. Each pair gets merged
|
|
102
104
|
# into the metadata JSON sent to the portal. Used by the screenshot
|
|
103
105
|
# upload loop to pass `origin=feature|regression` from the per-PNG
|
|
@@ -202,6 +204,28 @@ probe_base_url_drift() {
|
|
|
202
204
|
}
|
|
203
205
|
probe_base_url_drift
|
|
204
206
|
|
|
207
|
+
is_tracked_change_type() {
|
|
208
|
+
case "${1:-}" in
|
|
209
|
+
feat|fix|refactor|perf|compliance|revert) return 0 ;;
|
|
210
|
+
*) return 1 ;;
|
|
211
|
+
esac
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
resolve_sentinel_context() {
|
|
215
|
+
if ! is_tracked_change_type "$CHANGE_TYPE"; then
|
|
216
|
+
return 0
|
|
217
|
+
fi
|
|
218
|
+
if [ -f ".sdlc-implementer-invoked" ]; then
|
|
219
|
+
SENTINEL_CONTENT=$(cat .sdlc-implementer-invoked)
|
|
220
|
+
fi
|
|
221
|
+
local git_target="HEAD"
|
|
222
|
+
if [ -n "$GIT_SHA" ]; then
|
|
223
|
+
git_target="$GIT_SHA"
|
|
224
|
+
fi
|
|
225
|
+
COMMIT_TIMESTAMP=$(git show -s --format=%cI "$git_target" 2>/dev/null || true)
|
|
226
|
+
}
|
|
227
|
+
resolve_sentinel_context
|
|
228
|
+
|
|
205
229
|
# --- Build metadata JSON ---
|
|
206
230
|
# Assemble entries first; only emit `{ ... }` if at least one field is
|
|
207
231
|
# set. Each entry is a `"key":"value"` JSON pair with the value
|
|
@@ -213,6 +237,7 @@ META_ENTRIES=()
|
|
|
213
237
|
[ -n "$GIT_SHA" ] && META_ENTRIES+=("\"gitSha\":\"$(json_escape "$GIT_SHA")\"")
|
|
214
238
|
[ -n "$CI_RUN_ID" ] && META_ENTRIES+=("\"ciRunId\":\"$(json_escape "$CI_RUN_ID")\"")
|
|
215
239
|
[ -n "$BRANCH" ] && META_ENTRIES+=("\"branch\":\"$(json_escape "$BRANCH")\"")
|
|
240
|
+
[ -n "$COMMIT_TIMESTAMP" ] && META_ENTRIES+=("\"commitTimestamp\":\"$(json_escape "$COMMIT_TIMESTAMP")\"")
|
|
216
241
|
for KV in "${META_KEYS[@]}"; do
|
|
217
242
|
KEY="${KV%%=*}"
|
|
218
243
|
VAL="${KV#*=}"
|
|
@@ -337,8 +362,11 @@ upload_presigned() {
|
|
|
337
362
|
\"evidenceCategory\": \"${EVIDENCE_CATEGORY}\",
|
|
338
363
|
\"releaseTitle\": \"${RELEASE_TITLE}\",
|
|
339
364
|
\"releaseSummary\": \"${RELEASE_SUMMARY}\",
|
|
365
|
+
\"changeType\": \"${CHANGE_TYPE}\",
|
|
340
366
|
\"sdlcStage\": \"${SDLC_STAGE}\",
|
|
341
|
-
\"testCycleId\": \"${TEST_CYCLE}\"
|
|
367
|
+
\"testCycleId\": \"${TEST_CYCLE}\",
|
|
368
|
+
\"sentinelContent\": $(jq -Rn --arg v "$SENTINEL_CONTENT" '$v'),
|
|
369
|
+
\"commitTimestamp\": \"${COMMIT_TIMESTAMP}\"
|
|
342
370
|
}") || curl_exit=$?
|
|
343
371
|
curl_exit=${curl_exit:-0}
|
|
344
372
|
if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
@@ -503,6 +531,8 @@ for FILE in "${FILES[@]}"; do
|
|
|
503
531
|
[ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
|
|
504
532
|
[ -n "$SDLC_STAGE" ] && CURL_ARGS+=(-F "sdlcStage=${SDLC_STAGE}")
|
|
505
533
|
[ -n "$TEST_CYCLE" ] && CURL_ARGS+=(-F "testCycleId=${TEST_CYCLE}")
|
|
534
|
+
[ -n "$SENTINEL_CONTENT" ] && CURL_ARGS+=(-F "sentinelContent=${SENTINEL_CONTENT}")
|
|
535
|
+
[ -n "$COMMIT_TIMESTAMP" ] && CURL_ARGS+=(-F "commitTimestamp=${COMMIT_TIMESTAMP}")
|
|
506
536
|
|
|
507
537
|
ATTEMPT=1
|
|
508
538
|
BACKOFF=$INITIAL_BACKOFF_SECONDS
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# upload-evidence.test.sh — Focused regression tests for sentinel propagation.
|
|
3
|
+
#
|
|
4
|
+
# Verifies tracked uploads automatically forward `.sdlc-implementer-invoked`
|
|
5
|
+
# content and a commit timestamp to the portal-facing upload contract.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
HELPER="$SCRIPT_DIR/upload-evidence.sh"
|
|
11
|
+
[ -x "$HELPER" ] || chmod +x "$HELPER"
|
|
12
|
+
|
|
13
|
+
PASS=0
|
|
14
|
+
FAIL=0
|
|
15
|
+
|
|
16
|
+
WORK=$(mktemp -d)
|
|
17
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
18
|
+
|
|
19
|
+
assert_contains() {
|
|
20
|
+
local desc="$1" needle="$2" file="$3"
|
|
21
|
+
if grep -Fq "$needle" "$file"; then
|
|
22
|
+
echo " PASS: $desc"
|
|
23
|
+
PASS=$((PASS + 1))
|
|
24
|
+
else
|
|
25
|
+
echo " FAIL: $desc"
|
|
26
|
+
echo " missing: $needle"
|
|
27
|
+
echo " file:"
|
|
28
|
+
sed 's/^/ /' "$file"
|
|
29
|
+
FAIL=$((FAIL + 1))
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
make_fixture() {
|
|
34
|
+
local dir="$1"
|
|
35
|
+
rm -rf "$dir"
|
|
36
|
+
mkdir -p "$dir/bin"
|
|
37
|
+
cd "$dir"
|
|
38
|
+
git init -q --initial-branch=main
|
|
39
|
+
git config user.email "test@example.com"
|
|
40
|
+
git config user.name "test"
|
|
41
|
+
echo "evidence" > evidence.txt
|
|
42
|
+
printf '%s\n' '[{"currentPhase":"3","initializedBy":"skill","status":"active"}]' > .sdlc-implementer-invoked
|
|
43
|
+
git add evidence.txt .sdlc-implementer-invoked
|
|
44
|
+
git commit -q -m "feat: tracked upload fixture"
|
|
45
|
+
|
|
46
|
+
cat > bin/curl <<'EOF'
|
|
47
|
+
#!/usr/bin/env bash
|
|
48
|
+
set -euo pipefail
|
|
49
|
+
LOG_FILE="${UPLOAD_TEST_LOG:?}"
|
|
50
|
+
URL=""
|
|
51
|
+
OUT_FILE=""
|
|
52
|
+
HDR_FILE=""
|
|
53
|
+
WRITE_OUT=""
|
|
54
|
+
ARGS=("$@")
|
|
55
|
+
for ((i=0; i<${#ARGS[@]}; i++)); do
|
|
56
|
+
arg="${ARGS[$i]}"
|
|
57
|
+
case "$arg" in
|
|
58
|
+
-o) OUT_FILE="${ARGS[$((i+1))]}" ;;
|
|
59
|
+
-D) HDR_FILE="${ARGS[$((i+1))]}" ;;
|
|
60
|
+
-w) WRITE_OUT="${ARGS[$((i+1))]}" ;;
|
|
61
|
+
http://*|https://*) URL="$arg" ;;
|
|
62
|
+
-F)
|
|
63
|
+
echo "FORM:${ARGS[$((i+1))]}" >> "$LOG_FILE"
|
|
64
|
+
;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
echo "URL:${URL}" >> "$LOG_FILE"
|
|
68
|
+
if [[ "$URL" == *"/api/health" ]]; then
|
|
69
|
+
printf '200 '
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
if [ -n "$OUT_FILE" ]; then
|
|
73
|
+
printf '{"ok":true}' > "$OUT_FILE"
|
|
74
|
+
fi
|
|
75
|
+
if [ -n "$HDR_FILE" ]; then
|
|
76
|
+
: > "$HDR_FILE"
|
|
77
|
+
fi
|
|
78
|
+
if [ -n "$WRITE_OUT" ]; then
|
|
79
|
+
printf '201'
|
|
80
|
+
fi
|
|
81
|
+
EOF
|
|
82
|
+
chmod +x bin/curl
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
echo "=== upload-evidence.sh tests ==="
|
|
86
|
+
|
|
87
|
+
D1="$WORK/case1"
|
|
88
|
+
make_fixture "$D1"
|
|
89
|
+
LOG_FILE="$D1/upload.log"
|
|
90
|
+
touch "$LOG_FILE"
|
|
91
|
+
export PATH="$D1/bin:$PATH"
|
|
92
|
+
export UPLOAD_TEST_LOG="$LOG_FILE"
|
|
93
|
+
export DEVAUDIT_BASE_URL="https://devaudit.example.test"
|
|
94
|
+
export DEVAUDIT_API_KEY="mc_test_dummy"
|
|
95
|
+
|
|
96
|
+
bash "$HELPER" fixture-project REQ-091 test_report evidence.txt \
|
|
97
|
+
--release v2026.07.13 \
|
|
98
|
+
--environment uat \
|
|
99
|
+
--category test_report \
|
|
100
|
+
--change-type feat > "$D1/stdout.log" 2>&1
|
|
101
|
+
|
|
102
|
+
assert_contains "tracked upload forwards sentinelContent" \
|
|
103
|
+
'FORM:sentinelContent=[{"currentPhase":"3","initializedBy":"skill","status":"active"}]' \
|
|
104
|
+
"$LOG_FILE"
|
|
105
|
+
assert_contains "tracked upload forwards commitTimestamp field" \
|
|
106
|
+
'FORM:commitTimestamp=' \
|
|
107
|
+
"$LOG_FILE"
|
|
108
|
+
assert_contains "tracked upload forwards changeType" \
|
|
109
|
+
'FORM:changeType=feat' \
|
|
110
|
+
"$LOG_FILE"
|
|
111
|
+
assert_contains "tracked upload stamps commitTimestamp into metadata" \
|
|
112
|
+
'FORM:metadata={"commitTimestamp":"' \
|
|
113
|
+
"$LOG_FILE"
|
|
114
|
+
|
|
115
|
+
echo ""
|
|
116
|
+
echo "=== upload-evidence.test.sh: $PASS passed, $FAIL failed ==="
|
|
117
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
@@ -48,6 +48,27 @@ fi
|
|
|
48
48
|
echo "Requirements found in PR commits: $REQUIREMENTS"
|
|
49
49
|
echo ""
|
|
50
50
|
|
|
51
|
+
find_first_match() {
|
|
52
|
+
local pattern="$1"
|
|
53
|
+
local match=""
|
|
54
|
+
match=$(compgen -G "$pattern" | head -1 || true)
|
|
55
|
+
[ -n "$match" ] && printf '%s' "$match"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
require_bundled_fields() {
|
|
59
|
+
local file="$1"
|
|
60
|
+
local label="$2"
|
|
61
|
+
local missing=0
|
|
62
|
+
shift 2
|
|
63
|
+
for field in "$@"; do
|
|
64
|
+
if ! grep -qF "**${field}:**" "$file" 2>/dev/null; then
|
|
65
|
+
echo " ERROR: ${label} is missing bundled field '**${field}:**'"
|
|
66
|
+
missing=1
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
return $missing
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
for REQ in $REQUIREMENTS; do
|
|
52
73
|
echo "--- Checking $REQ ---"
|
|
53
74
|
|
|
@@ -149,9 +170,14 @@ for REQ in $REQUIREMENTS; do
|
|
|
149
170
|
fi
|
|
150
171
|
fi
|
|
151
172
|
|
|
152
|
-
# Check test-execution-summary.md exists
|
|
173
|
+
# Check test-execution-summary.md exists. For tracked REQs this is a
|
|
174
|
+
# hard requirement: the file is the per-release test report that
|
|
175
|
+
# satisfies the portal's Test Reports gate. Housekeeping flows never
|
|
176
|
+
# reach this loop because they have no REQ refs in the validated
|
|
177
|
+
# commits, so the stricter behavior remains scoped to tracked work.
|
|
153
178
|
if [ ! -f "compliance/evidence/$REQ/test-execution-summary.md" ]; then
|
|
154
|
-
echo "
|
|
179
|
+
echo " ERROR: Test execution summary missing: compliance/evidence/$REQ/test-execution-summary.md"
|
|
180
|
+
EXIT_CODE=1
|
|
155
181
|
else
|
|
156
182
|
echo " OK: test-execution-summary.md exists"
|
|
157
183
|
fi
|
|
@@ -193,16 +219,141 @@ for REQ in $REQUIREMENTS; do
|
|
|
193
219
|
compgen -G "$TICKET_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
|
|
194
220
|
compgen -G "$APPROVED_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
|
|
195
221
|
compgen -G "$SUPERSEDED_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
|
|
222
|
+
TICKET_FILE=""
|
|
196
223
|
if [ "$LOCATIONS" -gt 1 ]; then
|
|
197
224
|
echo " ERROR: RELEASE-TICKET-${REQ} exists in more than one release directory (pending/approved/superseded). Remove the stale pending copy — it will break the evidence-completeness gate (#192)."
|
|
198
225
|
EXIT_CODE=1
|
|
199
226
|
elif [ "$LOCATIONS" -eq 1 ]; then
|
|
200
227
|
echo " OK: Release ticket exists"
|
|
228
|
+
TICKET_FILE=$(find_first_match "$TICKET_PATTERN" || true)
|
|
229
|
+
[ -z "$TICKET_FILE" ] && TICKET_FILE=$(find_first_match "$APPROVED_PATTERN" || true)
|
|
230
|
+
[ -z "$TICKET_FILE" ] && TICKET_FILE=$(find_first_match "$SUPERSEDED_PATTERN" || true)
|
|
201
231
|
else
|
|
202
232
|
echo " ERROR: Release ticket missing: compliance/pending-releases/RELEASE-TICKET-${REQ}.md"
|
|
203
233
|
EXIT_CODE=1
|
|
204
234
|
fi
|
|
205
235
|
|
|
236
|
+
BUNDLED_FILE=""
|
|
237
|
+
for BUNDLED_PATTERN in \
|
|
238
|
+
"compliance/pending-releases/BUNDLED-CHANGES-${REQ}.md" \
|
|
239
|
+
"compliance/approved-releases/BUNDLED-CHANGES-${REQ}.md" \
|
|
240
|
+
"compliance/superseded-releases/BUNDLED-CHANGES-${REQ}.md"; do
|
|
241
|
+
BUNDLED_FILE=$(find_first_match "$BUNDLED_PATTERN" || true)
|
|
242
|
+
[ -n "$BUNDLED_FILE" ] && break
|
|
243
|
+
done
|
|
244
|
+
if [ -n "$BUNDLED_FILE" ]; then
|
|
245
|
+
echo " OK: Bundled release evidence present: ${BUNDLED_FILE}"
|
|
246
|
+
if grep -q '^## Bundled Changes' "$BUNDLED_FILE"; then
|
|
247
|
+
echo " OK: Bundled release evidence has canonical heading"
|
|
248
|
+
else
|
|
249
|
+
echo " ERROR: Bundled release evidence is missing '## Bundled Changes'"
|
|
250
|
+
EXIT_CODE=1
|
|
251
|
+
fi
|
|
252
|
+
if ! require_bundled_fields \
|
|
253
|
+
"$BUNDLED_FILE" \
|
|
254
|
+
"Bundled release evidence" \
|
|
255
|
+
"Core tracked release" \
|
|
256
|
+
"Absorbed predecessor releases" \
|
|
257
|
+
"Absorbed non-release work" \
|
|
258
|
+
"Why bundled here" \
|
|
259
|
+
"Evidence impact" \
|
|
260
|
+
"Reviewer impact" \
|
|
261
|
+
"Security / risk impact" \
|
|
262
|
+
"Reference"; then
|
|
263
|
+
EXIT_CODE=1
|
|
264
|
+
else
|
|
265
|
+
echo " OK: Bundled release evidence carries the required structured fields"
|
|
266
|
+
fi
|
|
267
|
+
if [ -n "$TICKET_FILE" ] && grep -qE '^## (Bundled Changes|Absorbed Predecessor Releases)' "$TICKET_FILE"; then
|
|
268
|
+
echo " OK: Release ticket documents bundled release context"
|
|
269
|
+
else
|
|
270
|
+
echo " ERROR: Bundled release evidence exists but the release ticket is missing '## Bundled Changes' or '## Absorbed Predecessor Releases'"
|
|
271
|
+
EXIT_CODE=1
|
|
272
|
+
fi
|
|
273
|
+
if [ -n "$TICKET_FILE" ]; then
|
|
274
|
+
if ! require_bundled_fields \
|
|
275
|
+
"$TICKET_FILE" \
|
|
276
|
+
"Release ticket" \
|
|
277
|
+
"Core tracked release" \
|
|
278
|
+
"Absorbed predecessor releases" \
|
|
279
|
+
"Absorbed non-release work" \
|
|
280
|
+
"Why bundled here" \
|
|
281
|
+
"Evidence impact" \
|
|
282
|
+
"Reviewer impact" \
|
|
283
|
+
"Security / risk impact" \
|
|
284
|
+
"Reference"; then
|
|
285
|
+
EXIT_CODE=1
|
|
286
|
+
else
|
|
287
|
+
echo " OK: Release ticket carries the required bundled fields"
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
290
|
+
if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/test-execution-summary.md" 2>/dev/null; then
|
|
291
|
+
echo " OK: test-execution-summary.md documents bundled release context"
|
|
292
|
+
else
|
|
293
|
+
echo " ERROR: Bundled release evidence exists but test-execution-summary.md is missing '## Bundled Release Context'"
|
|
294
|
+
EXIT_CODE=1
|
|
295
|
+
fi
|
|
296
|
+
if ! require_bundled_fields \
|
|
297
|
+
"compliance/evidence/$REQ/test-execution-summary.md" \
|
|
298
|
+
"test-execution-summary.md" \
|
|
299
|
+
"Core tracked release" \
|
|
300
|
+
"Absorbed predecessor releases" \
|
|
301
|
+
"Absorbed non-release work" \
|
|
302
|
+
"Why bundled here" \
|
|
303
|
+
"Evidence impact" \
|
|
304
|
+
"Reviewer impact" \
|
|
305
|
+
"Security / risk impact" \
|
|
306
|
+
"Reference"; then
|
|
307
|
+
EXIT_CODE=1
|
|
308
|
+
else
|
|
309
|
+
echo " OK: test-execution-summary.md carries the required bundled fields"
|
|
310
|
+
fi
|
|
311
|
+
if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/security-summary.md" 2>/dev/null; then
|
|
312
|
+
echo " OK: security-summary.md documents bundled release context"
|
|
313
|
+
else
|
|
314
|
+
echo " ERROR: Bundled release evidence exists but security-summary.md is missing '## Bundled Release Context'"
|
|
315
|
+
EXIT_CODE=1
|
|
316
|
+
fi
|
|
317
|
+
if ! require_bundled_fields \
|
|
318
|
+
"compliance/evidence/$REQ/security-summary.md" \
|
|
319
|
+
"security-summary.md" \
|
|
320
|
+
"Core tracked release" \
|
|
321
|
+
"Absorbed predecessor releases" \
|
|
322
|
+
"Absorbed non-release work" \
|
|
323
|
+
"Why bundled here" \
|
|
324
|
+
"Evidence impact" \
|
|
325
|
+
"Reviewer impact" \
|
|
326
|
+
"Security / risk impact" \
|
|
327
|
+
"Reference"; then
|
|
328
|
+
EXIT_CODE=1
|
|
329
|
+
else
|
|
330
|
+
echo " OK: security-summary.md carries the required bundled fields"
|
|
331
|
+
fi
|
|
332
|
+
if [ -f "compliance/evidence/$REQ/ai-use-note.md" ]; then
|
|
333
|
+
if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/ai-use-note.md"; then
|
|
334
|
+
echo " OK: ai-use-note.md documents bundled release context"
|
|
335
|
+
else
|
|
336
|
+
echo " ERROR: Bundled release evidence exists but ai-use-note.md is missing '## Bundled Release Context'"
|
|
337
|
+
EXIT_CODE=1
|
|
338
|
+
fi
|
|
339
|
+
if ! require_bundled_fields \
|
|
340
|
+
"compliance/evidence/$REQ/ai-use-note.md" \
|
|
341
|
+
"ai-use-note.md" \
|
|
342
|
+
"Core tracked release" \
|
|
343
|
+
"Absorbed predecessor releases" \
|
|
344
|
+
"Absorbed non-release work" \
|
|
345
|
+
"Why bundled here" \
|
|
346
|
+
"Evidence impact" \
|
|
347
|
+
"Reviewer impact" \
|
|
348
|
+
"Security / risk impact" \
|
|
349
|
+
"Reference"; then
|
|
350
|
+
EXIT_CODE=1
|
|
351
|
+
else
|
|
352
|
+
echo " OK: ai-use-note.md carries the required bundled fields"
|
|
353
|
+
fi
|
|
354
|
+
fi
|
|
355
|
+
fi
|
|
356
|
+
|
|
206
357
|
# Check implementation-plan.md for MEDIUM/HIGH risk (check RTM for risk level)
|
|
207
358
|
if grep "$REQ" compliance/RTM.md 2>/dev/null | grep -qiE 'MEDIUM|HIGH'; then
|
|
208
359
|
if [ ! -f "compliance/evidence/$REQ/implementation-plan.md" ]; then
|