@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.
- package/dist/index.js +53 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +55 -1
- package/sdlc/files/_common/0-project-setup.md +10 -4
- package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
- package/sdlc/files/_common/README_TEMPLATE.md +8 -8
- package/sdlc/files/_common/Test_Architecture.md +15 -15
- package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
- package/sdlc/files/_common/Test_Policy.md +2 -2
- package/sdlc/files/_common/Test_Strategy.md +10 -10
- package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
- package/sdlc/files/_common/governance/dpia.md.template +3 -3
- package/sdlc/files/_common/governance/incident-report.md.template +3 -3
- package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
- package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
- package/sdlc/files/_common/governance/risk-register.md.template +1 -1
- package/sdlc/files/_common/governance/ropa.md.template +2 -2
- package/sdlc/files/_common/joining-an-existing-project.md +4 -4
- package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
- package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
- package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
- package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +311 -64
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +133 -82
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
- package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
- package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
- package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
- package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
- package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
- package/sdlc/files/_common/scripts/report-release-check.sh +86 -0
- package/sdlc/files/_common/scripts/report-release-check.test.sh +97 -0
- package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
- package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
- package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
- package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
- package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
- package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
- package/sdlc/files/ci/ci.yml.template +119 -28
- package/sdlc/files/ci/close-out-release.yml.template +7 -4
- package/sdlc/files/ci/compliance-evidence.yml.template +230 -20
- package/sdlc/files/ci/feature-e2e.yml.template +84 -1
- package/sdlc/files/ci/post-deploy-prod.yml.template +189 -6
- package/sdlc/files/ci/python/ci.yml.template +60 -12
- package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +44 -2
- package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
- package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
- 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" \
|