test(server): address Claude review of the GameServer harness #9981
Workflow file for this run
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 | |
| on: | |
| # Allow contributors to schedule manual deployments. | |
| # Permission to deploy can be restricted by requiring approval in environment configuration. | |
| workflow_dispatch: | |
| inputs: | |
| target_domain: | |
| description: "Deployment Domain" | |
| required: true | |
| default: "openfront.dev" | |
| type: choice | |
| options: | |
| - openfront.io | |
| - openfront.dev | |
| target_host: | |
| description: "Deployment Host" | |
| required: true | |
| default: "staging" | |
| type: choice | |
| options: | |
| - masters | |
| - staging | |
| - falk2 | |
| target_subdomain: | |
| description: "Deployment Subdomain" | |
| required: false | |
| default: "" | |
| type: string | |
| # Automatic deployment on push | |
| # See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore | |
| push: | |
| branches: | |
| # "**" rather than "*" so branches with a slash (fix/foo, feat/bar) match too | |
| - "**" | |
| # Nightly build: rebuild and deploy main to nightly.openfront.dev | |
| # 07:00 UTC = 11:00 PM PST (midnight PDT during summer) | |
| schedule: | |
| - cron: "0 7 * * *" | |
| permissions: {} | |
| # GitHub keeps only ONE pending run per concurrency group - each new run | |
| # cancels the previously pending one, even across branches. With a single | |
| # shared group, a feature-branch push could evict a queued main deploy and | |
| # main.openfront.dev silently stayed stale until the next merge. So scope the | |
| # group to the deploy target instead: per branch for pushes, a dedicated group | |
| # for the nightly, per host for manual dispatches. Runs for different targets | |
| # may then overlap on the staging host; deploy.sh serializes the host-side | |
| # update with flock, so only the CI-side image builds actually run in parallel. | |
| concurrency: | |
| group: ${{ github.event_name == 'workflow_dispatch' && inputs.target_host || github.event_name == 'schedule' && 'staging-nightly' || format('staging-{0}', github.ref_name) }} | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| # Deploy on push/schedule/workflow_dispatch (see "on:") unless this is a fork | |
| if: ${{ github.repository == 'openfrontio/OpenFrontIO' }} | |
| # Use different logic based on event type | |
| name: Deploy to ${{ inputs.target_domain || 'openfront.dev' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| environment: ${{ inputs.target_domain == 'openfront.io' && 'prod' || '' }} | |
| env: | |
| DOMAIN: ${{ inputs.target_domain || 'openfront.dev' }} | |
| # Sanitized into SUBDOMAIN by the first step below - do not use directly. | |
| RAW_SUBDOMAIN: ${{ github.event_name == 'schedule' && 'nightly' || github.event_name == 'push' && github.ref_name || inputs.target_subdomain || 'main' }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: π Update job summary | |
| run: | | |
| # A branch name can hold characters that are illegal in a DNS label, a | |
| # Docker container name, and a file path (e.g. "fix/some-bug"), so | |
| # normalize it to an RFC 1123 label first: underscores and slashes | |
| # become hyphens to keep the name readable (fix/some-bug -> | |
| # fix-some-bug), anything else outside [a-zA-Z0-9-] is dropped, and the | |
| # result is capped at the 63-octet label limit with no edge hyphens. | |
| READABLE="$(printf '%s' "$RAW_SUBDOMAIN" | tr '_/' '--')" | |
| LABEL="$(printf '%s' "$READABLE" | tr -cd 'a-zA-Z0-9-')" | |
| LABEL="$(printf '%s' "${LABEL:0:63}" | sed 's/^-*//; s/-*$//')" | |
| if [ -z "$LABEL" ]; then | |
| echo "Error: subdomain is empty after sanitizing '$RAW_SUBDOMAIN'" | |
| exit 1 | |
| fi | |
| # The hyphen mapping above is deliberately canonical: fix/foo and | |
| # fix-foo share a subdomain. But dropping or trimming characters is | |
| # lossy in ways that aren't obvious from the branch name (feat/hΓ©llo | |
| # and feat/hllo would collide), so whenever sanitizing went beyond | |
| # the hyphen mapping, append a digest of the original to keep such | |
| # branches on distinct hosts. Names that pass through untouched | |
| # (main, nightly, a hand-typed prod subdomain) keep their exact | |
| # spelling, which update.sh relies on for its `SUBDOMAIN = main` | |
| # restart check. | |
| if [ "$LABEL" != "$READABLE" ]; then | |
| HASH="$(printf '%s' "$RAW_SUBDOMAIN" | sha256sum | cut -c1-6)" | |
| LABEL="$(printf '%s' "${LABEL:0:56}" | sed 's/-*$//')-${HASH}" | |
| fi | |
| SUBDOMAIN="$LABEL" | |
| FQDN="$SUBDOMAIN.$DOMAIN" | |
| echo "SUBDOMAIN=$SUBDOMAIN" >> $GITHUB_ENV | |
| echo "FQDN=$FQDN" >> $GITHUB_ENV | |
| cat <<EOF >> $GITHUB_STEP_SUMMARY | |
| ### In progress :ship: | |
| Deploying from $GITHUB_REF to $FQDN | |
| EOF | |
| - uses: actions/create-github-app-token@v3 | |
| id: generate-token | |
| if: ${{ github.repository == 'openfrontio/OpenFrontIO' }} | |
| with: | |
| client-id: ${{ vars.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Export the token | |
| if: ${{ github.repository == 'openfrontio/OpenFrontIO' }} | |
| env: | |
| GH_TOKEN: ${{ steps.generate-token.outputs.token }} | |
| run: | | |
| echo "GH_TOKEN=$GH_TOKEN" >> $GITHUB_ENV | |
| gh api octocat | |
| - name: π Create deployment | |
| if: ${{ github.repository == 'openfrontio/OpenFrontIO' && steps.generate-token.outputs.token != '' }} | |
| uses: actions/github-script@v9 | |
| id: deployment | |
| env: | |
| ENVIRONMENT: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }} | |
| FQDN: ${{ env.FQDN }} | |
| with: | |
| github-token: ${{ steps.generate-token.outputs.token }} | |
| script: | | |
| const response = await github.rest.repos.createDeployment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: process.env.GITHUB_SHA, | |
| environment: process.env.ENVIRONMENT, | |
| description: 'Deployment to ' + process.env.FQDN, | |
| auto_merge: false, | |
| required_contexts: [], | |
| transient_environment: process.env.ENVIRONMENT === 'staging' && context.ref !== 'refs/heads/main', | |
| production_environment: process.env.ENVIRONMENT === 'prod' | |
| }); | |
| const deployment = response.data; | |
| if (!deployment || !deployment.id) { | |
| core.setFailed('Failed to create deployment'); | |
| return; | |
| } | |
| core.setOutput('deployment_id', deployment.id); | |
| - name: π Log in to GHCR | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ vars.GHCR_USERNAME }} | |
| password: ${{ secrets.GHCR_TOKEN }} | |
| - name: π Create SSH private key | |
| env: | |
| SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }} | |
| SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} | |
| SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }} | |
| SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} | |
| run: | | |
| set -euxo pipefail | |
| mkdir -p ~/.ssh | |
| echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa | |
| test -n "$SERVER_HOST_MASTERS" && ssh-keyscan -H "$SERVER_HOST_MASTERS" >> ~/.ssh/known_hosts | |
| test -n "$SERVER_HOST_FALK2" && ssh-keyscan -H "$SERVER_HOST_FALK2" >> ~/.ssh/known_hosts | |
| test -n "$SERVER_HOST_STAGING" && ssh-keyscan -H "$SERVER_HOST_STAGING" >> ~/.ssh/known_hosts | |
| chmod 600 ~/.ssh/id_rsa | |
| - name: π’ Deploy | |
| env: | |
| GHCR_REPO: ${{ vars.GHCR_REPO }} | |
| GHCR_USERNAME: ${{ vars.GHCR_USERNAME }} | |
| ENV: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }} | |
| HOST: ${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }} # schedule and push both use staging | |
| CDN_BASE: ${{ vars.CDN_BASE }} | |
| OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }} | |
| OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }} | |
| API_KEY: ${{ secrets.API_KEY }} | |
| ADMIN_BOT_API_KEY: ${{ secrets.ADMIN_BOT_API_KEY }} | |
| NUM_WORKERS: ${{ vars.NUM_WORKERS }} | |
| TURNSTILE_SITE_KEY: ${{ vars.TURNSTILE_SITE_KEY }} | |
| SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }} | |
| SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} | |
| SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }} | |
| SSH_KEY: ~/.ssh/id_rsa | |
| VERSION_TAG: latest | |
| run: | | |
| echo "::group::deploy.sh" | |
| ./build-deploy.sh "$ENV" "$HOST" "$SUBDOMAIN" | |
| echo "Deployment created in ${SECONDS} seconds" >> $GITHUB_STEP_SUMMARY | |
| echo "::endgroup::" | |
| - name: β³ Wait for deployment to start | |
| env: | |
| API_KEY: ${{ secrets.API_KEY }} | |
| run: | | |
| echo "::group::Wait for deployment to start" | |
| set -euxo pipefail | |
| while [ "$(curl -s -H "X-API-Key: ${API_KEY}" https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do | |
| if [ "$SECONDS" -ge 300 ]; then | |
| echo "Timeout: deployment did not start within 5 minutes" | |
| exit 1 | |
| fi | |
| sleep 10 | |
| done | |
| echo "Deployment started in ${SECONDS} seconds" >> $GITHUB_STEP_SUMMARY | |
| echo "::endgroup::" | |
| - name: π Update deployment status | |
| if: ${{ always() && github.repository == 'openfrontio/OpenFrontIO' && steps.generate-token.outputs.token != '' && steps.deployment.outcome == 'success' && steps.deployment.outputs.deployment_id != '' }} | |
| uses: actions/github-script@v9 | |
| env: | |
| FQDN: ${{ env.FQDN }} | |
| DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }} | |
| STATUS: ${{ job.status }} | |
| with: | |
| github-token: ${{ steps.generate-token.outputs.token }} | |
| script: | | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| deployment_id: process.env.DEPLOYMENT_ID, | |
| state: process.env.STATUS === 'success' ? 'success' : 'failure', | |
| environment_url: 'https://' + process.env.FQDN | |
| }); | |
| - name: β Update job summary | |
| if: success() | |
| run: | | |
| cat <<EOF >> $GITHUB_STEP_SUMMARY | |
| ### Success! :rocket: | |
| Deployed from $GITHUB_REF to $FQDN | |
| EOF | |
| - name: β Update job summary | |
| if: failure() | |
| run: | | |
| cat <<EOF >> $GITHUB_STEP_SUMMARY | |
| ### Failure! :fire: | |
| Unable to deploy from $GITHUB_REF to $FQDN | |
| EOF |