@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.
@@ -32,7 +32,8 @@ jobs:
32
32
  # ──────────────────────────────────────────────
33
33
  quality-gates:
34
34
  name: Quality Gates
35
- if: ${{ github.event_name != 'pull_request' || !startsWith(github.head_ref, 'chore/close-out-') }}
35
+ needs: [register-release]
36
+ if: ${{ always() && (github.event_name != 'pull_request' || !startsWith(github.head_ref, 'chore/close-out-')) && (github.event_name == 'pull_request' || needs.register-release.result == 'success') }}
36
37
  runs-on: {{RUNNER}}
37
38
 
38
39
  services:
@@ -44,6 +45,8 @@ jobs:
44
45
  env:
45
46
  {{DATABASE_ENV}}
46
47
  {{APP_ENV}}
48
+ DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
49
+ DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
47
50
 
48
51
  steps:
49
52
  - uses: actions/checkout@v6
@@ -54,6 +57,29 @@ jobs:
54
57
  # tag each captured screenshot as feature vs regression.
55
58
  fetch-depth: 0
56
59
 
60
+ - name: Start authoritative quality-gate cycle
61
+ if: github.event_name != 'pull_request' && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.outputs.version != 'skip'
62
+ run: |
63
+ BASE=""
64
+ if [ -f sdlc-config.json ]; then
65
+ BASE=$(jq -r '.devaudit.base_url // empty' sdlc-config.json 2>/dev/null || true)
66
+ fi
67
+ [ -n "$BASE" ] || BASE="$DEVAUDIT_BASE_URL_VAR"
68
+ [ -n "$BASE" ] || { echo "::warning::No DevAudit URL; cycle lifecycle disabled"; exit 0; }
69
+ echo "DEVAUDIT_BASE_URL=${BASE%/}" >> "$GITHUB_ENV"
70
+ chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
71
+ bash scripts/report-test-cycle.sh start \
72
+ --project-slug {{PROJECT_SLUG}} \
73
+ --release "${{ needs.register-release.outputs.version }}" \
74
+ --sdlc-stage 2 --environment ci --cycle-kind quality_gate \
75
+ --provider github_actions --external-run-id "${{ github.run_id }}" \
76
+ --external-run-attempt "${{ github.run_attempt }}" \
77
+ --external-job-id "quality-gates" \
78
+ --idempotency-key "github:${{ github.repository }}:quality-gates.quality_gate:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.register-release.outputs.version }}" \
79
+ --commit-sha "${{ github.sha }}" --branch "${{ github.ref_name }}" \
80
+ --workflow-name "Quality Gates" \
81
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
82
+
57
83
  # ── Cached installs (skip if already present on self-hosted runner) ──
58
84
 
59
85
  - uses: actions/setup-node@v6
@@ -310,6 +336,26 @@ jobs:
310
336
  --environment uat --category planning \
311
337
  --git-sha ${{ github.sha }} --branch ${{ github.ref_name }} || true
312
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
+
313
359
  - name: Generate and upload bundled changes (REQ releases only)
314
360
  if: env.DEVAUDIT_BASE_URL != '' && steps.version.outputs.version != 'skip'
315
361
  run: |
@@ -418,7 +464,7 @@ jobs:
418
464
  # evidence — `status=failed` is itself the audit trail. `!cancelled()`
419
465
  # still guards against partial state on operator-cancel.
420
466
  # DevAudit-Installer#96.
421
- if: ${{ always() && !cancelled() && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.result == 'success' }}
467
+ if: ${{ always() && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.result == 'success' }}
422
468
  env:
423
469
  DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
424
470
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
@@ -465,15 +511,12 @@ jobs:
465
511
  bash scripts/report-test-cycle.sh start \
466
512
  --project-slug {{PROJECT_SLUG}} \
467
513
  --release "${{ needs.register-release.outputs.version }}" \
