Publish versioned Docker images to Gitea after container-network CI checks
All checks were successful
check / verify (push) Successful in 2m58s
check / publish (push) Successful in 2m51s

This commit is contained in:
Weapie 2026-10-07 10:44:34 +02:00
parent 007802d56e
commit 2bf0a0dd4c
7 changed files with 180 additions and 5 deletions

View File

@ -3,6 +3,7 @@ on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
container: node:22-bookworm
services:
postgres:
image: postgres:16-alpine
@ -10,10 +11,17 @@ jobs:
POSTGRES_USER: review
POSTGRES_PASSWORD: review-local-only
POSTGRES_DB: taskmanager_review
ports: ["55432:5432"]
options: --health-cmd "pg_isready -U review" --health-interval 5s --health-retries 10
postgres-upgrade:
image: postgres:16-alpine
env:
POSTGRES_USER: review
POSTGRES_PASSWORD: review-local-only
POSTGRES_DB: taskmanager_upgrade_test
options: --health-cmd "pg_isready -U review" --health-interval 5s --health-retries 10
env:
DATABASE_URL: postgresql://review:review-local-only@localhost:55432/taskmanager_review
DATABASE_URL: postgresql://review:review-local-only@postgres:5432/taskmanager_review
NO_PROXY: localhost,127.0.0.1,postgres,postgres-upgrade
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_SECRET: CI-only-not-a-production-secret-832910522
ALLOW_LOCAL_HTTP: "true"
@ -33,10 +41,72 @@ jobs:
- run: npm run test:integration
- name: Verify original-schema upgrade
run: |
docker exec ${{ job.services.postgres.id }} createdb -U review taskmanager_upgrade_test
npx tsx --test tests/upgrade.test.ts
env:
DATABASE_URL: postgresql://review:review-local-only@localhost:55432/taskmanager_upgrade_test
DATABASE_URL: postgresql://review:review-local-only@postgres-upgrade:5432/taskmanager_upgrade_test
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: npx playwright test
publish:
needs: verify
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Validate release version
id: release
env:
RELEASE_REF: ${{ github.ref }}
run: |
node scripts/release-version.mjs >> "$GITHUB_OUTPUT"
- uses: docker/login-action@v3
with:
registry: git.jfritzsche.de
username: ${{ secrets.REGISTRY_USERNAME || github.repository_owner }}
password: ${{ secrets.REGISTRY_TOKEN || secrets.GITEA_TOKEN }}
- uses: docker/setup-buildx-action@v3
- name: Refuse to overwrite an existing release
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
for image in taskmanager taskmanager-operations; do
if docker buildx imagetools inspect "git.jfritzsche.de/jf/$image:$VERSION" >/dev/null 2>&1; then
echo "Release $image:$VERSION already exists; use a new version."
exit 1
fi
done
- uses: docker/build-push-action@v6
with:
context: .
target: operations
platforms: linux/amd64
push: true
tags: git.jfritzsche.de/jf/taskmanager-operations:${{ steps.release.outputs.version }}
labels: |
org.opencontainers.image.source=https://git.jfritzsche.de/jf/taskmanager
org.opencontainers.image.version=${{ steps.release.outputs.version }}
org.opencontainers.image.revision=${{ github.sha }}
- uses: docker/build-push-action@v6
with:
context: .
target: runner
platforms: linux/amd64
push: true
tags: git.jfritzsche.de/jf/taskmanager:${{ steps.release.outputs.version }}
labels: |
org.opencontainers.image.source=https://git.jfritzsche.de/jf/taskmanager
org.opencontainers.image.version=${{ steps.release.outputs.version }}
org.opencontainers.image.revision=${{ github.sha }}
- name: Promote both published images to latest
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
set -eu
for image in taskmanager-operations taskmanager; do
docker buildx imagetools create --tag "git.jfritzsche.de/jf/$image:latest" "git.jfritzsche.de/jf/$image:$VERSION"
docker buildx imagetools inspect "git.jfritzsche.de/jf/$image:$VERSION"
docker buildx imagetools inspect "git.jfritzsche.de/jf/$image:latest"
done

View File

