Files
runicgateway.com/.gitea/workflows/build-image.yml
wtclaude 1558111050
All checks were successful
PR checks / checks (pull_request) Successful in 1m19s
docs(deploy): correct the image size, and say what a missing rgcom runner does
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>
2026-08-25 16:57:25 -05:00

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