Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,8 @@ indent_size = 2
end_of_line = lf
[*.{cmd,bat}]
end_of_line = crlf

# Caddy's formatter uses tabs for site blocks.
[Caddyfile*]
indent_style = tab
end_of_line = lf
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*.css text eol=lf
*.js text eol=lf
*.ts text eol=lf
Caddyfile* text eol=lf

# Scripts
*.sh text eol=lf
Expand Down
7 changes: 7 additions & 0 deletions .github/actions/validate/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ runs:
set -o pipefail
bash tests/deploy-edge/run.sh 2>&1 | tee artifacts/validation/deploy-edge.log

- name: Test shared and separate host configuration without a Docker daemon
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
set -o pipefail
bash tests/deploy-topology/run.sh 2>&1 | tee artifacts/validation/deploy-topology.log

- name: Test application rollback without Docker or SSH
shell: bash
working-directory: ${{ inputs.working-directory }}
Expand Down
30 changes: 20 additions & 10 deletions .github/workflows/_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,23 @@ jobs:
packages: write

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.resolve-source.outputs.protected-revision }}
persist-credentials: false

- name: Check deployment configuration
id: target
env:
DEPLOY_ENVIRONMENT: ${{ inputs.environment-slug }}
EDGE_PROFILE: ${{ vars.EDGE_PROFILE }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_KNOWN_HOSTS: ${{ vars.DEPLOY_KNOWN_HOSTS }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
set -euo pipefail
bash scripts/resolve-edge-profile.sh "$DEPLOY_ENVIRONMENT" "$EDGE_PROFILE" >> "$GITHUB_OUTPUT"
for name in DEPLOY_HOST DEPLOY_KNOWN_HOSTS DEPLOY_USER DEPLOY_SSH_KEY; do
if [ -z "${!name}" ]; then
echo "::error::${name} is unavailable to the deployment job."
Expand All @@ -177,11 +186,6 @@ jobs:
[[ "$DEPLOY_HOST" =~ ^[a-zA-Z0-9.-]+$ ]] || { echo '::error::DEPLOY_HOST is invalid.'; exit 1; }
[[ "$DEPLOY_USER" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || { echo '::error::DEPLOY_USER is invalid.'; exit 1; }

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.resolve-source.outputs.protected-revision }}
persist-credentials: false

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-${{ inputs.environment-slug }}
Expand Down Expand Up @@ -267,8 +271,14 @@ jobs:
chmod 644 ~/.ssh/known_hosts
ssh-keygen -l -f ~/.ssh/known_hosts

- name: Check production before staging update
if: ${{ inputs.environment-slug == 'staging' }}
- name: Verify the host edge profile before transferring a release
env:
EDGE_PROFILE: ${{ steps.target.outputs.profile }}
run: |
ssh deployment bash -s -- /srv/opengamebuilder/edge "$EDGE_PROFILE" < scripts/verify-edge-profile.sh

- name: Check colocated production before staging update
if: ${{ steps.target.outputs.check-production == 'true' }}
env:
PRODUCTION_URL: ${{ vars.PRODUCTION_SMOKE_TEST_URL }}
run: |
Expand Down Expand Up @@ -301,7 +311,7 @@ jobs:
release_id="$2"
mkdir -p "$app_dir/incoming/$release_id"
docker network inspect ogb-edge >/dev/null 2>&1 || {
echo "Shared edge network is missing; apply the edge configuration first." >&2
echo "Host edge network is missing; apply this host's edge configuration first." >&2
exit 1
}
EOF
Expand Down Expand Up @@ -356,8 +366,8 @@ jobs:
echo 'Intentional staging failure: the recovery step must restore the previous release.' >&2
exit 1

- name: Check production after staging update
if: ${{ inputs.environment-slug == 'staging' }}
- name: Check colocated production after staging update
if: ${{ steps.target.outputs.check-production == 'true' }}
env:
PRODUCTION_URL: ${{ vars.PRODUCTION_SMOKE_TEST_URL }}
run: |
Expand Down
105 changes: 78 additions & 27 deletions .github/workflows/cd-edge.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
name: 🌐 CD Shared Edge
name: 🌐 CD Edge

# Run from main when the shared Caddyfile or edge Compose definition changes.
# This is the only workflow that writes shared edge files or updates Caddy.
# One edge per host. Its profile explicitly selects the environments it serves.
on:
workflow_dispatch:
inputs:
environment:
description: "GitHub environment providing this host's credentials (shared edge requires production)"
required: true
type: choice
options:
- production
- staging
default: production
allow-profile-change:
description: "Production-approved migration: allow changing an already marked host's edge profile"
required: false
type: boolean
default: false

# Serialize edge maintenance, including two environments pointing at one host.
concurrency:
group: cd-shared-edge
cancel-in-progress: false
Expand All @@ -23,25 +37,55 @@ jobs:
DISPATCH_REF: ${{ github.ref }}
run: |
if [ "$DISPATCH_REF" != 'refs/heads/main' ]; then
echo "Shared edge updates must be dispatched from main, not $DISPATCH_REF." >&2
echo "Edge updates must be dispatched from main, not $DISPATCH_REF." >&2
exit 1
fi

apply:
name: Validate and apply shared edge
authorize-profile-change:
name: Authorize host profile migration
needs: require-main-dispatch
if: ${{ inputs.allow-profile-change }}
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 5
environment: production
steps:
- name: Check deployment configuration
- name: Record production authorization
run: echo 'Production environment authorization obtained for this host profile migration.'

apply:
name: Validate and apply host edge
needs:
- require-main-dispatch
- authorize-profile-change
# Normal isolated staging does not enter or depend on the production environment.
# A migration must have completed its separate production approval gate.
if: ${{ !cancelled() && needs.require-main-dispatch.result == 'success' && (needs.authorize-profile-change.result == 'success' || (!inputs.allow-profile-change && needs.authorize-profile-change.result == 'skipped')) }}
runs-on: ubuntu-latest
timeout-minutes: 15
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Check deployment configuration and edge ownership
id: target
env:
DEPLOY_ENVIRONMENT: ${{ inputs.environment }}
EDGE_PROFILE: ${{ vars.EDGE_PROFILE }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_KNOWN_HOSTS: ${{ vars.DEPLOY_KNOWN_HOSTS }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
STAGING_URL: ${{ vars.STAGING_SMOKE_TEST_URL }}
PRODUCTION_URL: ${{ vars.PRODUCTION_SMOKE_TEST_URL }}
run: |
set -euo pipefail
bash scripts/resolve-edge-profile.sh "$DEPLOY_ENVIRONMENT" "$EDGE_PROFILE" >> "$GITHUB_OUTPUT"
if [[ "$DEPLOY_ENVIRONMENT" != production && "$EDGE_PROFILE" == shared ]]; then
echo '::error::Shared edge updates require the production environment.'
exit 1
fi
for name in DEPLOY_HOST DEPLOY_KNOWN_HOSTS DEPLOY_USER DEPLOY_SSH_KEY; do
if [ -z "${!name}" ]; then
echo "::error::${name} is unavailable to the edge job."
Expand All @@ -50,10 +94,17 @@ jobs:
done
[[ "$DEPLOY_HOST" =~ ^[a-zA-Z0-9.-]+$ ]] || { echo '::error::DEPLOY_HOST is invalid.'; exit 1; }
[[ "$DEPLOY_USER" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || { echo '::error::DEPLOY_USER is invalid.'; exit 1; }
if [[ "$EDGE_PROFILE" == shared || "$EDGE_PROFILE" == staging ]]; then
[[ "$STAGING_URL" =~ ^https://[a-zA-Z0-9.-]+/?$ ]] || { echo '::error::STAGING_SMOKE_TEST_URL must be an HTTPS origin.'; exit 1; }
fi
if [[ "$EDGE_PROFILE" == shared || "$EDGE_PROFILE" == production ]]; then
[[ "$PRODUCTION_URL" =~ ^https://[a-zA-Z0-9.-]+/?$ ]] || { echo '::error::PRODUCTION_SMOKE_TEST_URL must be an HTTPS origin.'; exit 1; }
fi

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Render only this host's environments
env:
EDGE_PROFILE: ${{ steps.target.outputs.profile }}
run: bash scripts/render-edge.sh "$EDGE_PROFILE" artifacts/edge-candidate

- name: Set up SSH
env:
Expand Down Expand Up @@ -86,30 +137,30 @@ jobs:
- name: Transfer candidate and apply it
env:
CANDIDATE_DIR: /srv/opengamebuilder/edge-candidates/${{ github.run_id }}-${{ github.run_attempt }}
DEPLOY_ENVIRONMENT: ${{ inputs.environment }}
ALLOW_PROFILE_CHANGE: ${{ inputs.allow-profile-change }}
run: |
set -euo pipefail
ssh deployment \
mkdir -p "$CANDIDATE_DIR"
rsync -az \
./deploy/edge/compose.yml ./deploy/edge/Caddyfile \
ssh deployment mkdir -p "$CANDIDATE_DIR"
rsync -az artifacts/edge-candidate/compose.yml artifacts/edge-candidate/Caddyfile \
deployment:"$CANDIDATE_DIR/"
ssh deployment \
bash -s -- "$CANDIDATE_DIR" < ./scripts/apply-edge.sh
bash -s -- "$CANDIDATE_DIR" "$DEPLOY_ENVIRONMENT" "$ALLOW_PROFILE_CHANGE" < scripts/apply-edge.sh

- name: Check both sites after edge update
- name: Check selected edge routes on the deployed host
env:
EDGE_ENVIRONMENTS: ${{ steps.target.outputs.environments }}
STAGING_URL: ${{ vars.STAGING_SMOKE_TEST_URL }}
PRODUCTION_URL: ${{ vars.PRODUCTION_SMOKE_TEST_URL }}
run: |
set -euo pipefail
for name in STAGING_URL PRODUCTION_URL; do
url="${!name}"
if [ -z "$url" ]; then
echo "::error::${name} is not configured."
exit 1
fi
curl --fail --show-error --silent --location \
--connect-timeout 10 --max-time 30 --retry-max-time 180 \
--retry 5 --retry-delay 5 --retry-all-errors \
"${url%/}/api/alive" >/dev/null
for environment in $EDGE_ENVIRONMENTS; do
case "$environment" in
staging) url="$STAGING_URL" ;;
production) url="$PRODUCTION_URL" ;;
*) echo 'Unexpected edge environment.' >&2; exit 1 ;;
esac
# Probe the host reached through pinned SSH, not a possibly old DNS target.
# /health is edge readiness; initial setup does not need an API deployed yet.
ssh deployment bash -s -- "$environment" "$url" < scripts/check-edge-health.sh
done
43 changes: 0 additions & 43 deletions deploy/edge/Caddyfile

This file was deleted.

23 changes: 23 additions & 0 deletions deploy/edge/Caddyfile.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
www.opengamebuilder.com {
redir https://opengamebuilder.com{uri} permanent
}

opengamebuilder.com {
encode zstd gzip

handle /health {
respond "ok production" 200
}

handle /api/* {
reverse_proxy api-production:8080
}

handle {
root * /srv/opengamebuilder/production/web
try_files {path} /index.html
file_server {
precompressed br gzip
}
}
}
19 changes: 19 additions & 0 deletions deploy/edge/Caddyfile.staging
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
staging.opengamebuilder.com {
encode zstd gzip

handle /health {
respond "ok staging" 200
}

handle /api/* {
reverse_proxy api-staging:8080
}

handle {
root * /srv/opengamebuilder/staging/web
try_files {path} /index.html
file_server {
precompressed br gzip
}
}
}
4 changes: 4 additions & 0 deletions deploy/edge/compose.production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
caddy:
volumes:
- ../production/web:/srv/opengamebuilder/production/web:ro
4 changes: 4 additions & 0 deletions deploy/edge/compose.staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
caddy:
volumes:
- ../staging/web:/srv/opengamebuilder/staging/web:ro
2 changes: 0 additions & 2 deletions deploy/edge/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ services:
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ../production/web:/srv/opengamebuilder/production/web:ro
- ../staging/web:/srv/opengamebuilder/staging/web:ro
- caddy_data:/data
- caddy_config:/config
networks:
Expand Down
10 changes: 10 additions & 0 deletions docs/foundation-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,16 @@ Live staging deployment and production availability read-back remain to be
verified after the protected workflow change is merged; no edge or application
deployment was run for this local implementation.

**Host independence follow-up:** deployment environments explicitly select a
`shared`, `staging`, or `production` edge profile. Each host owns its own network,
proxy, and certificate volumes; an isolated profile contains no routes or web
mounts for the other environment. Only shared staging deployments probe
production availability. Profile changes require explicit production approval,
and application preflight checks the installed profile before transferring a
release. See [hosting setup](setup/hosting.md#host-edge-changes) for adoption and
future separation. Local regression/configuration validation does not establish
live separate-host acceptance; no host migration is performed by this change.

### 13. Make builds portable and promote identifiable artifacts

- [x] Prefer deployed frontend requests to the current origin's `/api` rather than
Expand Down
Loading
Loading