From 3b8e664753c961034c4d413243657e98af31e972 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 15 Jun 2026 14:04:45 +0000 Subject: [PATCH] fix(deploy): build Go binaries on the runner, ship them, SKIP_BUILD on box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deploy SSHed into the ~458MB droplet and ran `go build` there. The Go linker's peak memory OOM-killed the build — and with it the sshd serving the deploy session — surfacing as "Connection closed by remote host" (exit 255). It was flaky because it tracked momentary memory pressure from the co-resident ergo/forgejo/tor/podman/agentbbs processes (run #25 passed, #26 failed on near-identical code). Build both binaries on the 16GB GitHub runner instead (pure-Go, modernc sqlite, so CGO_ENABLED=0 static cross-build), scp them to the droplet, and run setup.sh with SKIP_BUILD=1 so the box never compiles. Arch is detected from the droplet so amd64/arm64 both work. setup.sh now also skips the Go toolchain download when SKIP_BUILD=1. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/deploy.yml | 74 +++++++++++++++++++++++++++++++++--- setup.sh | 5 ++- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a6967ea..bf2b90b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,10 +1,18 @@ name: deploy # Fully autonomous, idempotent deploy. On every push to main (or manual -# dispatch) this SSHes to the bbs.profullstack.com droplet and re-runs the -# idempotent provisioner (setup.sh), which pulls origin, rebuilds the Go -# binaries, and restarts the agentbbs service that answers -# `ssh join@bbs.profullstack.com`. Re-running is always safe. +# dispatch) this builds the Go binaries ON THE RUNNER (which has plenty of +# RAM), ships them to the bbs.profullstack.com droplet, and re-runs the +# idempotent provisioner (setup.sh) with SKIP_BUILD=1 so the tiny droplet +# never has to compile. setup.sh still pulls origin, refreshes config/assets, +# and restarts the agentbbs service that answers `ssh join@bbs.profullstack.com`. +# Re-running is always safe. +# +# Why build on the runner: the droplet is a ~458MB box also running ergo, +# forgejo, tor, podman and the live agentbbs. The Go linker's peak memory was +# OOM-killing the build — and with it the sshd serving the deploy session, +# surfacing as "Connection closed by remote host" (exit 255). Compiling on the +# 16GB runner removes that failure mode entirely. # # Required repo secrets (Settings -> Secrets and variables -> Actions): # DEPLOY_SSH_KEY private key whose public half is in the droplet admin @@ -30,6 +38,8 @@ jobs: deploy: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + - name: Configure SSH env: DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} @@ -43,7 +53,55 @@ jobs: chmod 600 ~/.ssh/id_deploy ssh-keyscan -p "$DEPLOY_PORT" -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null - - name: Provision / redeploy (idempotent) + - name: Detect droplet architecture + id: arch + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER || 'root' }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || '2202' }} + run: | + uname_m="$(ssh -i ~/.ssh/id_deploy -p "$DEPLOY_PORT" \ + -o BatchMode=yes -o StrictHostKeyChecking=yes \ + "${DEPLOY_USER}@${DEPLOY_HOST}" 'uname -m')" + case "$uname_m" in + x86_64|amd64) goarch=amd64 ;; + aarch64|arm64) goarch=arm64 ;; + *) echo "::error::unsupported droplet arch '$uname_m'"; exit 1 ;; + esac + echo "goarch=$goarch" >> "$GITHUB_OUTPUT" + echo "::notice::droplet arch $uname_m -> GOARCH=$goarch" + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build binaries (on the runner, not the droplet) + env: + GOOS: linux + GOARCH: ${{ steps.arch.outputs.goarch }} + CGO_ENABLED: '0' # pure-Go (modernc sqlite) — static, portable binary + run: | + mkdir -p dist + go build -trimpath -o dist/agentbbs ./cmd/agentbbs + go build -trimpath -o dist/ascii-live ./cmd/ascii-live + file dist/* || true + + - name: Ship binaries to the droplet + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER || 'root' }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || '2202' }} + run: | + # scp can only name one remote target; copy each binary explicitly. + scp -i ~/.ssh/id_deploy -P "$DEPLOY_PORT" \ + -o BatchMode=yes -o StrictHostKeyChecking=yes \ + dist/agentbbs "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/agentbbs-deploy-agentbbs" + scp -i ~/.ssh/id_deploy -P "$DEPLOY_PORT" \ + -o BatchMode=yes -o StrictHostKeyChecking=yes \ + dist/ascii-live "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/agentbbs-deploy-ascii-live" + + - name: Provision / redeploy (idempotent, SKIP_BUILD=1) env: DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_USER: ${{ secrets.DEPLOY_USER || 'root' }} @@ -76,7 +134,11 @@ jobs: fi git -C "$SRC" fetch --depth 1 origin "$BRANCH" git -C "$SRC" reset --hard "origin/$BRANCH" - exec env BRANCH="$BRANCH" \ + # Install the runner-built binaries, then tell setup.sh not to compile. + install -m 0755 /tmp/agentbbs-deploy-agentbbs /usr/local/bin/agentbbs + install -m 0755 /tmp/agentbbs-deploy-ascii-live /usr/local/bin/ascii-live + rm -f /tmp/agentbbs-deploy-agentbbs /tmp/agentbbs-deploy-ascii-live + exec env BRANCH="$BRANCH" SKIP_BUILD=1 \ COINPAY_API_KEY="${COINPAY_API_KEY:-}" \ COINPAY_MERCHANT_ID="${COINPAY_MERCHANT_ID:-}" \ AGENTBBS_QRYPT_ISSUER_KEY="${AGENTBBS_QRYPT_ISSUER_KEY:-}" \ diff --git a/setup.sh b/setup.sh index a440d0a..59b497c 100755 --- a/setup.sh +++ b/setup.sh @@ -119,8 +119,11 @@ if ! command -v yt-dlp >/dev/null; then fi # ---- 2. Go toolchain (system go is too old; pin GO_VERSION) ----------------- +# Skipped entirely when SKIP_BUILD=1: the CI deploy builds the binaries on the +# runner and ships them, so the droplet needs no Go toolchain at all. GO_ROOT="/usr/local/go" -if [ "$("$GO_ROOT/bin/go" version 2>/dev/null | awk '{print $3}')" != "go${GO_VERSION}" ]; then +if [ "$SKIP_BUILD" != "1" ] && \ + [ "$("$GO_ROOT/bin/go" version 2>/dev/null | awk '{print $3}')" != "go${GO_VERSION}" ]; then log "installing Go ${GO_VERSION}" tmp="$(mktemp -d)" curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz" -o "$tmp/go.tgz" \