9c20e42215
stacks/task-board/compose.yaml + .env.example describe the runtime —
image tag is task-board:local (built on the host), 7878 on host maps
to 7878 in container, SQLite lives at /opt/docker/conf/task-board/data/
(bind mount, uid 1000 friendly), homepage auto-card labels under
Toolchain group, on traefik-net like the rest of the fleet.
playbooks/deploy-task-board.yaml is the first real elway playbook —
exercises everything we built tier-1 + tier-2 idempotency for:
- `creates:` on mkdir + first-time clone + compose dir + data dir
- `when:` to chown /opt/docker/build only if it came up root-owned
- `when:` to seed .env only if one doesn't already exist (never
clobbers user edits on rerun)
- `changed_when:` on the `git reset --hard` step so repeat runs
against the same ref report `ok` instead of `changed`
- `changed_when: "false"` on every verify step (they attest, not
change)
- `upload:` with mode for compose.yaml + .env
Post-up the playbook polls /api/health for 30s before handing off to
the verify phase, so verification doesn't race the healthcheck's
start_period. Verify covers: /api/health 200, /api/tasks shape, /mcp
reachable, container on traefik-net.
Prereqs documented in the playbook header: Docker + compose plugin,
traefik-net network, git SSH access to gitea from the target host.
125 lines
5.6 KiB
YAML
125 lines
5.6 KiB
YAML
# Deploy task-board (https://gitea.phasefinal.com/vh/task-board) to a
|
|
# Docker host following the PFI /opt/docker/ convention (ana-docker by
|
|
# default, but the playbook works against any host with Docker +
|
|
# traefik-net in place).
|
|
#
|
|
# Idempotent: rerunning is safe. Creates-gates + conditional when:
|
|
# checks skip work that's already done; `docker compose up -d` is itself
|
|
# idempotent (no restart unless compose content or env changed).
|
|
#
|
|
# Usage:
|
|
# scripts/elway ana-docker --playbook playbooks/deploy-task-board.yaml
|
|
# scripts/elway ana-docker --playbook playbooks/deploy-task-board.yaml --var ref=v0.1.0
|
|
#
|
|
# Prereqs on the target host:
|
|
# - Docker + docker compose plugin
|
|
# - `traefik-net` docker network (external)
|
|
# - Target user (lkraven) has git SSH access to gitea.phasefinal.com
|
|
# — either SSH key authorized in gitea, or the repo is HTTPS-reachable
|
|
# if you swap `repo_url` below.
|
|
# - Target user is in the `docker` group.
|
|
|
|
vars:
|
|
repo_url: git@gitea.phasefinal.com:vh/task-board.git
|
|
ref: main
|
|
build_dir: /opt/docker/build/task-board
|
|
image_tag: task-board:local
|
|
compose_dir: /opt/docker/compose/task-board
|
|
data_dir: /opt/docker/conf/task-board/data
|
|
host_port: "7878"
|
|
|
|
steps:
|
|
# ── host-side directory prep ─────────────────────────────────────────
|
|
- name: Ensure /opt/docker/build parent exists
|
|
shell: mkdir -p /opt/docker/build
|
|
sudo: true
|
|
creates: /opt/docker/build
|
|
|
|
- name: Chown /opt/docker/build to lkraven (only if mkdir'd by root above)
|
|
shell: chown lkraven:lkraven /opt/docker/build
|
|
sudo: true
|
|
when: '[ "$(stat -c %U /opt/docker/build)" != lkraven ]'
|
|
|
|
# ── fetch / sync source ─────────────────────────────────────────────
|
|
- name: Clone task-board repo if absent
|
|
# Auto-accept the first-run host key so the playbook doesn't hang
|
|
# prompting for yes/no.
|
|
shell: GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new" git clone {{ repo_url }} {{ build_dir }}
|
|
creates: "{{ build_dir }}/.git"
|
|
|
|
- name: Fetch from origin
|
|
shell: cd {{ build_dir }} && git fetch --quiet origin
|
|
|
|
- name: Reset working tree to origin/{{ ref }}
|
|
shell: cd {{ build_dir }} && git reset --hard origin/{{ ref }}
|
|
# Report ok (no-change) when the tree was already at the requested
|
|
# ref — saves a noisy CHANGED status line on no-op reruns.
|
|
changed_when: '[ "$(cd {{ build_dir }} && git rev-parse HEAD 2>/dev/null)" != "$(cd {{ build_dir }} && git rev-parse origin/{{ ref }} 2>/dev/null)" ]'
|
|
|
|
# ── image build ─────────────────────────────────────────────────────
|
|
- name: Build image {{ image_tag }}
|
|
shell: cd {{ build_dir }} && docker build -t {{ image_tag }} .
|
|
# Docker build reuses layer cache and is fast on reruns, but it
|
|
# always runs — we can't cheaply know up-front whether anything
|
|
# downstream has changed. Leave it in the always-run lane; Docker
|
|
# itself handles the no-op efficiently.
|
|
|
|
# ── compose + data dirs ─────────────────────────────────────────────
|
|
- name: Ensure compose dir exists
|
|
shell: mkdir -p {{ compose_dir }}
|
|
creates: "{{ compose_dir }}"
|
|
|
|
- name: Ensure data dir exists
|
|
# Created as lkraven (uid 1000 on these hosts), matching the
|
|
# container's `app` user — no chown dance needed.
|
|
shell: mkdir -p {{ data_dir }}
|
|
creates: "{{ data_dir }}"
|
|
|
|
# ── deploy compose files ────────────────────────────────────────────
|
|
- name: Upload compose.yaml
|
|
upload:
|
|
src: stacks/task-board/compose.yaml
|
|
dest: "{{ compose_dir }}/compose.yaml"
|
|
mode: "0644"
|
|
|
|
- name: Seed .env from template (only if absent)
|
|
upload:
|
|
src: stacks/task-board/.env.example
|
|
dest: "{{ compose_dir }}/.env"
|
|
mode: "0644"
|
|
when: "[ ! -f {{ compose_dir }}/.env ]"
|
|
|
|
# ── bring up + wait for ready ───────────────────────────────────────
|
|
- name: docker compose up -d
|
|
shell: cd {{ compose_dir }} && docker compose up -d
|
|
|
|
- name: Wait for /api/health to respond
|
|
# Short retry loop — docker compose up returns before healthcheck
|
|
# stabilizes; we want verify: to run against a live server.
|
|
shell: |
|
|
for i in $(seq 1 30); do
|
|
curl -sf -o /dev/null http://localhost:{{ host_port }}/api/health && exit 0
|
|
sleep 1
|
|
done
|
|
exit 1
|
|
changed_when: "false"
|
|
|
|
verify:
|
|
- name: /api/health returns 200
|
|
shell: curl -sf -o /dev/null http://localhost:{{ host_port }}/api/health
|
|
changed_when: "false"
|
|
|
|
- name: /api/tasks returns a JSON tasks list
|
|
shell: curl -sf http://localhost:{{ host_port }}/api/tasks | grep -q '"tasks"'
|
|
changed_when: "false"
|
|
|
|
- name: MCP endpoint reachable (expect 3xx redirect for GET /mcp → /mcp/)
|
|
shell: '[ "$(curl -s -o /dev/null -w %{http_code} http://localhost:{{ host_port }}/mcp)" -ge 300 ]'
|
|
changed_when: "false"
|
|
|
|
- name: Container is in the traefik-net network
|
|
# `traefik-net` has a dash, so it's not accessible via Go template dot
|
|
# syntax — JSON-encode the networks map and grep for the key instead.
|
|
shell: docker inspect task-board --format '{{json .NetworkSettings.Networks}}' | grep -q traefik-net
|
|
changed_when: "false"
|