Skip to content

Deploy Ansible weekly instead of on every merge - #33

Merged
shenanigansd merged 1 commit into
mainfrom
claude/ansible-deployment-cleanup-q8hdoc
Aug 31, 2026
Merged

shenanigansd merged 1 commit into
mainfrom
claude/ansible-deployment-cleanup-q8hdoc

Conversation

@shenanigansd

Copy link
Copy Markdown
Member

Every push to main touching ansible/** fired a deploy, so a batch of merges meant a batch of deploys — and each run risks bouncing the Minecraft container, which players notice.

Converge once a week instead, and dispatch when a change should land sooner. live-restore: true keeps containers up across the dockerd restarts a weekly apt upgrade can trigger, so the scheduled run is safe to leave unattended.

Changes

.github/workflows/ansible-deploy.yaml

Seven lines. permissions, concurrency, environment, every step body and every SHA pin are byte-identical.

before after
trigger push on ansible/** + dispatch schedule: 17 0 * * 0 + dispatch
workflow name Deploy Ansible configuration Deploy Ansible
job id ansible-playbook run-playbook
job name Run the playbook against all hosts Run the playbook
deploy step Run the playbook Run ansible-playbook

The cron sits at :17 rather than :00 to sidestep the top-of-hour scheduling herd — midnight-UTC-Sunday is the most contended slot GitHub has.

On the naming: configuration was noise; the old job id was a bare noun; and against all hosts asserted a scope nothing in CI verifies — the playbook targets all, docker and microwave today, and nothing would catch it drifting. The deploy step is renamed to free Run the playbook for the job, and Run ansible-playbook matches the existing Run ansible-lint in lint.yaml.

The cron value is deliberately unquoted: yamlfix runs with preserve_quotes = false and would strip quotes off a quoted cron, dirtying the tree on the next prek run.

Renaming the job is safe — this workflow never fires on pull_request, so it can't be a required status check. Only lint.yaml's Lint and Ansible lint are, and both are untouched.

docs/ansible.md

Three prose edits: the ## CI deploys paragraph (the only prose statement of the trigger), the ## Required repository settings framing sentence, and the ansible Environment bullet.

The security framing needed reworking, not just a find-and-replace. Under the push trigger, landing a commit was the deploy, so one boundary covered everything. There are now two: whatever is on main when the cron fires, and who can start a dispatch. The main-only deployment branch policy is consequently load-bearing for a second reason.

⚠️ Required repo-settings change before this is correct

The docs now state the ansible Environment has no required reviewers. That's a setting outside the repo and it has to be cleared by hand.

Environment protection rules apply to scheduled runs, so with a required reviewer in place every Sunday run parks in Waiting until someone approves it, and GitHub fails unapproved deployments after 30 days — the cron would not be unattended, and the doc would disagree with reality.

Dropping it is safe: the reviewer was belt-and-braces. The topic-branch protection on the deploy secret comes from restricting deployment branches to main, which stays. Deployment branches must remain restricted to main.

Verification

Run locally against the pinned tool versions from .pre-commit-config.yaml:

  • yamlfix 1.19.1 over all 20 tracked YAML files — 0 fixed, 20 left unchanged. Unquoted cron and block-style schedule: are both stable fixed points, so prek run --all-files is clean on the first try.
  • zizmor 1.29.0 at --persona=auditor — no findings (the 1 ignored is the pre-existing dns-dry-run.yaml exception).
  • Parsed assertions: push gone, triggers exactly {schedule, workflow_dispatch}, cron loads as a string, job id / job name / step name all as intended.
  • croniter confirms the next three fires are Sundays at 00:17 UTC.
  • Whitespace, final-newline, LF and conflict-marker checks clean. The two >88-column lines in docs/ansible.md are the pre-existing ssh and shred commands, untouched.

Two checks that couldn't run in the dev sandbox, both pre-existing environment limits rather than anything about this diff: uv sync needs uv ≥0.11.8 and Python 3.14 (sandbox had 0.8.17 / 3.11), so the linters were run directly rather than through prek; and zizmor's online audits got a 401 through the sandbox proxy — those key off the uses: lines, which are unchanged. CI covers both.

After merge, schedule only ever reads the default branch, so the cron can't be exercised from this branch. Worth confirming on main that the workflow shows state: active, that a dispatch still runs end to end, and that merging an ansible/** change produces no run.

Known gaps, not addressed here

  • Silent Sunday failures. roles/unattended_upgrades surfaces a pending kernel reboot as an ansible.builtin.debug line, and a failed run is visible only to whoever opens Actions. Under the push trigger a human was watching; now nobody is. A failure()-conditioned notify step would fix it properly.
  • live-restore is not absolute. It doesn't cover daemon restarts where daemon options changed, or upgrades across major daemon versions — and needrestart runs against the daily unattended-upgrades pass too, so dockerd can still bounce any day. This makes the Ansible-caused restarts predictable, not the only ones.
  • Dependabot digest bumps are now the real restart event. With the image digest-pinned and the compose files written byte-identical, docker_compose_v2 is a no-op on an unchanged repo; the container is only recreated when the image is bumped. Those now land on a Sunday instead of mid-week, which is the actual win. Container patches wait up to a week after merge.
  • 60-day auto-disable. GitHub disables scheduled workflows in public repos after 60 days without repository activity. Monthly Dependabot PRs across four ecosystems make this a non-issue in practice, but it's a failure mode worth knowing.

Generated by Claude Code

Every push to main touching ansible/** fired a deploy, so a batch of merges
meant a batch of deploys — and each run risks bouncing the Minecraft container
on microwave, which players notice.

Converge once a week instead, and dispatch when a change should land sooner.
live-restore keeps containers up across the dockerd restarts a weekly apt
upgrade can trigger, so the scheduled run is safe to leave unattended. The cron
sits at 00:17 rather than 00:00 to sidestep the top-of-hour scheduling herd.

Also tighten the naming: drop "configuration" from the workflow name, give the
job id a verb, and stop the job name asserting a host scope that nothing in CI
verifies. The deploy step becomes "Run ansible-playbook", matching lint.yaml's
"Run ansible-lint" and freeing "Run the playbook" for the job.

docs/ansible.md follows, including the security framing: the boundary is now
whatever is on main at cron time plus who can dispatch, not merely who can push.
The ansible Environment's required reviewer is dropped — it would park every
Sunday run in Waiting until someone approved it, and GitHub fails unapproved
deployments after 30 days. The main-only deployment branch policy is what
actually keeps a topic-branch workflow edit away from SSH_PRIVATE_KEY.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQZdfLFsVJwrGXaECdPpA
@shenanigansd
shenanigansd requested a review from a team as a code owner August 31, 2026 03:49
@shenanigansd
shenanigansd merged commit 3a0b6eb into main Aug 31, 2026
4 checks passed
@shenanigansd
shenanigansd deleted the claude/ansible-deployment-cleanup-q8hdoc branch August 31, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants