diff --git a/debian/ansible/setup.yml b/debian/ansible/setup.yml index f6e4024..cb49684 100644 --- a/debian/ansible/setup.yml +++ b/debian/ansible/setup.yml @@ -10,7 +10,8 @@ dotfiles_dir: "{{ ansible_env.HOME }}/dotfiles" apt_repos: - name: helium - key: https://raw.githubusercontent.com/imputnet/helium-linux/main/pubkey.asc + key: >- + https://raw.githubusercontent.com/imputnet/helium-linux/main/pubkey.asc types: deb uris: https://pkg.helium.computer/deb suites: stable @@ -51,313 +52,23 @@ asset: "_{{ arch }}\\.deb$" tasks: - - name: Install base packages - ansible.builtin.apt: - name: - - curl - - git - - gnupg - - jq - - libglib2.0-bin - - python3-debian - - thunderbird - - wl-clipboard - - xdg-utils - - zsh - state: present - update_cache: true - become: true + - name: Packages and apt repositories + ansible.builtin.import_tasks: tasks/apt.yml - - name: Download apt signing keys - ansible.builtin.get_url: - url: "{{ item.key }}" - dest: "/usr/share/keyrings/{{ item.name }}.asc" - mode: "0644" - loop: "{{ apt_repos }}" - become: true + - name: Unattended security upgrades + ansible.builtin.import_tasks: tasks/auto-upgrades.yml - - name: Dearmor apt signing keys - ansible.builtin.command: - argv: - - gpg - - --dearmor - - --yes - - -o - - /usr/share/keyrings/{{ item.name }}.gpg - - /usr/share/keyrings/{{ item.name }}.asc - loop: "{{ apt_repos }}" - changed_when: false - become: true + - name: Packages from GitHub releases + ansible.builtin.import_tasks: tasks/github-debs.yml - - name: Add apt repositories - ansible.builtin.deb822_repository: - name: "{{ item.name }}" - types: "{{ item.types }}" - uris: "{{ item.uris }}" - suites: "{{ item.suites }}" - components: "{{ item.components }}" - signed_by: "{{ item.signed_by }}" - architectures: "{{ item.architectures | default(omit) }}" - state: present - loop: "{{ apt_repos }}" - become: true + - name: Dotfiles + ansible.builtin.import_tasks: tasks/dotfiles.yml - - name: Install apps from apt repositories - ansible.builtin.apt: - name: - - 1password - - 1password-cli - - helium-bin - - tailscale - state: present - update_cache: true - become: true + - name: Homebrew and mise + ansible.builtin.import_tasks: tasks/homebrew.yml - - name: Install signal-desktop - ansible.builtin.apt: - name: signal-desktop - state: present - when: arch == 'amd64' - become: true + - name: Desktop preferences + ansible.builtin.import_tasks: tasks/desktop.yml - - name: Enable unattended security upgrades - ansible.builtin.apt: - name: unattended-upgrades - state: present - become: true - - # The package's postinst writes this file from debconf answers; set it - # explicitly so the enabled state does not depend on debconf defaults. - - name: Run unattended upgrades daily - ansible.builtin.copy: - dest: /etc/apt/apt.conf.d/20auto-upgrades - mode: "0644" - content: | - APT::Periodic::Update-Package-Lists "1"; - APT::Periodic::Unattended-Upgrade "1"; - become: true - - - name: Look up latest ghostty and obsidian releases - ansible.builtin.uri: - url: "https://api.github.com/repos/{{ item.repo }}/releases?per_page=10" - headers: - Accept: application/vnd.github+json - return_content: true - loop: "{{ github_debs }}" - register: gh_releases - - - name: Download ghostty and obsidian debs - ansible.builtin.get_url: - url: >- - {{ item.1.json | map(attribute='assets') - | map('selectattr', 'name', 'search', item.0.asset) - | flatten | map(attribute='browser_download_url') | first }} - dest: "/tmp/{{ item.0.name }}.deb" - mode: "0644" - loop: "{{ github_debs | zip(gh_releases.results) | list }}" - - - name: Install ghostty and obsidian - ansible.builtin.apt: - deb: "/tmp/{{ item.name }}.deb" - state: present - loop: "{{ github_debs }}" - become: true - - # - name: Create chromium managed-policies directory - # ansible.builtin.file: - # path: /etc/chromium/policies/managed - # state: directory - # mode: "0755" - # become: true - # - # - name: Pre-install Helium extensions - # # Helium currently ignores force-install policies (upstream bug - # # imputnet/helium#1737). This file is correct Chromium config and will - # # apply once fixed; until then install the extensions from the store. - # ansible.builtin.copy: - # dest: /etc/chromium/policies/managed/helium-extensions.json - # mode: "0644" - # content: | - # { - # "ExtensionInstallForcelist": [ - # "aeblfdkhhhdcdjpifhhbdiojplfdncoa;https://clients2.google.com/service/update2/crx", - # "dbepggeogbaibhgnhhndojpepiihcmeb;https://clients2.google.com/service/update2/crx", - # "cgpmbiiagnehkikhcbnhiagfomajncpa;https://clients2.google.com/service/update2/crx", - # "eimadpbcbfnmbopodijcomhbndbaanm;https://clients2.google.com/service/update2/crx" - # ] - # } - # become: true - # - - name: Clone dotfiles - ansible.builtin.git: - repo: "{{ dotfiles_repo }}" - dest: "{{ dotfiles_dir }}" - update: false - - - name: Run dotfiles sync script - ansible.builtin.command: ./sync.sh - args: - chdir: "{{ dotfiles_dir }}" - register: sync - changed_when: >- - 'Creating symlink' in sync.stdout or - 'Removing existing symlink' in sync.stdout or - 'backing it up' in sync.stdout or - 'Cloning into' in sync.stderr - - # Homebrew: prefix created as root, clone and bundle as the user (the - # official installer refuses to run as root, and this avoids its - # interactive sudo prompts). - - name: Create homebrew directories - ansible.builtin.file: - path: "{{ item }}" - state: directory - owner: "{{ ansible_user_id }}" - mode: "0755" - loop: - - /home/linuxbrew - - /home/linuxbrew/.linuxbrew - become: true - - - name: Check for homebrew - ansible.builtin.stat: - path: /home/linuxbrew/.linuxbrew/bin/brew - register: brew_bin - - - name: Clone homebrew - ansible.builtin.git: - repo: https://github.com/Homebrew/brew - dest: /home/linuxbrew/.linuxbrew - when: not brew_bin.stat.exists - - # brewsync: cask entries are skipped automatically on Linux - - name: Sync homebrew packages - ansible.builtin.command: brew bundle --zap --force-cleanup --file Brewfile - args: - chdir: "{{ dotfiles_dir }}" - environment: - PATH: "/home/linuxbrew/.linuxbrew/bin:{{ ansible_env.PATH }}" - register: brewsync - changed_when: >- - 'Installing ' in (brewsync.stdout ~ brewsync.stderr) or - 'Uninstalling ' in (brewsync.stdout ~ brewsync.stderr) - - - name: Install mise tools - ansible.builtin.command: mise install -y - environment: - PATH: "/home/linuxbrew/.linuxbrew/bin:{{ ansible_env.PATH }}" - register: mise_install - changed_when: >- - 'installed 0 tools' not in - (mise_install.stdout ~ mise_install.stderr) - - - name: Set login shell to zsh - ansible.builtin.user: - name: "{{ ansible_user_id }}" - shell: /usr/bin/zsh - become: true - - # Caps lock -> ctrl at kernel level: GNOME on Wayland ignores gsettings - # xkb-options, so remap where every session (Wayland/X11/GDM/TTY) sees it. - # 3a = AT/PS2 scancode, 70039 = HID usage (USB/Bluetooth). Needs a reboot; - # udevadm trigger is unreliable for keycode remaps. - - name: Map caps lock to ctrl at the kernel level - ansible.builtin.copy: - dest: /etc/udev/hwdb.d/61-capslock-ctrl.hwdb - mode: "0644" - content: | - evdev:atkbd:dmi:* - KEYBOARD_KEY_3a=leftctrl - - evdev:input:b0003* - KEYBOARD_KEY_70039=leftctrl - - evdev:input:b0005* - KEYBOARD_KEY_70039=leftctrl - register: capslock_hwdb - become: true - - - name: Apply caps lock remap - ansible.builtin.command: "{{ item }}" - loop: - - systemd-hwdb update - - udevadm trigger - when: capslock_hwdb is changed - become: true - - - name: Read current default browser - ansible.builtin.command: gio mime x-scheme-handler/https - register: default_browser - changed_when: false - failed_when: false - - - name: Set Helium as default browser - ansible.builtin.command: gio mime {{ item }} helium.desktop - loop: - - x-scheme-handler/http - - x-scheme-handler/https - changed_when: >- - 'helium.desktop' not in - (default_browser.stdout | default('')) - - - name: Set default web browser (xdg) - ansible.builtin.command: - argv: - - xdg-settings - - set - - default-web-browser - - helium.desktop - changed_when: >- - 'helium.desktop' not in - (default_browser.stdout | default('')) - failed_when: false - - - name: Verify Helium is the default browser - ansible.builtin.command: gio mime x-scheme-handler/https - register: default_browser_check - changed_when: false - failed_when: "'helium.desktop' not in default_browser_check.stdout" - - # pi Umans login: pull the API key from 1Password and store it where pi - # reads it (~/.pi/agent/auth.json, same shape /login writes). - - name: Fetch Umans API key from 1Password - ansible.builtin.command: op item get umans-api-key --fields password - register: op_umans - changed_when: false - failed_when: false - no_log: true - - - name: Configure pi Umans login - ansible.builtin.shell: | - set -euo pipefail - key=$(op item get umans-api-key --fields password) - auth="$HOME/.pi/agent/auth.json" - mkdir -p "$(dirname "$auth")" - current=$(jq -r '.umans.access // empty' "$auth" 2>/dev/null || true) - if [ "$current" = "$key" ]; then - echo "already configured" - exit 0 - fi - never=8640000000000000 - cred='{type: "oauth", refresh: $key, access: $key, expires: $exp}' - tmp=$(mktemp) - if [ -f "$auth" ]; then - jq --arg key "$key" --argjson exp $never \ - ".umans = $cred" "$auth" > "$tmp" - else - jq -n --arg key "$key" --argjson exp $never \ - "{umans: $cred}" > "$tmp" - fi - chmod 600 "$tmp" - mv "$tmp" "$auth" - register: umans_login - changed_when: "'already configured' not in umans_login.stdout" - when: op_umans.rc == 0 - no_log: true - - - name: Note skipped pi Umans login - ansible.builtin.debug: - msg: >- - op CLI not available (sign in to 1Password and enable "Integrate - with 1Password CLI", then re-run setup) — pi Umans login skipped. - when: op_umans.rc != 0 + - name: pi Umans login + ansible.builtin.import_tasks: tasks/pi.yml diff --git a/debian/ansible/tasks/apt.yml b/debian/ansible/tasks/apt.yml new file mode 100644 index 0000000..b49493b --- /dev/null +++ b/debian/ansible/tasks/apt.yml @@ -0,0 +1,69 @@ +--- +- name: Install base packages + ansible.builtin.apt: + name: + - curl + - git + - gnupg + - jq + - libglib2.0-bin + - python3-debian + - thunderbird + - wl-clipboard + - xdg-utils + - zsh + state: present + update_cache: true + become: true + +- name: Download apt signing keys + ansible.builtin.get_url: + url: "{{ item.key }}" + dest: "/usr/share/keyrings/{{ item.name }}.asc" + mode: "0644" + loop: "{{ apt_repos }}" + become: true + +- name: Dearmor apt signing keys + ansible.builtin.command: + argv: + - gpg + - --dearmor + - --yes + - -o + - /usr/share/keyrings/{{ item.name }}.gpg + - /usr/share/keyrings/{{ item.name }}.asc + loop: "{{ apt_repos }}" + changed_when: false + become: true + +- name: Add apt repositories + ansible.builtin.deb822_repository: + name: "{{ item.name }}" + types: "{{ item.types }}" + uris: "{{ item.uris }}" + suites: "{{ item.suites }}" + components: "{{ item.components }}" + signed_by: "{{ item.signed_by }}" + architectures: "{{ item.architectures | default(omit) }}" + state: present + loop: "{{ apt_repos }}" + become: true + +- name: Install apps from apt repositories + ansible.builtin.apt: + name: + - 1password + - 1password-cli + - helium-bin + - tailscale + state: present + update_cache: true + become: true + +- name: Install signal-desktop + ansible.builtin.apt: + name: signal-desktop + state: present + when: arch == 'amd64' + become: true diff --git a/debian/ansible/tasks/auto-upgrades.yml b/debian/ansible/tasks/auto-upgrades.yml new file mode 100644 index 0000000..2948ce1 --- /dev/null +++ b/debian/ansible/tasks/auto-upgrades.yml @@ -0,0 +1,17 @@ +--- +- name: Enable unattended security upgrades + ansible.builtin.apt: + name: unattended-upgrades + state: present + become: true + +# The package's postinst writes this file from debconf answers; set it +# explicitly so the enabled state does not depend on debconf defaults. +- name: Run unattended upgrades daily + ansible.builtin.copy: + dest: /etc/apt/apt.conf.d/20auto-upgrades + mode: "0644" + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + become: true diff --git a/debian/ansible/tasks/desktop.yml b/debian/ansible/tasks/desktop.yml new file mode 100644 index 0000000..d3c113a --- /dev/null +++ b/debian/ansible/tasks/desktop.yml @@ -0,0 +1,94 @@ +--- +- name: Set login shell to zsh + ansible.builtin.user: + name: "{{ ansible_user_id }}" + shell: /usr/bin/zsh + become: true + +# Caps lock -> ctrl at kernel level: GNOME on Wayland ignores gsettings +# xkb-options, so remap where every session (Wayland/X11/GDM/TTY) sees it. +# 3a = AT/PS2 scancode, 70039 = HID usage (USB/Bluetooth). Needs a reboot; +# udevadm trigger is unreliable for keycode remaps. +- name: Map caps lock to ctrl at the kernel level + ansible.builtin.copy: + dest: /etc/udev/hwdb.d/61-capslock-ctrl.hwdb + mode: "0644" + content: | + evdev:atkbd:dmi:* + KEYBOARD_KEY_3a=leftctrl + + evdev:input:b0003* + KEYBOARD_KEY_70039=leftctrl + + evdev:input:b0005* + KEYBOARD_KEY_70039=leftctrl + register: capslock_hwdb + become: true + +- name: Apply caps lock remap + ansible.builtin.command: "{{ item }}" + loop: + - systemd-hwdb update + - udevadm trigger + when: capslock_hwdb is changed + become: true + +- name: Read current default browser + ansible.builtin.command: gio mime x-scheme-handler/https + register: default_browser + changed_when: false + failed_when: false + +- name: Set Helium as default browser + ansible.builtin.command: gio mime {{ item }} helium.desktop + loop: + - x-scheme-handler/http + - x-scheme-handler/https + changed_when: >- + 'helium.desktop' not in + (default_browser.stdout | default('')) + +- name: Set default web browser (xdg) + ansible.builtin.command: + argv: + - xdg-settings + - set + - default-web-browser + - helium.desktop + changed_when: >- + 'helium.desktop' not in + (default_browser.stdout | default('')) + failed_when: false + +- name: Verify Helium is the default browser + ansible.builtin.command: gio mime x-scheme-handler/https + register: default_browser_check + changed_when: false + failed_when: "'helium.desktop' not in default_browser_check.stdout" + +# - name: Create chromium managed-policies directory +# ansible.builtin.file: +# path: /etc/chromium/policies/managed +# state: directory +# mode: "0755" +# become: true +# +# - name: Pre-install Helium extensions +# # Helium currently ignores force-install policies (upstream bug +# # imputnet/helium#1737). This file is correct Chromium config and +# # will apply once fixed; until then install the extensions from +# # the store. +# vars: +# crx_url: https://clients2.google.com/service/update2/crx +# helium_extensions: +# ExtensionInstallForcelist: +# - "aeblfdkhhhdcdjpifhhbdiojplfdncoa;{{ crx_url }}" +# - "dbepggeogbaibhgnhhndojpepiihcmeb;{{ crx_url }}" +# - "cgpmbiiagnehkikhcbnhiagfomajncpa;{{ crx_url }}" +# - "eimadpbcbfnmbopodijcomhbndbaanm;{{ crx_url }}" +# ansible.builtin.copy: +# dest: /etc/chromium/policies/managed/helium-extensions.json +# mode: "0644" +# content: "{{ helium_extensions | to_json }}" +# become: true +# diff --git a/debian/ansible/tasks/dotfiles.yml b/debian/ansible/tasks/dotfiles.yml new file mode 100644 index 0000000..77a2cc5 --- /dev/null +++ b/debian/ansible/tasks/dotfiles.yml @@ -0,0 +1,17 @@ +--- +- name: Clone dotfiles + ansible.builtin.git: + repo: "{{ dotfiles_repo }}" + dest: "{{ dotfiles_dir }}" + update: false + +- name: Run dotfiles sync script + ansible.builtin.command: ./sync.sh + args: + chdir: "{{ dotfiles_dir }}" + register: sync + changed_when: >- + 'Creating symlink' in sync.stdout or + 'Removing existing symlink' in sync.stdout or + 'backing it up' in sync.stdout or + 'Cloning into' in sync.stderr diff --git a/debian/ansible/tasks/github-debs.yml b/debian/ansible/tasks/github-debs.yml new file mode 100644 index 0000000..370e612 --- /dev/null +++ b/debian/ansible/tasks/github-debs.yml @@ -0,0 +1,26 @@ +--- +- name: Look up latest ghostty and obsidian releases + ansible.builtin.uri: + url: "https://api.github.com/repos/{{ item.repo }}/releases?per_page=10" + headers: + Accept: application/vnd.github+json + return_content: true + loop: "{{ github_debs }}" + register: gh_releases + +- name: Download ghostty and obsidian debs + ansible.builtin.get_url: + url: >- + {{ item.1.json | map(attribute='assets') + | map('selectattr', 'name', 'search', item.0.asset) + | flatten | map(attribute='browser_download_url') | first }} + dest: "/tmp/{{ item.0.name }}.deb" + mode: "0644" + loop: "{{ github_debs | zip(gh_releases.results) | list }}" + +- name: Install ghostty and obsidian + ansible.builtin.apt: + deb: "/tmp/{{ item.name }}.deb" + state: present + loop: "{{ github_debs }}" + become: true diff --git a/debian/ansible/tasks/homebrew.yml b/debian/ansible/tasks/homebrew.yml new file mode 100644 index 0000000..d035b5f --- /dev/null +++ b/debian/ansible/tasks/homebrew.yml @@ -0,0 +1,46 @@ +--- +# Homebrew: prefix created as root, clone and bundle as the user (the +# official installer refuses to run as root, and this avoids its +# interactive sudo prompts). +- name: Create homebrew directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ ansible_user_id }}" + mode: "0755" + loop: + - /home/linuxbrew + - /home/linuxbrew/.linuxbrew + become: true + +- name: Check for homebrew + ansible.builtin.stat: + path: /home/linuxbrew/.linuxbrew/bin/brew + register: brew_bin + +- name: Clone homebrew + ansible.builtin.git: + repo: https://github.com/Homebrew/brew + dest: /home/linuxbrew/.linuxbrew + when: not brew_bin.stat.exists + +# brewsync: cask entries are skipped automatically on Linux +- name: Sync homebrew packages + ansible.builtin.command: brew bundle --zap --force-cleanup --file Brewfile + args: + chdir: "{{ dotfiles_dir }}" + environment: + PATH: "/home/linuxbrew/.linuxbrew/bin:{{ ansible_env.PATH }}" + register: brewsync + changed_when: >- + 'Installing ' in (brewsync.stdout ~ brewsync.stderr) or + 'Uninstalling ' in (brewsync.stdout ~ brewsync.stderr) + +- name: Install mise tools + ansible.builtin.command: mise install -y + environment: + PATH: "/home/linuxbrew/.linuxbrew/bin:{{ ansible_env.PATH }}" + register: mise_install + changed_when: >- + 'installed 0 tools' not in + (mise_install.stdout ~ mise_install.stderr) diff --git a/debian/ansible/tasks/pi.yml b/debian/ansible/tasks/pi.yml new file mode 100644 index 0000000..59fe8aa --- /dev/null +++ b/debian/ansible/tasks/pi.yml @@ -0,0 +1,44 @@ +--- +# pi Umans login: pull the API key from 1Password and store it where pi +# reads it (~/.pi/agent/auth.json, same shape /login writes). +- name: Fetch Umans API key from 1Password + ansible.builtin.command: op item get umans-api-key --fields password + register: op_umans + changed_when: false + failed_when: false + no_log: true + +- name: Configure pi Umans login + ansible.builtin.shell: | + set -euo pipefail + key=$(op item get umans-api-key --fields password) + auth="$HOME/.pi/agent/auth.json" + mkdir -p "$(dirname "$auth")" + current=$(jq -r '.umans.access // empty' "$auth" 2>/dev/null || true) + if [ "$current" = "$key" ]; then + echo "already configured" + exit 0 + fi + never=8640000000000000 + cred='{type: "oauth", refresh: $key, access: $key, expires: $exp}' + tmp=$(mktemp) + if [ -f "$auth" ]; then + jq --arg key "$key" --argjson exp $never \ + ".umans = $cred" "$auth" > "$tmp" + else + jq -n --arg key "$key" --argjson exp $never \ + "{umans: $cred}" > "$tmp" + fi + chmod 600 "$tmp" + mv "$tmp" "$auth" + register: umans_login + changed_when: "'already configured' not in umans_login.stdout" + when: op_umans.rc == 0 + no_log: true + +- name: Note skipped pi Umans login + ansible.builtin.debug: + msg: >- + op CLI not available (sign in to 1Password and enable "Integrate + with 1Password CLI", then re-run setup) — pi Umans login skipped. + when: op_umans.rc != 0