thermograph/.forgejo/workflows/frontend-build-push.yml
Emi Griffith 7b2db07722 CI: port the split repos' workflows to per-domain path-filtered monorepo pipelines
One root workflow set replaces the four repos' copies (deleted -- root-only
is where Forgejo reads them, and dead copies are a trap): per-domain
build-push with explicit image paths (emi/thermograph/backend|frontend; the
old github.repository-derived path collides in a monorepo), path-filtered
per-domain beta/prod/dev deploys, a domain-input reusable build check, a
single always-reporting PR gate (path-filtered required checks deadlock
auto-merge), a new infra-sync pipeline (host checkout + secrets render on
infra/** pushes), and ports of secrets-guard / ops-cron /
observability-validate to monorepo paths.
2026-07-22 22:11:33 -07:00

114 lines
5.2 KiB
YAML

name: Build + push frontend image (Forgejo registry)
# Builds the FRONTEND domain's image (frontend/Dockerfile, context frontend/) and
# pushes it to Forgejo's built-in container registry, tagged by git SHA (every
# push) and semver (version tags) -- build-once, deploy-everywhere.
#
# Monorepo port (reunification): the split repos each derived IMAGE_PATH from
# github.repository, which now collides for every domain -- so each domain's
# build-push names its image explicitly: this one is emi/thermograph/frontend
# (backend-build-push.yml publishes emi/thermograph/backend). infra's
# compose/stack files reference the two paths independently
# (FRONTEND_IMAGE_PATH / BACKEND_IMAGE_PATH -- their defaults were renamed to
# match in the same cutover).
#
# Path-filtered: only a push that touches frontend/** builds this image. A
# backend-only push builds nothing here; deploy.sh's persisted
# .image-tags.env keeps the frontend's live tag for the sibling's deploy.
# EXCEPTION: version-tag pushes (v*.*.*) carry no paths context, so a semver
# tag builds BOTH domain images -- intended, a release names the whole set.
#
# Runs on the `docker` label -- the LAN runner's job containers get the host's
# docker.sock automounted in (forgejo-runner config: container.docker_host:
# automount; see infra/deploy/forgejo/register-lan-runner.sh),
# Docker-outside-of-Docker rather than DinD -- no privileged mode needed.
# The job image itself (node:20-bookworm) has no docker CLI, hence the
# "Install Docker CLI" step.
#
# The registry is deliberately mesh-only: git.thermograph.org's public DNS
# resolves to beta's public IP, which Caddy's ACL rejects for /v2/ paths, so
# any runner host that pushes/pulls needs a /etc/hosts entry pointing
# git.thermograph.org at beta's WireGuard IP (10.10.0.2). The docker
# build/push commands run against the automounted HOST daemon, so it's the
# runner HOST's own resolver that has to route over the mesh, not anything
# settable from inside the job container.
#
# REGISTRY is the vars.REGISTRY_HOST Actions variable (git.thermograph.org),
# NOT github.server_url -- that context resolves to the bare mesh IP:port
# (http://10.10.0.2:3080, no TLS), which docker CLI refuses ("server gave HTTP
# response to HTTPS client"). git.thermograph.org gets real TLS via Caddy and
# routes over the mesh once /etc/hosts is set, so it's the only correct value.
# deploy.sh resolves the SHA tag at deploy time and `docker compose pull`s it.
on:
push:
branches: [dev, main, release]
paths: ['frontend/**']
tags: ['v*.*.*']
workflow_dispatch: {}
# The runner has capacity: 1 (one physical LAN box) -- a burst of pushes to the
# same ref would otherwise queue whole builds back to back even though only the
# last one still matters. Keying on domain + github.ref means each domain's
# dev/main/release/tag builds get their own group, so this only cancels a
# superseded build of the SAME domain on the SAME ref -- and a backend build
# never cancels a frontend one.
concurrency:
group: frontend-build-push-${{ github.ref }}
cancel-in-progress: true
jobs:
build-push:
runs-on: docker
env:
REGISTRY: https://${{ vars.REGISTRY_HOST }}
IMAGE_PATH: ${{ github.repository }}/frontend
steps:
- uses: actions/checkout@v4
- name: Install Docker CLI
run: |
apt-get update -qq
apt-get install -y -qq docker.io
- name: Compute image ref + tags
id: image
run: |
host="${REGISTRY#https://}"; host="${host#http://}"
path="$(printf '%s' "$IMAGE_PATH" | tr '[:upper:]' '[:lower:]')"
ref="$host/$path"
sha_tag="$ref:sha-${GITHUB_SHA:0:12}"
echo "host=$host" >> "$GITHUB_OUTPUT"
echo "sha_tag=$sha_tag" >> "$GITHUB_OUTPUT"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "semver_tag=$ref:${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
fi
- name: Log in to the Forgejo registry
run: |
# NOT secrets.GITHUB_TOKEN -- Forgejo's per-job auto-injected token
# can never push to the container registry (a known Forgejo
# limitation independent of any permission setting). Needs a
# manually provisioned PAT with write:package scope, stored as the
# REGISTRY_TOKEN secret.
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ steps.image.outputs.host }}" \
--username emi --password-stdin
- name: Build
run: |
# One build, both tags -- the semver tag (tag pushes only) rides
# along with the SHA tag instead of a second tag+push round trip.
# Context is the DOMAIN dir, so frontend/Dockerfile sees the same
# tree it saw as a repo root in the split era.
tag_args=(-t "${{ steps.image.outputs.sha_tag }}")
if [[ -n "${{ steps.image.outputs.semver_tag }}" ]]; then
tag_args+=(-t "${{ steps.image.outputs.semver_tag }}")
fi
docker build "${tag_args[@]}" frontend/
- name: Push (SHA tag, every push)
run: docker push "${{ steps.image.outputs.sha_tag }}"
- name: Push (semver tag, tag pushes only)
if: startsWith(github.ref, 'refs/tags/v')
run: docker push "${{ steps.image.outputs.semver_tag }}"