@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.
- package/dist/index.js +53 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +55 -1
- package/sdlc/files/_common/0-project-setup.md +10 -4
- package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
- package/sdlc/files/_common/README_TEMPLATE.md +8 -8
- package/sdlc/files/_common/Test_Architecture.md +15 -15
- package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
- package/sdlc/files/_common/Test_Policy.md +2 -2
- package/sdlc/files/_common/Test_Strategy.md +10 -10
- package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
- package/sdlc/files/_common/governance/dpia.md.template +3 -3
- package/sdlc/files/_common/governance/incident-report.md.template +3 -3
- package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
- package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
- package/sdlc/files/_common/governance/risk-register.md.template +1 -1
- package/sdlc/files/_common/governance/ropa.md.template +2 -2
- package/sdlc/files/_common/joining-an-existing-project.md +4 -4
- package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
- package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
- package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
- package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +311 -64
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +133 -82
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
- package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
- package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
- package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
- package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
- package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
- package/sdlc/files/_common/scripts/report-release-check.sh +86 -0
- package/sdlc/files/_common/scripts/report-release-check.test.sh +97 -0
- package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
- package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
- package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
- package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
- package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
- package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
- package/sdlc/files/ci/ci.yml.template +119 -28
- package/sdlc/files/ci/close-out-release.yml.template +7 -4
- package/sdlc/files/ci/compliance-evidence.yml.template +230 -20
- package/sdlc/files/ci/feature-e2e.yml.template +84 -1
- package/sdlc/files/ci/post-deploy-prod.yml.template +189 -6
- package/sdlc/files/ci/python/ci.yml.template +60 -12
- package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +44 -2
- package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
- package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
- 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 ]
|