Files
deploy_home/ansible/roles/gitea-actions/README.md
T
Bastian de BylandClaude Opus 5.5 be50798096
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
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>
2026-09-28 19:00:35 -04:00

4.5 KiB

gitea-actions

Runs the Gitea Actions runners on home.debyl.io. One act_runner process per Gitea instance (git.debyl.io, git.skudak.com), both as the gitea-runner user, both backed by the same rootless podman image store.

CI job images

Jobs do not run on the host. Each one gets an ephemeral container from one of three images:

runs-on / container: Image Used by
fedora, ubuntu-latest, ubuntu-22.04 git.debyl.io/gitbot/gitea-ci:latest Go / node / web jobs, docker build
container: image: git.debyl.io/gitbot/gitea-ci-espidf:<esp_idf_version> esp-mg-tpms, skudak/esp32-stm32-vcu
container: image: git.debyl.io/gitbot/gitea-ci-platformio:<pio_espressif32_version> skudak/esp32-web-interface

This role does not build them. .gitea/workflows/ci-images.yml builds files/Containerfile.* and pushes to the Gitea registry; the role logs gitea-runner in and pulls. Version pins live in defaults/main.yml and are read by both the role and the workflow, so a bump moves the image tag in one place.

Why the registry

The images used to exist only as localhost/gitea-ci* in the runner's store. The nightly prune (roles/podman, podman_prune_ci_until: 48h) deletes any CI-user image older than that which no container holds, so after an idle weekend every job failed in under a second on docker pull localhost/gitea-ci:latest, and the only fix was re-running this role and waiting out a full rebuild.

Two things now keep that from happening:

  • A registry copy. force_pull stays false, which in act_runner means pull only when missing — so a present image is never re-fetched, and a pruned one is restored by the next job without anyone noticing.
  • A prune exemption. Each Containerfile declares LABEL io.debyl.ci-base="true", and the prune skips that label (podman_prune_ci_keep_label). Its until counts from build time, not pull time, so without this a re-pulled image would be deleted again the same night — a 7.8 GB ESP-IDF download every single day.

The label is declared in the Containerfile rather than passed as --label so neither builder can omit it; the workflow re-checks it with docker inspect before pushing.

Authentication

Both the role and act_runner read /home/gitea-runner/.docker/config.json. act_runner uses it for the job-image pull it performs when a label's image is missing; podman falls back to the same file. The role writes it from gitea_registry_username / gitea_registry_token (vault), so one login covers both. The skudak runner pulls from git.debyl.io too — same host, same user, same file.

The workflow pushes with a REGISTRY_TOKEN secret on bastian/deploy_home, 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 to master, and the workflow rebuilds only the affected images. It also rebuilds everything weekly so base-image updates land without a commit, and takes a workflow_dispatch with an image selector.

Pull requests build but do not push, under a throwaway :pr-<n> tag that is deleted afterwards. The build runs in the live runner's image store, so a PR tagged with the real name would hand every later job on this host an unmerged image.

Bootstrap / CI is down

The workflow that builds gitea-ci runs in gitea-ci, so a registry that has never held it cannot bootstrap itself. Build on the host instead:

make deploy TAGS=gitea-actions EXTRA_VARS="gitea_ci_build_local=true"

That builds all three from the same Containerfiles and pushes them. One run is enough even on a cold registry: tasks/main.yml imports images.yml before runner.yml, so the images are published before the runner labels are flipped to point at them.

The alternative first-time path is to merge the workflow and dispatch it while the deployed labels still say localhost/ — the job then builds inside the old local image and seeds the registry — then run a plain make deploy TAGS=gitea-actions to switch the labels over.