fix(ci-images): static build matrix for Gitea
CI Images / Plan (push) Successful in 29s
CI Images / Build ci (push) Failing after 1m8s
CI Images / Build espidf (push) Failing after 1m16s
CI Images / Build platformio (push) Failing after 1m30s

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 <noreply@anthropic.com>
This commit is contained in:
Bastian de Byl
2026-09-28 19:00:35 -04:00
co-authored by Claude Opus 5.5
parent 8701ada7e2
commit be50798096
2 changed files with 45 additions and 9 deletions
+40 -9
View File
@@ -63,7 +63,7 @@ jobs:
name: Plan name: Plan
runs-on: fedora runs-on: fedora
outputs: outputs:
matrix: ${{ steps.plan.outputs.matrix }} images: ${{ steps.plan.outputs.images }}
any: ${{ steps.plan.outputs.any }} any: ${{ steps.plan.outputs.any }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -153,7 +153,12 @@ jobs:
else: else:
picked = [i for i in images if f"{ctx}/{i['containerfile']}" in touched] 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(f"any={'true' if picked else 'false'}")
print("building: " + (", ".join(i["tag"] for i in picked) or "nothing"), file=sys.stderr) print("building: " + (", ".join(i["tag"] for i in picked) or "nothing"), file=sys.stderr)
PY PY
@@ -167,11 +172,34 @@ jobs:
# One image failing must not cancel the others: they are independent, and # One image failing must not cancel the others: they are independent, and
# a half-published set is what this whole workflow exists to avoid. # a half-published set is what this whole workflow exists to avoid.
fail-fast: false 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: 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 - uses: actions/checkout@v4
if: steps.spec.outputs.build == 'true'
- name: Log in to the Gitea Container Registry - name: Log in to the Gitea Container Registry
if: steps.spec.outputs.build == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
@@ -184,18 +212,20 @@ jobs:
# image, so PRs get a throwaway tag that the cleanup step removes. # image, so PRs get a throwaway tag that the cleanup step removes.
- name: Resolve build tag - name: Resolve build tag
id: tag id: tag
if: steps.spec.outputs.build == 'true'
run: | run: |
set -euo pipefail set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then 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 else
echo "image=${{ matrix.tag }}" >> "$GITHUB_OUTPUT" echo "image=${{ steps.spec.outputs.tag }}" >> "$GITHUB_OUTPUT"
fi fi
- name: Build ${{ matrix.key }} - name: Build ${{ matrix.key }}
if: steps.spec.outputs.build == 'true'
env: env:
IMAGE: ${{ steps.tag.outputs.image }} IMAGE: ${{ steps.tag.outputs.image }}
BUILD_ARGS: ${{ matrix.build_args }} BUILD_ARGS: ${{ steps.spec.outputs.build_args }}
run: | run: |
set -euo pipefail set -euo pipefail
args=() args=()
@@ -205,10 +235,11 @@ jobs:
docker build --pull \ docker build --pull \
"${args[@]}" \ "${args[@]}" \
-t "$IMAGE" \ -t "$IMAGE" \
-f "$CONTEXT/${{ matrix.containerfile }}" \ -f "$CONTEXT/${{ steps.spec.outputs.containerfile }}" \
"$CONTEXT" "$CONTEXT"
- name: Verify the prune-exemption label survived the build - name: Verify the prune-exemption label survived the build
if: steps.spec.outputs.build == 'true'
env: env:
IMAGE: ${{ steps.tag.outputs.image }} IMAGE: ${{ steps.tag.outputs.image }}
run: | run: |
@@ -224,7 +255,7 @@ jobs:
} }
- name: Push ${{ matrix.key }} - name: Push ${{ matrix.key }}
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request' && steps.spec.outputs.build == 'true'
env: env:
IMAGE: ${{ steps.tag.outputs.image }} IMAGE: ${{ steps.tag.outputs.image }}
run: | run: |
@@ -236,7 +267,7 @@ jobs:
# so the nightly prune will not reclaim it and a few skipped cleanups add # so the nightly prune will not reclaim it and a few skipped cleanups add
# up to gigabytes in the runner's store. # up to gigabytes in the runner's store.
- name: Drop the pull-request image - 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: env:
IMAGE: ${{ steps.tag.outputs.image }} IMAGE: ${{ steps.tag.outputs.image }}
run: docker rmi -f "$IMAGE" || true run: docker rmi -f "$IMAGE" || true
+5
View File
@@ -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 token's owner, not by the path, so pushing to `gitbot/` means logging in as
`gitbot`. `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 ### Rebuilding
Normally nothing to do — edit a `files/Containerfile.*` or a version pin, push Normally nothing to do — edit a `files/Containerfile.*` or a version pin, push