All checks were successful
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 6s
PR build (required check) / changes (pull_request) Successful in 6s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / gate (pull_request) Successful in 2s
secrets-guard / encrypted (pull_request) Successful in 6s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
144 lines
6 KiB
YAML
144 lines
6 KiB
YAML
name: Build + push images (Forgejo registry)
|
|
|
|
# backend-build-push.yml and frontend-build-push.yml, collapsed into one.
|
|
# They were identical apart from the domain string: the paths filter, the
|
|
# concurrency group, IMAGE_PATH, the tag key and the build context.
|
|
#
|
|
# Images stay separate and independently deployable -- emi/thermograph/backend
|
|
# and emi/thermograph/frontend -- exactly as before. Build-once,
|
|
# deploy-everywhere; nothing about the published artefacts changes here.
|
|
#
|
|
# SEMVER EXCEPTION, preserved: a v*.*.* tag push carries no paths context, so a
|
|
# release tag builds BOTH domain images. That is intended -- a release names the
|
|
# whole set, not whichever domain happened to move last.
|
|
#
|
|
# Runs on the `docker` label. The LAN runner's job containers get the host's
|
|
# docker.sock automounted (Docker-outside-of-Docker, no privileged mode); the
|
|
# job image (node:20-bookworm) ships no docker CLI, hence the install step.
|
|
#
|
|
# The registry is deliberately mesh-only: git.thermograph.org's public DNS
|
|
# resolves to beta's public IP, whose Caddy ACL rejects /v2/ paths, so any
|
|
# runner host that pushes or pulls needs an /etc/hosts entry.
|
|
|
|
on:
|
|
push:
|
|
branches: [dev, main, release]
|
|
paths:
|
|
- 'backend/**'
|
|
- 'frontend/**'
|
|
tags: ['v*.*.*']
|
|
workflow_dispatch: {}
|
|
|
|
jobs:
|
|
build-push:
|
|
strategy:
|
|
fail-fast: false
|
|
# capacity: 1 physical runner. Serialising keeps a two-domain push from
|
|
# queueing two whole image builds against each other.
|
|
max-parallel: 1
|
|
matrix:
|
|
domain: [backend, frontend]
|
|
|
|
runs-on: docker
|
|
|
|
# Per-domain, per-ref: only a superseded build of the SAME domain on the
|
|
# SAME ref is cancelled, and a frontend build never cancels a backend one.
|
|
concurrency:
|
|
group: build-push-${{ matrix.domain }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
REGISTRY: https://${{ vars.REGISTRY_HOST }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
# fetch-depth 0: the tag is keyed to the LAST COMMIT THAT TOUCHED THIS
|
|
# DOMAIN, not the branch tip -- in a path-filtered monorepo the tip is
|
|
# often an unrelated domain's commit, and a depth-1 clone can't see past
|
|
# it to find the real key.
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Compute image ref + tags
|
|
id: image
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
host="${REGISTRY#https://}"; host="${host#http://}"
|
|
path="$(printf '%s/%s' "${{ github.repository }}" "${{ matrix.domain }}" | tr '[:upper:]' '[:lower:]')"
|
|
ref="$host/$path"
|
|
|
|
# Does this push touch this domain? The workflow-level paths filter
|
|
# only says backend OR frontend moved; without this a backend-only
|
|
# push would also rebuild and republish the frontend image.
|
|
before="${{ github.event.before }}"
|
|
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
|
|
changed=true # semver tag: build the whole set, see header
|
|
elif [ -z "$before" ] || [ "$before" = "0000000000000000000000000000000000000000" ] \
|
|
|| ! git cat-file -e "$before^{commit}" 2>/dev/null; then
|
|
changed=true # no usable base; build rather than silently skip
|
|
elif git diff --name-only "$before" "${{ github.sha }}" | grep -q "^${{ matrix.domain }}/"; then
|
|
changed=true
|
|
else
|
|
changed=false
|
|
fi
|
|
|
|
# Key the tag to the last commit that touched this domain -- the SAME
|
|
# key every deploy computes -- so build and deploy always agree even
|
|
# when the branch tip is another domain's commit. (A tag keyed to the
|
|
# push tip breaks the moment an infra-only commit lands: deploys go
|
|
# looking for an image no build ever produced.)
|
|
domain_sha="$(git log -1 --format=%H -- "${{ matrix.domain }}/")"
|
|
|
|
{
|
|
echo "changed=$changed"
|
|
echo "host=$host"
|
|
echo "sha_tag=$ref:sha-${domain_sha:0:12}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
|
|
echo "semver_tag=$ref:${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
echo "==> ${{ matrix.domain }} | changed=$changed | $ref:sha-${domain_sha:0:12}"
|
|
|
|
- name: Install Docker CLI
|
|
if: steps.image.outputs.changed == 'true'
|
|
run: |
|
|
apt-get update -qq
|
|
apt-get install -y -qq docker.io
|
|
|
|
- name: Log in to the Forgejo registry
|
|
if: steps.image.outputs.changed == 'true'
|
|
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 REGISTRY_TOKEN.
|
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ steps.image.outputs.host }}" \
|
|
--username emi --password-stdin
|
|
|
|
- name: Build
|
|
if: steps.image.outputs.changed == 'true'
|
|
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 the 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[@]}" "${{ matrix.domain }}/"
|
|
|
|
- name: Push (SHA tag, every push)
|
|
if: steps.image.outputs.changed == 'true'
|
|
run: docker push "${{ steps.image.outputs.sha_tag }}"
|
|
|
|
- name: Push (semver tag, tag pushes only)
|
|
if: steps.image.outputs.changed == 'true' && startsWith(github.ref, 'refs/tags/v')
|
|
run: docker push "${{ steps.image.outputs.semver_tag }}"
|
|
|
|
- name: Skipped
|
|
if: steps.image.outputs.changed != 'true'
|
|
run: echo "${{ matrix.domain }} unchanged in this push — no image built."
|