00_core.ymlยถ

---
- 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