@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
@@ -57,7 +57,7 @@ risk_level: "REPLACE — low | medium | high"
57
57
  For each risk, populate one row. Add or remove rows to fit your assessment.
58
58
 
59
59
  | # | Risk | Likelihood (1–3) | Severity (1–3) | Inherent risk | Existing controls | Residual risk | Acceptable? |
60
- |---|---|---|---|---|---|---|---|
60
+ | --- | --- | --- | --- | --- | --- | --- | --- |
61
61
  | 1 | REPLACE — e.g. unauthorised access to user accounts | REPLACE | REPLACE | REPLACE | REPLACE — MFA, RBAC, encryption-at-rest | REPLACE | REPLACE Y/N |
62
62
  | 2 | REPLACE | | | | | | |
63
63
 
@@ -76,7 +76,7 @@ For each risk, populate one row. Add or remove rows to fit your assessment.
76
76
  ## 6. Sign-off
77
77
 
78
78
  | Role | Name | Date | Decision |
79
- |---|---|---|---|
79
+ | --- | --- | --- | --- |
80
80
  | Controller | REPLACE | REPLACE | REPLACE — approved / rejected / conditional |
81
81
  | DPO | REPLACE | REPLACE | REPLACE |
82
82
  | Technical lead | REPLACE | REPLACE | REPLACE |
@@ -90,5 +90,5 @@ For each risk, populate one row. Add or remove rows to fit your assessment.
90
90
  ## Review log
91
91
 
92
92
  | Date | Reviewer | Changes |
93
- |---|---|---|
93
+ | --- | --- | --- |
94
94
  | REPLACE — YYYY-MM-DD | REPLACE | Initial DPIA authored from starter template |
@@ -72,7 +72,7 @@ If none of the conditional boxes apply, this is still a valid incident_report
72
72
  ## 2. Personal data scope (GDPR triage)
73
73
 
74
74
  | Question | Answer |
75
- |---|---|
75
+ | --- | --- |
76
76
  | Did the incident involve personal data? | REPLACE — Y / N |
77
77
  | If Y: estimated number of data subjects affected | REPLACE |
78
78
  | If Y: categories of personal data involved | REPLACE |
@@ -84,7 +84,7 @@ If none of the conditional boxes apply, this is still a valid incident_report
84
84
  ## 3. Timeline
85
85
 
86
86
  | Time (UTC) | Event |
87
- |---|---|
87
+ | --- | --- |
88
88
  | REPLACE — ISO-8601 | REPLACE — first signal observed |
89
89
  | REPLACE | REPLACE — detection escalated to on-call |
90
90
  | REPLACE | REPLACE — incident channel opened, IC assigned |
@@ -131,7 +131,7 @@ If none of the conditional boxes apply, this is still a valid incident_report
131
131
  ## 9. Sign-off
132
132
 
133
133
  | Role | Name | Date |
134
- |---|---|---|
134
+ | --- | --- | --- |
135
135
  | Incident Commander | REPLACE | REPLACE |
136
136
  | Engineering lead | REPLACE | REPLACE |
137
137
  | DPO (if personal data involved) | REPLACE | REPLACE |
@@ -29,7 +29,7 @@ No incidents or defects were discovered during the test cycle for release `<vers
29
29
  ## Sign-off
30
30
 
31
31
  | Role | Name | Date |
32
- |---|---|---|
32
+ | --- | --- | --- |
33
33
  | Test lead | REPLACE — assign | REPLACE |
34
34
  | Engineering lead | REPLACE — assign | REPLACE |
35
35
 
@@ -52,7 +52,7 @@ The 60% of this doc that the operator must fill (auto-generated sections at the
52
52
  ## 2. Activity summary (CI-derived, auto-fill when WS3 lands)
53
53
 
54
54
  | Metric | Value | Notes |
55
- |---|---|---|
55
+ | --- | --- | --- |
56
56
  | Releases shipped this period | REPLACE | Tracked + housekeeping combined |
57
57
  | Tracked releases (REQ-XXX) | REPLACE | |
58
58
  | Quality-gate pass rate | REPLACE — % | % of CI runs where every gate passed |
@@ -107,14 +107,14 @@ For each control area, document evidence + reviewer judgement.
107
107
  ## 5. Findings and follow-up actions
108
108
 
109
109
  | # | Finding | Severity | Owner | Due | Issue link |
110
- |---|---|---|---|---|---|
110
+ | --- | --- | --- | --- | --- | --- |
111
111
  | 1 | REPLACE | REPLACE | REPLACE | REPLACE | REPLACE |
112
112
  | 2 | REPLACE | | | | |
113
113
 
114
114
  ## 6. Sign-off
115
115
 
116
116
  | Role | Name | Date |
117
- |---|---|---|
117
+ | --- | --- | --- |
118
118
  | Reviewer | REPLACE | REPLACE |
119
119
  | Approver (dual-actor) | REPLACE | REPLACE |
120
120
  | Decision | REPLACE — controls effective / partially effective / not effective |
@@ -25,7 +25,7 @@ review_cadence_days: 90
25
25
  ## Scoring matrix
26
26
 
27
27
  | Inherent + Residual rating | Likelihood (frequency / ease-of-exploit) | Impact (worst-case if realised) |
28
- |---|---|---|
28
+ | --- | --- | --- |
29
29
  | **Low** | Unlikely in a typical year; requires unusual access or coincidence | Recoverable inconvenience; no regulatory exposure |
30
30
  | **Medium** | Plausible within a year; standard skill or access | Recoverable but disruptive; possible reportable incident |
31
31
  | **High** | Probable within months; trivial to trigger | Material loss; regulatory reporting required |
@@ -50,7 +50,7 @@ For each distinct processing activity your project performs, add one section bel
50
50
  ### Activity 1 — REPLACE (e.g. "User authentication and session management")
51
51
 
52
52
  | Field | Value |
53
- |---|---|
53
+ | --- | --- |
54
54
  | **Purpose(s) of processing** | REPLACE — why you process this data; lawful basis (Art. 6) |
55
55
  | **Categories of data subjects** | REPLACE — e.g. customers, employees, prospects |
56
56
  | **Categories of personal data** | REPLACE — e.g. name, email, IP address, hashed password |
@@ -73,5 +73,5 @@ For each distinct processing activity your project performs, add one section bel
73
73
  ## Review log
74
74
 
75
75
  | Date | Reviewer | Changes |
76
- |---|---|---|
76
+ | --- | --- | --- |
77
77
  | REPLACE — YYYY-MM-DD | REPLACE | Initial ROPA authored from starter template |
@@ -27,7 +27,7 @@ If `devaudit doctor` and `devaudit status .` are both green, you can skip `devau
27
27
  When you `git clone`, you've already got everything the framework synced into the project on first install:
28
28
 
29
29
  | Path | What it is | Who owns it |
30
- |---|---|---|
30
+ | --- | --- | --- |
31
31
  | `sdlc-config.json` | Project-wide config (stack, host, slug, runtime, UAT, approval mode, e2e knobs, …) | Team — committed by the operator |
32
32
  | `SDLC/*.md` | Stage walkthroughs (0-project-setup, 1-plan-requirement, …) — synced from DevAudit-Installer | Team — refreshed by `devaudit update` |
33
33
  | `compliance/RTM.md`, `compliance/risk-register.md`, … | Compliance artefacts | Team — appended by tracked work |
@@ -149,7 +149,7 @@ You can skip this step entirely if the synced templates from `git clone` are alr
149
149
  Two distinct credentials exist; conflating them is what causes the silent-CI-token rotation bug this guide exists to prevent.
150
150
 
151
151
  | Credential | Format | Where it lives | Who it identifies | Used by |
152
- |---|---|---|---|---|
152
+ | --- | --- | --- | --- | --- |
153
153
  | **Personal PAT** | `mctok_…` | `~/.config/devaudit/auth.json` (per developer) | You (the user) | Your local CLI commands |
154
154
  | **Project API key** | `dak_…` | Repo secret `DEVAUDIT_API_KEY` | The project | CI's `devaudit push` calls |
155
155
  | **Operator's PAT** | `mctok_…` | Repo secret `DEVAUDIT_USER_TOKEN` | The operator (singular) | CI's DevAudit portal calls that need human attribution (release submit / approval flows) |
@@ -181,7 +181,7 @@ A second dev running it silently rotates the team's `DEVAUDIT_USER_TOKEN` repo s
181
181
  The synced CI gates expect a specific environment. Here's what you need locally if you're running them against the same project:
182
182
 
183
183
  | Surface | CI | Local (your machine) |