@ -6,6 +6,7 @@ Aufgabenverwaltung mit Next.js, PostgreSQL, privaten Anhängen und RBAC aus Benu
## Inhalt
- [Fertige Docker-Images, Versionen und Registry-Deployment](REGISTRY.md)
- [Bestehende Installation übernehmen](UPGRADE.md)
- [Voraussetzungen](#voraussetzungen)
- [Neuinstallation](#neuinstallation)

71
REGISTRY.md Normal file
View File

@ -0,0 +1,71 @@
# Docker-Registry und Releases
Registry: `git.jfritzsche.de`, Eigentümer: `jf`.
| Verwendung | Image | Tags eines Releases `v1.0.0` |
| --- | --- | --- |
| Webanwendung | `git.jfritzsche.de/jf/taskmanager` | `1.0.0`, `latest` |
| Datenmigration und Worker | `git.jfritzsche.de/jf/taskmanager-operations` | `1.0.0`, `latest` |
Beide Images sind erforderlich. Das kleine Web-Image enthält absichtlich keine Migrationswerkzeuge. Veröffentlichte Architektur: `linux/amd64`. ARM64 ist derzeit kein veröffentlichtes Ziel.
## Automatische Veröffentlichung
`.github/workflows/check.yml` wird vom vorhandenen Gitea-Runner verarbeitet. Bei Push und Pull Request laufen Typecheck, Lint, Runtime-Audit, Sicherheits-/Integrationstests, die Migration vom Altschema, Produktionsbuild und Browsertests. Prüfjob und PostgreSQL-Services teilen ein Container-Netz; Verbindung über `postgres:5432` statt Container-Loopback. Die Upgrade-Probe verwendet einen getrennten Datenbank-Service, ohne Docker-Zugriff aus dem Prüfjob.
Nur ein gepushter stabiler Git-Tag `vX.Y.Z`, der exakt zur Version in `package.json` passt, startet nach erfolgreichen Prüfungen den Veröffentlichungsjob. Branches, Pull Requests und Vorabversionen aktualisieren `latest` nicht. Bereits vorhandene Versions-Tags werden vor dem Build geprüft und nicht bewusst überschrieben. Veröffentlichungen nacheinander ausführen; alte Release-Tags nicht erneut veröffentlichen. `latest` bezeichnet das zuletzt erfolgreich veröffentlichte stabile Release, keinen Entwicklungsbranch.
Zuerst werden beide Versionsimages veröffentlicht. Erst wenn beide Pushes erfolgreich waren, werden die `latest`-Tags gesetzt. Registry-Updates zweier Images sind nicht atomar; bei einem Abbruch während dieses letzten Schritts können die Aliase kurz unterschiedliche Versionen bezeichnen. Produktion deshalb mit gleicher expliziter Versionsnummer für alle drei Dienste betreiben.
### Registry-Anmeldung für Actions
Der Veröffentlichungsjob braucht einen Runner mit Docker/Buildx und HTTPS-Zugriff auf die Registry. Für Gitea 1.22.3 einen eigenen Access-Token des Benutzers `jf` mit **`write:package`** verwenden und im Repository unter **Einstellungen → Actions → Secrets** speichern:
- `REGISTRY_TOKEN`: eingeschränkter Paket-Token, kein Administratorpasswort.
- `REGISTRY_USERNAME`: optional, Standard ist der Repository-Eigentümer `jf`.
Ein bereits vorhandenes Secret bleibt unverändert. Der Workflow kann alternativ den eingebauten `GITEA_TOKEN` verwenden, sofern die eingesetzte Gitea-Version dessen Paketveröffentlichung unterstützt. `permissions: packages: write` allein rüstet diese Fähigkeit älterer Server nicht nach. Secrets nie in Git, Build-Argumenten oder Logs speichern. Das operations-Image erhält durch `.dockerignore` weder lokale `.env` noch Backups oder Uploads.
Gitea-Pakete gehören einem Benutzer/einer Organisation. Sichtbarkeit nach dem ersten Publish prüfen und Pakete bei Bedarf mit `jf/taskmanager` verknüpfen. Private Images erfordern beim Deployment einen separaten Token mit `read:package`. Dokumentation: [Gitea Container Registry](https://docs.gitea.com/usage/packages/container/).
### Neues Release erstellen
```bash
# Geprüften Release-Stand auschecken, keine uncommitteten Änderungen.
npm version 1.0.1 --no-git-tag-version
git add package.json package-lock.json
git commit -m "Release 1.0.1"
git push origin HEAD
git tag -a v1.0.1 -m "Release 1.0.1"
git push origin v1.0.1
```
Workflow-Erfolg und beide Registry-Images prüfen. Tags nicht verschieben. Bei teilweise veröffentlichtem Release die fehlenden Schritte gezielt reparieren oder eine neue Version verwenden; kein Force-Push eines anderen Images auf eine bestehende Version.
## Deployment ohne lokalen Image-Build
Docker Compose mindestens 2.24.4 wegen `!reset`. Checkout/Compose-Dateien müssen zum Image-Release passen. Die Upgrade- und Backup-Anleitung gilt unverändert: bei vorhandenen Daten zuerst [UPGRADE.md](UPGRADE.md), insbesondere keine Neuinitialisierung.
```bash
docker login git.jfritzsche.de --username jf
# Passwort-Prompt: Token mit read:package, nicht im Befehl ausschreiben.
export TASKMANAGER_VERSION=1.0.0
docker compose -p taskmanager -f docker-compose.yml -f docker-compose.registry.yml pull
docker compose -p taskmanager -f docker-compose.yml -f docker-compose.registry.yml up -d --wait db
docker compose -p taskmanager -f docker-compose.yml -f docker-compose.registry.yml run --rm migrate
# Nur leere Neuinstallation: einmaliges Bootstrap nach README.
docker compose -p taskmanager -f docker-compose.yml -f docker-compose.registry.yml up -d --wait app worker
```
`docker-compose.registry.yml` ersetzt alle Build-Definitionen durch Registry-Images. Volumes, private Anhänge, Healthchecks, Sicherheitsoptionen und Migrationsreihenfolge aus dem Basis-Compose bleiben erhalten. Den tatsächlichen Projektnamen verwenden; ein anderer Name kann leere neue Volumes erzeugen. Auch bei Backup-/Restore-Befehlen dieselben Compose-Dateien und denselben Projektnamen verwenden.
Zum Nachschlagen verfügbar, für geplante Upgrades aber die konkrete Version bevorzugen:
```bash
docker pull git.jfritzsche.de/jf/taskmanager:latest
docker pull git.jfritzsche.de/jf/taskmanager-operations:latest
docker buildx imagetools inspect git.jfritzsche.de/jf/taskmanager:1.0.0
docker buildx imagetools inspect git.jfritzsche.de/jf/taskmanager-operations:1.0.0
```
Kein automatischer Austausch der laufenden Produktivinstallation: Veröffentlichung und produktives Upgrade sind getrennte Schritte. Ein älteres Image allein macht Datenbankmigrationen nicht rückgängig.

View File

@ -0,0 +1,12 @@
# Requires Docker Compose >= 2.24.4. Use together with docker-compose.yml.
# Pin the SAME release for app, migration and worker; do not mix latest and versions.
services:
app:
build: !reset null
image: git.jfritzsche.de/jf/taskmanager:${TASKMANAGER_VERSION:?Set TASKMANAGER_VERSION}
migrate:
build: !reset null
image: git.jfritzsche.de/jf/taskmanager-operations:${TASKMANAGER_VERSION:?Set TASKMANAGER_VERSION}
worker:
build: !reset null
image: git.jfritzsche.de/jf/taskmanager-operations:${TASKMANAGER_VERSION:?Set TASKMANAGER_VERSION}

View File

@ -12,7 +12,7 @@
"prisma:migrate": "prisma migrate deploy",
"prisma:seed": "tsx prisma/seed.ts",
"typecheck": "tsc --noEmit",
"test": "tsx --test tests/security.test.ts",
"test": "tsx --test tests/security.test.ts tests/release.test.mjs",
"worker": "tsx scripts/worker.ts",
"test:integration": "tsx --test tests/worker.test.ts tests/upload-migration.test.ts"
},

View File

@ -0,0 +1,7 @@
import { readFileSync } from "node:fs";
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
const ref = process.env.RELEASE_REF;
if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version) || ref !== `refs/tags/v${version}`) {
throw new Error("Release tag must be vX.Y.Z and match the stable version in package.json");
}
console.log(`version=${version}`);

14
tests/release.test.mjs Normal file
View File

@ -0,0 +1,14 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
test("only the exact stable package version can be published", () => {
for (const ref of [`refs/tags/v${version}`, "refs/heads/main", "refs/tags/v99.99.99", `refs/tags/v${version}-rc.1`, "refs/tags/v1.0.0;echo bad"]) {
const result = spawnSync(process.execPath, ["scripts/release-version.mjs"], { env: { ...process.env, RELEASE_REF: ref }, encoding: "utf8" });
if (ref === `refs/tags/v${version}`) {
assert.equal(result.status, 0);
assert.equal(result.stdout.trim(), `version=${version}`);
} else assert.notEqual(result.status, 0, ref);
}
});