Add stackctl for running stacks by hand on a host
lint / yamllint + ansible-lint + syntax (push) Successful in 56s
lint / shellcheck (push) Successful in 7s
lint / secret scan (push) Successful in 2s

One shared script installed into /srv/stacks on every host, with per-host
groups in server/<host>/stack-groups. Tested in check.sh. Also two new
common mistakes in the adding-a-stack skill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-03 17:49:12 +02:00
1 parent d28494d21f
commit 35b1378080
14 files changed
+442 -4

No files matched your search

+16 -2
View File
@@ -48,9 +48,12 @@ files and one button. `AGENTS.md` holds the conventions and the full
Delete whichever platforms you are not using rather than letting them rot.
Leave a stack out only if it must never deploy from CI — see the table below.
5. **Verify statically.** `make check` from the repository root. It must pass;
5. **Group it, if it belongs in one.** Add it to a line in
`server/<host>/stack-groups` so `stackctl up <group>` on that host includes
it. Optional: `stackctl all` finds every stack regardless.
6. **Verify statically.** `make check` from the repository root. It must pass;
this is the definition of done for the playbook.
6. **Deploy.** Run the check mode, read the diff, then deploy. Never run a
7. **Deploy.** Run the check mode, read the diff, then deploy. Never run a
playbook against a host unless the person you are working with asked for it in
that turn.
@@ -100,6 +103,17 @@ working around it — that is information about the contract.
- **A reload task with no `when: compose_stack_synced`.** Every run then reports
a change, the playbook is never idempotent, and `changed=0` stops meaning
anything.
- **Committing an upstream example compose file without reading it.** They are
written for a quick start, not for a host. Look for three things: a password
given as a default (`${POSTGRES_PASSWORD:-changeme}`), which is a committed
secret — make it `${VAR:?}` and use `stack_secrets`; database or cache ports
published on every interface, which nothing outside the stack needs; and
`KEY:value` entries in a list-form `environment:`, which set a variable
literally named `KEY:value` and nothing else — the list form needs `=`.
- **A stateful service with no bind mount.** An app that falls back to SQLite
inside its container loses everything the next time Compose recreates it. Find
where it writes and mount that directory; if the image runs as a fixed uid,
give the directory to it with `stack_dirs`.
- **Putting the `stack_*` vars inside `roles:` for a two-phase playbook.** They
are scoped to that invocation and the second `include_role` will not see them.
Declare them at play level.