184
- |---|---|---|
184
+ | --- | --- | --- |
185
185
  | Personal identity | `secrets.DEVAUDIT_USER_TOKEN` (operator's, portal-only) | `~/.config/devaudit/auth.json` (yours) — `devaudit auth login` |
186
186
  | Project API key | `secrets.DEVAUDIT_API_KEY` | Usually unset locally — only needed if you're testing `devaudit push` against the live portal; ask the operator if you need to debug it |
187
187
  | Portal URL | `vars.DEVAUDIT_BASE_URL` | `~/.config/devaudit/auth.json` (set by `auth login`) or `$DEVAUDIT_BASE_URL` env |
@@ -196,7 +196,7 @@ For everything else (DB migrations, env files, `npm install`), the consumer's `R
196
196
  ## Troubleshooting
197
197
 
198
198
  | Symptom | Cause | Fix |
199
- |---|---|---|
199
+ | --- | --- | --- |
200
200
  | `devaudit status` reports missing framework files | Local clone is behind `main`, or the operator hasn't run a recent `devaudit update` | `git pull origin develop` first; if still missing, ping the operator |
201
201
  | `devaudit auth status` says "token rejected" | PAT expired, was revoked, or you typed it wrong | Issue a fresh PAT at `/settings/tokens` and re-run `devaudit auth login` |
202
202
  | `devaudit doctor` flags missing tool | The matching binary isn't on PATH | Install via your package manager (`brew install gh`, `apt install jq`, etc.) |
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ # check-host-deployment.sh — wait for a terminal GitHub deployment status for a
3
+ # merged production SHA before the release is promoted on the portal.
4
+
5
+ set -euo pipefail
6
+
7
+ REPO=""
8
+ SHA=""
9
+ MAX_ATTEMPTS=30
10
+ POLL_SECONDS=10
11
+
12
+ for arg in "$@"; do
13
+ case "$arg" in
14
+ --repo=*) REPO="${arg#*=}" ;;
15
+ --sha=*) SHA="${arg#*=}" ;;
16
+ --max-attempts=*) MAX_ATTEMPTS="${arg#*=}" ;;
17
+ --poll-seconds=*) POLL_SECONDS="${arg#*=}" ;;
18
+ *)
19
+ echo "Unknown argument: $arg" >&2
20
+ exit 2
21
+ ;;
22
+ esac
23
+ done
24
+
25
+ if [ -z "$REPO" ] || [ -z "$SHA" ]; then
26
+ echo "Usage: check-host-deployment.sh --repo=<owner/name> --sha=<sha> [--max-attempts=N] [--poll-seconds=N]" >&2
27
+ exit 2
28
+ fi
29
+
30
+ select_deployment_id() {
31
+ python3 - <<'PY'
32
+ import json
33
+ import os
34
+
35
+ deployments = json.loads(os.environ.get("DEPLOYMENTS_JSON", "[]"))
36
+ preferred = None
37
+ fallback = None
38
+ for deployment in deployments:
39
+ env = str(deployment.get("environment") or "").lower()
40
+ dep_id = deployment.get("id")
41
+ if dep_id is None:
42
+ continue
43
+ if fallback is None:
44
+ fallback = dep_id
45
+ if env in {"production", "prod"}:
46
+ preferred = dep_id
47
+ break
48
+ if preferred is not None:
49
+ print(preferred)
50
+ elif fallback is not None:
51
+ print(fallback)
52
+ PY
53
+ }
54
+
55
+ extract_status_state() {
56
+ python3 - <<'PY'
57
+ import json
58
+ import os
59
+
60
+ statuses = json.loads(os.environ.get("STATUSES_JSON", "[]"))
61
+ if statuses:
62
+ latest = statuses[0]
63
+ print(str(latest.get("state") or "").lower())
64
+ PY
65
+ }
66
+
67
+ for ATTEMPT in $(seq 1 "$MAX_ATTEMPTS"); do
68
+ DEPLOYMENTS_JSON=$(gh api "/repos/${REPO}/deployments?sha=${SHA}&per_page=20")
69
+ DEPLOYMENT_ID=$(DEPLOYMENTS_JSON="$DEPLOYMENTS_JSON" select_deployment_id)
70
+
71
+ if [ -z "$DEPLOYMENT_ID" ]; then
72
+ echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}: no GitHub deployment found yet for ${SHA}."
73
+ if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
74
+ sleep "$POLL_SECONDS"
75
+ continue
76
+ fi
77
+ echo "::error::No GitHub deployment was published for ${SHA} after ${MAX_ATTEMPTS} attempts."
78
+ exit 1
79
+ fi
80
+
81
+ STATUSES_JSON=$(gh api "/repos/${REPO}/deployments/${DEPLOYMENT_ID}/statuses?per_page=20")
82
+ DEPLOY_STATE=$(STATUSES_JSON="$STATUSES_JSON" extract_status_state)
83
+
84
+ case "$DEPLOY_STATE" in
85
+ success)
86
+ echo "GitHub deployment ${DEPLOYMENT_ID} for ${SHA} reached terminal success."
87
+ exit 0
88
+ ;;
89
+ failure|error)
90
+ echo "::error::GitHub deployment ${DEPLOYMENT_ID} for ${SHA} reached terminal failure (${DEPLOY_STATE})."
91
+ exit 1
92
+ ;;
93
+ queued|in_progress|pending|"")
94
+ echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}: deployment ${DEPLOYMENT_ID} for ${SHA} is ${DEPLOY_STATE:-pending}."
95
+ if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
96
+ sleep "$POLL_SECONDS"
97
+ continue
98
+ fi
99
+ echo "::error::GitHub deployment ${DEPLOYMENT_ID} for ${SHA} did not reach a terminal success state after ${MAX_ATTEMPTS} attempts."
100
+ exit 1
101
+ ;;
102
+ *)
103
+ echo "::error::Deployment ${DEPLOYMENT_ID} for ${SHA} returned unexpected state '${DEPLOY_STATE}'."
104
+ exit 1
105
+ ;;
106
+ esac
107
+ done
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # check-host-deployment.test.sh — Tests for host deployment wait helper.
3
+
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ HELPER="$SCRIPT_DIR/check-host-deployment.sh"
8
+
9
+ PASS=0
10
+ FAIL=0
11
+ WORK=$(mktemp -d)
12
+ trap 'rm -rf "$WORK"' EXIT
13
+
14
+ assert_exit() {
15
+ local name="$1" expected="$2" actual="$3"
16
+ if [ "$expected" = "$actual" ]; then
17
+ echo " ✓ $name"
18
+ PASS=$((PASS + 1))
19
+ else
20
+ echo " ✗ $name"
21
+ echo " Expected exit: $expected"
22
+ echo " Actual exit: $actual"
23
+ FAIL=$((FAIL + 1))
24
+ fi
25
+ }
26
+
27
+ assert_grep() {
28
+ local name="$1" pattern="$2" file="$3"
29
+ if grep -Eq "$pattern" "$file"; then
30
+ echo " ✓ $name"
31
+ PASS=$((PASS + 1))
32
+ else
33
+ echo " ✗ $name"
34
+ echo " Missing pattern: $pattern"
35
+ FAIL=$((FAIL + 1))
36
+ fi
37
+ }
38
+
39
+ write_mock_gh() {
40
+ local dir="$1"
41
+ mkdir -p "$dir/mock-bin"
42
+ cat > "$dir/mock-bin/gh" <<'EOF'
43
+ #!/usr/bin/env bash
44
+ set -euo pipefail
45
+ if [ "$1" != "api" ]; then
46
+ echo "unexpected gh invocation: $*" >&2
47
+ exit 1
48
+ fi
49
+ case "$2" in
50
+ */deployments\?sha=*)
51
+ printf '%s' "${MOCK_DEPLOYMENTS_JSON}"
52
+ ;;
53
+ */statuses\?per_page=20)
54
+ printf '%s' "${MOCK_STATUSES_JSON}"
55
+ ;;
56
+ *)
57
+ echo "unexpected gh api path: $2" >&2
58
+ exit 1
59
+ ;;
60
+ esac
61
+ EOF
62
+ chmod +x "$dir/mock-bin/gh"
63
+ }
64
+
65
+ run_check() {
66
+ local dir="$1" out="$2"
67
+ shift 2
68
+ (
69
+ cd "$dir"
70
+ PATH="$dir/mock-bin:$PATH" "$@" bash "$HELPER" --repo=metasession-dev/example --sha=abc123 --max-attempts=1 --poll-seconds=0
71
+ ) >"$out" 2>&1
72
+ }
73
+
74
+ echo "=== check-host-deployment.sh tests ==="
75
+ echo ""
76
+
77
+ write_mock_gh "$WORK"
78
+
79
+ echo "--- Test 1: success status passes ---"
80
+ OUT="$WORK/test1.out"
81
+ if run_check "$WORK" "$OUT" env \
82
+ MOCK_DEPLOYMENTS_JSON='[{"id":5453707224,"environment":"production"}]' \
83
+ MOCK_STATUSES_JSON='[{"state":"success"}]'; then
84
+ CODE=0
85
+ else
86
+ CODE=$?
87
+ fi
88
+ assert_exit "success deployment passes" 0 "$CODE"
89
+ assert_grep "success message emitted" 'terminal success' "$OUT"
90
+ echo ""
91
+
92
+ echo "--- Test 2: failure status fails ---"
93
+ OUT="$WORK/test2.out"
94
+ if run_check "$WORK" "$OUT" env \
95
+ MOCK_DEPLOYMENTS_JSON='[{"id":5453707224,"environment":"production"}]' \
96
+ MOCK_STATUSES_JSON='[{"state":"failure"}]'; then
97
+ CODE=0
98
+ else
99
+ CODE=$?
100
+ fi
101
+ assert_exit "failed deployment exits 1" 1 "$CODE"
102
+ assert_grep "failure message emitted" 'terminal failure' "$OUT"
103
+ echo ""
104
+
105
+ echo "--- Test 3: missing deployment fails closed ---"
106
+ OUT="$WORK/test3.out"
107
+ if run_check "$WORK" "$OUT" env \
108
+ MOCK_DEPLOYMENTS_JSON='[]' \
109
+ MOCK_STATUSES_JSON='[]'; then
110
+ CODE=0
111
+ else
112
+ CODE=$?
113
+ fi
114
+ assert_exit "missing deployment exits 1" 1 "$CODE"
115
+ assert_grep "missing deployment message emitted" 'No GitHub deployment was published' "$OUT"
116
+ echo ""
117
+
118
+ echo "=== check-host-deployment.test.sh: $PASS passed, $FAIL failed ==="
119
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ # check-release-pr-scope.sh — fail closed when a release PR's declared scope
3
+ # drifts from the release currently derived from the integration branch head.
4
+ #
5
+ # Usage:
6
+ # PR_TITLE="..." PR_BODY="..." HEAD_REF="develop" \
7
+ # bash scripts/check-release-pr-scope.sh
8
+
9
+ set -euo pipefail
10
+
11
+ is_hotfix_branch() {
12
+ case "${1:-}" in
13
+ hotfix/*|fix/hotfix-*)
14
+ return 0
15
+ ;;
16
+ *)
17
+ return 1
18
+ ;;
19
+ esac
20
+ }
21
+
22
+ extract_declared_scope() {
23
+ python3 - <<'PY'
24
+ import os
25
+ import re
26
+
27
+ title = os.environ.get("PR_TITLE", "")
28
+ body = os.environ.get("PR_BODY", "")
29
+ text = f"{title}\n{body}"
30
+ patterns = [
31
+ r"^[ \t\-*]*Release:\s*((?:REQ-\d+)|(?:v\d+\.\d+\.\d+)|(?:v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?))\b",
32
+ r"\b(REQ-\d+)\b",
33
+ r"\b(v\d+\.\d+\.\d+)\b",
34
+ r"\b(v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?)\b",
35
+ ]
36
+ flags = re.MULTILINE
37
+ for pattern in patterns:
38
+ match = re.search(pattern, text, flags)
39
+ if match:
40
+ print(match.group(1))
41
+ break
42
+ PY
43
+ }
44
+
45
+ HEAD_REF="${HEAD_REF:-}"
46
+ if is_hotfix_branch "$HEAD_REF"; then
47
+ echo "Hotfix PR detected (${HEAD_REF}) — release scope integrity check skipped."
48
+ exit 0
49
+ fi
50
+
51
+ if [ ! -x scripts/derive-release-version.sh ]; then
52
+ chmod +x scripts/derive-release-version.sh 2>/dev/null || true
53
+ fi
54
+ CURRENT_RELEASE=$(bash scripts/derive-release-version.sh)
55
+ if [ -z "$CURRENT_RELEASE" ]; then
56
+ echo "::error::Could not derive the effective release scope from the current branch head."
57
+ exit 1
58
+ fi
59
+
60
+ DECLARED_SCOPE=$(extract_declared_scope)
61
+ if [ -z "$DECLARED_SCOPE" ]; then
62
+ echo "::error::Release Scope Integrity: the PR title/body do not declare a release scope."
63
+ echo "::error::Derived effective scope: ${CURRENT_RELEASE}"
64
+ echo "::error::Update the PR metadata or regenerate the release PR from the current integration head."
65
+ exit 1
66
+ fi
67
+
68
+ if [ "$DECLARED_SCOPE" != "$CURRENT_RELEASE" ]; then
69
+ echo "::error::Release Scope Integrity mismatch."
70
+ echo "::error::Declared PR scope: ${DECLARED_SCOPE}"
71
+ echo "::error::Derived effective scope: ${CURRENT_RELEASE}"
72
+ echo "::error::Update the PR metadata or regenerate the release PR from the current integration head."
73
+ exit 1
74
+ fi
75
+
76
+ if [[ "$CURRENT_RELEASE" =~ ^REQ-[0-9]+$ ]]; then
77
+ if [ ! -x scripts/extract-release-metadata.sh ]; then
78
+ chmod +x scripts/extract-release-metadata.sh 2>/dev/null || true
79
+ fi
80
+ # shellcheck disable=SC1091
81
+ source scripts/extract-release-metadata.sh
82
+ extract_release_metadata "$CURRENT_RELEASE"
83
+ if printf '%s' "${RELEASE_SUMMARY:-}" | grep -q 'Bundled release context:'; then
84
+ if ! printf '%s\n%s\n' "${PR_TITLE:-}" "${PR_BODY:-}" | grep -q "BUNDLED-CHANGES-${CURRENT_RELEASE}.md"; then
85
+ echo "::error::Release Scope Integrity mismatch."
86
+ echo "::error::Declared PR scope: ${DECLARED_SCOPE}"
87
+ echo "::error::Derived effective scope: ${CURRENT_RELEASE} with bundled release context."
88
+ echo "::error::PR metadata is missing the bundled release context marker for BUNDLED-CHANGES-${CURRENT_RELEASE}.md."
89
+ echo "::error::Update the PR body or regenerate the release PR from the current integration head."
90
+ exit 1
91
+ fi
92
+ fi
93
+ fi
94
+
95
+ echo "Release Scope Integrity verified for ${CURRENT_RELEASE}."
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ # check-release-pr-scope.test.sh — Tests for release PR metadata drift checks.
3
+
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ HELPER="$SCRIPT_DIR/check-release-pr-scope.sh"
8
+
9
+ PASS=0
10
+ FAIL=0
11
+ WORK=$(mktemp -d)
12
+ trap 'rm -rf "$WORK"' EXIT
13
+
14
+ assert_exit() {
15
+ local name="$1" expected="$2" actual="$3"
16
+ if [ "$expected" = "$actual" ]; then
17
+ echo " ✓ $name"
18
+ PASS=$((PASS + 1))
19
+ else
20
+ echo " ✗ $name"
21
+ echo " Expected exit: $expected"
22
+ echo " Actual exit: $actual"
23
+ FAIL=$((FAIL + 1))
24
+ fi
25
+ }
26
+
27
+ assert_grep() {
28
+ local name="$1" pattern="$2" file="$3"
29
+ if grep -Eq "$pattern" "$file"; then
30
+ echo " ✓ $name"
31
+ PASS=$((PASS + 1))
32
+ else
33
+ echo " ✗ $name"
34
+ echo " Missing pattern: $pattern"
35
+ FAIL=$((FAIL + 1))
36
+ fi
37
+ }
38
+
39
+ make_fixture() {
40
+ local dir="$1" subject="$2"
41
+ rm -rf "$dir"
42
+ mkdir -p "$dir/scripts" "$dir/compliance/pending-releases"
43
+ cp "$SCRIPT_DIR/derive-release-version.sh" "$dir/scripts/derive-release-version.sh"
44
+ cp "$SCRIPT_DIR/extract-release-metadata.sh" "$dir/scripts/extract-release-metadata.sh"
45
+ cp "$HELPER" "$dir/scripts/check-release-pr-scope.sh"
46
+ chmod +x "$dir/scripts/"*.sh
47
+ (
48
+ cd "$dir"
49
+ git init -q
50
+ git config user.email test@example.com
51
+ git config user.name tester
52
+ git commit --allow-empty -q -m "$subject"
53
+ )
54
+ }
55
+
56
+ run_check() {
57
+ local dir="$1" out="$2"
58
+ shift 2
59
+ (
60
+ cd "$dir"
61
+ "$@" bash scripts/check-release-pr-scope.sh
62
+ ) >"$out" 2>&1
63
+ }
64
+
65
+ echo "=== check-release-pr-scope.sh tests ==="
66
+ echo ""
67
+
68
+ echo "--- Test 1: matching REQ scope passes ---"
69
+ make_fixture "$WORK/test1" "[REQ-093] feat: ship release"
70
+ cat > "$WORK/test1/compliance/pending-releases/RELEASE-TICKET-REQ-093.md" <<'EOF'
71
+ # Release Ticket — REQ-093
72
+
73
+ **Requirement:** REQ-093 — Example release
74
+
75
+ ## Summary
76
+ Normal tracked release.
77
+ EOF
78
+ OUT="$WORK/test1.out"
79
+ if run_check "$WORK/test1" "$OUT" env PR_TITLE="Release: REQ-093" PR_BODY=$'## Release\n- Release: REQ-093\n'; then
80
+ CODE=0
81
+ else
82
+ CODE=$?
83
+ fi
84
+ assert_exit "matching REQ scope passes" 0 "$CODE"
85
+ assert_grep "success message emitted" 'Release Scope Integrity verified for REQ-093' "$OUT"
86
+ echo ""
87
+
88
+ echo "--- Test 2: stale REQ scope fails ---"
89
+ make_fixture "$WORK/test2" "[REQ-093] feat: ship release"
90
+ OUT="$WORK/test2.out"
91
+ if run_check "$WORK/test2" "$OUT" env PR_TITLE="Release: REQ-092" PR_BODY=$'## Release\n- Release: REQ-092\n'; then
92
+ CODE=0
93
+ else
94
+ CODE=$?
95
+ fi
96
+ assert_exit "stale REQ scope fails" 1 "$CODE"
97
+ assert_grep "declared scope printed" 'Declared PR scope: REQ-092' "$OUT"
98
+ assert_grep "derived scope printed" 'Derived effective scope: REQ-093' "$OUT"
99
+ echo ""
100
+
101
+ echo "--- Test 3: bundled release must mention bundled marker ---"
102
+ make_fixture "$WORK/test3" "[REQ-093] feat: bundled release"
103
+ cat > "$WORK/test3/compliance/pending-releases/RELEASE-TICKET-REQ-093.md" <<'EOF'
104
+ # Release Ticket — REQ-093
105
+
106
+ **Requirement:** REQ-093 — Example release
107
+
108
+ ## Summary
109
+ Tracked release summary.
110
+ EOF
111
+ cat > "$WORK/test3/compliance/pending-releases/BUNDLED-CHANGES-REQ-093.md" <<'EOF'
112
+ # Bundled Changes — REQ-093
113
+ EOF
114
+ OUT="$WORK/test3.out"
115
+ if run_check "$WORK/test3" "$OUT" env PR_TITLE="Release: REQ-093" PR_BODY=$'## Release\n- Release: REQ-093\n'; then
116
+ CODE=0
117
+ else
118
+ CODE=$?
119
+ fi
120
+ assert_exit "missing bundled marker fails" 1 "$CODE"
121
+ assert_grep "bundled mismatch printed" 'bundled release context marker' "$OUT"
122
+ echo ""
123
+
124
+ echo "--- Test 4: hotfix branches skip scope integrity ---"
125
+ make_fixture "$WORK/test4" "[REQ-093] feat: ship release"
126
+ OUT="$WORK/test4.out"
127
+ if run_check "$WORK/test4" "$OUT" env HEAD_REF="hotfix/urgent-fix" PR_TITLE="fix: urgent prod bug" PR_BODY="HOTFIX"; then
128
+ CODE=0
129
+ else
130
+ CODE=$?
131
+ fi
132
+ assert_exit "hotfix branch skips" 0 "$CODE"
133
+ assert_grep "hotfix skip message" 'Hotfix PR detected' "$OUT"
134
+ echo ""
135
+
136
+ echo "=== check-release-pr-scope.test.sh: $PASS passed, $FAIL failed ==="
137
+ [ "$FAIL" -eq 0 ]