468
- --sdlc-stage 2 \
469
- --environment uat \
470
- --cycle-kind quality_gate \
471
- --provider github_actions \
472
- --external-run-id "${{ github.run_id }}" \
514
+ --sdlc-stage 2 --environment ci --cycle-kind quality_gate \
515
+ --provider github_actions --external-run-id "${{ github.run_id }}" \
473
516
  --external-run-attempt "${{ github.run_attempt }}" \
517
+ --external-job-id "quality-gates" \
474
518
  --idempotency-key "github:${{ github.repository }}:quality-gates.quality_gate:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.register-release.outputs.version }}" \
475
- --commit-sha "${{ github.sha }}" \
476
- --branch "${{ github.ref_name }}" \
519
+ --commit-sha "${{ github.sha }}" --branch "${{ github.ref_name }}" \
477
520
  --workflow-name "Quality Gates" \
478
521
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
479
522
  --output-file "$CYCLE_OUT"
@@ -882,27 +925,41 @@ jobs:
882
925
  - name: Complete primary quality-gate cycle
883
926
  if: always() && env.DEVAUDIT_BASE_URL != ''
884
927
  run: |
885
- case "${{ job.status }}" in
928
+ case "${{ needs.quality-gates.result }}" in
886
929
  success) OUTCOME=passed ;;
887
930
  cancelled) OUTCOME=cancelled ;;
888
931
  *) OUTCOME=failed ;;
889
932
  esac
890
- chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
933
+ chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
891
934
  bash scripts/report-test-cycle.sh complete \
892
935
  --project-slug {{PROJECT_SLUG}} \
893
936
  --release "${{ needs.register-release.outputs.version }}" \
894
937
  --sdlc-stage 2 \
895
- --environment uat \
938
+ --environment ci \
896
939
  --cycle-kind quality_gate \
897
940
  --provider github_actions \
898
941
  --external-run-id "${{ github.run_id }}" \
899
942
  --external-run-attempt "${{ github.run_attempt }}" \
943
+ --external-job-id "quality-gates" \
900
944
  --idempotency-key "github:${{ github.repository }}:quality-gates.quality_gate:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.register-release.outputs.version }}" \
901
945
  --commit-sha "${{ github.sha }}" \
902
946
  --branch "${{ github.ref_name }}" \
903
947
  --workflow-name "Quality Gates" \
904
948
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
905
949
  --outcome "$OUTCOME"
950
+ case "$OUTCOME" in
951
+ passed) CHECK_STATUS=successful ;;
952
+ *) CHECK_STATUS="$OUTCOME" ;;
953
+ esac
954
+ bash scripts/report-release-check.sh \
955
+ --project-slug {{PROJECT_SLUG}} \
956
+ --release "${{ needs.register-release.outputs.version }}" \
957
+ --check-key "quality-gates:${{ github.run_id }}:${{ github.run_attempt }}" \
958
+ --label "Quality Gates" --provider github_actions --status "$CHECK_STATUS" \
959
+ --external-run-id "${{ github.run_id }}" \
960
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
961
+ --commit-sha "${{ github.sha }}" --branch "${{ github.ref_name }}" \
962
+ --details-json '{"executionSource":"needs.quality-gates.result","evidenceUploadJob":"${{ job.status }}"}'
906
963
 
907
964
  - name: Summary
908
965
  run: echo "Evidence uploaded for ${{ needs.register-release.outputs.version }} (UAT)"
@@ -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 }}
@@ -768,6 +738,7 @@ jobs:
768
738
  if: steps.resolve.outputs.skip != 'true' && steps.version.outputs.version != 'skip'
769
739
  run: |
770
740
  set -euo pipefail
741
+ chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
771
742
  DERIVED_RELEASE="${{ steps.version.outputs.version }}"
772
743
 
773
744
  # Tier metadata for the portal (the operator filters/groups by
@@ -873,6 +844,7 @@ jobs:
873
844
  --branch "${PRIOR_BRANCH}" \
874
845
  --workflow-name "E2E Regression" \
875
846
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
847
+ --started-at "${{ github.event.workflow_run.run_started_at }}" \
876
848
  --output-file "$CYCLE_OUT"
