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) <noreply@anthropic.com>
This commit is contained in:
Bastian de Byl
2026-06-06 00:16:54 -04:00
parent 72ecc63e17
commit 2640d09cb5
11 changed files with 179 additions and 48 deletions
+18 -6
View File
@@ -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 }}"
@@ -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
+50 -19
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: