@metasession.co/devaudit-cli 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/index.js +53 -4
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/upload-evidence.sh +55 -1
  5. package/sdlc/files/_common/0-project-setup.md +10 -4
  6. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
  7. package/sdlc/files/_common/README_TEMPLATE.md +8 -8
  8. package/sdlc/files/_common/Test_Architecture.md +15 -15
  9. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
  10. package/sdlc/files/_common/Test_Policy.md +2 -2
  11. package/sdlc/files/_common/Test_Strategy.md +10 -10
  12. package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
  13. package/sdlc/files/_common/governance/dpia.md.template +3 -3
  14. package/sdlc/files/_common/governance/incident-report.md.template +3 -3
  15. package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
  16. package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
  17. package/sdlc/files/_common/governance/risk-register.md.template +1 -1
  18. package/sdlc/files/_common/governance/ropa.md.template +2 -2
  19. package/sdlc/files/_common/joining-an-existing-project.md +4 -4
  20. package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
  21. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
  22. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
  23. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
  24. package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
  25. package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
  26. package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
  27. package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
  28. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +311 -64
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +133 -82
  30. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
  31. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
  32. package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
  33. package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
  34. package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
  35. package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
  36. package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
  37. package/sdlc/files/_common/scripts/report-release-check.sh +86 -0
  38. package/sdlc/files/_common/scripts/report-release-check.test.sh +97 -0
  39. package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
  40. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  41. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  42. package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
  43. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
  44. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
  45. package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
  46. package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
  47. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
  48. package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
  49. package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
  50. package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
  51. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
  52. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
  53. package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
  54. package/sdlc/files/ci/ci.yml.template +119 -28
  55. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  56. package/sdlc/files/ci/compliance-evidence.yml.template +230 -20
  57. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  58. package/sdlc/files/ci/post-deploy-prod.yml.template +189 -6
  59. package/sdlc/files/ci/python/ci.yml.template +60 -12
  60. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  61. package/sdlc/package.json +1 -1
  62. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  63. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  64. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  65. package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
  66. package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
  67. package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
@@ -36,6 +36,7 @@ jobs:
36
36
  runs-on: {{RUNNER}}
37
37
  permissions:
38
38
  contents: read
39
+ deployments: read
39
40
  issues: write
40
41
  env:
41
42
  DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
@@ -116,19 +117,119 @@ jobs:
116
117
  echo "In-scope releases to promote: ${REQS}"
117
118
  echo "REQS=${REQS}" >> "$GITHUB_ENV"
118
119
 
120
+ - name: Start production deployment cycles
121
+ run: |
122
+ chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
123
+ for PREFIX in ${REQS}; do
124
+ RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
125
+ "${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
126
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
127
+ [ -n "$VERSION" ] || VERSION="$PREFIX"
128
+ bash scripts/report-test-cycle.sh start \
129
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
130
+ --sdlc-stage 5 --environment production --cycle-kind deployment \
131
+ --provider github_actions --external-run-id "$CI_RUN" \
132
+ --external-run-attempt "${{ github.run_attempt }}" --external-job-id "host-deployment" \
133
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.deployment:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
134
+ --commit-sha "$GIT_SHA" --branch main \
135
+ --workflow-name "Post-Deploy Production" \
136
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
137
+ done
138
+
119
139
  - name: Wait for production deployment
140
+ id: wait_deployment
120
141
  run: |
142
+ DEPLOY_READY=false
121
143
  for i in $(seq 1 30); do
122
144
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${PROD_URL}/" || echo "000")
123
145
  if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 400 ]; then
124
146
  echo "Production is up (HTTP ${HTTP_CODE})"
147
+ DEPLOY_READY=true
125
148
  break
126
149
  fi
127
150
  echo "Attempt ${i}/30: HTTP ${HTTP_CODE} — waiting 10s..."
128
151
  sleep 10
129
152
  done