877
849
  . "$CYCLE_OUT"
878
850
  rm -f "$CYCLE_OUT"
@@ -927,11 +899,15 @@ jobs:
927
899
  REQ_FAILURES=$((REQ_FAILURES + 1))
928
900
  fi
929
901
  fi
930
- if [ "$REQ_FAILURES" -gt 0 ]; then
931
- REQ_OUTCOME=failed
932
- else
933
- REQ_OUTCOME=passed
934
- fi
902
+ case "${{ github.event.workflow_run.conclusion }}" in
903
+ success) REQ_OUTCOME=passed ;;
904
+ failure) REQ_OUTCOME=failed ;;
905
+ cancelled) REQ_OUTCOME=cancelled ;;
906
+ skipped) REQ_OUTCOME=skipped ;;
907
+ timed_out) REQ_OUTCOME=timed_out ;;
908
+ action_required) REQ_OUTCOME=action_required ;;
909
+ *) REQ_OUTCOME=unknown ;;
910
+ esac
935
911
  bash scripts/report-test-cycle.sh complete \
936
912
  --project-slug {{PROJECT_SLUG}} \
937
913
  --release "${DERIVED_RELEASE}" \
@@ -946,8 +922,28 @@ jobs:
946
922
  --branch "${PRIOR_BRANCH}" \
947
923
  --workflow-name "E2E Regression" \
948
924
  --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
949
- --started-at "${cycle_started_at:-}" \
925
+ --started-at "${{ github.event.workflow_run.run_started_at }}" \
926
+ --completed-at "${{ github.event.workflow_run.updated_at }}" \
927
+ --outcome-reason "authoritative workflow_run conclusion: ${{ github.event.workflow_run.conclusion }}" \
950
928
  --outcome "${REQ_OUTCOME}"
929
+ case "$REQ_OUTCOME" in
930
+ passed) CHECK_STATUS=successful ;;
931
+ *) CHECK_STATUS="$REQ_OUTCOME" ;;
932
+ esac
933
+ CHECK_DETAILS=$(jq -cn \
934
+ --arg conclusion "${{ github.event.workflow_run.conclusion }}" \
935
+ --argjson artifactUploadFailures "$REQ_FAILURES" \
936
+ '{executionSource:"workflow_run.conclusion",conclusion:$conclusion,artifactUploadFailures:$artifactUploadFailures}')
937
+ bash scripts/report-release-check.sh \
938
+ --project-slug {{PROJECT_SLUG}} --release "${DERIVED_RELEASE}" \
939
+ --check-key "e2e-regression:${{ github.event.workflow_run.id }}:${{ github.event.workflow_run.run_attempt }}:${REQ}" \
940
+ --label "E2E Regression (${REQ})" --provider github_actions --status "$CHECK_STATUS" \
941
+ --external-run-id "${{ github.event.workflow_run.id }}" \
942
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \
943
+ --commit-sha "${{ github.event.workflow_run.head_sha }}" --branch "${PRIOR_BRANCH}" \
944
+ --started-at "${{ github.event.workflow_run.run_started_at }}" \
945
+ --completed-at "${{ github.event.workflow_run.updated_at }}" \
946
+ --details-json "$CHECK_DETAILS"
951
947
  done
952
948
 
953
949
  # Per-spec failure screenshots from test-results/ are NOT
@@ -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,25 +119,66 @@ 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}"
118
139
  echo "REQS=${REQS}" >> "$GITHUB_ENV"
119
140
 
141
+ - name: Start production deployment cycles
142
+ run: |
143
+ chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
144
+ for PREFIX in ${REQS}; do
145
+ RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
146
+ "${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
147
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
148
+ [ -n "$VERSION" ] || VERSION="$PREFIX"
149
+ bash scripts/report-test-cycle.sh start \
150
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
151
+ --sdlc-stage 5 --environment production --cycle-kind deployment \
152
+ --provider github_actions --external-run-id "$CI_RUN" \
153
+ --external-run-attempt "${{ github.run_attempt }}" --external-job-id "host-deployment" \
154
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.deployment:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
155
+ --commit-sha "$GIT_SHA" --branch main \
156
+ --workflow-name "Post-Deploy Production" \
157
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
158
+ done
159
+
120
160
  - name: Wait for production deployment
