@metasession.co/devaudit-cli 0.3.11 → 0.3.13

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 (68) hide show
  1. package/dist/index.js +94 -6
  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/ai-rules/SDLC_RULES.md +27 -7
  6. package/sdlc/files/_common/0-project-setup.md +13 -7
  7. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
  8. package/sdlc/files/_common/README_TEMPLATE.md +8 -8
  9. package/sdlc/files/_common/Test_Architecture.md +15 -15
  10. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
  11. package/sdlc/files/_common/Test_Policy.md +2 -2
  12. package/sdlc/files/_common/Test_Strategy.md +10 -10
  13. package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
  14. package/sdlc/files/_common/governance/dpia.md.template +3 -3
  15. package/sdlc/files/_common/governance/incident-report.md.template +3 -3
  16. package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
  17. package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
  18. package/sdlc/files/_common/governance/risk-register.md.template +1 -1
  19. package/sdlc/files/_common/governance/ropa.md.template +2 -2
  20. package/sdlc/files/_common/joining-an-existing-project.md +4 -4
  21. package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
  22. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
  23. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
  24. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
  25. package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
  26. package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
  27. package/sdlc/files/_common/scripts/extract-release-metadata.sh +65 -0
  28. package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +38 -0
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +307 -58
  30. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -78
  31. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
  32. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
  33. package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
  34. package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
  35. package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
  36. package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
  37. package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
  38. package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
  39. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  40. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  41. package/sdlc/files/_common/scripts/upload-evidence.sh +55 -1
  42. package/sdlc/files/_common/scripts/upload-evidence.test.sh +119 -0
  43. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +169 -2
  44. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +169 -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 +57 -3
  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/check-release-approval.yml.template +18 -28
  55. package/sdlc/files/ci/ci.yml.template +95 -35
  56. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  57. package/sdlc/files/ci/compliance-evidence.yml.template +234 -49
  58. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  59. package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
  60. package/sdlc/files/ci/python/ci.yml.template +60 -12
  61. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  62. package/sdlc/package.json +1 -1
  63. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  64. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  65. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  66. package/sdlc/src/blueprints/3-compile-evidence.raw.md +102 -20
  67. package/sdlc/src/blueprints/4-submit-for-review.raw.md +3 -3
  68. package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
@@ -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.11",
3
+ "version": "0.3.13",
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"
@@ -16,6 +16,15 @@ const DEFAULT_FLAKY_PATTERNS = [
16
16
  'flaky',
17
17
  'retry',
18
18
  ];
