Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ flowchart TD
| `developer` | Generic CLI dev base (the default: git, docker, shell utilities) |
| `developer-gui` | VS Code, Ghostty, DBeaver. Privacy + AI-upsell de-nag profile for VSCode/VSCodium/Cursor off unless `-e vscode_privacy_enabled=true` |
| `developer-rust` / `-go` / `-python` / `-node` / `-typescript` / `-c` | Language toolchains |
| `infrastructure` | OpenTofu, OpenBao, AWS CLI, helm, terraform-docs, `k8s` (kubectl, kubectx, kubens, k9s, kind, argocd, dive, kustomize, kubeconform, kube-linter), `data` (clickhouse-client, confluent-cli, rpk, valkey-cli, vector), `cloudflare` (flarectl, wrangler) |
| `infrastructure` | OpenTofu, OpenBao, AWS CLI, helm, terraform-docs, `k8s` (kubectl, kubectx, kubens, k9s, kind, argocd, kustomize, kubeconform, kube-linter), `data` (clickhouse-client, confluent-cli, rpk, valkey-cli, vector), `cloudflare` (flarectl, wrangler) |
| `packer` | HashiCorp Packer from HashiCorp's repo or tap. Opt-in, in no persona |
| `contributor` | hyperi-ci + its check tools (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, yamllint, ansible-lint, pre-commit, act, git-scrub, macbash |
| `soe` / `soe-gui` | HyperI org policy (opt-in) |
Expand Down Expand Up @@ -179,14 +179,14 @@ digest. Read that before changing a role or adding a tool.

- Docker (Engine on Linux, CLI-only via Homebrew on macOS, no Docker Desktop, bring your own daemon)
- Git, GitHub CLI, Git LFS
- CLI utilities: jq, gron, bat, fzf, ripgrep, fd, sd, git-delta, lazygit, moreutils, miller, tmux, htop, age, ...
- CLI utilities: jq, bat, fzf, ripgrep, fd, sd, git-delta, lazygit, moreutils, miller, tmux, htop, age, ...

**Opt-in, via tags:**

- `developer-gui`: VS Code, Ghostty (Solarized theme), DBeaver
- `vscode-privacy` (off by default): strips the Copilot/AI upsell UI and the telemetry that stock VSCode ships enabled, across VSCode, VSCodium and Cursor. Enable with `-e vscode_privacy_enabled=true`. It merges one marked block into `settings.json` and never touches a comment or a key it does not manage, backs the file up before its first write, and `-e vscode_privacy_uninstall=true` takes only its own keys back out. Where you have set one of those keys yourself further down the file, yours wins and the run tells you which ones -- so it cannot look applied while changing nothing
- Languages: Rust, Go, Python, C/C++, Node.js, TypeScript (the Astral suite -- uv, ruff, ty -- ships in the base, as does Node.js: it is core tooling that semantic-release and CI need)
- `infrastructure`: OpenTofu + OpenBao (the OSS forks in place of terraform and vault), AWS CLI v2, checkov, and terraform-docs for generating IaC module reference docs (engine-agnostic -- it reads `.tf` whichever binary runs it, and OpenTofu has no native `tofu docs`). Under `k8s`: kubectl + kubectx + kubens + k9s + kind + argocd + dive + kustomize + kubeconform + kube-linter. helm is NOT in that group -- it sits in `cloud`, so plain `--tags infrastructure` gets it whether or not you select `k8s`. The `data` group: clickhouse-client, confluent-cli, rpk, valkey-cli, vector. The `cloudflare` group: flarectl + wrangler (flarectl builds from source on both platforms -- Cloudflare ships no binary -- so Linux needs `developer-go`)
- `infrastructure`: OpenTofu + OpenBao (the OSS forks in place of terraform and vault), AWS CLI v2, checkov, and terraform-docs for generating IaC module reference docs (engine-agnostic -- it reads `.tf` whichever binary runs it, and OpenTofu has no native `tofu docs`). Under `k8s`: kubectl + kubectx + kubens + k9s + kind + argocd + kustomize + kubeconform + kube-linter. helm is NOT in that group -- it sits in `cloud`, so plain `--tags infrastructure` gets it whether or not you select `k8s`. The `data` group: clickhouse-client, confluent-cli, rpk, valkey-cli, vector. The `cloudflare` group: flarectl + wrangler (flarectl builds from source on both platforms -- Cloudflare ships no binary -- so Linux needs `developer-go`)
- `packer` (off by default, and in no persona): HashiCorp Packer, the one HashiCorp tool we install, because it is BUSL with no open-source fork. Ubuntu and Fedora take HashiCorp's repo, with its signing key checked against the fingerprint HashiCorp publishes before it is trusted; macOS takes `hashicorp/tap`. `--tags removals` removes that repo only where Packer is not installed -- see [roles/packer/README.md](ansible/roles/packer/README.md)
- `contributor`: hyperi-ci and the tools its checks drive (semgrep, alint), gitleaks, trivy, hadolint, pip-audit, ansible-lint, pre-commit, act, and git-scrub for rewriting AI residue or a leaked secret out of git history -- gitleaks scans FULL history, so a secret removed from HEAD still fails the gate. macbash is here too: it flags the GNU-only bash constructs that break on macOS, and CONTRIBUTING.md asks for it on every shell change
- `soe` / `soe-gui`: HyperI org policy: VPN clients, Claude Code, Slack, LibreOffice, RDP client, telemetry-disable, auto-updates, GNOME taskbar
Expand Down
53 changes: 38 additions & 15 deletions ansible/molecule/remediation/prepare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,29 @@
- linear # Linear CLI — removed by decision 2026-05-01
- minikube # superseded by kind

# DERIVED: k8s.yml put dive in /usr/local/bin on both distros, and Fedora took
# gron's release binary there, until both were dropped.
- name: Plant the dropped dive and gron release binaries
ansible.builtin.copy:
content: |
#!/bin/sh
echo "dropped GitHub-release fixture: {{ item }}"
dest: "/usr/local/bin/{{ item }}"
owner: root
group: root
mode: '0755'
loop:
- dive
- gron

# DERIVED: utilities.yml installed gron from apt on Ubuntu until it was dropped.
- name: Install the dropped gron package (Ubuntu)
ansible.builtin.apt:
name: gron
state: present
update_cache: true
when: ansible_facts['distribution'] == 'Ubuntu'

# Observed on the reference workstation 2026-10-06: helm v4.1.1, unowned, left by the
# get.helm.sh download an older revision did. It shadows packaged helm.
- name: Plant the stale root-owned helm (observed v4.1.1)
Expand Down Expand Up @@ -431,7 +454,7 @@
mode: '0755'
loop:
- kind
- dive
- lazygit
- argocd
- macbash
- git-scrub
Expand All @@ -456,7 +479,7 @@
mode: '0755'
loop:
- .local/bin/kind # shadows /usr/local/bin/kind -- must go
- go/bin/dive # shadows /usr/local/bin/dive -- must go
- go/bin/lazygit # shadows /usr/local/bin/lazygit -- must go
- .local/bin/kubeconform # no system copy here -- the only one, must stay
- .local/bin/tinygo-dev # not a tool the roles manage -- must stay
- .local/bin/yq # pip/uv's yq is another program -- must stay
Expand All @@ -471,7 +494,7 @@
force: true
loop:
- {name: argocd, src: /usr/local/bin/argocd}
- {name: dive, src: /nonexistent/dive}
- {name: lazygit, src: /nonexistent/lazygit}

# CONSTRUCTED: a directory that happens to carry a managed tool's name.
- name: Plant a directory named like a managed tool
Expand All @@ -483,8 +506,8 @@
# Observed on the reference workstation 2026-10-06: the macbash .deb, from no
# repository, beside the role's /usr/local/bin/macbash. CONSTRUCTED: a
# git-scrub package another package depends on, a kind package that is not
# on the purge list, and a dive package served by a repository -- all three
# must stay.
# on the purge list, and a k9s package served by a repository -- all three
# must stay. Ubuntu offers no k9s of its own.
- name: Install hand-built duplicate packages (Ubuntu)
when: ansible_facts['distribution'] == 'Ubuntu'
block:
Expand All @@ -498,7 +521,7 @@
path: "/root/dup-fixture/{{ item.0 }}/{{ item.1 }}"
state: directory
mode: '0755'
loop: "{{ ['macbash', 'git-scrub', 'git-scrub-dependant', 'kind', 'dive'] | product(['DEBIAN', 'usr/bin']) | list }}"
loop: "{{ ['macbash', 'git-scrub', 'git-scrub-dependant', 'kind', 'k9s'] | product(['DEBIAN', 'usr/bin']) | list }}"

- name: Write the fixture control files
ansible.builtin.copy:
Expand All @@ -516,7 +539,7 @@
- {name: git-scrub, depends: ''}
- {name: git-scrub-dependant, depends: git-scrub}
- {name: kind, depends: ''}
- {name: dive, depends: ''}
- {name: k9s, depends: ''}

- name: Write the packaged binaries
ansible.builtin.copy:
Expand All @@ -530,7 +553,7 @@
- git-scrub
- git-scrub-dependant
- kind
- dive
- k9s

- name: Build the fixture packages
ansible.builtin.command:
Expand All @@ -541,7 +564,7 @@
- git-scrub
- git-scrub-dependant
- kind
- dive
- k9s

- name: Install the hand-installed fixture packages
ansible.builtin.apt:
Expand All @@ -552,18 +575,18 @@
- git-scrub-dependant
- kind

# dive comes from a local repository instead, so a repository offers it.
# k9s comes from a local repository instead, so a repository offers it.
# Under /srv, not /root: apt fetches as the _apt user.
- name: Create the fixture repository
ansible.builtin.file:
path: /srv/fixture-repo
state: directory
mode: '0755'

- name: Place the dive package in the fixture repository
- name: Place the k9s package in the fixture repository
ansible.builtin.copy:
src: /root/dup-fixture/dive.deb
dest: /srv/fixture-repo/dive.deb
src: /root/dup-fixture/k9s.deb
dest: /srv/fixture-repo/k9s.deb
remote_src: true
mode: '0644'

Expand Down Expand Up @@ -594,9 +617,9 @@
- APT::Get::List-Cleanup=0
changed_when: false

- name: Install dive from the fixture repository
- name: Install k9s from the fixture repository
ansible.builtin.apt:
name: dive
name: k9s
state: present

# Fedora: the macbash rpm, from no repository, and golangci-lint from the
Expand Down
11 changes: 7 additions & 4 deletions ansible/molecule/remediation/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
- bw # Bitwarden — removed by decision
- linear # Linear CLI — removed by decision
- minikube # superseded by kind
- dive # dropped, unmaintained since 2025-05
- gron # dropped, dormant since 2022
register: verify_gone

- name: Assert retired artefacts were removed
Expand Down Expand Up @@ -64,6 +66,7 @@
- tree
- httpie
- docker-desktop
- gron
loop_control:
label: "{{ item }}"
when: ansible_facts['distribution'] == 'Ubuntu'
Expand Down Expand Up @@ -291,11 +294,11 @@
follow: false
loop:
- {path: .local/bin/kind, gone: true}
- {path: go/bin/dive, gone: true}
- {path: go/bin/lazygit, gone: true}
- {path: .local/bin/kubeconform, gone: false}
- {path: .local/bin/tinygo-dev, gone: false}
- {path: .local/bin/argocd, gone: false}
- {path: .local/bin/dive, gone: false}
- {path: .local/bin/lazygit, gone: false}
- {path: .local/bin/kubectx, gone: false}
- {path: .local/bin/yq, gone: false}
loop_control:
Expand All @@ -321,7 +324,7 @@
path: "{{ item }}"
loop:
- /usr/local/bin/kind
- /usr/local/bin/dive
- /usr/local/bin/lazygit
- /usr/local/bin/argocd
- /usr/local/bin/macbash
- /usr/local/bin/git-scrub
Expand Down Expand Up @@ -382,7 +385,7 @@
is offered by a repository, or is the only Go.
success_msg: "{{ item }} kept"
loop: >-
{{ ((['git-scrub', 'git-scrub-dependant', 'kind', 'dive']
{{ ((['git-scrub', 'git-scrub-dependant', 'kind', 'k9s']
+ (['golang-go'] if inventory_hostname is search('golink|gosrc') else []))
if ansible_facts['distribution'] == 'Ubuntu'
else ['golangci-lint', 'golang']) }}
Expand Down
20 changes: 19 additions & 1 deletion ansible/playbooks/group_vars/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Every tool resolves its version at install time, so a run in four months
# installs what is current in four months.
#
# Manual binaries query `/releases/latest`. Cargo tools install unversioned. Go
# Manual binaries take the newest GitHub release past the release-age cooldown
# below. Cargo tools install unversioned. Go
# and rustup fetch the current version with the checksum published beside it.
#
# What remains below is a selection rather than a version -- a Node LTS line,
Expand Down Expand Up @@ -31,6 +32,23 @@ hyperi_github_headers: >-
hyperi_github_env: >-
{{ {'GITHUB_TOKEN': hyperi_github_token} if hyperi_github_token else {} }}

# Every tool taken from a GitHub release installs the newest release at least
# this many days old, so a compromised or broken release has time to be pulled
# first (roles/github_release). Nothing newer is ever installed in its place:
# with no release old enough, the installed copy stays and the run warns.
# hyperi-update reads this and the exempt orgs below from
# /etc/default/hyperi-update, which update_command.yml writes.
hyperi_release_min_age_days: 7

# Orgs whose releases ship through our own CI gates, so they skip the wait.
hyperi_release_cooldown_exempt:
- hyperi-io

# The release-age rule as a process environment, for the scripts that look up
# their own releases.
hyperi_release_env:
HYPERI_RELEASE_MIN_AGE_DAYS: "{{ hyperi_release_min_age_days | string }}"

# ============================================================================
# Core component versions -- the ONE place to bump them
# ============================================================================
Expand Down
41 changes: 30 additions & 11 deletions ansible/roles/astral/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,18 @@
when: ansible_facts['distribution'] == 'Ubuntu'
tags: ['astral', 'uv']
block:
- name: Get latest uv version from GitHub API
ansible.builtin.uri:
url: https://api.github.com/repos/astral-sh/uv/releases/latest
return_content: true
headers: "{{ hyperi_github_headers }}"
register: uv_latest_release
check_mode: false
- name: Resolve the uv release
ansible.builtin.include_role:
name: github_release
# The include is already gated by this file's tags, and its tasks carry none.
apply:
tags: ['always']
vars:
github_release_repo: astral-sh/uv

- name: Set uv version fact
ansible.builtin.set_fact:
uv_version: "{{ uv_latest_release.json.tag_name }}"
uv_version: "{{ github_release_doc.tag_name }}"

# uv publishes a gnu build for both arches, so the libc token does not move.
- name: Determine uv binary architecture
Expand Down Expand Up @@ -103,9 +104,27 @@
- not ansible_check_mode
- ((astral_uv_installed.stdout | default('')).split() + ['', ''])[1] != uv_version

# No rescue: ruff and ty below are installed by `uv tool install`, so warning
# past a missing uv only moves the abort to a task that reports it as a missing
# command.
# A host that already has uv keeps it and warns. One without it still fails
# here: ruff and ty below are installed by `uv tool install`, so warning past
# a missing uv only moves the abort to a task that reports a missing command.
rescue:
- name: Check for an installed uv
ansible.builtin.stat:
path: "{{ user_home }}/.local/bin/uv"
register: astral_uv_present

- name: Fail without a uv to fall back on
ansible.builtin.fail:
msg: "uv: {{ ansible_failed_result.msg | default('install failed') }}"
when: not (astral_uv_present.stat.executable | default(false))

- name: Record that uv was not updated
# noqa: var-naming[no-role-prefix] -- deploy_warnings is the shared
# accumulator reported by playbooks/main.yml post_tasks.
ansible.builtin.set_fact:
deploy_warnings: >-
{{ deploy_warnings | default([])
+ ['uv: ' ~ (ansible_failed_result.msg | default('update failed'))] }}

# ============================================================================
# ruff + ty - repo-managed on Fedora/macOS; uv tools on Ubuntu
Expand Down
60 changes: 21 additions & 39 deletions ansible/roles/contributor/tasks/act.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,60 +31,42 @@
# UBUNTU - Install via binary from GitHub releases
# ============================================================================
# Tier 3: Ubuntu has no distro/vendor-repo/snap channel for act, so it stays a
# re-fetched GitHub release -- hyperi-update pulls the latest on each run.
# re-fetched GitHub release -- hyperi-update pulls the newest past the
# release-age cooldown on each run.

- name: Install act (Ubuntu)
block:
- name: Get latest act version from GitHub API
ansible.builtin.uri:
url: https://api.github.com/repos/nektos/act/releases/latest
return_content: true
headers: "{{ hyperi_github_headers }}"
register: act_release
check_mode: false

- name: Set act version fact
ansible.builtin.set_fact:
act_version: "{{ act_release.json.tag_name }}"
- name: Resolve the act release
ansible.builtin.include_role:
name: github_release
# The include is already gated by this file's tags, and its tasks carry none.
apply:
tags: ['always']
vars:
github_release_repo: nektos/act

# act does NOT use one naming scheme across arches: amd64 is `x86_64`,
# arm64 is `arm64` (not `aarch64`). So neither shared token fits both, and
# a substring swap on either would 404. Verified against the real release.
- name: Determine the act asset architecture
ansible.builtin.set_fact:
act_arch: "{{ 'arm64' if hyperi_arch_deb == 'arm64' else 'x86_64' }}"
contributor_act_arch: "{{ 'arm64' if hyperi_arch_deb == 'arm64' else 'x86_64' }}"

- name: Download act binary tarball
ansible.builtin.get_url:
url: "https://github.com/nektos/act/releases/download/{{ act_version }}/act_Linux_{{ act_arch }}.tar.gz"
dest: /tmp/act.tar.gz
mode: '0644'

- name: Extract act binary
# Straight from the URL, as gitleaks does: unarchive reports a change only
# when the binary differs, and no root-written file sits at a fixed /tmp path.
- name: Download and extract act
ansible.builtin.unarchive:
src: /tmp/act.tar.gz
dest: /tmp
src: >-
https://github.com/nektos/act/releases/download/{{ github_release_doc.tag_name
}}/act_Linux_{{ contributor_act_arch }}.tar.gz
dest: /usr/local/bin
remote_src: true
when: not ansible_check_mode

- name: Install act binary
ansible.builtin.copy:
src: /tmp/act
dest: /usr/local/bin/act
include: act
owner: root
group: root
mode: '0755'
remote_src: true
when: not ansible_check_mode

- name: Remove act installation files
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /tmp/act.tar.gz
- /tmp/act
- /tmp/LICENSE
- /tmp/README.md

rescue:
- name: Record that act did not install (Ubuntu)
ansible.builtin.set_fact:
Expand Down
Loading
Loading