161
+ id: wait_deployment
121
162
  run: |
163
+ DEPLOY_READY=false
122
164
  for i in $(seq 1 30); do
123
165
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${PROD_URL}/" || echo "000")
124
166
  if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 400 ]; then
125
167
  echo "Production is up (HTTP ${HTTP_CODE})"
168
+ DEPLOY_READY=true
126
169
  break
127
170
  fi
128
171
  echo "Attempt ${i}/30: HTTP ${HTTP_CODE} — waiting 10s..."
129
172
  sleep 10
130
173
  done
174
+ if [ "$DEPLOY_READY" != "true" ]; then
175
+ echo "::error::Production did not become ready within the deployment window"
176
+ exit 1
177
+ fi
131
178
 
132
179
  - name: Confirm terminal host deployment success
180
+ id: host_deployment
181
+ if: steps.wait_deployment.outcome == 'success'
133
182
  env:
134
183
  GH_TOKEN: ${{ github.token }}
135
184
  run: |
@@ -140,7 +189,68 @@ jobs:
140
189
  --max-attempts=30 \
141
190
  --poll-seconds=10
142
191
 
192
+ - name: Complete production deployment cycles
193
+ if: always() && env.BASE != ''
194
+ run: |
195
+ if [ "${{ steps.wait_deployment.outcome }}" = "success" ] && [ "${{ steps.host_deployment.outcome }}" = "success" ]; then
196
+ OUTCOME=passed
197
+ CHECK_STATUS=successful
198
+ elif [ "${{ steps.wait_deployment.outcome }}" = "cancelled" ] || [ "${{ steps.host_deployment.outcome }}" = "cancelled" ]; then
199
+ OUTCOME=cancelled
200
+ CHECK_STATUS=cancelled
201
+ else
202
+ OUTCOME=failed
203
+ CHECK_STATUS=failed
204
+ fi
205
+ chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
206
+ for PREFIX in ${REQS}; do
207
+ RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
208
+ "${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
209
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
210
+ [ -n "$VERSION" ] || VERSION="$PREFIX"
211
+ bash scripts/report-test-cycle.sh complete \
212
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
213
+ --sdlc-stage 5 --environment production --cycle-kind deployment \
214
+ --provider github_actions --external-run-id "$CI_RUN" \
215
+ --external-run-attempt "${{ github.run_attempt }}" --external-job-id "host-deployment" \
216
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.deployment:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
217
+ --commit-sha "$GIT_SHA" --branch main \
218
+ --workflow-name "Post-Deploy Production" \
219
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
220
+ --outcome "$OUTCOME" --outcome-reason "host deployment: ${{ steps.host_deployment.outcome }}"
221
+ bash scripts/report-release-check.sh \
222
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
223
+ --check-key "production-deployment:${CI_RUN}:${{ github.run_attempt }}" \
224
+ --label "Production Deployment" --provider github_actions --status "$CHECK_STATUS" \
225
+ --external-run-id "$CI_RUN" \
226
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
227
+ --commit-sha "$GIT_SHA" --branch main \
228
+ --details-json '{"waitOutcome":"${{ steps.wait_deployment.outcome }}","hostOutcome":"${{ steps.host_deployment.outcome }}"}'
229
+ done
230
+
231
+ - name: Start production smoke cycles
232
+ if: steps.wait_deployment.outcome == 'success' && steps.host_deployment.outcome == 'success'
233
+ run: |
234
+ chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
235
+ for PREFIX in ${REQS}; do
236
+ RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
237
+ "${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
238
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
239
+ [ -n "$VERSION" ] || VERSION="$PREFIX"
240
+ bash scripts/report-test-cycle.sh start \
241
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
242
+ --sdlc-stage 5 --environment production --cycle-kind smoke \
243
+ --provider github_actions --external-run-id "$CI_RUN" \
244
+ --external-run-attempt "${{ github.run_attempt }}" --external-job-id "production-smoke" \
245
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
246
+ --commit-sha "$GIT_SHA" --branch main \
247
+ --workflow-name "Post-Deploy Production" \
248
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
249
+ done
250
+
143
251
  - name: Production smoke tests (read-only)
252
+ id: production_smoke
253
+ if: steps.wait_deployment.outcome == 'success' && steps.host_deployment.outcome == 'success'
144
254
  run: |
145
255
  echo "=== Production Smoke Tests ==="
146
256
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${PROD_URL}/")
@@ -163,8 +273,43 @@ jobs:
163
273
  }
164
274
  RESULTS_EOF
165
275
 
276
+ - name: Complete production smoke cycles
277
+ if: always() && env.BASE != '' && steps.host_deployment.outcome == 'success'
278
+ run: |
279
+ case "${{ steps.production_smoke.outcome }}" in
280
+ success) OUTCOME=passed; CHECK_STATUS=successful ;;
281
+ cancelled) OUTCOME=cancelled; CHECK_STATUS=cancelled ;;
282
+ skipped) OUTCOME=skipped; CHECK_STATUS=skipped ;;
283
+ *) OUTCOME=failed; CHECK_STATUS=failed ;;
284
+ esac
285
+ chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
286
+ for PREFIX in ${REQS}; do
287
+ RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
288
+ "${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
289
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
290
+ [ -n "$VERSION" ] || VERSION="$PREFIX"
291
+ bash scripts/report-test-cycle.sh complete \
292
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
293
+ --sdlc-stage 5 --environment production --cycle-kind smoke \
294
+ --provider github_actions --external-run-id "$CI_RUN" \
295
+ --external-run-attempt "${{ github.run_attempt }}" --external-job-id "production-smoke" \
296
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
297
+ --commit-sha "$GIT_SHA" --branch main \
298
+ --workflow-name "Post-Deploy Production" \
299
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
300
+ --outcome "$OUTCOME" --outcome-reason "production smoke: ${{ steps.production_smoke.outcome }}"
301
+ bash scripts/report-release-check.sh \
302
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
303
+ --check-key "production-smoke:${CI_RUN}:${{ github.run_attempt }}" \
304
+ --label "Production Smoke" --provider github_actions --status "$CHECK_STATUS" \
305
+ --external-run-id "$CI_RUN" \
306
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
307
+ --commit-sha "$GIT_SHA" --branch main \
308
+ --details-json '{"executionSource":"steps.production_smoke.outcome"}'
309
+ done
310
+
166
311
  - name: File incident on smoke failure
167
- if: failure()
312
+ if: steps.production_smoke.outcome == 'failure'
168
313
  env:
169
314
  # GitHub issue/label mutations use the workflow token. DevAudit
170
315
  # user tokens are portal PATs, not guaranteed GitHub PATs (#305).
@@ -241,9 +386,11 @@ jobs:
241
386
  fi
242
387
 
243
388
  - name: Promote in-scope releases (evidence + status)
389
+ if: steps.production_smoke.outcome == 'success'
244
390
  run: |
245
- chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
391
+ chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
246
392
  PROMOTED=0
393
+ EVIDENCE_FAILURES=0
247
394
  for PREFIX in ${REQS}; do
248
395
  echo "=== Promoting ${PREFIX} ==="