19
+ const DEFAULT_INTEGRATION_BRANCH = 'develop';
20
+ const DEFAULT_RELEASE_BRANCH = 'main';
21
+ const REQUIRED_RELEASE_CHECKS = [
22
+ 'Quality Gates',
23
+ 'Release Scope Integrity',
24
+ 'Compliance Validation',
25
+ 'DevAudit Release Approval',
26
+ 'E2E Regression Suite',
27
+ ];
19
28
  const RELEASE_APPROVAL_STATUSES = new Set([
20
29
  'uat_approved',
21
30
  'release_approved',
@@ -84,6 +93,11 @@ function isReleaseGateCheck(check) {
84
93
  return haystack.includes('release approval') || haystack.includes('uat approval');
85
94
  }
86
95
 
96
+ function isReleasePromotionPr(pr, options) {
97
+ return normalizeState(pr.baseRefName) === normalizeState(options.releaseBranch || DEFAULT_RELEASE_BRANCH)
98
+ && normalizeState(pr.headRefName) === normalizeState(options.integrationBranch || DEFAULT_INTEGRATION_BRANCH);
99
+ }
100
+
87
101
  function isFlakyCheck(check, patterns) {
88
102
  const haystack = `${check.name || ''} ${check.workflow || ''}`.toLowerCase();
89
103
  return patterns.some(pattern => haystack.includes(pattern));
@@ -313,6 +327,14 @@ function classifyPrStatus(pr, checks, portalStatus, watch, options) {
313
327
  const failingChecks = checks.filter(check => isFailingState(check.state || check.bucket));
314
328
  const pendingChecks = checks.filter(check => isPendingState(check.state || check.bucket));
315
329
  const releaseGate = checks.find(isReleaseGateCheck) || null;
330
+ const isReleasePr = isReleasePromotionPr(pr || {}, options);
331
+ const releaseChecks = isReleasePr
332
+ ? checks.filter(check => REQUIRED_RELEASE_CHECKS.includes(String(check.name || '')))
333
+ : [];
334
+ const missingReleaseChecks = isReleasePr
335
+ ? REQUIRED_RELEASE_CHECKS.filter(required => !releaseChecks.some(check => String(check.name || '') === required))
336
+ : [];
337
+ const pendingReleaseChecks = releaseChecks.filter(check => isPendingState(check.state || check.bucket));
316
338
 
317
339
  if (normalizeState(pr.state) !== 'open') {
318
340
  return {
@@ -362,6 +384,22 @@ function classifyPrStatus(pr, checks, portalStatus, watch, options) {
362
384
  };
363
385
  }
364
386
 
387
+ if (isReleasePr && missingReleaseChecks.length > 0) {
388
+ return {
389
+ kind: 'waiting',
390
+ exitCode: 2,
391
+ summary: `PR #${watch.prNumber} is still waiting for required release checks to report: ${missingReleaseChecks.join(', ')}.`,
392
+ };
393
+ }
394
+
395
+ if (isReleasePr && pendingReleaseChecks.length > 0) {
396
+ return {
397
+ kind: 'waiting',
398
+ exitCode: 2,
399
+ summary: `PR #${watch.prNumber} still has required release checks running: ${pendingReleaseChecks.map(formatCheckName).join(', ')}.`,
400
+ };
401
+ }
402
+
365
403
  for (const check of failingChecks) {
366
404
  if (!isReleaseGateCheck(check) && isFlakyCheck(check, options.flakyPatterns || DEFAULT_FLAKY_PATTERNS)) {
367
405
  const runId = extractRunId(check.link);
@@ -405,7 +443,9 @@ function classifyPrStatus(pr, checks, portalStatus, watch, options) {
405
443
  return {
406
444
  kind: 'ready',
407
445
  exitCode: 0,
408
- summary: `PR #${watch.prNumber} is approved and all observed checks are green. Ready for merge.`,
446
+ summary: isReleasePr
447
+ ? `PR #${watch.prNumber} is approved and all required release checks reached terminal success. Ready for merge.`
448
+ : `PR #${watch.prNumber} is approved and all observed checks are green. Ready for merge.`,
409
449
  };
410
450
  }
411
451
 
@@ -454,6 +494,8 @@ async function watchPullRequest() {
454
494
 
455
495
  const options = {
456
496
  flakyPatterns: flakyPatterns.length > 0 ? flakyPatterns : DEFAULT_FLAKY_PATTERNS,
497
+ integrationBranch: getOption('integration-branch') || DEFAULT_INTEGRATION_BRANCH,
498
+ releaseBranch: getOption('release-branch') || DEFAULT_RELEASE_BRANCH,
457
499
  };
458
500
 
459
501
  let pollsRemaining = once ? 1 : maxPolls;
@@ -463,7 +505,7 @@ async function watchPullRequest() {
463
505
  let pr;
464
506
  let checks;
465
507
  try {
466
- pr = runGhJson(['pr', 'view', String(prNumber), '--repo', repo, '--json', 'state,isDraft,reviewDecision']);
508
+ pr = runGhJson(['pr', 'view', String(prNumber), '--repo', repo, '--json', 'state,isDraft,reviewDecision,baseRefName,headRefName']);
467
509
  checks = runGhJson(['pr', 'checks', String(prNumber), '--repo', repo, '--json', 'name,state,link,workflow,bucket']) || [];
468
510
  if (!Array.isArray(checks)) checks = [];
469
511
  } catch (error) {
@@ -70,6 +70,11 @@ Open `compliance/RTM.md`, Part B. The issue provides full context; the RTM is a
70
70
  | REQ-XXX | #NNN | [LOW/MEDIUM/HIGH] | compliance/evidence/REQ-XXX/ | DRAFT | -- | -- |
71
71
  ```
72
72
 
73
+ If any RTM cell content needs literal pipe characters, escape them as `\|`
74
+ rather than writing bare `|`. This matters for regex or enum examples inside
75
+ status/notes text because markdownlint otherwise misreads the row as having
76
+ extra columns.
77
+
73
78
  The auditor reads one row and follows the links: Issue for context and rationale, evidence directory for test artifacts, PR for code changes.
74
79
 
75
80
  ### Step 5: Create Evidence Directory
@@ -379,7 +384,7 @@ cat > compliance/evidence/REQ-XXX/test-plan.md << 'EOF'
379
384
 
380
385
  ## Functional Test Mapping
381
386
  | Acceptance Criterion | Test File | Test Name |
382
- |---------------------|-----------|-----------|
387
+ | --------------------- | ----------- | ----------- |
383
388
  | [From test-scope.md] | [spec file] | [test name] |
384
389
 
385
390
  ## Non-Functional Tests (MEDIUM/HIGH)
@@ -246,7 +246,7 @@ For LOW-risk docs/tooling/script-only changes or environments without the requir
246
246
  #### Exit Criteria
247
247
 
248
248
  | Gate | Threshold |
249
- |---|---|
249
+ | --- | --- |
250
250
  | TypeScript | 0 errors |
251
251
  | SAST (high/critical) | 0 findings |
252
252
  | Dependencies (high/critical) | 0 vulnerabilities |