diff --git a/ansible/roles/gitea-actions/defaults/main.yml b/ansible/roles/gitea-actions/defaults/main.yml index d182741..2ba8593 100644 --- a/ansible/roles/gitea-actions/defaults/main.yml +++ b/ansible/roles/gitea-actions/defaults/main.yml @@ -22,18 +22,57 @@ 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 (built locally into the gitea-runner rootless image -# store by tasks/images.yml; never pulled — force_pull is false). -gitea_ci_image: localhost/gitea-ci:latest +# Job container images. tasks/images.yml builds them into the gitea-runner +# rootless store and pushes them to 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. +# +# Workflows that pin `container: image:` must use these registry paths too +# (esp-mg-tpms, skudak/esp32-stm32-vcu, skudak/esp32-web-interface). +gitea_ci_registry: git.debyl.io +# Namespace = the owner of gitea_registry_username / gitea_registry_token (vault). +gitea_ci_registry_namespace: gitbot +gitea_ci_image: "{{ gitea_ci_registry }}/{{ gitea_ci_registry_namespace }}/gitea-ci:latest" # ESP-IDF firmware image tag tracks the upstream espressif/idf release we build from. esp_idf_version: v5.4.1 -gitea_ci_espidf_image: "localhost/gitea-ci-espidf:{{ esp_idf_version }}" +gitea_ci_espidf_image: "{{ gitea_ci_registry }}/{{ gitea_ci_registry_namespace }}/gitea-ci-espidf:{{ esp_idf_version }}" # PlatformIO image for Arduino-framework ESP32 builds (esp32-web-interface). # Tag tracks the pre-baked espressif32 platform version; both pins match the # hardware-validated local build. platformio_core_version: "6.1.19" pio_espressif32_version: "7.0.1" -gitea_ci_platformio_image: "localhost/gitea-ci-platformio:{{ pio_espressif32_version }}" +gitea_ci_platformio_image: "{{ gitea_ci_registry }}/{{ gitea_ci_registry_namespace }}/gitea-ci-platformio:{{ pio_espressif32_version }}" + +# Registry credentials for the gitea-runner user. The Docker-format path is read +# by both act_runner (to authenticate job image pulls) and podman (as its +# 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 + +gitea_ci_images: + - image: "{{ gitea_ci_image }}" + containerfile: Containerfile.ci + template: Containerfile.ci + - image: "{{ gitea_ci_espidf_image }}" + containerfile: Containerfile.espidf + template: Containerfile.espidf.j2 + - image: "{{ gitea_ci_platformio_image }}" + containerfile: Containerfile.platformio + template: Containerfile.platformio.j2 # Default labels for every runner — map runs-on values to the local CI image. # Firmware jobs opt into the ESP-IDF image per-job via `container:` in their workflow. diff --git a/ansible/roles/gitea-actions/tasks/images.yml b/ansible/roles/gitea-actions/tasks/images.yml index e33b5e4..8c41e34 100644 --- a/ansible/roles/gitea-actions/tasks/images.yml +++ b/ansible/roles/gitea-actions/tasks/images.yml @@ -1,4 +1,8 @@ --- +# CI job images: stage each Containerfile, restore the image from the Gitea +# registry if the nightly prune removed it, rebuild only when the Containerfile +# changed, then push so the registry always holds what the runner uses. +# See gitea_ci_images in defaults/main.yml. - name: create CI image build directory become: true become_user: "{{ gitea_runner_user }}" @@ -8,71 +12,74 @@ mode: "0755" tags: gitea-actions -- name: stage default CI Containerfile +- name: create gitea-runner registry auth directory + become: true + become_user: "{{ gitea_runner_user }}" + ansible.builtin.file: + path: "{{ gitea_ci_registry_authfile | dirname }}" + state: directory + mode: "0700" + tags: gitea-actions + +- name: log gitea-runner in to the Gitea container registry + become: true + become_user: "{{ gitea_runner_user }}" + containers.podman.podman_login: + registry: "{{ gitea_ci_registry }}" + username: "{{ gitea_registry_username }}" + password: "{{ gitea_registry_token }}" + authfile: "{{ gitea_ci_registry_authfile }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + no_log: true + tags: gitea-actions + +- name: stage CI Containerfiles become: true become_user: "{{ gitea_runner_user }}" ansible.builtin.template: - src: Containerfile.ci - dest: "{{ gitea_runner_home }}/ci-images/Containerfile.ci" + src: "{{ item.template }}" + dest: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}" mode: "0644" - register: ci_containerfile + loop: "{{ gitea_ci_images }}" + loop_control: + label: "{{ item.containerfile }}" + register: ci_containerfiles tags: gitea-actions -- name: stage ESP-IDF CI Containerfile - become: true - become_user: "{{ gitea_runner_user }}" - ansible.builtin.template: - src: Containerfile.espidf.j2 - dest: "{{ gitea_runner_home }}/ci-images/Containerfile.espidf" - mode: "0644" - register: espidf_containerfile - tags: gitea-actions - -- name: build default CI image ({{ gitea_ci_image }}) +# 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_user: "{{ gitea_runner_user }}" containers.podman.podman_image: - name: "{{ gitea_ci_image }}" - path: "{{ gitea_runner_home }}/ci-images" - build: - file: "{{ gitea_runner_home }}/ci-images/Containerfile.ci" - force: "{{ ci_containerfile is changed }}" + name: "{{ item.image }}" + auth_file: "{{ gitea_ci_registry_authfile }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + loop: "{{ gitea_ci_images }}" + loop_control: + label: "{{ item.image }}" + when: not (ci_containerfiles.results | selectattr('item.image', 'equalto', item.image) | first).changed + failed_when: false tags: gitea-actions -- name: stage PlatformIO CI Containerfile - become: true - become_user: "{{ gitea_runner_user }}" - ansible.builtin.template: - src: Containerfile.platformio.j2 - dest: "{{ gitea_runner_home }}/ci-images/Containerfile.platformio" - mode: "0644" - register: platformio_containerfile - tags: gitea-actions - -- name: build ESP-IDF CI image ({{ gitea_ci_espidf_image }}) +- name: build and push CI images become: true become_user: "{{ gitea_runner_user }}" containers.podman.podman_image: - name: "{{ gitea_ci_espidf_image }}" + name: "{{ item.image }}" path: "{{ gitea_runner_home }}/ci-images" build: - file: "{{ gitea_runner_home }}/ci-images/Containerfile.espidf" - force: "{{ espidf_containerfile is changed }}" - environment: - XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" - tags: gitea-actions - -- name: build PlatformIO CI image ({{ gitea_ci_platformio_image }}) - become: true - become_user: "{{ gitea_runner_user }}" - containers.podman.podman_image: - name: "{{ gitea_ci_platformio_image }}" - path: "{{ gitea_runner_home }}/ci-images" - build: - file: "{{ gitea_runner_home }}/ci-images/Containerfile.platformio" - force: "{{ platformio_containerfile is changed }}" + file: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}" + # Exempts the image from the nightly CI prune (gitea_ci_keep_label). + extra_args: "--label {{ gitea_ci_keep_label }}=true" + force: "{{ (ci_containerfiles.results | selectattr('item.image', 'equalto', item.image) | first).changed }}" + push: true + auth_file: "{{ gitea_ci_registry_authfile }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + loop: "{{ gitea_ci_images }}" + loop_control: + label: "{{ item.image }}" tags: gitea-actions diff --git a/ansible/roles/podman/defaults/main.yml b/ansible/roles/podman/defaults/main.yml index ba18998..7f962cc 100644 --- a/ansible/roles/podman/defaults/main.yml +++ b/ansible/roles/podman/defaults/main.yml @@ -311,6 +311,9 @@ podman_prune_ci_users: - gitea-runner - actions-runner podman_prune_ci_until: 48h +# CI base images built by roles/gitea-actions carry this label (gitea_ci_keep_label +# there -- keep the two in sync) and are skipped by the CI image prune. +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 # is what let the store reach 113 GB between runs. diff --git a/ansible/roles/podman/templates/podman-prune.sh.j2 b/ansible/roles/podman/templates/podman-prune.sh.j2 index 380b532..f59431f 100644 --- a/ansible/roles/podman/templates/podman-prune.sh.j2 +++ b/ansible/roles/podman/templates/podman-prune.sh.j2 @@ -43,9 +43,10 @@ run() { exec podman "$@"' _ "$@" } -# prune_user +# prune_user [image prune filter...] prune_user() { local u=$1 keep=$2 do_containers=$3 + shift 3 local before after img vol con if ! id "$u" >/dev/null 2>&1; then @@ -66,7 +67,7 @@ prune_user() { con=$(run "$u" container prune -f --filter "until=$keep" 2>&1 | tail -1) fi - img=$(run "$u" image prune -af --filter "until=$keep" 2>&1 | tail -1) + img=$(run "$u" image prune -af --filter "until=$keep" "$@" 2>&1 | tail -1) vol=$(run "$u" volume prune -f 2>&1 | tail -1) after=$(run "$u" system df --format '{{ '{{' }}.Size{{ '}}' }}' 2>/dev/null | head -1) @@ -80,7 +81,10 @@ for u in {{ podman_prune_users | join(' ') }}; do done for u in {{ podman_prune_ci_users | join(' ') }}; do - prune_user "$u" "{{ podman_prune_ci_until }}" yes + # CI base images (gitea-ci, -espidf, -platformio) carry the keep label: they + # are rebuilt or re-pulled from the registry only when missing, so pruning + # them just forces a multi-GB re-download on the next job. + prune_user "$u" "{{ podman_prune_ci_until }}" yes --filter "label!={{ podman_prune_ci_keep_label }}" done log "status=ok"