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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8701ada7e2
commit
be50798096
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user