From c414f04ac423e3059bbc4f7c0cfed0577c33f9df Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Mon, 14 Sep 2026 13:48:28 +0200 Subject: [PATCH 1/3] ci: push images to the owner's namespace, not the login user Registry image names were derived from the login user, which breaks Quay.io robot accounts (org+name) and can push Docker Hub images to the wrong namespace. Use the repository owner as the default namespace, with optional DOCKER_NAMESPACE and QUAY_NAMESPACE overrides. Reuse the computed namespace for both login and image tagging, and keep registry tokens out of the workflow environment. Fixes: #211 Signed-off-by: Josef Schlehofer --- .github/workflows/containers.yml | 47 ++++++++++++++++---------------- README.md | 18 ++++-------- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index b14e935..e6d24e6 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -3,10 +3,9 @@ run-name: ${{ inputs.ref }} ${{ inputs.target }} env: FILE_HOST: https://mirror-03.infra.openwrt.org - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} - QUAY_USER: ${{ secrets.QUAY_USER }} - QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} + # The login user is not the image namespace, e.g. Quay.io robot accounts + DOCKER_NAMESPACE: ${{ secrets.DOCKER_USER != '' && secrets.DOCKER_TOKEN != '' && (vars.DOCKER_NAMESPACE || github.repository_owner) || '' }} + QUAY_NAMESPACE: ${{ secrets.QUAY_USER != '' && secrets.QUAY_TOKEN != '' && (vars.QUAY_NAMESPACE || github.repository_owner) || '' }} on: # push: @@ -196,14 +195,14 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker.io Container Registry - if: github.event_name != 'pull_request' && env.DOCKER_USER != '' && env.DOCKER_TOKEN != '' + if: github.event_name != 'pull_request' && env.DOCKER_NAMESPACE != '' uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_TOKEN }} - name: Login to Quay.io Container Registry - if: github.event_name != 'pull_request' && env.QUAY_USER != '' && env.QUAY_TOKEN != '' + if: github.event_name != 'pull_request' && env.QUAY_NAMESPACE != '' uses: docker/login-action@v4 with: registry: quay.io @@ -216,8 +215,8 @@ jobs: with: images: | ghcr.io/${{ github.repository_owner }}/imagebuilder - ${{ env.DOCKER_USER != '' && format('docker.io/{0}/imagebuilder', env.DOCKER_USER) || '' }} - ${{ env.QUAY_USER != '' && format('quay.io/{0}/imagebuilder', env.QUAY_USER) || '' }} + ${{ env.DOCKER_NAMESPACE != '' && format('docker.io/{0}/imagebuilder', env.DOCKER_NAMESPACE) || '' }} + ${{ env.QUAY_NAMESPACE != '' && format('quay.io/{0}/imagebuilder', env.QUAY_NAMESPACE) || '' }} flavor: | latest=false prefix=${{ github.event.inputs.prefix }} @@ -288,14 +287,14 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker.io Container Registry - if: github.event_name != 'pull_request' && env.DOCKER_USER != '' && env.DOCKER_TOKEN != '' + if: github.event_name != 'pull_request' && env.DOCKER_NAMESPACE != '' uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_TOKEN }} - name: Login to Quay.io Container Registry - if: github.event_name != 'pull_request' && env.QUAY_USER != '' && env.QUAY_TOKEN != '' + if: github.event_name != 'pull_request' && env.QUAY_NAMESPACE != '' uses: docker/login-action@v4 with: registry: quay.io @@ -308,8 +307,8 @@ jobs: with: images: | ghcr.io/${{ github.repository_owner }}/sdk - ${{ env.DOCKER_USER != '' && format('docker.io/{0}/sdk', env.DOCKER_USER) || '' }} - ${{ env.QUAY_USER != '' && format('quay.io/{0}/sdk', env.QUAY_USER) || '' }} + ${{ env.DOCKER_NAMESPACE != '' && format('docker.io/{0}/sdk', env.DOCKER_NAMESPACE) || '' }} + ${{ env.QUAY_NAMESPACE != '' && format('quay.io/{0}/sdk', env.QUAY_NAMESPACE) || '' }} flavor: | latest=false prefix=${{ github.event.inputs.prefix }} @@ -322,8 +321,8 @@ jobs: with: images: | ghcr.io/${{ github.repository_owner }}/sdk - ${{ env.DOCKER_USER != '' && format('docker.io/{0}/sdk', env.DOCKER_USER) || '' }} - ${{ env.QUAY_USER != '' && format('quay.io/{0}/sdk', env.QUAY_USER) || '' }} + ${{ env.DOCKER_NAMESPACE != '' && format('docker.io/{0}/sdk', env.DOCKER_NAMESPACE) || '' }} + ${{ env.QUAY_NAMESPACE != '' && format('quay.io/{0}/sdk', env.QUAY_NAMESPACE) || '' }} flavor: | latest=false prefix=${{ github.event.inputs.prefix }} @@ -337,8 +336,8 @@ jobs: with: images: | ghcr.io/${{ github.repository_owner }}/sdk - ${{ env.DOCKER_USER != '' && format('docker.io/{0}/sdk', env.DOCKER_USER) || '' }} - ${{ env.QUAY_USER != '' && format('quay.io/{0}/sdk', env.QUAY_USER) || '' }} + ${{ env.DOCKER_NAMESPACE != '' && format('docker.io/{0}/sdk', env.DOCKER_NAMESPACE) || '' }} + ${{ env.QUAY_NAMESPACE != '' && format('quay.io/{0}/sdk', env.QUAY_NAMESPACE) || '' }} flavor: | latest=false prefix=${{ github.event.inputs.prefix }} @@ -352,8 +351,8 @@ jobs: with: images: | ghcr.io/${{ github.repository_owner }}/sdk - ${{ env.DOCKER_USER != '' && format('docker.io/{0}/sdk', env.DOCKER_USER) || '' }} - ${{ env.QUAY_USER != '' && format('quay.io/{0}/sdk', env.QUAY_USER) || '' }} + ${{ env.DOCKER_NAMESPACE != '' && format('docker.io/{0}/sdk', env.DOCKER_NAMESPACE) || '' }} + ${{ env.QUAY_NAMESPACE != '' && format('quay.io/{0}/sdk', env.QUAY_NAMESPACE) || '' }} flavor: | latest=false prefix=${{ github.event.inputs.prefix }} @@ -366,8 +365,8 @@ jobs: with: images: | ghcr.io/${{ github.repository_owner }}/sdk - ${{ env.DOCKER_USER != '' && format('docker.io/{0}/sdk', env.DOCKER_USER) || '' }} - ${{ env.QUAY_USER != '' && format('quay.io/{0}/sdk', env.QUAY_USER) || '' }} + ${{ env.DOCKER_NAMESPACE != '' && format('docker.io/{0}/sdk', env.DOCKER_NAMESPACE) || '' }} + ${{ env.QUAY_NAMESPACE != '' && format('quay.io/{0}/sdk', env.QUAY_NAMESPACE) || '' }} tags: latest - name: Build @@ -455,14 +454,14 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker.io Container Registry - if: github.event_name != 'pull_request' && env.DOCKER_USER != '' && env.DOCKER_TOKEN != '' + if: github.event_name != 'pull_request' && env.DOCKER_NAMESPACE != '' uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_TOKEN }} - name: Login to Quay.io Container Registry - if: github.event_name != 'pull_request' && env.QUAY_USER != '' && env.QUAY_TOKEN != '' + if: github.event_name != 'pull_request' && env.QUAY_NAMESPACE != '' uses: docker/login-action@v4 with: registry: quay.io @@ -475,8 +474,8 @@ jobs: with: images: | ghcr.io/${{ github.repository_owner }}/rootfs - ${{ env.DOCKER_USER != '' && format('docker.io/{0}/rootfs', env.DOCKER_USER) || '' }} - ${{ env.QUAY_USER != '' && format('quay.io/{0}/rootfs', env.QUAY_USER) || '' }} + ${{ env.DOCKER_NAMESPACE != '' && format('docker.io/{0}/rootfs', env.DOCKER_NAMESPACE) || '' }} + ${{ env.QUAY_NAMESPACE != '' && format('quay.io/{0}/rootfs', env.QUAY_NAMESPACE) || '' }} flavor: | latest=false prefix=${{ github.event.inputs.prefix }} diff --git a/README.md b/README.md index 581699e..9fc92cd 100644 --- a/README.md +++ b/README.md @@ -180,22 +180,14 @@ access. Once closed the container is removed. ## GitHub Actions CI & Registries -The GitHub Actions workflow `.github/workflows/containers.yml` automatically builds and pushes the containers to the registries. If you fork this repository, you can also push to your own registry accounts. +The GitHub Actions workflow `.github/workflows/containers.yml` builds the containers and pushes them to `//`, so forks push to their own namespace. -### GitHub Container Registry (GHCR) -By default, the workflow will push containers to `ghcr.io/${{ github.repository_owner }}/`. This works automatically out of the box using GitHub's built-in `GITHUB_TOKEN` and does not require any additional setup. +Pushing to ghcr.io works out of the box using GitHub's built-in `GITHUB_TOKEN`. To also push to Docker Hub or Quay.io, configure the following secrets under your repository's **Settings -> Secrets and variables -> Actions**. A registry without both its user and token is skipped. -### Docker Hub & Quay.io -To push to your own Docker Hub or Quay.io registries, you need to configure the following secrets under your repository's **Settings -> Secrets and variables -> Actions**: +* `DOCKER_USER` and `DOCKER_TOKEN` - the Docker Hub user and its Personal Access Token. +* `QUAY_USER` and `QUAY_TOKEN` - the Quay.io user or robot account (`org+name`) and its token. A robot account needs write permission on the existing `sdk`, `imagebuilder` and `rootfs` repositories. -* **Docker Hub (docker.io)**: - * `DOCKER_USER` - Your Docker Hub username. - * `DOCKER_TOKEN` - Your Docker Hub Personal Access Token. -* **Quay.io (quay.io)**: - * `QUAY_USER` - Your Quay.io username. - * `QUAY_TOKEN` - Your Quay.io OAuth Token / Password. - -If these secrets are not configured, the workflow will automatically skip logging in and pushing to these registries without failing the build. +To push to a different namespace than the repository owner, set the `DOCKER_NAMESPACE` or `QUAY_NAMESPACE` variable. The login user needs push access to it. ## Build Your Own From 6c0451b3998c46bb5a1b972b5b0d2d948aa14c83 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Mon, 14 Sep 2026 13:48:29 +0200 Subject: [PATCH 2/3] ci: push the smoke-tested image instead of rebuilding it The Push step ran the whole build again with no-cache, so the pushed image was not the one that passed the smoke test. Tag the image ID from the Build step and push it after the smoke test, so exactly the tested image is published. Signed-off-by: Josef Schlehofer --- .github/workflows/containers.yml | 57 ++++++++++++++------------------ 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index e6d24e6..94a1b71 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -253,18 +253,14 @@ jobs: - name: Push if: github.event_name != 'pull_request' - uses: docker/build-push-action@v7 - with: - push: true - no-cache: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - DOWNLOAD_FILE=imagebuilder-.*x86_64.tar.[xz|zst] - VERSION_PATH=${{ needs.generate_matrix.outputs.version_path }} - TARGET=${{ matrix.target }} - FILE_HOST=${{ needs.generate_matrix.outputs.file_host }} - RUN_SETUP=${{ needs.generate_matrix.outputs.run_setup }} + env: + IMAGE_ID: ${{ steps.build.outputs.imageid }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + for tag in $TAGS; do + docker tag "$IMAGE_ID" "$tag" + docker push "$tag" + done - name: Cleanup Docker containers run: docker system prune -f @@ -405,23 +401,19 @@ jobs: - name: Push if: github.event_name != 'pull_request' - uses: docker/build-push-action@v7 - with: - push: true - no-cache: true - tags: | + env: + IMAGE_ID: ${{ steps.build.outputs.imageid }} + TAGS: | ${{ steps.meta_ref.outputs.tags }} ${{ steps.meta_version.outputs.tags }} ${{ steps.meta_master.outputs.tags }} ${{ steps.meta_target_arch.outputs.tags }} ${{ steps.meta_latest.outputs.tags }} - build-args: | - DOWNLOAD_FILE=sdk-.*.Linux-x86_64.tar.[xz|zst] - VERSION_PATH=${{ needs.generate_matrix.outputs.version_path }} - TARGET=${{ matrix.target }} - FILE_HOST=${{ needs.generate_matrix.outputs.file_host }} - RUN_SETUP=${{ needs.generate_matrix.outputs.run_setup }} - labels: ${{ steps.meta_ref.outputs.labels }} + run: | + for tag in $TAGS; do + docker tag "$IMAGE_ID" "$tag" + docker push "$tag" + done - name: Cleanup Docker containers run: docker system prune -f @@ -524,15 +516,14 @@ jobs: - name: Push if: github.event_name != 'pull_request' - uses: docker/build-push-action@v7 - with: - no-cache: true - push: true - tags: ${{ steps.meta.outputs.tags }} - file: Dockerfile.rootfs - build-args: ${{ steps.build_args.outputs.args }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/${{ matrix.arch }} + env: + IMAGE_ID: ${{ steps.build.outputs.imageid }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + for tag in $TAGS; do + docker tag "$IMAGE_ID" "$tag" + docker push "$tag" + done - name: Cleanup Docker containers run: docker system prune -f From 979b1393aed4f9c1d3d633dfc4487f8912afdb13 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Mon, 14 Sep 2026 13:48:29 +0200 Subject: [PATCH 3/3] ci: limit GITHUB_TOKEN permissions The workflow does not set any permissions, so every job gets the repository default, which grants write access to everything. Drop all permissions by default and grant each job only what it needs: reading the repository, and writing packages to push to ghcr.io. Signed-off-by: Josef Schlehofer --- .github/workflows/containers.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 94a1b71..74c3225 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -30,6 +30,8 @@ on: description: "Prefix for the image name (add '-' at the end)" required: false +permissions: {} + jobs: dispatch-scheduled-rebuilds: name: Dispatch scheduled rebuilds @@ -50,6 +52,8 @@ jobs: name: Set matrix runs-on: ubuntu-latest if: github.event_name != 'schedule' + permissions: + contents: read outputs: imagebuilders: ${{ steps.find_targets.outputs.imagebuilders }} @@ -181,6 +185,9 @@ jobs: name: ImageBuilder runs-on: ubuntu-latest needs: generate_matrix + permissions: + contents: read + packages: write strategy: fail-fast: False matrix: ${{fromJson(needs.generate_matrix.outputs.imagebuilders)}} @@ -269,6 +276,9 @@ jobs: name: SDK runs-on: ubuntu-latest needs: generate_matrix + permissions: + contents: read + packages: write strategy: fail-fast: False matrix: ${{fromJson(needs.generate_matrix.outputs.sdks)}} @@ -422,6 +432,9 @@ jobs: name: RootFS runs-on: ubuntu-latest needs: generate_matrix + permissions: + contents: read + packages: write if: needs.generate_matrix.outputs.rootfs != '{"include":[]}' strategy: fail-fast: False