@msn-control/liftoff 0.7.0 → 0.8.0
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/README.md +10 -1
- package/assets/governance/single-maintainer-gitflow/policy.md +705 -0
- package/assets/locks/frontend/package-lock.json +1012 -1694
- package/assets/locks/frontend/package.json +5 -6
- package/assets/locks/go-backend/go.mod +17 -0
- package/assets/locks/go-backend/go.sum +159 -0
- package/assets/locks/node-backend/package-lock.json +1473 -2320
- package/assets/locks/node-backend/package.json +8 -7
- package/assets/locks/opentofu-azure/.terraform.lock.hcl +39 -0
- package/assets/locks/opentofu-azure/versions.tf +10 -0
- package/assets/locks/python-genai/function-requirements.txt +1734 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/PKG-INFO +25 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/SOURCES.txt +6 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/dependency_links.txt +1 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/requires.txt +23 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/top_level.txt +1 -0
- package/assets/locks/python-genai/pyproject.toml +42 -0
- package/assets/locks/python-genai/uv.lock +2536 -0
- package/assets/locks/python-standard/pyproject.toml +36 -0
- package/assets/locks/python-standard/uv.lock +1780 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +4 -4
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +667 -1108
- package/assets/supported-stack.json +711 -0
- package/dist/args.js +12 -0
- package/dist/args.js.map +1 -1
- package/dist/catalogs.d.ts +3 -1
- package/dist/catalogs.js +29 -7
- package/dist/catalogs.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +6 -0
- package/dist/commands.js +243 -40
- package/dist/commands.js.map +1 -1
- package/dist/container-validation.d.ts +2 -0
- package/dist/container-validation.js +26 -0
- package/dist/container-validation.js.map +1 -0
- package/dist/file-system.js +130 -5
- package/dist/file-system.js.map +1 -1
- package/dist/framework-adapters.js +1 -0
- package/dist/framework-adapters.js.map +1 -1
- package/dist/go-template-assets.d.ts +2 -0
- package/dist/go-template-assets.js +14 -0
- package/dist/go-template-assets.js.map +1 -0
- package/dist/init-filesystem.js +5 -4
- package/dist/init-filesystem.js.map +1 -1
- package/dist/interactive.js +11 -7
- package/dist/interactive.js.map +1 -1
- package/dist/opentofu-template-assets.d.ts +2 -0
- package/dist/opentofu-template-assets.js +10 -0
- package/dist/opentofu-template-assets.js.map +1 -0
- package/dist/package-identity.d.ts +8 -0
- package/dist/package-identity.js +16 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/planner.js +42 -3
- package/dist/planner.js.map +1 -1
- package/dist/power-apps-templates.js +6 -0
- package/dist/power-apps-templates.js.map +1 -1
- package/dist/project-dependencies.d.ts +1 -1
- package/dist/project-dependencies.js +15 -34
- package/dist/project-dependencies.js.map +1 -1
- package/dist/published-verifier.js +11 -1
- package/dist/published-verifier.js.map +1 -1
- package/dist/python-template-assets.d.ts +4 -0
- package/dist/python-template-assets.js +34 -0
- package/dist/python-template-assets.js.map +1 -0
- package/dist/repository-governance.d.ts +17 -0
- package/dist/repository-governance.js +507 -0
- package/dist/repository-governance.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -1
- package/dist/runtime.js.map +1 -1
- package/dist/self-upgrade.d.ts +77 -0
- package/dist/self-upgrade.js +499 -0
- package/dist/self-upgrade.js.map +1 -0
- package/dist/stable-release.d.ts +18 -0
- package/dist/stable-release.js +66 -0
- package/dist/stable-release.js.map +1 -0
- package/dist/standard-templates.js +28 -97
- package/dist/standard-templates.js.map +1 -1
- package/dist/supported-stack.d.ts +115 -0
- package/dist/supported-stack.js +439 -0
- package/dist/supported-stack.js.map +1 -0
- package/dist/telemetry/contract.d.ts +1 -1
- package/dist/telemetry/contract.js +1 -0
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/templates.js +214 -123
- package/dist/templates.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/workstation-catalog.js +15 -12
- package/dist/workstation-catalog.js.map +1 -1
- package/dist/workstation.js +16 -6
- package/dist/workstation.js.map +1 -1
- package/docs/cli-reference.md +59 -2
- package/docs/configuration-and-manifests.md +13 -5
- package/docs/existing-repositories.md +21 -0
- package/docs/getting-started.md +23 -7
- package/docs/prerequisites.md +30 -4
- package/docs/project-structure.md +20 -1
- package/docs/repository-governance.md +91 -0
- package/docs/safety-and-consent.md +25 -0
- package/docs/spec-workflows-and-agents.md +19 -6
- package/docs/supported-stack.md +89 -0
- package/docs/telemetry.md +6 -0
- package/docs/troubleshooting.md +78 -0
- package/docs/workloads.md +14 -5
- package/package.json +14 -6
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
---
|
|
2
|
+
schemaVersion: 1
|
|
3
|
+
profile: single-maintainer-gitflow
|
|
4
|
+
policyVersion: "1"
|
|
5
|
+
state: handoff-generated
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Repository bootstrap standard — GitFlow, governance, and security
|
|
9
|
+
|
|
10
|
+
Set up GitFlow branching, repository governance, and the security pipeline in this repository.
|
|
11
|
+
This is my standard for every new repository, so implement it as a repeatable baseline rather than a
|
|
12
|
+
one-off.
|
|
13
|
+
|
|
14
|
+
## Fixed context — these are settled, do not re-litigate them
|
|
15
|
+
|
|
16
|
+
- **Single-maintainer repositories, by design.** Each developer owns their own repository and is its
|
|
17
|
+
sole maintainer. There is no second reviewer, and none is wanted. **No change in this repository
|
|
18
|
+
requires another person's approval — including changes to workflows, rulesets and governance files.**
|
|
19
|
+
The repository owner merges their own pull requests. Automated checks are the entire gate, so they
|
|
20
|
+
must be strict and must fail closed. Never design anything that depends on someone else reviewing it,
|
|
21
|
+
and never treat the absence of peer review as a gap to be closed.
|
|
22
|
+
- **Do not require human approval anywhere.** Specifically: set
|
|
23
|
+
`required_approving_review_count: 0`, `require_code_owner_review: false` and
|
|
24
|
+
`require_last_push_approval: false`. Do not create a `CODEOWNERS` file. Do not add required
|
|
25
|
+
reviewers to any GitHub Environment. Do not add a manual approval step to any workflow. If a
|
|
26
|
+
best-practice default would introduce a human approver, override it and say so.
|
|
27
|
+
To be precise: me approving *your plan* in this conversation is expected and required. What is
|
|
28
|
+
forbidden is any *merge or deploy gate that waits on a person* once the automation is in place.
|
|
29
|
+
- **Repository-scoped only. Org-level rulesets are out of scope and will not be set up.** I am a
|
|
30
|
+
repository admin, not an org owner. Every control must be applied per-repository and must work
|
|
31
|
+
standalone in a single repository. Do not propose, recommend, or design around org-level rulesets,
|
|
32
|
+
org-level required workflows, or org-wide GitHub App installations — not even as a "better
|
|
33
|
+
alternative" or a future phase. Do not treat the per-repository approach as a limitation to be
|
|
34
|
+
worked around; it is the deliberate design. If a control genuinely cannot be enforced at repository
|
|
35
|
+
scope, say so plainly and leave it out rather than proposing an org-level substitute.
|
|
36
|
+
- **Automation identity is the built-in `GITHUB_TOKEN`.** Set the **GitHub Actions app** as the bypass
|
|
37
|
+
actor where automation must act on a protected ref — no custom GitHub App is available, since
|
|
38
|
+
installing one requires org ownership. Note that pushes made with `GITHUB_TOKEN` do not trigger
|
|
39
|
+
further workflows: never rely on a tag-push trigger, and do the follow-on work in the same workflow
|
|
40
|
+
run.
|
|
41
|
+
- **GitHub Advanced Security is licensed org-wide.** Use Secret Protection, CodeQL and Copilot Autofix.
|
|
42
|
+
Do not add third-party equivalents such as Gitleaks or Semgrep — they would duplicate licensed
|
|
43
|
+
capability.
|
|
44
|
+
- **Staging sits behind private networking.** DAST must run on a self-hosted runner with access to it.
|
|
45
|
+
- **The GitHub Release plus attestations are the audit record.** There is no external GRC or
|
|
46
|
+
change-management system to integrate with.
|
|
47
|
+
|
|
48
|
+
## Basis
|
|
49
|
+
|
|
50
|
+
The branching model is Vincent Driessen's original GitFlow:
|
|
51
|
+
https://nvie.com/posts/a-successful-git-branching-model/
|
|
52
|
+
Follow its branch roles and merge directions faithfully. Note Driessen's own 2020 addendum — GitFlow
|
|
53
|
+
suits versioned releases rather than continuous delivery. If this repository genuinely ships
|
|
54
|
+
continuously, say so and tell me where you are deviating and why, rather than forcing the full model.
|
|
55
|
+
|
|
56
|
+
## Phase 0 — Classify the repository first
|
|
57
|
+
|
|
58
|
+
Report before you change anything.
|
|
59
|
+
|
|
60
|
+
1. What kind of artifact does this repo produce — container image, mobile app, library, static site,
|
|
61
|
+
infrastructure only? This determines which parts of the standard apply.
|
|
62
|
+
2. Language(s), package managers, and the build and test commands that genuinely work today.
|
|
63
|
+
3. Anything already present: branches, workflows and their exact job names, rulesets, tags, releases,
|
|
64
|
+
environments, deployment pipelines, security scanning.
|
|
65
|
+
4. Whether a self-hosted runner group with Staging access exists. If it does not, say so plainly —
|
|
66
|
+
DAST cannot run without it, and the release lane will be unable to qualify a candidate.
|
|
67
|
+
5. What monitoring and alerting already exists — alert rules, action groups, where they route, and
|
|
68
|
+
which components have no coverage at all. Name the gaps explicitly.
|
|
69
|
+
6. Which components expose a health endpoint, and whether it is shallow (process is alive) or deep
|
|
70
|
+
(dependencies are reachable). A shallow check reported as health is a gap, not coverage.
|
|
71
|
+
|
|
72
|
+
Then state the gap and your proposed order of work, and **get my approval before making changes.**
|
|
73
|
+
|
|
74
|
+
**Adapt honestly.** Container scanning is meaningless for a mobile app; SBOM and image digests do not
|
|
75
|
+
apply to a library the same way. Implement what is real for this repo and tell me explicitly what you
|
|
76
|
+
skipped and why. Never ship a workflow that cannot pass.
|
|
77
|
+
|
|
78
|
+
## Phase 1 — Branching model
|
|
79
|
+
|
|
80
|
+
- `develop` is the integration branch and the **default branch**. Feature branches (`feat/`, `fix/`,
|
|
81
|
+
`chore/`, `ci/`, `docs/`) branch from `develop` and merge back into `develop` by pull request only.
|
|
82
|
+
- `main` is production truth. Every commit on it is a released version. It only ever receives merges
|
|
83
|
+
from `release/**` or `hotfix/**`.
|
|
84
|
+
- `release/X.Y.Z` branches from `develop` — cut only by `workflow_dispatch` from an exact named healthy
|
|
85
|
+
SHA, never from a local checkout. Only stabilisation fixes land on it. On completion it merges to
|
|
86
|
+
`main` and back into `develop` so fixes are never lost.
|
|
87
|
+
- `hotfix/X.Y.Z` branches from `main`, requires an incident reference, and merges to both `main` and
|
|
88
|
+
`develop` — or into the open release branch if one exists.
|
|
89
|
+
- Nobody pushes directly to any protected branch, including me.
|
|
90
|
+
|
|
91
|
+
## Phase 2 — Release versioning on `main`
|
|
92
|
+
|
|
93
|
+
Every production merge must produce a real, visible version.
|
|
94
|
+
|
|
95
|
+
- Semantic versioning. The `release/X.Y.Z` or `hotfix/X.Y.Z` branch name is the single source of truth
|
|
96
|
+
for the version; nothing else declares it.
|
|
97
|
+
- Merges into `main` are true merge commits, never squashed, so both parents stay traceable.
|
|
98
|
+
- After a successful production deploy — and only then — automation creates an annotated `vX.Y.Z` tag
|
|
99
|
+
on that exact `main` merge commit and a matching **GitHub Release** targeting `main`.
|
|
100
|
+
- The Release body must contain: the changelog between the previous tag and this one, the deployed
|
|
101
|
+
artifact digest, the source `develop` SHA the release was cut from, a link to the staging
|
|
102
|
+
qualification run, the **evidence bundle digest** and links to the SBOM and scan reports, the
|
|
103
|
+
**expected signer identity and OIDC issuer** an auditor should pin when verifying, and an AI
|
|
104
|
+
Acceptable Use Policy attestation record. For a hotfix, include the incident reference. Attach the
|
|
105
|
+
**SBOM, the full evidence bundle, the attestation bundle and `trusted_root.jsonl`** as Release
|
|
106
|
+
assets — these are the durable audit record and must not live only in expiring workflow artifacts.
|
|
107
|
+
Publishing the trusted root alongside the bundle is what makes offline, air-gapped verification
|
|
108
|
+
possible; without it an auditor with no network access to Sigstore cannot verify anything.
|
|
109
|
+
- Before tagging, validate: the version is valid semver, is strictly greater than the latest tag, does
|
|
110
|
+
not already exist as a tag or Release, and the commit is on `main` and actually deployed. Fail rather
|
|
111
|
+
than tag speculatively.
|
|
112
|
+
- Only automation creates `v*` tags. Tags are immutable — never deleted, moved or force-updated. A
|
|
113
|
+
mistaken release is corrected by publishing the next patch version.
|
|
114
|
+
- A failed production deploy creates no tag and no Release. That version is burnt; the repair path uses
|
|
115
|
+
the next patch version. Never reuse a burnt version.
|
|
116
|
+
- Any commit on `main` without a corresponding `vX.Y.Z` tag is an anomaly — add a check that reports it.
|
|
117
|
+
|
|
118
|
+
## Phase 3 — Promotion: build once, promote the identical artifact
|
|
119
|
+
|
|
120
|
+
The artifact is built once per candidate and recorded in a release manifest with its digest, SBOM
|
|
121
|
+
digest, provenance attestation and scan results. Dev, Staging and Production all deploy that same
|
|
122
|
+
digest. Never rebuild per environment; never resolve a floating tag like `latest`. Configuration
|
|
123
|
+
differs per environment; the artifact does not.
|
|
124
|
+
|
|
125
|
+
- **Dev** — every push to `develop` deploys automatically. No gate.
|
|
126
|
+
- **Staging** — a push to `release/**` or `hotfix/**` builds the candidate, scans it, generates its
|
|
127
|
+
SBOM, deploys to Staging, then runs the qualification suite and DAST. It records a qualification
|
|
128
|
+
record bound to that exact digest and commit.
|
|
129
|
+
- **Production** — merging into `main` promotes. The workflow resolves the already-qualified digest,
|
|
130
|
+
verifies the attestation and qualification record bind to that exact commit and digest, and refuses
|
|
131
|
+
to proceed on any mismatch. Deployment is recorded through a GitHub Environment for the deployment
|
|
132
|
+
history and audit trail only — configure it with **no required reviewers**, so promotion is never
|
|
133
|
+
blocked waiting on a person.
|
|
134
|
+
- Promotion is strictly forward. Rollback redeploys a previous release manifest **by digest**, never a
|
|
135
|
+
rebuild, and records which version it rolled back to. Rollback is never gated — see "Roll back
|
|
136
|
+
first, debug later" below.
|
|
137
|
+
|
|
138
|
+
### Deployment strategy — blue-green mechanism, canary exposure
|
|
139
|
+
|
|
140
|
+
Two separate decisions, often conflated. Be explicit about both.
|
|
141
|
+
|
|
142
|
+
**1. Mechanism — how a version replaces another within one deployment unit. Always blue-green.**
|
|
143
|
+
|
|
144
|
+
- Run the platform in a mode that supports parallel versions (for Azure Container Apps, `Multiple`
|
|
145
|
+
revision mode; for App Service, deployment slots; for Kubernetes, a parallel deployment behind a
|
|
146
|
+
switchable service).
|
|
147
|
+
- Deploy the new version alongside the current one at **zero traffic**.
|
|
148
|
+
- Qualify it on a **version-specific origin** — its own revision URL or slot hostname — while it still
|
|
149
|
+
serves no users. This is the step that makes the model work: verification happens before any user is
|
|
150
|
+
exposed, not after.
|
|
151
|
+
- Only then switch traffic **atomically** to 100% of the new version, setting all prior versions to 0.
|
|
152
|
+
- Run post-switch smoke checks. On failure, **restore the previous traffic weights** and set the
|
|
153
|
+
failed version to 0. Rollback is a traffic change measured in seconds, not a redeploy.
|
|
154
|
+
- Never leave a deployment unit in a mixed-version state. Two versions may exist simultaneously, but
|
|
155
|
+
only one serves traffic. That guarantee is what lets an auditor ask "what was running at 14:32" and
|
|
156
|
+
get exactly one answer — the attestation and evidence chain depends on it.
|
|
157
|
+
- Do **not** use rolling in-place updates. They create a mixed-version window, make rollback slow, and
|
|
158
|
+
break that guarantee.
|
|
159
|
+
|
|
160
|
+
**2. Exposure — how much traffic sees the new version, and when. Canary before full traffic.**
|
|
161
|
+
|
|
162
|
+
The Dev → Staging → Production path is already progressive exposure. Canary extends it *inside*
|
|
163
|
+
production so that a defect which survived Staging is caught by a small fraction of real users rather
|
|
164
|
+
than all of them.
|
|
165
|
+
|
|
166
|
+
Canary runs **on top of** the blue-green mechanism above — they are complementary, not alternatives.
|
|
167
|
+
Blue-green is how a version gets deployed and rolled back; canary is how traffic reaches it.
|
|
168
|
+
|
|
169
|
+
**The canary sequence:**
|
|
170
|
+
|
|
171
|
+
1. Deploy the new version at **zero traffic** and qualify it on its version-specific origin, exactly
|
|
172
|
+
as above. Nothing reaches a user until this passes.
|
|
173
|
+
2. Deploy a **fresh baseline revision of the current version** alongside it. Compare the canary
|
|
174
|
+
against this baseline, **never against the existing production fleet** — the running fleet has
|
|
175
|
+
warmed caches, longer uptime and settled runtime state, which makes it an invalid control and
|
|
176
|
+
produces false signals in both directions. This is the detail that makes canary analysis
|
|
177
|
+
trustworthy, and the one most often got wrong.
|
|
178
|
+
3. **Experiment phase.** Shift a small slice of traffic to the canary and an **equal slice to the
|
|
179
|
+
fresh baseline** — for example 10% canary, 10% baseline, with the remaining 80% still served by the
|
|
180
|
+
current fleet. Equal slices are what make the comparison fair.
|
|
181
|
+
4. Hold for a defined **bake window** and run automated analysis over it.
|
|
182
|
+
5. **Promotion phase.** On a passing analysis, switch **atomically to 100% canary** and retire both
|
|
183
|
+
the baseline and the old revision.
|
|
184
|
+
|
|
185
|
+
**Do not ramp the canary and baseline through intermediate steps such as 50/50.** Two reasons. First,
|
|
186
|
+
traffic weights sum to 100, so canary 50 plus baseline 50 leaves the original revision at zero — that
|
|
187
|
+
moves every user onto two freshly deployed, cold revisions and abandons the warm, proven fleet you
|
|
188
|
+
would otherwise fall back to. Second, the baseline is a *control*, not a rollout vehicle; its only job
|
|
189
|
+
is to match the canary's slice size. The comparison is either statistically valid at the experiment
|
|
190
|
+
slice or it is not, and a larger slice does not rescue an invalid one — it only exposes more users.
|
|
191
|
+
Go from the experiment slice straight to the atomic switch.
|
|
192
|
+
|
|
193
|
+
**Analysis and gating:**
|
|
194
|
+
|
|
195
|
+
- Compare error rate, latency (p50, p95, p99) and saturation between canary and baseline over the
|
|
196
|
+
bake window, against explicit, committed thresholds.
|
|
197
|
+
- **Advancement must be fully automated.** There is no reviewer on these repositories, so a canary
|
|
198
|
+
that waits for a human to click "promote" is not a control — it is a pause. Encode the decision.
|
|
199
|
+
- **Fail closed.** If metrics are missing, the analysis cannot reach a conclusion, or the bake window
|
|
200
|
+
times out, **roll back** — never advance on absent evidence. A canary that proceeds when it cannot
|
|
201
|
+
measure anything is the fail-open pattern in a different costume.
|
|
202
|
+
- Any threshold breach triggers an **immediate rollback to 100% previous version** — restore the
|
|
203
|
+
original revision's weight and set canary and baseline to 0.
|
|
204
|
+
- Record the canary analysis result — thresholds, observed values, decision — in the evidence bundle
|
|
205
|
+
for that release.
|
|
206
|
+
|
|
207
|
+
**Size the canary to the traffic that actually exists.** The bake window must be long enough for the
|
|
208
|
+
slice to produce a meaningful sample; a five-minute window on a service with a handful of requests
|
|
209
|
+
proves nothing and merely delays the release. State the assumed traffic volume and the resulting bake
|
|
210
|
+
window explicitly, and if the service is too quiet for a canary slice to be meaningful, say so and use
|
|
211
|
+
a single atomic switch with instant rollback instead — but say it, rather than shipping a canary that
|
|
212
|
+
is statistically empty.
|
|
213
|
+
|
|
214
|
+
Where more than one region or deployment unit exists, prefer making the **first unit** the canary
|
|
215
|
+
(Microsoft's Azure Safe Deployment Practices model — Canary → Pilot → Broad, each a region with its
|
|
216
|
+
own bake time) over splitting traffic within a single unit. A failure is then contained to one unit,
|
|
217
|
+
and rollback is a traffic switch you have already tested.
|
|
218
|
+
|
|
219
|
+
### Roll back first, debug later
|
|
220
|
+
|
|
221
|
+
This is the single most valuable deployment practice at any scale, and it is policy here, not advice.
|
|
222
|
+
|
|
223
|
+
- When canary analysis fails or post-switch checks fail, **restore traffic to the previous version
|
|
224
|
+
immediately.** Do not investigate first. Do not "just check one thing." The rollback is a
|
|
225
|
+
traffic-weight change measured in seconds — it costs almost nothing, and the exposure window while
|
|
226
|
+
you diagnose costs users.
|
|
227
|
+
- **Retain the failed revision at zero traffic** for forensics. It serves nobody but remains available
|
|
228
|
+
for inspection, along with logs and telemetry captured during the exposure.
|
|
229
|
+
- **Never forward-fix under pressure.** The next patch version, through the normal gated path, is the
|
|
230
|
+
only route back. A hotfix that skips the gates to resolve an incident faster is how an incident
|
|
231
|
+
becomes two incidents.
|
|
232
|
+
- **Rollback must never be gated.** It requires no approval, no qualification record, and no ruleset
|
|
233
|
+
bypass — it redeploys an artifact that already passed every gate. Verify that the rollback path
|
|
234
|
+
genuinely works, and rehearse it, before requiring anything that depends on it.
|
|
235
|
+
- Automated rollback is the default path; a manual rollback workflow exists as a backstop and takes an
|
|
236
|
+
incident reference.
|
|
237
|
+
|
|
238
|
+
If the target platform cannot support parallel versions, say so plainly and describe what you
|
|
239
|
+
implemented instead — do not silently fall back to an in-place update and call it a deployment.
|
|
240
|
+
|
|
241
|
+
### Runtime monitoring and alerting
|
|
242
|
+
|
|
243
|
+
The security pipeline proves the artifact was sound when it shipped. Monitoring is what tells you the
|
|
244
|
+
running system is sound now. Both are required; neither substitutes for the other.
|
|
245
|
+
|
|
246
|
+
**Alerting is infrastructure as code.** Define every alert rule, action group and routing target in
|
|
247
|
+
the same IaC as the resources they watch, reviewed and versioned alongside them. Never configure an
|
|
248
|
+
alert by hand in a portal — a hand-made alert is invisible to review, absent from a rebuilt
|
|
249
|
+
environment, and lost when a resource is recreated.
|
|
250
|
+
|
|
251
|
+
**Route everything to Slack, and route by severity.** Use a single action group per environment with
|
|
252
|
+
a Slack webhook receiver, and separate channels by severity so that noise and urgency do not share a
|
|
253
|
+
destination:
|
|
254
|
+
|
|
255
|
+
- **Sev 1 — service down or failing.** Sustained 5xx rate, container restart loop, health probe
|
|
256
|
+
failing, database unreachable. Goes to an alerting channel that is expected to interrupt someone.
|
|
257
|
+
- **Sev 2 — degraded or trending toward failure.** Saturation (CPU, memory, storage, connection
|
|
258
|
+
pool), elevated latency, queue depth growing, certificate or secret expiring within a defined
|
|
259
|
+
window. Goes to a lower-urgency channel.
|
|
260
|
+
- **Sev 3 — informational.** Deployment started and finished, canary promoted or rolled back,
|
|
261
|
+
scheduled re-scan found a new CVE in a released artifact. Goes to a log channel — useful context,
|
|
262
|
+
never an interruption.
|
|
263
|
+
|
|
264
|
+
Include environment, resource, severity, the firing condition with its observed value, and a direct
|
|
265
|
+
link to the resource and to the relevant dashboard in every message. An alert that says only
|
|
266
|
+
"something is wrong" costs more time than it saves.
|
|
267
|
+
|
|
268
|
+
**Cover every component that can fail independently**, not just the application:
|
|
269
|
+
|
|
270
|
+
- **Web / frontend** — availability from outside the network, 5xx rate, and a synthetic check of the
|
|
271
|
+
real user-facing hostname rather than an internal origin.
|
|
272
|
+
- **API / backend** — 5xx rate, p95 latency, restart count, health and readiness endpoint failures.
|
|
273
|
+
- **Database** — availability, storage percentage, CPU, connection saturation, replication lag,
|
|
274
|
+
and **backup success**. A failed backup is silent until the day it matters.
|
|
275
|
+
- **Cache / session store** — memory saturation and error rate. Where sessions or sign-in depend on
|
|
276
|
+
it, treat its failure as Sev 1: cache availability *is* authentication availability.
|
|
277
|
+
- **Messaging and communication services** (managed email/SMS/push services such as Azure
|
|
278
|
+
Communication Services, and message brokers such as Service Bus, Event Hubs, SQS or Kafka) —
|
|
279
|
+
delivery failure rate, queue depth, dead-letter count, and throttling. These fail quietly and are
|
|
280
|
+
usually discovered by a user reporting a message that never arrived, which is far too late.
|
|
281
|
+
- **Ingress / CDN / gateway** — origin health, TLS certificate expiry, and 4xx/5xx at the edge, which
|
|
282
|
+
catches failures that never reach the application at all.
|
|
283
|
+
- **Identity and secrets** — secret, certificate and credential expiry, with enough lead time to act.
|
|
284
|
+
- **Cost** — a budget threshold alert. A runaway cost is an incident, and it is often the first
|
|
285
|
+
visible symptom of a runaway process.
|
|
286
|
+
|
|
287
|
+
**Alert on symptoms, not causes.** Alert on what a user would notice — requests failing, requests
|
|
288
|
+
slow, messages not delivered. Resource-level signals belong to dashboards and to Sev 2 at most. A page
|
|
289
|
+
per underlying cause produces a flood during a single incident.
|
|
290
|
+
|
|
291
|
+
**Every alert must be actionable.** If nobody would do anything when it fires, it is a dashboard
|
|
292
|
+
metric, not an alert. Thresholds should be deliberately loose enough that a normal day is quiet: an
|
|
293
|
+
alert that fires routinely gets muted, and a muted alert is worse than no alert because it looks like
|
|
294
|
+
coverage while providing none.
|
|
295
|
+
|
|
296
|
+
**Alerting must not fail open** — the same defect class as a fail-open CI check, and considerably
|
|
297
|
+
harder to notice, because an environment with no alerts looks exactly like an environment with no
|
|
298
|
+
problems:
|
|
299
|
+
|
|
300
|
+
- Never make alert creation conditional on an optional variable being set. A pattern such as
|
|
301
|
+
`count = var.alert_webhook != "" ? 1 : 0` means a missing value silently produces an environment
|
|
302
|
+
with no alerts at all. Make the routing target **required**, and fail the deployment if it is
|
|
303
|
+
absent.
|
|
304
|
+
- Verify after deploy that the expected alert rules exist and are enabled, and fail the deployment if
|
|
305
|
+
any are missing. Treat a missing alert rule as a failed deployment.
|
|
306
|
+
- **Add a heartbeat.** Emit a scheduled signal that proves the whole path — metric to rule to action
|
|
307
|
+
group to Slack — is alive, and alert on its *absence*. Without one, a broken webhook is
|
|
308
|
+
indistinguishable from a healthy system.
|
|
309
|
+
- **Test that each alert fires**, exactly as required checks must be proven to go red. An alert that
|
|
310
|
+
has only ever been seen quiet has never been shown to work. Record the test.
|
|
311
|
+
|
|
312
|
+
**Wire alerting into the deployment path.** The canary analysis in the previous section reads the same
|
|
313
|
+
signals: use one definition of healthy so that the thresholds gating a canary and the thresholds
|
|
314
|
+
paging a human cannot drift apart. Announce deployment start, canary decision and rollback to Slack —
|
|
315
|
+
correlating "it broke" with "we shipped" is the single most useful piece of incident context.
|
|
316
|
+
Alert noise should be suppressed for the deploying resource during an expected restart, but never
|
|
317
|
+
suppressed globally.
|
|
318
|
+
|
|
319
|
+
Feed Defender for Cloud and Microsoft Sentinel where configured, and route their high-severity
|
|
320
|
+
findings to the same Slack destinations, so security and availability alerts reach one place rather
|
|
321
|
+
than two.
|
|
322
|
+
|
|
323
|
+
#### Service health model — know the state of everything, to recover quickly
|
|
324
|
+
|
|
325
|
+
Alerting tells you something broke. A health model tells you **what state every component is in right
|
|
326
|
+
now**, which is what recovery actually requires. Build both.
|
|
327
|
+
|
|
328
|
+
- Give **every component a machine-readable health state** — the web frontend, each API, the
|
|
329
|
+
database, the cache, messaging and communication services, storage, ingress, and every managed
|
|
330
|
+
cloud dependency. Not just "the app is up".
|
|
331
|
+
- Distinguish **shallow from deep checks**. A liveness endpoint that returns 200 from the process
|
|
332
|
+
proves the process is running. A readiness or deep check must exercise the real dependency path —
|
|
333
|
+
a database round trip, a cache read, a token acquisition — because "the API is up but cannot reach
|
|
334
|
+
the database" is the state you most need to see, and the one a shallow check hides.
|
|
335
|
+
- **Aggregate into one view** showing, per environment, every component and its state, the deployed
|
|
336
|
+
version and artifact digest, and when it was last checked. During an incident the first question is
|
|
337
|
+
always "what is broken and what is fine", and answering it by opening six consoles costs the
|
|
338
|
+
minutes that matter.
|
|
339
|
+
- **Record dependencies** so the view shows blast radius and recovery order. If the database is down,
|
|
340
|
+
the APIs depending on it are *consequences*, not separate incidents. Without this, one failure
|
|
341
|
+
presents as ten alerts and the actual cause is guesswork.
|
|
342
|
+
- Report each component's **currently deployed version and digest** alongside its health. Recovery
|
|
343
|
+
decisions turn on whether a component is running what you think it is running, and this is also
|
|
344
|
+
what makes "roll back first" actionable.
|
|
345
|
+
- The health view must be **queryable when things are broken**. Do not host it inside the system it
|
|
346
|
+
monitors, and do not let it depend on that system's database or identity provider. A status page
|
|
347
|
+
that goes down with the service is worse than none, because its silence is ambiguous.
|
|
348
|
+
- Expose it as **structured data, not only a dashboard**, so the deployment pipeline, the canary
|
|
349
|
+
analysis and the alert rules can all consume the same health definition rather than each
|
|
350
|
+
maintaining a private one.
|
|
351
|
+
- Also surface **cloud provider platform status** for the regions and services you depend on. A
|
|
352
|
+
provider-side incident needs a different response from a defect you shipped, and telling them apart
|
|
353
|
+
early prevents a pointless rollback.
|
|
354
|
+
|
|
355
|
+
#### Delivery performance — the DORA four keys
|
|
356
|
+
|
|
357
|
+
Measure delivery performance using Google's DORA metrics, and derive them from the events this
|
|
358
|
+
pipeline already emits rather than from a separate system:
|
|
359
|
+
|
|
360
|
+
| Metric | Derive from |
|
|
361
|
+
| --- | --- |
|
|
362
|
+
| **Deployment frequency** | Successful production deployments recorded through the GitHub Environment |
|
|
363
|
+
| **Lead time for changes** | Commit timestamp on `develop` → production deployment of the release containing it |
|
|
364
|
+
| **Change failure rate** | Releases that triggered a rollback, a canary failure, or a hotfix, over total releases |
|
|
365
|
+
| **Failed deployment recovery time** | Failure detected → traffic restored to the previous version |
|
|
366
|
+
|
|
367
|
+
Every input already exists: GitHub deployments and Releases, the tag and its timestamp, canary
|
|
368
|
+
analysis decisions, rollback records with their incident references, and alert firing and resolution
|
|
369
|
+
times. Nothing new needs instrumenting — the work is to make those events queryable and to publish the
|
|
370
|
+
trend. Compute the metrics on a schedule and post the trend to the Slack log channel.
|
|
371
|
+
|
|
372
|
+
Note the current definitions: **Failed Deployment Recovery Time** is DORA's renaming of what was
|
|
373
|
+
called MTTR, and it deliberately scopes to recovery from a failed deployment rather than to all
|
|
374
|
+
incidents. Track recovery from non-deployment incidents too, but do not conflate the two.
|
|
375
|
+
|
|
376
|
+
**Change failure rate and failed deployment recovery time are the pair that matters most here.**
|
|
377
|
+
They are DORA's stability measures, and they are exactly what the blue-green, canary and instant
|
|
378
|
+
rollback design exists to improve. A rising change failure rate means the Staging gate is not
|
|
379
|
+
catching what it should; a rising recovery time means the rollback path has decayed. Both are early
|
|
380
|
+
warnings that a control has stopped working, visible long before an incident proves it.
|
|
381
|
+
|
|
382
|
+
**Use these as trend indicators for this repository, not as targets or as comparisons between teams
|
|
383
|
+
or people.** DORA's own 2025 guidance dropped the Elite/High/Medium/Low ranking in favour of context —
|
|
384
|
+
and a metric that becomes a target gets gamed. Deployment frequency in particular is trivially
|
|
385
|
+
inflated and means nothing on its own; it is only informative alongside the stability pair.
|
|
386
|
+
|
|
387
|
+
## Phase 4 — Security pipeline
|
|
388
|
+
|
|
389
|
+
Pipeline stages: Secret Scanning → SCA → SAST → IaC → Container → DAST → Continuous → Compliance.
|
|
390
|
+
Implement every stage that applies, mapped to the trigger where it can actually run.
|
|
391
|
+
|
|
392
|
+
**Use exactly these tools. Do not add alternatives that duplicate them.**
|
|
393
|
+
|
|
394
|
+
| Job | Tool |
|
|
395
|
+
| --- | --- |
|
|
396
|
+
| Secrets | GitHub Secret Protection — push protection, Copilot secret scanning, custom patterns |
|
|
397
|
+
| SCA | Dependabot + Dependency Review |
|
|
398
|
+
| SAST | CodeQL + Copilot Autofix |
|
|
399
|
+
| IaC | Checkov — covers Terraform, Kubernetes, Dockerfiles, Actions, ARM/Bicep |
|
|
400
|
+
| Container + SBOM | Trivy — image scanning and CycloneDX SBOM. **This is the blocking gate.** |
|
|
401
|
+
| Risk prioritisation | Grype (pin >= v0.88.0) — consumes the SBOM, ranks by CVSS + EPSS + CISA KEV. **Never gates.** |
|
|
402
|
+
| DAST | OWASP ZAP |
|
|
403
|
+
| Provenance | GitHub artifact attestations (L2) and `slsa-github-generator` (L3) |
|
|
404
|
+
| Posture | OSSF Scorecard |
|
|
405
|
+
|
|
406
|
+
Explicitly excluded as duplicates — do not reintroduce: tfsec (deprecated, merged into Trivy),
|
|
407
|
+
Terrascan, Kics, Template Analyzer, IaCFileScanner, Kubesec, Syft, Microsoft sbom-tool, Gitleaks,
|
|
408
|
+
Semgrep, Burp Suite (no CLI, cannot be automated), and the MSDO wrapper (it re-runs Checkov and Trivy
|
|
409
|
+
a second time). **Keep `trivy config` disabled** — Checkov owns IaC, Trivy owns images.
|
|
410
|
+
|
|
411
|
+
**Trivy and Grype are not duplicates and must not be run as two gates.** Trivy core has no EPSS or
|
|
412
|
+
CISA KEV support and its maintainers consider it out of scope, so exploitability-based ranking is a
|
|
413
|
+
genuine capability gap rather than overlap. Give them strictly separate roles: Trivy is the blocking
|
|
414
|
+
gate; Grype is a non-gating prioritiser that consumes the SBOM Trivy produced and reports what is
|
|
415
|
+
actually being exploited in the wild. Maintain exactly one allowlist, owned by Trivy. If Grype ever
|
|
416
|
+
starts failing builds, the design has been broken — two gates means double triage, and with no
|
|
417
|
+
reviewer that is how a gate gets disabled.
|
|
418
|
+
|
|
419
|
+
Stage mapping:
|
|
420
|
+
|
|
421
|
+
- **PR into `develop`** — secret scanning, dependency review, CodeQL, Checkov, and an action
|
|
422
|
+
SHA-pinning check. Source-level only. Keep it fast; a slow PR gate gets worked around.
|
|
423
|
+
- **Push to `develop`** — the above, plus build the artifact, generate its SBOM from the built image
|
|
424
|
+
rather than the source tree, scan the image with Trivy, and attest provenance at **SLSA Build L2**
|
|
425
|
+
using `actions/attest-build-provenance`.
|
|
426
|
+
- **`release/**` and `hotfix/**`** — the full gate, all bound to the promoted digest: Trivy scan of
|
|
427
|
+
that exact digest, SBOM, **SLSA Build L3** provenance via `slsa-github-generator`, OSSF Scorecard,
|
|
428
|
+
deploy to Staging, then OWASP ZAP against the deployed instance on a self-hosted runner. DAST needs a
|
|
429
|
+
running application and belongs here and nowhere else. Also run Grype against the candidate SBOM to
|
|
430
|
+
produce an exploitability-ranked risk report for the Release body — report only, never blocking. The
|
|
431
|
+
qualification record must not be issuable if any of the gating checks fail.
|
|
432
|
+
- **`main`** — verification only. Verify the L3 attestation, SBOM digest and qualification record all
|
|
433
|
+
bind to the digest being promoted. No new scans; re-scanning would describe a rebuilt artifact.
|
|
434
|
+
- **Scheduled** — re-scan released artifacts by running Grype against their **stored SBOMs**, which
|
|
435
|
+
needs no rebuild and no image pull, and re-scan the live production digest with Trivy. A CVE
|
|
436
|
+
published after release makes the deployed artifact vulnerable with no code change. Raise an issue on
|
|
437
|
+
new CISA KEV entries, high-EPSS findings, or new HIGH/CRITICAL severities. Feed Defender for Cloud and
|
|
438
|
+
Sentinel if configured.
|
|
439
|
+
|
|
440
|
+
Gating and evidence:
|
|
441
|
+
|
|
442
|
+
- Fail on **fixable** HIGH and CRITICAL findings. Upload SARIF from every scanner so results appear in
|
|
443
|
+
the Security tab rather than buried in logs.
|
|
444
|
+
- Suppressions are explicit and expiring: a committed allowlist with a reason and expiry per entry, and
|
|
445
|
+
the pipeline fails when an entry expires. Never suppress by lowering the global severity threshold.
|
|
446
|
+
With no reviewer, an unbounded allowlist is how a gate quietly becomes decorative.
|
|
447
|
+
- Commit an **AI Acceptable Use Policy** document. It is a documented policy and an attestation
|
|
448
|
+
recorded in the Release, not an automated check.
|
|
449
|
+
- Every scan emits a stable, named status check suitable for requiring by ruleset.
|
|
450
|
+
|
|
451
|
+
### Audit evidence — every stage must produce a durable, reviewable report
|
|
452
|
+
|
|
453
|
+
An auditor must be able to answer, years after the fact: what was scanned, when, with which tool and
|
|
454
|
+
which vulnerability database, what was found, what was accepted and on whose authority, and proof that
|
|
455
|
+
the report describes the artifact that actually shipped. Design for that, not for a dashboard.
|
|
456
|
+
|
|
457
|
+
Three tiers, because retention and mutability differ:
|
|
458
|
+
|
|
459
|
+
1. **Live triage — code scanning.** Every scanner uploads SARIF to GitHub code scanning. This is the
|
|
460
|
+
working view for fixing things. It is mutable live state — alerts get dismissed and resolved — so
|
|
461
|
+
it is never sufficient as the audit record on its own.
|
|
462
|
+
2. **Run evidence — job summaries and workflow artifacts.** Every security job writes a rendered
|
|
463
|
+
human-readable summary to `$GITHUB_STEP_SUMMARY` and uploads its raw output as a workflow artifact.
|
|
464
|
+
Useful for debugging. Actions artifacts expire, so never treat them as the audit record.
|
|
465
|
+
3. **Release evidence — the durable record.** For every release candidate, assemble one **evidence
|
|
466
|
+
bundle** and attach it to the GitHub Release. Release assets do not expire. This is the audit record.
|
|
467
|
+
|
|
468
|
+
The evidence bundle contains, for each pipeline stage:
|
|
469
|
+
|
|
470
|
+
- The raw machine-readable output exactly as the tool produced it (SARIF, CycloneDX, JSON).
|
|
471
|
+
- A rendered human-readable summary an auditor can read without tooling.
|
|
472
|
+
- A metadata record: tool name, **tool version, vulnerability database version and its timestamp**,
|
|
473
|
+
the exact command line, start and end time, exit status, and the workflow run URL. A finding count
|
|
474
|
+
is meaningless without the database version behind it — always record it.
|
|
475
|
+
|
|
476
|
+
And at bundle level:
|
|
477
|
+
|
|
478
|
+
- The artifact digest, source commit SHA, release version and branch.
|
|
479
|
+
- A `manifest.json` listing every file with its SHA-256.
|
|
480
|
+
- The suppression allowlist exactly as it stood at that moment, with each entry's reason, expiry and
|
|
481
|
+
the commit that introduced it.
|
|
482
|
+
- The DAST result, the Grype exploitability report, and the staging qualification record.
|
|
483
|
+
- The **attestation bundle** and **`trusted_root.jsonl`**, so the bundle carries everything needed to
|
|
484
|
+
verify itself without network access to GitHub or Sigstore.
|
|
485
|
+
- A `VERIFY.md` giving the expected OIDC issuer and certificate identity pattern, plus the exact
|
|
486
|
+
commands for both verification paths. The bundle must be self-describing: an auditor who receives
|
|
487
|
+
it on a USB stick, with no other context, should be able to verify it.
|
|
488
|
+
|
|
489
|
+
**Bind the bundle to the artifact.** Attest it with GitHub artifact attestations so
|
|
490
|
+
`gh attestation verify` proves the bundle belongs to that exact digest. An unbound report proves
|
|
491
|
+
nothing — an auditor cannot otherwise tell whether it describes what shipped. Record the bundle's own
|
|
492
|
+
digest in the release manifest and in the Release body.
|
|
493
|
+
|
|
494
|
+
**Point the gate at the evidence.** The staging qualification record must reference the evidence
|
|
495
|
+
bundle digest, so the single check that permits production is traceable to the complete evidence set.
|
|
496
|
+
|
|
497
|
+
**Do not archive every pull request scan.** That is noise and it is not what auditors ask for. That the
|
|
498
|
+
gate was continuously enforced is evidenced by the rulesets in git, the committed observed-contexts
|
|
499
|
+
file, and GitHub's own check-run history. Permanent evidence is required for what shipped.
|
|
500
|
+
|
|
501
|
+
**Scheduled re-scans produce dated reports too.** When Grype re-scans a stored SBOM and finds a new
|
|
502
|
+
CVE, file that report against the release it affects, so the record distinguishes what was known at
|
|
503
|
+
release time from what emerged later. An auditor reads that as diligence, not as a failure.
|
|
504
|
+
|
|
505
|
+
**External auditors have no GitHub access.** Evidence must therefore be independently verifiable by
|
|
506
|
+
someone outside the organisation, and by someone who cannot be given a GitHub account:
|
|
507
|
+
|
|
508
|
+
- Publish the **attestation bundle itself** as a downloadable Release asset, not only through the
|
|
509
|
+
GitHub attestation API, which requires authentication. GitHub attestations are recorded in
|
|
510
|
+
Sigstore's public transparency log, so an auditor holding the bundle and the artifact digest can
|
|
511
|
+
verify the whole chain against public infrastructure with no access to this repository and no
|
|
512
|
+
account here. Preserve that property deliberately — it means the auditor never has to trust any
|
|
513
|
+
system of ours, including any portal we build.
|
|
514
|
+
- Also publish **`trusted_root.jsonl`** (from `gh attestation trusted-root`) as a Release asset.
|
|
515
|
+
Without it, offline and air-gapped verification is impossible, which is exactly the environment a
|
|
516
|
+
defence-sector auditor is likely to be working in.
|
|
517
|
+
- **Document the expected signer identity** in `docs/security/audit-evidence.md`: the OIDC issuer
|
|
518
|
+
(`https://token.actions.githubusercontent.com`) and the exact certificate identity pattern for the
|
|
519
|
+
workflow that signs. A verification that does not pin the expected identity proves only that
|
|
520
|
+
*somebody* signed something — an auditor could accept a perfectly valid signature from an unrelated
|
|
521
|
+
repository. Pinning the identity is what makes the check meaningful.
|
|
522
|
+
- Give auditors **both** verification paths, since they will not all have the same tooling:
|
|
523
|
+
- `gh attestation verify <artifact> --bundle <bundle> --custom-trusted-root trusted_root.jsonl`
|
|
524
|
+
- `cosign verify-blob-attestation --bundle <bundle> --new-bundle-format --certificate-oidc-issuer <issuer> --certificate-identity-regexp <pattern> <artifact>`
|
|
525
|
+
The cosign path needs no GitHub tooling or account at all and is the vendor-neutral option. Note
|
|
526
|
+
that cosign is **not** used in the pipeline itself — signing is handled by the GitHub attestation
|
|
527
|
+
actions, and cosign is purely an auditor-side verification tool. Do not add a cosign installer to CI.
|
|
528
|
+
- Give the evidence bundle a **stable, versioned schema** — include `schema_version` in
|
|
529
|
+
`manifest.json` — so a downstream reader can parse releases from different points in time without
|
|
530
|
+
special-casing each one.
|
|
531
|
+
- Assume the bundle will be mirrored into a read-only auditor portal. That portal is a **projection**;
|
|
532
|
+
this repository's Releases and attestations remain the system of record, and the portal must be
|
|
533
|
+
fully rebuildable from them at any time. Never make the pipeline push findings into an external
|
|
534
|
+
database as their primary home, and never make a green portal a precondition for release.
|
|
535
|
+
|
|
536
|
+
## Phase 5 — Governance as code
|
|
537
|
+
|
|
538
|
+
Commit each ruleset as JSON under `governance/rulesets/`, each the exact payload accepted by
|
|
539
|
+
`POST /repos/{owner}/{repo}/rulesets`. These files are the reviewable source of truth.
|
|
540
|
+
|
|
541
|
+
1. **Protected branches** — `develop`, `main`, `release/**`, `hotfix/**`: `bypass_actors: []`, deletion
|
|
542
|
+
and non-fast-forward blocked, required status checks with `strict_required_status_checks_policy: true`
|
|
543
|
+
and `do_not_enforce_on_create: true`, and a pull request rule configured for a single maintainer —
|
|
544
|
+
`required_approving_review_count: 0`, `require_code_owner_review: false`,
|
|
545
|
+
`require_last_push_approval: false`, `dismiss_stale_reviews_on_push: true`. The pull request
|
|
546
|
+
requirement exists to force the checks to run, **not** to obtain anyone's approval. Do not raise the
|
|
547
|
+
approval count.
|
|
548
|
+
2. **Main release gate** — a `main`-only ruleset requiring the staging qualification check, kept
|
|
549
|
+
separate so it is not demanded of `develop` or the release branches.
|
|
550
|
+
3. **Release tag creation** — `refs/tags/v*`, creation restricted, with the **GitHub Actions app** as
|
|
551
|
+
the only bypass actor.
|
|
552
|
+
4. **Release tag immutability** — `refs/tags/v*`, deletion and non-fast-forward blocked, no bypass
|
|
553
|
+
actors.
|
|
554
|
+
|
|
555
|
+
Add an idempotent script that applies these via `gh api`, resolving actor IDs at run time rather than
|
|
556
|
+
hard-coding them. Running it twice must be a no-op.
|
|
557
|
+
|
|
558
|
+
**Fail-closed sequencing.** Required status checks must be real job names you have already observed run
|
|
559
|
+
green. Author the workflows, merge them, confirm the exact context strings from
|
|
560
|
+
`gh api repos/{owner}/{repo}/commits/{sha}/check-runs --jq '.check_runs[].name'`, and only then install
|
|
561
|
+
the rulesets. Requiring a context that has never appeared blocks every merge including the fix — and
|
|
562
|
+
with no reviewer and no bypass actors, there is no manual escape. Commit the observed contexts as
|
|
563
|
+
evidence.
|
|
564
|
+
|
|
565
|
+
A required check that is skipped stays pending forever and deadlocks the ruleset. Every required job
|
|
566
|
+
must reach a terminal conclusion on every triggering event, including a deliberate successful
|
|
567
|
+
"nothing to do" path. **If the self-hosted runner group is unavailable, DAST must fail loudly with a
|
|
568
|
+
clear message rather than hang pending.**
|
|
569
|
+
|
|
570
|
+
**No required check may fail open.** A check that reports success when it could not actually do its
|
|
571
|
+
job is worse than no check at all, because it manufactures false assurance and nothing ever looks
|
|
572
|
+
wrong. Audit every required check against these patterns and fix any you find:
|
|
573
|
+
|
|
574
|
+
- `continue-on-error: true` on a job or step that a required check depends on.
|
|
575
|
+
- `|| true`, `|| echo`, or a trailing `exit 0` on a command whose failure is the thing being detected.
|
|
576
|
+
These are legitimate for diagnostics and cleanup; they are never acceptable on a validating command.
|
|
577
|
+
- Missing `set -euo pipefail` in a validation script, or a pipeline whose real exit status is masked
|
|
578
|
+
by a later command in the pipe.
|
|
579
|
+
- **Vacuous passes** — the most common and least visible case. A validation that iterates over files
|
|
580
|
+
and finds none, a `grep` that matches nothing, or a check guarded by `if [ -f ... ]` that silently
|
|
581
|
+
skips when the input is absent, all report success while having verified nothing. Every such check
|
|
582
|
+
must assert that it actually found something to inspect, and fail if its input set is unexpectedly
|
|
583
|
+
empty.
|
|
584
|
+
- An aggregator job using `if: always()` that tests only for `failure`. A dependency that is
|
|
585
|
+
`skipped` or `cancelled` is not `success` — compare explicitly against `success` for every needed
|
|
586
|
+
job, never against `failure`.
|
|
587
|
+
|
|
588
|
+
**Prove each check fails.** Observing a check run green establishes only that it can pass; it does not
|
|
589
|
+
establish that it can fail. Before requiring a check, run it once against a deliberate violation and
|
|
590
|
+
confirm it goes red — a malformed branch name, an unpinned action, a known-vulnerable dependency, a
|
|
591
|
+
planted dummy secret. Record that negative result alongside the observed contexts. A check that has
|
|
592
|
+
only ever been seen passing has never been shown to work at all.
|
|
593
|
+
|
|
594
|
+
Treat a discovered fail-open as a real defect even when nothing is currently broken, because by
|
|
595
|
+
definition it produces no symptom until the moment it matters. If changing `.github/workflows/` is
|
|
596
|
+
itself gated and you cannot land the fix in this session, **file a GitHub issue describing the exact
|
|
597
|
+
defect, the affected checks, and the reproduction — do not silently leave it undocumented.**
|
|
598
|
+
|
|
599
|
+
## Phase 6 — Documentation
|
|
600
|
+
|
|
601
|
+
- `docs/operations/github-governance.md` — the exact required contexts, which ruleset requires what,
|
|
602
|
+
and why. Keep it in step with the JSON.
|
|
603
|
+
- `CONTRIBUTING.md` — branching model, branch naming, commit and PR conventions, how to cut a release.
|
|
604
|
+
- `docs/security/scanning.md` — the tool set, the stage mapping, and the suppression policy.
|
|
605
|
+
- `docs/operations/alerting.md` — every alert rule, its severity, its threshold and the reasoning
|
|
606
|
+
behind it, the Slack routing per severity, and how to verify the alert path end to end.
|
|
607
|
+
- `docs/operations/service-health.md` — every component, its health endpoint, what its deep check
|
|
608
|
+
actually exercises, its dependencies, and the recovery order implied by them. This is read during
|
|
609
|
+
an incident, so write it to be scanned under pressure, not studied.
|
|
610
|
+
- `docs/security/audit-evidence.md` — where evidence lives, what the evidence bundle contains, the
|
|
611
|
+
**expected OIDC issuer and certificate identity pattern**, and the exact commands an auditor runs to
|
|
612
|
+
retrieve a release's bundle and verify its attestation independently, without needing me. Give both
|
|
613
|
+
the `gh attestation verify` and the `cosign verify-blob-attestation` paths, and state explicitly
|
|
614
|
+
that verification which does not pin the expected identity proves only that *somebody* signed
|
|
615
|
+
*something* — a valid signature from an unrelated repository would otherwise pass.
|
|
616
|
+
- `docs/ai-acceptable-use.md` — the AI acceptable use policy.
|
|
617
|
+
- `docs/runbooks/` — break-glass, production rollback, failed-release repair, and canary failure. The
|
|
618
|
+
rollback runbook must state that rollback is ungated and requires no approval, and must have been
|
|
619
|
+
rehearsed rather than merely written. The break-glass runbook matters most: with no reviewer and no
|
|
620
|
+
bypass actors, I need a written, logged way back in.
|
|
621
|
+
|
|
622
|
+
## Deliverables
|
|
623
|
+
|
|
624
|
+
1. Phase 0 classification and proposed plan — **stop and get my approval.**
|
|
625
|
+
2. A PR adding workflows, `governance/rulesets/*.json`, the apply script, docs and runbooks.
|
|
626
|
+
3. Evidence every required context ran green, with the exact context strings quoted, **and evidence
|
|
627
|
+
that each one goes red against a deliberate violation.**
|
|
628
|
+
4. Rulesets installed only after (3), then re-verified by reading the live rulesets back.
|
|
629
|
+
5. A summary of what is enforced, what is advisory, and what was skipped for this repo and why.
|
|
630
|
+
|
|
631
|
+
## Constraints
|
|
632
|
+
|
|
633
|
+
- Never weaken or delete an existing protection to make something pass. Raise it with me instead.
|
|
634
|
+
- Never force-push, delete or rewrite a protected branch or an existing tag.
|
|
635
|
+
- Never enable a ruleset whose required contexts have not been observed green.
|
|
636
|
+
- Never propose org-level rulesets, org-level required workflows, or org-wide App installations.
|
|
637
|
+
Everything is repository-scoped by design.
|
|
638
|
+
- Prefer fewer genuinely enforced controls over a large set that gets bypassed. Tell me honestly which
|
|
639
|
+
controls would be theatre in this repository.
|
|
640
|
+
|
|
641
|
+
## Liftoff activation protocol
|
|
642
|
+
|
|
643
|
+
This policy is a generated local handoff. Its presence does not mean that any
|
|
644
|
+
branch, check, ruleset, security feature, environment, deployment, monitor, or
|
|
645
|
+
alert is active.
|
|
646
|
+
|
|
647
|
+
### Prerequisites
|
|
648
|
+
|
|
649
|
+
Do not begin activation until the repository is committed, pushed, and
|
|
650
|
+
resolvable as a GitHub repository. If any prerequisite is missing, report it and
|
|
651
|
+
stop without mutation.
|
|
652
|
+
|
|
653
|
+
### Read-only Phase 0
|
|
654
|
+
|
|
655
|
+
Inspect and report all of the following with evidence before changing anything:
|
|
656
|
+
|
|
657
|
+
1. Repository owner/name, remote identity, visibility, default branch, current
|
|
658
|
+
local branch, working-tree state, and current `main` tip.
|
|
659
|
+
2. Artifact forms, languages, package managers, manifests, locks, build
|
|
660
|
+
commands, test commands, lint commands, generated environments, health
|
|
661
|
+
endpoints, and operations or infrastructure files.
|
|
662
|
+
3. Existing local and remote refs, GitFlow compatibility, workflows, exact job
|
|
663
|
+
and check-run names, rulesets, tags, releases, GitHub Environments, deployment
|
|
664
|
+
history, and rollback paths.
|
|
665
|
+
4. Secret Protection, Dependabot, Dependency Review, CodeQL, Copilot Autofix,
|
|
666
|
+
Checkov, Trivy, Grype, ZAP, attestations, SLSA, Scorecard, licenses, and every
|
|
667
|
+
required input or suppression policy.
|
|
668
|
+
5. GitHub Actions runner labels and whether a private-network runner can reach
|
|
669
|
+
Staging.
|
|
670
|
+
6. Live deployment mechanisms, parallel-version support, version-specific
|
|
671
|
+
origins, traffic volume, statistically valid canary capacity, and provider
|
|
672
|
+
status sources.
|
|
673
|
+
7. Monitoring signals, alert rules, severity routing, Slack delivery,
|
|
674
|
+
heartbeat coverage, alert-fire tests, dashboards, shallow/deep component
|
|
675
|
+
health, dependency graph, recovery order, deployed versions/digests, and DORA
|
|
676
|
+
event sources.
|
|
677
|
+
|
|
678
|
+
Report every gap, blocker, inapplicable control, and GitFlow-versus-continuous-
|
|
679
|
+
delivery conflict. Propose the current `main` SHA as the activation baseline for
|
|
680
|
+
an existing repository and provide an ordered implementation plan.
|
|
681
|
+
|
|
682
|
+
**STOP FOR EXPLICIT USER APPROVAL.** Phase 0 is read-only. Before approval, do
|
|
683
|
+
not write files, create a framework change, branch, commit, tag, release,
|
|
684
|
+
workflow, environment, ruleset, issue, deployment, cloud resource, monitor,
|
|
685
|
+
alert, or Slack route.
|
|
686
|
+
|
|
687
|
+
### Post-approval implementation
|
|
688
|
+
|
|
689
|
+
After approval, create a new governance change using the project's selected
|
|
690
|
+
OpenSpec or Spec Kit workflow. The generated Liftoff policy remains an input;
|
|
691
|
+
Liftoff does not own, name, restore, or recreate that active change.
|
|
692
|
+
|
|
693
|
+
Author workflows, exact ruleset payloads, runbooks, and documentation first.
|
|
694
|
+
Observe every proposed required context green on all applicable paths, then
|
|
695
|
+
prove that exact context deliberately red with a controlled violation. Treat
|
|
696
|
+
skipped or cancelled dependencies as not successful. Apply repository-scoped
|
|
697
|
+
rulesets idempotently last, then read the live rulesets and required contexts
|
|
698
|
+
back from GitHub.
|
|
699
|
+
|
|
700
|
+
Record the explicitly approved pre-governance `main` SHA in the user-owned
|
|
701
|
+
`governance/activation-baseline.json`. If `main` advanced since Phase 0, stop
|
|
702
|
+
and obtain a newly discovered and approved baseline. Apply release/tag anomaly
|
|
703
|
+
checks only to governed production commits after that SHA. Never invent a
|
|
704
|
+
historical release, move a tag, rewrite history, or treat the activation record
|
|
705
|
+
as Liftoff-owned.
|