Compare commits

...

7 Commits

Author SHA1 Message Date
Renovate Bot 5317cac466 chore(deps): update actions/checkout action to v6 2026-06-08 06:09:08 +00:00
enricobuehler 72a875f1cc ci(build-deploy-game): accept optional deploy_host input
PLAYED_HOST falls back to inputs.deploy_host when set, else the secret —
lets played/infra deploy-all target a freshly-provisioned box. Game
callers forward the input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 17:37:58 +00:00
played-ci 7f66c21ed9 build-deploy-game: default PLAYED_PORT to 22 (appleboy did; inline ssh must too) 2026-06-05 02:44:17 +00:00
enricobuehler 8dc5812f1b build-deploy-game: inline the SSH deploy (kill the act-cache race)
The deploy jobs used appleboy/ssh-action, a remote `uses:` action that
races on home-runner-1's shared /root/.cache/act/<hash> exactly like the
build actions did — which is why 8 concurrent game deploys all failed
("worktree contains unstaged changes" / no repo on the box). Replace all
4 ssh-action steps with inline `ssh`/`scp` so the deploy jobs pull no
remote action at runtime; concurrent multi-game deploys are now safe.

Also harden the secrets write: validate BUILD_ENV/provisioner-password
are non-empty, and push them as FILES via scp so `docker compose up` can
never auto-create a missing bind-mount source as a root-owned directory
(the "is a directory" cert-init failure we hit on rememed/cms). The
registry token is handed over via a transient 0600 file (out of process
args and the run log) instead of inline in the script.
2026-06-05 02:21:25 +00:00
enricobuehler aea479ff72 Merge pull request 'ci: self-bootstrap repo (clone-if-absent) on fresh hosts' (#5) from deploy-autoclone into main 2026-06-05 00:28:49 +00:00
enricobuehler b10c249b46 ci: self-bootstrap repo (clone-if-absent) on fresh hosts
Deploy assumed the repo was pre-cloned at ~/<name>; clone it over HTTPS+token
if absent so a brand-new host self-assembles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:18:39 +02:00
enricobuehler d0e039351a renovate: cap kysely at <0.29.0 fleet-wide
0.29.x drops DEFAULT_MIGRATION_LOCK_TABLE exports that @better-auth/
kysely-adapter bundles. Renovate kept auto-merging the 0.28→0.29 'minor'
bump and re-breaking every game's build; allowedVersions blocks it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:22:37 +00:00
2 changed files with 137 additions and 76 deletions
+132 -76
View File
@@ -22,15 +22,22 @@ name: Build & Deploy played game (reusable)
# Notes on reliability: # Notes on reliability:
# - act keys its remote-action cache by repo URL alone (not by full # - 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 # ref), so every concurrent game build on home-runner-1 shares the
# same /root/.cache/act/<hash> dir for setup-buildx-action + # same /root/.cache/act/<hash> dir for any `uses:` remote action.
# build-push-action. Two builds racing on that dir corrupt the # Two jobs racing on that dir corrupt the checked-out tree
# checked-out tree ("worktree contains unstaged changes") and the # ("worktree contains unstaged changes") and the next read of
# next read of dist/index.js throws → step exits 1. Pinning to # dist/index.js throws → step exits 1. Pinning to patch tags didn't
# patch tags didn't help because the cache key ignores the ref. # help because the cache key ignores the ref. THE FIX, applied to
# Fix: do buildx setup + build via inline `docker buildx ...` # BOTH build and deploy jobs: no `uses:` of any third-party action
# shell, so nothing needs to be cloned from GitHub at runtime. # at all — buildx setup/build/login is inline `docker buildx ...`,
# - Registry login is also an inline shell step. One fewer remote- # and the SSH deploy is inline `ssh`/`scp` (the deploy jobs used to
# action download = one fewer failure point per job. # 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: on:
workflow_call: workflow_call:
@@ -39,12 +46,17 @@ on:
description: Game slug (must match @played/games-registry's GAMES, e.g. relayer) description: Game slug (must match @played/games-registry's GAMES, e.g. relayer)
type: string type: string
required: true 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: jobs:
build-api-core: build-api-core:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4.3.1 - uses: actions/checkout@v6.0.3
- name: Set up Docker Buildx - name: Set up Docker Buildx
# Inline replacement for docker/setup-buildx-action. The job # Inline replacement for docker/setup-buildx-action. The job
@@ -114,67 +126,96 @@ jobs:
deploy-api-core: deploy-api-core:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: build-api-core 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: 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 - 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: env:
BUILD_ENV: ${{ secrets.BUILD_ENV }} BUILD_ENV: ${{ secrets.BUILD_ENV }}
STEP_CA_PROVISIONER_PASSWORD: ${{ secrets.STEP_CA_PROVISIONER_PASSWORD }} 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 - 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}
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: env:
GAME_ID: ${{ inputs.game-id }} 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
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 - name: Wait for api-core to be healthy
uses: appleboy/ssh-action@v1.2.5 run: |
with: /tmp/ssh/run "GAME_ID='${GAME_ID}' bash -s" <<'REMOTE'
host: ${{ secrets.PLAYED_HOST }} set -euo pipefail
username: ${{ secrets.PLAYED_USER }} cd "$HOME/$GAME_ID"
port: ${{ secrets.PLAYED_PORT }} EF="--env-file $HOME/${GAME_ID}-secrets/.env"
key: ${{ secrets.PLAYED_SSH_KEY }} echo "Waiting for api-core to be ready..."
envs: GAME_ID for i in $(seq 1 30); do
script: | if docker compose -f compose.production.yml $EF ps api-core | grep -q "(healthy)"; then
cd ~/${GAME_ID} echo "api-core is healthy"
echo "Waiting for api-core to be ready..." exit 0
for i in $(seq 1 30); do fi
if docker compose -f compose.production.yml --env-file ~/${GAME_ID}-secrets/.env ps api-core | grep -q "(healthy)"; then echo "Attempt $i/30..."
echo "api-core is healthy" sleep 5
exit 0 done
fi echo "api-core did not become healthy in time"
echo "Attempt $i/30..." docker compose -f compose.production.yml $EF logs api-core --tail 80 || true
sleep 5 exit 1
done REMOTE
echo "api-core did not become healthy in time"
exit 1
env:
GAME_ID: ${{ inputs.game-id }}
build-web: build-web:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -183,7 +224,7 @@ jobs:
# with build-api-core + deploy-api-core. deploy-web below still gates on # with build-api-core + deploy-api-core. deploy-web below still gates on
# deploy-api-core so the runtime sequence is preserved. # deploy-api-core so the runtime sequence is preserved.
steps: steps:
- uses: actions/checkout@v4.3.1 - uses: actions/checkout@v6.0.3
- name: Set up Docker Buildx - name: Set up Docker Buildx
# See the build-api-core step of the same name above for why # See the build-api-core step of the same name above for why
@@ -249,19 +290,34 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
# Both gates: image must be built AND api-core must be live before web flips. # Both gates: image must be built AND api-core must be live before web flips.
needs: [build-web, deploy-api-core] needs: [build-web, deploy-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: steps:
- name: Pull and start web - name: Set up SSH
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: env:
GAME_ID: ${{ inputs.game-id }} 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
+5
View File
@@ -6,6 +6,11 @@
"labels": ["dependencies"], "labels": ["dependencies"],
"platformAutomerge": true, "platformAutomerge": true,
"packageRules": [ "packageRules": [
{
"description": "Pin kysely to 0.28.x fleet-wide. 0.29.x dropped the DEFAULT_MIGRATION_LOCK_TABLE exports that @better-auth/kysely-adapter bundles, breaking every game's api-core build. Do NOT allow >=0.29 until better-auth ships a 0.29-safe adapter.",
"matchPackageNames": ["kysely"],
"allowedVersions": "<0.29.0"
},
{ {
"description": "Bump the internal @played/* packages together. Manual merge — 0.x bumps can be breaking and merging redeploys the game.", "description": "Bump the internal @played/* packages together. Manual merge — 0.x bumps can be breaking and merging redeploys the game.",
"matchPackageNames": ["/^@played//"], "matchPackageNames": ["/^@played//"],