From f8f8d5abba8411b53539a98617e4aca62facbb46 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 4 Nov 2026 23:19:10 +0000 Subject: [PATCH 1/6] feat: provision managed macOS Actions runners --- playbooks/site.yml | 7 + .../defaults/main.yml | 17 ++ .../handlers/main.yml | 13 ++ .../meta/argument_specs.yml | 52 +++++ .../meta/main.yml | 10 + .../tasks/main.yml | 198 ++++++++++++++++++ .../templates/runner.plist.j2 | 29 +++ 7 files changed, 326 insertions(+) create mode 100644 roles/github_runner_macos_actions_runner/defaults/main.yml create mode 100644 roles/github_runner_macos_actions_runner/handlers/main.yml create mode 100644 roles/github_runner_macos_actions_runner/meta/argument_specs.yml create mode 100644 roles/github_runner_macos_actions_runner/meta/main.yml create mode 100644 roles/github_runner_macos_actions_runner/tasks/main.yml create mode 100644 roles/github_runner_macos_actions_runner/templates/runner.plist.j2 diff --git a/playbooks/site.yml b/playbooks/site.yml index 6f1ad956..267431c7 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -40,6 +40,13 @@ roles: - exadev.github_runner.github_runner_macos_host +# A macOS Actions runner is a host process, not an ARC runner pod: Xcode and macOS archive jobs cannot run in a Linux container. The role registers it with a short-lived GitHub App token and supervises its persistent process through launchd. +- name: Install managed macOS Actions runners + hosts: "{{ github_runner_cluster_group | default('github_runner_cluster') }}" + gather_facts: true + roles: + - exadev.github_runner.github_runner_macos_actions_runner + # Before the fleet play, so a Linux host that would suspend, or whose Wi-Fi would power-save, is fixed before k3s starts on it. Linux hosts only (the role does nothing on any other), and both of its steps are off unless a host's variables turn them on. The tasks that change the host use become (sudo), as do those of a native node (github_runner_cluster_node_mode: native), so run the play with passwordless sudo for the connecting user or with --ask-become-pass. It gathers its own facts: only the platform subset the role needs, so the host's Python is the only requirement. - name: Set up the Linux hosts' host baseline hosts: "{{ github_runner_cluster_group | default('github_runner_cluster') }}" diff --git a/roles/github_runner_macos_actions_runner/defaults/main.yml b/roles/github_runner_macos_actions_runner/defaults/main.yml new file mode 100644 index 00000000..d221ba5b --- /dev/null +++ b/roles/github_runner_macos_actions_runner/defaults/main.yml @@ -0,0 +1,17 @@ +--- +# A persistent macOS runner is deliberately separate from ARC: runner pods require Linux containers, while Xcode and macOS archive jobs must run on the host OS. The runner's registration credential is minted only while Ansible configures it; launchd runs the already-registered process without any GitHub credential. +github_runner_macos_actions_runner_enabled: false +github_runner_macos_actions_runner_organization: "" +github_runner_macos_actions_runner_app_id: "" +github_runner_macos_actions_runner_installation_id: "" +github_runner_macos_actions_runner_private_key: "" +github_runner_macos_actions_runner_private_key_op_reference: "" +github_runner_macos_actions_runner_label: exadev-macos +github_runner_macos_actions_runner_name: "{{ inventory_hostname }}-macos" +github_runner_macos_actions_runner_version: "2.338.2" +github_runner_macos_actions_runner_sha256: 25a065af409ffa317ab4d5a3b614dd15fc9c40f2172bc1b35a1b1258e919a388 +github_runner_macos_actions_runner_root: "{{ ansible_facts['env']['HOME'] }}/.github-actions-runner" +github_runner_macos_actions_runner_work_dir: "{{ github_runner_macos_actions_runner_root }}/_work" +github_runner_macos_actions_runner_plist: "/Library/LaunchDaemons/io.exadev.github-actions-runner.{{ github_runner_macos_actions_runner_name }}.plist" +github_runner_macos_actions_runner_launchd_label: "io.exadev.github-actions-runner.{{ github_runner_macos_actions_runner_name }}" +github_runner_macos_actions_runner_path: /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin diff --git a/roles/github_runner_macos_actions_runner/handlers/main.yml b/roles/github_runner_macos_actions_runner/handlers/main.yml new file mode 100644 index 00000000..c258c052 --- /dev/null +++ b/roles/github_runner_macos_actions_runner/handlers/main.yml @@ -0,0 +1,13 @@ +--- +- name: Restart macOS Actions runner + become: true + block: + - name: Stop the previous runner launch daemon + ansible.builtin.command: + argv: [launchctl, bootout, "system/{{ github_runner_macos_actions_runner_launchd_label }}"] + failed_when: false + changed_when: false + - name: Start the runner launch daemon + ansible.builtin.command: + argv: [launchctl, bootstrap, system, "{{ github_runner_macos_actions_runner_plist }}"] + changed_when: true diff --git a/roles/github_runner_macos_actions_runner/meta/argument_specs.yml b/roles/github_runner_macos_actions_runner/meta/argument_specs.yml new file mode 100644 index 00000000..e20f4e00 --- /dev/null +++ b/roles/github_runner_macos_actions_runner/meta/argument_specs.yml @@ -0,0 +1,52 @@ +--- +argument_specs: + main: + short_description: Registers a macOS runner as a launchd-managed ephemeral GitHub Actions runner. + options: + github_runner_macos_actions_runner_enabled: + type: bool + default: false + github_runner_macos_actions_runner_organization: + type: str + default: "" + github_runner_macos_actions_runner_app_id: + type: str + default: "" + github_runner_macos_actions_runner_installation_id: + type: str + default: "" + github_runner_macos_actions_runner_private_key: + type: str + default: "" + no_log: true + github_runner_macos_actions_runner_private_key_op_reference: + type: str + default: "" + no_log: true + github_runner_macos_actions_runner_label: + type: str + default: exadev-macos + github_runner_macos_actions_runner_name: + type: str + default: "" + github_runner_macos_actions_runner_version: + type: str + default: "2.338.2" + github_runner_macos_actions_runner_sha256: + type: str + default: 25a065af409ffa317ab4d5a3b614dd15fc9c40f2172bc1b35a1b1258e919a388 + github_runner_macos_actions_runner_root: + type: path + default: "" + github_runner_macos_actions_runner_work_dir: + type: path + default: "" + github_runner_macos_actions_runner_plist: + type: path + default: "" + github_runner_macos_actions_runner_launchd_label: + type: str + default: "" + github_runner_macos_actions_runner_path: + type: str + default: /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin diff --git a/roles/github_runner_macos_actions_runner/meta/main.yml b/roles/github_runner_macos_actions_runner/meta/main.yml new file mode 100644 index 00000000..71dbe95c --- /dev/null +++ b/roles/github_runner_macos_actions_runner/meta/main.yml @@ -0,0 +1,10 @@ +--- +galaxy_info: + author: ExaDev + description: Registers a managed macOS GitHub Actions runner through launchd. + license: MIT + min_ansible_version: "2.15" + platforms: + - name: MacOSX + versions: [all] +dependencies: [] diff --git a/roles/github_runner_macos_actions_runner/tasks/main.yml b/roles/github_runner_macos_actions_runner/tasks/main.yml new file mode 100644 index 00000000..313cc6d0 --- /dev/null +++ b/roles/github_runner_macos_actions_runner/tasks/main.yml @@ -0,0 +1,198 @@ +--- +- name: Read the host platform and connecting user + ansible.builtin.setup: + gather_subset: + - "!all" + - "!min" + - platform + - user + when: "'system' not in ansible_facts or 'user_id' not in ansible_facts" + +- name: End the role on a non-macOS host + ansible.builtin.meta: end_role + when: ansible_facts['system'] != 'Darwin' + +- name: Read the macOS runner App key from 1Password + delegate_to: localhost + ansible.builtin.command: + argv: [op, read, "{{ github_runner_macos_actions_runner_private_key_op_reference }}"] + register: github_runner_macos_actions_runner_op_key + changed_when: false + no_log: true + when: + - github_runner_macos_actions_runner_enabled | bool + - github_runner_macos_actions_runner_private_key | length == 0 + - github_runner_macos_actions_runner_private_key_op_reference | length > 0 + +- name: Set the macOS runner App key + ansible.builtin.set_fact: + github_runner_macos_actions_runner_private_key: "{{ github_runner_macos_actions_runner_op_key.stdout }}" + no_log: true + when: github_runner_macos_actions_runner_op_key is not skipped + +- name: Require the macOS runner settings + ansible.builtin.assert: + that: + - github_runner_macos_actions_runner_organization | length > 0 + - github_runner_macos_actions_runner_app_id | string | regex_search('^[0-9]+$') is not none + - github_runner_macos_actions_runner_installation_id | string | regex_search('^[0-9]+$') is not none + - github_runner_macos_actions_runner_private_key | length > 0 + - github_runner_macos_actions_runner_label | regex_search('^[A-Za-z0-9_.-]+$') is not none + quiet: true + when: github_runner_macos_actions_runner_enabled | bool + no_log: true + +- name: Stop and remove a disabled macOS runner + become: true + when: not (github_runner_macos_actions_runner_enabled | bool) + block: + - name: Remove the runner launch daemon + ansible.builtin.file: + path: "{{ github_runner_macos_actions_runner_plist }}" + state: absent + - name: Stop the disabled runner service + ansible.builtin.command: + argv: [launchctl, bootout, "system/{{ github_runner_macos_actions_runner_launchd_label }}"] + failed_when: false + changed_when: false + +- name: Configure the enabled macOS runner + when: github_runner_macos_actions_runner_enabled | bool + block: + - name: Create the runner directory + ansible.builtin.file: + path: "{{ github_runner_macos_actions_runner_root }}" + state: directory + owner: "{{ ansible_facts['user_id'] }}" + mode: "0755" + + - name: Check the installed runner version + ansible.builtin.command: + argv: ["{{ github_runner_macos_actions_runner_root }}/bin/Runner.Listener", --version] + register: github_runner_macos_actions_runner_installed_version + changed_when: false + failed_when: false + check_mode: false + + - name: Download the pinned Apple Silicon runner package + ansible.builtin.get_url: + url: "https://github.com/actions/runner/releases/download/v{{ github_runner_macos_actions_runner_version }}/actions-runner-osx-arm64-{{ github_runner_macos_actions_runner_version }}.tar.gz" + dest: "{{ github_runner_macos_actions_runner_root }}-{{ github_runner_macos_actions_runner_version }}.tar.gz" + checksum: "sha256:{{ github_runner_macos_actions_runner_sha256 }}" + mode: "0644" + when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version + + - name: Remove the prior runner installation + ansible.builtin.file: + path: "{{ github_runner_macos_actions_runner_root }}" + state: absent + when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version + + - name: Recreate the runner directory + ansible.builtin.file: + path: "{{ github_runner_macos_actions_runner_root }}" + state: directory + owner: "{{ ansible_facts['user_id'] }}" + mode: "0755" + when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version + + - name: Install the pinned Apple Silicon runner package + ansible.builtin.unarchive: + src: "{{ github_runner_macos_actions_runner_root }}-{{ github_runner_macos_actions_runner_version }}.tar.gz" + dest: "{{ github_runner_macos_actions_runner_root }}" + remote_src: true + owner: "{{ ansible_facts['user_id'] }}" + when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version + + - name: Check whether the runner is registered + ansible.builtin.stat: + path: "{{ github_runner_macos_actions_runner_root }}/.runner" + register: github_runner_macos_actions_runner_registration + + - name: Register the runner through the GitHub App + delegate_to: localhost + check_mode: false + no_log: true + when: not github_runner_macos_actions_runner_registration.stat.exists + block: + - name: Create a temporary App key file + ansible.builtin.tempfile: + state: file + suffix: ".pem" + register: github_runner_macos_actions_runner_key_file + + - name: Write the GitHub App key + ansible.builtin.copy: + content: "{{ github_runner_macos_actions_runner_private_key }}" + dest: "{{ github_runner_macos_actions_runner_key_file.path }}" + mode: "0600" + + - name: Sign the GitHub App JWT + ansible.builtin.script: + cmd: "{{ role_path }}/../github_runner_arc/files/generate_app_jwt.py {{ github_runner_macos_actions_runner_app_id | quote }} {{ github_runner_macos_actions_runner_key_file.path | quote }}" + executable: python3 + changed_when: false + register: github_runner_macos_actions_runner_jwt + + - name: Mint a self-hosted runner installation token + ansible.builtin.uri: + url: "https://api.github.com/app/installations/{{ github_runner_macos_actions_runner_installation_id }}/access_tokens" + method: POST + headers: + Authorization: "Bearer {{ github_runner_macos_actions_runner_jwt.stdout | trim }}" + Accept: application/vnd.github+json + X-GitHub-Api-Version: "2022-11-28" + body_format: json + body: + permissions: + organization_self_hosted_runners: write + status_code: 201 + register: github_runner_macos_actions_runner_installation_token + + - name: Request a runner registration token + ansible.builtin.uri: + url: "https://api.github.com/orgs/{{ github_runner_macos_actions_runner_organization }}/actions/runners/registration-token" + method: POST + headers: + Authorization: "Bearer {{ github_runner_macos_actions_runner_installation_token.json.token }}" + Accept: application/vnd.github+json + X-GitHub-Api-Version: "2022-11-28" + status_code: 201 + register: github_runner_macos_actions_runner_registration_token + always: + - name: Remove the temporary App key file + ansible.builtin.file: + path: "{{ github_runner_macos_actions_runner_key_file.path }}" + state: absent + when: github_runner_macos_actions_runner_key_file.path is defined + + - name: Configure the macOS runner + ansible.builtin.command: + argv: + - "{{ github_runner_macos_actions_runner_root }}/config.sh" + - --unattended + - --url + - "https://github.com/{{ github_runner_macos_actions_runner_organization }}" + - --token + - "{{ github_runner_macos_actions_runner_registration_token.json.token }}" + - --name + - "{{ github_runner_macos_actions_runner_name }}" + - --labels + - "{{ github_runner_macos_actions_runner_label }}" + - --work + - "{{ github_runner_macos_actions_runner_work_dir }}" + - --replace + no_log: true + changed_when: true + when: not github_runner_macos_actions_runner_registration.stat.exists + + - name: Install the runner launch daemon + become: true + ansible.builtin.template: + src: runner.plist.j2 + dest: "{{ github_runner_macos_actions_runner_plist }}" + owner: root + group: wheel + mode: "0644" + validate: plutil -lint %s + notify: Restart macOS Actions runner diff --git a/roles/github_runner_macos_actions_runner/templates/runner.plist.j2 b/roles/github_runner_macos_actions_runner/templates/runner.plist.j2 new file mode 100644 index 00000000..a7cf6a54 --- /dev/null +++ b/roles/github_runner_macos_actions_runner/templates/runner.plist.j2 @@ -0,0 +1,29 @@ + + + + + Label + {{ github_runner_macos_actions_runner_launchd_label }} + ProgramArguments + + {{ github_runner_macos_actions_runner_root }}/run.sh + + WorkingDirectory + {{ github_runner_macos_actions_runner_root }} + UserName + {{ ansible_facts['user_id'] }} + EnvironmentVariables + + PATH + {{ github_runner_macos_actions_runner_path }} + + KeepAlive + + RunAtLoad + + StandardOutPath + {{ github_runner_macos_actions_runner_root }}/runner.stdout.log + StandardErrorPath + {{ github_runner_macos_actions_runner_root }}/runner.stderr.log + + From 336ac0fdfcd087ab3e907e454de5b6b8e4842820 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 4 Nov 2026 23:20:51 +0000 Subject: [PATCH 2/6] fix: isolate macOS runner deployment --- playbooks/macos_actions_runner.yml | 7 +++++++ playbooks/site.yml | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 playbooks/macos_actions_runner.yml diff --git a/playbooks/macos_actions_runner.yml b/playbooks/macos_actions_runner.yml new file mode 100644 index 00000000..34c40aca --- /dev/null +++ b/playbooks/macos_actions_runner.yml @@ -0,0 +1,7 @@ +--- +# A macOS Actions runner is a host process, not an ARC runner pod: Xcode and macOS archive jobs cannot run in a Linux container. The role registers it with a short-lived GitHub App token and supervises its persistent process through launchd. +- name: Install managed macOS Actions runners + hosts: "{{ github_runner_macos_actions_runner_hosts | default(github_runner_cluster_group | default('github_runner_cluster')) }}" + gather_facts: true + roles: + - exadev.github_runner.github_runner_macos_actions_runner diff --git a/playbooks/site.yml b/playbooks/site.yml index 267431c7..6f1ad956 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -40,13 +40,6 @@ roles: - exadev.github_runner.github_runner_macos_host -# A macOS Actions runner is a host process, not an ARC runner pod: Xcode and macOS archive jobs cannot run in a Linux container. The role registers it with a short-lived GitHub App token and supervises its persistent process through launchd. -- name: Install managed macOS Actions runners - hosts: "{{ github_runner_cluster_group | default('github_runner_cluster') }}" - gather_facts: true - roles: - - exadev.github_runner.github_runner_macos_actions_runner - # Before the fleet play, so a Linux host that would suspend, or whose Wi-Fi would power-save, is fixed before k3s starts on it. Linux hosts only (the role does nothing on any other), and both of its steps are off unless a host's variables turn them on. The tasks that change the host use become (sudo), as do those of a native node (github_runner_cluster_node_mode: native), so run the play with passwordless sudo for the connecting user or with --ask-become-pass. It gathers its own facts: only the platform subset the role needs, so the host's Python is the only requirement. - name: Set up the Linux hosts' host baseline hosts: "{{ github_runner_cluster_group | default('github_runner_cluster') }}" From 614119fed735f5a1cc95ad2f1b7441687fea75a6 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 4 Nov 2026 23:24:28 +0000 Subject: [PATCH 3/6] fix: stop runner before replacement --- roles/github_runner_macos_actions_runner/tasks/main.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/roles/github_runner_macos_actions_runner/tasks/main.yml b/roles/github_runner_macos_actions_runner/tasks/main.yml index 313cc6d0..68d854c4 100644 --- a/roles/github_runner_macos_actions_runner/tasks/main.yml +++ b/roles/github_runner_macos_actions_runner/tasks/main.yml @@ -82,6 +82,14 @@ mode: "0644" when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version + - name: Stop the runner before replacing its installation + become: true + ansible.builtin.command: + argv: [launchctl, bootout, "system/{{ github_runner_macos_actions_runner_launchd_label }}"] + failed_when: false + changed_when: false + when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version + - name: Remove the prior runner installation ansible.builtin.file: path: "{{ github_runner_macos_actions_runner_root }}" From 6cefbebb6e52f63ef965d2977e6b6e6cc60b00cc Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 5 Nov 2026 05:11:20 +0000 Subject: [PATCH 4/6] fix: restrict macOS runner to one repository --- .../github_runner_macos_actions_runner/defaults/main.yml | 3 ++- .../meta/argument_specs.yml | 3 +++ roles/github_runner_macos_actions_runner/tasks/main.yml | 8 +++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/roles/github_runner_macos_actions_runner/defaults/main.yml b/roles/github_runner_macos_actions_runner/defaults/main.yml index d221ba5b..20e68874 100644 --- a/roles/github_runner_macos_actions_runner/defaults/main.yml +++ b/roles/github_runner_macos_actions_runner/defaults/main.yml @@ -1,7 +1,8 @@ --- -# A persistent macOS runner is deliberately separate from ARC: runner pods require Linux containers, while Xcode and macOS archive jobs must run on the host OS. The runner's registration credential is minted only while Ansible configures it; launchd runs the already-registered process without any GitHub credential. +# A persistent macOS runner is deliberately separate from ARC: runner pods require Linux containers, while Xcode and macOS archive jobs must run on the host OS. Restrict it to one repository whose workflows are equally trusted, because a persistent native runner is not a disposable cross-repository execution environment. The registration credential is minted only while Ansible configures it; launchd runs the already-registered process without any GitHub credential. github_runner_macos_actions_runner_enabled: false github_runner_macos_actions_runner_organization: "" +github_runner_macos_actions_runner_repository: "" github_runner_macos_actions_runner_app_id: "" github_runner_macos_actions_runner_installation_id: "" github_runner_macos_actions_runner_private_key: "" diff --git a/roles/github_runner_macos_actions_runner/meta/argument_specs.yml b/roles/github_runner_macos_actions_runner/meta/argument_specs.yml index e20f4e00..586dcee0 100644 --- a/roles/github_runner_macos_actions_runner/meta/argument_specs.yml +++ b/roles/github_runner_macos_actions_runner/meta/argument_specs.yml @@ -9,6 +9,9 @@ argument_specs: github_runner_macos_actions_runner_organization: type: str default: "" + github_runner_macos_actions_runner_repository: + type: str + default: "" github_runner_macos_actions_runner_app_id: type: str default: "" diff --git a/roles/github_runner_macos_actions_runner/tasks/main.yml b/roles/github_runner_macos_actions_runner/tasks/main.yml index 68d854c4..f96e3e04 100644 --- a/roles/github_runner_macos_actions_runner/tasks/main.yml +++ b/roles/github_runner_macos_actions_runner/tasks/main.yml @@ -34,6 +34,7 @@ ansible.builtin.assert: that: - github_runner_macos_actions_runner_organization | length > 0 + - github_runner_macos_actions_runner_repository | length > 0 - github_runner_macos_actions_runner_app_id | string | regex_search('^[0-9]+$') is not none - github_runner_macos_actions_runner_installation_id | string | regex_search('^[0-9]+$') is not none - github_runner_macos_actions_runner_private_key | length > 0 @@ -153,13 +154,14 @@ body_format: json body: permissions: - organization_self_hosted_runners: write + administration: write + metadata: read status_code: 201 register: github_runner_macos_actions_runner_installation_token - name: Request a runner registration token ansible.builtin.uri: - url: "https://api.github.com/orgs/{{ github_runner_macos_actions_runner_organization }}/actions/runners/registration-token" + url: "https://api.github.com/repos/{{ github_runner_macos_actions_runner_organization }}/{{ github_runner_macos_actions_runner_repository }}/actions/runners/registration-token" method: POST headers: Authorization: "Bearer {{ github_runner_macos_actions_runner_installation_token.json.token }}" @@ -180,7 +182,7 @@ - "{{ github_runner_macos_actions_runner_root }}/config.sh" - --unattended - --url - - "https://github.com/{{ github_runner_macos_actions_runner_organization }}" + - "https://github.com/{{ github_runner_macos_actions_runner_organization }}/{{ github_runner_macos_actions_runner_repository }}" - --token - "{{ github_runner_macos_actions_runner_registration_token.json.token }}" - --name From aac4cf8bdd13f75a0fe273a8a5ebccf905612922 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 5 Nov 2026 05:55:22 +0000 Subject: [PATCH 5/6] fix: isolate macOS runner account and scope --- .../defaults/main.yml | 5 +- .../meta/argument_specs.yml | 6 +++ .../tasks/main.yml | 54 +++++++++++++++++-- .../templates/runner.plist.j2 | 2 +- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/roles/github_runner_macos_actions_runner/defaults/main.yml b/roles/github_runner_macos_actions_runner/defaults/main.yml index 20e68874..2dc79f50 100644 --- a/roles/github_runner_macos_actions_runner/defaults/main.yml +++ b/roles/github_runner_macos_actions_runner/defaults/main.yml @@ -9,9 +9,12 @@ github_runner_macos_actions_runner_private_key: "" github_runner_macos_actions_runner_private_key_op_reference: "" github_runner_macos_actions_runner_label: exadev-macos github_runner_macos_actions_runner_name: "{{ inventory_hostname }}-macos" +github_runner_macos_actions_runner_user: github-runner +github_runner_macos_actions_runner_uid: 401 github_runner_macos_actions_runner_version: "2.338.2" github_runner_macos_actions_runner_sha256: 25a065af409ffa317ab4d5a3b614dd15fc9c40f2172bc1b35a1b1258e919a388 -github_runner_macos_actions_runner_root: "{{ ansible_facts['env']['HOME'] }}/.github-actions-runner" +# Repository identity is part of the directory. Changing the repository therefore creates a new unregistered runner rather than leaving a runner enrolled in the old scope. +github_runner_macos_actions_runner_root: "/var/lib/github-actions-runner/{{ github_runner_macos_actions_runner_organization }}-{{ github_runner_macos_actions_runner_repository }}" github_runner_macos_actions_runner_work_dir: "{{ github_runner_macos_actions_runner_root }}/_work" github_runner_macos_actions_runner_plist: "/Library/LaunchDaemons/io.exadev.github-actions-runner.{{ github_runner_macos_actions_runner_name }}.plist" github_runner_macos_actions_runner_launchd_label: "io.exadev.github-actions-runner.{{ github_runner_macos_actions_runner_name }}" diff --git a/roles/github_runner_macos_actions_runner/meta/argument_specs.yml b/roles/github_runner_macos_actions_runner/meta/argument_specs.yml index 586dcee0..fedf910a 100644 --- a/roles/github_runner_macos_actions_runner/meta/argument_specs.yml +++ b/roles/github_runner_macos_actions_runner/meta/argument_specs.yml @@ -32,6 +32,12 @@ argument_specs: github_runner_macos_actions_runner_name: type: str default: "" + github_runner_macos_actions_runner_user: + type: str + default: github-runner + github_runner_macos_actions_runner_uid: + type: int + default: 401 github_runner_macos_actions_runner_version: type: str default: "2.338.2" diff --git a/roles/github_runner_macos_actions_runner/tasks/main.yml b/roles/github_runner_macos_actions_runner/tasks/main.yml index f96e3e04..6eae8d5c 100644 --- a/roles/github_runner_macos_actions_runner/tasks/main.yml +++ b/roles/github_runner_macos_actions_runner/tasks/main.yml @@ -60,14 +60,53 @@ - name: Configure the enabled macOS runner when: github_runner_macos_actions_runner_enabled | bool block: + - name: Read the runner account + ansible.builtin.command: + argv: [dscl, ., -read, "/Users/{{ github_runner_macos_actions_runner_user }}", UniqueID] + register: github_runner_macos_actions_runner_account + changed_when: false + failed_when: false + check_mode: false + + - name: Create the unprivileged runner account + become: true + ansible.builtin.command: + argv: + - sysadminctl + - -addUser + - "{{ github_runner_macos_actions_runner_user }}" + - -fullName + - GitHub Actions Runner + - -UID + - "{{ github_runner_macos_actions_runner_uid }}" + - -shell + - /usr/bin/false + - -home + - "{{ github_runner_macos_actions_runner_root }}" + - -password + - '*' + no_log: true + changed_when: true + when: github_runner_macos_actions_runner_account.rc != 0 + + - name: Require the runner account's expected identity + ansible.builtin.assert: + that: + - "github_runner_macos_actions_runner_account.rc != 0 or github_runner_macos_actions_runner_account.stdout is regex('UniqueID: ' ~ (github_runner_macos_actions_runner_uid | string) ~ '$')" + quiet: true + when: not ansible_check_mode + - name: Create the runner directory + become: true ansible.builtin.file: path: "{{ github_runner_macos_actions_runner_root }}" state: directory - owner: "{{ ansible_facts['user_id'] }}" - mode: "0755" + owner: "{{ github_runner_macos_actions_runner_user }}" + mode: "0750" - name: Check the installed runner version + become: true + become_user: "{{ github_runner_macos_actions_runner_user }}" ansible.builtin.command: argv: ["{{ github_runner_macos_actions_runner_root }}/bin/Runner.Listener", --version] register: github_runner_macos_actions_runner_installed_version @@ -76,6 +115,7 @@ check_mode: false - name: Download the pinned Apple Silicon runner package + become: true ansible.builtin.get_url: url: "https://github.com/actions/runner/releases/download/v{{ github_runner_macos_actions_runner_version }}/actions-runner-osx-arm64-{{ github_runner_macos_actions_runner_version }}.tar.gz" dest: "{{ github_runner_macos_actions_runner_root }}-{{ github_runner_macos_actions_runner_version }}.tar.gz" @@ -98,19 +138,21 @@ when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version - name: Recreate the runner directory + become: true ansible.builtin.file: path: "{{ github_runner_macos_actions_runner_root }}" state: directory - owner: "{{ ansible_facts['user_id'] }}" - mode: "0755" + owner: "{{ github_runner_macos_actions_runner_user }}" + mode: "0750" when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version - name: Install the pinned Apple Silicon runner package + become: true ansible.builtin.unarchive: src: "{{ github_runner_macos_actions_runner_root }}-{{ github_runner_macos_actions_runner_version }}.tar.gz" dest: "{{ github_runner_macos_actions_runner_root }}" remote_src: true - owner: "{{ ansible_facts['user_id'] }}" + owner: "{{ github_runner_macos_actions_runner_user }}" when: github_runner_macos_actions_runner_installed_version.stdout | trim != github_runner_macos_actions_runner_version - name: Check whether the runner is registered @@ -177,6 +219,8 @@ when: github_runner_macos_actions_runner_key_file.path is defined - name: Configure the macOS runner + become: true + become_user: "{{ github_runner_macos_actions_runner_user }}" ansible.builtin.command: argv: - "{{ github_runner_macos_actions_runner_root }}/config.sh" diff --git a/roles/github_runner_macos_actions_runner/templates/runner.plist.j2 b/roles/github_runner_macos_actions_runner/templates/runner.plist.j2 index a7cf6a54..62fb24d0 100644 --- a/roles/github_runner_macos_actions_runner/templates/runner.plist.j2 +++ b/roles/github_runner_macos_actions_runner/templates/runner.plist.j2 @@ -11,7 +11,7 @@ WorkingDirectory {{ github_runner_macos_actions_runner_root }} UserName - {{ ansible_facts['user_id'] }} + {{ github_runner_macos_actions_runner_user }} EnvironmentVariables PATH From 947d3a80c4ad938f944b7a20b02fc8d1753e8c13 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 5 Nov 2026 06:31:47 +0000 Subject: [PATCH 6/6] fix: keep runner registration token off argv --- roles/github_runner_macos_actions_runner/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/github_runner_macos_actions_runner/tasks/main.yml b/roles/github_runner_macos_actions_runner/tasks/main.yml index 6eae8d5c..a425b7ba 100644 --- a/roles/github_runner_macos_actions_runner/tasks/main.yml +++ b/roles/github_runner_macos_actions_runner/tasks/main.yml @@ -227,8 +227,6 @@ - --unattended - --url - "https://github.com/{{ github_runner_macos_actions_runner_organization }}/{{ github_runner_macos_actions_runner_repository }}" - - --token - - "{{ github_runner_macos_actions_runner_registration_token.json.token }}" - --name - "{{ github_runner_macos_actions_runner_name }}" - --labels @@ -236,6 +234,8 @@ - --work - "{{ github_runner_macos_actions_runner_work_dir }}" - --replace + environment: + ACTIONS_RUNNER_INPUT_TOKEN: "{{ github_runner_macos_actions_runner_registration_token.json.token }}" no_log: true changed_when: true when: not github_runner_macos_actions_runner_registration.stat.exists