249
396
  RESP=$(curl -s -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
@@ -263,6 +410,7 @@ jobs:
263
410
  --provider github_actions \
264
411
  --external-run-id "${CI_RUN}" \
265
412
  --external-run-attempt "${{ github.run_attempt }}" \
413
+ --external-job-id "production-smoke" \
266
414
  --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
267
415
  --commit-sha "${GIT_SHA}" \
268
416
  --branch main \
@@ -286,6 +434,9 @@ jobs:
286
434
  # Carry the release ticket into the production environment so the
287
435
  # prod-review gate is self-contained.
288
436
  TICKET=""
437
+ if [ -n "${STANDALONE_DECLARATION:-}" ] && [ "$VERSION" = "${REQS}" ]; then
438
+ TICKET="$STANDALONE_DECLARATION"
439
+ fi
289
440
  for DIR in compliance/pending-releases compliance/approved-releases; do
290
441
  if [ -f "${DIR}/RELEASE-TICKET-${VERSION}.md" ]; then
291
442
  TICKET="${DIR}/RELEASE-TICKET-${VERSION}.md"; break
@@ -296,33 +447,35 @@ jobs:
296
447
  "${PROJECT_SLUG}" "${VERSION}" release_ticket "$TICKET" \
297
448
  --release "${VERSION}" --create-release-if-missing --environment production \
298
449
  --category release_artifact --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
299
- || echo "Warning: ticket upload failed for ${VERSION}"
450
+ || { echo "Warning: ticket upload failed for ${VERSION}"; REQ_FAILURES=$((REQ_FAILURES + 1)); }
300
451
  else
301
452
  echo "No RELEASE-TICKET-${VERSION}.md found — skipping ticket (date-versioned or archived)."
302
453
  fi
303
454
  if [ "$REQ_FAILURES" -gt 0 ]; then
304
- REQ_OUTCOME=failed
455
+ EVIDENCE_STATUS=failed
456
+ EVIDENCE_FAILURES=$((EVIDENCE_FAILURES + 1))
305
457
  else
306
- REQ_OUTCOME=passed
458
+ EVIDENCE_STATUS=successful
459
+ fi
460
+ bash scripts/report-release-check.sh \
461
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
462
+ --check-key "production-evidence:${CI_RUN}:${{ github.run_attempt }}" \
463
+ --label "Production Evidence Completeness" --provider devaudit_installer \
464
+ --status "$EVIDENCE_STATUS" --external-run-id "$CI_RUN" \
465
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
466
+ --commit-sha "$GIT_SHA" --branch main \
467
+ --details-json "{\"uploadFailures\":${REQ_FAILURES}}"
468
+ if [ "$REQ_FAILURES" -gt 0 ]; then
469
+ echo "::error::Skipping status promotion for ${VERSION}: production evidence is incomplete"
470
+ continue
307
471
  fi
308
- bash scripts/report-test-cycle.sh complete \
309
- --project-slug "${PROJECT_SLUG}" \
310
- --release "${VERSION}" \
311
- --sdlc-stage 5 \
312
- --environment production \
313
- --cycle-kind smoke \
314
- --provider github_actions \
315
- --external-run-id "${CI_RUN}" \
316
- --external-run-attempt "${{ github.run_attempt }}" \
317
- --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
318
- --commit-sha "${GIT_SHA}" \
319
- --branch main \
320
- --workflow-name "Post-Deploy Production" \
321
- --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
322
- --started-at "${cycle_started_at:-}" \
323
- --outcome "${REQ_OUTCOME}"
324
472
  # Advance status (idempotent — re-PATCHing an already-promoted release is a no-op).
325
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
326
479
  curl -s -o /dev/null -w " ${VERSION} status patch: HTTP %{http_code}\n" \
327
480
  -X PATCH "${BASE}/api/ci/releases/${RELEASE_ID}" \
328
481
  -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
@@ -334,6 +487,10 @@ jobs:
334
487
  echo "::warning::No release_id resolved for ${PREFIX} — skipping status patch"
335
488
  fi
336
489
  done
490
+ if [ "$EVIDENCE_FAILURES" -gt 0 ]; then
491
+ echo "::error::${EVIDENCE_FAILURES} release(s) have incomplete production evidence"
492
+ exit 1
493
+ fi
337
494
  echo "Promoted ${PROMOTED} release(s) to ${TERMINAL_STATUS}."
338
495
  if [ "${TERMINAL_STATUS}" = "prod_review" ]; then
339
496
  echo "Next: a human in the portal clicks 'Approve Production' then 'Mark as Released' for each."
package/sdlc/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-sdlc",
3
- "version": "0.3.13",
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"