@metasession.co/devaudit-cli 0.3.11 → 0.3.13

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.
Files changed (68) hide show
  1. package/dist/index.js +94 -6
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/upload-evidence.sh +55 -1
  5. package/sdlc/ai-rules/SDLC_RULES.md +27 -7
  6. package/sdlc/files/_common/0-project-setup.md +13 -7
  7. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
  8. package/sdlc/files/_common/README_TEMPLATE.md +8 -8
  9. package/sdlc/files/_common/Test_Architecture.md +15 -15
  10. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
  11. package/sdlc/files/_common/Test_Policy.md +2 -2
  12. package/sdlc/files/_common/Test_Strategy.md +10 -10
  13. package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
  14. package/sdlc/files/_common/governance/dpia.md.template +3 -3
  15. package/sdlc/files/_common/governance/incident-report.md.template +3 -3
  16. package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
  17. package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
  18. package/sdlc/files/_common/governance/risk-register.md.template +1 -1
  19. package/sdlc/files/_common/governance/ropa.md.template +2 -2
  20. package/sdlc/files/_common/joining-an-existing-project.md +4 -4
  21. package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
  22. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
  23. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
  24. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
  25. package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
  26. package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
  27. package/sdlc/files/_common/scripts/extract-release-metadata.sh +65 -0
  28. package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +38 -0
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +307 -58
  30. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -78
  31. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
  32. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
  33. package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
  34. package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
  35. package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
  36. package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
  37. package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
  38. package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
  39. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  40. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  41. package/sdlc/files/_common/scripts/upload-evidence.sh +55 -1
  42. package/sdlc/files/_common/scripts/upload-evidence.test.sh +119 -0
  43. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +169 -2
  44. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +169 -0
  45. package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
  46. package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
  47. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
  48. package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
  49. package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
  50. package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
  51. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +57 -3
  52. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
  53. package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
  54. package/sdlc/files/ci/check-release-approval.yml.template +18 -28
  55. package/sdlc/files/ci/ci.yml.template +95 -35
  56. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  57. package/sdlc/files/ci/compliance-evidence.yml.template +234 -49
  58. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  59. package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
  60. package/sdlc/files/ci/python/ci.yml.template +60 -12
  61. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  62. package/sdlc/package.json +1 -1
  63. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  64. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  65. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  66. package/sdlc/src/blueprints/3-compile-evidence.raw.md +102 -20
  67. package/sdlc/src/blueprints/4-submit-for-review.raw.md +3 -3
  68. package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bash
