From 7bb31676ac73ed6186af53deabb19ac14c5a7909 Mon Sep 17 00:00:00 2001 From: Christopher Maher Date: Thu, 21 May 2026 00:04:05 -0700 Subject: [PATCH 1/2] feat(ci): add free macOS Apple Silicon E2E jobs Standard macos-26 runners on GitHub Actions are Apple Silicon (M1, 3 CPU, 7 GB RAM, 14 GB disk) and free for public repos with no minute limit. That's exactly the platform this repo targets, so it would be silly not to use them. What - playbook.yml: add per-role tags so CI can run a subset (system + homebrew) cleanly. Every base-path role now carries both 'base' and its own role name as tags. - .github/workflows/ci.yml: two new macOS jobs. - macos-check: ansible-playbook --syntax-check, then runs ONLY the pre_tasks asserts via --tags always. Proves the playbook boots cleanly on a real Apple Silicon mac without doing any installs. ~3 min. - macos-homebrew: runs system + homebrew roles for real. Catches typo'd brew formulas / casks before a user hits them on their own Mac. Stops there because the kubernetes role wants to launch Docker Desktop which is not viable inside a CI runner. ~5-10 min. Why The Ubuntu lint jobs catch about 80% of the things that would break the bootstrap. The remaining 20% are platform-bound: a brew formula typo, an Ansible module that behaves differently on macOS than linux, the arm64 + Sequoia preflight asserts themselves. Free macOS runners make checking that 20% on every PR cost zero. What is NOT in this PR - Full bootstrap end-to-end. The kubernetes role tries to start Docker Desktop; the model_starter role wants a real llama-server. Both are out of reach of a 7 GB / no-GPU-passthrough runner. Could be a label-gated Tier-3 job later if we move to a self-hosted Mac Mini. - Adding these new jobs as required status checks on main. Want to see them stable across a few PRs before promoting; require yamllint + ansible-lint + shellcheck for now. Signed-off-by: Christopher Maher --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++++++++++ playbook.yml | 18 ++++++++-------- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96f333c..ecf0100 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,3 +56,47 @@ jobs: with: scandir: "." ignore_paths: "collections .github" + + # --- macOS jobs -------------------------------------------------------- + # + # Standard GitHub-hosted macos-26 runners are Apple Silicon (M1, 3 CPU, + # 7 GB RAM, 14 GB disk) and free for public repos. These two jobs catch + # platform-specific bugs that the Ubuntu lint jobs cannot see. + + macos-check: + name: macos-check (syntax + asserts) + runs-on: macos-26 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install Ansible via Homebrew + run: brew install ansible + - name: Install Ansible Galaxy collections + run: ansible-galaxy collection install -r requirements.yml + - name: ansible-playbook --syntax-check + run: ansible-playbook -i inventory/localhost.yml playbook.yml --syntax-check + - name: Run preflight asserts only + # --tags always pulls just the pre_tasks block (macOS / arm64 / + # Sequoia-or-later asserts). Proves the playbook can boot on a + # real Apple Silicon mac without doing any installs. + run: ansible-playbook -i inventory/localhost.yml playbook.yml --tags always + + macos-homebrew: + name: macos-homebrew (install package set) + runs-on: macos-26 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - name: Install Ansible via Homebrew + run: brew install ansible + - name: Install Ansible Galaxy collections + run: ansible-galaxy collection install -r requirements.yml + - name: Run system + homebrew roles + # We stop here on purpose: the kubernetes role wants to launch + # Docker Desktop, which is not viable inside a CI runner. The + # homebrew install path is the high-leverage check for catching + # typo'd brew formulas / casks before a user hits them. + run: | + ansible-playbook -i inventory/localhost.yml playbook.yml \ + --tags 'always,system,homebrew' \ + --diff diff --git a/playbook.yml b/playbook.yml index 7f5b880..82fc018 100644 --- a/playbook.yml +++ b/playbook.yml @@ -44,21 +44,23 @@ tags: [always] roles: - # Base path -- everyone gets these in order. + # Base path -- everyone gets these in order. Each role also + # gets a tag of its own name so CI can run a subset cleanly + # (e.g. --tags system,homebrew for the macOS install test). - role: system - tags: [base] + tags: [base, system] - role: homebrew - tags: [base] + tags: [base, homebrew] - role: kubernetes - tags: [base] + tags: [base, kubernetes] - role: llmkube_core - tags: [base] + tags: [base, llmkube_core] - role: model_starter - tags: [base] + tags: [base, model_starter] - role: developer_tools - tags: [base] + tags: [base, developer_tools] - role: opencode - tags: [base] + tags: [base, opencode] # Opt-in -- only when --tags includes the matching tag. - role: carnice From 895cd75e41c176e981432504bd6214ca56d99a27 Mon Sep 17 00:00:00 2001 From: Christopher Maher Date: Thu, 21 May 2026 00:07:18 -0700 Subject: [PATCH 2/2] fix(ci): use ansible.builtin.default callback; rename collections_paths CI's macos-26 runner installs ansible 13.6.0 (current) which ships community.general 12.0.0+. That release removed the community.general.yaml callback plugin that our ansible.cfg was asking for under stdout_callback = yaml: [ERROR]: The 'community.general.yaml' callback plugin has been removed. The plugin has been superseded by the option `result_format=yaml` in callback plugin ansible.builtin.default from ansible-core 2.13 onwards. ansible-playbook exited 1 before our preflight tasks could run, so both macos-check and macos-homebrew jobs failed at first contact. What - stdout_callback = yaml -> stdout_callback = ansible.builtin.default - Add [callback_default] section with result_format = yaml so the output still renders in YAML (more readable when debug tasks fire) - collections_paths (deprecated) -> collections_path (current) Why The old config was written against an older Ansible cookbook style; this brings it forward to ansible-core 2.13+ idioms. No behavior change for users; CI now matches what local installs see on a fresh brew install ansible. How Verified locally: the same command CI runs (`ansible-playbook -i inventory/localhost.yml playbook.yml --tags always`) now passes with the four preflight asserts all OK. Signed-off-by: Christopher Maher --- ansible.cfg | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ansible.cfg b/ansible.cfg index 795a6ff..69b675f 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,14 +1,20 @@ [defaults] inventory = inventory/localhost.yml roles_path = roles -collections_paths = ./collections:~/.ansible/collections +collections_path = ./collections:~/.ansible/collections host_key_checking = False nocows = True -stdout_callback = yaml +# Use the in-tree default callback (community.general.yaml was +# removed in community.general v12; modern Ansible wants the +# `result_format` knob on ansible.builtin.default instead). +stdout_callback = ansible.builtin.default display_skipped_hosts = False retry_files_enabled = False interpreter_python = auto_silent +[callback_default] +result_format = yaml + [inventory] enable_plugins = yaml, ini, auto