Repository navigation
Expand file tree
/
Copy pathmise.toml
More file actions
240 lines (197 loc) · 10.4 KB
/
Copy pathmise.toml
File metadata and controls
240 lines (197 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# Task shortcuts. `mise tasks` lists them, `mise run <name>` runs one.
#
# (no prefix) development — compose.yaml, source bind-mounted, no secrets needed
# prod-* production — the Docker Swarm stack in compose.swarm.yaml
# uv is not otherwise installed on the host; `mise install` provides it, and uv
# manages the Python interpreter itself from pyproject.toml's requires-python.
[tools]
betterleaks = "latest"
pinact = "latest"
uv = "latest"
# ── Development ───────────────────────────────────────────────────────────
[tasks.up]
description = "Start the dev stack (db, valkey, web with live reload) in the background"
run = "docker compose up -d"
[tasks.down]
description = "Stop the dev stack, keeping volumes"
run = "docker compose down"
[tasks.logs]
description = "Follow the dev web container's logs"
run = "docker compose logs -f web"
[tasks.shell]
description = "Open a shell in the dev web container"
run = "docker compose exec web bash"
[tasks.restart]
description = "Restart the dev web container (settings change, or a wedged worker)"
run = "docker compose restart web"
[tasks.rebuild]
description = "Rebuild and restart dev web — needed after pyproject.toml / uv.lock changes"
run = "docker compose up -d --build web"
[tasks.migrate]
description = "Apply migrations to the dev database"
run = "docker compose exec web python manage.py migrate"
[tasks.makemigrations]
description = "Generate migrations in dev (the source is bind-mounted, so the files land in your checkout — commit them)"
run = "docker compose exec web python manage.py makemigrations"
[tasks.bootstrap]
description = "Create the page tree, site settings, and snippets in dev (idempotent)"
run = "docker compose exec web python manage.py bootstrap_site"
[tasks.superuser]
description = "Create a CMS superuser in dev"
run = "docker compose exec web python manage.py createsuperuser"
[tasks.manage]
description = "Run any manage.py command in the dev web container, e.g. mise run manage -- shell"
run = "docker compose exec web python manage.py"
[tasks.test]
description = "Run pytest on the host against the containerised db and valkey (parallel via pytest-xdist; append `-n 0` for serial)"
env = { DATABASE_URL = "postgres://bpd:bpd@localhost:5432/bpd", VALKEY_URL = "redis://localhost:6379/0" }
run = ["docker compose up -d db valkey", "uv run pytest -n auto"]
[tasks.lint]
description = "Ruff check and format --check"
run = ["uv run ruff check .", "uv run ruff format --check ."]
[tasks.scan-secrets]
description = "Scan the full git history for secrets with betterleaks"
run = "betterleaks git --verbose --redact --confidence medium ."
[tasks.install-hooks]
description = "Point git at .githooks so the betterleaks pre-commit check runs locally"
run = "git config core.hooksPath .githooks && chmod +x .githooks/pre-commit"
# ── Production (Docker Swarm) ─────────────────────────────────────────────
# compose.swarm.yaml rolls `web` out start-first: the new task must pass its
# healthcheck before the old one is stopped, so a deploy causes no downtime.
#
# The stack name is fixed at `blackpythondevs-prod`. Do not deploy under a
# different name, or the named volumes (postgres_data, media_data, ...) won't
# line up with the existing data.
#
# Only tasks that create or update the stack need `fnox exec --`, because
# compose interpolates secrets from env vars at deploy time. Tasks that exec
# into a running container don't: the container already has its environment.
[tasks.prod-init]
description = "One-time: put this host into swarm mode"
run = "docker swarm init"
[tasks.prod-build]
description = "Build the production web image, tagged with the current git commit"
run = '''
#!/usr/bin/env bash
set -euo pipefail
IMG="bpd-web:$(git rev-parse --short HEAD)"
docker build --target production -t "$IMG" -t bpd-web:latest .
echo "Built $IMG"
'''
[tasks.prod-deploy]
description = "Build web, roll it out with zero downtime, then prune old images. Does not migrate — run prod-migrate after."
run = '''
#!/usr/bin/env bash
set -euo pipefail
IMG="bpd-web:$(git rev-parse --short HEAD)"
docker build --target production -t "$IMG" -t bpd-web:latest .
fnox exec -- env WEB_IMAGE="$IMG" docker stack deploy -c compose.swarm.yaml blackpythondevs-prod
mise run clean-images
'''
[tasks.prod-rollback]
description = "Roll web back to its previous image (also start-first, zero downtime)"
run = "docker service rollback blackpythondevs-prod_web"
[tasks.prod-restart]
description = "Rolling restart of web with the same image (e.g. after changing a secret) — zero downtime"
run = "docker service update --force blackpythondevs-prod_web"
[tasks.prod-status]
description = "Show every task in the stack, including past failures"
run = "docker stack ps blackpythondevs-prod --no-trunc"
[tasks.prod-config]
description = "Show what the stack file resolves to — fails loudly on a missing variable"
run = "fnox exec -- docker stack config -c compose.swarm.yaml"
[tasks.prod-logs]
description = "Follow the production web logs"
run = "docker service logs -f blackpythondevs-prod_web"
[tasks.prod-shell]
description = "Open a shell in the running production web container"
run = "docker exec -it $(docker ps -q -f name=blackpythondevs-prod_web | head -1) bash"
[tasks.prod-migrate]
description = "Apply migrations against the production database"
run = "docker exec $(docker ps -q -f name=blackpythondevs-prod_web | head -1) python manage.py migrate"
[tasks.prod-db-snapshot]
description = "Dump the production database to backups/bpd-<time>-<commit>.dump (pg_dump custom format). Run before prod-migrate."
run = '''
#!/usr/bin/env bash
set -euo pipefail
DB="$(docker ps -q -f name=blackpythondevs-prod_db | head -1)"
[ -n "$DB" ] || { echo "No running blackpythondevs-prod_db container" >&2; exit 1; }
mkdir -p backups
chmod 700 backups
OUT="backups/bpd-$(date -u +%Y%m%dT%H%M%SZ)-$(git rev-parse --short HEAD).dump"
# The dump holds member data, so it is never world-readable, even mid-write.
umask 077
# No -t: a TTY would mangle the binary dump.
docker exec "$DB" pg_dump -U bpd -d bpd -Fc > "$OUT.partial"
# Prove the archive is readable before calling it a snapshot.
docker exec -i "$DB" pg_restore --list < "$OUT.partial" > /dev/null
mv "$OUT.partial" "$OUT"
echo "Snapshot: $OUT ($(du -h "$OUT" | cut -f1))"
'''
[tasks.prod-db-restore]
description = "Restore a prod-db-snapshot dump over the production database. Usage: mise run prod-db-restore -- backups/<file>.dump"
run = '''
#!/usr/bin/env bash
set -euo pipefail
FILE="${1:?Usage: mise run prod-db-restore -- backups/<file>.dump}"
[ -f "$FILE" ] || { echo "No such file: $FILE" >&2; exit 1; }
DB="$(docker ps -q -f name=blackpythondevs-prod_db | head -1)"
[ -n "$DB" ] || { echo "No running blackpythondevs-prod_db container" >&2; exit 1; }
echo "This replaces EVERY table in the production database with $FILE."
echo "If the snapshot predates a migration, also run prod-rollback so web's code matches the restored schema."
read -r -p "Type 'restore' to continue: " answer
[ "$answer" = "restore" ] || { echo "Aborted; nothing changed."; exit 1; }
# One transaction: if anything fails, the database is left exactly as it was.
docker exec -i "$DB" pg_restore -U bpd -d bpd --clean --if-exists --no-owner --no-privileges --single-transaction < "$FILE"
echo "Restored $FILE"
'''
[tasks.prod-bootstrap]
description = "Create the page tree, site settings, and snippets in production (idempotent)"
run = "docker exec $(docker ps -q -f name=blackpythondevs-prod_web | head -1) python manage.py bootstrap_site"
[tasks.prod-superuser]
description = "Create a CMS superuser in production"
run = "docker exec -it $(docker ps -q -f name=blackpythondevs-prod_web | head -1) python manage.py createsuperuser"
[tasks.prod-manage]
description = "Run any manage.py command in production, e.g. mise run prod-manage -- shell"
run = "docker exec -it $(docker ps -q -f name=blackpythondevs-prod_web | head -1) python manage.py"
[tasks.prod-check]
description = "Django's deployment security checklist against the running stack"
run = "docker exec $(docker ps -q -f name=blackpythondevs-prod_web | head -1) python manage.py check --deploy"
[tasks.prod-caddy-reload]
description = "Reload the Caddyfile in place — it is bind-mounted, so no redeploy"
run = "docker exec $(docker ps -q -f name=blackpythondevs-prod_caddy | head -1) caddy reload --config /etc/caddy/Caddyfile"
[tasks.prod-gunicorn-reload]
description = "Gracefully restart gunicorn workers in place (env change, no new code)"
run = "docker kill -s HUP $(docker ps -q -f name=blackpythondevs-prod_web | head -1)"
# ── Cleanup ───────────────────────────────────────────────────────────────
# prod-build tags every image with a commit hash, so they pile up (~1.7GB each
# unpruned). Running containers' images are never removed by these tasks.
[tasks.docker-df]
description = "Show how much disk Docker is using (images, volumes, build cache)"
run = "docker system df"
[tasks.clean-images]
description = "Remove old bpd-web:<commit> images, keeping the newest N (default 3) and any in use. Usage: mise run clean-images -- 5"
run = '''
#!/usr/bin/env bash
set -euo pipefail
KEEP="${1:-3}"
in_use="$(docker ps -a --format '{% raw %}{{.Image}}{% endraw %}' | sort -u)"
docker images bpd-web --format '{% raw %}{{.CreatedAt}}|{{.Repository}}:{{.Tag}}{% endraw %}' \
| sort -r | cut -d'|' -f2 \
| grep -v ':latest$' | tail -n +"$((KEEP + 1))" \
| while read -r img; do
if grep -qx "$img" <<<"$in_use"; then echo "keep (in use): $img"; else docker rmi "$img" || true; fi
done
'''
[tasks.clean-dangling]
description = "Remove dangling (untagged) images and unused build cache older than 7 days"
run = ["docker image prune -f", "docker builder prune -f --filter until=168h"]
[tasks.clean]
description = "Routine cleanup: old bpd-web images + dangling images + stale build cache"
depends = ["clean-images", "clean-dangling"]
[tasks.clean-all]
description = "Aggressive: remove ALL unused images and build cache (not volumes). Next build will be cold."
run = ["docker image prune -a -f", "docker builder prune -a -f"]
[tasks.prod-down]
description = "Tear down the production stack (keeps named volumes)"
run = "docker stack rm blackpythondevs-prod"