@metasession.co/devaudit-cli 0.3.12 → 0.3.14

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 (67) hide show
  1. package/dist/index.js +53 -4
  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/files/_common/0-project-setup.md +10 -4
  6. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
  7. package/sdlc/files/_common/README_TEMPLATE.md +8 -8
  8. package/sdlc/files/_common/Test_Architecture.md +15 -15
  9. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
  10. package/sdlc/files/_common/Test_Policy.md +2 -2
  11. package/sdlc/files/_common/Test_Strategy.md +10 -10
  12. package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
  13. package/sdlc/files/_common/governance/dpia.md.template +3 -3
  14. package/sdlc/files/_common/governance/incident-report.md.template +3 -3
  15. package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
  16. package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
  17. package/sdlc/files/_common/governance/risk-register.md.template +1 -1
  18. package/sdlc/files/_common/governance/ropa.md.template +2 -2
  19. package/sdlc/files/_common/joining-an-existing-project.md +4 -4
  20. package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
  21. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
  22. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
  23. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
  24. package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
  25. package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
  26. package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
  27. package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
  28. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +311 -64
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +133 -82
  30. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
  31. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
  32. package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
  33. package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
  34. package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
  35. package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
  36. package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
  37. package/sdlc/files/_common/scripts/report-release-check.sh +86 -0
  38. package/sdlc/files/_common/scripts/report-release-check.test.sh +97 -0
  39. package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
  40. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  41. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  42. package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
  43. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
  44. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -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 +21 -2
  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/ci.yml.template +119 -28
  55. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  56. package/sdlc/files/ci/compliance-evidence.yml.template +230 -20
  57. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  58. package/sdlc/files/ci/post-deploy-prod.yml.template +189 -6
  59. package/sdlc/files/ci/python/ci.yml.template +60 -12
  60. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  61. package/sdlc/package.json +1 -1
  62. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  63. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  64. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  65. package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
  66. package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
  67. package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env bash
