---
- include_tasks: utils/once/flag.yml
- name: ๐ณ Ensure Docker is installed and running
ansible.builtin.include_role:
name: sys-svc-container
tasks_from: 00_core.yml
when: run_once_sys_svc_container is not defined
- name: ๐ง Tune kernel inotify limits for parallel runner containers
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
state: present
reload: true
sysctl_file: "{{ RUNNER_SYSCTL_CONF }}"
loop:
- { key: fs.inotify.max_user_instances, value: "8192" }
- { key: fs.inotify.max_user_watches, value: "524288" }
- { key: fs.inotify.max_queued_events, value: "65536" }
when: not (DOCKER_IN_CONTAINER | bool)
- name: ๐ฅ Create GitHub Actions runner system group
group:
name: "{{ RUNNER_USER }}"
gid: "{{ RUNNER_GID }}"
system: true
- name: ๐ค Create GitHub Actions runner system user
user:
name: "{{ RUNNER_USER }}"
uid: "{{ RUNNER_UID }}"
group: "{{ RUNNER_USER }}"
groups: docker
append: true
system: true
shell: /usr/sbin/nologin
home: "{{ RUNNER_INSTALL_DIR }}"
create_home: true
- name: ๐ Grant passwordless sudo for CI cleanup operations
template: # nocheck: stack-host-template (writes host /etc/sudoers.d, gated per-host on DOCKER_IN_CONTAINER)
src: sudoers.j2
dest: "/etc/sudoers.d/{{ RUNNER_USER }}"
mode: "0440"
validate: visudo -cf %s
when: not (DOCKER_IN_CONTAINER | bool)
- name: ๐ Create runner base directory
file:
path: "{{ RUNNER_INSTALL_DIR }}"
state: directory
owner: "{{ RUNNER_USER }}"
group: "{{ RUNNER_USER }}"
mode: "0755"
- name: ๐ Fetch latest GitHub Actions runner version tag
uri_retry:
url: https://api.github.com/repos/actions/runner/releases/latest
return_content: true
register: _runner_latest_release
delegate_to: localhost
become: false
changed_when: false
- name: ๐ท๏ธ Set resolved runner version
set_fact:
runner_resolved_version: "{{ _runner_latest_release.json.tag_name | regex_replace('^v', '') }}"
- name: โฌ๏ธ Download GitHub Actions runner binary
get_url_retry:
url: "https://github.com/actions/runner/releases/download/v{{ runner_resolved_version }}/actions-runner-linux-x64-{{ runner_resolved_version }}.tar.gz"
dest: "{{ RUNNER_INSTALL_DIR }}/actions-runner.tar.gz"
owner: "{{ RUNNER_USER }}"
group: "{{ RUNNER_USER }}"
mode: "0644"
- name: ๐ Find existing runner instance directories
find:
paths: "{{ RUNNER_INSTALL_DIR }}"
file_type: directory
patterns: "^[0-9]+$"
use_regex: true
register: _existing_runner_dirs
- name: ๐งฎ Determine runner instances to remove
set_fact:
_runners_to_remove: >-
{{ _existing_runner_dirs.files
| map(attribute='path')
| map('basename')
| map('int')
| select('gt', RUNNER_COUNT | int)
| list }}
- name: ๐๏ธ Remove runner instances beyond RUNNER_COUNT
include_tasks: 01_cleanup_instance.yml
loop: "{{ _runners_to_remove }}"
loop_control:
loop_var: runner_instance
when: _runners_to_remove | length > 0
- name: ๐ Gather all containers for orphan detection
community.docker.docker_host_info: # nocheck: docker-module-uses-container-cli read-only info module; returns structured container facts the wrapper's raw JSON would need parsed
containers: true
register: _all_containers_info
changed_when: false
- name: ๐๏ธ Remove orphaned runner containers (beyond RUNNER_COUNT)
ansible.builtin.command: "container rm -f {{ (item.Names | default([item.Name]))[0] | regex_replace('^/', '') }}"
vars:
_project: "{{ (item.Labels | default({}))['com.docker.compose.project'] | default('') }}"
_instance_num: "{{ _project | regex_replace('^' ~ RUNNER_PROJECT_PREFIX ~ '-', '') }}"
loop: "{{ _all_containers_info.containers }}"
loop_control:
label: "{{ (item.Names | default([item.Name]))[0] }}"
when:
- _project is match('^' ~ RUNNER_PROJECT_PREFIX ~ '-[0-9]+$')
- _instance_num | int > RUNNER_COUNT | int
changed_when: true
failed_when: false
- name: ๐ฟ Unmount tmpfs from orphaned Docker volume dirs (beyond RUNNER_COUNT)
ansible.builtin.mount:
path: "{{ RUNNER_DOCKER_BASE }}/{{ item }}"
state: unmounted
loop: "{{ range(RUNNER_COUNT | int + 1, RUNNER_COUNT | int + 50) | list }}"
failed_when: false
changed_when: false
- name: ๐๏ธ Remove orphaned Docker volume dirs (beyond RUNNER_COUNT)
ansible.builtin.file:
path: "{{ RUNNER_DOCKER_BASE }}/{{ item }}"
state: absent
loop: "{{ range(RUNNER_COUNT | int + 1, RUNNER_COUNT | int + 50) | list }}"
- name: ๐ฟ Unmount any existing tmpfs from inner Docker volume directories
ansible.builtin.mount:
path: "{{ RUNNER_DOCKER_BASE }}/{{ item }}"
state: unmounted
loop: "{{ range(1, RUNNER_COUNT | int + 1) | list }}"
failed_when: false
changed_when: false
- name: ๐ Create inner Docker volume directories for runners
file:
path: "{{ RUNNER_DOCKER_BASE }}/{{ item }}"
state: directory
owner: "{{ RUNNER_USER }}"
group: "{{ RUNNER_USER }}"
mode: "0755"
loop: "{{ range(1, RUNNER_COUNT | int + 1) | list }}"
- name: ๐ Provision runner instances
include_tasks: 02_instance.yml
loop: "{{ range(1, RUNNER_COUNT | int + 1) | list }}"
loop_control:
loop_var: runner_instance
- name: ๐ Look up docker group GID
ansible.builtin.command: getent group docker
register: _docker_group_entry
changed_when: false
failed_when: false
- name: ๐ท๏ธ Set docker GID fact
ansible.builtin.set_fact:
_runner_docker_gid: "{{ (_docker_group_entry.stdout | split(':'))[2] | default(RUNNER_DOCKER_GID_FALLBACK) }}"
- name: ๐งฉ Install compose wrapper
ansible.builtin.include_role:
name: sys-svc-compose
tasks_from: 00_core.yml
when: run_once_sys_svc_compose is not defined
- name: ๐ Render runner compose file
template: # nocheck: stack-host-template (node-local runner: every node renders + deploys its own compose stack)
src: compose.yml.j2
dest: "{{ RUNNER_INSTALL_DIR }}/compose.yml"
owner: "{{ RUNNER_USER }}"
group: "{{ RUNNER_USER }}"
mode: "0640"
lstrip_blocks: true
- name: ๐ฆ Build and publish the runner image on the stack host
include_tasks: 03_image.yml
when: IS_STACK_HOST | bool
- name: ๐ฅ Pull runner image from the swarm registry
ansible.builtin.command:
argv:
- container
- pull
- "{{ RUNNER_IMAGE_REF }}"
changed_when: true
when: (SWARM_REGISTRY_PREFIX | length > 0) and not (IS_STACK_HOST | bool)
- name: ๐ Deploy runner compose stack
ansible.builtin.shell: |
set -euo pipefail
compose --chdir {{ RUNNER_INSTALL_DIR }} --project {{ application_id | entity_name }} up -d --remove-orphans
args:
executable: /bin/bash
changed_when: true