commit 388fcaac7a35a4c47984c7eb933a8134410d3478 Author: Chris Roberts <chris.roberts@learningunix.net> Date: Wed, 20 May 2026 05:08:38 -0500 initial commit. Added plays for installing homebrew, xcode, chrome, and iterm2 Diffstat:
| A | .gitignore | | | 14 | ++++++++++++++ |
| A | claude.md | | | 195 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | group_vars/all.yml | | | 9 | +++++++++ |
| A | inventory/local.ini | | | 2 | ++ |
| A | requirements.yml | | | 4 | ++++ |
| A | roles/chrome/defaults/main.yml | | | 1 | + |
| A | roles/chrome/meta/main.yml | | | 10 | ++++++++++ |
| A | roles/chrome/tasks/main.yml | | | 7 | +++++++ |
| A | roles/homebrew/defaults/main.yml | | | 5 | +++++ |
| A | roles/homebrew/meta/main.yml | | | 9 | +++++++++ |
| A | roles/homebrew/tasks/main.yml | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
| A | roles/iterm2/defaults/main.yml | | | 1 | + |
| A | roles/iterm2/meta/main.yml | | | 10 | ++++++++++ |
| A | roles/iterm2/tasks/main.yml | | | 20 | ++++++++++++++++++++ |
| A | roles/powerlevel10k/defaults/main.yml | | | 1 | + |
| A | roles/powerlevel10k/meta/main.yml | | | 10 | ++++++++++ |
| A | roles/powerlevel10k/tasks/main.yml | | | 39 | +++++++++++++++++++++++++++++++++++++++ |
| A | roles/xcode_cli/defaults/main.yml | | | 1 | + |
| A | roles/xcode_cli/meta/main.yml | | | 9 | +++++++++ |
| A | roles/xcode_cli/tasks/main.yml | | | 22 | ++++++++++++++++++++++ |
| A | site.yml | | | 10 | ++++++++++ |
21 files changed, 419 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -0,0 +1,14 @@ +# Ansible +ansible.cfg +*.retry +.vault_pass + +# iTerm2 prefs (may contain stored credentials) +roles/iterm2/files/com.googlecode.iterm2.plist + +# macOS +.DS_Store + +# Python +__pycache__/ +*.pyc diff --git a/claude.md b/claude.md @@ -0,0 +1,195 @@ +# Ansible macOS QA Workstation Provisioning + +## Project Overview + +Ansible playbook and roles for provisioning a MacBook Pro for a QA analyst. +Targets macOS (Apple Silicon and Intel compatible). Uses Ansible roles with +proper plugin-based task modules wherever possible — avoid `command`/`shell` +one-offs unless there is no suitable module. + +--- + +## Target Environment + +- **OS**: macOS (Sonoma or later) +- **User**: QA analyst +- **Connection**: `local` (runs on the Mac itself via `ansible-playbook --connection=local`) +- **Python**: System Python or Homebrew Python; do not assume a specific path + +--- + +## Role Structure + +Organize as individual roles under `roles/`. Each role is self-contained with +its own `tasks/`, `defaults/`, `vars/`, `handlers/`, and `meta/` directories +as needed. + +``` +roles/ + homebrew/ + xcode_cli/ + chrome/ + iterm2/ + powerlevel10k/ +``` + +Entry point: `site.yml` applies all roles in order. + +--- + +## Roles — Requirements and Constraints + +### homebrew + +- Use the [`community.general.homebrew`](https://docs.ansible.com/ansible/latest/collections/community/general/homebrew_module.html) module for all package installs. +- Use [`community.general.homebrew_cask`](https://docs.ansible.com/ansible/latest/collections/community/general/homebrew_cask_module.html) for cask installs. +- Bootstrap Homebrew itself only if it is not already installed. Use a + `stat` module check on `/opt/homebrew/bin/brew` (Apple Silicon) and + `/usr/local/bin/brew` (Intel) to detect presence before bootstrapping. +- Bootstrapping Homebrew requires a `command` task (the install script); + this is acceptable as a narrow exception since no plugin covers it. +- Set `HOMEBREW_NO_AUTO_UPDATE=1` in the environment for install tasks to + keep runs fast and deterministic. +- Tap `homebrew/cask` if not already present using `community.general.homebrew_tap`. + +### xcode_cli + +- Install Xcode Command Line Tools. +- Check for existing install using the `stat` module on + `/Library/Developer/CommandLineTools/usr/bin/git`. +- If absent, trigger install via `command: xcode-select --install` — this is + acceptable since no module covers CLT installation. +- After triggering, wait for completion using a `wait_for` or loop on the + `stat` check with `retries` and `delay`. Document that this step may + require user interaction on a fresh machine. +- Register the result and skip if already installed. + +### chrome + +- Install Google Chrome via `community.general.homebrew_cask` with + `name: google-chrome`. +- Ensure the role is idempotent: running it twice should produce no changes + the second time. + +### iterm2 + +- Install iTerm2 via `community.general.homebrew_cask` with `name: iterm2`. +- Idempotent same as chrome role. +- Optionally apply a default preferences plist if a `files/com.googlecode.iterm2.plist` + is present in the role — use the `ansible.builtin.copy` module to place it + at `~/Library/Preferences/com.googlecode.iterm2.plist`. + +### powerlevel10k + +- Install the `powerlevel10k` theme for Zsh using Homebrew: + - Tap `romkatv/powerlevel10k` via `community.general.homebrew_tap`. + - Install `powerlevel10k` via `community.general.homebrew`. +- Use `ansible.builtin.lineinfile` to add the sourcing line to `~/.zshrc`: + ``` + source $(brew --prefix)/opt/powerlevel10k/powerlevel10k.zsh-theme + ``` + Use `regexp` to make this idempotent (do not add duplicate lines). +- Do **not** attempt to run the interactive `p10k configure` wizard — document + in comments that the user must run this manually on first login. +- Optionally copy a `files/.p10k.zsh` into `~/` using `ansible.builtin.copy` + if the file exists in the role, so a pre-baked config can be committed. + +--- + +## Module / Plugin Preferences + +| Task Type | Preferred Module | Avoid | +|------------------------|-------------------------------------------|--------------------| +| Install brew packages | `community.general.homebrew` | `command: brew` | +| Install cask apps | `community.general.homebrew_cask` | `command: brew` | +| Manage taps | `community.general.homebrew_tap` | `command: brew tap`| +| File presence check | `ansible.builtin.stat` | `command: ls` | +| Edit shell config | `ansible.builtin.lineinfile` | `command: echo >>` | +| Copy config files | `ansible.builtin.copy` | `command: cp` | +| Directory creation | `ansible.builtin.file` (state: directory) | `command: mkdir` | +| Conditional skips | `when: result.stat.exists` | — | + +--- + +## Collections + +Declare required collections in `requirements.yml`: + +```yaml +collections: + - name: community.general + version: ">=8.0.0" +``` + +Install before running: `ansible-galaxy collection install -r requirements.yml` + +--- + +## Inventory + +Use a minimal local inventory. Suggested `inventory/local.ini`: + +```ini +[mac] +localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3 +``` + +--- + +## Variables + +Define sensible defaults in each role's `defaults/main.yml`. Example for +`homebrew` role: + +```yaml +homebrew_prefix_arm: /opt/homebrew +homebrew_prefix_intel: /usr/local +homebrew_packages: [] +homebrew_casks: [] +``` + +The top-level `group_vars/all.yml` can set QA-specific packages if needed. + +--- + +## Idempotency Requirements + +Every task must be idempotent. Running the playbook multiple times must +produce zero changes after the first successful run. Use: + +- `stat` + `when: not result.stat.exists` guards for bootstrapping steps +- Module-native idempotency for Homebrew tasks (modules handle this) +- `lineinfile` with `regexp` for shell config edits + +--- + +## Error Handling + +- Use `ignore_errors: false` (default) — fail loudly. +- For the Xcode CLT step, note in a comment that the task may time out on a + fresh machine if the user does not click through the GUI dialog. Consider + adding a `retries: 30 / delay: 10` loop on the stat check post-trigger. + +--- + +## Running the Playbook + +```bash +# Install dependencies +ansible-galaxy collection install -r requirements.yml + +# Dry run +ansible-playbook -i inventory/local.ini site.yml --check --diff + +# Apply +ansible-playbook -i inventory/local.ini site.yml +``` + +--- + +## Out of Scope + +- Managing macOS system preferences via `defaults write` (can be a follow-on role) +- Configuring SSH keys or Git identity +- Installing language runtimes (Node, Python, Ruby) — add roles as needed +- Any CI/CD pipeline integration diff --git a/group_vars/all.yml b/group_vars/all.yml @@ -0,0 +1,9 @@ +--- +# QA workstation packages — extend as needed +homebrew_packages: + - git + - wget + - curl + - jq + +homebrew_casks: [] diff --git a/inventory/local.ini b/inventory/local.ini @@ -0,0 +1,2 @@ +[mac] +localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3 diff --git a/requirements.yml b/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: community.general + version: ">=8.0.0" diff --git a/roles/chrome/defaults/main.yml b/roles/chrome/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/chrome/meta/main.yml b/roles/chrome/meta/main.yml @@ -0,0 +1,10 @@ +--- +galaxy_info: + author: local + description: Install Google Chrome via Homebrew Cask + platforms: + - name: MacOSX + versions: + - all +dependencies: + - role: homebrew diff --git a/roles/chrome/tasks/main.yml b/roles/chrome/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: Install Google Chrome + community.general.homebrew_cask: + name: google-chrome + state: present + environment: + HOMEBREW_NO_AUTO_UPDATE: "1" diff --git a/roles/homebrew/defaults/main.yml b/roles/homebrew/defaults/main.yml @@ -0,0 +1,5 @@ +--- +homebrew_prefix_arm: /opt/homebrew +homebrew_prefix_intel: /usr/local +homebrew_packages: [] +homebrew_casks: [] diff --git a/roles/homebrew/meta/main.yml b/roles/homebrew/meta/main.yml @@ -0,0 +1,9 @@ +--- +galaxy_info: + author: local + description: Bootstrap Homebrew and install packages + platforms: + - name: MacOSX + versions: + - all +dependencies: [] diff --git a/roles/homebrew/tasks/main.yml b/roles/homebrew/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Check for Apple Silicon Homebrew + ansible.builtin.stat: + path: "{{ homebrew_prefix_arm }}/bin/brew" + register: brew_arm + +- name: Check for Intel Homebrew + ansible.builtin.stat: + path: "{{ homebrew_prefix_intel }}/bin/brew" + register: brew_intel + +- name: Bootstrap Homebrew + ansible.builtin.command: + cmd: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + environment: + NONINTERACTIVE: "1" + when: not brew_arm.stat.exists and not brew_intel.stat.exists + +- name: Tap homebrew/cask + community.general.homebrew_tap: + name: homebrew/cask + state: present + environment: + HOMEBREW_NO_AUTO_UPDATE: "1" + +- name: Install Homebrew packages + community.general.homebrew: + name: "{{ homebrew_packages }}" + state: present + environment: + HOMEBREW_NO_AUTO_UPDATE: "1" + when: homebrew_packages | length > 0 + +- name: Install Homebrew casks + community.general.homebrew_cask: + name: "{{ homebrew_casks }}" + state: present + environment: + HOMEBREW_NO_AUTO_UPDATE: "1" + when: homebrew_casks | length > 0 diff --git a/roles/iterm2/defaults/main.yml b/roles/iterm2/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/iterm2/meta/main.yml b/roles/iterm2/meta/main.yml @@ -0,0 +1,10 @@ +--- +galaxy_info: + author: local + description: Install iTerm2 via Homebrew Cask + platforms: + - name: MacOSX + versions: + - all +dependencies: + - role: homebrew diff --git a/roles/iterm2/tasks/main.yml b/roles/iterm2/tasks/main.yml @@ -0,0 +1,20 @@ +--- +- name: Install iTerm2 + community.general.homebrew_cask: + name: iterm2 + state: present + environment: + HOMEBREW_NO_AUTO_UPDATE: "1" + +- name: Check for iTerm2 preferences file in role + ansible.builtin.stat: + path: "{{ role_path }}/files/com.googlecode.iterm2.plist" + register: iterm2_plist + delegate_to: localhost + +- name: Apply iTerm2 preferences + ansible.builtin.copy: + src: com.googlecode.iterm2.plist + dest: "{{ ansible_env.HOME }}/Library/Preferences/com.googlecode.iterm2.plist" + mode: '0644' + when: iterm2_plist.stat.exists diff --git a/roles/powerlevel10k/defaults/main.yml b/roles/powerlevel10k/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/powerlevel10k/meta/main.yml b/roles/powerlevel10k/meta/main.yml @@ -0,0 +1,10 @@ +--- +galaxy_info: + author: local + description: Install and configure Powerlevel10k Zsh theme + platforms: + - name: MacOSX + versions: + - all +dependencies: + - role: homebrew diff --git a/roles/powerlevel10k/tasks/main.yml b/roles/powerlevel10k/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: Tap romkatv/powerlevel10k + community.general.homebrew_tap: + name: romkatv/powerlevel10k + state: present + environment: + HOMEBREW_NO_AUTO_UPDATE: "1" + +- name: Install powerlevel10k + community.general.homebrew: + name: powerlevel10k + state: present + environment: + HOMEBREW_NO_AUTO_UPDATE: "1" + +- name: Source powerlevel10k in ~/.zshrc + ansible.builtin.lineinfile: + path: "{{ ansible_env.HOME }}/.zshrc" + line: 'source $(brew --prefix)/opt/powerlevel10k/powerlevel10k.zsh-theme' + regexp: 'powerlevel10k\.zsh-theme' + create: true + mode: '0644' + +- name: Check for pre-baked p10k config + ansible.builtin.stat: + path: "{{ role_path }}/files/.p10k.zsh" + register: p10k_config + delegate_to: localhost + +- name: Copy pre-baked p10k config + ansible.builtin.copy: + src: .p10k.zsh + dest: "{{ ansible_env.HOME }}/.p10k.zsh" + mode: '0644' + when: p10k_config.stat.exists + +- name: Remind user to configure prompt + ansible.builtin.debug: + msg: "Run 'p10k configure' on first login to customize the Powerlevel10k prompt." diff --git a/roles/xcode_cli/defaults/main.yml b/roles/xcode_cli/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/xcode_cli/meta/main.yml b/roles/xcode_cli/meta/main.yml @@ -0,0 +1,9 @@ +--- +galaxy_info: + author: local + description: Install Xcode Command Line Tools + platforms: + - name: MacOSX + versions: + - all +dependencies: [] diff --git a/roles/xcode_cli/tasks/main.yml b/roles/xcode_cli/tasks/main.yml @@ -0,0 +1,22 @@ +--- +- name: Check for Xcode Command Line Tools + ansible.builtin.stat: + path: /Library/Developer/CommandLineTools/usr/bin/git + register: xcode_clt + +- name: Trigger Xcode Command Line Tools install + ansible.builtin.command: + cmd: xcode-select --install + when: not xcode_clt.stat.exists + register: xcode_trigger + +# On a fresh machine this opens a GUI dialog — the user must click "Install". +# The loop below waits up to 5 minutes (30 × 10 s) for that to complete. +- name: Wait for Xcode Command Line Tools installation + ansible.builtin.stat: + path: /Library/Developer/CommandLineTools/usr/bin/git + register: xcode_clt_done + retries: 30 + delay: 10 + until: xcode_clt_done.stat.exists + when: not xcode_clt.stat.exists diff --git a/site.yml b/site.yml @@ -0,0 +1,10 @@ +--- +- name: Provision QA macOS workstation + hosts: mac + gather_facts: true + roles: + - xcode_cli + - homebrew + - chrome + - iterm2 + - powerlevel10k