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
+242
View File
@@ -0,0 +1,242 @@
---
# Builds the Gitea Actions job images and publishes them to the Gitea container
# registry, so the runner can re-pull one the nightly podman prune removed
# instead of waiting for a human to re-run `make deploy TAGS=gitea-actions`.
#
# Source of truth is ansible/roles/gitea-actions: files/Containerfile.* for the
# image contents, defaults/main.yml for the version pins and the registry path.
# This workflow reads those vars rather than repeating them. roles/gitea-actions
# then only pulls what lands here (gitea_ci_build_local is the escape hatch for
# seeding an empty namespace, since the job below runs *in* gitea-ci).
#
# `docker build` here talks to the gitea-runner user's rootless podman socket,
# mounted into every job container by roles/gitea-actions (config.yaml.j2), so
# the build happens in the same image store the runner pulls from and the layer
# cache survives between runs. That also means a build writes tags the live
# runner will use -- which is why pull requests build under a throwaway
# :pr-<n> tag and delete it again.
name: CI Images
on:
push:
branches: [master]
paths:
- ansible/roles/gitea-actions/files/Containerfile.*
- ansible/roles/gitea-actions/defaults/main.yml
- .gitea/workflows/ci-images.yml
pull_request:
branches: [master]
paths:
- ansible/roles/gitea-actions/files/Containerfile.*
- ansible/roles/gitea-actions/defaults/main.yml
- .gitea/workflows/ci-images.yml
workflow_dispatch:
inputs:
image:
description: Which image to rebuild
type: choice
options: [all, ci, espidf, platformio]
default: all
schedule:
# Weekly rebuild so base-image security updates land without a commit.
# Sunday 04:00, after the 02:00 podman prune has finished.
- cron: "0 4 * * 0"
env:
DEFAULTS: ansible/roles/gitea-actions/defaults/main.yml
CONTEXT: ansible/roles/gitea-actions/files
REGISTRY: git.debyl.io
# Not a secret: the same namespace is in defaults/main.yml. It must be the
# owner of REGISTRY_TOKEN -- Gitea authorises a package push by the token's
# user, not by the path, so pushing to gitbot/ means logging in as gitbot.
REGISTRY_USER: gitbot
KEEP_LABEL: io.debyl.ci-base
# One publisher at a time. Two runs pushing :latest concurrently would leave the
# registry holding whichever finished last, which need not be the newest commit.
concurrency:
group: ci-images
cancel-in-progress: false
jobs:
plan:
name: Plan
runs-on: fedora
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
any: ${{ steps.plan.outputs.any }}
steps:
- uses: actions/checkout@v4
with:
# Full history so the change detection below can diff against the
# pushed-from commit / the PR base.
fetch-depth: 0
- name: Decide which images to build
id: plan
env:
EVENT: ${{ github.event_name }}
SELECTED: ${{ github.event.inputs.image }}
BEFORE: ${{ github.event.before }}
PR_BASE: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import json, os, subprocess, sys, yaml
defaults = yaml.safe_load(open(os.environ["DEFAULTS"]))
ctx = os.environ["CONTEXT"]
reg, ns = os.environ["REGISTRY"], os.environ["REGISTRY_USER"]
# Mirrors gitea_ci_images in defaults/main.yml. The tags are rebuilt
# from the same version vars the role interpolates, so a pin bump in
# that file moves the image tag here and in ansible together.
images = [
{
"key": "ci",
"containerfile": "Containerfile.ci",
"tag": f"{reg}/{ns}/gitea-ci:latest",
"build_args": "",
},
{
"key": "espidf",
"containerfile": "Containerfile.espidf",
"tag": f"{reg}/{ns}/gitea-ci-espidf:{defaults['esp_idf_version']}",
"build_args": f"ESP_IDF_VERSION={defaults['esp_idf_version']}",
},
{
"key": "platformio",
"containerfile": "Containerfile.platformio",
"tag": f"{reg}/{ns}/gitea-ci-platformio:{defaults['pio_espressif32_version']}",
"build_args": (
f"PLATFORMIO_CORE_VERSION={defaults['platformio_core_version']} "
f"PIO_ESPRESSIF32_VERSION={defaults['pio_espressif32_version']}"
),
},
]
event = os.environ["EVENT"]
def changed_files(base):
"""Paths touched since `base`, or None if the diff is not usable."""
if not base or set(base) == {"0"}:
return None
try:
out = subprocess.run(
["git", "diff", "--name-only", f"{base}...HEAD"],
capture_output=True, text=True, check=True,
).stdout
except subprocess.CalledProcessError:
# Force push, shallow clone, first push of a branch: fall back
# to building everything rather than silently skipping a real
# change.
return None
return set(out.split())
if event == "workflow_dispatch":
selected = os.environ.get("SELECTED") or "all"
picked = images if selected == "all" else [i for i in images if i["key"] == selected]
elif event == "schedule":
picked = images
else:
base = os.environ["PR_BASE"] if event == "pull_request" else os.environ["BEFORE"]
touched = changed_files(base)
if touched is None:
picked = images
else:
# defaults/main.yml holds every pin, so a change there could
# retag any image; the workflow file itself changes how all of
# them are built. Either one rebuilds the lot.
wide = {os.environ["DEFAULTS"], ".gitea/workflows/ci-images.yml"}
if touched & wide:
picked = images
else:
picked = [i for i in images if f"{ctx}/{i['containerfile']}" in touched]
print(f"matrix={json.dumps({'include': picked})}")
print(f"any={'true' if picked else 'false'}")
print("building: " + (", ".join(i["tag"] for i in picked) or "nothing"), file=sys.stderr)
PY
build:
name: Build ${{ matrix.key }}
needs: plan
if: needs.plan.outputs.any == 'true'
runs-on: fedora
strategy:
# 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) }}
steps:
- uses: actions/checkout@v4
- name: Log in to the Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
# The build lands in the live runner's image store, and act_runner will
# not re-pull a tag it already has locally. Tagging a PR build with the
# real tag would therefore hand every later job on this host an unmerged
# image, so PRs get a throwaway tag that the cleanup step removes.
- name: Resolve build tag
id: tag
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "image=${{ matrix.tag }}-pr${{ github.event.number }}" >> "$GITHUB_OUTPUT"
else
echo "image=${{ matrix.tag }}" >> "$GITHUB_OUTPUT"
fi
- name: Build ${{ matrix.key }}
env:
IMAGE: ${{ steps.tag.outputs.image }}
BUILD_ARGS: ${{ matrix.build_args }}
run: |
set -euo pipefail
args=()
for a in $BUILD_ARGS; do args+=(--build-arg "$a"); done
# --pull so a scheduled run actually picks up a refreshed base image;
# without it an unchanged FROM line just hits the local layer cache.
docker build --pull \
"${args[@]}" \
-t "$IMAGE" \
-f "$CONTEXT/${{ matrix.containerfile }}" \
"$CONTEXT"
- name: Verify the prune-exemption label survived the build
env:
IMAGE: ${{ steps.tag.outputs.image }}
run: |
set -euo pipefail
# roles/podman's nightly prune keeps an image only if it carries this
# label (podman_prune_ci_keep_label). Publishing one without it would
# quietly restore the nightly-deletion behaviour this replaced, and
# nothing would notice until CI failed on a Monday morning.
got=$(docker inspect -f "{{ index .Config.Labels \"$KEEP_LABEL\" }}" "$IMAGE")
test "$got" = "true" || {
echo "::error::$IMAGE is missing LABEL $KEEP_LABEL=true"
exit 1
}
- name: Push ${{ matrix.key }}
if: github.event_name != 'pull_request'
env:
IMAGE: ${{ steps.tag.outputs.image }}
run: |
set -euo pipefail
docker push "$IMAGE"
echo "Pushed: $IMAGE"
# Always, including on failure: the throwaway tag carries the keep label,
# 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'
env:
IMAGE: ${{ steps.tag.outputs.image }}
run: docker rmi -f "$IMAGE" || true
+13
View File
@@ -45,6 +45,7 @@ ansible/
│ ├── ssl/ # Legacy SSL management (deprecated - Caddy handles certificates automatically) │ ├── ssl/ # Legacy SSL management (deprecated - Caddy handles certificates automatically)
│ ├── github-actions/# CI/CD runner setup │ ├── github-actions/# CI/CD runner setup
│ ├── labelprint/ # 4x6 label print proxy (Raspberry Pi, CUPS/TSPL) │ ├── labelprint/ # 4x6 label print proxy (Raspberry Pi, CUPS/TSPL)
│ ├── gitea-actions/ # Gitea Actions runners + CI job images (see its README)
│ └── pihole/ # DNS filtering │ └── pihole/ # DNS filtering
└── vars/ └── vars/
└── vault.yml # Encrypted secrets └── vault.yml # Encrypted secrets
@@ -90,6 +91,7 @@ Tasks are tagged by service/component for selective deployment:
- `ddns` - Dynamic DNS tasks - `ddns` - Dynamic DNS tasks
- ~~`drone` - CI/CD tasks (decommissioned)~~ - ~~`drone` - CI/CD tasks (decommissioned)~~
- `hass` - Home Assistant tasks - `hass` - Home Assistant tasks
- `gitea-actions` - Gitea Actions runners and their CI job images
- Common infrastructure tags like `common`, `ssl` - Common infrastructure tags like `common`, `ssl`
## Configuration Files ## Configuration Files
@@ -122,6 +124,17 @@ Tasks are tagged by service/component for selective deployment:
- Falls back to its own rescue Wi-Fi AP at 192.168.4.1 when the home SSID is - Falls back to its own rescue Wi-Fi AP at 192.168.4.1 when the home SSID is
unreachable unreachable
### Gitea Actions CI images
The runner's job images (`gitea-ci`, `gitea-ci-espidf`, `gitea-ci-platformio`)
are built by `.gitea/workflows/ci-images.yml` and published to the Gitea
container registry under `git.debyl.io/gitbot/`. The `gitea-actions` role only
pulls them - do NOT add build steps back to it. To change an image, edit
`ansible/roles/gitea-actions/files/Containerfile.*` (or a version pin in that
role's `defaults/main.yml`) and push to master; CI rebuilds only what changed.
See `ansible/roles/gitea-actions/README.md` for the registry rationale, the
prune-exemption label, and the bootstrap path when CI itself cannot build.
### Remote SSH Commands for Service Users ### Remote SSH Commands for Service Users
The `podman` user (and other service users) have `/bin/nologin` as their shell. To run commands as these users via SSH: The `podman` user (and other service users) have `/bin/nologin` as their shell. To run commands as these users via SSH:
+3 -1
View File
@@ -54,7 +54,9 @@ ${VAULT_FILE}: ${VAULT_PASS_FILE}
touch $@ touch $@
# Linting # Linting
YAML_FILES=$(shell find ansible/ -name '*.yml' -not -name '*vault*') # .gitea/workflows is linted too: the CI-image workflow is as much part of the
# deployment as the roles it publishes for.
YAML_FILES=$(shell find ansible/ .gitea/ -name '*.yml' -not -name '*vault*')
SKIP_FILE=./.lint-vars.sh SKIP_FILE=./.lint-vars.sh
# Targets # Targets
+91
View File
@@ -0,0 +1,91 @@
# 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`.
### 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:
```sh
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.
+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_config_dir: /etc/act_runner
act_runner_work_dir: /var/lib/act_runner act_runner_work_dir: /var/lib/act_runner
# Job container images. tasks/images.yml builds them into the gitea-runner # Job container images, served from the Gitea container registry.
# rootless store and pushes them to the Gitea container registry.
# #
# They used to live only under localhost/, and the nightly podman prune # They used to live only under localhost/, built by this role. The nightly
# (roles/podman: podman_prune_ci_until) deletes any CI-user image older than # podman prune (roles/podman: podman_prune_ci_until) deletes any CI-user image
# 48h that no container is using -- so every idle weekend CI failed in 0-1s on # older than 48h that no container is using, so every idle weekend CI failed in
# `docker pull localhost/gitea-ci:latest` until someone re-ran this role and # 0-1s on `docker pull localhost/gitea-ci:latest` until someone re-ran the role
# waited out a full rebuild. With a registry copy, a pruned image is simply # and waited out a full rebuild.
# 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 # Now .gitea/workflows/ci-images.yml builds them from files/Containerfile.* and
# Containerfile has not changed. # 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 # Workflows that pin `container: image:` must use these registry paths too
# (esp-mg-tpms, skudak/esp32-stm32-vcu, skudak/esp32-web-interface). # (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. # fallback auth file), so one login covers the runner and this role.
gitea_ci_registry_authfile: "{{ gitea_runner_home }}/.docker/config.json" 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 # The images this role keeps present on the runner. `build_args` is a literal
# podman_prune_ci_keep_label in roles/podman/defaults/main.yml. Without it the # podman-build argument string (podman_image has no structured build-arg
# prune (whose `until` counts from build time, not pull time) would delete a # option) and is only used by the gitea_ci_build_local fallback below -- the
# re-pulled image again the next night -- a 7.8 GB ESP-IDF re-download after # workflow passes the same --build-arg values, read out of the version vars
# every idle day. Superseded tags (e.g. after an esp_idf_version bump) are # above, so there is one source of truth for the pins.
# therefore kept too; remove them by hand.
gitea_ci_keep_label: io.debyl.ci-base
gitea_ci_images: gitea_ci_images:
- image: "{{ gitea_ci_image }}" - image: "{{ gitea_ci_image }}"
containerfile: Containerfile.ci containerfile: Containerfile.ci
template: Containerfile.ci build_args: ""
- image: "{{ gitea_ci_espidf_image }}" - image: "{{ gitea_ci_espidf_image }}"
containerfile: Containerfile.espidf containerfile: Containerfile.espidf
template: Containerfile.espidf.j2 build_args: "--build-arg ESP_IDF_VERSION={{ esp_idf_version }}"
- image: "{{ gitea_ci_platformio_image }}" - image: "{{ gitea_ci_platformio_image }}"
containerfile: Containerfile.platformio 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. # Firmware jobs opt into the ESP-IDF image per-job via `container:` in their workflow.
gitea_runner_labels: gitea_runner_labels:
- "fedora:docker://{{ gitea_ci_image }}" - "fedora:docker://{{ gitea_ci_image }}"
@@ -1,8 +1,18 @@
# Default Gitea Actions job image (managed by ansible: roles/gitea-actions). # Default Gitea Actions job image (managed by ansible: roles/gitea-actions).
# Covers Go/web/node jobs plus `docker build` (talks to the mounted rootless # Covers Go/web/node jobs plus `docker build` (talks to the mounted rootless
# podman socket). Go toolchains are provided per-job by actions/setup-go. # podman socket). Go toolchains are provided per-job by actions/setup-go.
#
# Built and published by .gitea/workflows/ci-images.yml; roles/gitea-actions
# only pulls the result (see gitea_ci_build_local for the local-build fallback).
# A plain Containerfile, not a template, so CI and ansible build the same bytes.
FROM node:20-bookworm-slim FROM node:20-bookworm-slim
# Exempts the image from the nightly CI prune -- see podman_prune_ci_keep_label
# in roles/podman/defaults/main.yml. Declared here rather than passed as a
# --label at build time so neither builder can forget it: without the label the
# prune deletes the image every night and the next job re-pulls a gigabyte.
LABEL io.debyl.ci-base="true"
ARG DOCKER_CLI_VERSION=27.3.1 ARG DOCKER_CLI_VERSION=27.3.1
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -14,7 +14,19 @@
# the release aborts *after* the firmware and version.json are already live — # the release aborts *after* the firmware and version.json are already live —
# clients get the new build while the tag, Gitea release and protocol manifest # clients get the new build while the tag, Gitea release and protocol manifest
# are never written. Keep it installed. # are never written. Keep it installed.
FROM espressif/idf:{{ esp_idf_version }} #
# Built and published by .gitea/workflows/ci-images.yml; roles/gitea-actions
# only pulls the result. ESP_IDF_VERSION is a build arg rather than an ansible
# template var so CI and ansible build the same bytes -- its value is read from
# esp_idf_version in roles/gitea-actions/defaults/main.yml by both.
ARG ESP_IDF_VERSION
FROM espressif/idf:${ESP_IDF_VERSION}
# Exempts the image from the nightly CI prune -- see podman_prune_ci_keep_label
# in roles/podman/defaults/main.yml. Declared here rather than passed as a
# --label at build time so neither builder can forget it: without the label the
# prune deletes the image every night and the next job re-pulls 7.8 GB.
LABEL io.debyl.ci-base="true"
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates unzip jq python3-yaml python3-jinja2 \ curl ca-certificates unzip jq python3-yaml python3-jinja2 \
@@ -8,8 +8,23 @@
# was validated on hardware — bump pio_espressif32_version / # was validated on hardware — bump pio_espressif32_version /
# platformio_core_version in defaults/main.yml to upgrade (the image tag # platformio_core_version in defaults/main.yml to upgrade (the image tag
# tracks the platform version). # tracks the platform version).
#
# Built and published by .gitea/workflows/ci-images.yml; roles/gitea-actions
# only pulls the result. The pins are build args rather than ansible template
# vars so CI and ansible build the same bytes -- their values are read from
# platformio_core_version / pio_espressif32_version in
# roles/gitea-actions/defaults/main.yml by both.
FROM python:3.12-slim-bookworm FROM python:3.12-slim-bookworm
ARG PLATFORMIO_CORE_VERSION
ARG PIO_ESPRESSIF32_VERSION
# Exempts the image from the nightly CI prune -- see podman_prune_ci_keep_label
# in roles/podman/defaults/main.yml. Declared here rather than passed as a
# --label at build time so neither builder can forget it: without the label the
# prune deletes the image every night and the next job re-pulls a gigabyte.
LABEL io.debyl.ci-base="true"
ENV PLATFORMIO_CORE_DIR=/opt/platformio ENV PLATFORMIO_CORE_DIR=/opt/platformio
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -18,12 +33,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& apt-get install -y --no-install-recommends nodejs \ && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir platformio=={{ platformio_core_version }} RUN pip install --no-cache-dir platformio==${PLATFORMIO_CORE_VERSION}
# Seed project mirroring the real projects' platformio.ini so `pio pkg install` # Seed project mirroring the real projects' platformio.ini so `pio pkg install`
# pulls the platform + toolchain + framework packages into the core dir. # pulls the platform + toolchain + framework packages into the core dir.
# %s + a quoted argument, not ${...} inside the single-quoted format string:
# RUN is `sh -c`, and sh does not expand inside single quotes, so an inlined
# ${PIO_ESPRESSIF32_VERSION} would be written to platformio.ini literally.
RUN mkdir -p /tmp/seed/src \ RUN mkdir -p /tmp/seed/src \
&& printf '[env:seed]\nplatform = espressif32@{{ pio_espressif32_version }}\nframework = arduino\nboard = esp32dev\nboard_build.filesystem = spiffs\nplatform_packages = platformio/tool-esptoolpy\n' > /tmp/seed/platformio.ini \ && printf '[env:seed]\nplatform = espressif32@%s\nframework = arduino\nboard = esp32dev\nboard_build.filesystem = spiffs\nplatform_packages = platformio/tool-esptoolpy\n' "${PIO_ESPRESSIF32_VERSION}" > /tmp/seed/platformio.ini \
&& pio pkg install -d /tmp/seed \ && pio pkg install -d /tmp/seed \
&& pio pkg install -d /tmp/seed --tool platformio/tool-mkspiffs \ && pio pkg install -d /tmp/seed --tool platformio/tool-mkspiffs \
&& rm -rf /tmp/seed \ && rm -rf /tmp/seed \
+42 -34
View File
@@ -1,17 +1,10 @@
--- ---
# CI job images: stage each Containerfile, restore the image from the Gitea # CI job images. .gitea/workflows/ci-images.yml builds files/Containerfile.*
# registry if the nightly prune removed it, rebuild only when the Containerfile # and pushes them to the Gitea registry; this role only logs the runner in and
# changed, then push so the registry always holds what the runner uses. # makes sure the images are present, so a plain deploy never waits on a build.
# See gitea_ci_images in defaults/main.yml. #
- name: create CI image build directory # Set gitea_ci_build_local=true to build and push from here instead -- see the
become: true # comment on that variable in defaults/main.yml.
become_user: "{{ gitea_runner_user }}"
ansible.builtin.file:
path: "{{ gitea_runner_home }}/ci-images"
state: directory
mode: "0755"
tags: gitea-actions
- name: create gitea-runner registry auth directory - name: create gitea-runner registry auth directory
become: true become: true
become_user: "{{ gitea_runner_user }}" become_user: "{{ gitea_runner_user }}"
@@ -21,6 +14,9 @@
mode: "0700" mode: "0700"
tags: gitea-actions tags: gitea-actions
# Docker-format path on purpose: act_runner reads ~/.docker/config.json to
# authenticate the job-image pull it does when a label's image is missing, and
# podman falls back to the same file. One login covers both.
- name: log gitea-runner in to the Gitea container registry - name: log gitea-runner in to the Gitea container registry
become: true become: true
become_user: "{{ gitea_runner_user }}" become_user: "{{ gitea_runner_user }}"
@@ -34,22 +30,7 @@
no_log: true no_log: true
tags: gitea-actions tags: gitea-actions
- name: stage CI Containerfiles - name: pull CI images from the registry
become: true
become_user: "{{ gitea_runner_user }}"
ansible.builtin.template:
src: "{{ item.template }}"
dest: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}"
mode: "0644"
loop: "{{ gitea_ci_images }}"
loop_control:
label: "{{ item.containerfile }}"
register: ci_containerfiles
tags: gitea-actions
# A missing image here is normal (first push, or a new tag): the build below
# creates it. Anything already present locally is left untouched.
- name: restore CI images from the registry
become: true become: true
become_user: "{{ gitea_runner_user }}" become_user: "{{ gitea_runner_user }}"
containers.podman.podman_image: containers.podman.podman_image:
@@ -60,8 +41,35 @@
loop: "{{ gitea_ci_images }}" loop: "{{ gitea_ci_images }}"
loop_control: loop_control:
label: "{{ item.image }}" label: "{{ item.image }}"
when: not (ci_containerfiles.results | selectattr('item.image', 'equalto', item.image) | first).changed when: not (gitea_ci_build_local | bool)
failed_when: false tags: gitea-actions
# --- local build fallback (gitea_ci_build_local=true) ------------------------
# Only reached when seeding a new namespace or when CI cannot build for us.
- name: create CI image build directory
become: true
become_user: "{{ gitea_runner_user }}"
ansible.builtin.file:
path: "{{ gitea_runner_home }}/ci-images"
state: directory
mode: "0755"
when: gitea_ci_build_local | bool
tags: gitea-actions
# copy, not template: these are plain Containerfiles that CI builds verbatim.
# Versions come in as --build-arg from the same defaults/main.yml the workflow
# reads, so neither builder can drift from the other.
- name: stage CI Containerfiles
become: true
become_user: "{{ gitea_runner_user }}"
ansible.builtin.copy:
src: "{{ item.containerfile }}"
dest: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}"
mode: "0644"
loop: "{{ gitea_ci_images }}"
loop_control:
label: "{{ item.containerfile }}"
when: gitea_ci_build_local | bool
tags: gitea-actions tags: gitea-actions
- name: build and push CI images - name: build and push CI images
@@ -72,9 +80,8 @@
path: "{{ gitea_runner_home }}/ci-images" path: "{{ gitea_runner_home }}/ci-images"
build: build:
file: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}" file: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}"
# Exempts the image from the nightly CI prune (gitea_ci_keep_label). extra_args: "{{ item.build_args }}"
extra_args: "--label {{ gitea_ci_keep_label }}=true" force: true
force: "{{ (ci_containerfiles.results | selectattr('item.image', 'equalto', item.image) | first).changed }}"
push: true push: true
auth_file: "{{ gitea_ci_registry_authfile }}" auth_file: "{{ gitea_ci_registry_authfile }}"
environment: environment:
@@ -82,4 +89,5 @@
loop: "{{ gitea_ci_images }}" loop: "{{ gitea_ci_images }}"
loop_control: loop_control:
label: "{{ item.image }}" label: "{{ item.image }}"
when: gitea_ci_build_local | bool
tags: gitea-actions tags: gitea-actions
+7 -2
View File
@@ -311,8 +311,13 @@ podman_prune_ci_users:
- gitea-runner - gitea-runner
- actions-runner - actions-runner
podman_prune_ci_until: 48h podman_prune_ci_until: 48h
# CI base images built by roles/gitea-actions carry this label (gitea_ci_keep_label # The CI base images declare LABEL io.debyl.ci-base="true" in
# there -- keep the two in sync) and are skipped by the CI image prune. # roles/gitea-actions/files/Containerfile.* (and .gitea/workflows/ci-images.yml
# verifies it before publishing -- keep all three in sync). Images carrying it
# are skipped below: `until` counts from build time and not pull time, so
# without the exemption a re-pulled image would be deleted 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) survive too; remove those by hand.
podman_prune_ci_keep_label: io.debyl.ci-base podman_prune_ci_keep_label: io.debyl.ci-base
# Daily rather than weekly: CI turns over many images a day, and a week of that # Daily rather than weekly: CI turns over many images a day, and a week of that
+1 -1
View File
@@ -123,7 +123,7 @@
- import_tasks: containers/home/gregtime.yml - import_tasks: containers/home/gregtime.yml
vars: vars:
image: localhost/greg-time-bot:3.18.1 image: localhost/greg-time-bot:3.18.3
tags: gregtime tags: gregtime
# Built and loaded by `make deploy-remote` in ~/src/rsvp-debylio; bump this to # Built and loaded by `make deploy-remote` in ~/src/rsvp-debylio; bump this to