From 9c528c6eca35ef632891ab4eeb605964af3cb5be Mon Sep 17 00:00:00 2001 From: BartelLuis Date: Mon, 14 Sep 2026 20:23:20 +0200 Subject: [PATCH] ci: migrate workflows to Gitea Actions --- {.github => .gitea}/workflows/ci.yml | 66 ++++----- README.md | 39 ++--- ci/container.sh | 22 ++- ci/publish-policy.sh | 30 ++++ ci/python-tests.sh | 3 +- compose.yaml | 2 +- config/deployment.env.example | 6 +- docs/deployment.md | 31 ++-- docs/gitea-actions.md | 210 +++++++++++++++++++++++++++ docs/github-actions.md | 205 +------------------------- docs/operations.md | 2 +- docs/test-report.md | 27 +++- tests/test_ci.py | 132 +++++++++++++++-- 13 files changed, 472 insertions(+), 303 deletions(-) rename {.github => .gitea}/workflows/ci.yml (53%) create mode 100644 ci/publish-policy.sh create mode 100644 docs/gitea-actions.md diff --git a/.github/workflows/ci.yml b/.gitea/workflows/ci.yml similarity index 53% rename from .github/workflows/ci.yml rename to .gitea/workflows/ci.yml index ffe94d6..faf7a4d 100644 --- a/.github/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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: | diff --git a/README.md b/README.md index 62b8c2d..b682feb 100644 --- a/README.md +++ b/README.md @@ -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-` 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-` 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). diff --git a/ci/container.sh b/ci/container.sh index 69f3b6d..4f84c1e 100644 --- a/ci/container.sh +++ b/ci/container.sh @@ -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}" diff --git a/ci/publish-policy.sh b/ci/publish-policy.sh new file mode 100644 index 0000000..ec1634f --- /dev/null +++ b/ci/publish-policy.sh @@ -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" diff --git a/ci/python-tests.sh b/ci/python-tests.sh index 8d0ea89..4e99293 100644 --- a/ci/python-tests.sh +++ b/ci/python-tests.sh @@ -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 diff --git a/compose.yaml b/compose.yaml index 0c37867..795110e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -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: diff --git a/config/deployment.env.example b/config/deployment.env.example index 6f9def8..55e9dca 100644 --- a/config/deployment.env.example +++ b/config/deployment.env.example @@ -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 diff --git a/docs/deployment.md b/docs/deployment.md index a226987..7e54c65 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -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: diff --git a/docs/gitea-actions.md b/docs/gitea-actions.md new file mode 100644 index 0000000..1083a80 --- /dev/null +++ b/docs/gitea-actions.md @@ -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 `/`, 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-` und `edge` | +| Tag `vX.Y.Z`, Push oder manueller Start, Veröffentlichung aktiviert | `container-publish` | `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-` 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. diff --git a/docs/github-actions.md b/docs/github-actions.md index fa731d1..8356ea7 100644 --- a/docs/github-actions.md +++ b/docs/github-actions.md @@ -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-` und `edge` | -| Geschützter Tag `vX.Y.Z` bei Push oder manuellem Start | `container-publish` | `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-` 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). diff --git a/docs/operations.md b/docs/operations.md index 585a0e1..39f652c 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -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. diff --git a/docs/test-report.md b/docs/test-report.md index d3f2971..a567860 100644 --- a/docs/test-report.md +++ b/docs/test-report.md @@ -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, diff --git a/tests/test_ci.py b/tests/test_ci.py index ad8cfe3..b528c98 100644 --- a/tests/test_ci.py +++ b/tests/test_ci.py @@ -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()