@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
@@ -1,12 +1,5 @@
1
1
  #!/usr/bin/env bash
2
2
  # generate-bundled-changes.test.sh — Tests for generate-bundled-changes.sh.
3
- #
4
- # Builds a throwaway git repo per case, crafts commits with mixed types,
5
- # runs the helper against it, asserts on the stdout content. Hermetic:
6
- # runs inside mktemp'd directories that are torn down at the end.
7
- #
8
- # Usage:
9
- # ./scripts/generate-bundled-changes.test.sh
10
3
 
11
4
  set -euo pipefail
12
5
 
@@ -20,7 +13,6 @@ FAIL=0
20
13
  TMPDIR_BASE="$(mktemp -d)"
21
14
  trap 'rm -rf "$TMPDIR_BASE"' EXIT
22
15
 
23
- # Build a fresh git fixture with an initial commit.
24
16
  make_fixture() {
25
17
  local dir="$1"
26
18
  rm -rf "$dir"
@@ -34,7 +26,6 @@ make_fixture() {
34
26
  git commit -q -m "feat: initial commit [REQ-001]"
35
27
  }
36
28
 
37
- # Add a commit with a given message.
38
29
  add_commit() {
39
30
  local msg="$1"
40
31
  echo "$(date +%s%N)" >> f.txt
@@ -42,6 +33,29 @@ add_commit() {
42
33
  git commit -q -m "$msg"
43
34
  }
44
35
 
36
+ write_release_ticket() {
37
+ local version="$1"
38
+ local requirement_line="$2"
39
+ local summary="$3"
40
+ local predecessors="$4"
41
+ mkdir -p compliance/pending-releases
42
+ cat > "compliance/pending-releases/RELEASE-TICKET-${version}.md" <<EOF
43
+ # Release Ticket - ${version}
44
+
45
+ **Requirement:** ${requirement_line}
46
+ **PR:** #123 https://github.com/example/repo/pull/123
47
+
48
+ ## Summary
49
+
50
+ ${summary}
51
+
52
+ ## Bundled Changes
53
+
54
+ - **Core tracked release:** \`${version}\`
55
+ - **Absorbed predecessor releases:** ${predecessors}
56
+ EOF
57
+ }
58
+
45
59
  assert_contains() {
46
60
  local desc="$1" want="$2" got="$3"
47
61
  if echo "$got" | grep -qF "$want"; then
@@ -70,117 +84,154 @@ assert_not_contains() {
70
84
  fi
71
85
  }
72
86
 
73
- # ─── Test 1: Mixed commits — only housekeeping types in output ─────
74
- echo "Test 1: mixed commits filter to housekeeping only"
87
+ assert_eq() {
88
+ local desc="$1" want="$2" got="$3"
89
+ if [ "$want" = "$got" ]; then
90
+ echo " PASS: $desc"
91
+ PASS=$((PASS + 1))
92
+ else
93
+ echo " FAIL: $desc"
94
+ echo " want: $want"
95
+ echo " got: $got"
96
+ FAIL=$((FAIL + 1))
97
+ fi
98
+ }
99
+
100
+ assert_file_exists() {
101
+ local desc="$1" path="$2"
102
+ if [ -f "$path" ]; then
103
+ echo " PASS: $desc"
104
+ PASS=$((PASS + 1))
105
+ else
106
+ echo " FAIL: $desc"
107
+ echo " missing file: $path"
108
+ FAIL=$((FAIL + 1))
109
+ fi
110
+ }
111
+
112
+ # Test 1: housekeeping commits appear in markdown + JSON.
113
+ echo "Test 1: housekeeping commits render as non-release work"
75
114
  DIR1="$TMPDIR_BASE/test1"
76
115
  make_fixture "$DIR1"
77
116
  add_commit "chore: sync DevAudit templates from v0.1.69 to v0.1.70 [skip ci]"
78
117
  add_commit "docs: update API reference for /bookings endpoint"
79
118
  add_commit "feat: add booking widget [REQ-042]"
80
- add_commit "chore: bump eslint 9.0.5 to 9.0.6"
119
+ add_commit "chore(deps): bump eslint 9.0.5 to 9.0.6"
81
120
  add_commit "fix: resolve null pointer in booking service [REQ-042]"
82
- # Tag the initial commit as the "since" ref
83
121
  SINCE=$(git rev-list --max-parents=0 HEAD)
84
- OUTPUT=$(bash "$HELPER" "$SINCE" "REQ-042" 2>&1 || true)
85
- assert_contains "housekeeping chore: sync in output" "chore: sync DevAudit templates" "$OUTPUT"
86
- assert_contains "housekeeping docs: update in output" "docs: update API reference" "$OUTPUT"
87
- assert_contains "housekeeping chore: bump eslint in output" "chore: bump eslint" "$OUTPUT"
88
- assert_not_contains "feat commit excluded" "feat: add booking widget" "$OUTPUT"
89
- assert_not_contains "fix commit excluded" "fix: resolve null pointer" "$OUTPUT"
90
- assert_contains "header present" "## Bundled Changes" "$OUTPUT"
91
- assert_contains "version in header" "REQ-042" "$OUTPUT"
92
- assert_contains "core tracked release field present" "**Core tracked release:**" "$OUTPUT"
93
- assert_contains "absorbed non-release work field present" "**Absorbed non-release work:**" "$OUTPUT"
94
- assert_contains "reviewer impact field present" "**Reviewer impact:**" "$OUTPUT"
95
- assert_contains "reference field present" "**Reference:**" "$OUTPUT"
122
+ JSON_OUT="$DIR1/bundle.json"
123
+ OUTPUT=$(bash "$HELPER" "$SINCE" "REQ-042" --json-out "$JSON_OUT" 2>&1)
124
+ assert_contains "markdown includes chore commit" "chore: sync DevAudit templates" "$OUTPUT"
125
+ assert_contains "markdown includes docs commit" "docs: update API reference" "$OUTPUT"
126
+ assert_contains "markdown includes scoped chore commit" "chore(deps): bump eslint" "$OUTPUT"
127
+ assert_not_contains "markdown excludes feat commit" "feat: add booking widget" "$OUTPUT"
128
+ assert_not_contains "markdown excludes fix commit" "fix: resolve null pointer" "$OUTPUT"
129
+ assert_file_exists "json manifest emitted" "$JSON_OUT"
130
+ assert_eq "non-release work count" "3" "$(jq -r '.nonReleaseWorkItems | length' "$JSON_OUT")"
131
+ assert_eq "member count with no tickets" "0" "$(jq -r '.members | length' "$JSON_OUT")"
132
+ assert_eq "first housekeeping kind" "housekeeping_commit" "$(jq -r '.nonReleaseWorkItems[0].kind' "$JSON_OUT")"
96
133
  echo
97
134
 
98
- # ─── Test 2: No housekeeping commits ────────────────────────────────
99
- echo "Test 2: no housekeeping commits — empty summary"
135
+ # Test 2: explicit predecessor tickets become manifest members.
136
+ echo "Test 2: explicit predecessor membership emitted"
100
137
  DIR2="$TMPDIR_BASE/test2"
101
138
  make_fixture "$DIR2"
102
- add_commit "feat: add feature A [REQ-010]"
103
- add_commit "fix: fix bug B [REQ-010]"
139
+ add_commit "feat: predecessor scope [REQ-041]"
140
+ add_commit "fix: predecessor bugfix [REQ-041]"
141
+ add_commit "feat: current release scope [REQ-042]"
142
+ write_release_ticket "REQ-041" "REQ-041 - Prior tracked release" "Prior release summary." "None"
143
+ write_release_ticket "REQ-042" "REQ-042 - Current tracked release" "Current release summary." "\`REQ-041\`"
104
144
  SINCE=$(git rev-list --max-parents=0 HEAD)
105
- OUTPUT=$(bash "$HELPER" "$SINCE" "REQ-010" 2>&1 || true)
106
- assert_contains "no housekeeping message" "No housekeeping commits found" "$OUTPUT"
145
+ JSON_OUT="$DIR2/bundle.json"
146
+ OUTPUT=$(bash "$HELPER" "$SINCE" "REQ-042" --json-out "$JSON_OUT" 2>&1)
147
+ assert_contains "markdown shows predecessor section" "REQ-041" "$OUTPUT"
148
+ assert_eq "manifest contains one explicit member" "1" "$(jq -r '.members | length' "$JSON_OUT")"
149
+ assert_eq "member version recorded" "REQ-041" "$(jq -r '.members[0].version' "$JSON_OUT")"
150
+ assert_eq "member role recorded" "predecessor" "$(jq -r '.members[0].role' "$JSON_OUT")"
151
+ assert_eq "member relationship recorded" "superseded" "$(jq -r '.members[0].relationship' "$JSON_OUT")"
152
+ assert_eq "manifest hash present" "true" "$(jq -r 'has("manifestHash")' "$JSON_OUT")"
153
+ assert_eq "manifest schema v2" "2" "$(jq -r '.schemaVersion' "$JSON_OUT")"
154
+ assert_eq "original title recorded" "Prior tracked release" "$(jq -r '.members[0].originalTitle' "$JSON_OUT")"
155
+ assert_eq "inheritance policy explicit" "all_eligible" "$(jq -r '.members[0].evidenceInheritancePolicy.mode' "$JSON_OUT")"
156
+ EXPECTED_HASH="sha256:$(jq -cS 'del(.manifestHash, .generator.generatedAt)' "$JSON_OUT" | tr -d '\n' | sha256sum | awk '{print $1}')"
157
+ assert_eq "canonical manifest hash verifies" "$EXPECTED_HASH" "$(jq -r '.manifestHash' "$JSON_OUT")"
107
158
  echo
108
159
 
109
- # ─── Test 3: All housekeeping types captured ────────────────────────
110
- echo "Test 3: all housekeeping commit types captured"
160
+ # Test 3: ambiguous predecessor ownership fails.
161
+ echo "Test 3: ambiguous predecessor ownership is rejected"
111
162
  DIR3="$TMPDIR_BASE/test3"
112
163
  make_fixture "$DIR3"
113
- add_commit "ci: update workflow timeout"
114
- add_commit "build: bump node version to 22"
115
- add_commit "test: add unit test for booking"
116
- add_commit "revert: remove experimental feature"
117
- add_commit "style: fix formatting in utils"
118
- add_commit "perf: optimize query in booking"
119
- add_commit "refactor: extract validation logic"
120
- SINCE=$(git rev-list --max-parents=0 HEAD)
121
- OUTPUT=$(bash "$HELPER" "$SINCE" "REQ-030" 2>&1 || true)
122
- assert_contains "ci: type captured" "ci: update workflow timeout" "$OUTPUT"
123
- assert_contains "build: type captured" "build: bump node version" "$OUTPUT"
124
- assert_contains "test: type captured" "test: add unit test for booking" "$OUTPUT"
125
- assert_contains "revert: type captured" "revert: remove experimental feature" "$OUTPUT"
126
- assert_contains "style: type captured" "style: fix formatting" "$OUTPUT"
127
- assert_contains "perf: type captured" "perf: optimize query" "$OUTPUT"
128
- assert_contains "refactor: type captured" "refactor: extract validation" "$OUTPUT"
164
+ write_release_ticket "REQ-041" "REQ-041 - Prior tracked release" "Prior release summary." "None"
165
+ write_release_ticket "REQ-042" "REQ-042 - Current tracked release" "Current release summary." "None"
166
+ set +e
167
+ OUTPUT=$(bash "$HELPER" "$(git rev-list --max-parents=0 HEAD)" "REQ-042" 2>&1)
168
+ RC=$?
169
+ set -e
170
+ if [ "$RC" -ne 0 ]; then
171
+ echo " PASS: non-zero exit for ambiguous predecessor set"
172
+ PASS=$((PASS + 1))
173
+ else
174
+ echo " FAIL: expected non-zero exit for ambiguous predecessor set"
175
+ FAIL=$((FAIL + 1))
176
+ fi
177
+ assert_contains "ambiguous error text" "ambiguous predecessor ownership" "$OUTPUT"
129
178
  echo
130
179
 
131
- # ─── Test 4: No commits since ref ───────────────────────────────────
132
- echo "Test 4: no commits since ref — empty summary"
180
+ # Test 4: self-supersession is rejected.
181
+ echo "Test 4: self-supersession is rejected"
133
182
  DIR4="$TMPDIR_BASE/test4"
134
183
  make_fixture "$DIR4"
135
- SINCE=$(git rev-parse HEAD)
136
- OUTPUT=$(bash "$HELPER" "$SINCE" "REQ-001" 2>&1 || true)
137
- assert_contains "no commits message" "No housekeeping commits found" "$OUTPUT"
138
- echo
139
-
140
- # ─── Test 5: Invalid ref ────────────────────────────────────────────
141
- echo "Test 5: invalid ref — error exit"
142
- DIR5="$TMPDIR_BASE/test5"
143
- make_fixture "$DIR5"
184
+ write_release_ticket "REQ-042" "REQ-042 - Current tracked release" "Current release summary." "\`REQ-042\`"
144
185
  set +e
145
- OUTPUT=$(bash "$HELPER" "nonexistent-ref" "REQ-001" 2>&1)
186
+ OUTPUT=$(bash "$HELPER" "$(git rev-list --max-parents=0 HEAD)" "REQ-042" 2>&1)
146
187
  RC=$?
147
188
  set -e
148
189
  if [ "$RC" -ne 0 ]; then
149
- echo " PASS: non-zero exit code for invalid ref"
190
+ echo " PASS: non-zero exit for self-supersession"
150
191
  PASS=$((PASS + 1))
151
192
  else
152
- echo " FAIL: expected non-zero exit code for invalid ref"
193
+ echo " FAIL: expected non-zero exit for self-supersession"
153
194
  FAIL=$((FAIL + 1))
154
195
  fi
196
+ assert_contains "self-supersession error text" "cannot self-supersede" "$OUTPUT"
155
197
  echo
156
198
 
157
- # ─── Test 6: Scoped commit types (chore(deps)) ─────────────────────
158
- echo "Test 6: scoped housekeeping commits captured"
159
- DIR6="$TMPDIR_BASE/test6"
160
- make_fixture "$DIR6"
161
- add_commit "chore(deps): bump eslint from 9.0.5 to 9.0.6"
162
- add_commit "ci(workflow): update timeout to 30 minutes"
163
- SINCE=$(git rev-list --max-parents=0 HEAD)
164
- OUTPUT=$(bash "$HELPER" "$SINCE" "REQ-050" 2>&1 || true)
165
- assert_contains "scoped chore(deps) captured" "chore(deps): bump eslint" "$OUTPUT"
166
- assert_contains "scoped ci(workflow) captured" "ci(workflow): update timeout" "$OUTPUT"
167
- echo
168
-
169
- # ─── Test 7: Missing since-ref argument ────────────────────────────
170
- echo "Test 7: missing since-ref argument — error exit"
171
- DIR7="$TMPDIR_BASE/test7"
172
- make_fixture "$DIR7"
199
+ # Test 5: duplicate explicit predecessors are rejected.
200
+ echo "Test 5: duplicate explicit predecessors are rejected"
201
+ DIR5="$TMPDIR_BASE/test5"
202
+ make_fixture "$DIR5"
203
+ write_release_ticket "REQ-041" "REQ-041 - Prior tracked release" "Prior release summary." "None"
204
+ write_release_ticket "REQ-042" "REQ-042 - Current tracked release" "Current release summary." "\`REQ-041\`, \`REQ-041\`"
173
205
  set +e
174
- OUTPUT=$(bash "$HELPER" 2>&1)
206
+ OUTPUT=$(bash "$HELPER" "$(git rev-list --max-parents=0 HEAD)" "REQ-042" 2>&1)
175
207
  RC=$?
176
208
  set -e
177
209
  if [ "$RC" -ne 0 ]; then
178
- echo " PASS: non-zero exit code for missing argument"
210
+ echo " PASS: non-zero exit for duplicate predecessors"
179
211
  PASS=$((PASS + 1))
180
212
  else
181
- echo " FAIL: expected non-zero exit code for missing argument"
213
+ echo " FAIL: expected non-zero exit for duplicate predecessors"
182
214
  FAIL=$((FAIL + 1))
183
215
  fi
216
+ assert_contains "duplicate predecessor error text" "duplicate explicit predecessor" "$OUTPUT"
217
+ echo
218
+
219
+ # Test 6: regeneration is deterministic apart from generatedAt.
220
+ echo "Test 6: manifest regeneration is deterministic"
221
+ DIR6="$TMPDIR_BASE/test6"
222
+ make_fixture "$DIR6"
223
+ add_commit "docs: update release notes"
224
+ write_release_ticket "REQ-042" "REQ-042 - Current tracked release" "Current release summary." "None"
225
+ SINCE=$(git rev-list --max-parents=0 HEAD)
226
+ JSON_ONE="$DIR6/bundle-one.json"
227
+ JSON_TWO="$DIR6/bundle-two.json"
228
+ bash "$HELPER" "$SINCE" "REQ-042" --json-out "$JSON_ONE" >/dev/null
229
+ sleep 1
230
+ bash "$HELPER" "$SINCE" "REQ-042" --json-out "$JSON_TWO" >/dev/null
231
+ NORMALIZED_ONE="$(jq -S 'del(.generator.generatedAt)' "$JSON_ONE")"
232
+ NORMALIZED_TWO="$(jq -S 'del(.generator.generatedAt)' "$JSON_TWO")"
233
+ assert_eq "normalized manifests match" "$NORMALIZED_ONE" "$NORMALIZED_TWO"
234
+ assert_eq "manifest hashes match" "$(jq -r '.manifestHash' "$JSON_ONE")" "$(jq -r '.manifestHash' "$JSON_TWO")"
184
235
  echo
185
236
 
186
237
  echo "=== Summary: ${PASS} pass / ${FAIL} fail ==="
@@ -43,6 +43,12 @@ fi
43
43
 
44
44
  TODAY=$(date -u +%Y-%m-%d)
45
45
 
46
+ # Escape freeform table-cell content so markdownlint does not misparse
47
+ # literal pipes as extra columns in generated release evidence.
48
+ markdown_table_cell() {
49
+ printf '%s' "${1:-}" | perl -0pe 's/(?<!\\)\|/\\|/g'
50
+ }
51
+
46
52
  # Last 20 commits with subject lines. Skip the version-deriver fallback
47
53
  # from itself (avoid recursion if this script is re-run on a branch
48
54
  # that already includes its own auto-commit).
@@ -104,13 +110,16 @@ REPLACE — confirm risk class for this release (typically LOW for housekeeping)
104
110
 
105
111
  ## Test Evidence
106
112
 
113
+ > When editing any markdown table below, escape literal pipe characters in cell
114
+ > content as \`\\|\` so RTM/release-evidence tables stay lint-safe.
115
+
107
116
  | Test Type | Status | Source |
108
- |---|---|---|
109
- | TypeScript | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`typecheck\` gate |
110
- | SAST | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`SAST\` gate |
111
- | Dependency audit | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`dependency_audit\` gate |
112
- | E2E | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`e2e\` gate |
113
- | Test reports | $(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A") | CI \`test_report\` gate |
117
+ | --- | --- | --- |
118
+ | TypeScript | $(markdown_table_cell "$(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A")") | CI \`typecheck\` gate |
119
+ | SAST | $(markdown_table_cell "$(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A")") | CI \`SAST\` gate |
120
+ | Dependency audit | $(markdown_table_cell "$(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A")") | CI \`dependency_audit\` gate |
121
+ | E2E | $(markdown_table_cell "$(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A")") | CI \`e2e\` gate |
122
+ | Test reports | $(markdown_table_cell "$(test -f gate-outcomes.json && echo "see gate-outcomes.json" || echo "REPLACE — green / N/A")") | CI \`test_report\` gate |
114
123
 
115
124
  > Companion artefact: \`compliance/security-summary-$VERSION.md\` (auto-generated by \`generate-security-summary.sh\` in the same workflow run; contains the SAST + dep-audit findings detail).
116
125
 
@@ -123,7 +132,7 @@ REPLACE — confirm risk class for this release (typically LOW for housekeeping)
123
132
  ## Post-Deploy Actions
124
133
 
125
134
  | Type | Script / Command | Target | Required | Notes |
126
- |------|-----------------|--------|----------|-------|
135
+ | ------ | ----------------- | -------- | ---------- | ------- |
127
136
  | — | None | — | — | Housekeeping releases typically have no post-deploy actions |
128
137
 
129
138
  <!-- Replace the "None" row above if this release requires post-deploy work. -->
@@ -133,14 +142,14 @@ REPLACE — confirm risk class for this release (typically LOW for housekeeping)
133
142
  ## Sign-off
134
143
 
135
144
  | Role | Name | Date | Notes |
136
- |---|---|---|---|
145
+ | --- | --- | --- | --- |
137
146
  | Submitter | REPLACE | $TODAY | Auto-generated by CI; reviewed + edited |
138
147
  | Reviewer (independent if project risk_tier ≠ low) | REPLACE | REPLACE | REPLACE |
139
148
 
140
149
  ## Audit Trail
141
150
 
142
151
  | Date | Action | Actor | Notes |
143
- |------|--------|-------|-------|
152
+ | ------ | -------- | ------- | ------- |
144
153
  | $TODAY | Stub auto-generated | devaudit-bot | Bare-date version $VERSION |
145
154
  | REPLACE | Operator sign-off | REPLACE | REPLACE |
146
155
  | REPLACE | Submitted for UAT review | REPLACE | Via portal |
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # generate-housekeeping-release-ticket.test.sh — Tests for the housekeeping
3
+ # release-ticket generator's markdown table output contract.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ HELPER="$SCRIPT_DIR/generate-housekeeping-release-ticket.sh"
9
+ [ -x "$HELPER" ] || chmod +x "$HELPER"
10
+
11
+ PASS=0
12
+ FAIL=0
13
+
14
+ assert_contains() {
15
+ local desc="$1" want="$2" got="$3"
16
+ if printf '%s' "$got" | grep -qF "$want"; then
17
+ echo " PASS: $desc"
18
+ PASS=$((PASS + 1))
19
+ else
20
+ echo " FAIL: $desc"
21
+ echo " want: $want"
22
+ FAIL=$((FAIL + 1))
23
+ fi
24
+ }
25
+
26
+ assert_not_contains_regex() {
27
+ local desc="$1" pattern="$2" got="$3"
28
+ if printf '%s' "$got" | grep -qE "$pattern"; then
29
+ echo " FAIL: $desc"
30
+ echo " unexpected pattern: $pattern"
31
+ FAIL=$((FAIL + 1))
32
+ else
33
+ echo " PASS: $desc"
34
+ PASS=$((PASS + 1))
35
+ fi
36
+ }
37
+
38
+ TMPDIR_BASE="$(mktemp -d)"
39
+ trap 'rm -rf "$TMPDIR_BASE"' EXIT
40
+
41
+ make_fixture() {
42
+ local dir="$1"
43
+ mkdir -p "$dir"
44
+ cd "$dir"
45
+ git init -q --initial-branch=main
46
+ git config user.email "test@example.com"
47
+ git config user.name "test"
48
+ echo "x" > f.txt
49
+ git add f.txt
50
+ git commit -q -m "docs: initial commit"
51
+ printf '{ "typecheck": "PASS", "sast": "PASS", "dependency_audit": "PASS", "e2e": "PASS", "test_report": "PASS" }\n' > gate-outcomes.json
52
+ }
53
+
54
+ echo "Test 1: generated markdown uses spaced separators and carries pipe-escape guidance"
55
+ DIR1="$TMPDIR_BASE/test1"
56
+ make_fixture "$DIR1"
57
+ OUTPUT=$(bash "$HELPER" "v2026.07.14" 2>&1)
58
+ assert_contains "pipe-escape guidance emitted" 'escape literal pipe characters in cell' "$OUTPUT"
59
+ assert_contains "spaced separator in test evidence table" '| --- | --- | --- |' "$OUTPUT"
60
+ assert_contains "spaced separator in sign-off table" '| --- | --- | --- | --- |' "$OUTPUT"
61
+ assert_contains "spaced separator in audit trail table" '| ------ | -------- | ------- | ------- |' "$OUTPUT"
62
+ assert_not_contains_regex "no compact separators remain" '^\|[-:|]+\|$' "$OUTPUT"
63
+ echo
64
+
65
+ echo "Result: $PASS passed, $FAIL failed"
66
+ [ "$FAIL" = "0" ]
@@ -38,6 +38,12 @@ fi
38
38
 
39
39
  TODAY=$(date -u +%Y-%m-%d)
40
40
 
41
+ # Escape freeform table-cell content so markdownlint does not misparse
42
+ # literal pipes as extra columns in generated release evidence.
43
+ markdown_table_cell() {
44
+ printf '%s' "${1:-}" | perl -0pe 's/(?<!\\)\|/\\|/g'
45
+ }
46
+
41
47
  # Detect release shape from the version.
42
48
  SHAPE="unknown"
43
49
  if [[ "$VERSION" =~ ^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
@@ -147,11 +153,14 @@ $GATES
147
153
 
148
154
  ## Access control + audit log
149
155
 
156
+ > When editing any markdown table below, escape literal pipe characters in cell
157
+ > content as \`\\|\` so RTM/release-evidence tables stay lint-safe.
158
+
150
159
  | Check | Result | Notes |
151
- |---|---|---|
152
- | Access control unchanged | REPLACE — yes/no | If yes, no further work. If no, document the auth/RBAC delta and confirm it landed an audit event. |
153
- | Audit log append-only invariant preserved | REPLACE — yes/no | If yes, no further work. If no, document why and confirm the change has independent review. |
154
- | Sensitive data exposure | REPLACE — yes/no | If yes, escalate to the GDPR triage in \`compliance/governance/dpia.md\` before merging. |
160
+ | --- | --- | --- |
161
+ | Access control unchanged | $(markdown_table_cell "REPLACE — yes/no") | If yes, no further work. If no, document the auth/RBAC delta and confirm it landed an audit event. |
162
+ | Audit log append-only invariant preserved | $(markdown_table_cell "REPLACE — yes/no") | If yes, no further work. If no, document why and confirm the change has independent review. |
163
+ | Sensitive data exposure | $(markdown_table_cell "REPLACE — yes/no") | If yes, escalate to the GDPR triage in \`compliance/governance/dpia.md\` before merging. |
155
164
 
156
165
  ## Risk Assessment
157
166
 
@@ -162,7 +171,7 @@ REPLACE — one paragraph summarising the security posture of this release. For
162
171
  ## Sign-off
163
172
 
164
173
  | Role | Name | Date | Notes |
165
- |---|---|---|---|
174
+ | --- | --- | --- | --- |
166
175
  | Author | devaudit-bot (auto-generated) | $TODAY | Stub generated from CI gate JSON |
167
176
  | Reviewer | REPLACE | REPLACE | REPLACE |
168
177
 
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # generate-security-summary.test.sh — Tests for the security-summary
3
+ # generator's markdown table output contract.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ HELPER="$SCRIPT_DIR/generate-security-summary.sh"
9
+ [ -x "$HELPER" ] || chmod +x "$HELPER"
10
+
11
+ PASS=0
12
+ FAIL=0
13
+
14
+ assert_contains() {
15
+ local desc="$1" want="$2" got="$3"
16
+ if printf '%s' "$got" | grep -qF "$want"; then
17
+ echo " PASS: $desc"
18
+ PASS=$((PASS + 1))
19
+ else
20
+ echo " FAIL: $desc"
21
+ echo " want: $want"
22
+ FAIL=$((FAIL + 1))
23
+ fi
24
+ }
25
+
26
+ assert_not_contains_regex() {
27
+ local desc="$1" pattern="$2" got="$3"
28
+ if printf '%s' "$got" | grep -qE "$pattern"; then
29
+ echo " FAIL: $desc"
30
+ echo " unexpected pattern: $pattern"
31
+ FAIL=$((FAIL + 1))
32
+ else
33
+ echo " PASS: $desc"
34
+ PASS=$((PASS + 1))
35
+ fi
36
+ }
37
+
38
+ TMPDIR_BASE="$(mktemp -d)"
39
+ trap 'rm -rf "$TMPDIR_BASE"' EXIT
40
+
41
+ DIR1="$TMPDIR_BASE/test1"
42
+ mkdir -p "$DIR1"
43
+ cd "$DIR1"
44
+ printf '{ "results": [] }\n' > sast-results.json
45
+ printf '{ "vulnerabilities": {} }\n' > dependency-audit.json
46
+ printf '{ "sast": "PASS", "dependency_audit": "PASS" }\n' > gate-outcomes.json
47
+
48
+ echo "Test 1: generated markdown uses spaced separators and carries pipe-escape guidance"
49
+ OUTPUT=$(bash "$HELPER" "REQ-321" 2>&1)
50
+ assert_contains "pipe-escape guidance emitted" 'escape literal pipe characters in cell' "$OUTPUT"
51
+ assert_contains "spaced separator in access-control table" '| --- | --- | --- |' "$OUTPUT"
52
+ assert_contains "spaced separator in sign-off table" '| --- | --- | --- | --- |' "$OUTPUT"
53
+ assert_not_contains_regex "no compact separators remain" '^\|[-:|]+\|$' "$OUTPUT"
54
+ echo
55
+
56
+ echo "Result: $PASS passed, $FAIL failed"
57
+ [ "$FAIL" = "0" ]
@@ -36,20 +36,47 @@ if [ -z "$CURRENT_RELEASE" ]; then
36
36
  exit 1
37
37
  fi
38
38
 
39
+ RELEASE_TITLE=""
40
+ RELEASE_SUMMARY=""
41
+ if [[ "$CURRENT_RELEASE" =~ ^REQ-[0-9]+$ ]] && [ -f scripts/extract-release-metadata.sh ]; then
42
+ # shellcheck disable=SC1091
43
+ source scripts/extract-release-metadata.sh
44
+ extract_release_metadata "$CURRENT_RELEASE"
45
+ fi
46
+
39
47
  TITLE="Release: ${CURRENT_RELEASE}"
48
+ if [ -n "$RELEASE_TITLE" ]; then
49
+ TITLE="${TITLE} — ${RELEASE_TITLE}"
50
+ fi
51
+
40
52
  BODY=$(cat <<EOF
41
53
  ## Release
42
54
 
43
55
  - Release: ${CURRENT_RELEASE}
44
56
  - Base: ${RELEASE_BRANCH}
45
57
  - Head: ${INTEGRATION_BRANCH}
58
+ EOF
59
+ )
60
+
61
+ if [ -n "$RELEASE_TITLE" ]; then
62
+ BODY="${BODY}
63
+ - Title: ${RELEASE_TITLE}"
64
+ fi
65
+
66
+ if [ -n "$RELEASE_SUMMARY" ]; then
67
+ BODY="${BODY}
68
+
69
+ ## Summary
70
+
71
+ ${RELEASE_SUMMARY}"
72
+ fi
73
+
74
+ BODY="${BODY}
46
75
 
47
76
  ## DevAudit
48
77
 
49
78
  - Release context is derived from the current ${INTEGRATION_BRANCH} head.
50
- - If this PR replaced an older release PR, the older PR was superseded because it no longer matched the governing release context.
51
- EOF
52
- )
79
+ - If this PR replaced an older release PR, the older PR was superseded because it no longer matched the governing release context."
53
80
 
54
81
  OPEN_PRS=$(gh pr list --base "$RELEASE_BRANCH" --head "$INTEGRATION_BRANCH" --state open --json number,title,body,url --limit 10)
55
82
  COUNT=$(echo "$OPEN_PRS" | jq 'length')
@@ -75,7 +102,20 @@ PR_TITLE=$(echo "$OPEN_PRS" | jq -r '.[0].title // ""')
75
102
  PR_BODY=$(echo "$OPEN_PRS" | jq -r '.[0].body // ""')
76
103
  PR_URL=$(echo "$OPEN_PRS" | jq -r '.[0].url')
77
104
 
78
- if printf '%s\n%s\n' "$PR_TITLE" "$PR_BODY" | grep -q "$CURRENT_RELEASE"; then
105
+ PR_IS_CURRENT=true
106
+ if ! printf '%s\n%s\n' "$PR_TITLE" "$PR_BODY" | grep -q "$CURRENT_RELEASE"; then
107
+ PR_IS_CURRENT=false
108
+ fi
109
+ if [ -n "$RELEASE_TITLE" ] && [ "$PR_TITLE" != "$TITLE" ]; then
110
+ PR_IS_CURRENT=false
111
+ fi
112
+ if printf '%s' "$RELEASE_SUMMARY" | grep -q 'Bundled release context:'; then
113
+ if ! printf '%s\n%s\n' "$PR_TITLE" "$PR_BODY" | grep -q "BUNDLED-CHANGES-${CURRENT_RELEASE}.md"; then
114
+ PR_IS_CURRENT=false
115
+ fi
116
+ fi
117
+
118
+ if [ "$PR_IS_CURRENT" = "true" ]; then
79
119
  echo "Open release PR #${PR_NUMBER} already matches ${CURRENT_RELEASE}: ${PR_URL}"
80
120
  exit 0
81
121
  fi