# Deploy news-digest (LLM-curated daily briefing) to ana-docker. # # Usage: # scripts/elway ana-docker --playbook playbooks/deploy-news-digest.yaml # # Idempotent — every step is creates-/when-gated; rerun is safe. # # After first deploy, the .env still has CHANGE_ME for the Miniflux # password. Edit it (see stacks/news-digest/README.md), then # `docker compose up -d` again — the container will run the first # digest at startup if /output is empty, so no need to wait for cron. vars: compose_dir: /opt/docker/compose/news-digest output_dir: /opt/docker/data/news-digest host_port: "8181" steps: # ── host-side dirs ────────────────────────────────────────────────── - name: Ensure compose dir exists shell: mkdir -p {{ compose_dir }} creates: "{{ compose_dir }}" - name: Ensure output dir exists (bind-mounted into both containers) shell: mkdir -p {{ output_dir }} creates: "{{ output_dir }}" # ── deploy build context (compose, env, dockerfile, app, templates) ── - name: Upload compose.yaml upload: src: stacks/news-digest/compose.yaml dest: "{{ compose_dir }}/compose.yaml" mode: "0644" - name: Upload Dockerfile upload: src: stacks/news-digest/Dockerfile dest: "{{ compose_dir }}/Dockerfile" mode: "0644" - name: Upload digest.py upload: src: stacks/news-digest/digest.py dest: "{{ compose_dir }}/digest.py" mode: "0644" - name: Upload web.py (FastAPI for the web container) upload: src: stacks/news-digest/web.py dest: "{{ compose_dir }}/web.py" mode: "0644" - name: Upload entrypoint.sh upload: src: stacks/news-digest/entrypoint.sh dest: "{{ compose_dir }}/entrypoint.sh" mode: "0755" - name: Upload run-digest.sh upload: src: stacks/news-digest/run-digest.sh dest: "{{ compose_dir }}/run-digest.sh" mode: "0755" - name: Upload crontab upload: src: stacks/news-digest/crontab dest: "{{ compose_dir }}/crontab" mode: "0644" - name: Ensure templates dir exists shell: mkdir -p {{ compose_dir }}/templates creates: "{{ compose_dir }}/templates" - name: Upload templates/digest.html.j2 upload: src: stacks/news-digest/templates/digest.html.j2 dest: "{{ compose_dir }}/templates/digest.html.j2" mode: "0644" - name: Upload templates/style.css upload: src: stacks/news-digest/templates/style.css dest: "{{ compose_dir }}/templates/style.css" mode: "0644" - name: Upload templates/favicon.svg upload: src: stacks/news-digest/templates/favicon.svg dest: "{{ compose_dir }}/templates/favicon.svg" mode: "0644" - name: Upload templates/app.js (× button + hidden tray client) upload: src: stacks/news-digest/templates/app.js dest: "{{ compose_dir }}/templates/app.js" mode: "0644" - name: Seed .env from template (only if absent) upload: src: stacks/news-digest/.env.example dest: "{{ compose_dir }}/.env" mode: "0644" when: "[ ! -f {{ compose_dir }}/.env ]" # ── build + bring up ──────────────────────────────────────────────── # The Dockerfile expects style.css to be inside the image too (it's # copied via `COPY templates /app/templates`). nginx serves the # generated index.html alongside its own copy of style.css from # /output, so the worker writes a copy of style.css into /output too. - name: docker compose build (~2-3 min first time) # ana-docker is now on docker-ce 29 (post 2026-04-24 fleet upgrade) # so BuildKit works natively — the old DOCKER_BUILDKIT=0 fallback is # no longer needed. Strip BuildKit's progress UI lines for cleaner # elway output. shell: | set -o pipefail cd {{ compose_dir }} && docker compose build 2>&1 \ | grep -vE '^#[0-9]+ |^ => |^=> |Collecting|Downloading|Requirement|Using cached|Installing collected|Successfully (installed|built)|━' - name: Pre-stage style.css + favicon.svg + app.js into /output # The worker writes HTML that references "style.css", "favicon.svg", # and "app.js" relative. None are generated dynamically; the web # container serves whichever copy lands in /output. Copy all three # from the templates dir at deploy-time. shell: | cp -f {{ compose_dir }}/templates/style.css {{ output_dir }}/style.css cp -f {{ compose_dir }}/templates/favicon.svg {{ output_dir }}/favicon.svg cp -f {{ compose_dir }}/templates/app.js {{ output_dir }}/app.js - name: docker compose up -d (rebuild + recreate so the new web image lands) shell: cd {{ compose_dir }} && docker compose up -d --build - name: Wait for the web container to serve / shell: | for i in $(seq 1 30); do curl -sf -o /dev/null --max-time 3 http://localhost:{{ host_port }}/ && exit 0 sleep 2 done exit 1 changed_when: "false" verify: - name: web returns 200 on / shell: curl -sf -o /dev/null http://localhost:{{ host_port }}/ changed_when: "false" - name: Both containers running shell: docker inspect news-digest-worker news-digest-web --format '{{.State.Status}}' | grep -c running | grep -q '^2$' changed_when: "false" - name: news-digest-web on traefik-net (homepage discovery) shell: docker inspect news-digest-web --format '{{json .NetworkSettings.Networks}}' | grep -q traefik-net changed_when: "false" - name: /api/hidden returns a JSON array shell: | curl -sf --max-time 5 http://localhost:{{ host_port }}/api/hidden \ | python3 -c "import sys, json; d = json.load(sys.stdin); assert isinstance(d, list)" changed_when: "false" - name: app.js is reachable shell: curl -sf -o /dev/null --max-time 5 http://localhost:{{ host_port }}/app.js changed_when: "false" - name: hide → /api/hidden contains it → restore → /api/hidden no longer contains it # End-to-end smoke of the hide/restore round-trip without touching # any real item id. Uses a synthetic id so we don't pollute state if # the deploy runs against a live install. shell: | set -e tid="smoke-$(date +%s)-$$" curl -sf -X POST -H 'Content-Type: application/json' \ -d "{\"id\":\"${tid}\"}" \ http://localhost:{{ host_port }}/api/hide >/dev/null curl -sf http://localhost:{{ host_port }}/api/hidden \ | python3 -c "import sys, json; assert '${tid}' in json.load(sys.stdin)" curl -sf -X POST -H 'Content-Type: application/json' \ -d "{\"id\":\"${tid}\"}" \ http://localhost:{{ host_port }}/api/restore >/dev/null curl -sf http://localhost:{{ host_port }}/api/hidden \ | python3 -c "import sys, json; assert '${tid}' not in json.load(sys.stdin)" changed_when: "false"