2
+ # submit-bundle-manifest.sh — resolve a release row, then POST its bundle manifest.
3
+ #
4
+ # Usage:
5
+ # ./scripts/submit-bundle-manifest.sh <project-slug> <release-version> <manifest-path> [--reconcile-existing-ownership]
6
+
7
+ set -euo pipefail
8
+
9
+ PROJECT_SLUG="${1:-}"
10
+ RELEASE_VERSION="${2:-}"
11
+ MANIFEST_PATH="${3:-}"
12
+ shift $(( $# >= 3 ? 3 : $# ))
13
+
14
+ RECONCILE_EXISTING_OWNERSHIP=false
15
+ while [ "$#" -gt 0 ]; do
16
+ case "$1" in
17
+ --reconcile-existing-ownership)
18
+ RECONCILE_EXISTING_OWNERSHIP=true
19
+ shift
20
+ ;;
21
+ *)
22
+ echo "Unknown argument: $1" >&2
23
+ exit 2
24
+ ;;
25
+ esac
26
+ done
27
+
28
+ if [ -z "$PROJECT_SLUG" ] || [ -z "$RELEASE_VERSION" ] || [ -z "$MANIFEST_PATH" ]; then
29
+ echo "Usage: $0 <project-slug> <release-version> <manifest-path> [--reconcile-existing-ownership]" >&2
30
+ exit 1
31
+ fi
32
+
33
+ : "${DEVAUDIT_BASE_URL:?DEVAUDIT_BASE_URL must be set}"
34
+ : "${DEVAUDIT_API_KEY:?DEVAUDIT_API_KEY must be set}"
35
+
36
+ if [ ! -f "$MANIFEST_PATH" ]; then
37
+ echo "Error: manifest file not found: $MANIFEST_PATH" >&2
38
+ exit 1
39
+ fi
40
+
41
+ if ! command -v jq >/dev/null 2>&1; then
42
+ echo "Error: jq is required." >&2
43
+ exit 1
44
+ fi
45
+
46
+ BASE_URL="${DEVAUDIT_BASE_URL%/}"
47
+
48
+ SCHEMA_VERSION="$(jq -r '.schemaVersion // empty' "$MANIFEST_PATH")"
49
+ if [ "$SCHEMA_VERSION" != "1" ]; then
50
+ echo "Error: manifest schemaVersion must be 1." >&2
51
+ exit 1
52
+ fi
53
+
54
+ MEMBER_COUNT="$(jq -r '(.members // []) | length' "$MANIFEST_PATH")"
55
+ WORK_ITEM_COUNT="$(jq -r '(.nonReleaseWorkItems // []) | length' "$MANIFEST_PATH")"
56
+ if [ "$MEMBER_COUNT" = "0" ] && [ "$WORK_ITEM_COUNT" = "0" ]; then
57
+ echo "Bundle manifest has no members or non-release work items; skipping submission."
58
+ exit 0
59
+ fi
60
+
61
+ RESOLVE_URL="${BASE_URL}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${RELEASE_VERSION}"
62
+ RESOLVE_RESPONSE="$(curl -sS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" "$RESOLVE_URL")"
63
+ RELEASE_ID="$(printf '%s' "$RESOLVE_RESPONSE" | jq -r '.latest.id // empty')"
64
+ RESOLVED_VERSION="$(printf '%s' "$RESOLVE_RESPONSE" | jq -r '.latest.version // empty')"
65
+
66
+ if [ -z "$RELEASE_ID" ]; then
67
+ echo "Error: could not resolve release ${RELEASE_VERSION} for project ${PROJECT_SLUG}." >&2
68
+ exit 1
69
+ fi
70
+ if [ "$RESOLVED_VERSION" != "$RELEASE_VERSION" ]; then
71
+ echo "Error: resolve endpoint returned ${RESOLVED_VERSION} instead of exact release ${RELEASE_VERSION}." >&2
72
+ exit 1
73
+ fi
74
+
75
+ PAYLOAD="$(cat "$MANIFEST_PATH")"
76
+ if [ "$RECONCILE_EXISTING_OWNERSHIP" = "true" ]; then
77
+ PAYLOAD="$(jq '. + { reconcileExistingOwnership: true }' "$MANIFEST_PATH")"
78
+ fi
79
+
80
+ POST_URL="${BASE_URL}/api/ci/releases/${RELEASE_ID}/bundle-manifest"
81
+ HTTP_BODY_FILE="$(mktemp)"
82
+ trap 'rm -f "$HTTP_BODY_FILE"' EXIT
83
+ HTTP_CODE="$(
84
+ curl -sS -o "$HTTP_BODY_FILE" -w "%{http_code}" \
85
+ -X POST \
86
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
87
+ -H "Content-Type: application/json" \
88
+ --data "$PAYLOAD" \
89
+ "$POST_URL"
90
+ )"
91
+
92
+ if [ "$HTTP_CODE" != "201" ]; then
93
+ echo "Error: bundle manifest submission failed with HTTP ${HTTP_CODE}." >&2
94
+ sed 's/^/ /' "$HTTP_BODY_FILE" >&2
95
+ exit 1
96
+ fi
97
+
98
+ echo "Submitted bundle manifest for ${RELEASE_VERSION} to release ${RELEASE_ID}."
99
+ echo " members: ${MEMBER_COUNT}"
100
+ echo " non-release work items: ${WORK_ITEM_COUNT}"
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bash
2
+ # submit-bundle-manifest.test.sh — Focused tests for bundle manifest submission.
3
+
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ HELPER="$SCRIPT_DIR/submit-bundle-manifest.sh"
8
+ [ -x "$HELPER" ] || chmod +x "$HELPER"
9
+
10
+ PASS=0
11
+ FAIL=0
12
+ WORK="$(mktemp -d)"
13
+ trap 'rm -rf "$WORK"' EXIT
14
+
15
+ assert_contains() {
16
+ local desc="$1" needle="$2" file="$3"
17
+ if grep -Fq "$needle" "$file"; then
18
+ echo " PASS: $desc"
19
+ PASS=$((PASS + 1))
20
+ else
21
+ echo " FAIL: $desc"
22
+ echo " missing: $needle"
23
+ echo " file:"
24
+ sed 's/^/ /' "$file"
25
+ FAIL=$((FAIL + 1))
26
+ fi
27
+ }
28
+
29
+ make_fixture() {
30
+ local dir="$1"
31
+ rm -rf "$dir"
32
+ mkdir -p "$dir/bin"
33
+ cd "$dir"
34
+ cat > bin/curl <<'EOF'
35
+ #!/usr/bin/env bash
36
+ set -euo pipefail
37
+ LOG_FILE="${SUBMIT_TEST_LOG:?}"
38
+ URL=""
39
+ BODY_FILE=""
40
+ WRITE_OUT=""
41
+ METHOD="GET"
42
+ BODY=""
43
+ ARGS=("$@")
44
+ for ((i=0; i<${#ARGS[@]}; i++)); do
45
+ arg="${ARGS[$i]}"
46
+ case "$arg" in
47
+ -X) METHOD="${ARGS[$((i+1))]}" ;;
48
+ -o) BODY_FILE="${ARGS[$((i+1))]}" ;;
49
+ -w) WRITE_OUT="${ARGS[$((i+1))]}" ;;
50
+ --data) BODY="${ARGS[$((i+1))]}" ;;
51
+ http://*|https://*) URL="$arg" ;;
52
+ esac
53
+ done
54
+ echo "METHOD:${METHOD}" >> "$LOG_FILE"
55
+ echo "URL:${URL}" >> "$LOG_FILE"
56
+ if [ -n "$BODY" ]; then
57
+ echo "BODY:${BODY}" >> "$LOG_FILE"
58
+ fi
59
+ if [[ "$URL" == *"/api/ci/releases/resolve"* ]]; then
60
+ printf '{"latest":{"id":"rel_123","version":"REQ-042","status":"draft"}}'
61
+ exit 0
62
+ fi
63
+ if [[ "$URL" == *"/api/ci/releases/rel_123/bundle-manifest" ]]; then
64
+ if [ -n "$BODY_FILE" ]; then
65
+ printf '{"ok":true}' > "$BODY_FILE"
66
+ fi
67
+ if [ -n "$WRITE_OUT" ]; then
68
+ printf '201'
69
+ fi
70
+ exit 0
71
+ fi
72
+ printf '{"error":"unexpected url"}'
73
+ exit 1
74
+ EOF
75
+ chmod +x bin/curl
76
+ }
77
+
78
+ echo "=== submit-bundle-manifest.sh tests ==="
79
+
80
+ D1="$WORK/case1"
81
+ make_fixture "$D1"
82
+ cat > "$D1/manifest.json" <<'EOF'
83
+ {
84
+ "schemaVersion": 1,
85
+ "approvalRelease": { "version": "REQ-042" },
86
+ "coreRelease": { "version": "REQ-042" },
87
+ "members": [
88
+ {
89
+ "version": "REQ-041",
90
+ "role": "predecessor",
91
+ "relationship": "superseded"
92
+ }
93
+ ],
94
+ "nonReleaseWorkItems": [
95
+ {
96
+ "kind": "housekeeping_commit",
97
+ "title": "docs: refresh release notes"
98
+ }
99
+ ],
100
+ "manifestHash": "sha256:test"
101
+ }
102
+ EOF
103
+ LOG_FILE="$D1/submit.log"
104
+ touch "$LOG_FILE"
105
+ export PATH="$D1/bin:$PATH"
106
+ export SUBMIT_TEST_LOG="$LOG_FILE"
107
+ export DEVAUDIT_BASE_URL="https://devaudit.example.test"
108
+ export DEVAUDIT_API_KEY="mc_test_dummy"
109
+
110
+ bash "$HELPER" fixture-project REQ-042 "$D1/manifest.json" > "$D1/stdout.log" 2>&1
111
+ assert_contains "resolve endpoint called" "/api/ci/releases/resolve?projectSlug=fixture-project&versionPrefix=REQ-042" "$LOG_FILE"
112
+ assert_contains "bundle endpoint called" "/api/ci/releases/rel_123/bundle-manifest" "$LOG_FILE"
113
+ assert_contains "member payload submitted" "\"version\": \"REQ-041\"" "$LOG_FILE"
114
+
115
+ D2="$WORK/case2"
116
+ make_fixture "$D2"
117
+ cat > "$D2/manifest.json" <<'EOF'
118
+ {
119
+ "schemaVersion": 1,
120
+ "approvalRelease": { "version": "REQ-042" },
121
+ "coreRelease": { "version": "REQ-042" },
122
+ "members": [],
123
+ "nonReleaseWorkItems": []
124
+ }
125
+ EOF
126
+ LOG_FILE="$D2/submit.log"
127
+ touch "$LOG_FILE"
128
+ export PATH="$D2/bin:$PATH"
129
+ export SUBMIT_TEST_LOG="$LOG_FILE"
130
+
131
+ bash "$HELPER" fixture-project REQ-042 "$D2/manifest.json" > "$D2/stdout.log" 2>&1
132
+ assert_contains "empty manifest skips submission" "Bundle manifest has no members or non-release work items; skipping submission." "$D2/stdout.log"
133
+
134
+ echo ""
135
+ echo "=== submit-bundle-manifest.test.sh: $PASS passed, $FAIL failed ==="
136
+ if [ "$FAIL" -gt 0 ]; then
137
+ exit 1
138
+ fi
@@ -47,6 +47,14 @@
47
47
  # portal group evidence by test cycle per
