diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 1f56a9376b..8a449b1743 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -98,6 +98,7 @@ jobs: 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 index 771d95e600..232ad33f0f 100644 --- a/.gitea/workflows/container.yaml +++ b/.gitea/workflows/container.yaml @@ -29,6 +29,7 @@ jobs: 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 dc3a12f5e0..88cfc2efe0 100644 --- a/README.md +++ b/README.md @@ -219,9 +219,19 @@ After changing `.env`, recreate the container; an image rebuild is not required. 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 an account allowed to publish under the repository owner. + 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 @@ -235,12 +245,12 @@ To publish from Gitea: migrations image is used temporarily for the matching database migrations. For this repository the image base is -`gitlab.epicnabbo.nl/remco/epicnext-cms`. Package access is controlled by Gitea. +`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/remco/epicnext-cms \ +CMS_IMAGE_REPOSITORY=gitlab.epicnabbo.nl/simo/epicnext-cms \ CMS_PUBLIC_URL=https://your-hotel.example \ bash scripts/docker-update.sh ``` diff --git a/scripts/publish-container.sh b/scripts/publish-container.sh index 81b0ac3fd4..8e1434fc81 100644 --- a/scripts/publish-container.sh +++ b/scripts/publish-container.sh @@ -11,6 +11,12 @@ 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 diff --git a/src/lib/publish-container.test.ts b/src/lib/publish-container.test.ts index 63a41059b7..8fe25c42bd 100644 --- a/src/lib/publish-container.test.ts +++ b/src/lib/publish-container.test.ts @@ -17,7 +17,7 @@ const bash = .find((path) => existsSync(path)) ?? "bash") : "bash"; const sha = "a".repeat(40); -function simulate(scenario: string) { +function simulate(scenario: string, namespace = "") { const dir = mkdtempSync(join(tmpdir(), "cms-publish-test-")); try { const result = spawnSync( @@ -35,7 +35,8 @@ function simulate(scenario: string) { SCENARIO: scenario, REGISTRY_SERVER: "https://registry.invalid", REGISTRY_REPOSITORY: "owner/cms", - REGISTRY_USER: "fixture", + REGISTRY_USER: "Simo", + REGISTRY_NAMESPACE: namespace, REGISTRY_TOKEN: "fixture-only", }, }, @@ -51,7 +52,7 @@ describe("verified application image reuse", () => { it("reuses only the exact image digest that passed deployment checks", () => { const calls = simulate("verified"); expect(calls).toContain( - `docker tag sha256:candidate registry.invalid/owner/cms:${sha}`, + `docker tag sha256:candidate registry.invalid/simo/cms:${sha}`, ); expect(calls).not.toContain("docker build --network=host --build-arg"); expect( @@ -67,3 +68,13 @@ describe("verified application image reuse", () => { }, ); }); + +it("uses the token account namespace instead of the repository owner", () => { + const calls = simulate("verified"); + expect(calls).toContain(`docker push registry.invalid/simo/cms:${sha}`); + expect(calls).not.toContain("registry.invalid/owner/cms"); +}); +it("supports an explicit organization namespace", () => { + const calls = simulate("verified", "My-Org"); + expect(calls).toContain(`docker push registry.invalid/my-org/cms:${sha}`); +});