From 885a0e3af985a37cf9dd9ab65dbec6ca4efbf557 Mon Sep 17 00:00:00 2001 From: Isaac Halvorson Date: Thu, 24 Sep 2026 12:20:53 -0500 Subject: [PATCH] remove references to orka throughout project --- .editorconfig | 4 ++ README.md | 47 +++++++------------ ...-actions-runners-macos-vm-bootstrapping.md | 7 +++ inventory/group_vars/macos_agents.yml | 4 ++ inventory/group_vars/orka_agents.yml | 7 --- inventory/group_vars/tart_agents.yml | 8 ---- inventory/macos_inventory.py | 45 ++++-------------- mise.toml | 19 +------- playbooks/provision.yml | 2 +- roles/auth_config/files/kcpassword.py | 9 ++-- roles/homebrew/defaults/main.yml | 3 -- roles/homebrew/tasks/main.yml | 9 ---- roles/system_config/defaults/main.yml | 2 - .../hostname/set-dynamic-hostname.sh} | 3 +- roles/system_config/tasks/hostname.yml | 6 +-- 15 files changed, 53 insertions(+), 122 deletions(-) delete mode 100644 inventory/group_vars/orka_agents.yml delete mode 100644 inventory/group_vars/tart_agents.yml delete mode 100644 roles/homebrew/defaults/main.yml delete mode 100644 roles/homebrew/tasks/main.yml delete mode 100644 roles/system_config/defaults/main.yml rename roles/system_config/{templates/hostname/set-dynamic-hostname.sh.j2 => files/hostname/set-dynamic-hostname.sh} (71%) diff --git a/.editorconfig b/.editorconfig index c44b1fa..8a9f6ad 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,10 @@ indent_style = tab insert_final_newline = true trim_trailing_whitespace = true +[*.py] +indent_size = 4 +indent_style = space + [{*.hcl,*.pkl,*.yaml,*.yml,mise.toml}] indent_size = 2 indent_style = space diff --git a/README.md b/README.md index f2a7c33..5a46f64 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # hisaac-ci -Ansible-first automation for provisioning macOS CI agents, with Tart VM support for local iteration. +Ansible-first automation for provisioning macOS CI agents using Tart for VM management. ## Overview -This repository provisions macOS hosts (VMs or physical machines) using Ansible roles and playbooks. +This repository provisions macOS Tart VMs using Ansible roles and playbooks. The current active stack is: - Root-level Ansible project (`ansible.cfg`, `inventory/`, `playbooks/`, `roles/`) -- Dynamic Tart inventory (`inventory/tart_inventory.py`) for VM IP discovery at runtime +- Dynamic Tart inventory (`inventory/macos_inventory.py`) for VM IP discovery at runtime - Role-scoped reference scripts in `scripts/` used as implementation references and helpers - Toolchain and workflow orchestration via `mise` @@ -20,13 +20,11 @@ The current active stack is: ├── ansible.cfg ├── requirements.yml ├── inventory/ -│ ├── tart_inventory.py +│ ├── macos_inventory.py │ └── group_vars/ │ ├── macos_agents.yml │ ├── macos_15_agents.yml -│ ├── macos_26_agents.yml -│ ├── tart_agents.yml -│ └── orka_agents.yml +│ └── macos_26_agents.yml ├── playbooks/ │ └── provision.yml ├── roles/ @@ -36,7 +34,6 @@ The current active stack is: │ ├── ssh_config/ │ ├── system_config/ │ ├── auth_config/ -│ ├── homebrew/ │ └── xcode/ ├── scripts/ │ ├── xcode/ @@ -81,18 +78,15 @@ Run commands from the repository root. ## Inventory and Variables -`ansible.cfg` points to `inventory/tart_inventory.py` as the default inventory. +Pass `-i inventory/macos_inventory.py` and set `VM_NAME` to the Tart VM to provision. -- `inventory/tart_inventory.py` discovers VM IPs via `tart ip ` -- Hosts are grouped into: - - `macos_agents` (parent) - - `tart_agents` (runtime Tart VM group) - - version-specific groups like `macos_15_agents` and `macos_26_agents` +- `inventory/macos_inventory.py` discovers the VM IP via `tart ip ` +- The VM belongs directly to `macos_agents` +- The playbook loads version-specific configuration after detecting the guest macOS version Primary variable files: -- `inventory/group_vars/macos_agents.yml`: shared settings (admin credentials, Homebrew base packages, Dock config) -- `inventory/group_vars/tart_agents.yml`: Tart-specific packages (`tart-guest-agent`) +- `inventory/group_vars/macos_agents.yml`: agent settings (admin credentials, Homebrew packages including `tart-guest-agent`, the `openai/tools` tap, and Dock config) - `inventory/group_vars/macos_15_agents.yml` and `inventory/group_vars/macos_26_agents.yml`: Xcode/runtime/simulator presets ## Xcode Cache Inputs @@ -104,15 +98,15 @@ The `xcode` role installs all `*.xip` files found in the admin user's Downloads Run the full playbook: ```bash -ansible-playbook playbooks/provision.yml +VM_NAME=macos:26 ansible-playbook playbooks/provision.yml -i inventory/macos_inventory.py ``` Or run common role slices with tags: ```bash -ansible-playbook playbooks/provision.yml --tags xcode -ansible-playbook playbooks/provision.yml --tags homebrew -ansible-playbook playbooks/provision.yml --tags system_config +VM_NAME=macos:26 ansible-playbook playbooks/provision.yml -i inventory/macos_inventory.py --tags xcode +VM_NAME=macos:26 ansible-playbook playbooks/provision.yml -i inventory/macos_inventory.py --tags homebrew +VM_NAME=macos:26 ansible-playbook playbooks/provision.yml -i inventory/macos_inventory.py --tags system_config ``` Playbook role order in `playbooks/provision.yml` is intentional: @@ -123,7 +117,7 @@ Playbook role order in `playbooks/provision.yml` is intentional: 4. `ssh_config` 5. `system_config` 6. `auth_config` -7. `homebrew` +7. `geerlingguy.mac.homebrew` 8. `geerlingguy.mac.dock` 9. `xcode` 10. Spotlight re-index task @@ -133,15 +127,10 @@ Playbook role order in `playbooks/provision.yml` is intentional: `mise` includes helper tasks for local Tart loops: -- `mise run run-tart-vms`: starts configured VMs and waits for SSH readiness -- `mise run provision`: verifies Xcode/simruntime cache inputs, starts VMs, then runs `ansible-playbook playbooks/provision.yml` -- `mise run shutdown-tart-vms`: shuts down hosts in `tart_agents` -- `mise run refresh-tart-vms`: recreates local Tart VM clones from frozen images - -Adjust VM names in: +- `mise run run-tart-vm macos:26`: starts the named Tart VM and waits for SSH readiness +- `mise run provision macos:26`: starts the named Tart VM, then runs the provisioning playbook -- `inventory/tart_inventory.py` (`VMS` list) -- `mise.toml` (`tasks.run-tart-vms` and `tasks.refresh-tart-vms`) +Replace `macos:26` with your local Tart VM name. ## Linting and Formatting diff --git a/docs/github-actions-runners-macos-vm-bootstrapping.md b/docs/github-actions-runners-macos-vm-bootstrapping.md index 5b02d26..021e04e 100644 --- a/docs/github-actions-runners-macos-vm-bootstrapping.md +++ b/docs/github-actions-runners-macos-vm-bootstrapping.md @@ -1,5 +1,12 @@ # macOS VM Bootstrapping: How GitHub Actions Runner Images Work +> **Provenance:** Local research summary of the macOS image-building scripts in +> [`actions/runner-images`](https://github.com/actions/runner-images), not an upstream document or a provisioning guide for this repository. +> +> **Last content update:** May 22, 2026, in local commit +> [`a19b31b9c25a`](https://github.com/hisaac/ci/commit/a19b31b9c25adf7c15685c4bdedef8b4a79339ea). +> The original upstream revision was not recorded. + This document explains how macOS virtual machines are bootstrapped for GitHub Actions runners. The VMs are not truly headless — they boot into a full GUI session — but are configured so that no human interaction is ever required. This enables unattended UI automation, Xcode testing, and job provisioning. --- diff --git a/inventory/group_vars/macos_agents.yml b/inventory/group_vars/macos_agents.yml index 5d9337d..c6d054b 100644 --- a/inventory/group_vars/macos_agents.yml +++ b/inventory/group_vars/macos_agents.yml @@ -15,6 +15,9 @@ macos_update_enabled: true homebrew_upgrade_all_packages: true +homebrew_taps: + - openai/tools + homebrew_installed_packages: - bash - curl @@ -23,6 +26,7 @@ homebrew_installed_packages: - git-lfs - mise - python + - tart-guest-agent - unxip - xcodes diff --git a/inventory/group_vars/orka_agents.yml b/inventory/group_vars/orka_agents.yml deleted file mode 100644 index d672398..0000000 --- a/inventory/group_vars/orka_agents.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -homebrew_installed_packages_additional: - - orka-vm-tools - -macos_update_enabled: true - -system_config_hostname_prefix: orka diff --git a/inventory/group_vars/tart_agents.yml b/inventory/group_vars/tart_agents.yml deleted file mode 100644 index 296468d..0000000 --- a/inventory/group_vars/tart_agents.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -homebrew_taps_additional: - - cirruslabs/cli - -homebrew_installed_packages_additional: - - tart-guest-agent - -system_config_hostname_prefix: tart diff --git a/inventory/macos_inventory.py b/inventory/macos_inventory.py index 4bb1e98..a051e96 100755 --- a/inventory/macos_inventory.py +++ b/inventory/macos_inventory.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 """ -Dynamic inventory script for macOS VMs (Tart or Orka). +Dynamic inventory script for Tart VMs. -Set VM_PROVIDER to 'tart' or 'orka', and VM_NAME to the VM name. +Set VM_NAME to the VM name. Usage (Ansible calls these automatically): - VM_PROVIDER=tart VM_NAME=macos:26 ./macos_inventory.py --list - VM_PROVIDER=orka VM_NAME=orka-agent-thmpm ./macos_inventory.py --list - VM_PROVIDER=tart VM_NAME=macos:26 ./macos_inventory.py --host + VM_NAME=macos:26 ./macos_inventory.py --list + ./macos_inventory.py --host macos:26 """ import json @@ -29,54 +28,26 @@ def get_tart_hostvars(vm_name: str) -> dict | None: return {"ansible_host": ip} if ip else None -def get_orka_hostvars(vm_name: str) -> dict | None: - result = subprocess.run( - ["orka3", "vm", "list", "-o", "json"], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - print(f"Error: orka3 vm list failed: {result.stderr}", file=sys.stderr) - return None - for vm in json.loads(result.stdout): - if vm.get("name") == vm_name and vm.get("status") == "Running": - return {"ansible_host": vm["ip"], "ansible_port": vm["ssh"]} - return None - - def list_inventory() -> dict: - provider = os.environ.get("VM_PROVIDER", "").strip().lower() vm_name = os.environ.get("VM_NAME", "").strip() - if not provider: - print("Error: VM_PROVIDER environment variable is not set (tart or orka)", file=sys.stderr) - sys.exit(1) - if provider not in ("tart", "orka"): - print(f"Error: VM_PROVIDER must be 'tart' or 'orka', got '{provider}'", file=sys.stderr) - sys.exit(1) if not vm_name: print("Error: VM_NAME environment variable is not set", file=sys.stderr) sys.exit(1) - get_hostvars = get_tart_hostvars if provider == "tart" else get_orka_hostvars - hostvars = get_hostvars(vm_name) + hostvars = get_tart_hostvars(vm_name) if not hostvars: print(f"Error: could not find running VM '{vm_name}'", file=sys.stderr) sys.exit(1) - provider_group = f"{provider}_agents" return { - "macos_agents": {"children": [provider_group]}, - provider_group: {"hosts": [vm_name]}, + "macos_agents": {"hosts": [vm_name]}, "_meta": {"hostvars": {vm_name: hostvars}}, } def host_vars(vm_name: str) -> dict: - provider = os.environ.get("VM_PROVIDER", "").strip().lower() - get_hostvars = get_tart_hostvars if provider == "tart" else get_orka_hostvars - return get_hostvars(vm_name) or {} + return get_tart_hostvars(vm_name) or {} if __name__ == "__main__": @@ -87,7 +58,7 @@ def host_vars(vm_name: str) -> dict: print(json.dumps(host_vars(sys.argv[idx + 1]))) else: print( - "Usage: VM_PROVIDER= VM_NAME= macos_inventory.py [--list | --host ]", + "Usage: VM_NAME= macos_inventory.py [--list | --host ]", file=sys.stderr, ) sys.exit(1) diff --git a/mise.toml b/mise.toml index 5a35c24..6bf7fc5 100644 --- a/mise.toml +++ b/mise.toml @@ -65,25 +65,10 @@ until nc -z "$ip" 22 &>/dev/null; do sleep 2; done echo "$vm is ready" ''' -[tasks.refresh-orka-vm] -usage = ''' -arg "" help="Name of the Orka VM to delete and recreate" -arg "" help="Name of the VM Config to recreate the VM with" -''' -run = [ - "orka3 vm delete {{ usage.vm }} || true", - "orka3 vm deploy {{ usage.vm }} --config {{ usage.vm_config }}", -] - -[tasks.provision-orka] -usage = 'arg "" help="Name of the Orka VM to provision"' -env = { VM_PROVIDER = "orka" } -run = 'VM_NAME="{{ usage.vm }}" ansible-playbook playbooks/provision.yml -i inventory/macos_inventory.py' - -[tasks.provision-tart] +[tasks.provision] usage = 'arg "" help="Name of the Tart VM to provision"' depends = ["run-tart-vm {{ usage.vm }}"] -env = { VM_PROVIDER = "tart" } + run = 'VM_NAME="{{ usage.vm }}" ansible-playbook playbooks/provision.yml -i inventory/macos_inventory.py' [tasks.upd] diff --git a/playbooks/provision.yml b/playbooks/provision.yml index c83a9f9..0925c90 100644 --- a/playbooks/provision.yml +++ b/playbooks/provision.yml @@ -107,7 +107,7 @@ hosts: macos_agents tags: [homebrew] roles: - - role: homebrew + - role: geerlingguy.mac.homebrew - name: Configure git hosts: macos_agents diff --git a/roles/auth_config/files/kcpassword.py b/roles/auth_config/files/kcpassword.py index d274cbb..3269d32 100755 --- a/roles/auth_config/files/kcpassword.py +++ b/roles/auth_config/files/kcpassword.py @@ -13,7 +13,7 @@ def encode_data(passwd): # The magic 11 bytes - these are just repeated # 0x7D 0x89 0x52 0x23 0xD2 0xBC 0xDD 0xEA 0xA3 0xB9 0x1F - key = [125,137,82,35,210,188,221,234,163,185,31] + key = [125, 137, 82, 35, 210, 188, 221, 234, 163, 185, 31] key_len = len(key) passwd = [ord(x) for x in list(passwd)] @@ -21,7 +21,7 @@ def encode_data(passwd): r = len(passwd) % key_len if len(passwd) == 11: passwd += [0] - elif (r > 0): + elif r > 0: passwd = passwd + [0] * (key_len - r) for j in range(len(passwd)): @@ -29,10 +29,11 @@ def encode_data(passwd): return bytearray(passwd) + if __name__ == "__main__": runner_pwd_encoded = encode_data(sys.argv[1]) - fd = os.open('/etc/kcpassword', os.O_WRONLY | os.O_CREAT, 0o600) - file = os.fdopen(fd, 'wb') + fd = os.open("/etc/kcpassword", os.O_WRONLY | os.O_CREAT, 0o600) + file = os.fdopen(fd, "wb") file.truncate(0) file.write(runner_pwd_encoded) file.close() diff --git a/roles/homebrew/defaults/main.yml b/roles/homebrew/defaults/main.yml deleted file mode 100644 index 7909b04..0000000 --- a/roles/homebrew/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -homebrew_installed_packages_additional: [] -homebrew_taps_additional: [] diff --git a/roles/homebrew/tasks/main.yml b/roles/homebrew/tasks/main.yml deleted file mode 100644 index 15610f6..0000000 --- a/roles/homebrew/tasks/main.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- name: Append additional packages to installed list - ansible.builtin.set_fact: - homebrew_installed_packages: "{{ homebrew_installed_packages + homebrew_installed_packages_additional }}" - homebrew_taps: "{{ homebrew_taps + homebrew_taps_additional }}" - -- name: Install Homebrew and packages - ansible.builtin.import_role: - name: geerlingguy.mac.homebrew diff --git a/roles/system_config/defaults/main.yml b/roles/system_config/defaults/main.yml deleted file mode 100644 index 0d3a1be..0000000 --- a/roles/system_config/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -system_config_hostname_prefix: Mac diff --git a/roles/system_config/templates/hostname/set-dynamic-hostname.sh.j2 b/roles/system_config/files/hostname/set-dynamic-hostname.sh similarity index 71% rename from roles/system_config/templates/hostname/set-dynamic-hostname.sh.j2 rename to roles/system_config/files/hostname/set-dynamic-hostname.sh index 1ea046a..055d032 100644 --- a/roles/system_config/templates/hostname/set-dynamic-hostname.sh.j2 +++ b/roles/system_config/files/hostname/set-dynamic-hostname.sh @@ -4,8 +4,7 @@ # Inspired by: # https://github.com/actions/runner-images/blob/main/images/macos/scripts/build/configure-hostname.sh -prefix="{{ system_config_hostname_prefix | default('Mac') }}" -name="${prefix}-$(python3 -c 'from time import time; print(int(round(time() * 1000)))')" +name="tart-$(/usr/bin/uuidgen)" scutil --set HostName "${name}.local" scutil --set LocalHostName "${name}" scutil --set ComputerName "${name}.local" diff --git a/roles/system_config/tasks/hostname.yml b/roles/system_config/tasks/hostname.yml index c3a50ed..e6cfb88 100644 --- a/roles/system_config/tasks/hostname.yml +++ b/roles/system_config/tasks/hostname.yml @@ -2,7 +2,7 @@ # Install a LaunchDaemon that generates a unique hostname on each boot. # Since many VMs are cloned from the same base image and run simultaneously, # they'd all share the same hostname causing Bonjour/mDNS conflicts. -# A millisecond-precision timestamp ensures uniqueness without external coordination. +# A random UUID avoids hostname collisions without external coordination. # # Inspired by: # https://github.com/actions/runner-images/blob/main/images/macos/scripts/build/configure-hostname.sh @@ -15,8 +15,8 @@ become: true - name: Install dynamic hostname script - ansible.builtin.template: - src: hostname/set-dynamic-hostname.sh.j2 + ansible.builtin.copy: + src: hostname/set-dynamic-hostname.sh dest: /usr/local/bin/set-dynamic-hostname.sh owner: root mode: "0755"