153
+ if [ "$DEPLOY_READY" != "true" ]; then
154
+ echo "::error::Production did not become ready within the deployment window"
155
+ exit 1
156
+ fi
157
+
158
+ - name: Confirm terminal host deployment success
159
+ id: host_deployment
160
+ if: steps.wait_deployment.outcome == 'success'
161
+ env:
162
+ GH_TOKEN: ${{ github.token }}
163
+ run: |
164
+ chmod +x scripts/check-host-deployment.sh 2>/dev/null || true
165
+ bash scripts/check-host-deployment.sh \
166
+ --repo="${{ github.repository }}" \
167
+ --sha="${GIT_SHA}" \
168
+ --max-attempts=30 \
169
+ --poll-seconds=10
170
+
171
+ - name: Complete production deployment cycles
172
+ if: always() && env.BASE != ''
173
+ run: |
174
+ if [ "${{ steps.wait_deployment.outcome }}" = "success" ] && [ "${{ steps.host_deployment.outcome }}" = "success" ]; then
175
+ OUTCOME=passed
176
+ CHECK_STATUS=successful
177
+ elif [ "${{ steps.wait_deployment.outcome }}" = "cancelled" ] || [ "${{ steps.host_deployment.outcome }}" = "cancelled" ]; then
178
+ OUTCOME=cancelled
179
+ CHECK_STATUS=cancelled
180
+ else
181
+ OUTCOME=failed
182
+ CHECK_STATUS=failed
183
+ fi
184
+ chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
185
+ for PREFIX in ${REQS}; do
186
+ RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
187
+ "${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
188
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
189
+ [ -n "$VERSION" ] || VERSION="$PREFIX"
190
+ bash scripts/report-test-cycle.sh complete \
191
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
192
+ --sdlc-stage 5 --environment production --cycle-kind deployment \
193
+ --provider github_actions --external-run-id "$CI_RUN" \
194
+ --external-run-attempt "${{ github.run_attempt }}" --external-job-id "host-deployment" \
195
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.deployment:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
196
+ --commit-sha "$GIT_SHA" --branch main \
197
+ --workflow-name "Post-Deploy Production" \
198
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
199
+ --outcome "$OUTCOME" --outcome-reason "host deployment: ${{ steps.host_deployment.outcome }}"
200
+ bash scripts/report-release-check.sh \
201
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
202
+ --check-key "production-deployment:${CI_RUN}:${{ github.run_attempt }}" \
203
+ --label "Production Deployment" --provider github_actions --status "$CHECK_STATUS" \
204
+ --external-run-id "$CI_RUN" \
205
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
206
+ --commit-sha "$GIT_SHA" --branch main \
207
+ --details-json '{"waitOutcome":"${{ steps.wait_deployment.outcome }}","hostOutcome":"${{ steps.host_deployment.outcome }}"}'
208
+ done
209
+
210
+ - name: Start production smoke cycles
211
+ if: steps.wait_deployment.outcome == 'success' && steps.host_deployment.outcome == 'success'
212
+ run: |
213
+ chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
214
+ for PREFIX in ${REQS}; do
215
+ RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
216
+ "${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
217
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
218
+ [ -n "$VERSION" ] || VERSION="$PREFIX"
219
+ bash scripts/report-test-cycle.sh start \
220
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
221
+ --sdlc-stage 5 --environment production --cycle-kind smoke \
222
+ --provider github_actions --external-run-id "$CI_RUN" \
223
+ --external-run-attempt "${{ github.run_attempt }}" --external-job-id "production-smoke" \
224
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
225
+ --commit-sha "$GIT_SHA" --branch main \
226
+ --workflow-name "Post-Deploy Production" \
227
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
228
+ done
130
229
 
131
230
  - name: Production smoke tests (read-only)
231
+ id: production_smoke
232
+ if: steps.wait_deployment.outcome == 'success' && steps.host_deployment.outcome == 'success'
132
233
  run: |
133
234
  echo "=== Production Smoke Tests ==="
134
235
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${PROD_URL}/")
@@ -151,8 +252,43 @@ jobs:
151
252
  }
152
253
  RESULTS_EOF
153
254
 
255
+ - name: Complete production smoke cycles
256
+ if: always() && env.BASE != '' && steps.host_deployment.outcome == 'success'
257
+ run: |
258
+ case "${{ steps.production_smoke.outcome }}" in
259
+ success) OUTCOME=passed; CHECK_STATUS=successful ;;
260
+ cancelled) OUTCOME=cancelled; CHECK_STATUS=cancelled ;;
261
+ skipped) OUTCOME=skipped; CHECK_STATUS=skipped ;;
262
+ *) OUTCOME=failed; CHECK_STATUS=failed ;;
263
+ esac
264
+ chmod +x scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
265
+ for PREFIX in ${REQS}; do
266
+ RESP=$(curl -fsS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
267
+ "${BASE}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${PREFIX}")
268
+ VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
269
+ [ -n "$VERSION" ] || VERSION="$PREFIX"
270
+ bash scripts/report-test-cycle.sh complete \
271
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
272
+ --sdlc-stage 5 --environment production --cycle-kind smoke \
273
+ --provider github_actions --external-run-id "$CI_RUN" \
274
+ --external-run-attempt "${{ github.run_attempt }}" --external-job-id "production-smoke" \
275
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
276
+ --commit-sha "$GIT_SHA" --branch main \
277
+ --workflow-name "Post-Deploy Production" \
278
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
279
+ --outcome "$OUTCOME" --outcome-reason "production smoke: ${{ steps.production_smoke.outcome }}"
280
+ bash scripts/report-release-check.sh \
281
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
282
+ --check-key "production-smoke:${CI_RUN}:${{ github.run_attempt }}" \
283
+ --label "Production Smoke" --provider github_actions --status "$CHECK_STATUS" \
284
+ --external-run-id "$CI_RUN" \
285
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
286
+ --commit-sha "$GIT_SHA" --branch main \
287
+ --details-json '{"executionSource":"steps.production_smoke.outcome"}'
288
+ done
289
+
154
290
  - name: File incident on smoke failure
155
- if: failure()
291
+ if: steps.production_smoke.outcome == 'failure'
156
292
  env:
157
293
  # GitHub issue/label mutations use the workflow token. DevAudit
158
294
  # user tokens are portal PATs, not guaranteed GitHub PATs (#305).
@@ -229,9 +365,11 @@ jobs:
229
365
  fi
230
366
 
231
367
  - name: Promote in-scope releases (evidence + status)
368
+ if: steps.production_smoke.outcome == 'success'
232
369
  run: |
233
- chmod +x scripts/upload-evidence.sh 2>/dev/null || true
370
+ chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh scripts/report-release-check.sh 2>/dev/null || true
234
371
  PROMOTED=0
372
+ EVIDENCE_FAILURES=0
235
373
  for PREFIX in ${REQS}; do
236
374
  echo "=== Promoting ${PREFIX} ==="
237
375
  RESP=$(curl -s -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
@@ -239,14 +377,38 @@ jobs:
239
377
  VERSION=$(echo "$RESP" | jq -r '.latest.version // empty')
240
378
  [ -z "$VERSION" ] && VERSION="${PREFIX}"
241
379
  RELEASE_ID=$(echo "$RESP" | jq -r '.latest.id // empty')
380
+ REQ_FAILURES=0
381
+ LINEAGE_FLAGS=(--test-cycle "${CI_RUN}")
382
+ CYCLE_OUT="$(mktemp)"
383
+ bash scripts/report-test-cycle.sh start \
384
+ --project-slug "${PROJECT_SLUG}" \
385
+ --release "${VERSION}" \
386
+ --sdlc-stage 5 \
387
+ --environment production \
388
+ --cycle-kind smoke \
389
+ --provider github_actions \
390
+ --external-run-id "${CI_RUN}" \
391
+ --external-run-attempt "${{ github.run_attempt }}" \
392
+ --external-job-id "production-smoke" \
393
+ --idempotency-key "github:${{ github.repository }}:post-deploy-prod.smoke:${CI_RUN}:${{ github.run_attempt }}:5:${VERSION}" \
394
+ --commit-sha "${GIT_SHA}" \
395
+ --branch main \
396
+ --workflow-name "Post-Deploy Production" \
397
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
398
+ --output-file "$CYCLE_OUT"
399
+ . "$CYCLE_OUT"
400
+ rm -f "$CYCLE_OUT"
401
+ if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
402
+ LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
403
+ fi
242
404
  # Production smoke evidence (whole-app health) attached to this release.
243
405
  if [ -f prod-smoke-results.json ]; then
244
406
  bash scripts/upload-evidence.sh \
245
407
  "${PROJECT_SLUG}" "${VERSION}" smoke_test prod-smoke-results.json \
246
408
  --release "${VERSION}" --create-release-if-missing --environment production \
247
409
  --category smoke_test --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
248
- --test-cycle "${CI_RUN}" \
249
- || echo "Warning: smoke upload failed for ${VERSION}"
410
+ "${LINEAGE_FLAGS[@]}" \
411
+ || { echo "Warning: smoke upload failed for ${VERSION}"; REQ_FAILURES=$((REQ_FAILURES + 1)); }
250
412
  fi
251
413
  # Carry the release ticket into the production environment so the
252
414
  # prod-review gate is self-contained.
@@ -261,11 +423,28 @@ jobs:
261
423
  "${PROJECT_SLUG}" "${VERSION}" release_ticket "$TICKET" \
262
424
  --release "${VERSION}" --create-release-if-missing --environment production \
263
425
  --category release_artifact --sdlc-stage 5 --git-sha "${GIT_SHA}" --ci-run-id "${CI_RUN}" --branch main \
264
- --test-cycle "${CI_RUN}" \
265
- || echo "Warning: ticket upload failed for ${VERSION}"
426
+ || { echo "Warning: ticket upload failed for ${VERSION}"; REQ_FAILURES=$((REQ_FAILURES + 1)); }
266
427
  else
267
428
  echo "No RELEASE-TICKET-${VERSION}.md found — skipping ticket (date-versioned or archived)."
268
429
  fi
430
+ if [ "$REQ_FAILURES" -gt 0 ]; then
431
+ EVIDENCE_STATUS=failed
432
+ EVIDENCE_FAILURES=$((EVIDENCE_FAILURES + 1))
433
+ else
434
+ EVIDENCE_STATUS=successful
435
+ fi
436
+ bash scripts/report-release-check.sh \
437
+ --project-slug "$PROJECT_SLUG" --release "$VERSION" \
438
+ --check-key "production-evidence:${CI_RUN}:${{ github.run_attempt }}" \
439
+ --label "Production Evidence Completeness" --provider devaudit_installer \
440
+ --status "$EVIDENCE_STATUS" --external-run-id "$CI_RUN" \
441
+ --external-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
442
+ --commit-sha "$GIT_SHA" --branch main \
443
+ --details-json "{\"uploadFailures\":${REQ_FAILURES}}"
444
+ if [ "$REQ_FAILURES" -gt 0 ]; then
445
+ echo "::error::Skipping status promotion for ${VERSION}: production evidence is incomplete"
446
+ continue
447
+ fi
269
448
  # Advance status (idempotent — re-PATCHing an already-promoted release is a no-op).
270
449
  if [ -n "$RELEASE_ID" ]; then
271
450
  curl -s -o /dev/null -w " ${VERSION} status patch: HTTP %{http_code}\n" \
@@ -279,6 +458,10 @@ jobs:
279
458
  echo "::warning::No release_id resolved for ${PREFIX} — skipping status patch"
280
459
  fi
281
460
  done
461
+ if [ "$EVIDENCE_FAILURES" -gt 0 ]; then
462
+ echo "::error::${EVIDENCE_FAILURES} release(s) have incomplete production evidence"
463
+ exit 1
464
+ fi
282
465
  echo "Promoted ${PROMOTED} release(s) to ${TERMINAL_STATUS}."
283
466
  if [ "${TERMINAL_STATUS}" = "prod_review" ]; then
284
467
  echo "Next: a human in the portal clicks 'Approve Production' then 'Mark as Released' for each."
@@ -5,8 +5,9 @@
5
5
  #
6
6
  # Single consolidated job. Order: install → ruff → mypy → semgrep → pip-audit → pytest → build.
7
7
  #
8
- # PRs to develop run Quality Gates before integration. PRs to main inherit
9
- # commit status via branch protection.
8
+ # PRs to {{INTEGRATION_BRANCH}} run Quality Gates before integration. PRs to
9
+ # {{RELEASE_BRANCH}} are handled by quality-gates-provenance.yml: provenance-only
10
+ # for release PRs, full CI rerouted for hotfix/* PRs.
10
11
  # Compliance validation runs separately on PRs (compliance-validation.yml).
11
12
 
12
13
  name: CI Pipeline
@@ -14,9 +15,9 @@ name: CI Pipeline
14
15
  on:
15
16
  workflow_dispatch:
16
17
  pull_request:
17
- branches: [develop]
18
+ branches: [{{INTEGRATION_BRANCH}}]
18
19
  push:
19
- branches: [develop]
20
+ branches: [{{INTEGRATION_BRANCH}}]
20
21
  paths-ignore:
21
22
  {{PATHS_IGNORE}} - 'sdlc-config.json'
22
23
 
@@ -190,7 +191,7 @@ jobs:
190
191
  # PRs to develop should report Quality Gates without mutating DevAudit
191
192
  # releases or evidence. Release registration remains a develop-push /
192
193
  # manual-dispatch side effect.
193
- if: ${{ github.event_name != 'pull_request' }}
194
+ if: ${{ github.event_name != 'pull_request' && github.ref_name == '{{INTEGRATION_BRANCH}}' }}
194
195
  outputs:
195
196
  version: ${{ steps.version.outputs.version }}
196
197
  env:
@@ -314,7 +315,7 @@ jobs:
314
315
  # evidence — `status=failed` is itself the audit trail. `!cancelled()`
315
316
  # still guards against partial state on operator-cancel.
316
317
  # DevAudit-Installer#96.
317
- if: ${{ always() && !cancelled() && needs.register-release.result == 'success' }}
318
+ if: ${{ always() && !cancelled() && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.result == 'success' }}
318
319
  env:
319
320
  DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
320
321
  DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
@@ -356,11 +357,33 @@ jobs:
356
357
  - name: Generate and upload gate evidence
357
358
  if: env.DEVAUDIT_BASE_URL != ''
358
359
  run: |
359
- chmod +x scripts/upload-evidence.sh 2>/dev/null || true
360
+ chmod +x scripts/upload-evidence.sh scripts/report-test-cycle.sh 2>/dev/null || true
360
361
  FLAGS="--git-sha ${{ github.sha }} --ci-run-id ${{ github.run_id }} --branch ${{ github.ref_name }}"
361
362
  FLAGS="${FLAGS} --release ${{ needs.register-release.outputs.version }} --create-release-if-missing"
362
363
  FLAGS="${FLAGS} --environment uat"
363
- FLAGS="${FLAGS} --test-cycle ${{ github.run_id }}"
364
+ FLAGS="${FLAGS} --sdlc-stage 2"
365
+ PRIMARY_LINEAGE_FLAGS=(--test-cycle "${{ github.run_id }}")
366
+ CYCLE_OUT="$(mktemp)"
367
+ bash scripts/report-test-cycle.sh start \
368
+ --project-slug {{PROJECT_SLUG}} \
369
+ --release "${{ needs.register-release.outputs.version }}" \
370
+ --sdlc-stage 2 \
371
+ --environment uat \
372
+ --cycle-kind quality_gate \
373
+ --provider github_actions \
374
+ --external-run-id "${{ github.run_id }}" \
375
+ --external-run-attempt "${{ github.run_attempt }}" \
376
+ --idempotency-key "github:${{ github.repository }}:quality-gates-python.quality_gate:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.register-release.outputs.version }}" \
377
+ --commit-sha "${{ github.sha }}" \
378
+ --branch "${{ github.ref_name }}" \
379
+ --workflow-name "Quality Gates" \
380
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
381
+ --output-file "$CYCLE_OUT"
382
+ . "$CYCLE_OUT"
383
+ rm -f "$CYCLE_OUT"
384
+ if [ "${cycle_supported:-false}" = "true" ] && [ -n "${cycle_record_id:-}" ]; then
385
+ PRIMARY_LINEAGE_FLAGS+=(--evidence-scope cycle --test-cycle-record-id "${cycle_record_id}")
386
+ fi
364
387
 
365
388
  UPLOAD_FAILURES=0
366
389
  upload() {
@@ -394,7 +417,7 @@ jobs:
394
417
  STATUS_TESTS=$(gate_status "$(jq -r '.tests // "skipped"' "$OUTCOMES_FILE")")
395
418
  upload gate-outcomes.json \
396
419
  {{PROJECT_SLUG}} _compliance-docs gate_outcome "$OUTCOMES_FILE" \
397
- --category ci_pipeline ${FLAGS}
420
+ --category ci_pipeline ${FLAGS} "${PRIMARY_LINEAGE_FLAGS[@]}"
398
421
  fi
399
422
 
400
423
  mkdir -p {{WORKING_DIR_PREFIX}}ci-evidence
@@ -405,14 +428,14 @@ jobs:
405
428
  if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/sast-results.json ]; then
406
429
  upload sast-results.json \
407
430
  {{PROJECT_SLUG}} _compliance-docs sast_report {{WORKING_DIR_PREFIX}}ci-evidence/sast-results.json \
408
- --category security_scan --gate-status "$STATUS_SAST" ${FLAGS}
431
+ --category security_scan --gate-status "$STATUS_SAST" ${FLAGS} "${PRIMARY_LINEAGE_FLAGS[@]}"
409
432
  fi
410
433
 
411
434
  # Precise evidence_type=dependency_audit (matches the node template).
412
435
  if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/dependency-audit.json ]; then
