From be50798096818282a2cef3fcc6e68905e59e1418 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 28 Sep 2026 19:00:35 -0400 Subject: [PATCH] fix(ci-images): static build matrix for Gitea Gitea expands a job's matrix when the run is created, before plan has any outputs, so fromJSON(needs.plan.outputs.matrix) collapsed to a single empty "Build ${{ matrix.key }}" job and nothing was ever built. The matrix is now the fixed list of image keys; plan emits every image's spec with a build flag and each matrix job looks its own entry up, no-opping when it wasn't picked. Also document that both registry tokens need write:package -- the vault token was read-only, so the gitea_ci_build_local bootstrap failed its push. Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/ci-images.yml | 49 ++++++++++++++++++++++----- ansible/roles/gitea-actions/README.md | 5 +++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/ci-images.yml b/.gitea/workflows/ci-images.yml index 4898516..79d0e0d 100644 --- a/.gitea/workflows/ci-images.yml +++ b/.gitea/workflows/ci-images.yml @@ -63,7 +63,7 @@ jobs: name: Plan runs-on: fedora outputs: - matrix: ${{ steps.plan.outputs.matrix }} + images: ${{ steps.plan.outputs.images }} any: ${{ steps.plan.outputs.any }} steps: - uses: actions/checkout@v4 @@ -153,7 +153,12 @@ jobs: else: picked = [i for i in images if f"{ctx}/{i['containerfile']}" in touched] - print(f"matrix={json.dumps({'include': picked})}") + # Every image, keyed by matrix.key, each flagged build or skip. The + # build job's matrix is static (see there), so it needs the full set + # to look its own entry up in, not just the picked ones. + picked_keys = {i["key"] for i in picked} + specs = {i["key"]: {**i, "build": i["key"] in picked_keys} for i in images} + print(f"images={json.dumps(specs)}") print(f"any={'true' if picked else 'false'}") print("building: " + (", ".join(i["tag"] for i in picked) or "nothing"), file=sys.stderr) PY @@ -167,11 +172,34 @@ jobs: # One image failing must not cancel the others: they are independent, and # a half-published set is what this whole workflow exists to avoid. fail-fast: false - matrix: ${{ fromJSON(needs.plan.outputs.matrix) }} + # Static on purpose. Gitea expands the matrix when the run is created, + # before plan has produced any outputs, so a + # fromJSON(needs.plan.outputs.*) matrix collapses to one empty job. Each + # entry instead looks its spec up in plan's output and no-ops its steps + # when plan did not pick it. Keys must match `images` in plan. + matrix: + key: [ci, espidf, platformio] steps: + - name: Look up the ${{ matrix.key }} image spec + id: spec + env: + IMAGES: ${{ needs.plan.outputs.images }} + KEY: ${{ matrix.key }} + run: | + set -euo pipefail + python3 - <<'PY' >> "$GITHUB_OUTPUT" + import json, os + spec = json.loads(os.environ["IMAGES"])[os.environ["KEY"]] + for k in ("containerfile", "tag", "build_args"): + print(f"{k}={spec[k]}") + print(f"build={'true' if spec['build'] else 'false'}") + PY + - uses: actions/checkout@v4 + if: steps.spec.outputs.build == 'true' - name: Log in to the Gitea Container Registry + if: steps.spec.outputs.build == 'true' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -184,18 +212,20 @@ jobs: # image, so PRs get a throwaway tag that the cleanup step removes. - name: Resolve build tag id: tag + if: steps.spec.outputs.build == 'true' run: | set -euo pipefail if [ "${{ github.event_name }}" = "pull_request" ]; then - echo "image=${{ matrix.tag }}-pr${{ github.event.number }}" >> "$GITHUB_OUTPUT" + echo "image=${{ steps.spec.outputs.tag }}-pr${{ github.event.number }}" >> "$GITHUB_OUTPUT" else - echo "image=${{ matrix.tag }}" >> "$GITHUB_OUTPUT" + echo "image=${{ steps.spec.outputs.tag }}" >> "$GITHUB_OUTPUT" fi - name: Build ${{ matrix.key }} + if: steps.spec.outputs.build == 'true' env: IMAGE: ${{ steps.tag.outputs.image }} - BUILD_ARGS: ${{ matrix.build_args }} + BUILD_ARGS: ${{ steps.spec.outputs.build_args }} run: | set -euo pipefail args=() @@ -205,10 +235,11 @@ jobs: docker build --pull \ "${args[@]}" \ -t "$IMAGE" \ - -f "$CONTEXT/${{ matrix.containerfile }}" \ + -f "$CONTEXT/${{ steps.spec.outputs.containerfile }}" \ "$CONTEXT" - name: Verify the prune-exemption label survived the build + if: steps.spec.outputs.build == 'true' env: IMAGE: ${{ steps.tag.outputs.image }} run: | @@ -224,7 +255,7 @@ jobs: } - name: Push ${{ matrix.key }} - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && steps.spec.outputs.build == 'true' env: IMAGE: ${{ steps.tag.outputs.image }} run: | @@ -236,7 +267,7 @@ jobs: # so the nightly prune will not reclaim it and a few skipped cleanups add # up to gigabytes in the runner's store. - name: Drop the pull-request image - if: always() && github.event_name == 'pull_request' + if: always() && github.event_name == 'pull_request' && steps.spec.outputs.build == 'true' env: IMAGE: ${{ steps.tag.outputs.image }} run: docker rmi -f "$IMAGE" || true diff --git a/ansible/roles/gitea-actions/README.md b/ansible/roles/gitea-actions/README.md index 906d1eb..c52078a 100644 --- a/ansible/roles/gitea-actions/README.md +++ b/ansible/roles/gitea-actions/README.md @@ -59,6 +59,11 @@ belonging to the same `gitbot` user: Gitea authorises a package push by the token's owner, not by the path, so pushing to `gitbot/` means logging in as `gitbot`. +Both tokens need the `write:package` scope, not just `read:package`: the +workflow pushes with `REGISTRY_TOKEN`, and the `gitea_ci_build_local` bootstrap +below pushes with the vault token. A read-only token logs in and pulls fine but +fails the push with `authentication required` (Gitea logs `reqPackageAccess`). + ### Rebuilding Normally nothing to do — edit a `files/Containerfile.*` or a version pin, push