Revise announce+agent proposal against the real deploy script
The original draft speced a hypothetical per-project agent.sh on a per-project cron. Reality turned out different: there's already a production bulk deploy script on the host (fixed project array, docker compose up -d per project, no explicit pull — freshness comes entirely from pull_policy: always) — so this patches that existing loop in place instead of introducing a parallel per-project script that would duplicate its array/failure-tracking logic. Also folds in a real prerequisite gap found while reconciling the proposal with the actual script: pull_policy: always isn't universal across the fleet yet, independent of keep entirely — the bulk script can't redeploy anything missing that line regardless of keep adoption. One instance of this was already fixed this session (a sibling project's docker-compose.yml missed the line when copying another project's compose pattern). Reframes §5's announce/watch benefit accordingly: since the real script is one all-or-nothing sweep, not N independent watchers, the natural fit is waking the whole sweep early (watch-any), not per-project selective redeploy — still deliberately left unbuilt until there's a real signal it's worth it over the already-scheduled sweep. Project names anonymized per policy — generic proj1..proj12 placeholders instead of the real fleet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,13 +23,50 @@ code. The relevant existing pieces for this proposal:
|
||||
- `src/cli/` — `identity.ts`, `recipient.ts`, `vault.ts`,
|
||||
`signedFetch.ts` (note: `signedFetch`'s `path` param must never
|
||||
include a query string — see the comment there for why).
|
||||
- `scripts/deploy.sh` — pulls `<project>/<env>` from `keep` into
|
||||
`$DEPLOY_ROOT/<project>/.env`, then `docker compose up -d`. Assumes
|
||||
the vault already exists and this machine already has a grant on it.
|
||||
- `.gitea/workflows/docker.yml` (present in `keep`, `wisp`,
|
||||
`npm-statuspage`) — builds and pushes an image to the Gitea container
|
||||
- `scripts/deploy.sh` (this repo) — pulls `<project>/<env>` from `keep`
|
||||
into `$DEPLOY_ROOT/<project>/.env`, then `docker compose up -d` for
|
||||
one project. Assumes the vault already exists and this machine
|
||||
already has a grant on it. **Superseded by the real deploy script
|
||||
below as the thing this proposal actually patches** — kept in this
|
||||
repo as a single-project example/fallback, not the production path.
|
||||
- **The real, already-running deploy script — on the host, not in any
|
||||
repo.** A fixed-array bulk script, already in production use, looping
|
||||
over roughly a dozen project directories:
|
||||
|
||||
```bash
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECTS=(proj1 proj2 proj3 proj4 proj5 proj6 proj7 proj8 proj9 proj10 proj11 proj12)
|
||||
for project in "${PROJECTS[@]}"; do
|
||||
dir="$ROOT/$project"
|
||||
[ -f "$dir/docker-compose.yml" ] || { echo "skip: $project"; continue; }
|
||||
(cd "$dir" && docker compose up -d) || FAILED+=("$project")
|
||||
done
|
||||
docker image prune -f
|
||||
```
|
||||
|
||||
Two things about it that matter for this proposal: **it never runs
|
||||
`docker compose pull`** — freshness comes entirely from each
|
||||
project's own `pull_policy: always` in its `docker-compose.yml`
|
||||
(`up -d` re-pulls implicitly). And **it loops over a fixed, explicit
|
||||
array**, not directory auto-discovery — adding a project means
|
||||
editing this array by hand. This is the actual thing to patch, not a
|
||||
new per-project script; see Design §4.
|
||||
- **Prerequisite gap, found while writing this proposal, not yet fixed
|
||||
everywhere**: `pull_policy: always` isn't universal across the
|
||||
fleet — confirmed present in roughly half the projects checked,
|
||||
confirmed missing in several others (one of which was fixed in this
|
||||
session — a sibling project's `docker-compose.yml` didn't have it, a
|
||||
regression from copying another project's compose file without
|
||||
carrying this line over), and unchecked for the rest (no local
|
||||
checkout available at proposal time). **This is independent of
|
||||
`keep` entirely** — the bulk script can't redeploy any project
|
||||
missing this line regardless of whether that project ever adopts
|
||||
`keep`, and it should be swept fleet-wide before or alongside this
|
||||
work, not as part of it.
|
||||
- `.gitea/workflows/docker.yml` (present in `keep` and other sibling
|
||||
projects) — builds and pushes an image to the Gitea container
|
||||
registry on every push to `main`. Does **not** currently deploy
|
||||
anything — that's a manual step today.
|
||||
anything — that's the manual/scheduled bulk script above today.
|
||||
|
||||
## Motivation
|
||||
|
||||
@@ -38,7 +75,7 @@ is still a manual "SSH in and run the deploy script" step. The obvious
|
||||
fix — CI SSHes into the VPS and runs the deploy — was considered and
|
||||
rejected: it means a standing SSH credential with production-deploy
|
||||
rights sits in Gitea's CI secrets, reachable by whatever's executing on
|
||||
the shared runner across all ~17 projects it builds. A compromised build
|
||||
the shared runner across every project it builds. A compromised build
|
||||
step in any one project could then reach every other project's deploy
|
||||
target. That's a real regression from where `keep` already left things
|
||||
(see IMPLEMENTATION.md's resolved "read no longer implies write" — the
|
||||
@@ -55,12 +92,11 @@ relay, same posture as everything else it does.
|
||||
**Not every project will have been migrated to `keep` yet.** Bootstrapping
|
||||
a project (register a recipient, grant it, push the first vault) is a
|
||||
real, deliberate step per project, and there'll be a period — maybe a
|
||||
long one — where some of the ~17 projects have it and some don't. The
|
||||
self-deploying agent has to work identically well for both, defaulting to
|
||||
exactly what happens today (a plain `docker compose pull && up -d`
|
||||
against whatever's already on disk) for anything not yet bootstrapped,
|
||||
rather than either blocking deploys entirely or hard-failing on missing
|
||||
vaults.
|
||||
long one — where some projects have it and some don't. The redeploy path
|
||||
has to work identically well for both, defaulting to exactly what
|
||||
happens today (a plain `docker compose up -d`, relying on that project's
|
||||
own `pull_policy: always`) for anything not yet bootstrapped, rather than
|
||||
either blocking deploys entirely or hard-failing on missing vaults.
|
||||
|
||||
## Design
|
||||
|
||||
@@ -113,91 +149,93 @@ keep watch <vault> [--interval 60] -- polls GET .../announce every N seconds,
|
||||
```
|
||||
|
||||
`keep watch` is deliberately dumb — plain polling, not a websocket or
|
||||
long-poll. Matches `npm-statuspage`'s existing polling pattern, and a
|
||||
60-second detection lag for a deploy is fine. Don't build push-based
|
||||
notification for this; it's more machinery for a problem polling already
|
||||
solves adequately.
|
||||
long-poll. Matches the polling pattern already used elsewhere in this
|
||||
project family, and a 60-second detection lag for a deploy is fine.
|
||||
Don't build push-based notification for this; it's more machinery for a
|
||||
problem polling already solves adequately.
|
||||
|
||||
### 4. The agent script and its fallback
|
||||
### 4. Patch the existing loop — don't build a parallel script
|
||||
|
||||
This is the part the fallback requirement is actually about. One script
|
||||
per VPS, running on a timer (systemd timer or cron, not a long-lived
|
||||
daemon — matches this project's bias toward simple, restartable,
|
||||
stateless-between-runs tooling), one invocation per project it's
|
||||
responsible for:
|
||||
This is the part the fallback requirement is actually about. The real
|
||||
deploy script already does the one thing that matters (a loop over a
|
||||
fixed project list, `docker compose up -d` per project, relying on
|
||||
`pull_policy: always` for freshness) — the change is inserting one
|
||||
`keep`-with-fallback step per iteration, in place, not writing a new
|
||||
per-project `agent.sh` that duplicates the loop/array/failure-tracking
|
||||
logic the real script already has:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# agent.sh <project> [env] — self-deploy check for one project. Meant to
|
||||
# run on a schedule (cron/systemd timer), not as a daemon.
|
||||
set -euo pipefail
|
||||
for project in "${PROJECTS[@]}"; do
|
||||
dir="$ROOT/$project"
|
||||
[ -f "$dir/docker-compose.yml" ] || { echo "skip: $project"; continue; }
|
||||
|
||||
project="${1:?usage: agent.sh <project> [env]}"
|
||||
env="${2:-production}"
|
||||
root="${DEPLOY_ROOT:-/opt/docker}"
|
||||
dir="$root/$project"
|
||||
vault="$project/$env"
|
||||
echo ""
|
||||
echo "▶ $project"
|
||||
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "error: $dir does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$dir"
|
||||
|
||||
# Best-effort: does this machine have a working keep grant on this
|
||||
# vault at all? If not — project not yet bootstrapped onto keep, or
|
||||
# this box was never granted access — fall through to the plain path.
|
||||
# Every keep call below is allowed to fail; failure just means "keep
|
||||
# isn't available for this project," not "the deploy is broken."
|
||||
if keep pull "$vault" --out .env.new 2>/dev/null; then
|
||||
mv .env.new .env
|
||||
docker compose pull -q
|
||||
docker compose up -d
|
||||
# Best-effort: does this machine have a working keep grant for
|
||||
# "$project/production"? If not — not bootstrapped onto keep yet, or
|
||||
# never will be (a static site with no secrets) — fall through to
|
||||
# exactly what this script already did before keep existed. Every
|
||||
# keep call here is allowed to fail; failure means "keep doesn't
|
||||
# apply to this project," never "stop the deploy."
|
||||
if keep pull "$project/production" --out "$dir/.env.new" 2>/dev/null; then
|
||||
mv "$dir/.env.new" "$dir/.env"
|
||||
else
|
||||
rm -f .env.new
|
||||
# No keep vault (or no grant) for this project — deploy exactly as it
|
||||
# would have worked before keep existed: whatever .env is already
|
||||
# sitting here (possibly none, if the compose file doesn't need one),
|
||||
# plain pull + up. This path must never hard-fail just because keep
|
||||
# doesn't know about this project.
|
||||
docker compose pull -q
|
||||
docker compose up -d
|
||||
rm -f "$dir/.env.new"
|
||||
fi
|
||||
|
||||
if (cd "$dir" && docker compose up -d 2>&1); then
|
||||
echo " ✓ $project up"
|
||||
else
|
||||
echo " ✗ $project failed"
|
||||
FAILED+=("$project")
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
Vault key convention: `<project>/production`, using the exact directory
|
||||
name already in `PROJECTS` — no separate mapping table to keep in sync.
|
||||
|
||||
Notice `announce`/`watch` aren't actually load-bearing in this version —
|
||||
the agent above just runs on a timer and lets `docker compose pull`
|
||||
figure out for itself whether there's anything new (a no-op if the
|
||||
digest hasn't changed, idempotent either way). That's deliberate: **a
|
||||
dumb periodic `compose pull && up -d` is already a complete, correct
|
||||
baseline deploy strategy on its own**, keep-bootstrapped or not. `announce`
|
||||
sits on top as an optimization for bootstrapped projects, not a
|
||||
requirement for the agent to function — see the next section.
|
||||
whatever already triggers this script (cron, systemd timer, or still
|
||||
run by hand) keeps triggering it exactly as before, and `keep pull`
|
||||
either updates `.env` or silently doesn't. That's deliberate: **the
|
||||
existing script, patched this way, is already a complete, correct
|
||||
baseline** — keep-bootstrapped projects get fresh secrets on every run,
|
||||
everything else behaves identically to today. `announce` sits on top as
|
||||
a latency optimization *for however this script gets triggered*, not a
|
||||
requirement for this patch to be complete — see the next section.
|
||||
|
||||
### 5. Where `announce`/`watch` actually earn their keep
|
||||
|
||||
For a project that *has* been bootstrapped, there are two independent
|
||||
benefits, worth keeping conceptually separate:
|
||||
Two independent benefits, worth keeping conceptually separate — and the
|
||||
second one looks different than it would for a one-agent-per-project
|
||||
design, because the real script is one bulk all-or-nothing sweep, not N
|
||||
independent watchers:
|
||||
|
||||
- **Secrets sourcing** — `keep pull` instead of a hand-placed `.env`.
|
||||
Available the moment a vault exists and this machine has a grant.
|
||||
Already true of `deploy.sh` today; unchanged by this proposal.
|
||||
- **Fast, precise redeploy** — instead of every VPS blindly polling
|
||||
`docker compose pull` on a fixed timer for every project it hosts
|
||||
(harmless, but imprecise and up to a full interval late), a
|
||||
bootstrapped project's agent can `keep watch` and react within
|
||||
seconds of the CI `announce` step, and only for the project that
|
||||
actually changed rather than sweeping all of them.
|
||||
Available per-project the moment its vault exists and this machine
|
||||
has a grant on it. This is what §4's patch already delivers, on its
|
||||
own, with zero dependency on `announce`.
|
||||
- **Waking the sweep early** — however the bulk script currently gets
|
||||
triggered (cron, systemd timer, or still by hand), `announce` could
|
||||
let it wake immediately on a push instead of waiting for the next
|
||||
scheduled run. The natural fit given the script's actual shape: `keep
|
||||
watch-any <vault1> <vault2> ...` blocking until *any* watched vault's
|
||||
tag changes, used as the trigger for a full sweep — not a per-project
|
||||
selective redeploy. Selective ("only redeploy the one project that
|
||||
actually changed") would require reshaping the script away from its
|
||||
current fixed-array-sweep model into something that takes a target
|
||||
project as an argument, which is a real behavior change to something
|
||||
already in production use, not a small addition — out of scope here.
|
||||
|
||||
A fancier version of the agent script could branch on `keep watch`
|
||||
succeeding vs. not to decide between "wait for a watch event" and "just
|
||||
poll on the dumb timer" — deliberately **not** speccing that split here.
|
||||
Ship the dumb-timer-plus-opportunistic-`keep-pull` version first (above);
|
||||
it already satisfies the fallback requirement completely, and whether
|
||||
the watch-driven fast path is worth the added complexity should be
|
||||
judged after living with the dumb version for a while, not designed in
|
||||
advance of any real signal that polling latency is actually a problem.
|
||||
Deliberately **not** building the wake-the-sweep-early mechanism in this
|
||||
pass. Ship §4's patch first — it already satisfies the fallback
|
||||
requirement completely and delivers the secrets-sourcing benefit on its
|
||||
own. Whether waiting up to one scheduled interval for a redeploy is
|
||||
actually a problem worth solving with `announce`/`watch-any` should be
|
||||
judged after living with the patched bulk script for a while, not
|
||||
designed in advance of any real signal that the wait matters.
|
||||
|
||||
### 6. CI workflow addition
|
||||
|
||||
@@ -249,16 +287,15 @@ not-yet-bootstrapped project, not a red X on the workflow.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Per-project agent scheduling** — one cron line per project, or one
|
||||
script that loops over every directory under `$DEPLOY_ROOT`? Leaning
|
||||
toward one-line-per-project (explicit, greppable in crontab, no
|
||||
surprise when a new project directory silently starts getting picked
|
||||
up) over auto-discovery.
|
||||
- **Does `watch` ever get used**, or does the dumb-timer version turn
|
||||
out to be good enough forever? Deliberately left unbuilt (see "Where
|
||||
announce/watch actually earn their keep") until there's a real signal
|
||||
it's worth the added complexity.
|
||||
- **Fleet-wide `pull_policy: always` sweep** — a prerequisite this
|
||||
proposal depends on but doesn't itself fix (see Context). Should
|
||||
happen before or alongside this work, as its own small pass, not
|
||||
bundled into this one.
|
||||
- **Does `watch`/`watch-any` ever get used**, or does the
|
||||
already-scheduled bulk sweep turn out to be good enough forever?
|
||||
Deliberately left unbuilt (see §5) until there's a real signal the
|
||||
wait between scheduled runs is worth solving.
|
||||
- **Multi-VPS fan-out for one project** — if a project ever runs on more
|
||||
than one box, `announce` already supports that for free (any number of
|
||||
agents can poll the same vault's announce endpoint), but this hasn't
|
||||
watchers can poll the same vault's announce endpoint), but this hasn't
|
||||
been exercised and might reveal something not accounted for here.
|
||||
|
||||
Reference in New Issue
Block a user