feat(gitea-actions): build the CI job images in Gitea CI

The runner's job images were built by ansible into localhost/ only, so the
nightly CI prune deleted them and every idle stretch ended with CI failing in
under a second on `docker pull localhost/gitea-ci:latest` until someone re-ran
the role and waited out a rebuild. The previous commit moved them to the Gitea
registry; this moves the *build* off the deploy path entirely.

- .gitea/workflows/ci-images.yml builds files/Containerfile.* and pushes to
  git.debyl.io/gitbot/. Per-image change detection, so an ESP-IDF pin bump does
  not rebuild the other two; weekly schedule for base-image updates; a
  workflow_dispatch selector. PRs build under a throwaway :pr-<n> tag and drop
  it -- the build lands in the live runner's store, and act_runner will not
  re-pull a tag it already has, so a PR using the real tag would hand every
  later job on this host an unmerged image.
- The Containerfiles stop being ansible templates: their version vars are now
  --build-arg, read by the workflow out of the same defaults/main.yml the role
  interpolates, so CI and ansible build the same bytes from one set of pins.
- LABEL io.debyl.ci-base moves into each Containerfile so neither builder can
  forget the prune exemption; the workflow re-checks it before pushing.
- roles/gitea-actions pulls instead of building. gitea_ci_build_local=true
  restores the local build+push for seeding a cold registry or when CI is
  down -- the workflow that builds gitea-ci runs in gitea-ci.
- Lint .gitea/ alongside ansible/, and document the flow in the role README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian de Byl
2026-09-21 11:10:43 -04:00
co-authored by Claude Opus 5
parent a52209ff6a
commit d0e76bd6cf
11 changed files with 475 additions and 63 deletions
+33 -22
View File
@@ -22,17 +22,18 @@ act_runner_bin: /usr/local/bin/act_runner
act_runner_config_dir: /etc/act_runner
act_runner_work_dir: /var/lib/act_runner
# Job container images. tasks/images.yml builds them into the gitea-runner
# rootless store and pushes them to the Gitea container registry.
# Job container images, served from the Gitea container registry.
#
# They used to live only under localhost/, and the nightly podman prune
# (roles/podman: podman_prune_ci_until) deletes any CI-user image older than
# 48h that no container is using -- so every idle weekend CI failed in 0-1s on
# `docker pull localhost/gitea-ci:latest` until someone re-ran this role and
# waited out a full rebuild. With a registry copy, a pruned image is simply
# re-pulled by the next job (force_pull stays false, so a present image is
# never re-pulled), and this role pulls instead of rebuilding when the
# Containerfile has not changed.
# They used to live only under localhost/, built by this role. The nightly
# podman prune (roles/podman: podman_prune_ci_until) deletes any CI-user image
# older than 48h that no container is using, so every idle weekend CI failed in
# 0-1s on `docker pull localhost/gitea-ci:latest` until someone re-ran the role
# and waited out a full rebuild.
#
# Now .gitea/workflows/ci-images.yml builds them from files/Containerfile.* and
# pushes them here, and this role only pulls. A pruned image is re-pulled by the
# next job on its own (force_pull stays false, which means "pull only when
# missing", so a present image is never re-fetched).
#
# Workflows that pin `container: image:` must use these registry paths too
# (esp-mg-tpms, skudak/esp32-stm32-vcu, skudak/esp32-web-interface).
@@ -55,26 +56,36 @@ gitea_ci_platformio_image: "{{ gitea_ci_registry }}/{{ gitea_ci_registry_namespa
# fallback auth file), so one login covers the runner and this role.
gitea_ci_registry_authfile: "{{ gitea_runner_home }}/.docker/config.json"
# Label stamped on the CI base images so the nightly prune skips them; must match
# podman_prune_ci_keep_label in roles/podman/defaults/main.yml. Without it the
# prune (whose `until` counts from build time, not pull time) would delete a
# re-pulled image again the next night -- a 7.8 GB ESP-IDF re-download after
# every idle day. Superseded tags (e.g. after an esp_idf_version bump) are
# therefore kept too; remove them by hand.
gitea_ci_keep_label: io.debyl.ci-base
# The images this role keeps present on the runner. `build_args` is a literal
# podman-build argument string (podman_image has no structured build-arg
# option) and is only used by the gitea_ci_build_local fallback below -- the
# workflow passes the same --build-arg values, read out of the version vars
# above, so there is one source of truth for the pins.
gitea_ci_images:
- image: "{{ gitea_ci_image }}"
containerfile: Containerfile.ci
template: Containerfile.ci
build_args: ""
- image: "{{ gitea_ci_espidf_image }}"
containerfile: Containerfile.espidf
template: Containerfile.espidf.j2
build_args: "--build-arg ESP_IDF_VERSION={{ esp_idf_version }}"
- image: "{{ gitea_ci_platformio_image }}"
containerfile: Containerfile.platformio
template: Containerfile.platformio.j2
build_args: >-
--build-arg PLATFORMIO_CORE_VERSION={{ platformio_core_version }}
--build-arg PIO_ESPRESSIF32_VERSION={{ pio_espressif32_version }}
# Default labels for every runner — map runs-on values to the local CI image.
# Escape hatch: build the images on the host and push them, instead of pulling
# what CI published. Needed to seed a brand-new registry namespace, and when CI
# itself is down -- the workflow that builds gitea-ci runs *in* gitea-ci, so a
# registry that has never held it cannot bootstrap itself.
#
# make deploy TAGS=gitea-actions EXTRA_VARS="gitea_ci_build_local=true"
#
# Off by default: a plain deploy should never sit through a 15-minute ESP-IDF
# rebuild, and two publishers racing on the same tag is worth avoiding.
gitea_ci_build_local: false
# Default labels for every runner — map runs-on values to the registry CI image.
# Firmware jobs opt into the ESP-IDF image per-job via `container:` in their workflow.
gitea_runner_labels:
- "fedora:docker://{{ gitea_ci_image }}"