diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 08c9ff68..8dbd4586 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -150,26 +150,3 @@ jobs: path: build-reports/ if-no-files-found: warn retention-days: 14 - - # Publish only after checks and the production deployment have succeeded. - # Serial execution also avoids two builds competing on the self-hosted runner. - publish-container: - needs: deploy - if: gitea.event_name == 'push' && (gitea.ref_name == 'main' || gitea.ref_name == 'master') - runs-on: self-hosted - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - repository: ${{ gitea.repository }} - token: ${{ gitea.token }} - - - name: Build, verify portability and publish - shell: bash - env: - REGISTRY_SERVER: ${{ gitea.server_url }} - REGISTRY_REPOSITORY: ${{ gitea.repository }} - REGISTRY_NAMESPACE: ${{ vars.CONTAINER_REGISTRY_NAMESPACE }} - REGISTRY_USER: ${{ secrets.CONTAINER_REGISTRY_USER }} - REGISTRY_TOKEN: ${{ secrets.CONTAINER_REGISTRY_TOKEN }} - run: bash scripts/publish-container.sh diff --git a/.gitea/workflows/container.yaml b/.gitea/workflows/container.yaml deleted file mode 100644 index 232ad33f..00000000 --- a/.gitea/workflows/container.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: Publish portable container - -on: - workflow_dispatch: - -jobs: - publish: - runs-on: self-hosted - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - repository: ${{ gitea.repository }} - token: ${{ gitea.token }} - - name: Install and verify - run: | - node scripts/check-node-toolchain.mjs - pnpm install --frozen-lockfile - pnpm typecheck - pnpm biome:lint - pnpm test:coverage - env: - SKIP_ENV_VALIDATION: 1 - NODE_ENV: test - DATABASE_URL: mysql://test:test@127.0.0.1:9/test - AUTH_SECRET: container-test-secret-at-least-32-characters - - name: Build, verify portability and publish - shell: bash - env: - REGISTRY_SERVER: ${{ gitea.server_url }} - REGISTRY_REPOSITORY: ${{ gitea.repository }} - REGISTRY_NAMESPACE: ${{ vars.CONTAINER_REGISTRY_NAMESPACE }} - REGISTRY_USER: ${{ secrets.CONTAINER_REGISTRY_USER }} - REGISTRY_TOKEN: ${{ secrets.CONTAINER_REGISTRY_TOKEN }} - run: bash scripts/publish-container.sh diff --git a/README.md b/README.md index 80cc8d2e..c786c6a7 100644 --- a/README.md +++ b/README.md @@ -250,9 +250,7 @@ bash cms update The updater pulls the configured Git upstream, loads `.docker-install`, uses the matching application/migration images and verifies the running release locally -and at the saved public URL. If publication for the new commit is still running, -it stops before replacing the current container; run the same command after CI -succeeds. Existing application rollback remains available on a failed cutover; +and at the saved public URL. Existing application rollback remains available on a failed cutover; database migrations are not reversed. `bash cms install --configure-only` saves configuration without preparing runtime @@ -261,67 +259,7 @@ committed or sent in Docker build contexts. Existing users of `bash scripts/docker-update.sh` retain the previous behavior when no wizard profile exists; explicit `CMS_IMAGE_REPOSITORY`/`CMS_PUBLIC_URL` overrides still work. -To publish from Gitea: - -Gitea packages belong to an account or organization, independently of repository -permissions. Publication defaults to the lowercase `CONTAINER_REGISTRY_USER` -namespace, so a Simo token publishes `simo/epicnext-cms` even though the Git -repository belongs to remco. Set the Actions variable -`CONTAINER_REGISTRY_NAMESPACE` only to override this (for example, an organization -where the token account has package write access). Keep the login username and -token from the same account. Changing namespace also changes the image URL used -by installations; existing remco image tags are not moved automatically. - -1. In the repository's Actions secrets, configure `CONTAINER_REGISTRY_USER` and - `CONTAINER_REGISTRY_TOKEN`. Use a Gitea access token with package read/write - permission belonging to the login account. For the Simo token, set - `CONTAINER_REGISTRY_USER=Simo`; the default package namespace will be `simo`. -2. Every push to `main` or `master` automatically builds and publishes the images - after the CI checks and production deployment succeed. Pull requests do not - publish images. The publication job builds from committed source only and checks - the same application image with two runtime configurations before pushing. - Missing registry secrets fail the publication job explicitly; they do not undo - an already successful production deployment. No `latest` tag is moved. - **Publish portable container** remains available for manual retries on the - commit/branch to distribute, without redeploying production. -3. The images are `//:` and - `:-migrations`. Only the application image runs the website; the - migrations image is used temporarily for the matching database migrations. - -Publication uses checksum-pinned regctl v0.11.6 with 8 MiB blob requests to -avoid monolithic layer uploads exceeding reverse-proxy limits. Both images are -exported and uploaded sequentially; temporary archives and credentials are removed -on exit. The runner needs curl, sha256sum and temporary disk space for one Docker -image archive plus its extracted OCI layout. The remote image config digest is checked against the locally normalized archive -after each upload. A proxy must still allow the OCI registry PATCH/PUT endpoints. - -For this repository the image base is -`gitlab.epicnabbo.nl/simo/epicnext-cms`. Package access is controlled by Gitea. -For private packages, run `docker login gitlab.epicnabbo.nl` on the installation -with a token that can read packages. Then update with: - -```bash -CMS_IMAGE_REPOSITORY=gitlab.epicnabbo.nl/simo/epicnext-cms \ -CMS_PUBLIC_URL=https://your-hotel.example \ -bash scripts/docker-update.sh -``` - -The updater pulls the configured Git upstream and requires both images for that -exact commit. A missing image or failed login stops before replacing the running -CMS. Local builds remain the default when `CMS_IMAGE_REPOSITORY` is unset. Both -paths retain the existing image/HTTP checks and automatic application rollback. -Migration secrets are mounted read-only for the temporary migration container; -they are never copied into its image. Registry images currently target the Linux -architecture of the self-hosted build runner; this is not a multi-architecture release. - -The portability gate checks release identity, runtime avatar/badge routing and -absence of installation environment files in the application image. It uses an -unreachable fixture database and does not replace a full live database/site smoke -test. See `scripts/verify-portable-image.mjs`. Production deployment remains verified -separately by the existing CI workflow. - -References: [Gitea container registry](https://docs.gitea.com/usage/packages/container/) -and [Next.js runtime environment variables](https://nextjs.org/docs/app/guides/self-hosting). +Container publication is disabled. CI builds, checks and deploys the CMS, but it does not log in to a registry or upload container images. ### Diagnose an update that is not visible diff --git a/scripts/publish-container.sh b/scripts/publish-container.sh deleted file mode 100644 index f8f6ff12..00000000 --- a/scripts/publish-container.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail -: "${REGISTRY_SERVER:?Missing Gitea server URL}" -: "${REGISTRY_REPOSITORY:?Missing owner/repository}" -: "${REGISTRY_USER:?Configure CONTAINER_REGISTRY_USER}" -: "${REGISTRY_TOKEN:?Configure CONTAINER_REGISTRY_TOKEN with package write access}" -sha="$(git rev-parse HEAD)" -[[ "$sha" =~ ^[0-9a-f]{40}$ ]] || exit 1 -registry="${REGISTRY_SERVER#https://}" -registry="${registry%/}" -[[ "$REGISTRY_SERVER" = https://* && "$registry" != */* ]] || { echo "Registry must use HTTPS at the Gitea server root" >&2; exit 1; } -repository="${REGISTRY_REPOSITORY,,}" -[[ "$repository" =~ ^[a-z0-9._-]+/[a-z0-9._-]+$ ]] || exit 1 -# Gitea packages belong to a user/organization, independently of repository ACLs. -# A collaborator token cannot publish to another user's personal namespace. -namespace="${REGISTRY_NAMESPACE:-$REGISTRY_USER}" -namespace="${namespace,,}" -[[ "$namespace" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || { echo "Invalid registry namespace; use a Gitea username or organization" >&2; exit 1; } -repository="$namespace/${repository#*/}" -image="$registry/$repository:$sha" -# Isolate credentials from the self-hosted runner's normal Docker configuration. -export DOCKER_CONFIG -DOCKER_CONFIG="$(mktemp -d)" -context="$(mktemp -d)" -trap 'rm -rf -- "$DOCKER_CONFIG" "$context"' EXIT -# Build only the committed source, never untracked files from a shared runner. -git archive HEAD | tar -x -C "$context" -printf '%s' "$REGISTRY_TOKEN" | docker login "$registry" --username "$REGISTRY_USER" --password-stdin -unset REGISTRY_TOKEN -# On the shared runner, publish the exact image already verified by deployment. -local_image="epicnext-cms:$sha" -local_revision="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$local_image" 2>/dev/null || true)" -local_id="$(docker image inspect --format '{{.Id}}' "$local_image" 2>/dev/null || true)" -verified_id="$(docker image inspect --format '{{.Id}}' "epicnext-cms:verified-$sha" 2>/dev/null || true)" -if [[ "$local_revision" = "$sha" && -n "$local_id" && "$local_id" = "$verified_id" ]]; then - docker tag "$verified_id" "$image" - echo "Reusing verified release image $local_image" -else - docker build --network=host --build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" "$context" -fi -docker build --network=host --target migrations -t "$image-migrations" "$context" -node scripts/verify-portable-image.mjs "$image" "$sha" -# Publish only after the same application image passed both runtime configurations. -# Bound each blob request below reverse-proxy upload limits. Pin the uploader -# and verify its checksum before giving it access to the temporary Docker login. -case "$(uname -m)" in - x86_64) arch=amd64; checksum=8e0e62a497fcdb8048d18aa927a139613176ba0531f412bc541044e28f9856bd ;; - aarch64|arm64) arch=arm64; checksum=a9b71a3ee79b2d1dbbd7d51fd5e8fa214722c192864235d3d8764463c751a1ff ;; - *) echo "Unsupported registry uploader architecture" >&2; exit 1 ;; -esac -curl --fail --silent --show-error --location --retry 3 --connect-timeout 15 --max-time 120 \ - "https://github.com/regclient/regclient/releases/download/v0.11.6/regctl-linux-$arch" -o "$context/regctl" -printf '%s %s\n' "$checksum" "$context/regctl" | sha256sum --check --status -chmod 700 "$context/regctl" -export REGCTL_CONFIG="$DOCKER_CONFIG/regctl.json" -regctl() { "$context/regctl" "$@"; } -regctl registry set "$registry" --blob-chunk 8388608 --blob-max 8388608 -for target in "$image-migrations" "$image"; do - echo "Publishing $target with blob requests up to 8 MiB" - docker image save --output "$context/image.tar" "$target" - # Normalize the saved archive locally before copying it unchanged to Gitea. - # Docker engine IDs and OCI index IDs are not interchangeable with config IDs. - local_ref="ocidir://$context/oci:verified" - regctl image import "$local_ref" "$context/image.tar" - expected_config="$(regctl manifest get "$local_ref" --platform "linux/$arch" --format '{{.GetConfig.Digest}}')" - [[ "$expected_config" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "Invalid local image config digest" >&2; exit 1; } - regctl image copy "$local_ref" "$target" - remote_config="$(regctl manifest get "$target" --platform "linux/$arch" --format '{{.GetConfig.Digest}}')" - [[ "$remote_config" = "$expected_config" ]] || { echo "Published image config does not match verified local image" >&2; exit 1; } - rm -f -- "$context/image.tar" - rm -rf -- "$context/oci" -done -echo "Published application and migrations: $image"