2
+ # report-test-cycle.sh — Producer helper for first-class cycle lifecycle events.
3
+ #
4
+ # Usage:
5
+ # ./scripts/report-test-cycle.sh start|complete [flags...]
6
+ #
7
+ # Required flags:
8
+ # --project-slug <slug>
9
+ # --release <version>
10
+ # --sdlc-stage <1-5>
11
+ # --environment <ci|uat|production>
12
+ # --cycle-kind <kind>
13
+ # --idempotency-key <key>
14
+ #
15
+ # Optional flags:
16
+ # --provider <name> Defaults to github_actions
17
+ # --external-run-id <id>
18
+ # --external-run-attempt <n>
19
+ # --external-job-id <id>
20
+ # --commit-sha <sha>
21
+ # --branch <name>
22
+ # --workflow-name <name>
23
+ # --workflow-url <url>
24
+ # --started-at <iso8601> Defaults to current UTC on `start`
25
+ # --completed-at <iso8601> Defaults to current UTC on `complete`
26
+ # --outcome <value> Terminal outcome for `complete`
27
+ # --outcome-reason <text>
28
+ # --incident-reference <text>
29
+ # --output-file <path> Writes key=value outputs for callers
30
+ #
31
+ # Output keys:
32
+ # cycle_supported=true|false
33
+ # cycle_release_id=<uuid-if-resolved>
34
+ # cycle_release_version=<exact-version-if-resolved>
35
+ # cycle_record_id=<uuid-if-created-or-updated>
36
+ # cycle_idempotency_key=<input-key>
37
+ # cycle_started_at=<iso8601-if-known>
38
+ # cycle_completed_at=<iso8601-if-known>
39
+ # cycle_endpoint=<start|complete|reconcile-if-used>
40
+
41
+ set -euo pipefail
42
+
43
+ usage() {
44
+ echo "Usage: $0 start|complete [flags...]"
45
+ exit 1
46
+ }
47
+
48
+ [ "$#" -ge 1 ] || usage
49
+ MODE="$1"
50
+ shift
51
+
52
+ case "$MODE" in
53
+ start|complete) ;;
54
+ *) usage ;;
55
+ esac
56
+
57
+ PROJECT_SLUG=""
58
+ RELEASE_VERSION=""
59
+ SDLC_STAGE=""
60
+ ENVIRONMENT=""
61
+ CYCLE_KIND=""
62
+ PROVIDER="github_actions"
63
+ EXTERNAL_RUN_ID=""
64
+ EXTERNAL_RUN_ATTEMPT=""
65
+ EXTERNAL_JOB_ID=""
66
+ COMMIT_SHA=""
67
+ BRANCH=""
68
+ WORKFLOW_NAME=""
69
+ WORKFLOW_URL=""
70
+ IDEMPOTENCY_KEY=""
71
+ STARTED_AT=""
72
+ COMPLETED_AT=""
73
+ OUTCOME=""
74
+ OUTCOME_REASON=""
75
+ INCIDENT_REFERENCE=""
76
+ OUTPUT_FILE=""
77
+
78
+ while [ "$#" -gt 0 ]; do
79
+ case "$1" in
80
+ --project-slug) PROJECT_SLUG="$2"; shift 2 ;;
81
+ --release) RELEASE_VERSION="$2"; shift 2 ;;
82
+ --sdlc-stage) SDLC_STAGE="$2"; shift 2 ;;
83
+ --environment) ENVIRONMENT="$2"; shift 2 ;;
84
+ --cycle-kind) CYCLE_KIND="$2"; shift 2 ;;
85
+ --provider) PROVIDER="$2"; shift 2 ;;
86
+ --external-run-id) EXTERNAL_RUN_ID="$2"; shift 2 ;;
87
+ --external-run-attempt) EXTERNAL_RUN_ATTEMPT="$2"; shift 2 ;;
88
+ --external-job-id) EXTERNAL_JOB_ID="$2"; shift 2 ;;
89
+ --commit-sha) COMMIT_SHA="$2"; shift 2 ;;
90
+ --branch) BRANCH="$2"; shift 2 ;;
91
+ --workflow-name) WORKFLOW_NAME="$2"; shift 2 ;;
92
+ --workflow-url) WORKFLOW_URL="$2"; shift 2 ;;
93
+ --idempotency-key) IDEMPOTENCY_KEY="$2"; shift 2 ;;
94
+ --started-at) STARTED_AT="$2"; shift 2 ;;
95
+ --completed-at) COMPLETED_AT="$2"; shift 2 ;;
96
+ --outcome) OUTCOME="$2"; shift 2 ;;
97
+ --outcome-reason) OUTCOME_REASON="$2"; shift 2 ;;
98
+ --incident-reference) INCIDENT_REFERENCE="$2"; shift 2 ;;
99
+ --output-file) OUTPUT_FILE="$2"; shift 2 ;;
100
+ *) echo "Unknown option: $1"; exit 1 ;;
101
+ esac
102
+ done
103
+
104
+ require_arg() {
105
+ local value="$1" name="$2"
106
+ if [ -z "$value" ]; then
107
+ echo "Error: ${name} is required" >&2
108
+ exit 1
109
+ fi
110
+ }
111
+
112
+ require_arg "$PROJECT_SLUG" "--project-slug"
113
+ require_arg "$RELEASE_VERSION" "--release"
114
+ require_arg "$SDLC_STAGE" "--sdlc-stage"
115
+ require_arg "$ENVIRONMENT" "--environment"
116
+ require_arg "$CYCLE_KIND" "--cycle-kind"
117
+ require_arg "$IDEMPOTENCY_KEY" "--idempotency-key"
118
+
119
+ if [ -z "${DEVAUDIT_BASE_URL:-}" ]; then
120
+ echo "Error: DEVAUDIT_BASE_URL environment variable is required" >&2
121
+ exit 1
122
+ fi
123
+ if [ -z "${DEVAUDIT_API_KEY:-}" ]; then
124
+ echo "Error: DEVAUDIT_API_KEY environment variable is required" >&2
125
+ exit 1
126
+ fi
127
+
128
+ if ! [[ "$SDLC_STAGE" =~ ^[1-5]$ ]]; then
129
+ echo "Error: --sdlc-stage must be an integer 1-5 (got: $SDLC_STAGE)" >&2
130
+ exit 1
131
+ fi
132
+
133
+ case "$ENVIRONMENT" in
134
+ ci|uat|production) ;;
135
+ *)
136
+ echo "Error: --environment must be one of ci, uat, production" >&2
137
+ exit 1
138
+ ;;
139
+ esac
140
+
141
+ if [ "$MODE" = "complete" ] && [ -z "$OUTCOME" ]; then
142
+ echo "Error: --outcome is required for complete" >&2
143
+ exit 1
144
+ fi
145
+
146
+ case "$OUTCOME" in
147
+ ""|passed|failed|cancelled|skipped|timed_out|action_required|unknown) ;;
148
+ *)
149
+ echo "Error: --outcome must be a terminal cycle outcome" >&2
150
+ exit 1
151
+ ;;
152
+ esac
153
+
154
+ iso_now() {
155
+ date -u +"%Y-%m-%dT%H:%M:%SZ"
156
+ }
157
+
158
+ [ -n "$STARTED_AT" ] || STARTED_AT="$(iso_now)"
159
+ if [ "$MODE" = "complete" ] && [ -z "$COMPLETED_AT" ]; then
160
+ COMPLETED_AT="$(iso_now)"
161
+ fi
162
+
163
+ DEVAUDIT_BASE_URL="${DEVAUDIT_BASE_URL%/}"
164
+ MAX_TIME_SECONDS=${UPLOAD_MAX_TIME_SECONDS:-120}
165
+ CONNECT_TIMEOUT_SECONDS=${UPLOAD_CONNECT_TIMEOUT_SECONDS:-10}
166
+
167
+ write_output() {
168
+ local key="$1" value="$2"
169
+ if [ -n "$OUTPUT_FILE" ]; then
170
+ printf '%s=%s\n' "$key" "$value" >> "$OUTPUT_FILE"
171
+ fi
172
+ }
173
+
174
+ emit_fallback() {
175
+ local release_id="${1:-}" resolved_version="${2:-}"
176
+ write_output cycle_supported false
177
+ write_output cycle_release_id "$release_id"
178
+ write_output cycle_release_version "$resolved_version"
179
+ write_output cycle_record_id ""
180
+ write_output cycle_idempotency_key "$IDEMPOTENCY_KEY"
181
+ write_output cycle_started_at "$STARTED_AT"
182
+ write_output cycle_completed_at "$COMPLETED_AT"
183
+ write_output cycle_endpoint "$MODE"
184
+ }
185
+
186
+ uri_escape() {
187
+ jq -rn --arg v "$1" '$v|@uri'
188
+ }
189
+
190
+ resolve_release_exact() {
191
+ local encoded_release encoded_slug url body code latest_id latest_version
192
+ encoded_release=$(uri_escape "$RELEASE_VERSION")
193
+ encoded_slug=$(uri_escape "$PROJECT_SLUG")
194
+ url="${DEVAUDIT_BASE_URL}/api/ci/releases/resolve?projectSlug=${encoded_slug}&versionPrefix=${encoded_release}"
195
+ body=$(mktemp)
196
+ code=$(curl -sS -o "$body" -w "%{http_code}" \
197
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
198
+ --connect-timeout "$CONNECT_TIMEOUT_SECONDS" \
199
+ --max-time "$MAX_TIME_SECONDS" \
200
+ "$url")
201
+ if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then
202
+ echo "::warning::Cycle lifecycle skipped: could not resolve release ${RELEASE_VERSION} (HTTP ${code})." >&2
203
+ rm -f "$body"
204
+ emit_fallback "" ""
205
+ return 1
206
+ fi
207
+ latest_id=$(jq -r '.latest.id // empty' "$body" 2>/dev/null || true)
208
+ latest_version=$(jq -r '.latest.version // empty' "$body" 2>/dev/null || true)
209
+ rm -f "$body"
210
+ if [ -z "$latest_id" ] || [ "$latest_version" != "$RELEASE_VERSION" ]; then
211
+ echo "::warning::Cycle lifecycle skipped: release resolve for ${RELEASE_VERSION} was empty or ambiguous (latest=${latest_version:-none})." >&2
212
+ emit_fallback "$latest_id" "$latest_version"
213
+ return 1
214
+ fi
215
+ RELEASE_ID="$latest_id"
216
+ RESOLVED_RELEASE_VERSION="$latest_version"
217
+ write_output cycle_release_id "$RELEASE_ID"
218
+ write_output cycle_release_version "$RESOLVED_RELEASE_VERSION"
219
+ return 0
220
+ }
221
+
222
+ build_payload() {
223
+ local mode="$1"
224
+ if [ -n "$EXTERNAL_RUN_ATTEMPT" ]; then
225
+ EXTERNAL_RUN_ATTEMPT_JSON="$EXTERNAL_RUN_ATTEMPT"
226
+ else
227
+ EXTERNAL_RUN_ATTEMPT_JSON="null"
228
+ fi
229
+ jq -n \
230
+ --argjson schemaVersion 1 \
231
+ --arg mode "$mode" \
232
+ --arg sdlcStage "$SDLC_STAGE" \
233
+ --arg environment "$ENVIRONMENT" \
234
+ --arg cycleKind "$CYCLE_KIND" \
235
+ --arg provider "$PROVIDER" \
236
+ --arg externalRunId "$EXTERNAL_RUN_ID" \
237
+ --argjson externalRunAttempt "$EXTERNAL_RUN_ATTEMPT_JSON" \
238
+ --arg externalJobId "$EXTERNAL_JOB_ID" \
239
+ --arg idempotencyKey "$IDEMPOTENCY_KEY" \
240
+ --arg startedAt "$STARTED_AT" \
241
+ --arg completedAt "$COMPLETED_AT" \
242
+ --arg outcome "$OUTCOME" \
243
+ --arg outcomeReason "$OUTCOME_REASON" \
244
+ --arg commitSha "$COMMIT_SHA" \
245
+ --arg branch "$BRANCH" \
246
+ --arg workflowName "$WORKFLOW_NAME" \
247
+ --arg workflowUrl "$WORKFLOW_URL" \
248
+ --arg incidentReference "$INCIDENT_REFERENCE" \
249
+ '
250
+ {
251
+ schemaVersion: $schemaVersion,
252
+ sdlcStage: ($sdlcStage | tonumber),
253
+ environment: $environment,
254
+ cycleKind: $cycleKind,
255
+ provider: $provider,
256
+ externalRunId: ($externalRunId | if . == "" then null else . end),
257
+ externalRunAttempt: $externalRunAttempt,
258
+ externalJobId: ($externalJobId | if . == "" then null else . end),
259
+ idempotencyKey: $idempotencyKey,
260
+ startedAt: $startedAt,
261
+ commitSha: ($commitSha | if . == "" then null else . end),
262
+ branch: ($branch | if . == "" then null else . end),
263
+ workflowName: ($workflowName | if . == "" then null else . end),
264
+ workflowUrl: ($workflowUrl | if . == "" then null else . end),
265
+ incidentReference: ($incidentReference | if . == "" then null else . end)
266
+ }
267
+ + (if $mode == "start"
268
+ then { outcome: "running" }
269
+ else {
270
+ completedAt: $completedAt,
271
+ outcome: $outcome,
272
+ outcomeReason: ($outcomeReason | if . == "" then null else . end)
273
+ }
274
+ end)
275
+ '
276
+ }
277
+
278
+ post_cycle_event() {
279
+ local endpoint="$1" payload="$2" body code
280
+ body=$(mktemp)
281
+ code=$(curl -sS -o "$body" -w "%{http_code}" \
282
+ -X POST \
283
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
284
+ -H "Content-Type: application/json" \
285
+ --connect-timeout "$CONNECT_TIMEOUT_SECONDS" \
286
+ --max-time "$MAX_TIME_SECONDS" \
287
+ "${DEVAUDIT_BASE_URL}/api/ci/releases/${RELEASE_ID}/cycles/${endpoint}" \
288
+ -d "$payload")
289
+ CYCLE_HTTP_CODE="$code"
290
+ CYCLE_RESPONSE_BODY_FILE="$body"
291
+ }
292
+
293
+ handle_unavailable_endpoint() {
294
+ case "$CYCLE_HTTP_CODE" in
295
+ 404|405|501)
296
+ echo "::warning::Cycle lifecycle endpoint ${MODE} unavailable on this portal (HTTP ${CYCLE_HTTP_CODE}); continuing in legacy testCycleId mode." >&2
297
+ emit_fallback "$RELEASE_ID" "$RESOLVED_RELEASE_VERSION"
298
+ rm -f "$CYCLE_RESPONSE_BODY_FILE"
299
+ return 0
300
+ ;;
301
+ esac
302
+ return 1
303
+ }
304
+
305
+ if ! resolve_release_exact; then
306
+ exit 0
307
+ fi
308
+
309
+ PAYLOAD="$(build_payload "$MODE")"
310
+ ENDPOINT="$MODE"
311
+ post_cycle_event "$ENDPOINT" "$PAYLOAD"
312
+
313
+ if [ "$MODE" = "complete" ] && [ "$CYCLE_HTTP_CODE" -eq 400 ]; then
314
+ if grep -q 'different terminal outcome' "$CYCLE_RESPONSE_BODY_FILE" 2>/dev/null; then
315
+ ENDPOINT="reconcile"
316
+ post_cycle_event "$ENDPOINT" "$PAYLOAD"
317
+ fi
318
+ fi
319
+
320
+ if [ "$CYCLE_HTTP_CODE" -lt 200 ] || [ "$CYCLE_HTTP_CODE" -ge 300 ]; then
321
+ if handle_unavailable_endpoint; then
322
+ exit 0
323
+ fi
324
+ BODY_EXCERPT="$(head -c 500 "$CYCLE_RESPONSE_BODY_FILE" 2>/dev/null || true)"
325
+ rm -f "$CYCLE_RESPONSE_BODY_FILE"
326
+ echo "Error: cycle ${ENDPOINT} failed for ${RELEASE_VERSION} (HTTP ${CYCLE_HTTP_CODE})${BODY_EXCERPT:+ — ${BODY_EXCERPT}}" >&2
327
+ exit 1
328
+ fi
329
+
330
+ CYCLE_RECORD_ID=$(jq -r '.id // empty' "$CYCLE_RESPONSE_BODY_FILE" 2>/dev/null || true)
331
+ rm -f "$CYCLE_RESPONSE_BODY_FILE"
332
+ if [ -z "$CYCLE_RECORD_ID" ]; then
333
+ echo "Error: cycle ${ENDPOINT} succeeded but response did not include an id" >&2
334
+ exit 1
335
+ fi
336
+
337
+ write_output cycle_supported true
338
+ write_output cycle_record_id "$CYCLE_RECORD_ID"
339
+ write_output cycle_idempotency_key "$IDEMPOTENCY_KEY"
340
+ write_output cycle_started_at "$STARTED_AT"
341
+ write_output cycle_completed_at "$COMPLETED_AT"
342
+ write_output cycle_endpoint "$ENDPOINT"
343
+
344
+ echo "Cycle ${ENDPOINT} recorded for ${RELEASE_VERSION}: ${CYCLE_RECORD_ID}"
@@ -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,8 @@ CHANGE_TYPE=""
98
106
  GATE_STATUS=""
99
107
  SDLC_STAGE=""
100
108
  TEST_CYCLE=""
109
+ EVIDENCE_SCOPE=""
110
+ TEST_CYCLE_RECORD_ID=""
101
111
  SENTINEL_CONTENT=""
102
112
  COMMIT_TIMESTAMP=""
103
113
  # Repeatable `--meta-key key=value` accumulator. Each pair gets merged
@@ -127,6 +137,8 @@ while [ "$#" -gt 0 ]; do
127
137
  --gate-status) GATE_STATUS="$2"; shift 2 ;;
128
138
  --sdlc-stage) SDLC_STAGE="$2"; shift 2 ;;
129
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 ;;
130
142
  # --meta-key key=value (repeatable). Merged into the metadata JSON
131
143
  # before posting. Validates the `key=value` shape; rejects bare
132
144
  # keys without `=`.
@@ -154,6 +166,14 @@ if [ -n "$SDLC_STAGE" ] && ! [[ "$SDLC_STAGE" =~ ^[1-5]$ ]]; then
154
166
  echo "Error: --sdlc-stage must be an integer 1-5 (got: $SDLC_STAGE)"
155
167
  exit 1
156
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
157
177
 
158
178
  if [ -z "${DEVAUDIT_BASE_URL:-}" ]; then
