|
|
|
@@ -22,15 +22,22 @@ name: Build & Deploy played game (reusable)
|
|
|
|
|
# Notes on reliability:
|
|
|
|
|
# - act keys its remote-action cache by repo URL alone (not by full
|
|
|
|
|
# ref), so every concurrent game build on home-runner-1 shares the
|
|
|
|
|
# same /root/.cache/act/<hash> dir for setup-buildx-action +
|
|
|
|
|
# build-push-action. Two builds racing on that dir corrupt the
|
|
|
|
|
# checked-out tree ("worktree contains unstaged changes") and the
|
|
|
|
|
# next read of dist/index.js throws → step exits 1. Pinning to
|
|
|
|
|
# patch tags didn't help because the cache key ignores the ref.
|
|
|
|
|
# Fix: do buildx setup + build via inline `docker buildx ...`
|
|
|
|
|
# shell, so nothing needs to be cloned from GitHub at runtime.
|
|
|
|
|
# - Registry login is also an inline shell step. One fewer remote-
|
|
|
|
|
# action download = one fewer failure point per job.
|
|
|
|
|
# same /root/.cache/act/<hash> dir for any `uses:` remote action.
|
|
|
|
|
# Two jobs racing on that dir corrupt the checked-out tree
|
|
|
|
|
# ("worktree contains unstaged changes") and the next read of
|
|
|
|
|
# dist/index.js throws → step exits 1. Pinning to patch tags didn't
|
|
|
|
|
# help because the cache key ignores the ref. THE FIX, applied to
|
|
|
|
|
# BOTH build and deploy jobs: no `uses:` of any third-party action
|
|
|
|
|
# at all — buildx setup/build/login is inline `docker buildx ...`,
|
|
|
|
|
# and the SSH deploy is inline `ssh`/`scp` (the deploy jobs used to
|
|
|
|
|
# use appleboy/ssh-action, which raced exactly like the build
|
|
|
|
|
# actions and broke concurrent multi-game deploys). The only
|
|
|
|
|
# `uses:` left is actions/checkout in the build jobs.
|
|
|
|
|
# - Secrets are written to the server as FILES via scp (never piped
|
|
|
|
|
# through a step that lets `docker compose up` auto-create a missing
|
|
|
|
|
# bind-mount source as a root-owned directory — that poisons
|
|
|
|
|
# ~/<id>-secrets/step-provisioner-password.txt and breaks cert-init
|
|
|
|
|
# with "is a directory" on every subsequent deploy).
|
|
|
|
|
|
|
|
|
|
on:
|
|
|
|
|
workflow_call:
|
|
|
|
@@ -39,6 +46,11 @@ on:
|
|
|
|
|
description: Game slug (must match @played/games-registry's GAMES, e.g. relayer)
|
|
|
|
|
type: string
|
|
|
|
|
required: true
|
|
|
|
|
deploy_host:
|
|
|
|
|
description: Box IP to deploy to (passed by deploy-all). Blank = the PLAYED_HOST secret.
|
|
|
|
|
type: string
|
|
|
|
|
required: false
|
|
|
|
|
default: ""
|
|
|
|
|
|
|
|
|
|
jobs:
|
|
|
|
|
build-api-core:
|
|
|
|
@@ -114,57 +126,86 @@ jobs:
|
|
|
|
|
deploy-api-core:
|
|
|
|
|
runs-on: ubuntu-24.04
|
|
|
|
|
needs: build-api-core
|
|
|
|
|
env:
|
|
|
|
|
PLAYED_HOST: ${{ inputs.deploy_host || secrets.PLAYED_HOST }}
|
|
|
|
|
PLAYED_USER: ${{ secrets.PLAYED_USER }}
|
|
|
|
|
PLAYED_PORT: ${{ secrets.PLAYED_PORT }}
|
|
|
|
|
GAME_ID: ${{ inputs.game-id }}
|
|
|
|
|
steps:
|
|
|
|
|
- name: Set up SSH
|
|
|
|
|
# Writes the deploy key + tiny ssh/scp wrappers. No remote action
|
|
|
|
|
# is used anywhere in this job, so there is nothing to race on
|
|
|
|
|
# home-runner-1's shared act remote-action cache.
|
|
|
|
|
env:
|
|
|
|
|
PLAYED_SSH_KEY: ${{ secrets.PLAYED_SSH_KEY }}
|
|
|
|
|
run: |
|
|
|
|
|
install -m 700 -d /tmp/ssh
|
|
|
|
|
printf '%s\n' "$PLAYED_SSH_KEY" > /tmp/ssh/key
|
|
|
|
|
chmod 600 /tmp/ssh/key
|
|
|
|
|
cat > /tmp/ssh/run <<EOF
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
exec ssh -i /tmp/ssh/key -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/ssh/known_hosts -o ConnectTimeout=20 -p "${PLAYED_PORT:-22}" "${PLAYED_USER}@${PLAYED_HOST}" "\$@"
|
|
|
|
|
EOF
|
|
|
|
|
cat > /tmp/ssh/put <<EOF
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
exec scp -i /tmp/ssh/key -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/ssh/known_hosts -o ConnectTimeout=20 -P "${PLAYED_PORT:-22}" "\$1" "${PLAYED_USER}@${PLAYED_HOST}:\$2"
|
|
|
|
|
EOF
|
|
|
|
|
chmod +x /tmp/ssh/run /tmp/ssh/put
|
|
|
|
|
|
|
|
|
|
- name: Write secrets to server
|
|
|
|
|
uses: appleboy/ssh-action@v1.2.5
|
|
|
|
|
with:
|
|
|
|
|
host: ${{ secrets.PLAYED_HOST }}
|
|
|
|
|
username: ${{ secrets.PLAYED_USER }}
|
|
|
|
|
port: ${{ secrets.PLAYED_PORT }}
|
|
|
|
|
key: ${{ secrets.PLAYED_SSH_KEY }}
|
|
|
|
|
envs: BUILD_ENV,STEP_CA_PROVISIONER_PASSWORD,GAME_ID
|
|
|
|
|
script: |
|
|
|
|
|
mkdir -p ~/${GAME_ID}-secrets
|
|
|
|
|
printf '%s' "$BUILD_ENV" > ~/${GAME_ID}-secrets/.env
|
|
|
|
|
printf '%s' "$STEP_CA_PROVISIONER_PASSWORD" > ~/${GAME_ID}-secrets/step-provisioner-password.txt
|
|
|
|
|
chmod 644 ~/${GAME_ID}-secrets/.env
|
|
|
|
|
chmod 600 ~/${GAME_ID}-secrets/step-provisioner-password.txt
|
|
|
|
|
env:
|
|
|
|
|
BUILD_ENV: ${{ secrets.BUILD_ENV }}
|
|
|
|
|
STEP_CA_PROVISIONER_PASSWORD: ${{ secrets.STEP_CA_PROVISIONER_PASSWORD }}
|
|
|
|
|
GAME_ID: ${{ inputs.game-id }}
|
|
|
|
|
run: |
|
|
|
|
|
test -n "$BUILD_ENV" || { echo "::error::BUILD_ENV secret is empty — refusing to deploy"; exit 1; }
|
|
|
|
|
test -n "$STEP_CA_PROVISIONER_PASSWORD" || { echo "::error::STEP_CA_PROVISIONER_PASSWORD secret is empty"; exit 1; }
|
|
|
|
|
printf '%s' "$BUILD_ENV" > /tmp/env.prod
|
|
|
|
|
printf '%s' "$STEP_CA_PROVISIONER_PASSWORD" > /tmp/prov.txt
|
|
|
|
|
# Pre-create the secrets dir, then push the two secrets as FILES.
|
|
|
|
|
# This guarantees the bind-mount sources exist as files before any
|
|
|
|
|
# `compose up`, so Docker never auto-creates them as root-owned
|
|
|
|
|
# directories (which breaks cert-init + blocks future deploys).
|
|
|
|
|
/tmp/ssh/run "mkdir -p ~/${GAME_ID}-secrets"
|
|
|
|
|
/tmp/ssh/put /tmp/env.prod "${GAME_ID}-secrets/.env"
|
|
|
|
|
/tmp/ssh/put /tmp/prov.txt "${GAME_ID}-secrets/step-provisioner-password.txt"
|
|
|
|
|
/tmp/ssh/run "chmod 644 ~/${GAME_ID}-secrets/.env && chmod 600 ~/${GAME_ID}-secrets/step-provisioner-password.txt"
|
|
|
|
|
rm -f /tmp/env.prod /tmp/prov.txt
|
|
|
|
|
|
|
|
|
|
- name: Pull and start api-core
|
|
|
|
|
uses: appleboy/ssh-action@v1.2.5
|
|
|
|
|
with:
|
|
|
|
|
host: ${{ secrets.PLAYED_HOST }}
|
|
|
|
|
username: ${{ secrets.PLAYED_USER }}
|
|
|
|
|
port: ${{ secrets.PLAYED_PORT }}
|
|
|
|
|
key: ${{ secrets.PLAYED_SSH_KEY }}
|
|
|
|
|
envs: GAME_ID
|
|
|
|
|
script: |
|
|
|
|
|
docker login git.unom.io -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
|
cd ~/${GAME_ID}
|
|
|
|
|
env:
|
|
|
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
|
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
|
run: |
|
|
|
|
|
# Hand the registry token to the box via a transient 0600 file
|
|
|
|
|
# (kept out of process args / the run log); the remote script
|
|
|
|
|
# reads it then deletes it.
|
|
|
|
|
printf '%s' "$REGISTRY_TOKEN" > /tmp/reg_token
|
|
|
|
|
/tmp/ssh/put /tmp/reg_token ".played_reg_token"
|
|
|
|
|
rm -f /tmp/reg_token
|
|
|
|
|
/tmp/ssh/run "chmod 600 ~/.played_reg_token"
|
|
|
|
|
/tmp/ssh/run "GAME_ID='${GAME_ID}' REGISTRY_USER='${REGISTRY_USER}' bash -s" <<'REMOTE'
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
TOKEN="$(cat ~/.played_reg_token)"; rm -f ~/.played_reg_token
|
|
|
|
|
printf '%s' "$TOKEN" | docker login git.unom.io -u "$REGISTRY_USER" --password-stdin
|
|
|
|
|
# Clone-if-absent so a freshly provisioned box self-installs the repo.
|
|
|
|
|
[ -d "$HOME/$GAME_ID/.git" ] || git clone "https://${REGISTRY_USER}:${TOKEN}@git.unom.io/played/${GAME_ID}.git" "$HOME/$GAME_ID"
|
|
|
|
|
cd "$HOME/$GAME_ID"
|
|
|
|
|
git fetch origin main
|
|
|
|
|
git reset --hard origin/main
|
|
|
|
|
docker compose -f compose.production.yml --env-file ~/${GAME_ID}-secrets/.env pull api-core
|
|
|
|
|
docker compose -f compose.production.yml --env-file ~/${GAME_ID}-secrets/.env up -d --no-build api-core
|
|
|
|
|
env:
|
|
|
|
|
GAME_ID: ${{ inputs.game-id }}
|
|
|
|
|
EF="--env-file $HOME/${GAME_ID}-secrets/.env"
|
|
|
|
|
docker compose -f compose.production.yml $EF pull api-core
|
|
|
|
|
docker compose -f compose.production.yml $EF up -d --no-build api-core
|
|
|
|
|
REMOTE
|
|
|
|
|
|
|
|
|
|
- name: Wait for api-core to be healthy
|
|
|
|
|
uses: appleboy/ssh-action@v1.2.5
|
|
|
|
|
with:
|
|
|
|
|
host: ${{ secrets.PLAYED_HOST }}
|
|
|
|
|
username: ${{ secrets.PLAYED_USER }}
|
|
|
|
|
port: ${{ secrets.PLAYED_PORT }}
|
|
|
|
|
key: ${{ secrets.PLAYED_SSH_KEY }}
|
|
|
|
|
envs: GAME_ID
|
|
|
|
|
script: |
|
|
|
|
|
cd ~/${GAME_ID}
|
|
|
|
|
run: |
|
|
|
|
|
/tmp/ssh/run "GAME_ID='${GAME_ID}' bash -s" <<'REMOTE'
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
cd "$HOME/$GAME_ID"
|
|
|
|
|
EF="--env-file $HOME/${GAME_ID}-secrets/.env"
|
|
|
|
|
echo "Waiting for api-core to be ready..."
|
|
|
|
|
for i in $(seq 1 30); do
|
|
|
|
|
if docker compose -f compose.production.yml --env-file ~/${GAME_ID}-secrets/.env ps api-core | grep -q "(healthy)"; then
|
|
|
|
|
if docker compose -f compose.production.yml $EF ps api-core | grep -q "(healthy)"; then
|
|
|
|
|
echo "api-core is healthy"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
@@ -172,9 +213,9 @@ jobs:
|
|
|
|
|
sleep 5
|
|
|
|
|
done
|
|
|
|
|
echo "api-core did not become healthy in time"
|
|
|
|
|
docker compose -f compose.production.yml $EF logs api-core --tail 80 || true
|
|
|
|
|
exit 1
|
|
|
|
|
env:
|
|
|
|
|
GAME_ID: ${{ inputs.game-id }}
|
|
|
|
|
REMOTE
|
|
|
|
|
|
|
|
|
|
build-web:
|
|
|
|
|
runs-on: ubuntu-24.04
|
|
|
|
@@ -249,19 +290,34 @@ jobs:
|
|
|
|
|
runs-on: ubuntu-24.04
|
|
|
|
|
# Both gates: image must be built AND api-core must be live before web flips.
|
|
|
|
|
needs: [build-web, deploy-api-core]
|
|
|
|
|
steps:
|
|
|
|
|
- name: Pull and start web
|
|
|
|
|
uses: appleboy/ssh-action@v1.2.5
|
|
|
|
|
with:
|
|
|
|
|
host: ${{ secrets.PLAYED_HOST }}
|
|
|
|
|
username: ${{ secrets.PLAYED_USER }}
|
|
|
|
|
port: ${{ secrets.PLAYED_PORT }}
|
|
|
|
|
key: ${{ secrets.PLAYED_SSH_KEY }}
|
|
|
|
|
envs: GAME_ID
|
|
|
|
|
script: |
|
|
|
|
|
docker login git.unom.io -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
|
cd ~/${GAME_ID}
|
|
|
|
|
docker compose -f compose.production.yml --env-file ~/${GAME_ID}-secrets/.env pull web
|
|
|
|
|
docker compose -f compose.production.yml --env-file ~/${GAME_ID}-secrets/.env up -d --no-build web
|
|
|
|
|
env:
|
|
|
|
|
PLAYED_HOST: ${{ inputs.deploy_host || secrets.PLAYED_HOST }}
|
|
|
|
|
PLAYED_USER: ${{ secrets.PLAYED_USER }}
|
|
|
|
|
PLAYED_PORT: ${{ secrets.PLAYED_PORT }}
|
|
|
|
|
GAME_ID: ${{ inputs.game-id }}
|
|
|
|
|
steps:
|
|
|
|
|
- name: Set up SSH
|
|
|
|
|
env:
|
|
|
|
|
PLAYED_SSH_KEY: ${{ secrets.PLAYED_SSH_KEY }}
|
|
|
|
|
run: |
|
|
|
|
|
install -m 700 -d /tmp/ssh
|
|
|
|
|
printf '%s\n' "$PLAYED_SSH_KEY" > /tmp/ssh/key
|
|
|
|
|
chmod 600 /tmp/ssh/key
|
|
|
|
|
cat > /tmp/ssh/run <<EOF
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
exec ssh -i /tmp/ssh/key -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/ssh/known_hosts -o ConnectTimeout=20 -p "${PLAYED_PORT:-22}" "${PLAYED_USER}@${PLAYED_HOST}" "\$@"
|
|
|
|
|
EOF
|
|
|
|
|
chmod +x /tmp/ssh/run
|
|
|
|
|
|
|
|
|
|
- name: Pull and start web
|
|
|
|
|
# No registry login needed here: the box's docker auth (baked at
|
|
|
|
|
# provision + refreshed by deploy-api-core) already covers `pull`,
|
|
|
|
|
# and the repo was already cloned by deploy-api-core.
|
|
|
|
|
run: |
|
|
|
|
|
/tmp/ssh/run "GAME_ID='${GAME_ID}' bash -s" <<'REMOTE'
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
cd "$HOME/$GAME_ID"
|
|
|
|
|
EF="--env-file $HOME/${GAME_ID}-secrets/.env"
|
|
|
|
|
docker compose -f compose.production.yml $EF pull web
|
|
|
|
|
docker compose -f compose.production.yml $EF up -d --no-build web
|
|
|
|
|
REMOTE
|
|
|
|
|