@metasession.co/devaudit-cli 0.3.13 → 0.3.15
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 +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +11 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +42 -1
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +13 -4
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +5 -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/standalone-housekeeping-release.sh +98 -0
- package/sdlc/files/_common/scripts/standalone-housekeeping-release.test.sh +45 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +9 -13
- package/sdlc/files/ci/ci.yml.template +69 -12
- package/sdlc/files/ci/compliance-evidence.yml.template +45 -49
- package/sdlc/files/ci/post-deploy-prod.yml.template +181 -24
- package/sdlc/package.json +1 -1
- package/sdlc/src/blueprints/5-deploy-main.raw.md +23 -50
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.15",
|
|
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.15",
|
|
37
37
|
"ajv": "^8.20.0",
|
|
38
38
|
"commander": "^12.1.0",
|
|
39
39
|
"consola": "^3.2.3",
|
|
@@ -73,6 +73,17 @@ if [ "$DECLARED_SCOPE" != "$CURRENT_RELEASE" ]; then
|
|
|
73
73
|
exit 1
|
|
74
74
|
fi
|
|
75
75
|
|
|
76
|
+
if [[ "$CURRENT_RELEASE" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
|
|
77
|
+
DECLARATION="compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-${CURRENT_RELEASE}.json"
|
|
78
|
+
if ! printf '%s\n%s\n' "${PR_TITLE:-}" "${PR_BODY:-}" | grep -Fqi "Standalone housekeeping promotion"; then
|
|
79
|
+
echo "::error::Bare-date releases may only be promoted with an explicit standalone housekeeping exception."
|
|
80
|
+
echo "::error::Add 'Standalone housekeeping promotion' to the PR title or body and include ${DECLARATION}."
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
chmod +x scripts/standalone-housekeeping-release.sh 2>/dev/null || true
|
|
84
|
+
bash scripts/standalone-housekeeping-release.sh validate "$CURRENT_RELEASE" "$DECLARATION"
|
|
85
|
+
fi
|
|
86
|
+
|
|
76
87
|
if [[ "$CURRENT_RELEASE" =~ ^REQ-[0-9]+$ ]]; then
|
|
77
88
|
if [ ! -x scripts/extract-release-metadata.sh ]; then
|
|
78
89
|
chmod +x scripts/extract-release-metadata.sh 2>/dev/null || true
|
|
@@ -39,9 +39,10 @@ assert_grep() {
|
|
|
39
39
|
make_fixture() {
|
|
40
40
|
local dir="$1" subject="$2"
|
|
41
41
|
rm -rf "$dir"
|
|
42
|
-
mkdir -p "$dir/scripts" "$dir/compliance/pending-releases"
|
|
42
|
+
mkdir -p "$dir/scripts" "$dir/compliance/pending-releases" "$dir/compliance/standalone-housekeeping"
|
|
43
43
|
cp "$SCRIPT_DIR/derive-release-version.sh" "$dir/scripts/derive-release-version.sh"
|
|
44
44
|
cp "$SCRIPT_DIR/extract-release-metadata.sh" "$dir/scripts/extract-release-metadata.sh"
|
|
45
|
+
cp "$SCRIPT_DIR/standalone-housekeeping-release.sh" "$dir/scripts/standalone-housekeeping-release.sh"
|
|
45
46
|
cp "$HELPER" "$dir/scripts/check-release-pr-scope.sh"
|
|
46
47
|
chmod +x "$dir/scripts/"*.sh
|
|
47
48
|
(
|
|
@@ -133,5 +134,45 @@ assert_exit "hotfix branch skips" 0 "$CODE"
|
|
|
133
134
|
assert_grep "hotfix skip message" 'Hotfix PR detected' "$OUT"
|
|
134
135
|
echo ""
|
|
135
136
|
|
|
137
|
+
echo "--- Test 5: bare-date releases require an explicit standalone declaration ---"
|
|
138
|
+
make_fixture "$WORK/test5" "chore: standalone housekeeping release"
|
|
139
|
+
cat > "$WORK/test5/compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-v2026.07.18.json" <<'EOF'
|
|
140
|
+
{
|
|
141
|
+
"schemaVersion": 1,
|
|
142
|
+
"version": "v2026.07.18",
|
|
143
|
+
"releaseMode": "standalone_housekeeping",
|
|
144
|
+
"reason": "Urgent operational change cannot wait for the next tracked requirement release."
|
|
145
|
+
}
|
|
146
|
+
EOF
|
|
147
|
+
OUT="$WORK/test5.out"
|
|
148
|
+
if run_check "$WORK/test5" "$OUT" env PR_TITLE="Standalone housekeeping promotion: v2026.07.18" PR_BODY=$'## Release\n- Release: v2026.07.18\n'; then
|
|
149
|
+
CODE=0
|
|
150
|
+
else
|
|
151
|
+
CODE=$?
|
|
152
|
+
fi
|
|
153
|
+
assert_exit "declared standalone housekeeping release passes" 0 "$CODE"
|
|
154
|
+
assert_grep "standalone validation is emitted" 'Standalone housekeeping declaration is valid' "$OUT"
|
|
155
|
+
echo ""
|
|
156
|
+
|
|
157
|
+
echo "--- Test 6: bare-date releases without the exception marker fail ---"
|
|
158
|
+
make_fixture "$WORK/test6" "chore: standalone housekeeping release"
|
|
159
|
+
cat > "$WORK/test6/compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-v2026.07.18.json" <<'EOF'
|
|
160
|
+
{
|
|
161
|
+
"schemaVersion": 1,
|
|
162
|
+
"version": "v2026.07.18",
|
|
163
|
+
"releaseMode": "standalone_housekeeping",
|
|
164
|
+
"reason": "Urgent operational change cannot wait for the next tracked requirement release."
|
|
165
|
+
}
|
|
166
|
+
EOF
|
|
167
|
+
OUT="$WORK/test6.out"
|
|
168
|
+
if run_check "$WORK/test6" "$OUT" env PR_TITLE="Release: v2026.07.18" PR_BODY=$'## Release\n- Release: v2026.07.18\n'; then
|
|
169
|
+
CODE=0
|
|
170
|
+
else
|
|
171
|
+
CODE=$?
|
|
172
|
+
fi
|
|
173
|
+
assert_exit "bare-date release without exception marker fails" 1 "$CODE"
|
|
174
|
+
assert_grep "exception marker failure is clear" 'explicit standalone housekeeping exception' "$OUT"
|
|
175
|
+
echo ""
|
|
176
|
+
|
|
136
177
|
echo "=== check-release-pr-scope.test.sh: $PASS passed, $FAIL failed ==="
|
|
137
178
|
[ "$FAIL" -eq 0 ]
|
|
@@ -76,7 +76,7 @@ extract_ticket_title() {
|
|
|
76
76
|
printf '%s' "$line" \
|
|
77
77
|
| sed -E 's/^\*\*Requirement:\*\*[[:space:]]*//' \
|
|
78
78
|
| sed -E 's/^REQ-[0-9]+[[:space:]]*[—–:-][[:space:]]*//' \
|
|
79
|
-
| sed -E 's
|
|
79
|
+
| sed -E 's/^[[:space:]]+|[[:space:]]+$//g'
|
|
80
80
|
return 0
|
|
81
81
|
fi
|
|
82
82
|
line=$(grep -m1 '^# ' "$file" 2>/dev/null || true)
|
|
@@ -227,6 +227,7 @@ for version in "${EXPLICIT_PREDECESSORS[@]}"; do
|
|
|
227
227
|
--arg commitTo "$commit_to" \
|
|
228
228
|
--arg prNumber "$pr_number" \
|
|
229
229
|
--arg prUrl "$pr_url" \
|
|
230
|
+
--arg originalTitle "${title:-Release $version}" \
|
|
230
231
|
'. + [{
|
|
231
232
|
version: $version,
|
|
232
233
|
role: $role,
|
|
@@ -236,7 +237,14 @@ for version in "${EXPLICIT_PREDECESSORS[@]}"; do
|
|
|
236
237
|
commitFrom: (if $commitFrom == "" then null else $commitFrom end),
|
|
237
238
|
commitTo: (if $commitTo == "" then null else $commitTo end),
|
|
238
239
|
prNumber: (if $prNumber == "" then null else ($prNumber | tonumber) end),
|
|
239
|
-
prUrl: (if $prUrl == "" then null else $prUrl end)
|
|
240
|
+
prUrl: (if $prUrl == "" then null else $prUrl end),
|
|
241
|
+
originalTitle: $originalTitle,
|
|
242
|
+
evidenceInheritancePolicy: {
|
|
243
|
+
mode: "all_eligible",
|
|
244
|
+
includeCycles: true,
|
|
245
|
+
scopes: ["release", "stage", "cycle"],
|
|
246
|
+
reason: $reason
|
|
247
|
+
}
|
|
240
248
|
}]' <<<"$MEMBERS"
|
|
241
249
|
)"
|
|
242
250
|
PREDECESSOR_LINES+=("- \`${version}\` (${role}/${relationship}) — ${title:-Untitled release ticket}")
|
|
@@ -267,6 +275,7 @@ fi
|
|
|
267
275
|
GENERATOR_VERSION="$(jq -r '.version' cli/package.json 2>/dev/null || echo '0.0.0')"
|
|
268
276
|
REPOSITORY_SLUG="$(git config --get remote.origin.url 2>/dev/null | sed -E 's#(git@github.com:|https://github.com/)##; s#\.git$##' || true)"
|
|
269
277
|
GENERATED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
278
|
+
[ -n "$REPOSITORY_SLUG" ] || REPOSITORY_SLUG="${GITHUB_REPOSITORY:-local/unknown}"
|
|
270
279
|
|
|
271
280
|
MANIFEST_BASE="$(
|
|
272
281
|
jq -c -n \
|
|
@@ -277,7 +286,7 @@ MANIFEST_BASE="$(
|
|
|
277
286
|
--arg repository "$REPOSITORY_SLUG" \
|
|
278
287
|
--arg generatedAt "$GENERATED_AT" \
|
|
279
288
|
'{
|
|
280
|
-
schemaVersion:
|
|
289
|
+
schemaVersion: 2,
|
|
281
290
|
approvalRelease: { version: $version },
|
|
282
291
|
coreRelease: { version: $version },
|
|
283
292
|
members: ($members | fromjson),
|
|
@@ -291,7 +300,7 @@ MANIFEST_BASE="$(
|
|
|
291
300
|
}'
|
|
292
301
|
)"
|
|
293
302
|
|
|
294
|
-
HASH_INPUT="$(jq -
|
|
303
|
+
HASH_INPUT="$(jq -cS 'del(.generator.generatedAt)' <<<"$MANIFEST_BASE")"
|
|
295
304
|
MANIFEST_HASH="sha256:$(printf '%s' "$HASH_INPUT" | sha256sum | awk '{print $1}')"
|
|
296
305
|
MANIFEST_JSON="$(jq --arg manifestHash "$MANIFEST_HASH" '. + { manifestHash: $manifestHash }' <<<"$MANIFEST_BASE")"
|
|
297
306
|
|
|
@@ -150,6 +150,11 @@ assert_eq "member version recorded" "REQ-041" "$(jq -r '.members[0].version' "$J
|
|
|
150
150
|
assert_eq "member role recorded" "predecessor" "$(jq -r '.members[0].role' "$JSON_OUT")"
|
|
151
151
|
assert_eq "member relationship recorded" "superseded" "$(jq -r '.members[0].relationship' "$JSON_OUT")"
|
|
152
152
|
assert_eq "manifest hash present" "true" "$(jq -r 'has("manifestHash")' "$JSON_OUT")"
|
|
153
|
+
assert_eq "manifest schema v2" "2" "$(jq -r '.schemaVersion' "$JSON_OUT")"
|
|
154
|
+
assert_eq "original title recorded" "Prior tracked release" "$(jq -r '.members[0].originalTitle' "$JSON_OUT")"
|
|
155
|
+
assert_eq "inheritance policy explicit" "all_eligible" "$(jq -r '.members[0].evidenceInheritancePolicy.mode' "$JSON_OUT")"
|
|
156
|
+
EXPECTED_HASH="sha256:$(jq -cS 'del(.manifestHash, .generator.generatedAt)' "$JSON_OUT" | tr -d '\n' | sha256sum | awk '{print $1}')"
|
|
157
|
+
assert_eq "canonical manifest hash verifies" "$EXPECTED_HASH" "$(jq -r '.manifestHash' "$JSON_OUT")"
|
|
153
158
|
echo
|
|
154
159
|
|
|
155
160
|
# Test 3: ambiguous predecessor ownership fails.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Records authoritative required-check state independently of evidence upload.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PROJECT_SLUG="" RELEASE_VERSION="" CHECK_KEY="" LABEL="" PROVIDER="github_actions"
|
|
6
|
+
STATUS="" REQUIRED="true" EXTERNAL_RUN_ID="" EXTERNAL_URL="" COMMIT_SHA="" BRANCH=""
|
|
7
|
+
STARTED_AT="" COMPLETED_AT="" DETAILS_JSON='{}'
|
|
8
|
+
|
|
9
|
+
while [ "$#" -gt 0 ]; do
|
|
10
|
+
case "$1" in
|
|
11
|
+
--project-slug) PROJECT_SLUG="$2"; shift 2 ;;
|
|
12
|
+
--release) RELEASE_VERSION="$2"; shift 2 ;;
|
|
13
|
+
--check-key) CHECK_KEY="$2"; shift 2 ;;
|
|
14
|
+
--label) LABEL="$2"; shift 2 ;;
|
|
15
|
+
--provider) PROVIDER="$2"; shift 2 ;;
|
|
16
|
+
--status) STATUS="$2"; shift 2 ;;
|
|
17
|
+
--required) REQUIRED="$2"; shift 2 ;;
|
|
18
|
+
--external-run-id) EXTERNAL_RUN_ID="$2"; shift 2 ;;
|
|
19
|
+
--external-url) EXTERNAL_URL="$2"; shift 2 ;;
|
|
20
|
+
--commit-sha) COMMIT_SHA="$2"; shift 2 ;;
|
|
21
|
+
--branch) BRANCH="$2"; shift 2 ;;
|
|
22
|
+
--started-at) STARTED_AT="$2"; shift 2 ;;
|
|
23
|
+
--completed-at) COMPLETED_AT="$2"; shift 2 ;;
|
|
24
|
+
--details-json) DETAILS_JSON="$2"; shift 2 ;;
|
|
25
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
26
|
+
esac
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
for value in PROJECT_SLUG RELEASE_VERSION CHECK_KEY LABEL STATUS; do
|
|
30
|
+
if [ -z "${!value}" ]; then echo "Error: ${value} is required" >&2; exit 1; fi
|
|
31
|
+
done
|
|
32
|
+
case "$STATUS" in
|
|
33
|
+
queued|running|successful|failed|cancelled|timed_out|skipped|action_required|unknown) ;;
|
|
34
|
+
*) echo "Error: invalid check status '$STATUS'" >&2; exit 1 ;;
|
|
35
|
+
esac
|
|
36
|
+
jq -e 'type == "object"' >/dev/null <<<"$DETAILS_JSON" || {
|
|
37
|
+
echo "Error: --details-json must be a JSON object" >&2; exit 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
BASE="${DEVAUDIT_BASE_URL%/}"
|
|
41
|
+
if [ -z "$BASE" ] || [ -z "${DEVAUDIT_API_KEY:-}" ]; then
|
|
42
|
+
echo "::warning::Release check reporting skipped: DevAudit URL/API key missing" >&2
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
ENCODED_SLUG=$(jq -rn --arg v "$PROJECT_SLUG" '$v|@uri')
|
|
46
|
+
ENCODED_VERSION=$(jq -rn --arg v "$RELEASE_VERSION" '$v|@uri')
|
|
47
|
+
RESOLVE=$(curl -fsS \
|
|
48
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
49
|
+
"${BASE}/api/ci/releases/resolve?projectSlug=${ENCODED_SLUG}&versionPrefix=${ENCODED_VERSION}" || true)
|
|
50
|
+
RELEASE_ID=$(jq -r --arg version "$RELEASE_VERSION" \
|
|
51
|
+
'if .latest.version == $version then .latest.id // empty else empty end' <<<"$RESOLVE" 2>/dev/null || true)
|
|
52
|
+
if [ -z "$RELEASE_ID" ]; then
|
|
53
|
+
echo "::warning::Release check reporting skipped: exact release ${RELEASE_VERSION} not found" >&2
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
PAYLOAD=$(jq -n \
|
|
58
|
+
--arg checkKey "$CHECK_KEY" --arg displayLabel "$LABEL" --arg provider "$PROVIDER" \
|
|
59
|
+
--arg status "$STATUS" --argjson required "$REQUIRED" \
|
|
60
|
+
--arg externalRunId "$EXTERNAL_RUN_ID" --arg externalUrl "$EXTERNAL_URL" \
|
|
61
|
+
--arg commitSha "$COMMIT_SHA" --arg branch "$BRANCH" \
|
|
62
|
+
--arg startedAt "$STARTED_AT" --arg completedAt "$COMPLETED_AT" \
|
|
63
|
+
--argjson details "$DETAILS_JSON" \
|
|
64
|
+
'{checkKey:$checkKey,label:$displayLabel,provider:$provider,status:$status,required:$required,
|
|
65
|
+
externalRunId:(if $externalRunId=="" then null else $externalRunId end),
|
|
66
|
+
externalUrl:(if $externalUrl=="" then null else $externalUrl end),
|
|
67
|
+
commitSha:(if $commitSha=="" then null else $commitSha end),
|
|
68
|
+
branch:(if $branch=="" then null else $branch end),
|
|
69
|
+
startedAt:(if $startedAt=="" then null else $startedAt end),
|
|
70
|
+
completedAt:(if $completedAt=="" then null else $completedAt end),details:$details}')
|
|
71
|
+
|
|
72
|
+
RESPONSE_FILE=$(mktemp)
|
|
73
|
+
trap 'rm -f "$RESPONSE_FILE"' EXIT
|
|
74
|
+
HTTP_CODE=$(curl -sS -o "$RESPONSE_FILE" -w '%{http_code}' \
|
|
75
|
+
-X POST "${BASE}/api/ci/releases/${RELEASE_ID}/checks" \
|
|
76
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}" -H 'Content-Type: application/json' \
|
|
77
|
+
-d "$PAYLOAD")
|
|
78
|
+
if [ "$HTTP_CODE" = "404" ]; then
|
|
79
|
+
echo "::warning::Portal does not support first-class release checks yet" >&2
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
|
|
83
|
+
cat "$RESPONSE_FILE" >&2 || true
|
|
84
|
+
echo "Error: release check API returned HTTP ${HTTP_CODE}" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Focused regression tests for authoritative release-check reporting.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
HELPER="$SCRIPT_DIR/report-release-check.sh"
|
|
7
|
+
PASS=0
|
|
8
|
+
FAIL=0
|
|
9
|
+
|
|
10
|
+
ok() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|
11
|
+
no() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
12
|
+
|
|
13
|
+
make_fixture() {
|
|
14
|
+
local dir="$1"
|
|
15
|
+
mkdir -p "$dir/bin"
|
|
16
|
+
cat > "$dir/bin/curl" <<'EOF'
|
|
17
|
+
#!/usr/bin/env bash
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
URL=""
|
|
20
|
+
OUT=""
|
|
21
|
+
BODY=""
|
|
22
|
+
ARGS=("$@")
|
|
23
|
+
for ((i=0; i<${#ARGS[@]}; i++)); do
|
|
24
|
+
case "${ARGS[$i]}" in
|
|
25
|
+
-o) OUT="${ARGS[$((i+1))]}" ;;
|
|
26
|
+
-d) BODY="${ARGS[$((i+1))]}" ;;
|
|
27
|
+
http://*|https://*) URL="${ARGS[$i]}" ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
printf 'URL:%s\nBODY:%s\n' "$URL" "$BODY" >> "${CHECK_TEST_LOG:?}"
|
|
31
|
+
if [[ "$URL" == *'/api/ci/releases/resolve'* ]]; then
|
|
32
|
+
printf '%s' "${RESOLVE_BODY:-{}}"
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
if [ -n "$OUT" ]; then printf '%s' "${CHECK_BODY:-{}}" > "$OUT"; fi
|
|
36
|
+
printf '%s' "${CHECK_STATUS:-200}"
|
|
37
|
+
EOF
|
|
38
|
+
chmod +x "$dir/bin/curl"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
run_helper() {
|
|
42
|
+
PATH="$1/bin:$PATH" \
|
|
43
|
+
CHECK_TEST_LOG="$1/curl.log" \
|
|
44
|
+
DEVAUDIT_BASE_URL="https://devaudit.example.test" \
|
|
45
|
+
DEVAUDIT_API_KEY="mc_test_dummy" \
|
|
46
|
+
"$HELPER" "${@:2}"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
base_args=(
|
|
50
|
+
--project-slug fixture-app
|
|
51
|
+
--release REQ-100
|
|
52
|
+
--check-key quality-gates
|
|
53
|
+
--label "Quality Gates"
|
|
54
|
+
--status failed
|
|
55
|
+
--external-run-id 123
|
|
56
|
+
--commit-sha abc123
|
|
57
|
+
--details-json '{"artifactUpload":"successful"}'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
echo "=== report-release-check.sh tests ==="
|
|
61
|
+
|
|
62
|
+
dir=$(mktemp -d)
|
|
63
|
+
make_fixture "$dir"
|
|
64
|
+
RESOLVE_BODY='{"latest":{"id":"release-1","version":"REQ-100"}}' \
|
|
65
|
+
CHECK_STATUS=201 run_helper "$dir" "${base_args[@]}"
|
|
66
|
+
if grep -q '/api/ci/releases/release-1/checks' "$dir/curl.log"; then ok "posts to exact release"; else no "missing check endpoint"; fi
|
|
67
|
+
if grep -q 'status.*failed' "$dir/curl.log" && grep -q 'artifactUpload.*successful' "$dir/curl.log"; then
|
|
68
|
+
ok "keeps execution outcome separate from upload detail"
|
|
69
|
+
else
|
|
70
|
+
no "incorrect check payload"
|
|
71
|
+
fi
|
|
72
|
+
rm -rf "$dir"
|
|
73
|
+
|
|
74
|
+
dir=$(mktemp -d)
|
|
75
|
+
make_fixture "$dir"
|
|
76
|
+
RESOLVE_BODY='{"latest":{"id":"release-1","version":"REQ-100"}}' \
|
|
77
|
+
CHECK_STATUS=404 run_helper "$dir" "${base_args[@]}" >/dev/null 2>&1 && ok "tolerates older portal" || no "404 should be tolerant"
|
|
78
|
+
rm -rf "$dir"
|
|
79
|
+
|
|
80
|
+
dir=$(mktemp -d)
|
|
81
|
+
make_fixture "$dir"
|
|
82
|
+
if RESOLVE_BODY='{"latest":{"id":"release-1","version":"REQ-100"}}' \
|
|
83
|
+
CHECK_STATUS=500 CHECK_BODY='{"error":"failed"}' run_helper "$dir" "${base_args[@]}" >/dev/null 2>&1; then
|
|
84
|
+
no "500 should fail"
|
|
85
|
+
else
|
|
86
|
+
ok "fails on portal error"
|
|
87
|
+
fi
|
|
88
|
+
rm -rf "$dir"
|
|
89
|
+
|
|
90
|
+
if "$HELPER" "${base_args[@]/failed/not-a-status}" >/dev/null 2>&1; then
|
|
91
|
+
no "invalid status should fail"
|
|
92
|
+
else
|
|
93
|
+
ok "rejects invalid status"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
echo "=== report-release-check.sh: $PASS passed, $FAIL failed ==="
|
|
97
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# standalone-housekeeping-release.sh — validate and promote an explicit
|
|
3
|
+
# standalone-housekeeping exception after a reviewed, green main promotion.
|
|
4
|
+
#
|
|
5
|
+
# Manifest contract:
|
|
6
|
+
# compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-vYYYY.MM.DD.json
|
|
7
|
+
# {
|
|
8
|
+
# "schemaVersion": 1,
|
|
9
|
+
# "version": "vYYYY.MM.DD",
|
|
10
|
+
# "releaseMode": "standalone_housekeeping",
|
|
11
|
+
# "reason": "Why this cannot wait for the next tracked REQ release."
|
|
12
|
+
# }
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
COMMAND="${1:-}"
|
|
17
|
+
VERSION="${2:-}"
|
|
18
|
+
MANIFEST_PATH="${3:-}"
|
|
19
|
+
|
|
20
|
+
usage() {
|
|
21
|
+
echo "Usage: $0 validate <version> <manifest-path>" >&2
|
|
22
|
+
echo " $0 promote <project-slug> <version> <manifest-path>" >&2
|
|
23
|
+
exit 2
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
validate_manifest() {
|
|
27
|
+
local expected_version="$1" path="$2"
|
|
28
|
+
if ! [[ "$expected_version" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
|
|
29
|
+
echo "Error: ${expected_version} is not a bare-date housekeeping version." >&2
|
|
30
|
+
return 1
|
|
31
|
+
fi
|
|
32
|
+
if [ ! -f "$path" ]; then
|
|
33
|
+
echo "Error: standalone-housekeeping declaration not found: ${path}" >&2
|
|
34
|
+
return 1
|
|
35
|
+
fi
|
|
36
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
37
|
+
echo "Error: jq is required." >&2
|
|
38
|
+
return 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
local schema_version manifest_version release_mode reason
|
|
42
|
+
schema_version=$(jq -r '.schemaVersion // empty' "$path")
|
|
43
|
+
manifest_version=$(jq -r '.version // empty' "$path")
|
|
44
|
+
release_mode=$(jq -r '.releaseMode // empty' "$path")
|
|
45
|
+
reason=$(jq -r '.reason // empty' "$path")
|
|
46
|
+
|
|
47
|
+
[ "$schema_version" = "1" ] || { echo "Error: schemaVersion must be 1." >&2; return 1; }
|
|
48
|
+
[ "$manifest_version" = "$expected_version" ] || { echo "Error: declaration version must exactly match ${expected_version}." >&2; return 1; }
|
|
49
|
+
[ "$release_mode" = "standalone_housekeeping" ] || { echo "Error: releaseMode must be standalone_housekeeping." >&2; return 1; }
|
|
50
|
+
if [ "${#reason}" -lt 20 ]; then
|
|
51
|
+
echo "Error: reason must explain why this cannot wait for the next tracked REQ (minimum 20 characters)." >&2
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case "$COMMAND" in
|
|
57
|
+
validate)
|
|
58
|
+
[ -n "$VERSION" ] && [ -n "$MANIFEST_PATH" ] || usage
|
|
59
|
+
validate_manifest "$VERSION" "$MANIFEST_PATH"
|
|
60
|
+
echo "Standalone housekeeping declaration is valid for ${VERSION}."
|
|
61
|
+
;;
|
|
62
|
+
promote)
|
|
63
|
+
PROJECT_SLUG="${2:-}"
|
|
64
|
+
VERSION="${3:-}"
|
|
65
|
+
MANIFEST_PATH="${4:-}"
|
|
66
|
+
[ -n "$PROJECT_SLUG" ] && [ -n "$VERSION" ] && [ -n "$MANIFEST_PATH" ] || usage
|
|
67
|
+
validate_manifest "$VERSION" "$MANIFEST_PATH"
|
|
68
|
+
: "${DEVAUDIT_BASE_URL:?DEVAUDIT_BASE_URL must be set}"
|
|
69
|
+
: "${DEVAUDIT_API_KEY:?DEVAUDIT_API_KEY must be set}"
|
|
70
|
+
|
|
71
|
+
BASE_URL="${DEVAUDIT_BASE_URL%/}"
|
|
72
|
+
ENCODED_SLUG=$(jq -rn --arg value "$PROJECT_SLUG" '$value|@uri')
|
|
73
|
+
ENCODED_VERSION=$(jq -rn --arg value "$VERSION" '$value|@uri')
|
|
74
|
+
RESOLVED=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
75
|
+
"${BASE_URL}/api/ci/releases/resolve?projectSlug=${ENCODED_SLUG}&versionPrefix=${ENCODED_VERSION}")
|
|
76
|
+
RELEASE_ID=$(jq -r '.latest.id // empty' <<<"$RESOLVED")
|
|
77
|
+
RESOLVED_VERSION=$(jq -r '.latest.version // empty' <<<"$RESOLVED")
|
|
78
|
+
[ -n "$RELEASE_ID" ] || { echo "Error: could not resolve ${VERSION}." >&2; exit 1; }
|
|
79
|
+
[ "$RESOLVED_VERSION" = "$VERSION" ] || { echo "Error: resolve returned ${RESOLVED_VERSION}, not exact ${VERSION}." >&2; exit 1; }
|
|
80
|
+
|
|
81
|
+
REASON=$(jq -r '.reason' "$MANIFEST_PATH")
|
|
82
|
+
PAYLOAD=$(jq -n --arg releaseMode standalone_housekeeping --arg standaloneReason "$REASON" \
|
|
83
|
+
'{releaseMode: $releaseMode, standaloneReason: $standaloneReason}')
|
|
84
|
+
RESPONSE_FILE=$(mktemp)
|
|
85
|
+
trap 'rm -f "$RESPONSE_FILE"' EXIT
|
|
86
|
+
HTTP_CODE=$(curl -sS -o "$RESPONSE_FILE" -w '%{http_code}' \
|
|
87
|
+
-X PATCH "${BASE_URL}/api/ci/releases/${RELEASE_ID}" \
|
|
88
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
89
|
+
-H 'Content-Type: application/json' --data "$PAYLOAD")
|
|
90
|
+
if [ "$HTTP_CODE" != "200" ]; then
|
|
91
|
+
echo "Error: standalone housekeeping promotion failed with HTTP ${HTTP_CODE}." >&2
|
|
92
|
+
sed 's/^/ /' "$RESPONSE_FILE" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
echo "Promoted ${VERSION} as standalone housekeeping (release ${RELEASE_ID})."
|
|
96
|
+
;;
|
|
97
|
+
*) usage ;;
|
|
98
|
+
esac
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
HELPER="$SCRIPT_DIR/standalone-housekeeping-release.sh"
|
|
6
|
+
WORK=$(mktemp -d)
|
|
7
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
8
|
+
|
|
9
|
+
MANIFEST="$WORK/standalone.json"
|
|
10
|
+
cat > "$MANIFEST" <<'JSON'
|
|
11
|
+
{"schemaVersion":1,"version":"v2026.07.18","releaseMode":"standalone_housekeeping","reason":"Urgent production CI repair cannot wait for the next tracked requirement."}
|
|
12
|
+
JSON
|
|
13
|
+
|
|
14
|
+
bash "$HELPER" validate v2026.07.18 "$MANIFEST"
|
|
15
|
+
|
|
16
|
+
MOCK_BIN="$WORK/bin"
|
|
17
|
+
mkdir -p "$MOCK_BIN"
|
|
18
|
+
cat > "$MOCK_BIN/curl" <<'MOCK'
|
|
19
|
+
#!/usr/bin/env bash
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
if [[ "$*" == *"/resolve?"* ]]; then
|
|
22
|
+
printf '{"latest":{"id":"rel_123","version":"v2026.07.18"}}'
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
output_file=""
|
|
26
|
+
for ((i=1; i <= $#; i++)); do
|
|
27
|
+
if [ "${!i}" = "-o" ]; then
|
|
28
|
+
next=$((i + 1))
|
|
29
|
+
output_file="${!next}"
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
[ -z "$output_file" ] || printf '{"ok":true}' > "$output_file"
|
|
33
|
+
printf '200'
|
|
34
|
+
MOCK
|
|
35
|
+
chmod +x "$MOCK_BIN/curl"
|
|
36
|
+
|
|
37
|
+
PATH="$MOCK_BIN:$PATH" DEVAUDIT_BASE_URL=https://devaudit.example.test DEVAUDIT_API_KEY=mc_test \
|
|
38
|
+
bash "$HELPER" promote fixture-project v2026.07.18 "$MANIFEST"
|
|
39
|
+
|
|
40
|
+
if bash "$HELPER" validate REQ-999 "$MANIFEST" >/dev/null 2>&1; then
|
|
41
|
+
echo "Expected tracked version validation to fail" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo "standalone-housekeeping-release: PASS"
|
|
@@ -341,8 +341,9 @@ Reached from Phase 0 for non-tracked change-types. The skill drives this end-to-
|
|
|
341
341
|
- **Post-merge-only CI (older generated workflows — `push: branches: [<integration>]` with no `pull_request:` trigger)** — say so explicitly in the LAST/NEXT sticky: _"no PR-time checks will fire; review + merge is the gate; CI runs post-merge on `$INTEGRATION_BRANCH`."_ Don't poll the PR for checks that won't arrive. The post-merge run (CI Pipeline + Compliance Evidence Upload on the integration branch) is the actual gate; address it via fix-forward if it fails.
|
|
342
342
|
|
|
343
343
|
Either way, never bypass a gate (no `--no-verify`, no `--admin` merge of a red required check); the only difference is **where** you wait for the gate to fire — before merge vs. after merge.
|
|
344
|
-
8. **Guide review
|
|
345
|
-
9. **
|
|
344
|
+
8. **Guide review -> merge.** A human still reviews the PR. Merge only after all required checks are terminal green on the current PR head SHA; queued, running, stale, cancelled, unexpectedly skipped, or failed checks are not green. There is no portal release approval, UAT four-eyes, Production gate, or standalone close-out on the normal lightweight path.
|
|
345
|
+
9. **Record the correct release meaning.** A housekeeping push produces at most a bare-date release (`vYYYY.MM.DD`) that is integration/history, not an active approval by default. It waits on `$INTEGRATION_BRANCH` until the next tracked release absorbs it through the required bundled-changes markdown/JSON manifest and portal lineage. A doc-only push attaches documents to the existing `REQ-XXX` release.
|
|
346
|
+
10. **Standalone exception.** Only when housekeeping cannot reasonably wait for the next tracked release may the skill prepare a standalone promotion. It must state `Standalone housekeeping promotion` and why in the release PR, add `compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-vYYYY.MM.DD.json` with `schemaVersion: 1`, the exact derived bare-date version, `releaseMode: standalone_housekeeping`, and a specific reason. It must obtain terminal-green CI and PR review. The installed release-scope check validates the declaration before merge; post-deploy uploads it as release-ticket evidence and changes the portal row to standalone housekeeping. Use portal approval only if project policy explicitly opts in. If the installed workflow cannot implement that policy, halt and keep the change on the normal integration path.
|
|
346
347
|
|
|
347
348
|
### Phase 1 — Plan (SDLC stage 1)
|
|
348
349
|
|
|
@@ -699,16 +700,11 @@ Invoked separately by the user after UAT activity on the portal. Trigger: "resum
|
|
|
699
700
|
- If `released` (Option B): PATCH directly to `released` — `PATCH /releases/<version>` with `{"status": "released"}`.
|
|
700
701
|
- If `prod_review` (Option A, default): after `post-deploy-prod.yml` completes, the release is at `prod_review`. Update the sticky to "Phase 5 — production deployed; awaiting prod approval on portal" and halt. The operator clicks "Approve Production" (`prod_review` → `prod_approved`) then "Mark as Released" (`prod_approved` → `released`) in the portal. On next `resume REQ-XXX`, the skill reads the portal state — if `released`, finalise (close issue, update sticky to terminal). If `prod_approved`, prompt the operator to click "Mark as Released". If still `prod_review`, report "awaiting production approval" and stop.
|
|
701
702
|
- Comment on the issue: "Released. Production smoke evidence: <link>." (only after release status is `released`)
|
|
702
|
-
- **Phase 5 close-out — mandatory
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
5. **Push the close-out commit** to `$INTEGRATION_BRANCH`.
|
|
708
|
-
|
|
709
|
-
These steps are mandatory — the native agent must not treat "PR merged" or "release marked Released" as completion without performing the close-out. Without moving the release ticket to `approved-releases/`, CI continues uploading evidence for the REQ on every subsequent release, polluting the portal's audit trail.
|
|
710
|
-
- **Update SDLC status sticky** to the terminal state: `bash scripts/update-sdlc-status.sh "$ISSUE_NUM" "Phase 5 complete — release marked Released; production smoke evidence uploaded; close-out committed (RTM → APPROVED - DEPLOYED, ticket → approved-releases/)" "Done — close issue + retire feature branch (sdlc-implementer halts)"`.
|
|
711
|
-
- Close the issue.
|
|
703
|
+
- **Phase 5 close-out — mandatory automated reconciliation.** After the portal release is `released`, wait for the portal's `repository_dispatch('release-closed')` event to open `chore/close-out-REQ-XXX` against `$INTEGRATION_BRANCH`. Verify the PR updates the RTM, moves the ticket to `approved-releases/`, and moves manifest-listed predecessors to `superseded-releases/`. Review and merge the close-out PR only after its required checks are terminal green on its current head SHA. If no PR appears, manually dispatch the Release Close-out workflow. Use `scripts/close-out-release.sh` locally only for documented recovery/catch-up; never make a normal direct close-out commit to a protected branch.
|
|
704
|
+
|
|
705
|
+
These steps are mandatory — "PR merged" or "release marked Released" is not complete until the automated close-out PR has merged or a documented fallback reconciliation is complete. Without it, CI can keep uploading evidence against a completed REQ and pollute the audit trail.
|
|
706
|
+
- **Update SDLC status sticky** to the terminal state: `bash scripts/update-sdlc-status.sh "$ISSUE_NUM" "Phase 5 complete — release marked Released; production smoke evidence uploaded; automated close-out reconciled RTM and release ticket" "Done — close issue + retire feature branch (sdlc-implementer halts)"`.
|
|
707
|
+
- Close the issue only after close-out reconciliation completes.
|
|
712
708
|
- If production smoke fails: do NOT mark as Released. **Delegate incident filing to `governance-doc-author` (devaudit-installer#210 §6c)** — invoke `Skill(name: "governance-doc-author", args: "Production smoke failure for REQ-XXX. File an incident issue with the `incident` label + `### Framework attribution` section (ISO29119.3.5.4 + SOC2.CC7.2), severity `blocker`. Include the production URL, git SHA, testCycleId (CI run ID), and smoke results in the issue body.")`. Page the on-call per the project's incident playbook, follow the rollback plan from the implementation plan. If the rollback also fails: update the sticky to "Phase 5 CRITICAL — production smoke failed AND rollback failed. Production is in a broken state. Operator action — page on-call immediately, engage hosting platform support, declare incident per the project's incident playbook. This is beyond the skill's scope." Do NOT attempt further automated remediation. **Update the sticky** to reflect the incident state: `… "Phase 5 BLOCKED — production smoke failed; INCIDENT issue #N filed" "Operator action — read INCIDENT #N + execute rollback per plan"`.
|
|
713
709
|
|
|
714
710
|
- **Changes requested** → run change-request loop:
|
|
@@ -769,7 +765,7 @@ The skill re-reads state from the filesystem and continues from where it left of
|
|
|
769
765
|
|
|
770
766
|
The native agent must NOT continue to the next phase (Phase 3 → Phase 4, Phase 2 → Phase 3) without re-invoking the skill. The only exception is the skill's own auto-continue steps (Phase 2 step 11) where the skill explicitly says it will continue to the next phase in the same turn.
|
|
771
767
|
|
|
772
|
-
**PR merged to main
|
|
768
|
+
**PR merged to main is not done.** After the release PR is merged, Phase 5 waits for deploy/host verification, portal `released`, and the automated close-out PR back to the integration branch. The merge is not the end of the workflow.
|
|
773
769
|
|
|
774
770
|
### Commit-history hygiene recovery (devaudit-installer#315)
|
|
775
771
|
|