159
179
  echo "Error: DEVAUDIT_BASE_URL environment variable is required"
@@ -365,6 +385,8 @@ upload_presigned() {
365
385
  \"changeType\": \"${CHANGE_TYPE}\",
366
386
  \"sdlcStage\": \"${SDLC_STAGE}\",
367
387
  \"testCycleId\": \"${TEST_CYCLE}\",
388
+ \"evidenceScope\": \"${EVIDENCE_SCOPE}\",
389
+ \"testCycleRecordId\": \"${TEST_CYCLE_RECORD_ID}\",
368
390
  \"sentinelContent\": $(jq -Rn --arg v "$SENTINEL_CONTENT" '$v'),
369
391
  \"commitTimestamp\": \"${COMMIT_TIMESTAMP}\"
370
392
  }") || curl_exit=$?
@@ -531,6 +553,8 @@ for FILE in "${FILES[@]}"; do
531
553
  [ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
532
554
  [ -n "$SDLC_STAGE" ] && CURL_ARGS+=(-F "sdlcStage=${SDLC_STAGE}")
533
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}")
534
558
  [ -n "$SENTINEL_CONTENT" ] && CURL_ARGS+=(-F "sentinelContent=${SENTINEL_CONTENT}")
535
559
  [ -n "$COMMIT_TIMESTAMP" ] && CURL_ARGS+=(-F "commitTimestamp=${COMMIT_TIMESTAMP}")
536
560
 
@@ -243,6 +243,20 @@ for REQ in $REQUIREMENTS; do
243
243
  done
244
244
  if [ -n "$BUNDLED_FILE" ]; then
245
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
246
260
  if grep -q '^## Bundled Changes' "$BUNDLED_FILE"; then
247
261
  echo " OK: Bundled release evidence has canonical heading"
248
262
  else
@@ -253,6 +267,8 @@ for REQ in $REQUIREMENTS; do
253
267
  "$BUNDLED_FILE" \
254
268
  "Bundled release evidence" \
255
269
  "Core tracked release" \
270
+ "Bundle manifest" \
271
+ "Manifest hash" \
256
272
  "Absorbed predecessor releases" \
257
273
  "Absorbed non-release work" \
258
274
  "Why bundled here" \