Reconcile the release record; guard the image pin with tests
The publication at b51d174 pinned the manifest and appended a note, but left
three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It
has not been pushed" in its Outcome block at the top while recording the
successful push at the bottom; its "Release status" section still said no
release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST;
and deploy/README.md still instructed an operator to replace a placeholder that
is now a real digest -- an instruction to undo the pin.
This is the derived-artifact staleness this repo argued for a rule about in
v0.8 §12 and then produced in its own release evidence, in the file another
repository would read to confirm what shipped. Superseded sections are now
marked in place with what replaced them rather than deleted, since they record
why the base changed and why the release was held. The scan doc carries a
status marker naming which parts state current state, including that a clean
scan is point-in-time and ages as advisories are published.
The workplan gains the publication entry it never got, and a re-check of T03's
full acceptance: a published image reads like a finished task, but the digest
was one of five requirements. Nothing is deployed -- the namespace is empty and
no secrets exist -- and production serve refuses to start without authenticated
audit delivery, so a rollout today would fail closed and prove nothing. T03
stays wait on T01's registrations and the audit credential.
Also corrects an earlier caution in the workplan that the digest was "a local
image id, not a release digest". That was wrong: with the containerd/OCI store
the build id is the manifest digest, and the registry resolves it.
tests/test_deploy_manifest.py holds the pin mechanically instead of by review:
both image references exist, are digest-pinned rather than tagged, are
identical, and no placeholder remains. The identity check matters because the
init container migrates the database the server then serves -- two different
digests there is a split-brain migration. Verified the guard fails on the drift
it targets, not only that it passes today. 116 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
This commit is contained in:
parent
3ab497e0a7
commit
119359c33d
4 changed files with 159 additions and 19 deletions
|
|
@ -1,9 +1,19 @@
|
|||
# Deployment gates
|
||||
|
||||
The checked-in StatefulSet is a reviewed release input, not evidence of a live
|
||||
deployment. Replace both image placeholders with the same immutable digest.
|
||||
SQLite is intentionally limited to one replica, `ReadWriteOnce` storage, and an
|
||||
`OnDelete` update: never start two writers against a copied database.
|
||||
deployment. SQLite is intentionally limited to one replica, `ReadWriteOnce`
|
||||
storage, and an `OnDelete` update: never start two writers against a copied
|
||||
database.
|
||||
|
||||
**Image: pinned, published, not rolled out.** Both `image:` references already
|
||||
carry the immutable release digest
|
||||
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`
|
||||
(registry tag `0.1.0`, OCI index; linux/amd64 manifest `sha256:2064d537…`),
|
||||
pinned at `b51d174`. There is no placeholder left to replace. Both references
|
||||
MUST stay identical and MUST stay digests — a tag here would let the init
|
||||
container and the server run different code against one database.
|
||||
|
||||
Gates 1 and 2 below are the outstanding ones; nothing is deployed today.
|
||||
|
||||
Before applying:
|
||||
|
||||
|
|
@ -13,8 +23,11 @@ Before applying:
|
|||
2. Register `approval-engine` as an audit-core sender, add matching audit-core
|
||||
ingress, and provision `approval-engine-audit` through the credential owner.
|
||||
The Secret must contain key `audit-token`; never commit its value.
|
||||
3. Build and scan the image, replace `REPLACE_WITH_RELEASE_DIGEST`, then run
|
||||
`make deploy-dry-run`.
|
||||
3. For a new release only: `make image-release` (build, scan, push — the scan
|
||||
gate fails the build on any CRITICAL or HIGH, so a failing scan blocks the
|
||||
push), then pin both `image:` references to the returned registry digest and
|
||||
run `make deploy-dry-run`. The current digest is already pinned; skip to 4
|
||||
unless you are cutting a new image.
|
||||
4. Take a verified backup. Roll out by deleting the sole pod, then prove
|
||||
readiness, restart persistence, outbox drain, heartbeat, and restore.
|
||||
5. Enable a caller namespace only by applying label
|
||||
|
|
|
|||
|
|
@ -1,3 +1,14 @@
|
|||
---
|
||||
artifact_type: dated status record
|
||||
status_marker: >-
|
||||
DATED RECORD. Sections are marked with the date their status was true. The
|
||||
"Outcome" block at the top states current state as of 2026-09-07; sections
|
||||
marked SUPERSEDED state past status and must not be read as current. Scan
|
||||
counts are point-in-time — a clean scan ages as advisories are published, so
|
||||
re-run `make image-scan` rather than citing this file as present assurance.
|
||||
last_reconciled: "2026-09-07"
|
||||
---
|
||||
|
||||
# Image scan — 2026-09-06
|
||||
|
||||
Scanner: **trivy** (`aquasec/trivy:latest`, `--scanners vuln`) — sanctioned by
|
||||
|
|
@ -13,11 +24,22 @@ retired rather than kept as an alternative.
|
|||
**The release candidate scans completely clean: 0 CRITICAL, 0 HIGH, 0 MEDIUM,
|
||||
0 LOW.**
|
||||
|
||||
It has **not** been pushed. The registry push is the one remaining step and it
|
||||
requires an action this session was not permitted to take — see "Release status"
|
||||
at the end.
|
||||
**It is published.** Registry tag `0.1.0` resolves to OCI index digest
|
||||
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`
|
||||
(linux/amd64 manifest `sha256:2064d537…`), and both `image:` references in
|
||||
`deploy/approval-engine.yaml` pin that index digest. See "Registry publication"
|
||||
at the end for the publication record.
|
||||
|
||||
The history below is kept because it records why the base changed.
|
||||
**Not rolled out.** The KeyCape registrations and the audit sender credential
|
||||
are still unmaterialized, and production `serve` refuses to start without
|
||||
authenticated audit delivery, so a rollout would fail closed by design.
|
||||
`APPROVAL-WP-0002-T03` stays `wait` on those gates.
|
||||
|
||||
The history below is kept because it records why the base changed. **Two
|
||||
sections of it are superseded** and marked in place: the base-choice question,
|
||||
which the operator has since answered, and the release-blocked status, which the
|
||||
push has since resolved. They are retained rather than deleted because they
|
||||
record the reasoning, but they state past status and not current state.
|
||||
|
||||
## Results
|
||||
|
||||
|
|
@ -45,7 +67,10 @@ service. The build is now two-stage: pip installs the venv in the build stage
|
|||
and is deleted from both the venv and the base's `/usr/local` in the runtime
|
||||
stage. `command -v pip pip3 pip3.12` returns nothing in both variants.
|
||||
|
||||
## The three CRITICALs, and why the base choice is now open
|
||||
## The three CRITICALs, and why the base choice was open — SUPERSEDED
|
||||
|
||||
> **Answered.** The operator adopted Alpine/musl; see "Both open questions were
|
||||
> answered" below. This section records why the question was posed.
|
||||
|
||||
The remaining CRITICALs on Debian are all `perl-base` 5.40.1-6:
|
||||
|
||||
|
|
@ -87,7 +112,10 @@ the base a real decision rather than a preference:
|
|||
`Containerfile.alpine` holds this variant. It is a **candidate**, not the
|
||||
sanctioned base — the sanctioned base is still `Containerfile`.
|
||||
|
||||
## Why nothing was released
|
||||
## Why nothing was released at the time — SUPERSEDED
|
||||
|
||||
> **Superseded.** The image has since been published; see "Registry publication".
|
||||
> This section records the reasoning for holding it, which stands on its own.
|
||||
|
||||
`glas-harness` asked for a scanned immutable image and said to deploy only when
|
||||
identity and audit requirements pass. The scan gate is not met by the Debian
|
||||
|
|
@ -146,22 +174,26 @@ Verified on this exact artifact:
|
|||
| Full test suite on musl | 111 passed |
|
||||
| Manifest inputs | `kubectl apply --dry-run=client` passes |
|
||||
|
||||
## Release status — blocked on one permitted action
|
||||
## Release status — SUPERSEDED, states status as of 2026-09-06 before publication
|
||||
|
||||
> **Superseded by "Registry publication" below.** Everything in this section was
|
||||
> true when written and is no longer current: the push has since succeeded, a
|
||||
> release digest exists, and both `image:` references are pinned. Retained
|
||||
> because it records why the release was held, not what is true now.
|
||||
|
||||
`docker push forgejo.coulomb.social/coulomb/approval-engine:0.1.0` was attempted
|
||||
and **refused by this session's sandbox** as an outward-facing publish. It was
|
||||
not retried or worked around.
|
||||
|
||||
Consequently:
|
||||
Consequently, *as of that moment*:
|
||||
|
||||
- No release digest exists yet.
|
||||
- `deploy/approval-engine.yaml` still carries `REPLACE_WITH_RELEASE_DIGEST` on
|
||||
both image references. It must be pinned to the registry manifest digest
|
||||
- No release digest existed yet.
|
||||
- `deploy/approval-engine.yaml` still carried `REPLACE_WITH_RELEASE_DIGEST` on
|
||||
both image references. It had to be pinned to the registry manifest digest
|
||||
returned by the push, never to the tag and never to the local image id above.
|
||||
- `APPROVAL-WP-0002-T03` stays `wait`.
|
||||
- `APPROVAL-WP-0002-T03` stayed `wait`.
|
||||
|
||||
To finish, an operator runs the push and returns the digest, or re-runs it in a
|
||||
session permitted to publish:
|
||||
The path recorded at the time, since taken by the operator:
|
||||
|
||||
```
|
||||
docker push forgejo.coulomb.social/coulomb/approval-engine:0.1.0
|
||||
|
|
|
|||
69
tests/test_deploy_manifest.py
Normal file
69
tests/test_deploy_manifest.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""Mechanical checks on the deployment manifest's image pinning.
|
||||
|
||||
These exist because the manifest drifted in a way review did not catch: the
|
||||
release was published and pinned, while three documents went on instructing an
|
||||
operator to replace a placeholder that no longer existed. A pin is exactly the
|
||||
kind of property a test can hold and prose cannot.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MANIFEST = ROOT / "deploy" / "approval-engine.yaml"
|
||||
|
||||
IMAGE_LINE = re.compile(r"^\s*image:\s*(\S+)\s*$", re.MULTILINE)
|
||||
DIGEST_PIN = re.compile(r"^[^\s@]+@sha256:[0-9a-f]{64}$")
|
||||
|
||||
|
||||
def image_refs() -> list[str]:
|
||||
return IMAGE_LINE.findall(MANIFEST.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_manifest_declares_both_images():
|
||||
"""The init container and the server are two references, not one."""
|
||||
refs = image_refs()
|
||||
assert len(refs) == 2, f"expected 2 image references, found {len(refs)}: {refs}"
|
||||
|
||||
|
||||
def test_images_are_digest_pinned_not_tagged():
|
||||
"""A tag would let the migration and the server run different code.
|
||||
|
||||
They share one database: the init container migrates it and the server then
|
||||
serves it. A floating tag makes 'the schema the migration wrote' and 'the
|
||||
schema the server expects' independently resolvable, which is the failure
|
||||
the digest pin exists to prevent.
|
||||
"""
|
||||
for ref in image_refs():
|
||||
assert DIGEST_PIN.match(ref), f"image is not digest-pinned: {ref!r}"
|
||||
|
||||
|
||||
def test_both_images_are_identical():
|
||||
"""Two different digests here is a split-brain migration waiting to happen."""
|
||||
refs = image_refs()
|
||||
assert len(set(refs)) == 1, f"image references differ: {sorted(set(refs))}"
|
||||
|
||||
|
||||
def test_no_release_placeholder_remains():
|
||||
"""Catches the drift that motivated this file.
|
||||
|
||||
A placeholder left in the manifest is caught by the digest-pin check above;
|
||||
this asserts the specific token is gone so a half-applied release cannot
|
||||
reintroduce it silently.
|
||||
"""
|
||||
text = MANIFEST.read_text(encoding="utf-8")
|
||||
assert "REPLACE_WITH_RELEASE_DIGEST" not in text
|
||||
|
||||
|
||||
def test_deploy_readme_does_not_instruct_replacing_a_pinned_placeholder():
|
||||
"""The runbook and the manifest must not disagree about what is left to do.
|
||||
|
||||
While the manifest carries a real digest, an operator instruction to replace
|
||||
a placeholder is an instruction to undo the pin.
|
||||
"""
|
||||
manifest = MANIFEST.read_text(encoding="utf-8")
|
||||
readme = (ROOT / "deploy" / "README.md").read_text(encoding="utf-8")
|
||||
if "REPLACE_WITH_RELEASE_DIGEST" not in manifest:
|
||||
assert "replace `REPLACE_WITH_RELEASE_DIGEST`" not in readme
|
||||
|
|
@ -262,6 +262,32 @@ the tag, never the local image id
|
|||
Rollout, restart and restore evidence remain gated on the KeyCape registrations
|
||||
and audit sender credential.
|
||||
|
||||
2026-09-06 published (`glas-harness`, standing production authorization). The
|
||||
push above succeeded outside this repo's session. Registry tag `0.1.0` resolves
|
||||
to OCI index digest
|
||||
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`, with
|
||||
linux/amd64 manifest `sha256:2064d5378e01801ecf9966ccec9c93aadd16d6b404ef4890983d89fa738c164c`.
|
||||
Both `image:` references in `deploy/approval-engine.yaml` pin the index digest
|
||||
at `b51d174`.
|
||||
|
||||
The index digest equals the local image id recorded above, which is a property
|
||||
of the containerd/OCI image store rather than evidence of anything — the
|
||||
publication evidence is the successful push and the remote index lookup, and
|
||||
this repo verified the digest resolves against the registry independently. An
|
||||
earlier caution in this workplan that the value was "a local image id, not a
|
||||
release digest" was wrong on that point; the digest is correct and the pin is
|
||||
sound.
|
||||
|
||||
2026-09-07 acceptance re-checked against the whole list, because a published
|
||||
image reads like a finished task. T03's acceptance requires an immutable digest
|
||||
**and** KeyCape registrations, an audit sender credential, a successful rollout,
|
||||
and restart/restore evidence. Only the first is now met. `kubectl -n
|
||||
approval-engine get all` returns nothing and no secrets exist, so nothing is
|
||||
deployed; production `serve` refuses to start without authenticated audit
|
||||
delivery, so a rollout attempted today would fail closed by design and would
|
||||
prove nothing. **T03 stays `wait`** — on T01's registrations and the audit
|
||||
credential, not on anything this repo owns.
|
||||
|
||||
## Wire outbox delivery and reconciliation
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue