ci: migrate workflows to Gitea Actions
CI / container-policy (push) Successful in 8s
CI / javascript-check (push) Successful in 18s
CI / python-tests (push) Failing after 4m44s
CI / container-verify (push) Skipped
CI / container-publish (push) Skipped

This commit is contained in:
BartelLuis
2026-09-14 20:23:20 +02:00
parent 06c3474636
commit 9c528c6eca
13 changed files with 472 additions and 303 deletions
@@ -8,38 +8,38 @@ on:
permissions:
contents: read
# Protected runs get their own group; publication is queued at job level.
# Publication runs are not canceled by newer checks; pushes serialize per job.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name != 'pull_request' && github.ref_protected && github.run_id || 'checks' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || !github.ref_protected }}
group: ci-${{ gitea.workflow }}-${{ gitea.ref }}-${{ gitea.event_name != 'pull_request' && vars.PUBLISH_IMAGES == 'true' && gitea.run_id || 'checks' }}
cancel-in-progress: ${{ gitea.event_name == 'pull_request' || vars.PUBLISH_IMAGES != 'true' }}
defaults:
run:
shell: bash
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
DEFAULT_BRANCH: ${{ gitea.event.repository.default_branch }}
PUBLISH_IMAGES: ${{ vars.PUBLISH_IMAGES }}
REGISTRY_HOST: ${{ vars.REGISTRY_HOST || 'gitlab.bartelluis.de' }}
jobs:
python-tests:
runs-on: ubuntu-24.04
runs-on: ${{ vars.AIS_RUNNER_LABEL || 'ubuntu-latest' }}
timeout-minutes: 15
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: https://github.com/actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
- uses: https://github.com/actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.13"
cache: pip
cache-dependency-path: pyproject.toml
- name: Run Python tests
run: sh ci/python-tests.sh
- name: Upload JUnit report
if: ${{ always() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: https://gitea.com/actions/gitea-upload-artifact@62ac910c5d3dfa85c7cb2df15afe2e342b2407c2
with:
name: python-test-results
path: reports/pytest.xml
@@ -47,13 +47,13 @@ jobs:
if-no-files-found: warn
javascript-check:
runs-on: ubuntu-24.04
runs-on: ${{ vars.AIS_RUNNER_LABEL || 'ubuntu-latest' }}
timeout-minutes: 5
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: https://github.com/actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: https://github.com/actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
package-manager-cache: false
@@ -61,37 +61,31 @@ jobs:
run: node --check provisioner/static/app.js
container-policy:
runs-on: ubuntu-24.04
runs-on: ${{ vars.AIS_RUNNER_LABEL || 'ubuntu-latest' }}
timeout-minutes: 5
permissions: {}
outputs:
publish: ${{ steps.policy.outputs.publish }}
steps:
# Ref names are read from the runner environment, never inserted into shell code.
# The container script independently enforces these publication rules.
- uses: https://github.com/actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
# Gitea supplies GITHUB_* compatibility variables. Keep ref names out of
# shell expressions and use the same policy in the container script.
- name: Select verification or publication
id: policy
run: |
publish=false
if [[ "$GITHUB_REF_PROTECTED" == true ]] &&
[[ "$GITHUB_EVENT_NAME" == push || "$GITHUB_EVENT_NAME" == workflow_dispatch ]]; then
if [[ "$GITHUB_REF_TYPE" == branch && -n "$DEFAULT_BRANCH" && "$GITHUB_REF_NAME" == "$DEFAULT_BRANCH" ]] ||
[[ "$GITHUB_REF_TYPE" == tag && "$GITHUB_REF_NAME" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
publish=true
fi
fi
publish=$(sh ci/publish-policy.sh)
printf 'publish=%s\n' "$publish" >> "$GITHUB_OUTPUT"
container-verify:
needs: [python-tests, javascript-check, container-policy]
if: ${{ needs.container-policy.outputs.publish == 'false' }}
runs-on: ubuntu-24.04
runs-on: ${{ vars.AIS_RUNNER_LABEL || 'ubuntu-latest' }}
timeout-minutes: 30
env:
DOCKER_HOST: unix:///var/run/docker.sock
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: https://github.com/actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Check Docker tools
@@ -100,7 +94,7 @@ jobs:
docker buildx version
- name: Build and smoke-test image
run: sh ci/container.sh verify
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: https://gitea.com/actions/gitea-upload-artifact@62ac910c5d3dfa85c7cb2df15afe2e342b2407c2
with:
name: container-build
path: build.env
@@ -110,20 +104,17 @@ jobs:
container-publish:
needs: [python-tests, javascript-check, container-policy]
if: ${{ needs.container-policy.outputs.publish == 'true' }}
runs-on: ubuntu-24.04
runs-on: ${{ vars.AIS_RUNNER_LABEL || 'ubuntu-latest' }}
timeout-minutes: 30
permissions:
contents: read
packages: write
concurrency:
group: ghcr-container-publish
group: container-publish
cancel-in-progress: false
queue: max
env:
DOCKER_HOST: unix:///var/run/docker.sock
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: https://github.com/actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Check Docker tools
@@ -132,9 +123,10 @@ jobs:
docker buildx version
- name: Build, smoke-test and publish image
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: sh ci/container.sh publish
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: https://gitea.com/actions/gitea-upload-artifact@62ac910c5d3dfa85c7cb2df15afe2e342b2407c2
with:
name: container-deploy
path: |
+22 -17
View File
@@ -55,7 +55,7 @@ und TLS-Konfiguration stehen in der [Betriebsanleitung](docs/operations.md).
Auf dem Zielserver werden Docker Engine, das Compose-Plugin und ein
HTTPS-Reverse-Proxy benötigt. Die Anwendung wird als fertiges Image aus der
GitHub Container Registry (GHCR) heruntergeladen. Quellcode und Build-Werkzeuge werden
Container Registry der Gitea-Instanz heruntergeladen. Quellcode und Build-Werkzeuge werden
für das Deployment nicht benötigt.
[compose.yaml](compose.yaml) und die [Umgebungsvorlage](config/deployment.env.example)
@@ -67,14 +67,14 @@ einen bereits veröffentlichten Versionstag aus der Projektregistry verwenden.
Compose benötigt ausdrücklich `PROVISIONER_IMAGE`; ein lokaler Build ist nicht
Teil dieser Deployment-Konfiguration.
Im Deployment-Verzeichnis ausführen. Für ein privates GHCR-Paket beim Login den
eigenen GitHub-Benutzernamen und einen Personal Access Token (classic) mit
`read:packages` als Passwort verwenden. Bei einem öffentlichen Paket entfällt
Im Deployment-Verzeichnis ausführen. Für ein privates Gitea-Paket beim Login den
eigenen Gitea-Benutzernamen und einen Personal Access Token mit
`read:package` als Passwort verwenden. Bei einem öffentlichen Paket entfällt
der Login. Details stehen unter [Registry-Anmeldung](docs/deployment.md#an-der-registry-anmelden-und-image-laden).
```bash
# Nur für ein privates Paket; GITHUB_USERNAME ersetzen:
docker login ghcr.io --username GITHUB_USERNAME
# Nur für ein privates Paket; GITEA_USERNAME ersetzen:
docker login gitlab.bartelluis.de --username GITEA_USERNAME
docker compose config --quiet
docker compose pull provisioner
# Nur bei der ersten Inbetriebnahme:
@@ -91,20 +91,25 @@ Master-Key getrennt in `proxmox-ais-keys`.
Die [Deployment-Anleitung](docs/deployment.md) beschreibt Image-Auswahl,
Erstinitialisierung, Updates, Rollback und die Prüfung des laufenden Containers.
## GitHub Actions CI/CD
## Gitea Actions CI/CD
Der [GitHub-Actions-Workflow](.github/workflows/ci.yml) prüft Python und JavaScript, baut das Image
Das Projekt liegt auf [Gitea](https://gitlab.bartelluis.de/BartelLuis/Proxmox-AIS-Server).
Der [Gitea-Actions-Workflow](.gitea/workflows/ci.yml) prüft Python und JavaScript, baut das Image
und testet HTTPS, Anmeldung und Datenerhalt beim Neustart im gehärteten Container.
Geschützte Standardbranch-Pushes veröffentlichen `sha-<Commit>` und `edge` in
`ghcr.io/netivra/proxmox-ais`; geschützte Release-Tags wie `v0.1.0` zusätzlich
die passende Versionsnummer. Feature-Branches und Pull Requests werden ohne Push geprüft.
Bei aktivierter Veröffentlichung (`PUBLISH_IMAGES=true`) veröffentlichen
Standardbranch-Pushes `sha-<Commit>` und `edge` in
`gitlab.bartelluis.de/bartelluis/proxmox-ais-server`; Release-Tags wie `v0.1.0`
zusätzlich die passende Versionsnummer. Feature-Branches und Pull Requests
werden ohne Registry-Push geprüft.
Die Jobs verwenden GitHub-gehostete Runner mit `ubuntu-24.04`, Python 3.13,
Node.js 24 und Docker/Buildx. Ein eigener CI-Runner ist nicht erforderlich.
Für die Veröffentlichung müssen der Standardbranch und die Release-Tags durch
aktive GitHub-Regeln geschützt sein. GHCR erhält das automatisch bereitgestellte
`GITHUB_TOKEN`; ein eigenes CI-Secret wird nicht benötigt. Einrichtung,
Schutzregeln und Artefakte stehen in der [GitHub-Anleitung](docs/github-actions.md).
Gitea benötigt einen registrierten, erreichbaren Linux-amd64-Runner. Das
Runner-Label ist über `AIS_RUNNER_LABEL` wählbar und lautet standardmäßig
`ubuntu-latest`. Die Build-Umgebung benötigt Python 3.13, Node.js 24 und
Docker/Buildx. Für die Veröffentlichung werden die Variable `REGISTRY_USERNAME`
und das Secret `REGISTRY_TOKEN` mit `write:package` eingerichtet. Standardbranch
und Release-Tags in Gitea schützen, bevor `PUBLISH_IMAGES` aktiviert wird.
Runner-Einrichtung, Schutzregeln und Artefakte stehen in der
[Gitea-Anleitung](docs/gitea-actions.md).
Die Pipeline liefert das getestete Image und dessen Digest in `deploy.env`.
Die Installation auf dem Zielserver folgt der [Deployment-Anleitung](docs/deployment.md).
+14 -8
View File
@@ -1,5 +1,6 @@
#!/bin/sh
# Build and smoke-test the same image that is optionally published to GHCR.
# Build and smoke-test the same image that is optionally published to Gitea.
# Gitea Actions supplies the GitHub-compatible GITHUB_* runner variables.
set -eu
fail() {
@@ -19,17 +20,22 @@ esac
: "${GITHUB_JOB:?GITHUB_JOB is required}"
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
: "${REGISTRY_HOST:?REGISTRY_HOST is required (hostname, optionally with port)}"
printf '%s\n' "$GITHUB_SHA" | grep -Eq '^[0-9a-f]{40}$' || fail 'Expected a full Git commit SHA'
for run_identifier in "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT"; do
case "$run_identifier" in
''|*[!0-9]*) fail 'GitHub run ID and attempt must be numeric' ;;
''|*[!0-9]*) fail 'Actions run ID and attempt must be numeric' ;;
esac
done
case "$GITHUB_JOB" in
''|[!A-Za-z_]*|*[!A-Za-z0-9_-]*) fail 'GITHUB_JOB must be a safe job identifier' ;;
esac
printf '%s\n' "$GITHUB_REPOSITORY" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9_][A-Za-z0-9_.-]*$' || fail 'Expected GITHUB_REPOSITORY in owner/repository form'
registry_image="ghcr.io/$(printf '%s' "$GITHUB_REPOSITORY" | LC_ALL=C tr '[:upper:]' '[:lower:]')"
case "$REGISTRY_HOST" in
''|*[!a-z0-9.:-]*) fail 'REGISTRY_HOST must be a lowercase hostname, optionally with port' ;;
esac
printf '%s\n' "$REGISTRY_HOST" | grep -Eq '^[a-z0-9][a-z0-9.-]*(:[0-9]+)?$' || fail 'Invalid REGISTRY_HOST'
registry_image="$REGISTRY_HOST/$(printf '%s' "$GITHUB_REPOSITORY" | LC_ALL=C tr '[:upper:]' '[:lower:]')"
project_url="${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}"
job_identifier="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}"
@@ -50,7 +56,7 @@ project_version=$(awk '
publish_tag=
if [ "$mode" = publish ]; then
[ "${GITHUB_REF_PROTECTED:-}" = true ] || fail 'Publication requires a protected branch or tag'
[ "$(sh ci/publish-policy.sh)" = true ] || fail 'Publication requires PUBLISH_IMAGES=true and a default-branch or release-tag push/manual run'
case "${GITHUB_EVENT_NAME:-}" in
push|workflow_dispatch) ;;
*) fail 'Publication is allowed only from push or workflow_dispatch events' ;;
@@ -67,8 +73,8 @@ if [ "$mode" = publish ]; then
;;
*) fail 'Publication requires a branch or tag ref' ;;
esac
: "${GITHUB_ACTOR:?GITHUB_ACTOR is required for publication}"
: "${GHCR_TOKEN:?GHCR_TOKEN is required for publication}"
: "${REGISTRY_USERNAME:?REGISTRY_USERNAME must be the package token owner}"
: "${REGISTRY_TOKEN:?REGISTRY_TOKEN requires write:package permission}"
fi
BUILD_IMAGE="${registry_image}:ci-${job_identifier}"
@@ -119,8 +125,8 @@ if [ "$mode" = publish ]; then
docker_config_dir=$(mktemp -d "${TMPDIR:-/tmp}/ais-ci-docker.XXXXXXXX")
DOCKER_CONFIG=$docker_config_dir
export DOCKER_CONFIG
printf '%s' "$GHCR_TOKEN" | docker login ghcr.io \
--username "$GITHUB_ACTOR" --password-stdin
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" \
--username "$REGISTRY_USERNAME" --password-stdin
commit_image="${registry_image}:sha-${GITHUB_SHA}"
channel_image="${registry_image}:${publish_tag}"
+30
View File
@@ -0,0 +1,30 @@
#!/bin/sh
# Gitea 1.27 does not populate ref_protected. Repository administrators opt in
# after configuring branch/tag protection and package credentials in Gitea.
set -eu
publish=false
if [ "${PUBLISH_IMAGES:-}" = true ]; then
case "${GITHUB_EVENT_NAME:-}" in
push|workflow_dispatch)
case "${GITHUB_REF_TYPE:-}" in
branch)
if [ -n "${DEFAULT_BRANCH:-}" ] && [ "${GITHUB_REF_NAME:-}" = "$DEFAULT_BRANCH" ]; then
publish=true
fi
;;
tag)
case "${GITHUB_REF_NAME:-}" in
''|*[!v0-9.]*) ;;
*)
if printf '%s\n' "$GITHUB_REF_NAME" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then
publish=true
fi
;;
esac
;;
esac
;;
esac
fi
printf '%s\n' "$publish"
+2 -1
View File
@@ -1,5 +1,6 @@
#!/bin/sh
# Run tests in an isolated, disposable environment on a Linux Actions runner.
# Gitea Actions supplies the GitHub-compatible GITHUB_* runner variables.
set -eu
fail() {
@@ -13,7 +14,7 @@ fail() {
: "${GITHUB_JOB:?GITHUB_JOB is required}"
for run_identifier in "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT"; do
case "$run_identifier" in
''|*[!0-9]*) fail 'GitHub run ID and attempt must be numeric' ;;
''|*[!0-9]*) fail 'Actions run ID and attempt must be numeric' ;;
esac
done
case "$GITHUB_JOB" in
+1 -1
View File
@@ -1,6 +1,6 @@
services:
provisioner:
image: ${PROVISIONER_IMAGE:?PROVISIONER_IMAGE in .env auf das Image aus der GitHub Container Registry setzen}
image: ${PROVISIONER_IMAGE:?PROVISIONER_IMAGE in .env auf das Image aus der Gitea Container Registry setzen}
restart: unless-stopped
init: true
ports:
+3 -3
View File
@@ -2,7 +2,7 @@
PUBLIC_URL=https://provision.example.net
MAINTENANCE=false
# Durch die vollstaendige PROVISIONER_IMAGE-Zeile aus deploy.env im GitHub-
# Durch die vollstaendige PROVISIONER_IMAGE-Zeile aus deploy.env im Gitea-
# Actions-Artefakt container-deploy ersetzen. Alternativ einen vorhandenen
# Versionstag aus GHCR verwenden. Der Beispieltag muss veroeffentlicht sein.
PROVISIONER_IMAGE=ghcr.io/netivra/proxmox-ais:0.1.0
# Versionstag aus der Gitea-Registry verwenden. Der Beispieltag muss veroeffentlicht sein.
PROVISIONER_IMAGE=gitlab.bartelluis.de/bartelluis/proxmox-ais-server:0.1.0
+17 -14
View File
@@ -1,7 +1,7 @@
# Deployment mit einem Image aus der Container Registry
Proxmox AIS wird auf dem Zielserver aus einem bereits veröffentlichten
Containerimage gestartet. Der [GitHub-Actions-Workflow](github-actions.md) erstellt dieses Image; auf dem
Containerimage gestartet. Der [Gitea-Actions-Workflow](gitea-actions.md) erstellt dieses Image; auf dem
Zielserver werden nur Docker Compose und die Betriebskonfiguration benötigt.
Diese Anleitung setzt einen erfolgreichen `container-publish`-Job und ein
weiterhin abrufbares Image voraus.
@@ -42,20 +42,20 @@ Wert führt bereits bei der Konfigurationsprüfung zu einem Fehler.
## Image und öffentliche Adresse festlegen
In GitHub unter **Actions** den erfolgreichen Workflow-Lauf mit
In Gitea unter **Actions** den erfolgreichen Workflow-Lauf mit
`container-publish` öffnen, unter **Artifacts** das Archiv `container-deploy`
herunterladen und entpacken. Es enthält `build.env` und `deploy.env`.
Die vollständige Zeile `PROVISIONER_IMAGE=…@sha256:…` aus `deploy.env` in die
`.env` auf dem Zielserver übernehmen. Der Digest legt genau das veröffentlichte
Image fest. Compose liest `deploy.env` nicht automatisch ein.
[GitHub-Artefakte herunterladen](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/download-workflow-artifacts).
Alternativ auf GitHub das zugehörige **Packages**-Paket öffnen und den vollständigen
Alternativ in Gitea beim Benutzer `BartelLuis` das zugehörige **Packages**-Paket
öffnen und den vollständigen
Imagepfad mit einem tatsächlich vorhandenen Versions-Tag kopieren. Der aktuelle
Imagepfad der Beispieldatei sieht so aus:
```dotenv
PROVISIONER_IMAGE=ghcr.io/netivra/proxmox-ais:0.1.0
PROVISIONER_IMAGE=gitlab.bartelluis.de/bartelluis/proxmox-ais-server:0.1.0
PUBLIC_URL=https://provision.example.net
MAINTENANCE=false
```
@@ -64,7 +64,9 @@ MAINTENANCE=false
Imagepfad entspricht dem aktuellen Wert der Vorlage; die Verfügbarkeit des
Tags `0.1.0` wird damit nicht vorausgesetzt. Maßgeblich sind das
Pipeline-Artefakt oder die Registry-Anzeige deines Projekts.
Der Registry-Endpunkt ist `ghcr.io`. Für reproduzierbare Deployments den Digest
Der Registry-Endpunkt der Vorlage ist `gitlab.bartelluis.de`. Bei einer abweichenden
CI-Registry den Host aus `deploy.env` auch für `docker login` verwenden.
Für reproduzierbare Deployments den Digest
bevorzugen und die bisher verwendeten Digests für spätere Updates aufbewahren.
`PUBLIC_URL` ist die vollständige HTTPS-Basisadresse ohne zusätzlichen Pfad,
@@ -78,19 +80,20 @@ einrichten; `127.0.0.1` bezeichnet dort jeweils das eigene System.
## An der Registry anmelden und Image laden
Öffentliche GHCR-Pakete lassen sich ohne Anmeldung herunterladen. Für ein
privates Paket einen GitHub Personal Access Token **(classic)** mit dem Scope
`read:packages` verwenden. Das zugehörige GitHub-Konto benötigt Lesezugriff auf
das Paket; bei Organisations-SSO den Token dafür autorisieren. Der Token wird
Öffentliche Gitea-Pakete lassen sich ohne Anmeldung herunterladen. Für ein
privates Paket einen Gitea Personal Access Token mit dem Scope
`read:package` verwenden. Das zugehörige Gitea-Konto benötigt Lesezugriff auf
das Paket. Der Token wird
bei der Anmeldung als Passwort verdeckt eingegeben und gehört nicht in `.env`.
Das kurzlebige `GITHUB_TOKEN` des CI-Jobs ist kein Deployment-Zugang.
[GHCR-Authentifizierung](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic).
Das kurzlebige `GITEA_TOKEN` des CI-Jobs ist kein Deployment-Zugang.
[Gitea-Container-Registry](https://docs.gitea.com/usage/packages/container/),
[Paketberechtigungen](https://docs.gitea.com/usage/packages/overview/#access-restrictions).
Nur für private Pakete anmelden und `GITHUB_USERNAME` durch den GitHub-Namen
Nur für private Pakete anmelden und `GITEA_USERNAME` durch den Gitea-Namen
des Token-Inhabers ersetzen:
```bash
docker login ghcr.io --username GITHUB_USERNAME
docker login gitlab.bartelluis.de --username GITEA_USERNAME
```
Konfiguration prüfen und das gewählte Image herunterladen:
+210
View File
@@ -0,0 +1,210 @@
# Gitea Actions und Container Registry
Das Projekt liegt auf
[Gitea](https://gitlab.bartelluis.de/BartelLuis/Proxmox-AIS-Server).
Der [Workflow](../.gitea/workflows/ci.yml) prüft Python und JavaScript, baut
das Image und führt den Container-Smoke-Test aus. Bei aktivierter
Veröffentlichung lädt er dasselbe getestete Image in die Gitea Container
Registry. Der Workflow endet beim Image-Push; die Schritte auf dem Zielserver
stehen in der [Deployment-Anleitung](deployment.md).
## Gitea und Runner einrichten
In den Repository-Einstellungen **Actions** aktivieren und unter
**Settings → Actions → Runners** einen registrierten Runner prüfen oder
einrichten. Gitea benötigt einen eigenen Runner-Dienst; ein GitHub-Runner-Label
stellt keine Rechenkapazität bereit.
[Gitea Actions einrichten](https://docs.gitea.com/usage/actions/quickstart/).
Alle Jobs verwenden `vars.AIS_RUNNER_LABEL` oder, wenn die Variable nicht gesetzt
ist, `ubuntu-latest`. Das ausgewählte Label muss einem für dieses Repository
verfügbaren, erreichbaren Linux-amd64-Runner entsprechen. Die Angabe
`ubuntu-latest` wählt ein Runner-Label; sie installiert kein Ubuntu-Image.
Für ein anderes Label in **Settings → Actions → Variables** beispielsweise
`AIS_RUNNER_LABEL=ais-ci` setzen und genau dieses Label am Runner konfigurieren.
Die tatsächliche Job-Umgebung benötigt Git, Bash, OpenSSL, `ssh-keygen`, Python
3.13 mit `venv`, Node.js 24 sowie Docker CLI und Buildx mit Zugriff auf einen
laufenden Docker-Daemon. Die Setup-Actions richten Python 3.13 und Node.js 24
für ihre jeweiligen Jobs ein. Die Umgebung muss außerdem bereits die
JavaScript-Laufzeit zum Ausführen der Actions unterstützen. Bei einem
Container-Runner müssen Werkzeuge und Docker-Zugriff im Job-Container verfügbar
sein, nicht nur auf dem Host. Ein Standard-Node-Image allein enthält nicht die
gesamte Build-Umgebung. Runner-Konfiguration und Label-Zuordnung sind in der
[Runner-Anleitung](https://docs.gitea.com/1.26/usage/actions/act-runner/#labels)
beschrieben.
Der Runner muss Gitea, die Registry, die absoluten Action-URLs im Workflow und
die Downloadquellen für Python, Node.js, Python-Pakete und das Docker-Basisimage
erreichen. Die Actions sind auf vollständige Commit-SHAs festgelegt.
Der Artefakt-Upload verwendet die Gitea-kompatible Action
`https://gitea.com/actions/gitea-upload-artifact`.
Diese Build-Umgebung ist unabhängig vom AIS-Host-Runner für die
Postinstallation auf Proxmox-Hosts. CI-Code mit Docker-Zugriff auf einer dafür
vorgesehenen Build-Maschine ausführen; Änderungen an Workflow und CI-Skripten
vor der Übernahme prüfen.
## Registry und Veröffentlichung einrichten
Ohne `PUBLISH_IMAGES=true` führen alle Refs ausschließlich Prüfungen durch.
In **Settings → Actions → Variables** folgende Repository-Variablen setzen:
| Variable | Wert und Bedeutung |
| --- | --- |
| `AIS_RUNNER_LABEL` | Optional: Label eines verfügbaren Runners; Standard `ubuntu-latest` |
| `REGISTRY_HOST` | Optional: Registry-Host ohne Schema oder Pfad; Standard `gitlab.bartelluis.de` |
| `REGISTRY_USERNAME` | Gitea-Benutzername des Token-Inhabers; für Veröffentlichung erforderlich |
| `PUBLISH_IMAGES` | Genau `true`, nachdem Registry-Zugang und Schutzregeln eingerichtet sind |
Im Gitea-Benutzerkonto einen Personal Access Token mit Paket-Schreibrecht
(`write:package`, in der Oberfläche **package: Read & Write**) erstellen.
Den Wert unter **Settings → Actions → Secrets** als `REGISTRY_TOKEN` speichern.
Das zugehörige Konto muss beim Paketinhaber `BartelLuis` Schreibzugriff besitzen.
Das kurzlebige `GITEA_TOKEN` des Jobs unterstützt die Paketveröffentlichung
nicht; es ersetzt dieses Secret nicht.
[Gitea-Secrets](https://docs.gitea.com/usage/actions/secrets/),
[Paketberechtigungen](https://docs.gitea.com/usage/packages/overview/#access-restrictions),
[Gitea-Paketautorisierung](https://docs.gitea.com/usage/actions/comparison/#package-repository-authorization).
Der Imagepfad ist `<REGISTRY_HOST>/<kleingeschriebener Repositoryname>`, hier
`gitlab.bartelluis.de/bartelluis/proxmox-ais-server`. Bei einem Fork oder einer
Umbenennung ändert sich der Pfad. Pakete gehören in Gitea einem Benutzer oder
einer Organisation. Nach dem ersten Push kann das Paket in seinen Einstellungen
mit dem Repository verknüpft werden, damit es auch in dessen Paketliste erscheint.
[Gitea Container Registry](https://docs.gitea.com/usage/packages/container/).
## Standardbranch und Release-Tags schützen
Vor `PUBLISH_IMAGES=true` in den Gitea-Repository-Einstellungen den
Standardbranch `main` und die Release-Tags schützen. Für `main` direkte Pushes
deaktivieren, Reviews und erfolgreiche Statuschecks verlangen sowie Force-Push
und Löschen einschränken.
Für `v*` die Erstellung, Änderung und Löschung auf Release-Verantwortliche
begrenzen. Release-Tags auf einen geprüften Commit mit passender Projektversion
setzen. Die Schutzregeln werden durch den Workflow-Commit nicht angelegt.
[Geschützte Branches](https://docs.gitea.com/usage/access-control/protected-branches/),
[Geschützte Tags](https://docs.gitea.com/usage/access-control/protected-tags/).
Nach dem ersten Pull-Request-Lauf die tatsächlich gemeldeten Statuskontexte
für `python-tests`, `javascript-check`, `container-policy` und `container-verify`
als verpflichtend auswählen. `container-publish` wird bei Pull Requests übersprungen und ist
deshalb keine Pflichtprüfung.
Die CI entscheidet anhand von `PUBLISH_IMAGES`, Ereignis und Ref. Sie fragt
keinen Schutzstatus ab: Giteas kompatibler `ref_protected`-Wert ist dafür nicht
verwendbar. Die Gitea-Schutzregeln und restriktive Schreibrechte bleiben daher
ein eigener Einrichtungsschritt.
## Auslöser und Image-Tags
Der Workflow läuft bei Branch- und Tag-Pushes, bei Pull Requests sowie bei
manuellem Start über `workflow_dispatch`.
| Ref und Ereignis | Container-Job | Veröffentlichung |
| --- | --- | --- |
| `PUBLISH_IMAGES` fehlt oder ist nicht `true` | `container-verify` | Keine |
| Pull Request, Feature-Branch oder sonstiger Tag | `container-verify` | Keine |
| Standardbranch, Push oder manueller Start, Veröffentlichung aktiviert | `container-publish` | `sha-<vollständiger Commit-SHA>` und `edge` |
| Tag `vX.Y.Z`, Push oder manueller Start, Veröffentlichung aktiviert | `container-publish` | `sha-<vollständiger Commit-SHA>` und `X.Y.Z` |
Release-Tags müssen vollständig dem Muster
`v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)` entsprechen. Führende
Nullen, Vorabversionen und Build-Zusätze veröffentlichen kein Image.
Bei einem zur Veröffentlichung ausgewählten Release muss die Version außerdem
exakt `[project].version` in [pyproject.toml](../pyproject.toml) entsprechen;
eine Abweichung bricht den Publish-Job ab.
`edge` ist ein beweglicher Tag. Ein erneuter Lauf eines älteren Commits kann
ihn zurücksetzen. SHA-Tags enthalten alle 40 Zeichen des Commits; externe
Build-Abhängigkeiten können bei erneuten Builds zu einem anderen Digest führen.
Es gibt keinen `latest`-Tag. Im Betrieb den Digest aus `deploy.env` verwenden
und bisherige Registry-Versionen für Updates und Rollback aufbewahren.
## Jobs und Artefakte
`python-tests` führt über [ci/python-tests.sh](../ci/python-tests.sh) Pytest in
einer temporären virtuellen Umgebung aus. `javascript-check` prüft mit
`node --check provisioner/static/app.js` die JavaScript-Syntax.
`container-policy` wählt den Container-Job aus. Nach erfolgreichen
Quellcodeprüfungen läuft genau einer der Jobs `container-verify` und
`container-publish`. Beide verwenden [ci/container.sh](../ci/container.sh);
das Skript prüft vor einer Veröffentlichung erneut Ereignis, Ref und Opt-in.
Der [Container-Smoke-Test](../tests/container_smoke.py) prüft das gebaute
`linux/amd64`-Image auf TLS, Anmeldung, CSRF, Webdateien und Datenerhalt über
einen Dienstneustart. Er läuft als UID 10001 mit schreibgeschütztem Dateisystem,
entfernten Linux-Capabilities und temporären Testdaten. Eine Proxmox-Installation
ist kein Bestandteil dieses Tests. Der Publish-Job lädt das getestete Image
ohne zweiten Build hoch. OCI-Labels enthalten Quellrepository, Commit und
Anwendungsversion; Provenance- und SBOM-Attestierungen werden nicht erzeugt.
| Artefakt im Gitea-Lauf | Inhalt | Angefragte Aufbewahrung |
| --- | --- | --- |
| `python-test-results` | `reports/pytest.xml`, auch bei fehlgeschlagenen Tests, sofern erzeugt | 7 Tage |
| `container-build` | `build.env` nach erfolgreicher Verifikation | 7 Tage |
| `container-deploy` | `build.env` und `deploy.env` nach erfolgreicher Veröffentlichung | 30 Tage |
Die tatsächliche Aufbewahrung unterliegt der Gitea-Instanzkonfiguration.
Unter **Actions** den erfolgreichen Lauf öffnen und das gewünschte Artefakt
herunterladen. `build.env` enthält den temporären lokalen Image-Tag, Commit
und Anwendungsversion. Der lokale `ci-…`-Tag wird nicht veröffentlicht und am
Jobende entfernt. `deploy.env` enthält ausschließlich
`PROVISIONER_IMAGE=gitlab.bartelluis.de/bartelluis/proxmox-ais-server@sha256:…`
beziehungsweise die konfigurierte Registry-Adresse. Beide Dateien enthalten
keine Zugangsdaten. Die Image-Zeile in `.env` auf dem Zielserver übernehmen.
## Erster Lauf und Release
Nach dem Push unter **Actions** prüfen, dass der Lauf einen Runner erhält und
Python-, JavaScript- und Containerprüfungen erfolgreich sind. Nach aktivierter
Veröffentlichung muss ein Standardbranch-Lauf außerdem die Image-Tags
`sha-<Commit>` und `edge` sowie `container-deploy` mit `deploy.env` liefern.
Lokale Tests ersetzen diesen Lauf mit Registry-Anmeldung und Push nicht.
Für ein Release zuerst `[project].version` ändern und den geprüften Commit
übernehmen. Anschließend mit einem berechtigten Konto den passenden Tag
erstellen und an den Gitea-Remote pushen. Im vorhandenen Checkout heißt er
`origin`; vor dem Push mit `git remote -v` prüfen. Für Projektversion `0.1.0`,
sofern der Tag noch nicht existiert:
```bash
git tag -a v0.1.0 -m "Release 0.1.0"
git push origin v0.1.0
```
Der Release-Lauf muss den Image-Tag `0.1.0` erzeugen. Für einen manuellen Lauf
unter **Actions** den Workflow und den gewünschten Ref auswählen. Auch dabei
gelten Opt-in, Standardbranch- beziehungsweise Tag-Regel und Versionsabgleich.
Branch-Push und Pull Request können für denselben Quellstand getrennte Läufe
erzeugen. Pull Requests veröffentlichen keine Images.
## Fehlersuche und Deployment
Bei **Waiting** beziehungsweise **no matching online runner** das angeforderte
Label mit den erreichbaren Runnern unter **Settings → Actions → Runners**
vergleichen. Der bisherige Workflow verlangte `ubuntu-24.04`; dafür war kein
passender Online-Runner verfügbar. `AIS_RUNNER_LABEL` auf ein tatsächlich
vorhandenes Label setzen oder einen geeigneten Runner registrieren und starten.
Ein Label-Wechsel allein ersetzt keine fehlende Runner-Installation.
Bei fehlendem Docker-Zugriff `docker info` und `docker buildx version` in der
Job-Umgebung prüfen. Bei DNS-, Checkout- oder Artefaktfehlern müssen Giteas
öffentliche `ROOT_URL`, die Checkout-Adresse und die vom Runner erreichbare
Adresse zusammenpassen. Das Projekt verwendet `gitlab.bartelluis.de`; eine
von der Instanz zurückgegebene abweichende Adresse wie `git.bartelluis.de` muss
ebenfalls auflösbar und erreichbar sein oder in der Gitea-Konfiguration
korrigiert werden.
Bei übersprungenem Publish-Job `PUBLISH_IMAGES`, Ereignis, Standardbranch und
Tagformat prüfen. Bei `denied` oder `unauthorized` Registry-Host, Benutzername,
`REGISTRY_TOKEN`, dessen `write:package`-Scope und die Rechte am Paketinhaber
kontrollieren. Bei Versionsfehlern Git-Tag und `pyproject.toml` abgleichen.
Fehlt `container-deploy`, die Build-, Smoke-, Push- und Upload-Logs prüfen.
Zum Deployment [compose.yaml](../compose.yaml) und
[config/deployment.env.example](../config/deployment.env.example) auf den
Zielserver kopieren. Die [Deployment-Anleitung](deployment.md) beschreibt
Registry-Anmeldung, Erstinitialisierung, HTTPS-Prüfung, Updates und Rollback.
Für private Pakete benötigt das Deploymentkonto einen eigenen Token mit
`read:package`; öffentliche Pakete lassen sich anonym laden.
+3 -202
View File
@@ -1,203 +1,4 @@
# GitHub Actions und Container Registry
# CI/CD ist nach Gitea umgezogen
Der [Workflow](../.github/workflows/ci.yml) führt die Python-Tests aus, prüft die
JavaScript-Syntax und baut und testet das Anwendungsimage. Freigegebene Builds
werden in die GitHub Container Registry (GHCR) veröffentlicht. Der Workflow
endet beim Image-Push und aktualisiert keinen Zielserver. Die anschließenden
Betriebsschritte stehen in der [Deployment-Anleitung](deployment.md).
## Auslöser und Image-Tags
Der Workflow läuft bei Pushes auf alle Branches und Tags, bei Pull Requests
und beim manuellen Start mit `workflow_dispatch`.
| Ref und Ereignis | Container-Job | Veröffentlichung |
| --- | --- | --- |
| Pull Request, auch aus einem Fork | `container-verify` | Keine |
| Feature-Branch, ungeschützter Ref oder sonstiger Tag | `container-verify` | Keine |
| Geschützter Standardbranch bei Push oder manuellem Start | `container-publish` | `sha-<vollständiger Commit-SHA>` und `edge` |
| Geschützter Tag `vX.Y.Z` bei Push oder manuellem Start | `container-publish` | `sha-<vollständiger Commit-SHA>` und `X.Y.Z` |
Release-Tags müssen dem Muster `v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)`
vollständig entsprechen. Führende Nullen, Vorabversionen wie `v1.0.0-rc.1` und
Build-Zusätze veröffentlichen daher kein Image. Bei einem gültigen geschützten
Release-Tag muss dessen Version außerdem exakt `[project].version` in
[pyproject.toml](../pyproject.toml) entsprechen; eine Abweichung bricht den Lauf ab.
Das Image liegt für dieses Repository unter `ghcr.io/netivra/proxmox-ais`.
Der Workflow leitet den Pfad aus dem kleingeschriebenen GitHub-Repositorynamen
ab; bei einem Fork oder einer Umbenennung ändert sich deshalb der Imagepfad.
`edge` bezeichnet den zuletzt veröffentlichten Standardbranch-Build. Ein
erneuter Lauf eines älteren Commits kann `edge` zurücksetzen. SHA-Tags enthalten
alle 40 Zeichen des Commits; erneute Builds desselben Quellstands können durch
externe Build-Abhängigkeiten einen anderen Digest ergeben. Es gibt keinen
`latest`-Tag. Im Betrieb den Digest aus `deploy.env` verwenden und die
zugehörigen Registry-Versionen für Updates und Rollback aufbewahren.
## Jobs und Artefakte
`python-tests` und `javascript-check` prüfen den Quellstand. Das Skript
[ci/python-tests.sh](../ci/python-tests.sh) installiert `.[dev]` in einer
eigenen temporären virtuellen Umgebung, führt Pytest aus und entfernt die
Umgebung anschließend. Die JavaScript-Prüfung verwendet
`node --check provisioner/static/app.js`; sie ist kein Browsertest.
`container-policy` bestimmt anhand des GitHub-Ereignisses, des Ref-Schutzes
und des Branch- beziehungsweise Tag-Namens, welcher Container-Job laufen darf.
Nach erfolgreichen Python- und JavaScript-Prüfungen läuft genau einer der
Jobs `container-verify` und `container-publish`. Beide bauen über
[ci/container.sh](../ci/container.sh) ein `linux/amd64`-Image und prüfen es
mit dem [Container-Smoke-Test](../tests/container_smoke.py). Der Publish-Job
lädt genau das bereits getestete Image hoch, ohne einen zweiten Build.
Die Veröffentlichung wird im Skript erneut auf Ereignis und Ref geprüft;
dort erfolgt auch der Abgleich mit der Projektversion.
Publish-Jobs teilen eine Warteschlange mit `queue: max` und laufen einzeln.
Bis zu 100 Jobs können warten; ein laufender Publish-Job wird durch neue
Commits nicht abgebrochen. Die Reihenfolge richtet sich nach dem Eintritt in
die Warteschlange, nicht zwingend nach dem Alter der Commits. Bei Pull Requests
und ungeschützten Refs ersetzt ein neuer Lauf den vorherigen Lauf desselben Refs.
[GitHub-Parallelität und Warteschlangen](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency).
Der Smoke-Test prüft TLS, Anmeldung, CSRF, ausgelieferte Webdateien und
Datenerhalt über einen Dienstneustart. Er läuft als UID 10001 mit
schreibgeschütztem Dateisystem, entfernten Linux-Capabilities und temporären
Testdaten. Eine Proxmox-Installation ist kein Bestandteil dieses Tests.
Der Build verwendet `--provenance=false --sbom=false` und veröffentlicht
keine Provenance- oder SBOM-Attestierungen. Die OCI-Labels für Quellrepository,
Commit und Anwendungsversion bleiben erhalten.
| Artefakt in GitHub Actions | Inhalt | Aufbewahrung |
| --- | --- | --- |
| `python-test-results` | `reports/pytest.xml` als JUnit-Datei, auch nach fehlgeschlagenen Tests, sofern erzeugt | 7 Tage |
| `container-build` | `build.env` nach erfolgreichem Verify-Job | 7 Tage |
| `container-deploy` | `build.env` und `deploy.env` nach erfolgreichem Publish-Job | 30 Tage |
Die Aufbewahrung wird im Workflow mit `retention-days` gesetzt und ist durch
die übergeordneten GitHub-Einstellungen begrenzt. Die JUnit-Datei wird als
Download bereitgestellt; der Workflow installiert keinen zusätzlichen
Testberichtsdienst. Auf der Übersicht des jeweiligen Laufs unter **Artifacts**
das gewünschte Archiv herunterladen und entpacken.
[Workflow-Artefakte](https://docs.github.com/en/actions/tutorials/store-and-share-data),
[Artefakte herunterladen](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/download-workflow-artifacts).
`build.env` enthält den temporären lokalen Image-Tag, Commit und
Anwendungsversion. Dieser `ci-…`-Tag wird nicht veröffentlicht und beim
Jobende lokal entfernt. `deploy.env` enthält ausschließlich die
Deploymentreferenz `PROVISIONER_IMAGE=ghcr.io/netivra/proxmox-ais@sha256:…`.
Die Dateien enthalten keine Zugangsdaten. `deploy.env` wird auf dem Zielserver
nicht automatisch eingelesen; seine Image-Zeile in die dortige `.env` übernehmen.
## GitHub einrichten
Ein eigener CI-Runner ist nicht erforderlich. Alle Jobs verwenden
GitHub-gehostete Linux-amd64-Runner mit `ubuntu-24.04`. Der Workflow richtet
Python 3.13 über `actions/setup-python` und Node.js 24 über `actions/setup-node`
ein. Docker und Buildx werden vom Runner bereitgestellt und vor dem Build
geprüft. Bash, OpenSSL und der OpenSSH-Client werden dort ebenfalls verwendet.
Diese Build-Umgebung ist
unabhängig vom AIS-Host-Runner für die Postinstallation auf Proxmox-Hosts.
[GitHub-gehostete Runner](https://docs.github.com/en/actions/reference/runners/github-hosted-runners).
Im Repository unter **Settings → Actions → General** GitHub Actions aktivieren
und die im Workflow verwendeten Actions von `actions/*` zulassen. Diese sind
im Workflow auf vollständige Commit-SHAs festgelegt. Es werden
keine selbst angelegten Repository-Secrets oder CI-Variablen benötigt.
Der Workflow verwendet standardmäßig `contents: read`; nur
`container-publish` erhält zusätzlich `packages: write` und verwendet das
automatisch bereitgestellte `GITHUB_TOKEN` für die Anmeldung an `ghcr.io`.
[Actions-Einstellungen](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository).
GHCR-Pakete sind bei der ersten Veröffentlichung standardmäßig privat. Ein
bereits existierendes Paket muss mit diesem Repository verbunden sein und
dessen Actions Schreibzugriff haben. In den Paketeinstellungen unter
**Manage Actions access** gegebenenfalls `Netivra/Proxmox-AIS` mit Schreibrecht
hinzufügen. Dort auch die gewünschte Paketsichtbarkeit festlegen.
[GHCR und GITHUB_TOKEN](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-in-a-github-actions-workflow),
[Paketzugriff und Sichtbarkeit](https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility).
## Standardbranch und Release-Tags schützen
**Vor der ersten Veröffentlichung müssen aktive Schutzregeln für die
veröffentlichten Refs eingerichtet sein.** Der Workflow und das Skript
verlangen `github.ref_protected == true`. GitHub setzt dieses Merkmal, wenn
Branchschutz oder ein passendes Ruleset für den auslösenden Ref konfiguriert
ist. Ohne Schutz wird nur geprüft.
[GitHub-Ref-Kontext](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context).
Unter den Repository-Einstellungen für Rulesets zwei Regeln anlegen und
jeweils **Enforcement status: Active** wählen:
| Ruleset | Ziel | Vorgesehene Regeln |
| --- | --- | --- |
| Standardbranch | **Include default branch**, etwa `main` | Pull Request und Review verlangen; Löschen und Force-Push verhindern; erfolgreiche Statuschecks verlangen |
| Releases | Tags mit dem Muster `v*` | Erstellung, Aktualisierung und Löschung beschränken; nur Release-Verantwortliche in die Bypass-Liste aufnehmen |
Für Pull Requests die Statuschecks `python-tests`, `javascript-check`,
`container-policy` und `container-verify` nach ihrem ersten Lauf als
verpflichtend auswählen. `container-publish` ist bei Pull Requests absichtlich
übersprungen und wird deshalb nicht als Pflichtprüfung eingerichtet.
Das Tag-Ruleset schützt alle `v*`-Tags; die engere Versionsprüfung übernimmt
zusätzlich das CI-Skript. Release-Verantwortliche setzen Tags auf einen
geprüften Commit mit der passenden Projektversion. Ein Tag auf einem Commit
des geschützten Standardbranches ist selbst noch kein geschützter Tag.
[Rulesets anlegen](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository),
[Verfügbare Schutzregeln](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets).
Die Regeln und Paketberechtigungen im GitHub-Projekt sind Teil der Einrichtung;
sie werden durch den Commit der Workflow-Datei nicht automatisch angelegt.
Welche Rulesets verfügbar sind, hängt von Repository-Sichtbarkeit und
GitHub-Tarif ab. Die CI-Abfragen ersetzen keine restriktiven Schreibrechte
auf Repository und Paket; Änderungen an Workflow und CI-Skripten im Review prüfen.
[Ruleset-Verfügbarkeit](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository).
## Erster Lauf und Release
Nach dem Push der Workflow-Datei unter **Actions** den Lauf öffnen.
Python- und JavaScript-Prüfung sowie der passende Container-Job müssen
erfolgreich sein. Für einen geschützten Standardbranch zusätzlich prüfen,
dass das GHCR-Paket `sha-<Commit>` und `edge` enthält und das Artefakt
`container-deploy` mit `deploy.env` bereitsteht. Ein lokaler Test ersetzt
diesen ersten GitHub-Lauf samt GHCR-Anmeldung und Push nicht.
Für ein Release zuerst `[project].version` auf die gewünschte Version setzen
und den geprüften Commit übernehmen. Anschließend durch ein für das
Release-Ruleset berechtigtes Konto den passenden Tag erstellen und pushen.
Im vorhandenen Checkout heißt der GitHub-Remote `GH-AIS`; in anderen Checkouts
den Namen mit `git remote -v` prüfen und im Befehl entsprechend ersetzen.
Für Projektversion `0.1.0`, sofern der Tag noch nicht existiert:
```bash
git tag -a v0.1.0 -m "Release 0.1.0"
git push GH-AIS v0.1.0
```
Der Release-Lauf muss zusätzlich den Image-Tag `0.1.0` erzeugen. Für einen
manuellen Branch-Lauf unter **Actions** den Workflow und **Run workflow**
wählen. Dafür muss die Workflow-Datei bereits auf dem Standardbranch liegen.
Ein manueller Lauf veröffentlicht nur, wenn dieselben Ref-Schutz- und
Versionsbedingungen erfüllt sind.
[Workflow manuell starten](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow).
Branch-Push und Pull Request können für denselben Quellstand getrennte Läufe
erzeugen. Pull Requests werden im Merge-Kontext geprüft; ihre Tests führen
keinen Registry-Push aus. Die Jobs laden keine produktiven Deployment-Zugänge.
## Fehlersuche und Deployment
Bei übersprungenem Publish-Job zuerst Ereignis, Standardbranch, Tagformat und
aktive Schutzregeln prüfen. Bei einer Versionsabweichung müssen Git-Tag und
`pyproject.toml` aufeinander abgestimmt werden. Bei `denied` oder `unauthorized`
im Publish-Job die Organisationsrichtlinien, `packages: write` und den
Actions-Zugriff auf das GHCR-Paket kontrollieren. Fehlt `container-deploy`,
die Logs des Build-, Smoke- und Push-Schritts prüfen; vor erfolgreicher
Veröffentlichung steht kein Deployment-Digest bereit.
Zum Deployment [compose.yaml](../compose.yaml) und
[config/deployment.env.example](../config/deployment.env.example) auf den
Zielserver kopieren und die Image-Zeile aus `deploy.env` verwenden.
Die [Deployment-Anleitung](deployment.md) beschreibt GHCR-Anmeldung,
Erstinitialisierung, HTTPS-Prüfung, Updates und Rollback. Für private Pakete
benötigt das Deploymentkonto einen Personal Access Token (classic) mit
`read:packages`; öffentliche Pakete lassen sich anonym laden.
Die aktuelle Anleitung steht unter [Gitea Actions und Container Registry](gitea-actions.md).
Der Workflow liegt in [.gitea/workflows/ci.yml](../.gitea/workflows/ci.yml).
+1 -1
View File
@@ -1,6 +1,6 @@
# Betrieb und Wiederherstellung
Für die Installation aus der GitHub Container Registry (GHCR) siehe
Für die Installation aus der Gitea Container Registry siehe
[Deployment-Anleitung](deployment.md). Alle `docker compose`-Befehle unten
werden im Deployment-Verzeichnis mit `compose.yaml` und `.env` ausgeführt.
`PROVISIONER_IMAGE` verweist auf das veröffentlichte Image, vorzugsweise per Digest.
+23 -4
View File
@@ -1,11 +1,30 @@
# Softwaretestbericht
Stand: 13. September 2026. Die hier dokumentierten Softwaretests führen keine
Die hier dokumentierten Softwaretests führen keine
Proxmox-Installation aus und verändern keine Hostdatenträger, Paketquellen oder
Produktivdienste. Die noch offenen praktischen Prüfungen stehen in der
[Kompatibilitätsmatrix](compatibility.md).
## Automatisierte Prüfungen
## Gitea-Migration: 14. September 2026
Der aktuelle [Workflow](../.gitea/workflows/ci.yml) und die
[Einrichtungsanleitung](gitea-actions.md) verwenden Gitea Actions und die
Gitea Container Registry. Die Prüfung unter Windows mit Python 3.13.15 ergab:
- 120 CI-Skripttests bestanden: Veröffentlichungs-Opt-in, Ereignis- und Ref-Regeln,
Versionsabgleich, Registry-Adressen und Paket-Token sowie Build-, Smoke-,
Push- und Aufräumfehler. Der konstante Gitea-Wert `ref_protected=false`
verhindert bei gesetztem Opt-in keine Veröffentlichung mehr.
- 47 Anwendungstests bestanden; zwei Tests zur nativen Linux-Prozessüberwachung
wurden unter Windows übersprungen.
- Workflow-YAML, Shell-Syntax und JavaScript-Syntax wurden erfolgreich geprüft.
Ein neuer Linux-Containerbuild und ein vollständiger Gitea-Lauf einschließlich
Registry-Push sind noch offen. Der vorhandene Gitea-Lauf wartet auf einen
Online-Runner mit passendem Label; die Repository-Runnerliste ist leer.
Die folgenden Ergebnisse dokumentieren den Stand vor dieser Migration.
## Automatisierte Prüfungen: 13. September 2026
Die 49 Anwendungstests wurden unter Windows mit Python 3.14 ausgeführt:
47 Tests bestanden; zwei Linux-spezifische Prozesstests wurden dort übersprungen.
@@ -86,9 +105,9 @@ TLS-Reverse-Proxy, reale Provisionierungsnetze, Medienboot, Initialisierung und
Wiederherstellung in der späteren Betriebsumgebung müssen zusätzlich geprüft werden.
Ein erfolgreicher Image-Build allein ist keine Betriebsfreigabe.
## GitHub Actions und GHCR
## GitHub Actions und GHCR: historischer Stand
Der [Workflow](../.github/workflows/ci.yml) verwendet GitHub-gehostete
Der damalige Workflow verwendete GitHub-gehostete
Ubuntu-24.04-Runner, Python 3.13 und Node.js 24. Python- und JavaScript-Prüfung
müssen erfolgreich sein, bevor das Containerimage gebaut und getestet wird.
Die [Einrichtungsanleitung](github-actions.md) beschreibt Schutzregeln,
+117 -15
View File
@@ -12,7 +12,8 @@ import pytest
ROOT = Path(__file__).resolve().parents[1]
VERSION = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
SHA = "1234567890abcdef1234567890abcdef12345678"
REGISTRY_IMAGE = "ghcr.io/team/proxmox-ais"
REGISTRY_HOST = "gitlab.bartelluis.de"
REGISTRY_IMAGE = f"{REGISTRY_HOST}/team/proxmox-ais"
DIGEST = f"{REGISTRY_IMAGE}@sha256:{'a' * 64}"
PASSWORD = "dummy-ci-token-$()-do-not-print"
@@ -37,6 +38,7 @@ def container_ci(tmp_path):
(tmp_path / "bin").mkdir()
(tmp_path / "tmp").mkdir()
shutil.copyfile(ROOT / "ci/container.sh", tmp_path / "ci/container.sh")
shutil.copyfile(ROOT / "ci/publish-policy.sh", tmp_path / "ci/publish-policy.sh")
shutil.copyfile(ROOT / "pyproject.toml", tmp_path / "pyproject.toml")
smoke_source = (ROOT / "tests/container_smoke.py").read_bytes()
(tmp_path / "tests/container_smoke.py").write_bytes(smoke_source)
@@ -78,14 +80,17 @@ esac
"GITHUB_RUN_ATTEMPT": "1",
"GITHUB_JOB": "container_publish",
"GITHUB_REPOSITORY": "Team/Proxmox-AIS",
"GITHUB_SERVER_URL": "https://github.com",
"GITHUB_SERVER_URL": "https://gitlab.bartelluis.de",
"GITHUB_EVENT_NAME": "push",
"GITHUB_REF_PROTECTED": "true",
"GITHUB_REF_PROTECTED": "false",
"GITHUB_REF_TYPE": "branch",
"GITHUB_REF_NAME": "main",
"DEFAULT_BRANCH": "main",
"PUBLISH_IMAGES": "true",
"GITHUB_ACTOR": "ci-user",
"GHCR_TOKEN": PASSWORD,
"REGISTRY_HOST": REGISTRY_HOST,
"REGISTRY_USERNAME": "registry-owner",
"REGISTRY_TOKEN": PASSWORD,
"MOCK_DOCKER_LOG": (tmp_path / "docker.log").as_posix(),
"MOCK_SMOKE_STDIN": (tmp_path / "smoke.stdin").as_posix(),
"MOCK_LOGIN_STDIN": (tmp_path / "login.stdin").as_posix(),
@@ -123,7 +128,7 @@ def test_default_branch_publishes_only_after_hardened_image_smoke(container_ci):
assert build[build.index("--platform") + 1] == "linux/amd64"
assert "--provenance=false" in build and "--sbom=false" in build
assert f"org.opencontainers.image.revision={SHA}" in build
assert "org.opencontainers.image.source=https://github.com/Team/Proxmox-AIS" in build
assert "org.opencontainers.image.source=https://gitlab.bartelluis.de/Team/Proxmox-AIS" in build
assert build[build.index("--tag") + 1] == build_image
smoke = outcome.calls[1]
for option in ["--read-only", "--cap-drop", "ALL", "no-new-privileges:true", "/tmp:rw,noexec,nosuid,size=128m"]:
@@ -138,7 +143,7 @@ def test_default_branch_publishes_only_after_hardened_image_smoke(container_ci):
]
assert (outcome.root / "deploy.env").read_text() == f"PROVISIONER_IMAGE={DIGEST}\n"
assert "ci-210-1-container_publish" in (outcome.root / "build.env").read_text()
assert outcome.calls[2] == ["login", "ghcr.io", "--username", "ci-user", "--password-stdin"]
assert outcome.calls[2] == ["login", REGISTRY_HOST, "--username", "registry-owner", "--password-stdin"]
assert (outcome.root / "login.stdin").read_text() == PASSWORD
assert PASSWORD not in outcome.result.stdout + outcome.result.stderr + (outcome.root / "docker.log").read_text()
config_path = Path((outcome.root / "docker-config.path").read_text())
@@ -155,8 +160,8 @@ def test_release_tag_matches_project_version_and_does_not_move_edge(container_ci
def test_pull_request_verifies_without_registry_credentials(container_ci):
outcome = container_ci(
"verify", GITHUB_EVENT_NAME="pull_request", GITHUB_REF_NAME="1/merge",
GITHUB_REF_PROTECTED="false", GITHUB_ACTOR=None, GHCR_TOKEN=None,
"verify", GITHUB_EVENT_NAME="pull_request", GITHUB_REF_NAME="1/head",
PUBLISH_IMAGES=None, GITHUB_ACTOR=None, REGISTRY_USERNAME=None, REGISTRY_TOKEN=None,
)
assert outcome.result.returncode == 0, outcome.result.stderr
assert [call[0] for call in outcome.calls] == ["build", "run", "rm", "image"]
@@ -166,14 +171,49 @@ def test_pull_request_verifies_without_registry_credentials(container_ci):
assert not (outcome.root / "deploy.env").exists()
@pytest.mark.parametrize("protected", ["false", None])
def test_gitea_publication_opt_in_does_not_depend_on_ref_protected(container_ci, protected):
outcome = container_ci(GITHUB_REF_PROTECTED=protected, GITHUB_ACTOR=None)
assert outcome.result.returncode == 0, outcome.result.stderr
assert outcome.calls[2] == ["login", REGISTRY_HOST, "--username", "registry-owner", "--password-stdin"]
assert (outcome.root / "deploy.env").is_file()
@pytest.mark.parametrize("registry_host", [
None, "", "https://registry.example.test", "registry.example.test/images",
"registry.example.test\nother.example.test", "Registry.example.test",
"registry.example.test:invalid", "registry.example.test:",
"-registry.example.test", "user@registry.example.test",
])
def test_invalid_registry_host_fails_before_build(container_ci, registry_host):
outcome = container_ci(REGISTRY_HOST=registry_host)
assert outcome.result.returncode != 0
assert not outcome.calls
assert not (outcome.root / "deploy.env").exists()
def test_registry_port_is_used_for_login_tags_and_deployment_digest(container_ci):
registry_host = "registry.example.test:5000"
registry_image = f"{registry_host}/team/proxmox-ais"
digest = f"{registry_image}@sha256:{'b' * 64}"
outcome = container_ci(REGISTRY_HOST=registry_host, MOCK_REPO_DIGESTS=digest)
assert outcome.result.returncode == 0, outcome.result.stderr
assert outcome.calls[2] == ["login", registry_host, "--username", "registry-owner", "--password-stdin"]
assert [call[1] for call in outcome.calls if call[0] == "push"] == [
f"{registry_image}:sha-{SHA}", f"{registry_image}:edge"
]
assert (outcome.root / "deploy.env").read_text() == f"PROVISIONER_IMAGE={digest}\n"
@pytest.mark.parametrize("overrides", [
{"GITHUB_REF_NAME": "feature/ci"},
{"GITHUB_REF_NAME": f"v{VERSION}"},
{"GITHUB_REF_NAME": None},
{"DEFAULT_BRANCH": ""},
{"GITHUB_REF_PROTECTED": "false"},
{"GITHUB_REF_PROTECTED": None},
{"GITHUB_REF_PROTECTED": "TRUE"},
{"PUBLISH_IMAGES": "false"},
{"PUBLISH_IMAGES": None},
{"PUBLISH_IMAGES": "TRUE"},
{"PUBLISH_IMAGES": "1"},
{"GITHUB_EVENT_NAME": "pull_request"},
{"GITHUB_EVENT_NAME": "pull_request_target"},
{"GITHUB_EVENT_NAME": "workflow_run"},
@@ -191,9 +231,9 @@ def test_pull_request_verifies_without_registry_credentials(container_ci):
{"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v0.1.00"},
{"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v0.1"},
{"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v0.1.0.0"},
{"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": f"v{VERSION}", "GITHUB_REF_PROTECTED": "false"},
{"GHCR_TOKEN": None},
{"GITHUB_ACTOR": None},
{"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": f"v{VERSION}", "PUBLISH_IMAGES": "false"},
{"REGISTRY_TOKEN": None},
{"REGISTRY_USERNAME": None},
{"GITHUB_SHA": "1234"},
{"GITHUB_RUN_ID": "invalid"},
{"GITHUB_RUN_ATTEMPT": "../2"},
@@ -214,7 +254,7 @@ def test_unauthorized_or_invalid_publication_fails_before_build(container_ci, ov
@pytest.mark.parametrize("ref_type, ref_name", [("branch", "main"), ("tag", f"v{VERSION}")])
def test_manual_run_can_publish_protected_default_branch_or_release(container_ci, ref_type, ref_name):
def test_manual_run_can_publish_opted_in_default_branch_or_release(container_ci, ref_type, ref_name):
outcome = container_ci(
GITHUB_EVENT_NAME="workflow_dispatch", GITHUB_REF_TYPE=ref_type, GITHUB_REF_NAME=ref_name,
)
@@ -269,6 +309,68 @@ def test_missing_or_invalid_registry_digest_fails_without_artifact(container_ci,
assert not (outcome.root / "deploy.env").exists()
@pytest.mark.parametrize("overrides, expected", [
({}, "true"),
({"GITHUB_EVENT_NAME": "workflow_dispatch"}, "true"),
({"GITHUB_REF_NAME": "trunk", "DEFAULT_BRANCH": "trunk"}, "true"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v0.0.0"}, "true"),
({"GITHUB_EVENT_NAME": "workflow_dispatch", "GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v12.34.56"}, "true"),
({"GITHUB_REF_PROTECTED": None}, "true"),
({"PUBLISH_IMAGES": None}, "false"),
({"PUBLISH_IMAGES": ""}, "false"),
({"PUBLISH_IMAGES": "false"}, "false"),
({"PUBLISH_IMAGES": "TRUE"}, "false"),
({"PUBLISH_IMAGES": "1"}, "false"),
({"PUBLISH_IMAGES": "true\n"}, "false"),
({"PUBLISH_IMAGES": "false", "GITHUB_REF_PROTECTED": "true"}, "false"),
({"GITHUB_EVENT_NAME": None}, "false"),
({"GITHUB_EVENT_NAME": "pull_request"}, "false"),
({"GITHUB_EVENT_NAME": "pull_request_target"}, "false"),
({"GITHUB_EVENT_NAME": "workflow_run"}, "false"),
({"GITHUB_EVENT_NAME": "schedule"}, "false"),
({"GITHUB_REF_TYPE": None}, "false"),
({"GITHUB_REF_TYPE": "pull_request"}, "false"),
({"GITHUB_REF_NAME": "feature/ci"}, "false"),
({"GITHUB_REF_NAME": None}, "false"),
({"DEFAULT_BRANCH": None}, "false"),
({"DEFAULT_BRANCH": "", "GITHUB_REF_NAME": ""}, "false"),
({"GITHUB_REF_NAME": "main;touch injected"}, "false"),
({"GITHUB_REF_NAME": "$(touch injected)"}, "false"),
({"GITHUB_REF_NAME": "`touch injected`"}, "false"),
({"GITHUB_REF_NAME": "release/$(touch${IFS}injected)", "DEFAULT_BRANCH": "release/$(touch${IFS}injected)"}, "true"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "main"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v01.2.3"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v1.02.3"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v1.2.03"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v1.2"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v1.2.3.4"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v1.2.3-rc1"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v1.2.3+build"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v1.2.3\nother"}, "false"),
({"GITHUB_REF_TYPE": "tag", "GITHUB_REF_NAME": "v1.2.3$(touch injected)"}, "false"),
])
def test_publication_policy_matrix(tmp_path, overrides, expected):
shutil.copyfile(ROOT / "ci/publish-policy.sh", tmp_path / "publish-policy.sh")
environment = {
**os.environ,
"PUBLISH_IMAGES": "true",
"GITHUB_EVENT_NAME": "push",
"GITHUB_REF_TYPE": "branch",
"GITHUB_REF_NAME": "main",
"DEFAULT_BRANCH": "main",
"GITHUB_REF_PROTECTED": "false",
**overrides,
}
environment = {name: value for name, value in environment.items() if value is not None}
result = subprocess.run(
[posix_shell(), "publish-policy.sh"], cwd=tmp_path, env=environment,
capture_output=True, text=True, timeout=10,
)
assert result.returncode == 0, result.stderr
assert result.stdout == f"{expected}\n"
assert not (tmp_path / "injected").exists(), "Ref names must never be evaluated as shell code"
@pytest.fixture
def python_ci(tmp_path):
shell = posix_shell()