mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
* ci(deploy): preempt self-update timer so it can't starve the deploy lock
The self-update systemd timer redeploys from source (no SKIP_BUILD) and
can hold setup.sh's flock for >5min while compiling on a tiny droplet.
When it fires close to a CI push it starves the deploy, which waits the
full 5min on the lock and then fails with 'another setup.sh run is in
progress (lock held >5m)'.
The CI push is authoritative (ships prebuilt binaries + resets to the
exact commit), so stop any in-flight timer run to release the lock and
pause the timer before taking it. setup.sh re-enables the timer at the
end of its run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(files): isolate over-quota test from seeded-README baseline
TestWebSaveOverQuotaPreservesExistingFile set quota=5 but left sess.used
at the newSession baseline, which already counts the README.txt that
ensureUserPub seeds into /public (added in d19c5c4). That baseline alone
exceeds 5 bytes, so the initial 2-byte save was rejected with
'quota exceeded' before the test could exercise the over-quota replace.
Reset sess.used to 0 after setting the tiny quota, mirroring
TestQuotaEnforced, so the writer starts from a clean gauge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
8 KiB
YAML
170 lines
8 KiB
YAML
name: deploy
|
|
|
|
# Fully autonomous, idempotent deploy. On every push to main (or manual
|
|
# 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
|
|
# user's ~/.ssh/authorized_keys
|
|
# DEPLOY_HOST bbs.profullstack.com (or the droplet's public IP)
|
|
# Optional (have sensible defaults below):
|
|
# DEPLOY_USER admin SSH user (default: root)
|
|
# DEPLOY_PORT admin SSH port (default: 2202 — setup.sh moves OpenSSH here)
|
|
#
|
|
# The admin user needs passwordless sudo (root already does).
|
|
|
|
on:
|
|
push:
|
|
branches: [main, master]
|
|
workflow_dispatch:
|
|
|
|
# Never let two deploys overlap; setup.sh also self-locks, this is belt+braces.
|
|
concurrency:
|
|
group: deploy-production
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Configure SSH
|
|
env:
|
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || '2202' }}
|
|
run: |
|
|
test -n "$DEPLOY_SSH_KEY" || { echo "::error::DEPLOY_SSH_KEY secret is not set"; exit 1; }
|
|
test -n "$DEPLOY_HOST" || { echo "::error::DEPLOY_HOST secret is not set"; exit 1; }
|
|
install -d -m 700 ~/.ssh
|
|
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_deploy
|
|
chmod 600 ~/.ssh/id_deploy
|
|
ssh-keyscan -p "$DEPLOY_PORT" -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
|
|
|
- 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@v6
|
|
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' }}
|
|
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || '2202' }}
|
|
# Deploy whichever branch was pushed (main or master), so a rename
|
|
# "just works". For workflow_dispatch this is the chosen branch.
|
|
DEPLOY_BRANCH: ${{ github.ref_name }}
|
|
# App secrets injected into the droplet's env file by setup.sh.
|
|
# GitHub masks these in logs; setup.sh upserts them idempotently.
|
|
COINPAY_API_KEY: ${{ secrets.COINPAY_API_KEY }}
|
|
COINPAY_MERCHANT_ID: ${{ secrets.COINPAY_MERCHANT_ID }}
|
|
AGENTBBS_QRYPT_ISSUER_KEY: ${{ secrets.AGENTBBS_QRYPT_ISSUER_KEY }}
|
|
run: |
|
|
ssh -i ~/.ssh/id_deploy -p "$DEPLOY_PORT" \
|
|
-o BatchMode=yes -o StrictHostKeyChecking=yes \
|
|
"${DEPLOY_USER}@${DEPLOY_HOST}" \
|
|
"sudo -n env BRANCH=$(printf %q "$DEPLOY_BRANCH") \
|
|
COINPAY_API_KEY=$(printf %q "$COINPAY_API_KEY") \
|
|
COINPAY_MERCHANT_ID=$(printf %q "$COINPAY_MERCHANT_ID") \
|
|
AGENTBBS_QRYPT_ISSUER_KEY=$(printf %q "$AGENTBBS_QRYPT_ISSUER_KEY") \
|
|
bash -s" <<'REMOTE'
|
|
set -euo pipefail
|
|
REPO=https://github.com/profullstack/agentbbs.git
|
|
BRANCH="${BRANCH:-main}"
|
|
SRC=/opt/agentbbs
|
|
# Bootstrap on a fresh box, then always sync to origin so we run the
|
|
# latest setup.sh (it may have changed in this very push).
|
|
if [ ! -d "$SRC/.git" ]; then
|
|
git clone --depth 1 -b "$BRANCH" "$REPO" "$SRC"
|
|
fi
|
|
git -C "$SRC" fetch --depth 1 origin "$BRANCH"
|
|
git -C "$SRC" reset --hard "origin/$BRANCH"
|
|
# Preempt the self-update timer. It redeploys from source (no
|
|
# SKIP_BUILD) and can hold setup.sh's flock for >5min while compiling
|
|
# on this tiny droplet, which would starve and time out the deploy
|
|
# below. This CI push is authoritative (prebuilt binaries + reset to
|
|
# the exact commit), so stop any in-flight timer run to release the
|
|
# lock and pause the timer so it can't re-fire mid-deploy; setup.sh
|
|
# re-enables the timer at the end of its run.
|
|
systemctl stop agentbbs-update.timer agentbbs-update.service 2>/dev/null || true
|
|
# 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:-}" \
|
|
"$SRC/setup.sh"
|
|
REMOTE
|
|
|
|
- name: Smoke-test that agentbbs serves :22
|
|
if: success()
|
|
env:
|
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
run: |
|
|
# Confirm an SSH server answers on :22 WITHOUT authenticating — a real
|
|
# join@ connection would register the connecting key as a new account,
|
|
# so we never complete a handshake here. Admin OpenSSH lives on the
|
|
# admin port, so anything serving :22 is agentbbs answering join@/bbs@.
|
|
if timeout 15 ssh-keyscan -T 10 -p 22 "$DEPLOY_HOST" 2>/dev/null | grep -q .; then
|
|
echo "::notice::agentbbs is serving SSH on ${DEPLOY_HOST}:22 (join@ is reachable)"
|
|
else
|
|
echo "::error::nothing is serving SSH on ${DEPLOY_HOST}:22 — agentbbs may be down"
|
|
exit 1
|
|
fi
|