Add guard rails for several agents working on the repo at once

Deploys from unpushed branches were being reverted by the next deploy
from main, and in-place vault edits could overwrite each other.

- deploy-guard.yml (imported by site.yml and dns.yml) refuses real runs
  unless the checkout is clean and exactly origin/main; --check is fine.
- bin/deploy holds a lock so only one deploy runs at a time.
- bin/vault-run edits the vault under a lock with an atomic swap.
- bin/worktree starts a task in a fresh worktree from origin/main.
- CLAUDE.md explains the workflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Phil
2026-10-31 09:29:41 -06:00
co-authored by Claude Opus 5.5
parent 9363bc24d5
commit 0875389d02
8 changed files with 149 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Shared by the bin/ helpers. The main checkout holds the venv and the
# (gitignored) vault files that every worktree uses.
repo=$(git rev-parse --show-toplevel)
main_checkout=$(git -C "$repo" worktree list --porcelain | awk '/^worktree /{print $2; exit}')
lock_dir=${XDG_RUNTIME_DIR:-/tmp}/ansible-selfhosting
mkdir -p "$lock_dir" && chmod 700 "$lock_dir"
# take_lock NAME: wait for, then hold, a lock shared by every agent on this machine.
take_lock() {
local name=$1
exec 9>"$lock_dir/$name.lock"
if ! flock -n 9; then
echo "Waiting for $name lock, held by: $(cat "$lock_dir/$name.owner" 2>/dev/null || echo unknown)" >&2
flock 9
fi
echo "$(basename "$repo") (pid $$, $(date '+%H:%M:%S'))" > "$lock_dir/$name.owner"
}
# Worktrees need the gitignored vault files from the main checkout.
link_vault() {
[ "$repo" = "$main_checkout" ] && return
[ -e "$repo/group_vars/all/vault.yml" ] || ln -s "$main_checkout/group_vars/all/vault.yml" "$repo/group_vars/all/vault.yml"
[ -e "$repo/vault_pass" ] || ln -s "$main_checkout/vault_pass" "$repo/vault_pass"
}
Executable
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Run a playbook while holding the deploy lock, so only one agent deploys at a
# time. Arguments go straight to ansible-playbook.
# bin/deploy --tags gitea (site.yml)
# bin/deploy --check --diff
# PLAYBOOK=dns.yml bin/deploy
# Real runs only work from a clean checkout of origin/main (deploy-guard.yml).
set -euo pipefail
. "$(dirname "$0")/_common.sh"
link_vault
take_lock deploy
cd "$repo"
"$main_checkout/.venv/bin/ansible-playbook" "${PLAYBOOK:-site.yml}" -i hosts.yml \
--vault-password-file "$main_checkout/vault_pass" "$@"
Executable
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Edit the shared vault safely: lock, back up, decrypt to a private temp file,
# run your command on it, check the YAML, re-encrypt, and swap it in at once.
# The command gets the plaintext path as $VAULT_PLAIN (also appended as the
# last argument). Never use `ansible-vault edit`.
# bin/vault-run python3 my_edit.py
# bin/vault-run nvim
set -euo pipefail
. "$(dirname "$0")/_common.sh"
[ $# -gt 0 ] || { echo "usage: bin/vault-run <command...>" >&2; exit 2; }
vault="$main_checkout/group_vars/all/vault.yml"
pass="$main_checkout/vault_pass"
av="$main_checkout/.venv/bin/ansible-vault"
take_lock vault
backups="$HOME/Documents/backups/ansible-vault"; mkdir -p "$backups"
cp -p "$vault" "$backups/vault.yml.$(date +%Y%m%d-%H%M%S)"
tmp=$(mktemp -d "$lock_dir/vault.XXXXXX"); trap 'shred -u "$tmp"/* 2>/dev/null; rm -rf "$tmp"' EXIT
export VAULT_PLAIN="$tmp/vault.yml"
"$av" decrypt --vault-password-file "$pass" --output "$VAULT_PLAIN" "$vault" >/dev/null
"$@" "$VAULT_PLAIN"
"$main_checkout/.venv/bin/python" -c 'import sys,yaml; yaml.safe_load(open(sys.argv[1]))' "$VAULT_PLAIN"
"$av" encrypt --vault-password-file "$pass" --output "$tmp/vault.enc" "$VAULT_PLAIN" >/dev/null
chmod --reference="$vault" "$tmp/vault.enc" 2>/dev/null || true
mv "$tmp/vault.enc" "$vault"
echo "vault updated"
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Start work on a fresh branch in its own worktree, based on the latest main.
# bin/worktree <branch-name>
set -euo pipefail
. "$(dirname "$0")/_common.sh"
name=${1:?usage: bin/worktree <branch-name>}
dest="$(dirname "$main_checkout")/ansible-worktrees/$name"
git -C "$main_checkout" fetch -q origin
git -C "$main_checkout" worktree add -b "$name" "$dest" origin/main
cd "$dest" && repo=$dest && link_vault
echo "Ready: $dest"