@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-cli",
3
- "version": "0.3.13",
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.13",
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/[[:space:]]*$//'
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: 1,
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 -S 'del(.generator.generatedAt)' <<<"$MANIFEST_BASE")"
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 merge.** A human still reviews the PR (separation of duties). There is **no** portal release approval, no UAT four-eyes, no Production gate, and no close-out. Merge once CI is green and the reviewer approves.
345
- 9. **Done.** A housekeeping push produces at most a bare-date release (`vYYYY.MM.DD`) with no approval gate; a doc-only push attaches its docs to the existing `REQ-XXX` release. No further action required — report completion and stop.
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 post-merge compliance steps (devaudit-installer#226).** After the release is marked `released` on the portal, perform the close-out:
703
- 1. **Update RTM status** — change the REQ-XXX row from `TESTED - PENDING SIGN-OFF` to `APPROVED - DEPLOYED` in `compliance/RTM.md`.
704
- 2. **Move release ticket**move `compliance/pending-releases/RELEASE-TICKET-REQ-XXX.md` to `compliance/approved-releases/`. This prevents CI's `upload-evidence` job from treating the REQ as in-scope for subsequent releases.
705
- 3. **Verify portal approval** — confirm the DevAudit portal release record is in `released` status.
706
- 4. **Commit the close-out** `git add compliance/RTM.md compliance/pending-releases/ compliance/approved-releases/ && git commit -m "compliance: [REQ-XXX] close out release — RTM updated to APPROVED - DEPLOYED, ticket moved to approved-releases/"`.
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 done.** After the release PR is merged to `main`, the native agent must re-invoke the skill for Phase 5 close-out (see Phase 5 step 0 below). The merge is not the end of the workflow — post-merge compliance steps are mandatory.
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