Delivery
Delivery is the everyday path from “I changed something” to “the system is safely running it.” In ATO, that path matters because each step creates evidence: the PR explains intent, review proves separation of duties, checks prove verification, release tags identify the artifact, environment approvals prove authorization, and ArgoCD records what actually reached the cluster.
If you are new to this workflow, read this page as the default route. Do not start with Kubernetes, ArgoCD, or a production dashboard. Start with the branch, PR, review, checks, release, and evidence trail.
Delivery model
flowchart LR subgraph HumanWork["Human work"] OpenPR["Open PR"] --> ReviewMerge["Review and merge"] ReviewMerge --> Promote["Promote to production"] end
subgraph AutoWork["Automation"] Checks["CI checks and build"] TestSync["Test auto-sync"] end
OpenPR -->|on push| Checks Checks -->|if passing| ReviewMerge ReviewMerge -->|on merge| TestSync TestSync -->|if healthy| Promote
style Checks fill:#fef3c7,stroke:#b45309,color:#451a03 style Promote fill:#dcfce7,stroke:#16a34a,color:#14532dmain is the only long-lived code branch. We do not use staging or prod branches because environments are promotion targets, not alternate source histories. The live system follows Git through ArgoCD; if a runtime change is not represented in Git, it is drift to remove or reconcile.
The simple rule is this: a safe change should be explainable from Git history alone. You should be able to answer who changed it, who reviewed it, which checks passed, which image was built, who promoted it, and what ArgoCD synced without opening a shell on a production node.
Branch names
Use prefix/short-kebab-slug. The prefix tells reviewers what kind of risk to expect before they open the diff.
| Prefix | Use for | Example |
|---|---|---|
feat/ | New user-visible behavior | feat/language-selector |
fix/ | Bug fixes and remediation | fix/duplicate-submission |
security/ | Auth, secrets, hardening, or vulnerability fixes | security/rotate-signing-key |
infra/ | Helm, ArgoCD, Kubernetes, cloud, or runners | infra/preview-namespaces |
ci/ | GitHub Actions, checks, or release automation | ci/add-commitlint-check |
build/ | Build system and packaging | build/container-cache |
docs/ | Documentation-only changes | docs/ato-delivery |
test/ | Test-only changes | test/add-profile-regression |
refactor/ | Behavior-preserving structure changes | refactor/form-state |
perf/ | Performance improvements | perf/query-cache |
style/ | Formatting and style-only changes | style/lint-cleanup |
revert/ | Reverting a prior change | revert/bad-cache-change |
chore/ | Routine maintenance | chore/update-dependencies |
What to notice: the branch name is not decoration. It helps route review, automate governance, and search audit history later.
Emergency work still uses fix/ or revert/ plus an incident reference in the PR. Do not use hotfix/ or emergency/; those names imply a second path around the controls.
Conventional commits and release impact
Conventional Commits are the grammar for PR titles and squash commits. They are not just style preferences; release automation reads them to decide whether a release is a patch, minor, major, or non-release change.
Use this shape:
type(optional-scope): short imperative subject
optional body that explains why
optional footer with refs, incident ids, or breaking-change notesThe PR title becomes the squash commit on main, so write the title as the final changelog line. It should describe the result, not the task you performed.
| Type | Use for | Release impact |
|---|---|---|
feat | New user-visible behavior | Minor |
fix | Bug fix | Patch |
security | Security hardening or vulnerability remediation | Patch |
perf | Performance improvement | Patch |
feat!, fix!, security! | Backward-incompatible change | Major |
docs, test, ci, style, chore, build, infra, refactor | Non-product or internal work | None by itself |
revert | Reverse a previous change | Matches the reverted impact |
Good one-line examples:
feat(profile): add language selectorfix(api): reject duplicate permit submissionssecurity(auth): rotate session signing keyinfra(argocd): add preview namespace controlsdocs(ato): clarify production promotion evidenceUse a body when the reviewer needs context that the title cannot hold:
feat(profile): add language selector
Allow residents to choose a portal language from the header.Default remains English for compatibility with existing bookmarks.
Refs: ADR-070Closes: PROJ-2211Use ! or a BREAKING CHANGE: footer when callers, data, APIs, or operational behavior must change:
feat(auth)!: migrate login flow to OIDC bearer assertions
The previous cookie-based session format is removed.Clients must use the token exchange endpoint before this release.
BREAKING CHANGE: API session token exchange now requires the target client id.Refs: SEC-4412Avoid vague titles like updates, fix stuff, work in progress, or changes from review. Those titles erase release meaning and make incident review harder.
Pull requests
A PR is the controlled gate between work and main. The reviewer should understand what changed, why it changed, how to verify it, and what could go wrong before reading every file.
Every PR should include:
- what changed
- why the change is needed
- how it was verified
- screenshots, preview links, or logs when behavior is visible
- risk, rollout, and rollback notes when the change can affect users
- ticket, advisory, incident, or ADR references when they exist
Use this body shape when you are unsure what to write:
## What changed
- Added the language selector to the profile page.- Stored the selected language in the existing profile settings record.
## Why
Residents need to choose a preferred language before the translated notification work ships.
## Verification
- Ran the focused unit tests for profile settings.- Checked the profile page in the preview environment.- Confirmed the default remains English for existing users.
## Risk and rollout
- Feature flag stays off by default.- Rollout requires Product approval before exposure.- Safe recovery is turning `profile.language-selector` off.The PR body is not busywork. It is the handoff contract for review, future maintenance, and audit evidence.
Review expectations
Review protects main. It is where we confirm the change is correct, secure, reversible, and ready to merge.
Every PR needs:
- focused scope with no unrelated cleanup
- automated checks, tests, and scans where relevant
- at least one approving non-author review
- resolved blocking comments before merge
- squash merge only
Reviewers check the highest-risk questions first:
| Question | Why it matters |
|---|---|
| Does the PR do exactly what it says? | Prevents hidden scope and surprise behavior |
| Are the tests and checks appropriate for the risk? | Prevents unverified production changes |
| Are auth, secrets, data, and input boundaries safe? | Prevents security regressions |
| Is the change reversible or safely recoverable? | Prevents incidents from becoming manual repair work |
| Are rollout notes clear? | Prevents production promoters from guessing |
Review is also the separation-of-duties evidence. The author, reviewer, and production promoter must be distinct actors when the change moves toward production.
Release and promotion
The release pipeline builds once and promotes the same image forward. Suffixes communicate trust stage; bytes remain the same.
| Stage | Trigger | Output | Human decision |
|---|---|---|---|
| Test | Merge to main | Immutable image and beta tag | No promotion decision; checks must pass |
| Staging | Manual promotion | Same image pinned as release candidate | Decide whether the change is ready for production-like validation |
| Production | Protected production approval | Same image pinned as clean release tag | Decide whether the release is ready for public traffic |
The important detail is “same image.” Do not rebuild for each environment. If test, staging, and production use different images, you no longer know whether production is running what was verified earlier.
Versioning is calculated from the commit types since the last release.
| Commit mix since last tag | Next bump |
|---|---|
Any breaking change (! or BREAKING CHANGE) | Major |
Any feat and no breaking change | Minor |
Only fix, security, or perf | Patch |
Only infra, docs, chore, ci, test, style, build, or refactor | No release version bump |
Production movement requires an explicit approval action. No production change should depend on a person applying Kubernetes resources by hand.
Feature flags
Feature flags separate deployment from launch. Deploying code means the code is present in the environment. Launching behavior means users can experience it.
A flag is useful when code is complete enough to merge but exposure needs timing, approval, cohort rollout, or fast disablement. A flag is not a hiding place for broken code.
| Use a flag when | Do not use a flag when |
|---|---|
| Complete behavior needs agency sign-off before exposure | The code fails checks |
| Gradual rollout is needed | Authorization or tenant isolation is missing |
| A fast disable path is useful | The value is permanent configuration |
| Experiment metrics are defined | Nobody owns cleanup |
Every flag needs:
- a safe default
- an owner
- tests for on and off paths
- a rollout approval path
- a metric or signal that says rollout is healthy
- a cleanup issue and removal date
The standard implementation is GrowthBook through @dmwd-io/feature-flags. Do not create a custom flag provider unless a current, concrete requirement justifies it.
Enforcement
GitHub rulesets, central governance workflows, protected environments, and ArgoCD enforce the delivery path.
| Enforcement point | What it prevents |
|---|---|
branch-protections | Direct default-branch writes, branch deletion, force-pushes, and non-linear history |
code-review | Unreviewed or stale-approved changes reaching main |
| Conventional branch and commit rules | Ambiguous release metadata and weak audit search |
release-tags | Malformed tags driving release automation |
| Production environment protection | One-person or branch-based production deployment |
| ArgoCD reconciliation | Manual cluster state drifting away from Git |
If an emergency requires a bypass, the bypass must name the incident or ticket, the approver, the reason, and the follow-up review. The bypass is evidence too.
ATO evidence
Delivery evidence should be collected as work happens, not reconstructed later. For each release, retain the PR, reviewer approval, required checks, security scan status, workflow run, approving actor, environment, image digest, Git SHA, ArgoCD sync result, release record, and timestamp.
| Evidence | Supports |
|---|---|
| Non-author PR approval and resolved review threads | AC-5, CM-3, SA-10 |
| Required checks, tests, and security scans | SA-11, SI-2 |
| Workflow run and production approval | CM-3, CM-5, AU-2 |
| Same image digest across environments | CM-4 |
| ArgoCD sync and deployment event history | AU-12, CM-3 |
| Feature flag approval and cleanup record | CM-4, CM-5, IR-4 |
First-change checklist
Use this when you are about to open a PR:
- branch name uses the right prefix
- PR title is a valid Conventional Commit
- PR body explains what changed, why, verification, and risk
- checks that match the risk have run
- secrets are referenced, not committed
- feature flags have owner, safe default, and cleanup plan
- reviewer is not the author
- production promotion notes are clear if the change can ship