48
48
  # ISO/IEC/IEEE 29119-3. Optional — older
49
49
  # portals ignore the field (no error).
50
+ # --evidence-scope <scope> Evidence ownership scope: release | stage |
51
+ # cycle | approval. Forwarded as
52
+ # `evidenceScope`; optional and ignored by
53
+ # older portals.
54
+ # --test-cycle-record-id <id> First-class portal cycle UUID. Requires
55
+ # `--evidence-scope cycle`. Forwarded as
56
+ # `testCycleRecordId`; preserved alongside
57
+ # legacy `--test-cycle` during dual-write.
50
58
  #
51
59
  # Required environment variables:
52
60
  # DEVAUDIT_BASE_URL e.g. https://meta-comply-production.up.railway.app
@@ -98,6 +106,10 @@ CHANGE_TYPE=""
98
106
  GATE_STATUS=""
99
107
  SDLC_STAGE=""
100
108
  TEST_CYCLE=""
109
+ EVIDENCE_SCOPE=""
110
+ TEST_CYCLE_RECORD_ID=""
111
+ SENTINEL_CONTENT=""
112
+ COMMIT_TIMESTAMP=""
101
113
  # Repeatable `--meta-key key=value` accumulator. Each pair gets merged
102
114
  # into the metadata JSON sent to the portal. Used by the screenshot
