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.
This commit is contained in:
parent
f2fd8f6835
commit
7b2db07722
26 changed files with 727 additions and 648 deletions
|
|
@ -1,21 +1,26 @@
|
|||
name: Build + push image (Forgejo registry)
|
||||
name: Build + push backend 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.
|
||||
# Builds the BACKEND domain's image (backend/Dockerfile, context backend/) 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.
|
||||
# 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/backend
|
||||
# (frontend-build-push.yml publishes emi/thermograph/frontend). infra's
|
||||
# compose/stack files reference the two paths independently
|
||||
# (BACKEND_IMAGE_PATH / FRONTEND_IMAGE_PATH -- their defaults were renamed to
|
||||
# match in the same cutover).
|
||||
#
|
||||
# Path-filtered: only a push that touches backend/** builds this image. A
|
||||
# frontend-only push builds nothing here; deploy.sh's persisted
|
||||
# .image-tags.env keeps the backend'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 thermograph-infra/deploy/forgejo/register-lan-runner.sh),
|
||||
# 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.
|
||||
|
|
@ -38,15 +43,18 @@ name: Build + push image (Forgejo registry)
|
|||
on:
|
||||
push:
|
||||
branches: [dev, main, release]
|
||||
paths: ['backend/**']
|
||||
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.
|
||||
# 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 frontend build
|
||||
# never cancels a backend one.
|
||||
concurrency:
|
||||
group: build-push-${{ github.ref }}
|
||||
group: backend-build-push-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
|
@ -54,7 +62,7 @@ jobs:
|
|||
runs-on: docker
|
||||
env:
|
||||
REGISTRY: https://${{ vars.REGISTRY_HOST }}
|
||||
IMAGE_PATH: ${{ github.repository }}/app
|
||||
IMAGE_PATH: ${{ github.repository }}/backend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
|
@ -90,11 +98,13 @@ jobs:
|
|||
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 backend/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[@]}" -f Dockerfile .
|
||||
docker build "${tag_args[@]}" backend/
|
||||
|
||||
- name: Push (SHA tag, every push)
|
||||
run: docker push "${{ steps.image.outputs.sha_tag }}"
|
||||
65
.forgejo/workflows/backend-deploy-dev.yml
Normal file
65
.forgejo/workflows/backend-deploy-dev.yml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
name: Deploy backend to LAN dev server
|
||||
|
||||
# Fires on a push to `dev` touching backend/** -- a direct push, or the real
|
||||
# merge commit a native "auto merge when checks succeed" produces (see
|
||||
# pr-build.yml). A `build` job (the reusable build.yml sanity check) gates a
|
||||
# `deploy` job that rolls the LAN dev box.
|
||||
#
|
||||
# CUTOVER NOTE (monorepo reunification): the LAN box's ~/thermograph-dev is a
|
||||
# thermograph-infra checkout from the split era. This job calls
|
||||
# ~/thermograph-dev/infra/deploy/deploy-dev.sh -- the MONOREPO layout -- so it
|
||||
# is INERT until ~/thermograph-dev is re-pointed at the monorepo (a fresh
|
||||
# clone; deploy-dev.sh then self-updates via deploy.sh's git reset like the
|
||||
# beta/prod paths).
|
||||
#
|
||||
# SERVICE=backend + BACKEND_IMAGE_TAG is the same env-var contract the
|
||||
# beta/prod deploy workflows use against infra/deploy/deploy.sh, so a dev-only
|
||||
# rollout never touches the frontend's running container or tag.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths: ['backend/**']
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
# Fixed (unparameterized) group: if two backend pushes to dev land close
|
||||
# together, the newer push's build cancels the older's still-running one
|
||||
# on the single physical runner -- the older result would be thrown away
|
||||
# once needs:build gates its deploy anyway. Safe to cancel mid-run:
|
||||
# build.yml only runs a throwaway `docker build`, nothing persisted.
|
||||
concurrency:
|
||||
group: dev-push-build-backend
|
||||
cancel-in-progress: true
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
with:
|
||||
domain: backend
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
# NOT [self-hosted, thermograph-lan] -- GitHub implicitly tags every
|
||||
# self-hosted runner with "self-hosted"; Forgejo's runner has only the
|
||||
# labels it was registered with ("docker" + "thermograph-lan"). The array
|
||||
# form silently makes this job unschedulable on every push.
|
||||
runs-on: thermograph-lan
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: dev-lan-deploy-backend
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Compute image tag
|
||||
id: tag
|
||||
# Same 12-hex truncation the beta/prod deploys use -- must match
|
||||
# backend-build-push.yml's `sha-${GITHUB_SHA:0:12}` tag exactly.
|
||||
run: echo "tag=sha-${GITHUB_SHA:0:12}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy to the LAN dev server
|
||||
# thermograph-lan is host-native (the runner job runs directly on the
|
||||
# LAN box, not over SSH like beta/prod), so plain env vars on the
|
||||
# command reach deploy-dev.sh directly -- no ssh-action needed here.
|
||||
run: SERVICE=backend BACKEND_IMAGE_TAG=${{ steps.tag.outputs.tag }} bash "$HOME/thermograph-dev/infra/deploy/deploy-dev.sh"
|
||||
46
.forgejo/workflows/backend-deploy-prod.yml
Normal file
46
.forgejo/workflows/backend-deploy-prod.yml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
name: Deploy backend to prod VPS
|
||||
|
||||
# On a push to `release` that touches backend/**, SSH to prod and roll ONLY
|
||||
# the backend service onto the image backend-build-push.yml published for this
|
||||
# commit. Same shape as backend-deploy.yml (the `main`->beta path) against a
|
||||
# completely separate secret set (PROD_SSH_HOST/USER/KEY/PORT) so a beta
|
||||
# credential leak can't reach prod. Frontend deploys itself independently via
|
||||
# frontend-deploy-prod.yml -- a backend change ships to prod without touching
|
||||
# frontend and vice versa, exactly as in the split era.
|
||||
#
|
||||
# The tag MUST match backend-build-push.yml's `sha-${GITHUB_SHA:0:12}` (12
|
||||
# hex) -- see backend-deploy.yml for why it's truncated here. deploy.sh
|
||||
# retries the pull because the build for this push may still be in flight.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [release]
|
||||
paths: ['backend/**']
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: prod-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.PROD_SSH_HOST }}
|
||||
username: ${{ secrets.PROD_SSH_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
port: ${{ secrets.PROD_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/infra/deploy/deploy.sh
|
||||
env:
|
||||
SERVICE: backend
|
||||
BACKEND_IMAGE_TAG: ${{ steps.tag.outputs.tag }}
|
||||
55
.forgejo/workflows/backend-deploy.yml
Normal file
55
.forgejo/workflows/backend-deploy.yml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
name: Deploy backend to beta VPS
|
||||
|
||||
# On a push to `main` that touches backend/**, SSH to beta and roll ONLY the
|
||||
# backend service onto the image backend-build-push.yml just published for
|
||||
# this commit. Frontend is deployed independently by frontend-deploy.yml --
|
||||
# the FE/BE CI-CD split survives reunification intact: a backend change ships
|
||||
# without touching frontend and vice versa. infra/deploy/deploy.sh persists
|
||||
# each service's live tag host-side, so a single-service roll never disturbs
|
||||
# the other. An infra-only push rolls nothing (infra-sync.yml syncs the
|
||||
# host checkouts instead); a mixed backend+frontend push fires both deploy
|
||||
# workflows, serialized host-side by deploy.sh's flock.
|
||||
#
|
||||
# The SSH_HOST/USER/KEY/PORT secrets point at beta. appleboy/ssh-action is
|
||||
# referenced by full GitHub URL because it isn't mirrored in Forgejo's default
|
||||
# action registry.
|
||||
#
|
||||
# The tag MUST match backend-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 workflow, so this push's build may still
|
||||
# be in flight.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['backend/**']
|
||||
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/infra/deploy/deploy.sh
|
||||
env:
|
||||
SERVICE: backend
|
||||
BACKEND_IMAGE_TAG: ${{ steps.tag.outputs.tag }}
|
||||
36
.forgejo/workflows/build.yml
Normal file
36
.forgejo/workflows/build.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
name: Build check (reusable)
|
||||
|
||||
# Build-only `docker build` sanity check for ONE app domain, reused via
|
||||
# `uses:` by pr-build.yml and the deploy-dev workflows -- the monorepo port of
|
||||
# each app repo's build.yml. Still deliberately NOT a live boot+healthz check:
|
||||
# that was tried in the split repos and repeatedly hit this runner's own
|
||||
# environment quirks (bridge-IP reachability, non-unique $$, squatted host
|
||||
# ports) without ever going reliably green. Image boot is verified by hand
|
||||
# instead (docker run + alembic + /healthz 200 + a real /api/v2/place 200).
|
||||
#
|
||||
# Monorepo port: the domain (backend|frontend) arrives as a workflow_call
|
||||
# input, and the build CONTEXT is that domain's subdirectory -- so each
|
||||
# Dockerfile sees exactly the tree it saw when its domain was a repo root,
|
||||
# byte-for-byte the same build as the split era.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
domain:
|
||||
description: "App domain to build: backend | frontend"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker.io
|
||||
|
||||
- name: Build
|
||||
run: docker build -t thermograph-${{ inputs.domain }}:ci ${{ inputs.domain }}/
|
||||
|
|
@ -1,21 +1,26 @@
|
|||
name: Build + push image (Forgejo registry)
|
||||
name: Build + push frontend image (Forgejo registry)
|
||||
|
||||
# Builds THIS repo's image (frontend/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.
|
||||
# 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.
|
||||
#
|
||||
# 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-frontend/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.
|
||||
# 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 thermograph-infra/deploy/forgejo/register-lan-runner.sh),
|
||||
# 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.
|
||||
|
|
@ -38,15 +43,18 @@ name: Build + push image (Forgejo registry)
|
|||
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 github.ref means dev/main/release/each tag
|
||||
# get their own group, so this only cancels a superseded build of the SAME ref.
|
||||
# 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: build-push-${{ github.ref }}
|
||||
group: frontend-build-push-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
|
@ -54,7 +62,7 @@ jobs:
|
|||
runs-on: docker
|
||||
env:
|
||||
REGISTRY: https://${{ vars.REGISTRY_HOST }}
|
||||
IMAGE_PATH: ${{ github.repository }}/app
|
||||
IMAGE_PATH: ${{ github.repository }}/frontend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
|
@ -90,11 +98,13 @@ jobs:
|
|||
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[@]}" -f Dockerfile .
|
||||
docker build "${tag_args[@]}" frontend/
|
||||
|
||||
- name: Push (SHA tag, every push)
|
||||
run: docker push "${{ steps.image.outputs.sha_tag }}"
|
||||
65
.forgejo/workflows/frontend-deploy-dev.yml
Normal file
65
.forgejo/workflows/frontend-deploy-dev.yml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
name: Deploy frontend to LAN dev server
|
||||
|
||||
# Fires on a push to `dev` touching frontend/** -- a direct push, or the real
|
||||
# merge commit a native "auto merge when checks succeed" produces (see
|
||||
# pr-build.yml). A `build` job (the reusable build.yml sanity check) gates a
|
||||
# `deploy` job that rolls the LAN dev box.
|
||||
#
|
||||
# CUTOVER NOTE (monorepo reunification): the LAN box's ~/thermograph-dev is a
|
||||
# thermograph-infra checkout from the split era. This job calls
|
||||
# ~/thermograph-dev/infra/deploy/deploy-dev.sh -- the MONOREPO layout -- so it
|
||||
# is INERT until ~/thermograph-dev is re-pointed at the monorepo (a fresh
|
||||
# clone; deploy-dev.sh then self-updates via deploy.sh's git reset like the
|
||||
# beta/prod paths).
|
||||
#
|
||||
# SERVICE=frontend + FRONTEND_IMAGE_TAG is the same env-var contract the
|
||||
# beta/prod deploy workflows use against infra/deploy/deploy.sh, so a dev-only
|
||||
# rollout never touches the backend's running container or tag.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths: ['frontend/**']
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
# Fixed (unparameterized) group: if two frontend pushes to dev land close
|
||||
# together, the newer push's build cancels the older's still-running one
|
||||
# on the single physical runner -- the older result would be thrown away
|
||||
# once needs:build gates its deploy anyway. Safe to cancel mid-run:
|
||||
# build.yml only runs a throwaway `docker build`, nothing persisted.
|
||||
concurrency:
|
||||
group: dev-push-build-frontend
|
||||
cancel-in-progress: true
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
with:
|
||||
domain: frontend
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
# NOT [self-hosted, thermograph-lan] -- GitHub implicitly tags every
|
||||
# self-hosted runner with "self-hosted"; Forgejo's runner has only the
|
||||
# labels it was registered with ("docker" + "thermograph-lan"). The array
|
||||
# form silently makes this job unschedulable on every push.
|
||||
runs-on: thermograph-lan
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: dev-lan-deploy-frontend
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Compute image tag
|
||||
id: tag
|
||||
# Same 12-hex truncation the beta/prod deploys use -- must match
|
||||
# frontend-build-push.yml's `sha-${GITHUB_SHA:0:12}` tag exactly.
|
||||
run: echo "tag=sha-${GITHUB_SHA:0:12}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy to the LAN dev server
|
||||
# thermograph-lan is host-native (the runner job runs directly on the
|
||||
# LAN box, not over SSH like beta/prod), so plain env vars on the
|
||||
# command reach deploy-dev.sh directly -- no ssh-action needed here.
|
||||
run: SERVICE=frontend FRONTEND_IMAGE_TAG=${{ steps.tag.outputs.tag }} bash "$HOME/thermograph-dev/infra/deploy/deploy-dev.sh"
|
||||
51
.forgejo/workflows/frontend-deploy-prod.yml
Normal file
51
.forgejo/workflows/frontend-deploy-prod.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
name: Deploy frontend to prod VPS
|
||||
|
||||
# On a push to `release` that touches frontend/**, SSH to prod and roll ONLY
|
||||
# the frontend service onto the image frontend-build-push.yml published for this
|
||||
# commit. Same shape as frontend-deploy.yml (the `main`->beta path) against a
|
||||
# completely separate secret set (PROD_SSH_HOST/USER/KEY/PORT) so a beta
|
||||
# credential leak can't reach prod. Backend deploys itself independently via
|
||||
# backend-deploy-prod.yml -- a frontend change ships to prod without touching
|
||||
# backend and vice versa, exactly as in the split era.
|
||||
#
|
||||
# The tag MUST match frontend-build-push.yml's `sha-${GITHUB_SHA:0:12}` (12
|
||||
# hex) -- see frontend-deploy.yml for why it's truncated here. deploy.sh
|
||||
# retries the pull because the build for this push may still be in flight.
|
||||
|
||||
#
|
||||
# Frontend's own register() fetches the IndexNow key from backend at boot, so
|
||||
# deploy.sh waits on a healthy backend before declaring the frontend roll OK
|
||||
# (its compose depends_on already encodes that ordering).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [release]
|
||||
paths: ['frontend/**']
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: prod-deploy-frontend
|
||||
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 frontend over SSH
|
||||
uses: https://github.com/appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.PROD_SSH_HOST }}
|
||||
username: ${{ secrets.PROD_SSH_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
port: ${{ secrets.PROD_SSH_PORT }}
|
||||
# Forward the target service + the image tag to run. deploy.sh reads
|
||||
# FRONTEND_IMAGE_TAG and rolls just `frontend`.
|
||||
envs: SERVICE,FRONTEND_IMAGE_TAG
|
||||
script: /opt/thermograph/infra/deploy/deploy.sh
|
||||
env:
|
||||
SERVICE: frontend
|
||||
FRONTEND_IMAGE_TAG: ${{ steps.tag.outputs.tag }}
|
||||
60
.forgejo/workflows/frontend-deploy.yml
Normal file
60
.forgejo/workflows/frontend-deploy.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
name: Deploy frontend to beta VPS
|
||||
|
||||
# On a push to `main` that touches frontend/**, SSH to beta and roll ONLY the
|
||||
# frontend service onto the image frontend-build-push.yml just published for
|
||||
# this commit. Backend is deployed independently by backend-deploy.yml --
|
||||
# the FE/BE CI-CD split survives reunification intact: a frontend change ships
|
||||
# without touching backend and vice versa. infra/deploy/deploy.sh persists
|
||||
# each service's live tag host-side, so a single-service roll never disturbs
|
||||
# the other. An infra-only push rolls nothing (infra-sync.yml syncs the
|
||||
# host checkouts instead); a mixed frontend+backend push fires both deploy
|
||||
# workflows, serialized host-side by deploy.sh's flock.
|
||||
#
|
||||
# The SSH_HOST/USER/KEY/PORT secrets point at beta. appleboy/ssh-action is
|
||||
# referenced by full GitHub URL because it isn't mirrored in Forgejo's default
|
||||
# action registry.
|
||||
#
|
||||
# The tag MUST match frontend-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 workflow, so this push's build may still
|
||||
# be in flight.
|
||||
|
||||
#
|
||||
# Frontend's own register() fetches the IndexNow key from backend at boot, so
|
||||
# deploy.sh waits on a healthy backend before declaring the frontend roll OK
|
||||
# (its compose depends_on already encodes that ordering).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['frontend/**']
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: beta-deploy-frontend
|
||||
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 frontend 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
|
||||
# FRONTEND_IMAGE_TAG and rolls just `frontend`.
|
||||
envs: SERVICE,FRONTEND_IMAGE_TAG
|
||||
script: /opt/thermograph/infra/deploy/deploy.sh
|
||||
env:
|
||||
SERVICE: frontend
|
||||
FRONTEND_IMAGE_TAG: ${{ steps.tag.outputs.tag }}
|
||||
73
.forgejo/workflows/infra-sync.yml
Normal file
73
.forgejo/workflows/infra-sync.yml
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
name: Sync infra to hosts
|
||||
|
||||
# The infra DOMAIN's own pipeline (new with the monorepo -- the split era had
|
||||
# no infra deploy workflow at all; infra changes rode along lazily with the
|
||||
# next app deploy's `git reset`). On a push to `main` touching infra/**, SSH
|
||||
# to beta AND prod, fast-forward the host's /opt/thermograph monorepo checkout
|
||||
# and re-render /etc/thermograph.env from the SOPS vault -- so a compose edit
|
||||
# or a secret rotation lands push-button instead of waiting for the next app
|
||||
# deploy.
|
||||
#
|
||||
# Deliberately does NOT roll any service: image tags are the app domains'
|
||||
# axis, not infra's. A compose change that must recreate containers takes
|
||||
# effect on the next app deploy, or a by-hand
|
||||
# `SERVICE=all ... infra/deploy/deploy.sh` run (see that script's header).
|
||||
#
|
||||
# Both hosts track infra via `main` (the split-era model, unchanged): prod's
|
||||
# *app images* are staged by the `release` branch, but its checkout follows
|
||||
# main -- deploy.sh's own BRANCH default encodes the same thing.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['infra/**']
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
sync-beta:
|
||||
runs-on: docker
|
||||
concurrency:
|
||||
group: infra-sync-beta
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Sync beta checkout + re-render secrets
|
||||
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 }}
|
||||
script: |
|
||||
set -euo pipefail
|
||||
cd /opt/thermograph
|
||||
git fetch --prune origin main
|
||||
git reset --hard origin/main
|
||||
if [ -f infra/deploy/render-secrets.sh ]; then
|
||||
. infra/deploy/render-secrets.sh
|
||||
render_thermograph_secrets /opt/thermograph/infra
|
||||
fi
|
||||
echo "synced to $(git log --oneline -1)"
|
||||
|
||||
sync-prod:
|
||||
runs-on: docker
|
||||
concurrency:
|
||||
group: infra-sync-prod
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Sync prod checkout + re-render secrets
|
||||
uses: https://github.com/appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.PROD_SSH_HOST }}
|
||||
username: ${{ secrets.PROD_SSH_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
port: ${{ secrets.PROD_SSH_PORT }}
|
||||
script: |
|
||||
set -euo pipefail
|
||||
cd /opt/thermograph
|
||||
git fetch --prune origin main
|
||||
git reset --hard origin/main
|
||||
if [ -f infra/deploy/render-secrets.sh ]; then
|
||||
. infra/deploy/render-secrets.sh
|
||||
render_thermograph_secrets /opt/thermograph/infra
|
||||
fi
|
||||
echo "synced to $(git log --oneline -1)"
|
||||
72
.forgejo/workflows/observability-validate.yml
Normal file
72
.forgejo/workflows/observability-validate.yml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
name: Validate observability stack
|
||||
|
||||
# The observability DOMAIN deploys by hand (`docker compose up -d` on beta +
|
||||
# the Alloy agent on each node) with no build step, so nothing caught a
|
||||
# malformed compose file, a broken dashboard JSON, or an unparseable config
|
||||
# until it failed live on the host. This is that missing guard: it parses
|
||||
# every YAML/JSON artifact that ships to a node. It does NOT deploy, and
|
||||
# deliberately needs no docker CLI (the node:20-bookworm job image doesn't
|
||||
# ship one) -- a YAML parse catches the real breakage without it.
|
||||
#
|
||||
# Monorepo port: paths are prefixed observability/, the push trigger is
|
||||
# path-filtered to the domain, and workflow_call lets pr-build.yml reuse this
|
||||
# as the domain's PR check under its single `gate` required status.
|
||||
#
|
||||
# Not validated here: the Alloy config (observability/alloy/config.alloy, an
|
||||
# HCL-like format, needs the `alloy` binary) and docker-compose *schema*
|
||||
# (unknown-key checks, which would need the docker CLI). Add either if the
|
||||
# churn warrants the extra tooling.
|
||||
|
||||
on:
|
||||
workflow_call: {}
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths: ['observability/**']
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Dashboards are valid JSON
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import json, pathlib, sys
|
||||
bad = False
|
||||
files = sorted(pathlib.Path("observability/grafana/dashboards").glob("*.json"))
|
||||
if not files:
|
||||
print("no dashboards found"); sys.exit(1)
|
||||
for f in files:
|
||||
try:
|
||||
json.loads(f.read_text()); print("OK", f)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("INVALID JSON", f, "-", e); bad = True
|
||||
sys.exit(1 if bad else 0)
|
||||
PY
|
||||
|
||||
- name: YAML artifacts parse (compose + loki + grafana provisioning)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update -qq && apt-get install -y -qq python3-yaml >/dev/null
|
||||
python3 - <<'PY'
|
||||
import pathlib, sys
|
||||
import yaml
|
||||
bad = False
|
||||
root = pathlib.Path("observability")
|
||||
targets = [
|
||||
root / "docker-compose.yml",
|
||||
root / "alloy/docker-compose.agent.yml",
|
||||
root / "loki/config.yml",
|
||||
*sorted((root / "grafana/provisioning").rglob("*.yml")),
|
||||
]
|
||||
for f in targets:
|
||||
if not f.exists():
|
||||
print("MISSING", f); bad = True; continue
|
||||
try:
|
||||
yaml.safe_load(f.read_text()); print("OK", f)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("INVALID YAML", f, "-", e); bad = True
|
||||
sys.exit(1 if bad else 0)
|
||||
PY
|
||||
|
|
@ -20,6 +20,11 @@ name: Ops cron (backup + IndexNow)
|
|||
# beta; prod itself had no backup at all. The prod database is the one that must
|
||||
# be backed up, so both jobs use PROD_SSH_*.
|
||||
|
||||
# Monorepo port: the compose file now lives under infra/ of the host's
|
||||
# /opt/thermograph monorepo checkout, so every docker-compose invocation cd's
|
||||
# into /opt/thermograph/infra (compose also pins `name: thermograph` so the
|
||||
# project name no longer depends on the directory). Everything else unchanged.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 03:00 UTC daily -- a low-traffic window for both jobs.
|
||||
|
|
@ -48,7 +53,7 @@ jobs:
|
|||
port: ${{ secrets.PROD_SSH_PORT }}
|
||||
script: |
|
||||
set -euo pipefail
|
||||
cd /opt/thermograph
|
||||
cd /opt/thermograph/infra
|
||||
# Source the env so `docker compose` can interpolate POSTGRES_PASSWORD;
|
||||
# without it compose fails to parse docker-compose.yml, the redirect
|
||||
# still creates the target, and the job leaves a 0-byte .dump and exits
|
||||
|
|
@ -63,7 +68,7 @@ jobs:
|
|||
# (prod post-cutover); resolve the container either way so the
|
||||
# backup survives the deploy-mode switch.
|
||||
dbc=$(docker ps -q --filter "label=com.docker.swarm.service.name=thermograph_db" | head -1)
|
||||
[ -z "$dbc" ] && dbc=$(cd /opt/thermograph && docker compose ps -q db 2>/dev/null | head -1)
|
||||
[ -z "$dbc" ] && dbc=$(cd /opt/thermograph/infra && docker compose ps -q db 2>/dev/null | head -1)
|
||||
[ -n "$dbc" ] || { echo "!! no db container found (compose or stack)"; exit 1; }
|
||||
# Write to a .partial and rename on success so a mid-dump failure can
|
||||
# never leave a truncated file that looks like a good backup.
|
||||
|
|
@ -95,10 +100,10 @@ jobs:
|
|||
port: ${{ secrets.PROD_SSH_PORT }}
|
||||
script: |
|
||||
set -euo pipefail
|
||||
cd /opt/thermograph
|
||||
cd /opt/thermograph/infra
|
||||
set -a; . /etc/thermograph.env 2>/dev/null || true; set +a
|
||||
bec=$(docker ps -q --filter "label=com.docker.swarm.service.name=thermograph_web" | head -1)
|
||||
[ -z "$bec" ] && bec=$(cd /opt/thermograph && docker compose ps -q backend 2>/dev/null | head -1)
|
||||
[ -z "$bec" ] && bec=$(cd /opt/thermograph/infra && docker compose ps -q backend 2>/dev/null | head -1)
|
||||
[ -n "$bec" ] || { echo "!! no backend/web container found"; exit 1; }
|
||||
docker exec "$bec" python indexnow.py --if-changed \
|
||||
"${THERMOGRAPH_BASE_URL:-https://thermograph.org}"
|
||||
98
.forgejo/workflows/pr-build.yml
Normal file
98
.forgejo/workflows/pr-build.yml
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
name: PR build (required check)
|
||||
|
||||
# Monorepo port of the app repos' pr-build.yml: ONE workflow, ONE stable
|
||||
# required-check name (`gate`), any number of domains. A `changes` job diffs
|
||||
# the PR against its base; per-domain jobs run only for domains the PR
|
||||
# touches; `gate` reduces their results into the single status branch
|
||||
# protection requires.
|
||||
#
|
||||
# Deliberately NOT path-filtered at the workflow level: a path-filtered
|
||||
# required check that never fires leaves the PR stuck "expected -- waiting for
|
||||
# status" forever, so auto-merge never proceeds. An always-running gate whose
|
||||
# domain jobs skip cleanly gives the same compute savings without the
|
||||
# deadlock.
|
||||
#
|
||||
# One-time web UI setup (Settings -> Branches, rules for `dev` and `main`):
|
||||
# -> enable "Enable Status Check", list this workflow's "gate" job
|
||||
# -> merge style: squash
|
||||
# Then on a ready PR: "Auto merge when checks succeed".
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev, main]
|
||||
|
||||
concurrency:
|
||||
group: pr-build-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: docker
|
||||
outputs:
|
||||
backend: ${{ steps.diff.outputs.backend }}
|
||||
frontend: ${{ steps.diff.outputs.frontend }}
|
||||
observability: ${{ steps.diff.outputs.observability }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The PR base commit isn't reachable from a depth-1 clone; full
|
||||
# history keeps the diff computable however far the branch diverged.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed domains
|
||||
id: diff
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base="${{ github.event.pull_request.base.sha }}"
|
||||
changed=$(git diff --name-only "$base" "$GITHUB_SHA")
|
||||
echo "changed files:"; printf '%s\n' "$changed"
|
||||
for d in backend frontend observability; do
|
||||
if printf '%s\n' "$changed" | grep -q "^$d/"; then
|
||||
echo "$d=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "$d=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
done
|
||||
|
||||
build-backend:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.backend == 'true'
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
with:
|
||||
domain: backend
|
||||
|
||||
build-frontend:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
with:
|
||||
domain: frontend
|
||||
|
||||
validate-observability:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.observability == 'true'
|
||||
uses: ./.forgejo/workflows/observability-validate.yml
|
||||
|
||||
gate:
|
||||
name: gate
|
||||
# always(): skipped domain jobs (nothing changed there) must not skip the
|
||||
# gate itself -- the required check has to report on EVERY PR.
|
||||
if: always()
|
||||
needs: [changes, build-backend, build-frontend, validate-observability]
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Reduce domain results
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ok=1
|
||||
for pair in \
|
||||
"backend=${{ needs.build-backend.result }}" \
|
||||
"frontend=${{ needs.build-frontend.result }}" \
|
||||
"observability=${{ needs.validate-observability.result }}"; do
|
||||
echo "$pair"
|
||||
case "${pair#*=}" in
|
||||
success|skipped) ;;
|
||||
*) ok=0 ;;
|
||||
esac
|
||||
done
|
||||
[ "$ok" = 1 ] || { echo "a changed domain's check failed"; exit 1; }
|
||||
41
.forgejo/workflows/secrets-guard.yml
Normal file
41
.forgejo/workflows/secrets-guard.yml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
name: secrets-guard
|
||||
|
||||
# Fail the build if any infra/deploy/secrets/*.yaml is committed UNENCRYPTED.
|
||||
# SOPS stores a `sops:` metadata block and wraps every value in ENC[...]; a
|
||||
# plaintext leak has neither. This is the backstop against the classic
|
||||
# "committed the decrypted file by accident" incident. .sops.yaml (the
|
||||
# plaintext config) and README.md are not *.yaml under infra/deploy/secrets/,
|
||||
# so they're correctly ignored.
|
||||
#
|
||||
# Deliberately NOT path-filtered: it's a ~1s grep, and a backstop that only
|
||||
# runs when you expect it to isn't a backstop.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [dev, main, release]
|
||||
|
||||
jobs:
|
||||
encrypted:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Assert infra/deploy/secrets/*.yaml are SOPS-encrypted
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
files=(infra/deploy/secrets/*.yaml)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "no infra/deploy/secrets/*.yaml yet — nothing to check"
|
||||
exit 0
|
||||
fi
|
||||
fail=0
|
||||
for f in "${files[@]}"; do
|
||||
if grep -q '^sops:' "$f" && grep -q 'ENC\[AES256_GCM' "$f"; then
|
||||
echo "ok: $f is SOPS-encrypted"
|
||||
else
|
||||
echo "::error file=$f::NOT SOPS-encrypted — refusing to accept a plaintext secrets file"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
exit $fail
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
name: Build check
|
||||
|
||||
# Proves the split Dockerfile actually builds (repo-split Stage 7b) -- NOT a
|
||||
# live boot+healthz check anymore. That was tried and repeatedly hit this
|
||||
# specific runner's own environment quirks (a sibling container's bridge IP
|
||||
# is unreachable from the job container; $$ isn't unique across job
|
||||
# containers; a fixed host port gets permanently squatted by a leftover
|
||||
# the AppArmor bug won't let anything remove; boot time varies a lot under
|
||||
# the runner's shared capacity=2 contention) without ever settling into a
|
||||
# reliably green check. The image DOES boot correctly standalone --
|
||||
# independently verified by hand: `docker run` + real alembic migrations +
|
||||
# `/healthz` returning 200 + a real `/api/v2/place` 200. Full pytest-suite
|
||||
# migration is separate, deferred follow-up work too, same category as
|
||||
# thermograph-copy's deferred vendoring.
|
||||
|
||||
on:
|
||||
# Reusable (uses: ./.forgejo/workflows/build.yml) by pr-build.yml and
|
||||
# deploy-dev.yml for the dev-branch flow, AND still directly triggered on
|
||||
# push/PR to main (unchanged pre-existing behavior) plus dev (new -- so a
|
||||
# direct push to dev or a PR into dev gets this same check without relying
|
||||
# solely on the reusable call from the other two files).
|
||||
workflow_call: {}
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker.io
|
||||
|
||||
- name: Build
|
||||
run: docker build -t thermograph-backend:ci .
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
name: Deploy to LAN dev server
|
||||
|
||||
# Fires on every push to `dev` -- a direct push, or the real merge commit a
|
||||
# native "auto merge when checks succeed" produces (see pr-build.yml). Mirrors
|
||||
# the monorepo's deploy-dev.yml shape: a `build` job (the build.yml sanity
|
||||
# check, reused) followed by a `deploy` job that rolls the LAN dev box.
|
||||
#
|
||||
# Unlike the monorepo (a single repo, so deploy-dev.sh lived in the same
|
||||
# checkout this workflow ran `actions/checkout` on), this repo publishes only
|
||||
# an image (build-push.yml, already triggers on push to dev and tags it
|
||||
# sha-<12hex> in the Forgejo registry) -- deploy-dev.sh itself now lives in
|
||||
# the INFRA repo's checkout on the runner host, at ~/thermograph-dev (it
|
||||
# self-updates there via its own `git reset` against thermograph-infra, same
|
||||
# pattern deploy.sh/deploy-prod.sh already use for main/release). That means
|
||||
# this job is INERT until the thermograph-lan box is reprovisioned from its
|
||||
# current monorepo checkout to an infra checkout -- there is no
|
||||
# ~/thermograph-dev/deploy/deploy-dev.sh on the box yet for this step to run.
|
||||
#
|
||||
# SERVICE=backend + BACKEND_IMAGE_TAG is the same env-var contract deploy.yml/
|
||||
# deploy-prod.yml use against thermograph-infra's deploy.sh, so a dev-only
|
||||
# rollout never touches the frontend's running container or tag, exactly like
|
||||
# the beta/prod paths.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
# Fixed (unparameterized) group, same convention as the deploy job's
|
||||
# below: if two pushes to dev land close together, the newer push's
|
||||
# build cancels the older's still-running one rather than letting both
|
||||
# run to completion on the single physical runner -- the older push's
|
||||
# result would just be thrown away once needs:build gates its deploy
|
||||
# against a stale build anyway. Safe to cancel mid-run: build.yml only
|
||||
# runs a throwaway `docker build` sanity check, nothing persisted.
|
||||
concurrency:
|
||||
group: dev-push-build
|
||||
cancel-in-progress: true
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
# NOT [self-hosted, thermograph-lan] -- that's what the original GitHub
|
||||
# version used, but GitHub implicitly tags every self-hosted runner with
|
||||
# "self-hosted" automatically; Forgejo's runner has only the labels it
|
||||
# was explicitly registered with ("docker" + "thermograph-lan", no
|
||||
# "self-hosted"). The array form silently makes this job unschedulable on
|
||||
# every push -- see the monorepo's deploy-dev.yml, which documents this
|
||||
# same bug against its own history.
|
||||
runs-on: thermograph-lan
|
||||
# Only needs to read the repo to fetch it into the checkout that computes
|
||||
# the tag below -- deploy-dev.sh itself lives in the separate INFRA
|
||||
# checkout on this host, not this repo's checkout.
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: dev-lan-deploy
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Compute image tag
|
||||
id: tag
|
||||
# Same 12-hex truncation deploy.yml/deploy-prod.yml use -- must match
|
||||
# build-push.yml's `sha-${GITHUB_SHA:0:12}` tag exactly (Actions
|
||||
# expressions have no substring function for the full 40-char SHA).
|
||||
run: echo "tag=sha-${GITHUB_SHA:0:12}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy to the LAN dev server
|
||||
# thermograph-lan is host-native (the runner job runs directly on the
|
||||
# LAN box, not over SSH like beta/prod), so plain env vars on the
|
||||
# command reach deploy-dev.sh directly -- no ssh-action needed here.
|
||||
#
|
||||
# INERT until thermograph-lan is reprovisioned: deploy-dev.sh doesn't
|
||||
# exist yet at this path there (the box is currently a monorepo
|
||||
# checkout, not an infra checkout) -- see the file-level comment
|
||||
# above. Once reprovisioned, deploy-dev.sh self-updates via its own
|
||||
# `git reset` against thermograph-infra before running, the same way
|
||||
# deploy.sh/deploy-prod.sh already do for the beta/prod paths.
|
||||
run: SERVICE=backend BACKEND_IMAGE_TAG=${{ steps.tag.outputs.tag }} bash "$HOME/thermograph-dev/deploy/deploy-dev.sh"
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
name: Deploy backend to prod VPS
|
||||
|
||||
# On a push to `release`, SSH to prod and roll ONLY the backend service onto
|
||||
# the image build-push.yml just published for this commit. This is the
|
||||
# release->prod path, distinct from deploy.yml's main->beta path -- same
|
||||
# shape, different branch, different target host, and a completely separate
|
||||
# secret set (PROD_SSH_HOST/USER/KEY/PORT), so a beta credential leak can't
|
||||
# touch prod and vice versa. Frontend is deployed independently by its own
|
||||
# repo's deploy-prod.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 prod SSH user (agent) has full sudo because the SOPS secrets render
|
||||
# sudo-reads the age key on that box, but that's handled inside deploy.sh --
|
||||
# nothing extra is needed here. 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: [release]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: prod-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.PROD_SSH_HOST }}
|
||||
username: ${{ secrets.PROD_SSH_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
port: ${{ secrets.PROD_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 }}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
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 }}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
name: PR build (required check)
|
||||
|
||||
# Every PR into `dev` runs the build check (build.yml's build-only `docker
|
||||
# build` sanity check, reused via `uses:`). Mirrors the monorepo's
|
||||
# pr-build.yml: no custom merge step here -- Forgejo has native branch
|
||||
# protection + auto-merge, so this workflow's only job is to BE the required
|
||||
# status check on PRs targeting `dev`.
|
||||
#
|
||||
# One-time web UI setup (not expressible in this file):
|
||||
# Settings -> Branches -> add a protected-branch rule for `dev`
|
||||
# -> enable "Enable Status Check", list this job's name ("build")
|
||||
# -> merge style: squash
|
||||
# Then on a ready PR: "Auto merge when checks succeed".
|
||||
#
|
||||
# A native auto-merge is a real git merge, so it fires deploy-dev.yml's push
|
||||
# trigger naturally -- no separate deploy job needed here, and no "direct push
|
||||
# vs PR merge" double-trigger to avoid.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
|
||||
concurrency:
|
||||
group: dev-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
name: Build check
|
||||
|
||||
# Proves the split Dockerfile actually builds (repo-split Stage 7b) -- NOT a
|
||||
# live boot+healthz check. content.register() fetches the IndexNow key from
|
||||
# backend at import time with no retry (documented Stage 4 behavior,
|
||||
# unchanged here); without a real, reachable backend this process crashes
|
||||
# immediately on import, by design -- so a standalone boot check would either
|
||||
# be fake (skip the real import path) or require checking out and booting
|
||||
# thermograph-backend too. That's the real fix (a genuine cross-repo
|
||||
# contract-test job, same pattern the original repo-split plan already
|
||||
# scoped for the /cell + ETag contract), not yet built -- flagged as a real
|
||||
# gap, not silently skipped. Full pytest-suite migration is separate,
|
||||
# deferred follow-up work too, same category as thermograph-copy's deferred
|
||||
# vendoring.
|
||||
#
|
||||
# workflow_call added alongside `dev` on both push/pull_request filters
|
||||
# (dev-branch flow, mirroring the monorepo's build.yml): pr-build.yml (PRs
|
||||
# into dev) and deploy-dev.yml (pushes to dev) both `uses:` this file as
|
||||
# their shared build gate, same build-only behavior either way -- direct
|
||||
# push/PR-to-main triggers are unchanged.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
workflow_call: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker.io
|
||||
|
||||
- name: Build
|
||||
run: docker build -t thermograph-frontend:ci .
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
name: Deploy to LAN dev server
|
||||
|
||||
# Fires on every push to `dev` -- a direct push, or the real merge commit a
|
||||
# native "auto merge when checks succeed" produces (see pr-build.yml). Mirrors
|
||||
# the monorepo's .forgejo/workflows/deploy-dev.yml, split to roll only the
|
||||
# frontend service.
|
||||
#
|
||||
# INERT until the LAN dev box is reprovisioned: deploy-dev.sh does not live in
|
||||
# this repo's checkout -- it lives in the INFRA checkout at
|
||||
# $HOME/thermograph-dev on that box (and self-updates there via its own `git
|
||||
# reset` against thermograph-infra), the same way the monorepo's deploy-dev.sh
|
||||
# is invoked. That box is currently provisioned as a thermograph (monorepo)
|
||||
# checkout, not a thermograph-infra one, so until it's reprovisioned this
|
||||
# job's deploy step has nothing to run against and will fail at the shell
|
||||
# call below -- expected, not a bug in this workflow.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
# Fixed (unparameterized) group, same convention as the deploy job's
|
||||
# below: if two pushes to dev land close together, the newer push's
|
||||
# build cancels the older's still-running one rather than letting both
|
||||
# run to completion on the single physical runner -- the older push's
|
||||
# result would just be thrown away once needs:build gates its deploy
|
||||
# against a stale build anyway. Safe to cancel mid-run: build.yml only
|
||||
# proves the Dockerfile builds, nothing persisted.
|
||||
concurrency:
|
||||
group: dev-push-build
|
||||
cancel-in-progress: true
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
# NOT [self-hosted, thermograph-lan] -- GitHub implicitly tags every
|
||||
# self-hosted runner with "self-hosted" automatically; Forgejo's runner
|
||||
# has only the labels it was explicitly registered with ("docker" +
|
||||
# "thermograph-lan", no "self-hosted"). The array form would silently
|
||||
# make this job unschedulable on every push, same gotcha the monorepo's
|
||||
# deploy-dev.yml documents and fixes the same way.
|
||||
runs-on: thermograph-lan
|
||||
# Only needs to read the repo to compute the SHA tag.
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: dev-lan-deploy
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Compute image tag
|
||||
id: tag
|
||||
run: echo "tag=sha-${GITHUB_SHA:0:12}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy to the LAN dev server
|
||||
run: |
|
||||
SERVICE=frontend FRONTEND_IMAGE_TAG=${{ steps.tag.outputs.tag }} \
|
||||
bash "$HOME/thermograph-dev/deploy/deploy-dev.sh"
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
name: Deploy frontend to prod VPS
|
||||
|
||||
# On a push to `release`, SSH to prod and roll ONLY the frontend service onto
|
||||
# the image build-push.yml just published for this commit. This is the
|
||||
# release->prod path, distinct from deploy.yml's main->beta path -- same
|
||||
# shape, different branch, different target host, and a completely separate
|
||||
# secret set (PROD_SSH_HOST/USER/KEY/PORT), so a beta credential leak can't
|
||||
# touch prod and vice versa. Backend is deployed independently by its own
|
||||
# repo's deploy-prod.yml -- that is the whole point of the FE/BE CI-CD split:
|
||||
# a frontend change ships without touching backend 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 prod SSH user (agent) has full sudo because the SOPS secrets render
|
||||
# sudo-reads the age key on that box, but that's handled inside deploy.sh --
|
||||
# nothing extra is needed here. 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.
|
||||
#
|
||||
# Frontend's own register() fetches the IndexNow key from backend at boot, so
|
||||
# deploy.sh waits on a healthy backend before declaring the frontend roll OK
|
||||
# (its compose depends_on already encodes that ordering).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [release]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: prod-deploy-frontend
|
||||
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 frontend over SSH
|
||||
uses: https://github.com/appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.PROD_SSH_HOST }}
|
||||
username: ${{ secrets.PROD_SSH_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
port: ${{ secrets.PROD_SSH_PORT }}
|
||||
# Forward the target service + the image tag to run. deploy.sh reads
|
||||
# FRONTEND_IMAGE_TAG and rolls just `frontend`.
|
||||
envs: SERVICE,FRONTEND_IMAGE_TAG
|
||||
script: /opt/thermograph/deploy/deploy.sh
|
||||
env:
|
||||
SERVICE: frontend
|
||||
FRONTEND_IMAGE_TAG: ${{ steps.tag.outputs.tag }}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
name: Deploy frontend to beta VPS
|
||||
|
||||
# On a push to `main`, SSH to beta and roll ONLY the frontend service onto the
|
||||
# image build-push.yml just published for this commit. Backend is deployed
|
||||
# independently by its own repo's deploy.yml -- that is the whole point of the
|
||||
# FE/BE CI-CD split: a frontend change ships without touching backend 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.
|
||||
#
|
||||
# Frontend's own register() fetches the IndexNow key from backend at boot, so
|
||||
# deploy.sh waits on a healthy backend before declaring the frontend roll OK
|
||||
# (its compose depends_on already encodes that ordering).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: beta-deploy-frontend
|
||||
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 frontend 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
|
||||
# FRONTEND_IMAGE_TAG and rolls just `frontend`.
|
||||
envs: SERVICE,FRONTEND_IMAGE_TAG
|
||||
script: /opt/thermograph/deploy/deploy.sh
|
||||
env:
|
||||
SERVICE: frontend
|
||||
FRONTEND_IMAGE_TAG: ${{ steps.tag.outputs.tag }}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
name: PR build (required check)
|
||||
|
||||
# Every PR into `dev` runs the build check. Mirrors the monorepo's
|
||||
# .forgejo/workflows/pr-build.yml -- Forgejo has native branch protection +
|
||||
# auto-merge, so this workflow's only job is to BE the required status check;
|
||||
# there is no custom merge step here.
|
||||
#
|
||||
# One-time web UI setup (not expressible in this file):
|
||||
# Settings -> Branches -> add a protected-branch rule for `dev`
|
||||
# -> enable "Enable Status Check", list this job's name ("build")
|
||||
# -> merge style: squash
|
||||
# Then on a ready PR: "Auto merge when checks succeed".
|
||||
#
|
||||
# A native auto-merge is a real git merge, so it fires deploy-dev.yml's push
|
||||
# trigger naturally -- no separate deploy job needed here.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
|
||||
concurrency:
|
||||
group: dev-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
uses: ./.forgejo/workflows/build.yml
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
name: secrets-guard
|
||||
|
||||
# Fail the build if any deploy/secrets/*.yaml is committed UNENCRYPTED. SOPS stores a
|
||||
# `sops:` metadata block and wraps every value in ENC[...]; a plaintext leak has
|
||||
# neither. This is the backstop against the classic "committed the decrypted file by
|
||||
# accident" incident. .sops.yaml (the plaintext config) and README.md are not *.yaml
|
||||
# under deploy/secrets/, so they're correctly ignored.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [dev, main, release]
|
||||
|
||||
jobs:
|
||||
encrypted:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Assert deploy/secrets/*.yaml are SOPS-encrypted
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
files=(deploy/secrets/*.yaml)
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "no deploy/secrets/*.yaml yet — nothing to check"
|
||||
exit 0
|
||||
fi
|
||||
fail=0
|
||||
for f in "${files[@]}"; do
|
||||
if grep -q '^sops:' "$f" && grep -q 'ENC\[AES256_GCM' "$f"; then
|
||||
echo "ok: $f is SOPS-encrypted"
|
||||
else
|
||||
echo "::error file=$f::NOT SOPS-encrypted — refusing to accept a plaintext secrets file"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
exit $fail
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
name: Validate observability stack
|
||||
|
||||
# This repo deploys by hand (`docker compose up -d` on beta + the Alloy agent on
|
||||
# each node) with no build step, so nothing caught a malformed compose file, a
|
||||
# broken dashboard JSON, or an unparseable config until it failed live on the
|
||||
# host. This is that missing guard: it parses every YAML/JSON artifact that ships
|
||||
# to a node. It does NOT deploy, and deliberately needs no docker CLI (the
|
||||
# node:20-bookworm job image doesn't ship one) — a YAML parse catches the real
|
||||
# breakage (bad indent / unparseable file) without it.
|
||||
#
|
||||
# Not validated here: the Alloy config (alloy/config.alloy, an HCL-like format,
|
||||
# needs the `alloy` binary) and docker-compose *schema* (unknown-key checks, which
|
||||
# would need the docker CLI). Add either if the churn warrants the extra tooling.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Dashboards are valid JSON
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import json, pathlib, sys
|
||||
bad = False
|
||||
files = sorted(pathlib.Path("grafana/dashboards").glob("*.json"))
|
||||
if not files:
|
||||
print("no dashboards found"); sys.exit(1)
|
||||
for f in files:
|
||||
try:
|
||||
json.loads(f.read_text()); print("OK", f)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("INVALID JSON", f, "-", e); bad = True
|
||||
sys.exit(1 if bad else 0)
|
||||
PY
|
||||
|
||||
- name: YAML artifacts parse (compose + loki + grafana provisioning)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update -qq && apt-get install -y -qq python3-yaml >/dev/null
|
||||
python3 - <<'PY'
|
||||
import pathlib, sys
|
||||
import yaml
|
||||
bad = False
|
||||
targets = [
|
||||
pathlib.Path("docker-compose.yml"),
|
||||
pathlib.Path("alloy/docker-compose.agent.yml"),
|
||||
pathlib.Path("loki/config.yml"),
|
||||
*sorted(pathlib.Path("grafana/provisioning").rglob("*.yml")),
|
||||
]
|
||||
for f in targets:
|
||||
if not f.exists():
|
||||
print("MISSING", f); bad = True; continue
|
||||
try:
|
||||
yaml.safe_load(f.read_text()); print("OK", f)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("INVALID YAML", f, "-", e); bad = True
|
||||
sys.exit(1 if bad else 0)
|
||||
PY
|
||||
Loading…
Reference in a new issue