From 2640d09cb5a84a7e4112050cb1003179ac0ec49f Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Sat, 6 Jun 2026 00:16:54 -0400 Subject: [PATCH] gitea-actions: run CI jobs in rootless-podman containers Switch the act_runners from :host execution to docker:// images backed by a rootless podman socket under the gitea-runner user, so each job runs in its own ephemeral container with per-job Go caches. This eliminates the cross-repo GOMODCACHE/go-build poisoning that forced the debyl runner to capacity:1. - deps.yml: enable the rootless --user podman.socket, ensure subuid/subgid, register gitea_runner_uid; drop the rootful system socket override, podman-docker and host golang - images.yml + Containerfile.ci/.espidf: build localhost/gitea-ci and localhost/gitea-ci-espidf into the runner's rootless image store - config.yaml.j2: docker:// labels (per-runner overridable), docker_host -> rootless socket, force_pull false - act_runner.service.j2: XDG_RUNTIME_DIR + DOCKER_HOST -> user socket - defaults: uniform capacity:4 (drop the debyl capacity:1 workaround); esp_idf_version now tags the espressif/idf-based image - main.yml: import images.yml, drop the host esp-idf install (firmware jobs use the espressif/idf job container instead) Co-Authored-By: Claude Opus 4.8 (1M context) --- ansible/roles/gitea-actions/defaults/main.yml | 24 +++++-- ansible/roles/gitea-actions/handlers/main.yml | 13 ---- ansible/roles/gitea-actions/tasks/deps.yml | 69 ++++++++++++++----- ansible/roles/gitea-actions/tasks/images.yml | 55 +++++++++++++++ ansible/roles/gitea-actions/tasks/main.yml | 2 +- ansible/roles/gitea-actions/tasks/runner.yml | 2 + ansible/roles/gitea-actions/tasks/user.yml | 2 - .../gitea-actions/templates/Containerfile.ci | 24 +++++++ .../templates/Containerfile.espidf.j2 | 16 +++++ .../templates/act_runner.service.j2 | 5 +- .../gitea-actions/templates/config.yaml.j2 | 15 ++-- 11 files changed, 179 insertions(+), 48 deletions(-) create mode 100644 ansible/roles/gitea-actions/tasks/images.yml create mode 100644 ansible/roles/gitea-actions/templates/Containerfile.ci create mode 100644 ansible/roles/gitea-actions/templates/Containerfile.espidf.j2 diff --git a/ansible/roles/gitea-actions/defaults/main.yml b/ansible/roles/gitea-actions/defaults/main.yml index a648200..47684fe 100644 --- a/ansible/roles/gitea-actions/defaults/main.yml +++ b/ansible/roles/gitea-actions/defaults/main.yml @@ -3,23 +3,35 @@ gitea_runner_user: gitea-runner gitea_runner_home: /home/gitea-runner gitea_runner_version: "0.2.13" gitea_runner_arch: linux-amd64 + +# Max concurrent jobs per runner. Each job runs in its own ephemeral container +# (docker:// labels backed by rootless podman), so jobs no longer share the +# gitea-runner user's Go caches and can run fully in parallel without corruption. gitea_runner_capacity: 4 -# Multiple Gitea instances to run actions runners for +# Gitea instances to run actions runners for. Override `labels` or `capacity` +# per runner here if needed. gitea_runners: - name: debyl instance_url: https://git.debyl.io - name: skudak instance_url: https://git.skudak.com -# Old single-instance format (replaced by gitea_runners list above): -# gitea_instance_url: https://git.debyl.io - # Paths act_runner_bin: /usr/local/bin/act_runner act_runner_config_dir: /etc/act_runner act_runner_work_dir: /var/lib/act_runner -# ESP-IDF configuration +# 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 +# ESP-IDF firmware image tag tracks the upstream espressif/idf release we build from. esp_idf_version: v5.4.1 -esp_idf_path: /opt/esp-idf +gitea_ci_espidf_image: "localhost/gitea-ci-espidf:{{ esp_idf_version }}" + +# 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. +gitea_runner_labels: + - "fedora:docker://{{ gitea_ci_image }}" + - "ubuntu-latest:docker://{{ gitea_ci_image }}" + - "ubuntu-22.04:docker://{{ gitea_ci_image }}" diff --git a/ansible/roles/gitea-actions/handlers/main.yml b/ansible/roles/gitea-actions/handlers/main.yml index 80a1cb7..5db3be1 100644 --- a/ansible/roles/gitea-actions/handlers/main.yml +++ b/ansible/roles/gitea-actions/handlers/main.yml @@ -6,16 +6,3 @@ state: restarted daemon_reload: true loop: "{{ gitea_runners }}" - -- name: restart podman socket - become: true - ansible.builtin.systemd: - name: podman.socket - state: restarted - daemon_reload: true - -- name: restore esp-idf selinux context - become: true - ansible.builtin.command: - cmd: restorecon -R {{ esp_idf_path }} - changed_when: true diff --git a/ansible/roles/gitea-actions/tasks/deps.yml b/ansible/roles/gitea-actions/tasks/deps.yml index 533d544..38debcd 100644 --- a/ansible/roles/gitea-actions/tasks/deps.yml +++ b/ansible/roles/gitea-actions/tasks/deps.yml @@ -1,38 +1,69 @@ --- -- name: install podman-docker for docker CLI compatibility +- name: install podman for rootless CI job containers become: true ansible.builtin.dnf: name: - - podman-docker - - golang + - podman state: present tags: gitea-actions -- name: create podman socket override directory +- name: look up gitea-runner uid become: true - ansible.builtin.file: - path: /etc/systemd/system/podman.socket.d - state: directory - mode: "0755" + changed_when: false + check_mode: false + ansible.builtin.command: id -u {{ gitea_runner_user }} + register: gitea_runner_id + tags: + - gitea-actions + - always + +- name: set gitea_runner_uid fact + ansible.builtin.set_fact: + gitea_runner_uid: "{{ gitea_runner_id.stdout | trim }}" + tags: + - gitea-actions + - always + +# Rootless podman needs subuid/subgid ranges for the runner user. Fedora's +# useradd normally assigns them automatically; ensure they exist regardless. +- name: check gitea-runner subuid mapping + become: true + ansible.builtin.command: grep -q "^{{ gitea_runner_user }}:" /etc/subuid + register: gitea_runner_subuid + changed_when: false + failed_when: false tags: gitea-actions -- name: configure podman socket for gitea-runner access +- name: assign subuid/subgid ranges for gitea-runner become: true - ansible.builtin.copy: - dest: /etc/systemd/system/podman.socket.d/override.conf - content: | - [Socket] - SocketMode=0660 - SocketGroup={{ gitea_runner_user }} - mode: "0644" - notify: restart podman socket + ansible.builtin.command: >- + usermod + --add-subuids 100000000-100065535 + --add-subgids 100000000-100065535 + {{ gitea_runner_user }} + when: gitea_runner_subuid.rc != 0 + register: gitea_runner_subuid_added tags: gitea-actions -- name: enable system podman socket +- name: migrate gitea-runner podman storage to new id mapping become: true + become_user: "{{ gitea_runner_user }}" + ansible.builtin.command: podman system migrate + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + when: gitea_runner_subuid_added is changed + changed_when: true + tags: gitea-actions + +- name: enable rootless podman socket for gitea-runner + become: true + become_user: "{{ gitea_runner_user }}" ansible.builtin.systemd: name: podman.socket - daemon_reload: true + scope: user enabled: true state: started + daemon_reload: true + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" tags: gitea-actions diff --git a/ansible/roles/gitea-actions/tasks/images.yml b/ansible/roles/gitea-actions/tasks/images.yml new file mode 100644 index 0000000..dcd10e5 --- /dev/null +++ b/ansible/roles/gitea-actions/tasks/images.yml @@ -0,0 +1,55 @@ +--- +- 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" + tags: gitea-actions + +- name: stage default CI Containerfile + become: true + become_user: "{{ gitea_runner_user }}" + ansible.builtin.template: + src: Containerfile.ci + dest: "{{ gitea_runner_home }}/ci-images/Containerfile.ci" + mode: "0644" + register: ci_containerfile + 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 }}) + 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 }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + tags: gitea-actions + +- name: build ESP-IDF CI image ({{ gitea_ci_espidf_image }}) + become: true + become_user: "{{ gitea_runner_user }}" + containers.podman.podman_image: + name: "{{ gitea_ci_espidf_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 diff --git a/ansible/roles/gitea-actions/tasks/main.yml b/ansible/roles/gitea-actions/tasks/main.yml index f2f0aa7..dd8de35 100644 --- a/ansible/roles/gitea-actions/tasks/main.yml +++ b/ansible/roles/gitea-actions/tasks/main.yml @@ -3,7 +3,7 @@ tags: gitea-actions - import_tasks: deps.yml tags: gitea-actions -- import_tasks: esp-idf.yml +- import_tasks: images.yml tags: gitea-actions - import_tasks: runner.yml tags: gitea-actions diff --git a/ansible/roles/gitea-actions/tasks/runner.yml b/ansible/roles/gitea-actions/tasks/runner.yml index f1ac472..4092ada 100644 --- a/ansible/roles/gitea-actions/tasks/runner.yml +++ b/ansible/roles/gitea-actions/tasks/runner.yml @@ -45,6 +45,8 @@ mode: "0644" vars: runner_name: "{{ item.name }}" + runner_capacity: "{{ item.capacity | default(gitea_runner_capacity) }}" + runner_labels: "{{ item.labels | default(gitea_runner_labels) }}" loop: "{{ gitea_runners }}" notify: restart act_runner services tags: gitea-actions diff --git a/ansible/roles/gitea-actions/tasks/user.yml b/ansible/roles/gitea-actions/tasks/user.yml index cccce1f..a4e88c9 100644 --- a/ansible/roles/gitea-actions/tasks/user.yml +++ b/ansible/roles/gitea-actions/tasks/user.yml @@ -7,8 +7,6 @@ shell: /bin/bash createhome: true home: "{{ gitea_runner_home }}" - groups: docker - append: true tags: gitea-actions - name: check if gitea-runner lingering enabled diff --git a/ansible/roles/gitea-actions/templates/Containerfile.ci b/ansible/roles/gitea-actions/templates/Containerfile.ci new file mode 100644 index 0000000..ef1352e --- /dev/null +++ b/ansible/roles/gitea-actions/templates/Containerfile.ci @@ -0,0 +1,24 @@ +# Default Gitea Actions job image (managed by ansible: roles/gitea-actions). +# 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. +FROM node:20-bookworm-slim + +ARG DOCKER_CLI_VERSION=27.3.1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl git openssh-client make build-essential \ + python3 python3-pip jq unzip \ + && rm -rf /var/lib/apt/lists/* + +# Static docker client (no daemon) for jobs that run `docker build` against the +# mounted podman socket (/var/run/docker.sock). +RUN curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_CLI_VERSION}.tgz" \ + | tar -xz -C /tmp \ + && install -m0755 /tmp/docker/docker /usr/local/bin/docker \ + && rm -rf /tmp/docker + +# AWS CLI v2 — several workflows upload artifacts / deploy Lambda. +RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \ + && unzip -q /tmp/awscliv2.zip -d /tmp \ + && /tmp/aws/install \ + && rm -rf /tmp/aws /tmp/awscliv2.zip diff --git a/ansible/roles/gitea-actions/templates/Containerfile.espidf.j2 b/ansible/roles/gitea-actions/templates/Containerfile.espidf.j2 new file mode 100644 index 0000000..fe2a61d --- /dev/null +++ b/ansible/roles/gitea-actions/templates/Containerfile.espidf.j2 @@ -0,0 +1,16 @@ +# ESP-IDF firmware job image (managed by ansible: roles/gitea-actions). +# Adds node (required by actions/checkout and other JS actions) and the AWS CLI +# (firmware artifacts ship to S3) on top of the official Espressif toolchain. +# IDF lives at /opt/esp/idf — firmware jobs source /opt/esp/idf/export.sh. +FROM espressif/idf:{{ esp_idf_version }} + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates unzip \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \ + && unzip -q /tmp/awscliv2.zip -d /tmp \ + && /tmp/aws/install \ + && rm -rf /tmp/aws /tmp/awscliv2.zip diff --git a/ansible/roles/gitea-actions/templates/act_runner.service.j2 b/ansible/roles/gitea-actions/templates/act_runner.service.j2 index fd249b4..e218729 100644 --- a/ansible/roles/gitea-actions/templates/act_runner.service.j2 +++ b/ansible/roles/gitea-actions/templates/act_runner.service.j2 @@ -1,7 +1,7 @@ [Unit] Description=Gitea Actions runner ({{ runner_name }}) Documentation=https://gitea.com/gitea/act_runner -After=network.target podman.socket +After=network.target [Service] ExecStart={{ act_runner_bin }} daemon --config {{ act_runner_config_dir }}/config-{{ runner_name }}.yaml @@ -10,7 +10,8 @@ TimeoutSec=0 RestartSec=10 Restart=always User={{ gitea_runner_user }} -Environment="DOCKER_HOST=unix:///run/podman/podman.sock" +Environment="XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}" +Environment="DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/podman/podman.sock" [Install] WantedBy=multi-user.target diff --git a/ansible/roles/gitea-actions/templates/config.yaml.j2 b/ansible/roles/gitea-actions/templates/config.yaml.j2 index 0e0a140..ddca565 100644 --- a/ansible/roles/gitea-actions/templates/config.yaml.j2 +++ b/ansible/roles/gitea-actions/templates/config.yaml.j2 @@ -3,27 +3,32 @@ log: runner: file: {{ act_runner_work_dir }}/{{ runner_name }}/.runner - capacity: {{ gitea_runner_capacity | default(4) }} + capacity: {{ runner_capacity | default(gitea_runner_capacity) | default(4) }} timeout: 3h insecure: false fetch_timeout: 5s fetch_interval: 2s labels: - - ubuntu-latest:host - - ubuntu-22.04:host - - fedora:host +{% for label in runner_labels | default(gitea_runner_labels) %} + - {{ label }} +{% endfor %} cache: enabled: true dir: {{ act_runner_work_dir }}/{{ runner_name }}/cache container: + # Each job runs in its own ephemeral container (docker:// labels) backed by + # the gitea-runner user's rootless podman socket — this is what isolates the + # per-job Go module/build caches and fixes cross-repo cache poisoning. network: host privileged: false options: workdir_parent: valid_volumes: [] - docker_host: "" + # Point act at the real rootless socket so it mounts the correct path into + # job containers (the documented rootless-podman gotcha). + docker_host: "unix:///run/user/{{ gitea_runner_uid }}/podman/podman.sock" force_pull: false host: