Point at the playbook's new home

The playbook now lives in ansible-playbooks/menu-admin/ with the other
playbooks; a second copy here would be edited by mistake.
This commit is contained in:
tanshu committed 2026-09-22 21:27:23 +05:30
1 parent 1f06e32854
commit f89bd63685
9 files changed
+4 -293

No files matched your search

+4
View File
@@ -40,8 +40,12 @@ config/ Per-Site config, one file per Site
test/ node:test suites
docs/plan.md The agreed plan
docs/adr/ Decisions worth remembering
deploy.sh Tests, build, push
```
The Ansible playbook lives in `ansible-playbooks/menu-admin/`, beside the other
playbooks, because that is where it is run from.
## Working on it
Node 24 or newer runs the TypeScript sources directly.
-19
View File
@@ -1,19 +0,0 @@
---
# Deploys the menu app for one Site. Copy this directory to
# ansible-playbooks/menu-admin/ and run it from there:
#
# ansible-playbook -l monoco -e site=hopsngrains -e tag=<tag> playbook.yml
#
# The first run targets a temporary vhost (menu-new...) so the new site can be
# checked against the live one. Cutover is the same playbook with the real vhost
# and the two retire flags set; see readme.md.
- hosts: all
become: true
vars_files:
- vars/default.yml
- "vars/{{ site }}.yml"
roles:
- network
- menu-app
- caddy
-81
View File
@@ -1,81 +0,0 @@
# menu-admin playbook
Deploys the menu app for one Site, beside the existing nginx container at first
and in place of it after cutover. Copy this directory to
`ansible-playbooks/menu-admin/` and run it from there, so `playbook_dir` and the
inventory match the other playbooks.
This playbook owns the `menu-app` container and the Site's snippet in the shared
Caddyfile. It does not touch the app's source, the image, or the other Sites'
containers.
## What lives where
| Thing | Where |
| --- | --- |
| Sections, brand, theme | `config/<site>.site.json` in the app repo, baked into the image |
| Menu content, Drafts, Originals | `/var/lib/menu-app.<site>/data` on the host |
| Admin password hash, session secret | `/var/lib/menu-app.<site>/site.env` (untracked, mode 600) |
| Which image tag runs | `-e tag=<tag>` |
Sections are deliberately not editable on the server: changing them is a commit
to the app repo, a new image, and a redeploy (ADR 0005).
## Deploying
```sh
# 1. Build and push from the app repo
./deploy.sh 2026-09-22 # -> registry.tanshu.com/menu-admin:2026-09-22
# 2. First run: a temporary hostname, so the live site keeps serving
ansible-playbook -l monoco -e site=hopsngrains -e tag=2026-09-22 playbook.yml
# 3. The admin needs a password before it can be used
ssh monoco scripts/provision-site-env.sh /var/lib/menu-app.hopsngrains.com
```
The first run creates `/var/lib/menu-app.hopsngrains.com`, starts the container
on the `menu-app.hopsngrains.com_net` network, adds a snippet for
`menu-new.hopsngrains.com` to the shared Caddyfile, and reloads Caddy. The
existing `menu.hopsngrains.com` vhost still points at the old nginx container.
`site.env` is deliberately not created by Ansible: the password is typed by a
human on the server. Until it exists the admin is disabled and the playbook says
so — the public menu still serves.
## Checking the parallel run
From a phone on restaurant wifi, not from the server:
- `https://menu-new.hopsngrains.com/` lists the Sections that have a current menu.
- A Section page scrolls, the pour line tracks, tapping a page opens it full width.
- The admin signs in, uploads a real photo, reorders, previews, publishes.
- After publishing, the phone shows the new menu and the old set appears under
"Older menus".
## Cutover
```sh
ansible-playbook -l monoco -e site=hopsngrains -e tag=2026-09-22 \
-e vhost=menu.hopsngrains.com \
-e retire_nginx_snippet=true \
-e retire_nginx_container=true \
playbook.yml
```
This repoints the real vhost at the app, removes the snippet the old playbook
wrote for that hostname, and stops the old container. The old container and
`/var/lib/menu.hopsngrains.com` are kept, not deleted: deleting 319 MB of the
only copy of the old menus is a human decision, made a week later, once the new
site has been trusted.
## Rolling back
```sh
docker start nginx-menu.hopsngrains.com
ansible-playbook -l monoco -e site=hopsngrains -e tag=<previous> \
-e vhost=menu.hopsngrains.com -e retire_nginx_snippet=false playbook.yml
```
Then delete the app's snippet from `/var/lib/caddy/conf/Caddyfile` by hand, or
set `vhost` back to the temporary name, and reload Caddy.
@@ -1,6 +0,0 @@
---
- name: Reload Caddy
# Caddy validates before it applies, so a mistake here leaves the running
# config untouched and fails the play loudly.
command: "docker exec -w /etc/caddy {{ caddy_container }} caddy reload"
listen: Reload Caddy
@@ -1,39 +0,0 @@
---
- name: Check the Caddyfile exists
stat:
path: "{{ caddyfile_path }}"
register: menu_caddyfile
- name: Fail when Caddy is not where we expect it
fail:
msg: >-
{{ caddyfile_path }} is missing. Deploy the caddy playbook (which owns the
Caddy container and its config) before this one.
when: not menu_caddyfile.stat.exists
# blockinfile replaces whatever sits between its markers, so changing `vhost`
# updates the snippet in place. The sibling nginx-menu role only appended a
# block when its exact text was absent, which cannot change a proxy target.
- name: Point Caddy at the app
blockinfile:
path: "{{ caddyfile_path }}"
marker: "# {mark} ANSIBLE MANAGED: menu app ({{ site }})"
block: "{{ caddy_snippet }}"
create: no
notify: Reload Caddy
- name: Remove the snippet the nginx-menu playbook wrote for this vhost
# A different marker, written by a different role. Leaving it in place would
# leave Caddy with two blocks for the same hostname.
blockinfile:
path: "{{ caddyfile_path }}"
marker: "# {mark} Ansible managed Caddy snippet for {{ host }}"
state: absent
when: retire_nginx_snippet | bool
notify: Reload Caddy
- name: Stop the old nginx container
docker_container:
name: "{{ legacy_container }}"
state: stopped
when: retire_nginx_container | bool
@@ -1,91 +0,0 @@
---
- name: Pull the app image
docker_image:
name: "{{ image_name }}"
tag: "{{ image_tag }}"
source: pull
force_source: yes
- name: Ensure the app root exists
file:
path: "{{ app_root }}"
state: directory
owner: "{{ app_uid }}"
group: "{{ app_gid }}"
mode: "0750"
- name: Ensure the data directory exists
file:
path: "{{ app_root }}/data"
state: directory
owner: "{{ app_uid }}"
group: "{{ app_gid }}"
mode: "0750"
- name: Look for the untracked site.env
stat:
path: "{{ app_root }}/site.env"
register: menu_site_env
- name: Warn when the admin has no password yet
debug:
msg: >-
{{ app_root }}/site.env is missing, so the admin is disabled: the public
menu still serves. Run scripts/provision-site-env.sh {{ app_root }} on
this host, then re-run this playbook.
when: not menu_site_env.stat.exists
- name: Run the menu app
docker_container:
name: "{{ docker_container }}"
image: "{{ image_name }}:{{ image_tag }}"
state: started
restart_policy: unless-stopped
user: "{{ app_uid }}:{{ app_gid }}"
env:
MENU_ROOT: /data
MENU_SITE_CONFIG: "/app/config/{{ site }}.site.json"
SECURE_COOKIES: "true"
LOG_LEVEL: info
PORT: "3000"
volumes:
- "{{ app_root }}:/data"
networks:
- name: "{{ docker_network }}"
- name: Wait for the app to answer
uri:
url: "http://{{ docker_container }}:3000/healthz"
status_code: 200
register: menu_health
retries: 10
delay: 2
until: menu_health.status == 200
changed_when: false
- name: Check the data directory for integrity problems
# Reads the same directory the container writes, as the same user, so a
# permissions mistake shows up here rather than at 8pm service.
command:
argv:
- docker
- run
- --rm
- --user
- "{{ app_uid }}:{{ app_gid }}"
- -e
- MENU_ROOT=/data
- -e
- "MENU_SITE_CONFIG=/app/config/{{ site }}.site.json"
- --entrypoint
- node
- "{{ image_name }}:{{ image_tag }}"
- src/cli/verify.ts
register: menu_verify
changed_when: false
failed_when: false
- name: Report integrity problems
debug:
msg: "{{ menu_verify.stdout_lines | default([]) }}"
when: menu_verify.rc != 0
@@ -1,8 +0,0 @@
---
- name: "Ensure {{ docker_network }} exists and Caddy is on it"
docker_network:
name: "{{ docker_network }}"
state: present
connected:
- "{{ caddy_container }}"
appends: yes
-29
View File
@@ -1,29 +0,0 @@
---
# Defaults shared by every Site. Override per Site in vars/<site>.yml, and pass
# -e site=<site> on the command line.
# Which Site config inside the image to run, and therefore which vhost it is
# for. The file lives in the app repo at config/<site>.site.json.
site: hopsngrains
image_name: registry.tanshu.com/menu-admin
image_tag: "{{ tag | default('latest') }}"
# The hostname Caddy serves this Site on. The parallel run uses a temporary
# name; cutover points the real one at the app.
vhost: "{{ host }}"
# Cutover switches. Both stay off until the parallel run has been checked.
retire_nginx_snippet: false
retire_nginx_container: false
app_uid: 1000
app_gid: 1000
caddy_container: caddy
caddyfile_path: /var/lib/caddy/conf/Caddyfile
caddy_snippet: |
{{ vhost }} {
reverse_proxy {{ docker_container }}:3000
}
-20
View File
@@ -1,20 +0,0 @@
---
# Hops n Grains. The Section list and branding live in the app repo at
# config/hopsngrains.site.json, which is baked into the image, so this file
# only says where to run it.
site: hopsngrains
host: menu.hopsngrains.com
docker_container: menu-app.hopsngrains.com
docker_network: "{{ docker_container }}_net"
# First deployment: a temporary hostname beside the existing site.
vhost: menu-new.hopsngrains.com
# Everything the app writes — data/ and the untracked site.env.
app_root: /var/lib/menu-app.hopsngrains.com
# The old nginx container serving /var/lib/menu.hopsngrains.com. Left alone
# until cutover.
legacy_container: nginx-menu.hopsngrains.com