413
436
  upload dependency-audit.json \
414
437
  {{PROJECT_SLUG}} _compliance-docs dependency_audit {{WORKING_DIR_PREFIX}}ci-evidence/dependency-audit.json \
415
- --category security_scan --gate-status "$STATUS_DEPAUDIT" ${FLAGS}
438
+ --category security_scan --gate-status "$STATUS_DEPAUDIT" ${FLAGS} "${PRIMARY_LINEAGE_FLAGS[@]}"
416
439
  fi
417
440
 
418
441
  # pytest junit.xml is the Python equivalent of e2e-results.json —
@@ -421,7 +444,7 @@ jobs:
421
444
  if [ -f {{WORKING_DIR_PREFIX}}ci-evidence/junit.xml ]; then
422
445
  upload junit.xml \
423
446
  {{PROJECT_SLUG}} _compliance-docs e2e_result {{WORKING_DIR_PREFIX}}ci-evidence/junit.xml \
424
- --category e2e_result --gate-status "$STATUS_TESTS" ${FLAGS}
447
+ --category e2e_result --gate-status "$STATUS_TESTS" ${FLAGS} "${PRIMARY_LINEAGE_FLAGS[@]}"
425
448
  fi
426
449
 
427
450
  # Project-level Test Summary Report — intentional compliance_document
