diff --git a/.tangled/workflows/deploy.yaml b/.tangled/workflows/deploy.yaml new file mode 100644 index 0000000..142a49e --- /dev/null +++ b/.tangled/workflows/deploy.yaml @@ -0,0 +1,35 @@ +# Builds the image natively (no QEMU: nixery steps run on the spindle host's +# own arch, and the Dockerfile's multi-stage build never cross-compiles), then +# SSH-deploys it to the droplet. Replaces the manual scp+ssh recipe in +# docs/deploy.md. Needs "GHCR_TOKEN", "DEPLOY_SSH_KEY", and "DEPLOY_HOST" set +# under this repo's Settings -> Secrets on tangled.sh. +when: + - event: ["push"] + branch: ["main"] + +engine: nixery + +dependencies: + nixpkgs: + - docker + - openssh + +steps: + - name: "Build and push image" + command: | + echo "$GHCR_TOKEN" | docker login ghcr.io -u nmokkenstorm --password-stdin + docker build -t ghcr.io/nmokkenstorm/at-record:$TANGLED_SHA . + docker tag ghcr.io/nmokkenstorm/at-record:$TANGLED_SHA ghcr.io/nmokkenstorm/at-record:latest + docker push ghcr.io/nmokkenstorm/at-record:$TANGLED_SHA + docker push ghcr.io/nmokkenstorm/at-record:latest + + - name: "Deploy to droplet" + command: | + mkdir -p ~/.ssh + echo "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + # The droplet's authorized_keys pins this key to a forced command + # (/opt/at-record/deploy.sh) that ignores whatever we pass here, so + # there is nothing to parameterize: it always pulls ":latest" and + # restarts. Any string works; "deploy" just documents intent. + ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$DEPLOY_HOST" deploy diff --git a/docs/deploy.md b/docs/deploy.md index 6f659b2..017153b 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -61,28 +61,41 @@ set to `https://crate.mokkenstorm.dev/api/discogs/callback`. ## Build, push, deploy -Build **natively per arch**: an emulated amd64 build of the Erlang toolchain -crashes under QEMU (OTP #10355), so `buildx` multi-arch from an arm Mac won't -produce a working amd64 image. For the alpha, build on the droplet (amd64): - -```sh -TAG=$(git rev-parse --short HEAD) -git archive --format=tar.gz -o /tmp/src.tgz HEAD -scp /tmp/src.tgz root@:/opt/at-record/src.tgz -ssh root@ "cd /opt/at-record && rm -rf src && mkdir src \ - && tar xzf src.tgz -C src && cd src \ - && docker build -t ghcr.io/nmokkenstorm/at-record:$TAG ." - -# Stage deploy files + the rendered .env, then bring it up. -deploy/gen-env.sh -scp deploy/docker-compose.yml deploy/Caddyfile deploy/.env root@:/opt/at-record/ -ssh root@ "cd /opt/at-record && sed -i 's/^AT_RECORD_TAG=.*/AT_RECORD_TAG=$TAG/' .env \ - && docker compose up -d && docker compose ps" -``` - -Redeploys: rebuild on the droplet with a new tag, bump `AT_RECORD_TAG` in -`.env`, `docker compose up -d`. A real multi-arch pipeline needs native runners -per arch (amd64 + arm64) stitched with `buildx`. +A push to `main` runs [`.tangled/workflows/deploy.yaml`](../.tangled/workflows/deploy.yaml) +on Tangled's hosted spindle (`spindle.tangled.sh`), which builds the image and +SSHes it onto the droplet: no manual scp/ssh round-trip anymore. Build +**natively, still**: an emulated amd64 build of the Erlang toolchain crashes +under QEMU (OTP #10355), so the workflow uses spindle's `nixery` engine, which +runs steps as plain Docker containers on the runner's own arch (no emulation +layer at all, unlike the `microvm` engine's cross-arch case) rather than +cross-building. + +One-time setup (already done for `crate.mokkenstorm.dev` as of 2026-07-12): + +1. Attach this repo to the hosted spindle from its Tangled settings page + (`/settings/spindles` -> verify). +2. On the droplet, `/opt/at-record/deploy.sh` runs the actual redeploy + (`docker compose pull app && docker compose up -d app`), and the + droplet's `.env` pins `AT_RECORD_TAG=latest` so that pull always picks up + the newest pushed image. +3. A dedicated ed25519 deploy key is pinned in root's `authorized_keys` with + a forced command so it can ONLY ever run `deploy.sh`, regardless of what + command the client sends (`no-pty`, `no-port-forwarding`, etc. also set): + ``` + command="/opt/at-record/deploy.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc ssh-ed25519 AAAA... at-record-ci-deploy + ``` + A leaked `DEPLOY_SSH_KEY` secret can redeploy the current `:latest` image + and nothing else. +4. Under the repo's **Settings -> Secrets**, set: + - `GHCR_TOKEN`: a GitHub PAT with `write:packages` scope, for + `docker login ghcr.io`. + - `DEPLOY_SSH_KEY`: the private half of the deploy key above. + - `DEPLOY_HOST`: `root@`. + +Redeploys happen automatically on every push to `main`. First run is also the +test: watch the pipeline in the repo's CI view, and confirm +`docker compose ps` on the droplet shows the new tag if you want to +double-check. ## Smoke test (also the first confidential-client test)