All checks were successful
PR checks / checks (pull_request) Successful in 9m46s
PLAN.md §13 phase 12, the last one. Four decisions of record, D54–D57, taking the count to fifty-seven; recorded in §6, "How phase 12 delivered it". A two-stage Dockerfile, a pull-only docker-compose.yml carrying both bind mounts, .env.example, the workflow that publishes and deploys, CONTRIBUTING.md, the community-health files this was the only repository of the ten to lack, and DEPLOY.md. D54 — a merge deploys, amending D6. build-image.yml pushes runicgateway-site:latest and :sha-<7>, then rolls the container over on the `rgcom` runner out of /opt/runicgateway.com, and waits for the container's own healthcheck rather than for `up -d` to return. D55 — the site runs on its own host behind a generic reverse proxy, so DEPLOY.md states the four requirements rather than one worked example, and the container binds 127.0.0.1 so the safe configuration is the default. D56 — @astrojs/node derives the request protocol from req.socket.encrypted and never reads x-forwarded-proto, so behind a TLS-terminating proxy the browser sends Origin: https://… while the container computes http://… and Astro's CSRF check compares them for equality. Every beta signup, from every visitor, was answered 403. serve.mjs now normalises both forwarded headers, unconditionally — the image should deploy and work. Two assertions in test/headers.test.mjs hold both halves. D57 — DEPLOY.md rather than a README section; SECURITY.md and CODE_OF_CONDUCT.md are pointers to the org's copies rather than copies, because a copy would hard-code the contact address D13 confines to brand.json. Verified: npm run verify green (eleven checks, 36 unit tests, 7 served tests, astro check 0 errors). The image was built and run with both mounts — a mounted brand reached 51 files and all 50 search pages, /brand/* fell back per file, a proxy-shaped signup reached the store, and the export CLI wrote both Play files to the host mount. docker compose config caught a YAML trap in the healthcheck: a block sequence reads the `: ` in `r.ok ? 0 : 1` as a mapping. Co-Authored-By: Claude <noreply@anthropic.com>
151 lines
6.1 KiB
YAML
151 lines
6.1 KiB
YAML
# Build the container image, publish it to Gitea's container registry, then roll
|
|
# the site onto it — on every merge to main.
|
|
#
|
|
# Gitea Actions caution, learned elsewhere in this org and repeated from
|
|
# pr-checks.yml because it costs one comment and has already cost months: never
|
|
# leave an empty template expression anywhere in a `run:` script, not even inside
|
|
# a comment. The runner silently SKIPS the whole step without failing the job,
|
|
# and the problem is invisible in the workflow list.
|
|
#
|
|
# Two jobs, in sequence:
|
|
#
|
|
# build — builds and pushes the image (on `ubuntu-latest`)
|
|
# deploy — `needs: build`, so it starts only after a clean build and push, and
|
|
# pulls + recreates the stack on the host (on `rgcom`)
|
|
#
|
|
# Prerequisites, one-time:
|
|
#
|
|
# • A runner labelled `ubuntu-latest` whose jobs have the host Docker socket
|
|
# mounted (/var/run/docker.sock), so `docker build` talks to the host daemon.
|
|
# This also gives free layer caching between runs. The org already runs one.
|
|
#
|
|
# • A runner labelled `rgcom` ON the host that serves the site, able to reach
|
|
# the Docker daemon and /opt/runicgateway.com — the directory holding the
|
|
# production docker-compose.yml and .env. DEPLOY.md has the registration
|
|
# command and the directory layout.
|
|
#
|
|
# • Two repository secrets (Settings → Actions → Secrets), both of which
|
|
# already exist for pr-checks.yml's cross-repository checks:
|
|
# REGISTRY_USER — the Gitea username owning the token below
|
|
# REGISTRY_TOKEN — a token with write:package (and read:package)
|
|
#
|
|
# Produces, in gitea.whitlocktech.com/runicgateway/ :
|
|
# runicgateway-site:latest + runicgateway-site:sha-<7>
|
|
#
|
|
# and deploys `:latest`, which is what docker-compose.yml defaults IMAGE_TAG to.
|
|
#
|
|
# WHY THIS REPOSITORY DEPLOYS AND D6 SAID IT WOULD NOT: D6 was written before
|
|
# there was a host to deploy to, and read "ship the image, the org lead deploys".
|
|
# The org lead amended it on 2026-08-25 (D54): a marketing site whose content is
|
|
# its whole purpose is a bad fit for a manual step between merging a fix and the
|
|
# fix being visible. What D6 was protecting — that a bad build cannot reach
|
|
# production — is held by `needs: build` instead, plus every check in
|
|
# pr-checks.yml having already run on the pull request.
|
|
|
|
name: Build and publish the image
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch: {}
|
|
|
|
concurrency:
|
|
group: image-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
REGISTRY: gitea.whitlocktech.com
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Check out the merged commit
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Derive the image ref
|
|
# The registry path must be lowercase for Docker; the org is `RunicGateway`.
|
|
run: |
|
|
set -euo pipefail
|
|
OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
|
|
SHORT_SHA="${GITHUB_SHA:0:7}"
|
|
echo "IMAGE=${REGISTRY}/${OWNER}/runicgateway-site" >> "$GITHUB_ENV"
|
|
echo "TAG=sha-${SHORT_SHA}" >> "$GITHUB_ENV"
|
|
|
|
- name: Verify the Docker daemon is reachable
|
|
# Fails fast with a clear message if the host socket is not mounted into
|
|
# the job container — the one hard runner prerequisite.
|
|
run: |
|
|
set -euo pipefail
|
|
if ! docker info >/dev/null 2>&1; then
|
|
echo "::error::Docker daemon not reachable. Mount /var/run/docker.sock into the runner's job containers."
|
|
exit 1
|
|
fi
|
|
echo "Docker daemon OK"
|
|
|
|
- name: Log in to the Gitea container registry
|
|
run: |
|
|
set -euo pipefail
|
|
echo "${{ secrets.REGISTRY_TOKEN }}" \
|
|
| docker login "${REGISTRY}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
|
|
|
- name: Build and push
|
|
# Two tags from one build: `latest` for the compose default, `sha-<7>` so
|
|
# a deploy can be pinned or rolled back to an exact commit.
|
|
run: |
|
|
set -euo pipefail
|
|
docker build -f Dockerfile \
|
|
-t "${IMAGE}:latest" \
|
|
-t "${IMAGE}:${TAG}" \
|
|
.
|
|
docker push "${IMAGE}:latest"
|
|
docker push "${IMAGE}:${TAG}"
|
|
|
|
- name: Log out
|
|
if: always()
|
|
run: docker logout "${REGISTRY}" || true
|
|
|
|
deploy:
|
|
# Roll the site onto the image `build` just pushed. `needs: build` makes this
|
|
# wait for a clean build and push — if the build fails, deploy never fires and
|
|
# the running container is left alone rather than torn down for nothing.
|
|
needs: build
|
|
runs-on: rgcom
|
|
# Guard against a workflow_dispatch fired from a branch: only main is deployed.
|
|
if: github.ref == 'refs/heads/main'
|
|
|
|
steps:
|
|
- name: Pull the fresh image and recreate the container
|
|
# No `down` first, deliberately. There is one service and no database to
|
|
# keep still, so `up -d` recreates it in place when the pulled digest
|
|
# differs — a couple of seconds of connection refused behind the proxy
|
|
# rather than the whole stack stopped while an image is fetched.
|
|
run: |
|
|
set -euo pipefail
|
|
cd /opt/runicgateway.com
|
|
docker compose pull
|
|
docker compose up -d --remove-orphans
|
|
docker compose ps
|
|
|
|
- name: Wait for the container to report healthy
|
|
# The image's healthcheck watches an actual response, and `npm start` runs
|
|
# the brand rewrite before the server starts — so "running" arrives well
|
|
# before "serving". Without this the job would go green on a container
|
|
# that is about to crash-loop on, say, an unwritable ./data.
|
|
run: |
|
|
set -euo pipefail
|
|
cd /opt/runicgateway.com
|
|
for attempt in $(seq 1 30); do
|
|
STATUS="$(docker compose ps --format '{{.Health}}' site | head -n 1)"
|
|
echo "attempt ${attempt}: ${STATUS:-unknown}"
|
|
if [ "$STATUS" = "healthy" ]; then
|
|
echo "Site is healthy."
|
|
exit 0
|
|
fi
|
|
sleep 5
|
|
done
|
|
echo "::error::The site did not become healthy within 150s."
|
|
docker compose logs --tail 100 site
|
|
exit 1
|