@@ -523,5 +546,30 @@ jobs:
523
546
  exit 1
524
547
  fi
525
548
 
549
+ - name: Complete primary quality-gate cycle
550
+ if: always() && env.DEVAUDIT_BASE_URL != ''
551
+ run: |
552
+ case "${{ job.status }}" in
553
+ success) OUTCOME=passed ;;
554
+ cancelled) OUTCOME=cancelled ;;
555
+ *) OUTCOME=failed ;;
556
+ esac
557
+ chmod +x scripts/report-test-cycle.sh 2>/dev/null || true
558
+ bash scripts/report-test-cycle.sh complete \
559
+ --project-slug {{PROJECT_SLUG}} \
560
+ --release "${{ needs.register-release.outputs.version }}" \
561
+ --sdlc-stage 2 \
562
+ --environment uat \
563
+ --cycle-kind quality_gate \
564
+ --provider github_actions \
565
+ --external-run-id "${{ github.run_id }}" \
566
+ --external-run-attempt "${{ github.run_attempt }}" \
567
+ --idempotency-key "github:${{ github.repository }}:quality-gates-python.quality_gate:${{ github.run_id }}:${{ github.run_attempt }}:2:${{ needs.register-release.outputs.version }}" \
568
+ --commit-sha "${{ github.sha }}" \
569
+ --branch "${{ github.ref_name }}" \
570
+ --workflow-name "Quality Gates" \
571
+ --workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
572
+ --outcome "$OUTCOME"
573
+
526
574
  - name: Summary
527
575
  run: echo "Evidence uploaded for ${{ needs.register-release.outputs.version }} (UAT)"
@@ -1,4 +1,5 @@
1
- # Quality Gates Provenance — verifies prior Quality Gates success on PRs to main
1
+ # Quality Gates Provenance — routes PRs to the release branch into the correct
2
+ # Quality Gates path.
2
3
  #
3
4
  # Generated by `devaudit install` / `devaudit update` from sdlc-config.json.
4
5
  # Do not edit manually — re-run the CLI (`devaudit update`) to regenerate.
@@ -9,10 +10,11 @@
9
10
  # Quality Gates result. When that happens, neither existing workflow fires on
10
11
  # the PR to main, so the release PR sits with missing/ambiguous protection.
11
12
  #
