files: seed a default README.txt into every member's public area #74
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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@v4 | |
| - 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@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' }} | |
| 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" | |
| # 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 |