Publish versioned Docker images to Gitea after container-network CI checks
This commit is contained in:
parent
007802d56e
commit
2bf0a0dd4c
78
.github/workflows/check.yml
vendored
78
.github/workflows/check.yml
vendored
@ -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
|
||||
|
||||
@ -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
71
REGISTRY.md
Normal 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.
|
||||
12
docker-compose.registry.yml
Normal file
12
docker-compose.registry.yml
Normal 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}
|
||||
@ -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"
|
||||
},
|
||||
|
||||
7
scripts/release-version.mjs
Normal file
7
scripts/release-version.mjs
Normal 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
14
tests/release.test.mjs
Normal 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);
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user