103
115
  # upload loop to pass `origin=feature|regression` from the per-PNG
@@ -125,6 +137,8 @@ while [ "$#" -gt 0 ]; do
125
137
  --gate-status) GATE_STATUS="$2"; shift 2 ;;
126
138
  --sdlc-stage) SDLC_STAGE="$2"; shift 2 ;;
127
139
  --test-cycle) TEST_CYCLE="$2"; shift 2 ;;
140
+ --evidence-scope) EVIDENCE_SCOPE="$2"; shift 2 ;;
141
+ --test-cycle-record-id) TEST_CYCLE_RECORD_ID="$2"; shift 2 ;;
128
142
  # --meta-key key=value (repeatable). Merged into the metadata JSON
129
143
  # before posting. Validates the `key=value` shape; rejects bare
130
144
  # keys without `=`.
@@ -152,6 +166,14 @@ if [ -n "$SDLC_STAGE" ] && ! [[ "$SDLC_STAGE" =~ ^[1-5]$ ]]; then
152
166
  echo "Error: --sdlc-stage must be an integer 1-5 (got: $SDLC_STAGE)"
153
167
  exit 1
154
168
  fi
169
+ if [ -n "$EVIDENCE_SCOPE" ] && ! [[ "$EVIDENCE_SCOPE" =~ ^(release|stage|cycle|approval)$ ]]; then
170
+ echo "Error: --evidence-scope must be one of: release, stage, cycle, approval"
171
+ exit 1
172
+ fi
173
+ if [ -n "$TEST_CYCLE_RECORD_ID" ] && [ "$EVIDENCE_SCOPE" != "cycle" ]; then
174
+ echo "Error: --test-cycle-record-id requires --evidence-scope cycle"
175
+ exit 1
176
+ fi
155
177
 
156
178
  if [ -z "${DEVAUDIT_BASE_URL:-}" ]; then
157
179
  echo "Error: DEVAUDIT_BASE_URL environment variable is required"
@@ -202,6 +224,28 @@ probe_base_url_drift() {
202
224
  }
203
225
  probe_base_url_drift
204
226
 
227
+ is_tracked_change_type() {
228
+ case "${1:-}" in
229
+ feat|fix|refactor|perf|compliance|revert) return 0 ;;
230
+ *) return 1 ;;
231
+ esac
232
+ }
233
+
234
+ resolve_sentinel_context() {
235
+ if ! is_tracked_change_type "$CHANGE_TYPE"; then
236
+ return 0
237
+ fi
238
+ if [ -f ".sdlc-implementer-invoked" ]; then
239
+ SENTINEL_CONTENT=$(cat .sdlc-implementer-invoked)
240
+ fi
241
+ local git_target="HEAD"
242
+ if [ -n "$GIT_SHA" ]; then
243
+ git_target="$GIT_SHA"
244
+ fi
245
+ COMMIT_TIMESTAMP=$(git show -s --format=%cI "$git_target" 2>/dev/null || true)
246
+ }
247
+ resolve_sentinel_context
248
+
205
249
  # --- Build metadata JSON ---
206
250
  # Assemble entries first; only emit `{ ... }` if at least one field is
207
251
  # set. Each entry is a `"key":"value"` JSON pair with the value
@@ -213,6 +257,7 @@ META_ENTRIES=()
213
257
  [ -n "$GIT_SHA" ] && META_ENTRIES+=("\"gitSha\":\"$(json_escape "$GIT_SHA")\"")
214
258
  [ -n "$CI_RUN_ID" ] && META_ENTRIES+=("\"ciRunId\":\"$(json_escape "$CI_RUN_ID")\"")
215
259
  [ -n "$BRANCH" ] && META_ENTRIES+=("\"branch\":\"$(json_escape "$BRANCH")\"")
260
+ [ -n "$COMMIT_TIMESTAMP" ] && META_ENTRIES+=("\"commitTimestamp\":\"$(json_escape "$COMMIT_TIMESTAMP")\"")
216
261
  for KV in "${META_KEYS[@]}"; do
217
262
  KEY="${KV%%=*}"
218
263
  VAL="${KV#*=}"
@@ -337,8 +382,13 @@ upload_presigned() {
337
382
  \"evidenceCategory\": \"${EVIDENCE_CATEGORY}\",
338
383
  \"releaseTitle\": \"${RELEASE_TITLE}\",
339
384
  \"releaseSummary\": \"${RELEASE_SUMMARY}\",
385
+ \"changeType\": \"${CHANGE_TYPE}\",
340
386
  \"sdlcStage\": \"${SDLC_STAGE}\",
341
- \"testCycleId\": \"${TEST_CYCLE}\"
387
+ \"testCycleId\": \"${TEST_CYCLE}\",
388
+ \"evidenceScope\": \"${EVIDENCE_SCOPE}\",
389
+ \"testCycleRecordId\": \"${TEST_CYCLE_RECORD_ID}\",
390
+ \"sentinelContent\": $(jq -Rn --arg v "$SENTINEL_CONTENT" '$v'),
391
+ \"commitTimestamp\": \"${COMMIT_TIMESTAMP}\"
342
392
  }") || curl_exit=$?
