@metasession.co/devaudit-cli 0.3.14 → 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.14",
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.14",
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 ]
@@ -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
 
@@ -336,6 +336,26 @@ jobs:
336
336
  --environment uat --category planning \
337
337
  --git-sha ${{ github.sha }} --branch ${{ github.ref_name }} || true
338
338
 
339
+ - name: Upload standalone housekeeping declaration
340
+ if: env.DEVAUDIT_BASE_URL != '' && steps.version.outputs.version != 'skip'
341
+ run: |
342
+ VERSION="${{ steps.version.outputs.version }}"
343
+ if ! [[ "$VERSION" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
344
+ exit 0
345
+ fi
346
+ DECLARATION="compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-${VERSION}.json"
347
+ if [ ! -f "$DECLARATION" ]; then
348
+ echo "${VERSION} is integration housekeeping; no standalone declaration uploaded."
349
+ exit 0
350
+ fi
351
+ chmod +x scripts/standalone-housekeeping-release.sh scripts/upload-evidence.sh 2>/dev/null || true
352
+ bash scripts/standalone-housekeeping-release.sh validate "$VERSION" "$DECLARATION"
353
+ bash scripts/upload-evidence.sh \
354
+ {{PROJECT_SLUG}} "$VERSION" release_ticket "$DECLARATION" \
355
+ --release "$VERSION" --create-release-if-missing \
356
+ --environment uat --category release_artifact --sdlc-stage 2 \
357
+ --git-sha ${{ github.sha }} --branch ${{ github.ref_name }}
358
+
339
359
  - name: Generate and upload bundled changes (REQ releases only)
340
360
  if: env.DEVAUDIT_BASE_URL != '' && steps.version.outputs.version != 'skip'
341
361
  run: |
@@ -49,26 +49,11 @@ jobs:
49
49
  # double-upload every compliance doc.
50
50
  if: github.event_name != 'workflow_run'
51
51
  runs-on: {{RUNNER}}
52
- # Permissions are needed because the housekeeping path now does two
53
- # GitHub mutations:
54
- # 1. pushes a stub branch + opens a PR via `gh pr create`
55
- # 2. dispatches `ci.yml` on develop after that stub PR merges, so
56
- # compliance-only housekeeping releases still get the gate evidence
57
- # the portal approval path currently requires (#361)
58
- #
59
- # Without these the github-actions[bot] token gets HTTP 403 on the push,
60
- # PR create, or workflow dispatch and the housekeeping path leaves a red
61
- # badge or an unapprovable release behind.
62
- #
63
- # `DEVAUDIT_USER_TOKEN` is no longer consulted by this workflow (the
64
- # auto-stub step now uses `github.token` directly — see the env block
65
- # on that step). The permissions below grant everything `gh pr create`
66
- # needs; preferring a (potentially stale) PAT was the cause of the
67
- # 2026-06-07 401 chain on this issue.
52
+ # #409: this workflow only uploads evidence. Normal housekeeping is
53
+ # integration history, so it must not create PRs, dispatch gates, or
54
+ # receive write-scoped GitHub credentials.
68
55
  permissions:
69
- actions: write # gh workflow run ci.yml --ref develop
70
- contents: write # push the auto-PR branch
71
- pull-requests: write # gh pr create
56
+ contents: read
72
57
  env:
73
58
  DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
74
59
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
@@ -141,28 +126,13 @@ jobs:
141
126
  echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
142
127
  echo "Release version: ${VERSION}"
143
128
 
144
- # Housekeeping artefact auto-generation (DevAudit-Installer#116 WS3+WS4)
145
- #
146
- # Housekeeping releases (bare-date version) need two operator-authored
147
- # artefacts to clear the portal's release-completeness checklist:
148
- # `RELEASE-TICKET-<version>.md` and `security-summary-<version>.md`.
149
- # When the version resolves to a bare-date AND those artefacts don't
150
- # already exist on develop, generate stubs from the CI gate JSON and
151
- # open a PR that the operator reviews + signs off + merges. Once
152
- # merged, the next compliance-evidence.yml run uploads them as
153
- # evidence and the matrix flips both items to ✓.
154
- #
155
- # devaudit#284 — release reconciliation merges (from close-out-release.yml)
156
- # carry a `Release-Closeout: REQ-XXX` marker in the commit body.
157
- # derive-release-version.sh detects this marker and emits an empty
158
- # version, which this workflow converts to the explicit `skip` sentinel.
159
- # Reconciliation pushes therefore never create housekeeping stubs and
160
- # never attach fresh evidence to an already released tracked record.
161
- #
162
- # Mirrors the `incident-export.yml` pattern: auto-PR, operator fills
163
- # in sign-off, merge to land.
164
- - name: Auto-generate housekeeping stubs (if needed)
165
- if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
129
+ # #409: normal bare-date changes are integration CI history, not portal
130
+ # releases awaiting approval. The old auto-generated ticket/security
131
+ # summary path is deliberately disabled. An independently promoted
132
+ # exception must carry the reviewed standalone declaration validated by
133
+ # check-release-pr-scope.sh and post-deploy-prod.yml.
134
+ - name: Legacy housekeeping approval path (disabled)
135
+ if: ${{ false }}
166
136
  env:
167
137
  # Use the workflow's GITHUB_TOKEN directly. The `permissions:`
168
138
  # block on the job grants `contents: write` + `pull-requests:
@@ -241,8 +211,8 @@ jobs:
241
211
  echo "PR #${EXISTING} already open for ${VERSION} — branch updated in place."
242
212
  fi
243
213
 
244
- - name: Dispatch ci.yml for merged housekeeping stub
245
- if: github.event_name == 'push' && steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
214
+ - name: Legacy housekeeping gate dispatch (disabled)
215
+ if: ${{ false }}
246
216
  env:
247
217
  GH_TOKEN: ${{ github.token }}
248
218
  VERSION: ${{ steps.version.outputs.version }}
@@ -97,10 +97,18 @@ jobs:
97
97
  # release ticket (the same set the dev/UAT pipeline versioned via
98
98
  # derive-release-version.sh → REQ-XXX). A bundled develop→main PR
99
99
  # carries several; promote ALL of them, not just the first. A manual
100
- # dispatch can target one via the `release` input. Fall back to a
101
- # bare date for date-versioned (ticketless) releases.
100
+ # dispatch can target one via the `release` input. A ticketless
101
+ # main promotion is only valid when its explicit standalone
102
+ # housekeeping declaration is present and valid.
102
103
  if [ -n "${RELEASE_INPUT}" ]; then
103
104
  REQS="${RELEASE_INPUT}"
105
+ if [[ "$REQS" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
106
+ chmod +x scripts/standalone-housekeeping-release.sh 2>/dev/null || true
107
+ DECLARATION="compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-${REQS}.json"
108
+ bash scripts/standalone-housekeeping-release.sh validate "$REQS" "$DECLARATION"
109
+ echo "STANDALONE_DECLARATION=${DECLARATION}" >> "$GITHUB_ENV"
110
+ echo "TERMINAL_STATUS=released" >> "$GITHUB_ENV"
111
+ fi
104
112
  else
105
113
  REQS=""
106
114
  if [ -d compliance/pending-releases ]; then
@@ -111,7 +119,20 @@ jobs:
111
119
  fi
112
120
  REQS=$(echo ${REQS} | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/[[:space:]]*$//')
113
121
  if [ -z "${REQS}" ]; then
114
- REQS="v$(date +%Y.%m.%d)"
122
+ chmod +x scripts/derive-release-version.sh scripts/standalone-housekeeping-release.sh 2>/dev/null || true
123
+ VERSION=$(bash scripts/derive-release-version.sh)
124
+ DECLARATION="compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-${VERSION}.json"
125
+ if [ -f "$DECLARATION" ]; then
126
+ bash scripts/standalone-housekeeping-release.sh validate "$VERSION" "$DECLARATION"
127
+ REQS="$VERSION"
128
+ echo "STANDALONE_DECLARATION=${DECLARATION}" >> "$GITHUB_ENV"
129
+ # Standalone housekeeping is reviewed in its GitHub promotion
130
+ # PR. It does not enter the portal UAT/production queue unless
131
+ # a project deliberately supplies a tracked REQ instead.
132
+ echo "TERMINAL_STATUS=released" >> "$GITHUB_ENV"
133
+ else
134
+ echo "No tracked release ticket or standalone declaration: ordinary housekeeping has no portal promotion."
135
+ fi
115
136
  fi
116
137
  fi
117
138
  echo "In-scope releases to promote: ${REQS}"
@@ -413,6 +434,9 @@ jobs:
413
434
  # Carry the release ticket into the production environment so the
414
435
  # prod-review gate is self-contained.
415
436
  TICKET=""
437
+ if [ -n "${STANDALONE_DECLARATION:-}" ] && [ "$VERSION" = "${REQS}" ]; then
438
+ TICKET="$STANDALONE_DECLARATION"
439
+ fi
416
440
  for DIR in compliance/pending-releases compliance/approved-releases; do
417
441
  if [ -f "${DIR}/RELEASE-TICKET-${VERSION}.md" ]; then
418
442
  TICKET="${DIR}/RELEASE-TICKET-${VERSION}.md"; break
@@ -447,6 +471,11 @@ jobs:
447
471
  fi
448
472
  # Advance status (idempotent — re-PATCHing an already-promoted release is a no-op).
449
473
  if [ -n "$RELEASE_ID" ]; then
474
+ if [ -n "${STANDALONE_DECLARATION:-}" ] && [ "$VERSION" = "${REQS}" ]; then
475
+ chmod +x scripts/standalone-housekeeping-release.sh 2>/dev/null || true
476
+ bash scripts/standalone-housekeeping-release.sh promote \
477
+ "$PROJECT_SLUG" "$VERSION" "$STANDALONE_DECLARATION"
478
+ fi
450
479
  curl -s -o /dev/null -w " ${VERSION} status patch: HTTP %{http_code}\n" \
451
480
  -X PATCH "${BASE}/api/ci/releases/${RELEASE_ID}" \
452
481
  -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
package/sdlc/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-sdlc",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "DevAudit SDLC CLI Engine — cross-agent terminal-driven SDLC orchestrator.",
5
5
  "bin": {
6
6
  "devaudit-sdlc": "./src/bin/devaudit-sdlc.js"
@@ -45,13 +45,13 @@ gh pr merge [PR-NUMBER] --merge --delete-branch=false
45
45
 
46
46
  **Do NOT delete `develop`** — it's the permanent working branch.
47
47
 
48
- ### Step 2: Sync Branches
48
+ ### Step 2: Preserve GitFlow
49
49
 
50
- ```bash
51
- git checkout main && git pull origin main
52
- git checkout develop && git pull origin develop
53
- git merge main --no-edit && git push origin develop
54
- ```
50
+ Normal release synchronization is performed by the portal-dispatched close-out
51
+ PR after the release reaches `released`; do not directly merge the release
52
+ branch into the integration branch. A real hotfix is the exception: after its
53
+ reviewed merge to the release branch, open the mandatory `backmerge/*` PR to the
54
+ integration branch and merge it only after terminal-green checks.
55
55
 
56
56
  ### Step 3: Verify Production Deployment
57
57
 
@@ -149,11 +149,11 @@ The backend stores both with reviewer identity, SHA, and timestamp. This satisfi
149
149
 
150
150
  #### Steps
151
151
 
152
- 1. Wait for `post-deploy-prod.yml` to complete (the workflow's "Advance release status" step prints `Release vYYYY.MM.DD prod_review` when done).
152
+ 1. Wait for `post-deploy-prod.yml` to complete successfully **and** for its host-deployment check to confirm terminal success for the merged SHA.
153
153
  2. Open the release in DevAudit: `https://[DEVAUDIT_BASE_URL]/projects/[PROJECT_SLUG]/releases/[releaseId]`.
154
- 3. Review the `prod-smoke-results.json` evidence (uploaded by the workflow) plus any post-deploy actions logged in the release ticket.
154
+ 3. Review the production deployment and smoke cycle records, their evidence, and any post-deploy actions logged in the release ticket. Do not approve while either cycle or the host deployment remains queued/in progress.
155
155
  4. Click **Approve Production** — status transitions to `prod_approved`.
156
- 5. Click **Mark as Released** — status transitions to `released`. Pipeline lifecycle complete in DevAudit.
156
+ 5. Click **Mark as Released** — status transitions to `released`. This dispatches the automated close-out flow.
157
157
 
158
158
  If the smoke results look wrong or a manual verification fails, click **Reject** on the production approval and follow the Rollback procedure below before retrying.
159
159
 
@@ -161,41 +161,21 @@ If the smoke results look wrong or a manual verification fails, click **Reject**
161
161
 
162
162
  `approval.mode` is checked again here. `dual_actor` means the post-deploy approver must differ from the release creator. `solo_with_gap` accepts self-approval but records the control gap. `auto_low_risk` allows LOW-risk requirements to auto-advance through both transitions on workflow completion; MEDIUM/HIGH always require a human click.
163
163
 
164
- ### Step 6: Finalize Compliance (Tracked Requirements Only)
165
-
166
- > **Automated path (preferred).** Run the synced helper instead of doing this by hand — it flips the ticket Status → `RELEASED` (+ backlinks the release PR and records the sign-off), flips the matching `RTM.md` row, and `git mv`s the ticket to `approved-releases/`, then stages the changes for you to commit:
167
- >
168
- > ```bash
169
- > ./scripts/close-out-release.sh REQ-XXX --release-pr <release-PR-#>
170
- > git add -A && git commit -m "docs(compliance): close out REQ-XXX release ticket (RELEASED)" && git push origin develop
171
- > ```
172
- >
173
- > It is **idempotent** (a no-op if already closed out) and, when `DEVAUDIT_API_KEY` + `DEVAUDIT_BASE_URL` are set, **refuses** unless the portal reports the release as `released` (so you can't flip the local tree ahead of the Production approval). The `close-out-release.yml` workflow runs the same script automatically when the portal marks a release released (and is `workflow_dispatch`-able for catch-up). The manual steps below are the fallback / reference for what the script does.
174
-
175
- ```bash
176
- mv compliance/pending-releases/RELEASE-TICKET-REQ-XXX.md compliance/approved-releases/
177
- ```
164
+ ### Step 6: Automated Close-out (Tracked Requirements Only)
178
165
 
179
- Update `compliance/RTM.md`:
180
- ```markdown
181
- | REQ-XXX | Description | [RISK] | files | evidence | APPROVED - DEPLOYED | [Reviewer] | [Date] |
182
- ```
166
+ When the portal status becomes `released`, it dispatches `release-closed` to the
167
+ consumer repository. `close-out-release.yml` then opens a
168
+ `chore/close-out-REQ-XXX` PR to the configured integration branch. That PR is
169
+ the canonical reconciliation record: it updates the RTM, moves the release
170
+ ticket to `approved-releases/`, and moves manifest-listed absorbed predecessors
171
+ to `superseded-releases/`.
183
172
 
184
- Add audit trail to release ticket:
185
- ```markdown
186
- | [date] | UAT verification passed | [who] | Health + smoke + feature verified on UAT |
187
- | [date] | PR approved | [reviewer] | PR #[number] |
188
- | [date] | CI verification | GitHub Actions | All gates passed independently |
189
- | [date] | Deployed to production | System | Auto-deploy from main |
190
- | [date] | PROD post-deploy verification | [who] | Health + security checks passed on PROD |
191
- ```
192
-
193
- ```bash
194
- git add compliance/RTM.md compliance/approved-releases/ compliance/evidence/REQ-XXX/
195
- git rm compliance/pending-releases/RELEASE-TICKET-REQ-XXX.md 2>/dev/null
196
- git commit -m "compliance: [REQ-XXX] approved and deployed - PR #[number]"
197
- git push origin develop
198
- ```
173
+ Review and merge the close-out PR after its required checks are terminal green
174
+ on its current head SHA. It is an administrative reconciliation PR, not a new
175
+ production release. If the dispatch did not arrive, manually dispatch the
176
+ Release Close-out workflow. Run `scripts/close-out-release.sh` locally only for
177
+ documented recovery or historical catch-up; never create a normal direct
178
+ close-out commit on a protected branch.
199
179
 
200
180
  ### Step 7: Close the GitHub Issue
201
181
 
@@ -207,17 +187,10 @@ gh issue close [ISSUE-NUMBER] --comment "Implemented in PR #[PR-NUMBER] (REQ-XXX
207
187
 
208
188
  This is the final traceability link: Issue → Requirement → PR → Deployment → Issue closed.
209
189
 
210
- ### Step 8: Final Sync
211
-
212
- ```bash
213
- git checkout main && git merge develop --no-edit && git push origin main
214
- git checkout develop
215
- ```
216
-
217
190
  ## Rollback
218
191
 
219
192
  1. **Hosting dashboard:** Redeploy previous version
220
- 2. **Git:** `git checkout main && git revert HEAD --no-edit && git push origin main`
193
+ 2. **Git:** create `hotfix/revert-REQ-XXX` from the release branch, revert the merge, and open a reviewed PR to the release branch. After terminal-green checks and merge, open the mandatory backmerge PR to integration.
221
194
  3. **Document:** Add rollback entry to release ticket audit trail
222
195
  4. **Correlate with incident (DevAudit-Installer#210 §10a):** If the rollback was triggered by a post-deploy smoke failure, the `post-deploy-prod.yml` workflow files an incident issue with the `incident` label. Reference this incident in the rollback commit message (`Fixes #N` or a comment on the issue) so the incident report captures the containment action. This ensures `incident-export.yml` exports the full incident timeline including the rollback.
223
196