From 8a19c35353d045262ac3e2420439b17d0e2d3dad Mon Sep 17 00:00:00 2001 From: Oleksandr Kozachuk Date: Mon, 13 Jul 2026 15:06:38 +0200 Subject: [PATCH] deploy foundation: push-based deploy.sh, spec-007, v3.0.0 --- DECISIONS.md | 5 ++++ Makefile | 4 ++++ deploy/deploy.sh | 52 ++++++++++++++++++++++++++++++++++++++++ manual-verification.md | 9 ++++--- pyproject.toml | 2 +- specs/SPEC-007-deploy.md | 46 +++++++++++++++++++++++++++++++++++ uv.lock | 2 +- 7 files changed, 115 insertions(+), 5 deletions(-) create mode 100755 deploy/deploy.sh create mode 100644 specs/SPEC-007-deploy.md diff --git a/DECISIONS.md b/DECISIONS.md index 107e8f3..ca52884 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -54,6 +54,11 @@ Decisions inside the set architecture. D-NNN, never renumbered. fact-level erasure ships with FDB-007 structured memory — stated in the user-facing confirmation, not hidden. (Superseded by D-014: erasure now covers facts, observations and episode traces.) +- **D-015** — Deploys are push-based from the dev machine + (`git archive | ssh`), not pull-based: no deploy keys or git + state on the hosts, the artifact is exactly the tag tree, untracked + live config survives in-place extraction. Trade-off: deploys need + the dev machine; acceptable for a one-operator project. - **D-014** — Structured memory (FDB-007): observations are the only consolidation feed (independent of history trimming); consolidation returns NEW facts only (no wholesale rewrite — the lossiness of the diff --git a/Makefile b/Makefile index be686c4..9503176 100644 --- a/Makefile +++ b/Makefile @@ -79,3 +79,7 @@ build: clean ## Build distribution packages # CI targets ci: install-dev all-checks ## Full CI pipeline (install deps and run all checks) + +# Deploy targets (SPEC-007) +deploy: ## Deploy a tag to a host: make deploy HOST=ggg TAG=v3.0.0 + bash deploy/deploy.sh $(HOST) $(TAG) diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..158aa0f --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# deploy.sh — push-based deploy from the dev machine (SPEC-007). +# Rollback = run again with the previous tag (DEP-06); restore the +# bot.db.pre- backup first when the schema version moved (PER-06). +set -euo pipefail + +HOST="${1:?usage: deploy.sh }" +TAG="${2:?usage: deploy.sh }" + +case "$HOST" in + fjerkroa) SERVICE=kroa CONFIG=kroa.toml ;; + ggg) SERVICE=luma CONFIG=ggg.toml ;; + *) echo "unknown host: $HOST (known: fjerkroa, ggg)" >&2; exit 1 ;; +esac + +# Tags only — no branch/commit deploys (DEP-01) +git rev-parse -q --verify "refs/tags/$TAG" >/dev/null || { echo "not a tag: $TAG" >&2; exit 1; } + +# Restaurant service window (DEP-05) +if [ "$HOST" = fjerkroa ] && [ "${DEPLOY_FORCE:-0}" != 1 ]; then + HOUR=$(TZ=Europe/Oslo date +%H) + if [ "$HOUR" -ge 11 ] && [ "$HOUR" -lt 22 ]; then + echo "refusing kroa deploy during service hours (11-22 Europe/Oslo); DEPLOY_FORCE=1 overrides (DEP-05)" >&2 + exit 1 + fi +fi + +echo "== deploy $TAG -> $HOST (service $SERVICE, config $CONFIG) ==" + +# Code push: tag tree over ~/fjerkroa_bot; untracked config/state survives (DEP-01) +git archive "$TAG" | ssh "$HOST" 'mkdir -p ~/fjerkroa_bot && tar -x -C ~/fjerkroa_bot' + +ssh "$HOST" "set -e + [ -x ~/venv-bot/bin/python ] || python3.11 -m venv ~/venv-bot + ~/venv-bot/bin/pip install -q --upgrade pip + ~/venv-bot/bin/pip install -q ~/fjerkroa_bot + printf '#!/bin/sh\ncd %s/fjerkroa_bot || exit 1\nexec %s/venv-bot/bin/python -m fjerkroa_bot --config $CONFIG\n' \"\$HOME\" \"\$HOME\" > ~/fjerkroa_bot/start.sh + chmod +x ~/fjerkroa_bot/start.sh + ~/venv-bot/bin/python -c 'import fjerkroa_bot' + find ~/fjerkroa_bot -maxdepth 3 -name bot.db | while read -r db; do cp \"\$db\" \"\$db.pre-$TAG\"; done # DEP-03 + supervisorctl restart $SERVICE" + +echo "== waiting for startsecs ==" +sleep 35 + +# Smoke (DEP-04) +ssh "$HOST" "supervisorctl status $SERVICE | grep -q RUNNING" \ + || { echo "SMOKE FAIL: $SERVICE not RUNNING on $HOST — rollback: deploy.sh $HOST (DEP-06)" >&2; exit 1; } +ssh "$HOST" "tail -80 ~/logs/supervisord.log | grep -q 'We have logged in as'" \ + || { echo "SMOKE FAIL: no fresh Discord login line on $HOST — check logs, consider rollback (DEP-06)" >&2; exit 1; } + +echo "== OK: $HOST runs $TAG — RUNNING + logged in (DEP-04) ==" diff --git a/manual-verification.md b/manual-verification.md index 42ab789..bca9db0 100644 --- a/manual-verification.md +++ b/manual-verification.md @@ -6,6 +6,9 @@ with date + result. | ID | Date | Result | | --- | --- | --- | - -_No manual-coverage requirements declared yet — DEP-NN arrives with -FDB-017._ +| DEP-01 | 2026-07-13 | Verified with the v3.0.0 ggg deploy: tag-only refusal + untracked config/state survived. fjerkroa redeploy after the service window (tree already identical to 3d22894). | +| DEP-02 | 2026-07-13 | Service map exercised: luma restart via script (v3.0.0); kroa mapping code-reviewed, exercised on its next deploy. | +| DEP-03 | 2026-07-13 | bot.db.pre-v3.0.0 backup confirmed on ggg after deploy. | +| DEP-04 | 2026-07-13 | Smoke gate exercised on ggg: RUNNING + fresh login line. | +| DEP-05 | 2026-07-13 | Live-verified: kroa deploy attempt ~15h Oslo refused without DEPLOY_FORCE=1. | +| DEP-06 | 2026-07-13 | Rollback documented (older tag + db backup restore); live drill pending — next release. | diff --git a/pyproject.toml b/pyproject.toml index 6d57e4b..c6ed356 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "fjerkroa-bot" -version = "2.0" +version = "3.0.0" description = "Discord bot with OpenAI responder for Fjærkroa and GGG" authors = [{ name = "Oleksandr Kozachuk", email = "ddeus.lp@mailnull.com" }] requires-python = ">=3.11" diff --git a/specs/SPEC-007-deploy.md b/specs/SPEC-007-deploy.md new file mode 100644 index 0000000..4f6c78d --- /dev/null +++ b/specs/SPEC-007-deploy.md @@ -0,0 +1,46 @@ +# SPEC-007 — Deploy + hosts + +Two uberspace hosts, one release: **fjerkroa** (service `kroa`, +config `kroa.toml`) and **ggg** (service `luma`, config `ggg.toml`). +Deploys are push-based from the dev machine — `git archive ` +over ssh, no repo credentials on the hosts (D-015). Coverage here is +`manual`: rows in `manual-verification.md` with date + result. + +### DEP-01 — Deploys go by tag, in place, configs survive (coverage: manual) + +`deploy/deploy.sh ` refuses unknown hosts and refs that +are not tags. The tag's tree is extracted over `~/fjerkroa_bot` — +untracked files (live TOML config, `history/`, news snapshot) are +never touched. Dev-on-host drift ends here: hosts run tag content +only. + +### DEP-02 — Per-host service map (coverage: manual) + +fjerkroa → supervisord program `kroa`, config `kroa.toml`; ggg → +program `luma`, config `ggg.toml`. The script owns this map; restart +via `supervisorctl restart `. + +### DEP-03 — Pre-deploy state backup (coverage: manual) + +Before the restart, every `bot.db` under `~/fjerkroa_bot` is copied +to `bot.db.pre-` on the host. Schema migrations are +forward-only (PER-06) — rolling back past a schema bump means +restoring this backup. + +### DEP-04 — Smoke test gates the deploy (coverage: manual) + +After restart the script fails loudly unless the service reports +RUNNING and the log shows a fresh Discord login line. On failure the +operator instruction is printed: deploy the previous tag (DEP-06). + +### DEP-05 — No kroa deploys during service hours (coverage: manual) + +Deploys to fjerkroa between 11:00 and 22:00 Europe/Oslo are refused +unless `DEPLOY_FORCE=1` is set. The restaurant does not beta-test +during dinner. + +### DEP-06 — Rollback is a deploy of an older tag (coverage: manual) + +`deploy.sh ` is the rollback path; when the +schema version moved, restore the DEP-03 backup first. Venv is +rebuilt from the tag's pyproject either way. diff --git a/uv.lock b/uv.lock index 2aa6550..919872e 100644 --- a/uv.lock +++ b/uv.lock @@ -623,7 +623,7 @@ wheels = [ [[package]] name = "fjerkroa-bot" -version = "2.0" +version = "3.0.0" source = { editable = "." } dependencies = [ { name = "aiohttp" },