Skip to content

Deploy (VPS)

Deploy (VPS) #24

Workflow file for this run

# Deploys the hosted-worlds fleet services to the VPS (bbs-host-1, see docs/developer/HOSTED_WORLDS.md
# and deploy/README.md). Manual-only by design: run it from the Actions tab, pick the service and
# optionally pin image tags. The `production` environment adds a required-reviewer approval gate.
#
# Secrets model (deliberate):
# * GitHub holds ONE secret — DEPLOY_SSH_KEY, a dedicated ed25519 key for the `bbs` user that exists
# only for this workflow (not the operator's personal key). The host key below is pinned.
# * All service secrets (claim code, report keys, admin credentials) live ONLY in /opt/bbs/*/.env on
# the host. This workflow never reads, writes or logs them — remote-deploy.sh rewrites only the
# *_TAG line of those files.
name: Deploy (VPS)
on:
workflow_dispatch:
inputs:
service:
description: "Which service to deploy."
type: choice
options: [all, caddy, worldhost, reports, ai]
default: all
worldhost_tag:
description: "WorldHost image tag to pin (e.g. sha-abc1234). Empty = keep the currently pinned tag."
type: string
default: ""
reports_tag:
description: "ReportHost image tag to pin (e.g. sha-abc1234). Empty = keep the currently pinned tag."
type: string
default: ""
ai_tag:
description: "AI-backend image tag to pin (e.g. sha-abc1234). Empty = keep the currently pinned tag."
type: string
default: ""
permissions:
contents: read
# Never two deploys at once; a second run queues instead of cancelling a half-finished deploy.
concurrency:
group: deploy-production
jobs:
deploy:
name: Deploy to bbs-host-1
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v5
- name: Validate inputs
env:
WORLDHOST_TAG: ${{ inputs.worldhost_tag }}
REPORTS_TAG: ${{ inputs.reports_tag }}
AI_TAG: ${{ inputs.ai_tag }}
run: |
# Empty = keep the currently pinned tag; grep sees no line on empty input, so skip explicitly.
for t in "$WORLDHOST_TAG" "$REPORTS_TAG" "$AI_TAG"; do
[ -z "$t" ] && continue
if ! printf '%s' "$t" | grep -Eq '^[A-Za-z0-9._-]+$'; then
echo "invalid image tag: $t"; exit 1
fi
done
- name: Set up SSH (pinned host key)
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
echo '31.70.113.90 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuF64bEL7WPCFjuC5jT91w0TOupsWe2VjMPUxFanFow' >> ~/.ssh/known_hosts
- name: Sync deploy files (never touches the host .env files)
run: |
rsync -rtv --exclude='.env' -e "ssh -i ~/.ssh/deploy_key" \
deploy/caddy deploy/worldhost deploy/reports deploy/ai \
[email protected]:/opt/bbs/
- name: Deploy + health check
env:
SERVICE: ${{ inputs.service }}
WORLDHOST_TAG: ${{ inputs.worldhost_tag }}
REPORTS_TAG: ${{ inputs.reports_tag }}
AI_TAG: ${{ inputs.ai_tag }}
run: |
ssh -i ~/.ssh/deploy_key [email protected] \
"bash -s -- '$SERVICE' '$WORLDHOST_TAG' '$REPORTS_TAG' '$AI_TAG'" < deploy/remote-deploy.sh