All checks were successful
PR checks / checks (pull_request) Successful in 1m19s
The image measures 757 MB, not the ~600 MB the requirements table guessed. And until the rgcom runner exists the deploy job simply queues, which is worth stating in both the workflow and DEPLOY.md: build has already published the image by then, so the manual pull works throughout and the queued job goes when the runner does. Co-Authored-By: Claude <noreply@anthropic.com>
155 lines
6.4 KiB
YAML
155 lines
6.4 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'
|
|
# Until a runner with this label exists, this job simply QUEUES. That is the
|
|
# intended behaviour and it breaks nothing: `build` has already published the
|
|
# image, so `docker compose pull && up -d` by hand is available the whole time,
|
|
# and the queued job runs the moment the runner registers.
|
|
|
|
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
|