diff --git a/.forgejo/workflows/build-push.yml b/.forgejo/workflows/build-push.yml new file mode 100644 index 0000000..2a86cd7 --- /dev/null +++ b/.forgejo/workflows/build-push.yml @@ -0,0 +1,104 @@ +name: Build + push image (Forgejo registry) + +# Builds THIS repo's image (backend/Dockerfile) and pushes it to Forgejo's +# built-in container registry, tagged by git SHA (every push) and semver +# (version tags) -- build-once, deploy-everywhere. +# +# Repo-split "finish separating" (was Stage 6/7): the frontend and backend no +# longer share the single `emi/thermograph/app` image the old monorepo +# build-push.yml produced. Each app repo now publishes its OWN image from its +# OWN Dockerfile -- this one is emi/thermograph-backend/app -- and +# thermograph-infra's compose references the two paths independently +# (BACKEND_IMAGE_PATH / FRONTEND_IMAGE_PATH). IMAGE_PATH is derived from +# github.repository, so this file is byte-identical in the frontend repo; the +# repo it lives in is what makes the image distinct. +# +# 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 thermograph-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] + 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 github.ref means dev/main/release/each tag +# get their own group, so this only cancels a superseded build of the SAME ref. +concurrency: + group: build-push-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-push: + runs-on: docker + env: + REGISTRY: https://${{ vars.REGISTRY_HOST }} + IMAGE_PATH: ${{ github.repository }}/app + 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. + 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[@]}" -f Dockerfile . + + - 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 }}" diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..217734f --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,52 @@ +name: Deploy backend to beta VPS + +# On a push to `main`, SSH to beta and roll ONLY the backend service onto the +# image build-push.yml just published for this commit. Frontend is deployed +# independently by its own repo's deploy.yml -- that is the whole point of the +# FE/BE CI-CD split: a backend change ships without touching frontend and vice +# versa. thermograph-infra/deploy/deploy.sh persists each service's live tag +# host-side, so a single-service roll never disturbs the other. +# +# The SSH_HOST/USER/KEY/PORT secrets point at beta. Prod is NOT deployed by a +# workflow -- it's deployed with `terraform apply` on the `release` branch, so +# there is deliberately no release-triggered workflow. appleboy/ssh-action is +# referenced by full GitHub URL because it isn't mirrored in Forgejo's default +# action registry. +# +# The tag MUST match build-push.yml's `sha-${GITHUB_SHA:0:12}` (12 hex), not +# the full 40-char ${{ github.sha }} -- default Actions expressions have no +# substring function, so the compute step below truncates it. deploy.sh also +# retries the pull for ~5 min because Forgejo `needs:` can't gate across the +# separate build-push.yml, so this push's build may still be in flight. + +on: + push: + branches: [main] + workflow_dispatch: {} + +concurrency: + group: beta-deploy-backend + cancel-in-progress: false + +jobs: + deploy: + runs-on: docker + steps: + - name: Compute image tag + id: tag + run: echo "tag=sha-${GITHUB_SHA:0:12}" >> "$GITHUB_OUTPUT" + + - name: Deploy backend over SSH + uses: https://github.com/appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.SSH_PORT }} + # Forward the target service + the image tag to run. deploy.sh reads + # BACKEND_IMAGE_TAG and rolls just `backend`. + envs: SERVICE,BACKEND_IMAGE_TAG + script: /opt/thermograph/deploy/deploy.sh + env: + SERVICE: backend + BACKEND_IMAGE_TAG: ${{ steps.tag.outputs.tag }}