343
393
  curl_exit=${curl_exit:-0}
344
394
  if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
@@ -503,6 +553,10 @@ for FILE in "${FILES[@]}"; do
503
553
  [ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
504
554
  [ -n "$SDLC_STAGE" ] && CURL_ARGS+=(-F "sdlcStage=${SDLC_STAGE}")
505
555
  [ -n "$TEST_CYCLE" ] && CURL_ARGS+=(-F "testCycleId=${TEST_CYCLE}")
556
+ [ -n "$EVIDENCE_SCOPE" ] && CURL_ARGS+=(-F "evidenceScope=${EVIDENCE_SCOPE}")
557
+ [ -n "$TEST_CYCLE_RECORD_ID" ] && CURL_ARGS+=(-F "testCycleRecordId=${TEST_CYCLE_RECORD_ID}")
558
+ [ -n "$SENTINEL_CONTENT" ] && CURL_ARGS+=(-F "sentinelContent=${SENTINEL_CONTENT}")
559
+ [ -n "$COMMIT_TIMESTAMP" ] && CURL_ARGS+=(-F "commitTimestamp=${COMMIT_TIMESTAMP}")
506
560
 
507
561
  ATTEMPT=1
508
562
  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 " WARNING: Test execution summary missing: compliance/evidence/$REQ/test-execution-summary.md"
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,157 @@ 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
+ BUNDLED_MANIFEST=""
247
+ for MANIFEST_PATTERN in \
248
+ "compliance/pending-releases/BUNDLED-CHANGES-${REQ}.json" \
249
+ "compliance/approved-releases/BUNDLED-CHANGES-${REQ}.json" \
250
+ "compliance/superseded-releases/BUNDLED-CHANGES-${REQ}.json"; do
251
+ BUNDLED_MANIFEST=$(find_first_match "$MANIFEST_PATTERN" || true)
252
+ [ -n "$BUNDLED_MANIFEST" ] && break
253
+ done
254
+ if [ -n "$BUNDLED_MANIFEST" ]; then
255
+ echo " OK: Bundled release manifest present: ${BUNDLED_MANIFEST}"
256
+ else
257
+ echo " ERROR: Bundled release evidence exists but BUNDLED-CHANGES-${REQ}.json is missing"
258
+ EXIT_CODE=1
259
+ fi
260
+ if grep -q '^## Bundled Changes' "$BUNDLED_FILE"; then
261
+ echo " OK: Bundled release evidence has canonical heading"
262
+ else
263
+ echo " ERROR: Bundled release evidence is missing '## Bundled Changes'"
264
+ EXIT_CODE=1
265
+ fi
266
+ if ! require_bundled_fields \
267
+ "$BUNDLED_FILE" \
268
+ "Bundled release evidence" \
269
+ "Core tracked release" \
270
+ "Bundle manifest" \
271
+ "Manifest hash" \
272
+ "Absorbed predecessor releases" \
273
+ "Absorbed non-release work" \
274
+ "Why bundled here" \
275
+ "Evidence impact" \
276
+ "Reviewer impact" \
277
+ "Security / risk impact" \
278
+ "Reference"; then
279
+ EXIT_CODE=1
280
+ else
281
+ echo " OK: Bundled release evidence carries the required structured fields"
282
+ fi
283
+ if [ -n "$TICKET_FILE" ] && grep -qE '^## (Bundled Changes|Absorbed Predecessor Releases)' "$TICKET_FILE"; then
284
+ echo " OK: Release ticket documents bundled release context"
285
+ else
286
+ echo " ERROR: Bundled release evidence exists but the release ticket is missing '## Bundled Changes' or '## Absorbed Predecessor Releases'"
287
+ EXIT_CODE=1
288
+ fi
289
+ if [ -n "$TICKET_FILE" ]; then
290
+ if ! require_bundled_fields \
291
+ "$TICKET_FILE" \
292
+ "Release ticket" \
293
+ "Core tracked release" \
294
+ "Absorbed predecessor releases" \
295
+ "Absorbed non-release work" \
296
+ "Why bundled here" \
297
+ "Evidence impact" \
298
+ "Reviewer impact" \
299
+ "Security / risk impact" \
300
+ "Reference"; then
301
+ EXIT_CODE=1
302
+ else
303
+ echo " OK: Release ticket carries the required bundled fields"
304
+ fi
305
+ fi
306
+ if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/test-execution-summary.md" 2>/dev/null; then
307
+ echo " OK: test-execution-summary.md documents bundled release context"
308
+ else
309
+ echo " ERROR: Bundled release evidence exists but test-execution-summary.md is missing '## Bundled Release Context'"
310
+ EXIT_CODE=1
311
+ fi
312
+ if ! require_bundled_fields \
313
+ "compliance/evidence/$REQ/test-execution-summary.md" \
314
+ "test-execution-summary.md" \
315
+ "Core tracked release" \
316
+ "Absorbed predecessor releases" \
317
+ "Absorbed non-release work" \
318
+ "Why bundled here" \
319
+ "Evidence impact" \
320
+ "Reviewer impact" \
321
+ "Security / risk impact" \
322
+ "Reference"; then
323
+ EXIT_CODE=1
324
+ else
325
+ echo " OK: test-execution-summary.md carries the required bundled fields"
326
+ fi
327
+ if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/security-summary.md" 2>/dev/null; then
328
+ echo " OK: security-summary.md documents bundled release context"
329
+ else
330
+ echo " ERROR: Bundled release evidence exists but security-summary.md is missing '## Bundled Release Context'"
331
+ EXIT_CODE=1
332
+ fi
333
+ if ! require_bundled_fields \
334
+ "compliance/evidence/$REQ/security-summary.md" \
335
+ "security-summary.md" \
336
+ "Core tracked release" \
337
+ "Absorbed predecessor releases" \
338
+ "Absorbed non-release work" \
339
+ "Why bundled here" \
340
+ "Evidence impact" \
341
+ "Reviewer impact" \
342
+ "Security / risk impact" \
343
+ "Reference"; then
344
+ EXIT_CODE=1
345
+ else
346
+ echo " OK: security-summary.md carries the required bundled fields"
347
+ fi
348
+ if [ -f "compliance/evidence/$REQ/ai-use-note.md" ]; then
349
+ if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/ai-use-note.md"; then
350
+ echo " OK: ai-use-note.md documents bundled release context"
351
+ else
352
+ echo " ERROR: Bundled release evidence exists but ai-use-note.md is missing '## Bundled Release Context'"
353
+ EXIT_CODE=1
354
+ fi
355
+ if ! require_bundled_fields \
356
+ "compliance/evidence/$REQ/ai-use-note.md" \
357
+ "ai-use-note.md" \
358
+ "Core tracked release" \
359
+ "Absorbed predecessor releases" \
360
+ "Absorbed non-release work" \
361
+ "Why bundled here" \
362
+ "Evidence impact" \
363
+ "Reviewer impact" \
364
+ "Security / risk impact" \
365
+ "Reference"; then
366
+ EXIT_CODE=1
367
+ else
368
+ echo " OK: ai-use-note.md carries the required bundled fields"
369
+ fi
370
+ fi
371
+ fi
372
+
206
373
  # Check implementation-plan.md for MEDIUM/HIGH risk (check RTM for risk level)
207
374
  if grep "$REQ" compliance/RTM.md 2>/dev/null | grep -qiE 'MEDIUM|HIGH'; then
208
375
  if [ ! -f "compliance/evidence/$REQ/implementation-plan.md" ]; then