Add a start flag, and let --check survive a first deploy
-e start=false (mode sync in CI) lands the payload and starts nothing, for migrations. A check run on a never-deployed stack now skips Compose with a note instead of failing on the missing directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c9a4d56851
commit
78b227f32d
12 files changed
+151
-14
No files matched your search
@@ -38,12 +38,15 @@ on:
|
||||
- webapp
|
||||
- webapp-staging
|
||||
mode:
|
||||
description: check reports what would change; deploy does it
|
||||
description: >-
|
||||
check reports what would change; sync copies the files but starts
|
||||
nothing; deploy does both
|
||||
required: true
|
||||
default: check
|
||||
type: choice
|
||||
options:
|
||||
- check
|
||||
- sync
|
||||
- deploy
|
||||
pull:
|
||||
description: >-
|
||||
@@ -112,7 +115,7 @@ jobs:
|
||||
''|*[!a-z0-9_-]*) echo "refusing STACK='$STACK'"; exit 1 ;;
|
||||
esac
|
||||
case "$MODE" in
|
||||
check|deploy) ;;
|
||||
check|sync|deploy) ;;
|
||||
*) echo "refusing MODE='$MODE'"; exit 1 ;;
|
||||
esac
|
||||
# An unchecked value can smuggle extra `-e` overrides onto the command
|
||||
@@ -161,6 +164,10 @@ jobs:
|
||||
# no error message and a green-looking cancel.
|
||||
if [ "$MODE" = "check" ]; then
|
||||
set -- --check --diff
|
||||
elif [ "$MODE" = "sync" ]; then
|
||||
# A real run that stops before Compose: the payload lands, the
|
||||
# directories and networks are created, no container is touched.
|
||||
set -- -e start=false
|
||||
else
|
||||
set --
|
||||
fi
|
||||
|
||||
@@ -30,12 +30,15 @@ on:
|
||||
- webapp
|
||||
- webapp-staging
|
||||
mode:
|
||||
description: check reports what would change; deploy does it
|
||||
description: >-
|
||||
check reports what would change; sync copies the files but starts
|
||||
nothing; deploy does both
|
||||
required: true
|
||||
default: check
|
||||
type: choice
|
||||
options:
|
||||
- check
|
||||
- sync
|
||||
- deploy
|
||||
pull:
|
||||
description: >-
|
||||
@@ -96,7 +99,7 @@ jobs:
|
||||
''|*[!a-z0-9_-]*) echo "refusing STACK='$STACK'"; exit 1 ;;
|
||||
esac
|
||||
case "$MODE" in
|
||||
check|deploy) ;;
|
||||
check|sync|deploy) ;;
|
||||
*) echo "refusing MODE='$MODE'"; exit 1 ;;
|
||||
esac
|
||||
# An unchecked value can smuggle extra `-e` overrides onto the command
|
||||
@@ -145,6 +148,10 @@ jobs:
|
||||
# with no error message and a green-looking cancel.
|
||||
if [ "$MODE" = "check" ]; then
|
||||
set -- --check --diff
|
||||
elif [ "$MODE" = "sync" ]; then
|
||||
# A real run that stops before Compose: the payload lands, the
|
||||
# directories and networks are created, no container is touched.
|
||||
set -- -e start=false
|
||||
else
|
||||
set --
|
||||
fi
|
||||
|
||||
+7
-2
@@ -104,8 +104,12 @@ ansible:ping:
|
||||
# as STACK, so it gets the same treatment: an unchecked value can smuggle in
|
||||
# extra `-e` overrides (stack_dest, ansible_host) and redirect a deploy.
|
||||
- case "$PULL" in ''|always|missing|never|policy) ;; *) echo "refusing PULL='$PULL'"; exit 1;; esac
|
||||
# START=false syncs the payload and starts nothing -- the `sync` mode of the
|
||||
# GitHub and Gitea workflows. Validated for the same reason as PULL.
|
||||
- case "$START" in ''|true|false) ;; *) echo "refusing START='$START'"; exit 1;; esac
|
||||
- test -f "ansible/playbooks/${STACK}.yml" || { echo "no playbook for '$STACK'"; exit 1; }
|
||||
- cd ansible && ansible-playbook "playbooks/${STACK}.yml" ${ANSIBLE_EXTRA_ARGS} ${PULL:+-e pull=$PULL}
|
||||
- cd ansible
|
||||
- ansible-playbook "playbooks/${STACK}.yml" ${ANSIBLE_EXTRA_ARGS} ${PULL:+-e pull=$PULL} ${START:+-e start=$START}
|
||||
|
||||
# Run a pipeline with STACK set (Build > Pipelines > Run pipeline, add a
|
||||
# variable) and these two appear.
|
||||
@@ -152,7 +156,8 @@ deploy:triggered:
|
||||
#
|
||||
# To update rather than deploy, run the pipeline with PULL=always and press the
|
||||
# same deploy button. That also enables the work gated behind an update run --
|
||||
# webapp's backup and migration.
|
||||
# webapp's backup and migration. To copy the files and start nothing, run it
|
||||
# with START=false.
|
||||
#
|
||||
# Order mirrors ansible/playbooks/site.yml. Add a pair when a playbook lands.
|
||||
.check:
|
||||
|
||||
@@ -53,11 +53,12 @@ These are the conventions. Follow them; do not invent per-stack variations.
|
||||
stack runs on several hosts it is still one playbook with a host group, not
|
||||
one per host. `metrics.yml` is the example.
|
||||
2. **Self-contained.** Every playbook must run standalone as
|
||||
`ansible-playbook playbooks/<stack>.yml` with no required extra vars — `pull`
|
||||
is the one optional flag. It declares its own `hosts`, its own vars, and
|
||||
everything the stack needs (directories, networks, compose files). Never rely
|
||||
on another playbook having run first. Where that means two playbooks create
|
||||
the same directory, that is correct and the repetition is deliberate.
|
||||
`ansible-playbook playbooks/<stack>.yml` with no required extra vars. `pull`
|
||||
and `start` are the two optional flags, and both are repository-wide. It
|
||||
declares its own `hosts`, its own vars, and everything the stack needs
|
||||
(directories, networks, compose files). Never rely on another playbook having
|
||||
run first. Where that means two playbooks create the same directory, that is
|
||||
correct and the repetition is deliberate.
|
||||
3. **Tag every play** with the stack name, so `site.yml --tags metrics` works.
|
||||
4. **Reuse goes in `roles/compose_stack`, not in copy-pasted tasks.** If two
|
||||
playbooks need the same logic, extend the role's variable contract rather than
|
||||
@@ -96,6 +97,7 @@ stack needs something new; do not work around it with loose tasks.
|
||||
| `stack_pull` | `{{ pull \| default('policy') }}` | Per-invocation override of the pull flag |
|
||||
| `stack_build` | `policy` | `always` for stacks built from a Dockerfile |
|
||||
| `stack_state` | `present` | `present`, `absent`, or `restarted` |
|
||||
| `stack_start` | `{{ start \| default(true) }}` | `false` syncs and starts nothing |
|
||||
|
||||
Worked examples for each: `static-site.yml` (the minimum), `reverse-proxy.yml`
|
||||
(`stack_dirs`, reload), `metrics.yml` (`stack_src`, per-host maps, ownership),
|
||||
@@ -195,6 +197,28 @@ actually move. Gate those tasks on the same flag rather than creating a separate
|
||||
Two consequences worth stating plainly: **a default run must never destroy or
|
||||
migrate anything**, and **an update run must be safe to repeat.**
|
||||
|
||||
### Deploying without starting
|
||||
|
||||
`start` is the second repo-wide extra-var, and the mirror image of `pull`:
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/webapp.yml -e start=false
|
||||
```
|
||||
|
||||
The payload is synced, `stack_dirs` and `stack_networks` are created, and the
|
||||
play stops before Compose — no container is created, started or recreated. It is
|
||||
for the case where something has to be put *into* a bind mount between the files
|
||||
landing and the stack opening them: a data directory copied across from a
|
||||
hand-deployed instance, a database restored from a dump. When you bring a
|
||||
hand-deployed service under Ansible this way, write the steps you used into the
|
||||
playbook's header comment, so the next migration has a recipe.
|
||||
|
||||
Prefer it to commenting a playbook out or running the role with
|
||||
`tasks_from: sync` by hand, and prefer the two entry points to it when the step
|
||||
in the middle is something Ansible should own — `start=false` is for work a
|
||||
person does, once. Nothing needs to read the flag: the role gates the Compose
|
||||
task on it in `tasks/up.yml`, so it applies to two-phase playbooks too.
|
||||
|
||||
## Safety
|
||||
|
||||
- **Never run a playbook against a real host unless the user asks in that turn.**
|
||||
|
||||
@@ -60,7 +60,7 @@ ansible/
|
||||
├── requirements.yml # Galaxy collections
|
||||
├── inventory/
|
||||
│ ├── hosts.yml
|
||||
│ └── group_vars/all.yml # connection settings, stack_root, the pull default
|
||||
│ └── group_vars/all.yml # connection settings, stack_root, pull/start defaults
|
||||
├── roles/compose_stack/ # the one shared "sync payload, bring stack up" role
|
||||
└── playbooks/
|
||||
├── site.yml # every stack, in dependency order
|
||||
@@ -149,6 +149,18 @@ you mean to update.
|
||||
|
||||
Verify with `--check --diff` first. These are production servers.
|
||||
|
||||
### Deploy without starting
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/webapp.yml -e start=false
|
||||
```
|
||||
|
||||
Syncs the payload and creates the directories and networks, then stops before
|
||||
Compose. It is for the gap between the files landing and the stack opening its
|
||||
bind mounts — copying data across from a hand-deployed instance, or restoring a
|
||||
database dump, before anything starts on top of it. Run again without the flag
|
||||
to start the stack.
|
||||
|
||||
## The examples
|
||||
|
||||
Six stacks, each chosen to demonstrate one thing. Read them in this order.
|
||||
|
||||
@@ -22,3 +22,8 @@ pull: policy
|
||||
# Repository root, so playbooks and the role can find payloads under server/
|
||||
# regardless of the directory ansible-playbook was invoked from.
|
||||
repo_root: "{{ (inventory_dir ~ '/../..') | realpath }}"
|
||||
|
||||
# Whether a deploy starts the stack once its files are on the host. Overridden
|
||||
# per run with `-e start=false` to land a payload and stop short of Compose —
|
||||
# see "Deploy without starting" in the README.
|
||||
start: true
|
||||
@@ -51,3 +51,10 @@ stack_pull: "{{ pull | default('policy') }}"
|
||||
# Compose build policy. `policy` builds only when the image is missing; stacks
|
||||
# that build from a Dockerfile in their payload want `always` on an update run.
|
||||
stack_build: policy
|
||||
|
||||
# Whether to start the stack at all. False syncs the payload, creates the
|
||||
# directories and the networks, and stops before Compose — the shape a migration
|
||||
# needs, where data has to be moved into a bind mount after the files land and
|
||||
# before anything opens them. Defaults to the repo-wide `start` flag, so
|
||||
# `-e start=false` works on every playbook without editing any of them.
|
||||
stack_start: "{{ start | default(true) | bool }}"
|
||||
@@ -1,4 +1,49 @@
|
||||
---
|
||||
# docker_compose_v2 validates project_src with os.path.isdir, which is false both
|
||||
# for a path that is missing and for one that is a file, and reports either as
|
||||
# `"..." is not a directory`. Stat it first so the two are told apart here rather
|
||||
# than left to that one message.
|
||||
- name: Look for the stack directory on the host
|
||||
ansible.builtin.stat:
|
||||
path: "{{ stack_dest }}"
|
||||
register: compose_stack_dir
|
||||
changed_when: false
|
||||
|
||||
- name: Verify the stack directory is a directory
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- compose_stack_dir.stat.isdir | default(false)
|
||||
fail_msg: >-
|
||||
{{ stack_dest }} exists on {{ inventory_hostname }} but is not a directory
|
||||
quiet: true
|
||||
when: compose_stack_dir.stat.exists
|
||||
|
||||
# A check run creates nothing, so on a host that has never deployed this stack
|
||||
# there is no project directory for the module to inspect and it fails on its
|
||||
# absence — a first `--check` would report the payload diff correctly and then
|
||||
# die for a reason that says nothing about the stack. Skip it instead; the sync
|
||||
# tasks above have already reported everything check mode can know.
|
||||
- name: Note that the stack cannot be checked before its first deploy
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ stack_dest }} does not exist yet, so the compose step was skipped.
|
||||
Deploy for real to create it.
|
||||
when:
|
||||
- stack_start | bool
|
||||
- ansible_check_mode
|
||||
- not compose_stack_dir.stat.exists
|
||||
|
||||
# The counterpart to that message, for `-e start=false`. Without it the play ends
|
||||
# with the compose task skipped and nothing saying so, which reads exactly like a
|
||||
# run that quietly failed to do its job.
|
||||
- name: Note that the stack was synced but deliberately not started
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
start=false, so the payload is on {{ inventory_hostname }} and
|
||||
{{ stack_name }} was left as it was. Run again without the flag to start
|
||||
it.
|
||||
when: not stack_start | bool
|
||||
|
||||
- name: Bring the stack up
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ stack_dest }}"
|
||||
@@ -12,3 +57,8 @@
|
||||
# project directory. The module sets the process working directory but not PWD,
|
||||
# so ${PWD} would silently become the SSH login directory. Set it explicitly.
|
||||
environment: "{{ {'PWD': stack_dest} | combine(stack_env) }}"
|
||||
# stack_start is gated here rather than around the include in main.yml, because
|
||||
# a two-phase playbook calls tasks_from: up directly and main.yml never sees it.
|
||||
when:
|
||||
- stack_start | bool
|
||||
- compose_stack_dir.stat.exists or not ansible_check_mode
|
||||
@@ -76,6 +76,22 @@ Two properties follow, and both are load-bearing:
|
||||
- **An update run must be safe to repeat.** It is not transactional. Something
|
||||
will fail halfway through, and the fix is to run it again.
|
||||
|
||||
## Starting is a flag too
|
||||
|
||||
`start` is the same idea pointed at a different question. `pull` asks *which
|
||||
images*; `start` asks *whether to run Compose at all*. `-e start=false` lands the
|
||||
payload, creates the directories and the networks, and stops.
|
||||
|
||||
The alternative — a one-off playbook that syncs and does not start, or a
|
||||
commented-out `roles:` block during a migration — is a copy of the real playbook
|
||||
that exists only while somebody is mid-task, which is exactly when a copy is most
|
||||
likely to be deployed by accident. A flag leaves one playbook, and leaves it
|
||||
honest.
|
||||
|
||||
The role gates the Compose task itself rather than the `up.yml` include, because
|
||||
a two-phase playbook calls `tasks_from: up` directly and would otherwise slip
|
||||
past the flag.
|
||||
|
||||
## The `SSH_KEY_PATH` seam
|
||||
|
||||
Ansible knows one thing about credentials: a path to a private key. One script
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ Actions → **deploy** → Run workflow. Three fields:
|
||||
| Field | Values | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `stack` | the dropdown | Which playbook to run. `site` is all of them. |
|
||||
| `mode` | `check`, `deploy` | `check` runs `--check --diff` and touches nothing |
|
||||
| `mode` | `check`, `sync`, `deploy` | `check` runs `--check --diff` and touches nothing; `sync` copies the files for real but starts no container (`-e start=false`) |
|
||||
| `pull` | `policy`, `always`, `missing`, `never` | `always` fetches newer images **and** enables per-stack update work |
|
||||
|
||||
Work up from harmless to real the first time:
|
||||
|
||||
@@ -67,7 +67,7 @@ Actions → **deploy** → Run workflow. Three fields:
|
||||
| Field | Values | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `stack` | the dropdown | Which playbook to run. `site` is all of them. |
|
||||
| `mode` | `check`, `deploy` | `check` runs `--check --diff` and touches nothing |
|
||||
| `mode` | `check`, `sync`, `deploy` | `check` runs `--check --diff` and touches nothing; `sync` copies the files for real but starts no container (`-e start=false`) |
|
||||
| `pull` | `policy`, `always`, `missing`, `never` | `always` fetches newer images **and** enables per-stack update work |
|
||||
|
||||
Work up from harmless to real the first time:
|
||||
|
||||
@@ -54,6 +54,10 @@ Pipelines → Run pipeline, add a variable) and press the same deploy button. Th
|
||||
also enables the work gated behind an update run — `webapp`'s backup and
|
||||
migration.
|
||||
|
||||
To copy the files and start nothing — the `sync` mode of the other two
|
||||
platforms — run it with `START=false` instead. `true`, `false` and empty are the
|
||||
only values accepted.
|
||||
|
||||
`ansible:ping` is the right first thing to run after setting the variables up. It
|
||||
should print `SUCCESS => {"ping": "pong"}` for every host, and changes nothing.
|
||||
|
||||
|
||||
Reference in new issue
Block a user