12
- # This workflow fires on PRs to main, queries the GitHub API for prior
13
- # successful `Quality Gates` check runs on the PR head SHA, and fails closed
14
- # if none are found. It does NOT rerun heavy CI — it is a same-SHA provenance
15
- # check only.
13
+ # This workflow fires on PRs to {{RELEASE_BRANCH}} and chooses between two modes:
14
+ # - release PR (`{{INTEGRATION_BRANCH}} -> {{RELEASE_BRANCH}}`): same-SHA provenance
15
+ # check only
16
+ # - hotfix PR (`hotfix/* -> {{RELEASE_BRANCH}}`): dispatch the real ci.yml gates on
17
+ # the hotfix branch and proxy that result back onto the PR
16
18
  #
17
19
  # The job name is exactly `Quality Gates` to satisfy existing branch-protection
18
20
  # rules without introducing a new required-check name.
@@ -22,14 +24,102 @@ name: CI Pipeline
22
24
  on:
23
25
  workflow_dispatch:
24
26
  pull_request:
25
- branches: [main]
27
+ branches: [{{RELEASE_BRANCH}}]
26
28
 
27
29
  jobs:
30
+ release-scope-integrity:
31
+ name: Release Scope Integrity
32
+ if: ${{ !startsWith(github.event.pull_request.head.ref, 'hotfix/') && !startsWith(github.event.pull_request.head.ref, 'fix/hotfix-') }}
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v6
36
+ with:
37
+ ref: ${{ github.event.pull_request.head.sha }}
38
+ fetch-depth: 0
39
+
40
+ - name: Check release PR metadata against derived scope
41
+ env:
42
+ PR_TITLE: ${{ github.event.pull_request.title }}
43
+ PR_BODY: ${{ github.event.pull_request.body }}
44
+ HEAD_REF: ${{ github.event.pull_request.head.ref }}
45
+ run: |
46
+ chmod +x scripts/check-release-pr-scope.sh 2>/dev/null || true
47
+ bash scripts/check-release-pr-scope.sh
48
+
28
49
  quality-gates:
29
50
  name: Quality Gates
30
51
  runs-on: ubuntu-latest
31
52
  steps:
53
+ - name: Classify PR path
54
+ id: classify
55
+ env:
56
+ HEAD_REF: ${{ github.event.pull_request.head.ref }}
57
+ BASE_REF: ${{ github.event.pull_request.base.ref }}
58
+ run: |
59
+ case "$HEAD_REF" in
60
+ hotfix/*|fix/hotfix-*)
61
+ echo "kind=hotfix" >> "$GITHUB_OUTPUT"
62
+ ;;
63
+ *)
64
+ echo "kind=release" >> "$GITHUB_OUTPUT"
65
+ ;;
66
+ esac
67
+ echo "base_ref=${BASE_REF}" >> "$GITHUB_OUTPUT"
68
+ echo "head_ref=${HEAD_REF}" >> "$GITHUB_OUTPUT"
69
+
70
+ - name: Dispatch ci.yml Quality Gates for hotfix PR
71
+ if: steps.classify.outputs.kind == 'hotfix'
72
+ env:
73
+ GH_TOKEN: ${{ github.token }}
74
+ HEAD_REF: ${{ steps.classify.outputs.head_ref }}
75
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
76
+ run: |
77
+ BEFORE_JSON=$(gh run list \
78
+ --workflow "CI Pipeline" \
79
+ --branch "$HEAD_REF" \
80
+ --event workflow_dispatch \
81
+ --limit 50 \
82
+ --json databaseId,headSha)
83
+
84
+ gh workflow run ci.yml --ref "$HEAD_REF"
85
+
86
+ RUN_ID=""
87
+ for ATTEMPT in $(seq 1 20); do
88
+ AFTER_JSON=$(gh run list \
89
+ --workflow "CI Pipeline" \
90
+ --branch "$HEAD_REF" \
91
+ --event workflow_dispatch \
92
+ --limit 50 \
93
+ --json databaseId,headSha)
94
+ RUN_ID=$(BEFORE_JSON="$BEFORE_JSON" AFTER_JSON="$AFTER_JSON" HEAD_SHA="$HEAD_SHA" python3 - <<'PY'
95
+ import json
96
+ import os
97
+
98
+ before = {str(item["databaseId"]) for item in json.loads(os.environ["BEFORE_JSON"])}
99
+ head_sha = os.environ["HEAD_SHA"]
100
+ for item in json.loads(os.environ["AFTER_JSON"]):
101
+ if item.get("headSha") == head_sha and str(item["databaseId"]) not in before:
102
+ print(item["databaseId"])
103
+ break
104
+ PY
105
+ )
106
+ if [ -n "$RUN_ID" ]; then
107
+ break
108
+ fi
109
+ echo "Waiting for dispatched ci.yml run to appear for ${HEAD_REF}..."
110
+ sleep 10
111
+ done
112
+
113
+ if [ -z "$RUN_ID" ]; then
114
+ echo "::error::Unable to locate the dispatched ci.yml run for hotfix branch ${HEAD_REF}."
115
+ exit 1
116
+ fi
117
+
118
+ echo "Watching dispatched ci.yml run ${RUN_ID} for hotfix branch ${HEAD_REF}..."
119
+ gh run watch "$RUN_ID" --exit-status
120
+
32
121
  - name: Verify prior Quality Gates success on head SHA
122
+ if: steps.classify.outputs.kind != 'hotfix'
33
123
  env:
34
124
  GH_TOKEN: ${{ github.token }}
35
125
  HEAD_SHA: ${{ github.event.pull_request.head.sha }}
@@ -37,26 +127,31 @@ jobs:
37
127
  run: |
38
128
  echo "Checking for prior successful 'Quality Gates' on SHA: $HEAD_SHA"
39
129
  echo "Excluding current workflow run: $CURRENT_RUN_ID"
40
-
41
- # Query GitHub API for check runs on this SHA with name "Quality Gates"
42
130
  CURRENT_RUN_URL="/actions/runs/${CURRENT_RUN_ID}"
43
- RESPONSE=$(gh api \
44
- "/repos/${{ github.repository }}/commits/${HEAD_SHA}/check-runs?name=Quality%20Gates" \
45
- --jq ".check_runs[]
46
- | select(.status == \"completed\" and .conclusion == \"success\")
47
- | select((.details_url // \"\") | contains(\"${CURRENT_RUN_URL}\") | not)")
48
-
49
- if [ -n "$RESPONSE" ]; then
50
- echo "Found prior successful Quality Gates result:"
51
- echo "$RESPONSE" | head -5
52
- echo ""
53
- echo "Provenance verified this SHA passed develop-side Quality Gates."
54
- exit 0
55
- else
56
- echo "::error::This SHA did not pass develop-side Quality Gates."
57
- echo "::error::Push this exact commit through the normal develop flow first."
58
- echo ""
59
- echo "No prior successful 'Quality Gates' check run found for SHA $HEAD_SHA"
60
- echo "(excluding current workflow run $CURRENT_RUN_ID)."
61
- exit 1
62
- fi
131
+ MAX_ATTEMPTS=10
132
+ WAIT_SECONDS=30
133
+
134
+ for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
135
+ RESPONSE=$(gh api \
136
+ "/repos/${{ github.repository }}/commits/${HEAD_SHA}/check-runs?name=Quality%20Gates" \
137
+ --jq ".check_runs[]
138
+ | select(.status == \"completed\" and .conclusion == \"success\")
139
+ | select((.details_url // \"\") | contains(\"${CURRENT_RUN_URL}\") | not)")
140
+
141
+ if [ -n "$RESPONSE" ]; then
142
+ echo "Found prior successful Quality Gates result:"
143
+ echo "$RESPONSE" | head -5
144
+ echo ""
145
+ echo "Provenance verified this SHA passed develop-side Quality Gates."
146
+ exit 0
147
+ fi
148
+
149
+ if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
150
+ echo "Develop-side Quality Gates for SHA ${HEAD_SHA} not successful yet. Waiting ${WAIT_SECONDS}s (${ATTEMPT}/${MAX_ATTEMPTS})..."
151
+ sleep "$WAIT_SECONDS"
152
+ fi
153
+ done
154
+
155
+ echo "::error::No successful develop-side Quality Gates result found for SHA ${HEAD_SHA} after waiting $((MAX_ATTEMPTS * WAIT_SECONDS / 60)) minutes."
156
+ echo "::error::Push this exact commit through the normal develop flow first, or wait for the in-flight develop pipeline to finish and rerun this check."
157
+ exit 1
package/sdlc/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-sdlc",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
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"