#203: pin all third-party deploy images by digest
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m32s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 8m22s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 56s
CD / Smoke tests against Test (push) Successful in 1m24s
CD / Promote to Int (push) Successful in 52s
CI / Lint, typecheck, test (push) Successful in 5m36s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m3s
CI / Import/export fidelity gate (push) Successful in 57s
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m32s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 8m22s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 56s
CD / Smoke tests against Test (push) Successful in 1m24s
CD / Promote to Int (push) Successful in 52s
CI / Lint, typecheck, test (push) Successful in 5m36s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m3s
CI / Import/export fidelity gate (push) Successful in 57s
The four third-party images in the deploy compose (postgres, pandoc, gotenberg — previously a floating MAJOR tag —, caddy) are now name:tag@sha256 pins; the tag stays for readability, the digest decides what runs. The pinned digests are exactly what the stages already run (verified against the live containers' RepoDigests on ONE), so the next recreation is byte-identical. A new early CI step fails on any third-party compose image without a digest; compose.dev.yml is a local convenience and deliberately exempt (its node helpers now follow the #236 pin). Update + rollout procedure in deploy/stages.md — CD does not sync stage composes, so the hand rollout to test/int/prod is part of this issue's definition of done. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
This commit is contained in:
parent
000d110727
commit
db4f517e44
@ -91,6 +91,22 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
exit "$bad"
|
exit "$bad"
|
||||||
|
|
||||||
|
# Third-party deploy images are pinned by digest (issue #203): every
|
||||||
|
# image in the deploy compose that is not one of our own
|
||||||
|
# (${IMAGE_PREFIX}…) must carry @sha256 — the tag stays for
|
||||||
|
# readability, the digest decides what runs. Update procedure:
|
||||||
|
# deploy/stages.md §Third-party image digests. compose.dev.yml is a
|
||||||
|
# local convenience, deliberately not held to this.
|
||||||
|
- name: Third-party compose images are digest-pinned
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
bad=$(grep -hE '^ *image: ' deploy/compose/docker-compose.yml | grep -v 'IMAGE_PREFIX' | grep -v '@sha256:' || true)
|
||||||
|
if [ -n "$bad" ]; then
|
||||||
|
echo "third-party image reference(s) without a digest:"
|
||||||
|
echo "$bad"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set up pnpm
|
- name: Set up pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
image: node:22.15-alpine
|
image: node:22.15.1-alpine
|
||||||
build: !reset null
|
build: !reset null
|
||||||
working_dir: /repo
|
working_dir: /repo
|
||||||
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/web dev -- --host 0.0.0.0"
|
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/web dev -- --host 0.0.0.0"
|
||||||
@ -33,7 +33,7 @@ services:
|
|||||||
depends_on: !reset []
|
depends_on: !reset []
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: node:22.15-alpine
|
image: node:22.15.1-alpine
|
||||||
build: !reset null
|
build: !reset null
|
||||||
working_dir: /repo
|
working_dir: /repo
|
||||||
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/shared build && pnpm --filter @dorfteich/api start:dev"
|
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/shared build && pnpm --filter @dorfteich/api start:dev"
|
||||||
@ -53,7 +53,7 @@ services:
|
|||||||
- pnpm-store:/root/.local/share/pnpm/store
|
- pnpm-store:/root/.local/share/pnpm/store
|
||||||
|
|
||||||
collab:
|
collab:
|
||||||
image: node:22.15-alpine
|
image: node:22.15.1-alpine
|
||||||
build: !reset null
|
build: !reset null
|
||||||
working_dir: /repo
|
working_dir: /repo
|
||||||
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/shared build && pnpm --filter @dorfteich/collab start:dev"
|
command: sh -c "npm i -g pnpm@11 && pnpm install && pnpm --filter @dorfteich/shared build && pnpm --filter @dorfteich/collab start:dev"
|
||||||
|
|||||||
@ -193,7 +193,7 @@ services:
|
|||||||
<<: *logging
|
<<: *logging
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:17.5-alpine
|
image: postgres:17.5-alpine@sha256:6567bca8d7bc8c82c5922425a0baee57be8402df92bae5eacad5f01ae9544daa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: dorfteich
|
POSTGRES_USER: dorfteich
|
||||||
@ -213,7 +213,7 @@ services:
|
|||||||
# on the internal network only — never exposed. Pinned image; the api reaches
|
# on the internal network only — never exposed. Pinned image; the api reaches
|
||||||
# it at http://pandoc:3030. `wget` ships in the (busybox-based) image.
|
# it at http://pandoc:3030. `wget` ships in the (busybox-based) image.
|
||||||
pandoc:
|
pandoc:
|
||||||
image: pandoc/core:3.6
|
image: pandoc/core:3.6@sha256:5b8a29d9b70d5d8ca766e5d1dcfc41916b23ab79276a80527be70516110f4c1e
|
||||||
command: ['server']
|
command: ['server']
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks: [internal]
|
networks: [internal]
|
||||||
@ -228,7 +228,7 @@ services:
|
|||||||
# on the internal network only — never exposed. Pinned image; the api reaches
|
# on the internal network only — never exposed. Pinned image; the api reaches
|
||||||
# it at http://gotenberg:3000 and posts export HTML to its Chromium route.
|
# it at http://gotenberg:3000 and posts export HTML to its Chromium route.
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:8
|
image: gotenberg/gotenberg:8@sha256:67097317623a503ba2a6a7e9ae8db6929a1f7e1bbd88077bacf2d325fbdab923
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks: [internal]
|
networks: [internal]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@ -244,7 +244,7 @@ services:
|
|||||||
# `localhost` default uses Caddy's internal CA — handy for smoke tests).
|
# `localhost` default uses Caddy's internal CA — handy for smoke tests).
|
||||||
# Instances behind an existing host proxy simply never enable the profile.
|
# Instances behind an existing host proxy simply never enable the profile.
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:2.10-alpine
|
image: caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d
|
||||||
profiles: [caddy]
|
profiles: [caddy]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@ -122,6 +122,35 @@ The pipeline pushes images to the Gitea container registry
|
|||||||
- On the host, `docker login gitea.101010.cloud` for the `deploy` user
|
- On the host, `docker login gitea.101010.cloud` for the `deploy` user
|
||||||
with a `read:package` token, so `compose pull` works.
|
with a `read:package` token, so `compose pull` works.
|
||||||
|
|
||||||
|
## 5a. Third-party image digests (issue #203, ADR 0024)
|
||||||
|
|
||||||
|
Every third-party image in `deploy/compose/docker-compose.yml` is pinned
|
||||||
|
as `name:tag@sha256:…` — the tag stays for readability, the digest
|
||||||
|
decides what runs, so the deployed artefact is exactly the reviewed one.
|
||||||
|
An early CI step fails on any third-party `image:` reference without a
|
||||||
|
digest. Our own images are pinned per release by the deploy pipeline
|
||||||
|
(`TAG` in the stage `.env`); `compose.dev.yml` is a local convenience and
|
||||||
|
deliberately not digest-pinned.
|
||||||
|
|
||||||
|
**Updating a digest** (e.g. to take a rebased base image or a new tag):
|
||||||
|
|
||||||
|
1. Resolve the new digest — this prints the manifest-list digest every
|
||||||
|
platform pulls:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker buildx imagetools inspect <name:tag> # → Digest: sha256:…
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update the reference in `deploy/compose/docker-compose.yml` to
|
||||||
|
`<name:tag>@sha256:…` and let CI confirm.
|
||||||
|
3. **Roll out by hand**: CD does NOT sync stage composes — apply the same
|
||||||
|
change to `/srv/DOCKER/dorfteich-{test,int,prod}/docker-compose.yml`
|
||||||
|
on ONE. The next `compose pull && up -d` (any CD run for test/int, the
|
||||||
|
next release deploy for prod) recreates the containers from the pinned
|
||||||
|
digest.
|
||||||
|
4. Verify after rollout: `docker inspect --format '{{.Image}}' <container>`
|
||||||
|
must print the pinned digest (or check `RepoDigests` on the image).
|
||||||
|
|
||||||
## 6. Verification checklist
|
## 6. Verification checklist
|
||||||
|
|
||||||
- [ ] `https://test.dorfteich.cloud/healthz` → `ok`
|
- [ ] `https://test.dorfteich.cloud/healthz` → `ok`
|
||||||
|
|||||||
@ -69,7 +69,7 @@ Digest-Pinning (#203) läuft vorgezogen in `M25`_
|
|||||||
Hochgezogen, weil das eine Frage im **ersten** Behördengespräch ist. „Sollte
|
Hochgezogen, weil das eine Frage im **ersten** Behördengespräch ist. „Sollte
|
||||||
gehen" ist dort eine schlechtere Antwort als „getestet, hier ist die Anleitung".
|
gehen" ist dort eine schlechtere Antwort als „getestet, hier ist die Anleitung".
|
||||||
|
|
||||||
- [ ] Alle Images auf Digest pinnen (schließt den `gotenberg:8`-Punkt ein) · 1 AT · #203
|
- [x] Alle Images auf Digest pinnen (schließt den `gotenberg:8`-Punkt ein) · 1 AT · #203
|
||||||
- [ ] Mirror-Verfahren in interne Registry dokumentieren · 1 AT · #218
|
- [ ] Mirror-Verfahren in interne Registry dokumentieren · 1 AT · #218
|
||||||
- [ ] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
|
- [ ] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
|
||||||
Prebuilt-Images) · 2–3 AT · #219
|
Prebuilt-Images) · 2–3 AT · #219
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user