ci: collapse the eight deploy and build-push workflows into two #87

Merged
admin_emi merged 1 commit from ci/consolidate-deploys into dev 2026-07-25 07:48:50 +00:00
Owner

Phase 4a — the CI consolidation, deliberately without the branch-model collapse.

Why split 4a from 4b

Two traps made the original single-pass Phase 4 a bad idea:

  1. Renaming a workflow changes its required-status-check context. Branch protection requires PR build (required check) / gate. If that context stops appearing, every PR becomes unmergeable with fully green CI and nothing in the UI says which check is missing — promotion.ts:25 documents this exact incident.
  2. Three open PRs target dev (#62, #59, #34). Collapsing the branch orphans them, and retargeting a base does not re-fire CI.

Neither applies to the deploy and build-push workflows: they are push-triggered only, so they never produce a PR check. This PR takes that win and leaves the branch model completely alone.

What changed

Eight files → two. The six deploy workflows were one file written six times, differing only in a branch name, paths filter, concurrency group, service name, *_IMAGE_TAG variable and secret prefix. The two build-push files differed only in the domain string. deploy.sh's SERVICE + *_IMAGE_TAG contract was already fully parameterised, so the duplication bought nothing.

  • deploy.yml — branch selects environment (main → beta, release → prod), matrix covers backend + frontend, each leg checks whether the push touched its domain before rolling.
  • build-push.yml — same shape for images. Published artefacts are unchanged.
  • The two *-deploy-dev.yml are deleted, not ported. Already documented as inert: they call a monorepo path on the LAN box, whose ~/thermograph-dev is still a split-era thermograph-infra checkout.

15 workflows → 9. 1365 lines → 1019.

Deliberately boring expressions

No dynamic matrix (fromJSON), and the *_IMAGE_TAG selection happens in shell rather than a matrix.service == 'x' && a || b ternary. Those are GitHub idioms a Forgejo/act runner may evaluate differently, and the failure mode here is silent: an empty *_IMAGE_TAG makes deploy.sh fall back to the tag already running, so the job goes green having deployed nothing. Beta and prod get two explicit mutually-exclusive steps rather than a ternary over secrets, where an empty host would be worse still.

Preserved, all load-bearing

fetch-depth: 0 and the domain-keyed 12-hex tag (the branch tip is usually another domain's commit); per-service per-ref concurrency with cancel-in-progress: false; separate PROD_SSH_* credentials so a beta leak can't reach prod; the v*.*.* both-images exception; appleboy/ssh-action by full URL.

Verification

Replayed the plan step against real history:

Push backend frontend
infra-only (#85) changed=false changed=false — nothing rolls
backend-only (#83) changed=true changed=false — sibling untouched
no usable before-sha changed=true changed=true — deploy rather than silently skip

The frontend tag computed for a backend-only push is sha-ca84e0ce95f0exactly the tag live on beta today — so the derivation matches what the old workflows produced.

All 9 workflow files parse. Docs naming the deleted files are updated in the same commit, per the rule the root CLAUDE.md now carries.

Explicitly left alone

pr-build.yml (its name and gate job are the required check), plus secrets-guard.yml and shell-lint.yml — the only other consolidation candidates that run on pull_request. The branch-protection API returned 401 without credentials, so I could not confirm whether their contexts are required. Merging two small files wasn't worth risking an unmergeable repo. That's a follow-up once the required-check list is confirmed.

Follow-up for 4b

promotion.ts hardcodes the monorepo's check name and applies it to every repo — it flagged centralis PR #14 as having an absent required check, yet the merge succeeded, proving centralis' protection doesn't require that context. CENTRALIS_REQUIRED_CHECK should become per-repo alongside the branch-model change.

Phase 4a — the CI consolidation, deliberately **without** the branch-model collapse. ## Why split 4a from 4b Two traps made the original single-pass Phase 4 a bad idea: 1. **Renaming a workflow changes its required-status-check context.** Branch protection requires `PR build (required check) / gate`. If that context stops appearing, every PR becomes unmergeable with fully green CI and nothing in the UI says which check is missing — `promotion.ts:25` documents this exact incident. 2. **Three open PRs target `dev`** (#62, #59, #34). Collapsing the branch orphans them, and retargeting a base does **not** re-fire CI. Neither applies to the deploy and build-push workflows: they are push-triggered only, so they never produce a PR check. This PR takes that win and leaves the branch model completely alone. ## What changed **Eight files → two.** The six deploy workflows were one file written six times, differing only in a branch name, paths filter, concurrency group, service name, `*_IMAGE_TAG` variable and secret prefix. The two build-push files differed only in the domain string. `deploy.sh`'s `SERVICE` + `*_IMAGE_TAG` contract was already fully parameterised, so the duplication bought nothing. - **`deploy.yml`** — branch selects environment (`main` → beta, `release` → prod), matrix covers backend + frontend, each leg checks whether the push touched its domain before rolling. - **`build-push.yml`** — same shape for images. Published artefacts are unchanged. - **The two `*-deploy-dev.yml` are deleted, not ported.** Already documented as inert: they call a monorepo path on the LAN box, whose `~/thermograph-dev` is still a split-era `thermograph-infra` checkout. **15 workflows → 9. 1365 lines → 1019.** ## Deliberately boring expressions No dynamic matrix (`fromJSON`), and the `*_IMAGE_TAG` selection happens in shell rather than a `matrix.service == 'x' && a || b` ternary. Those are GitHub idioms a Forgejo/act runner may evaluate differently, and **the failure mode here is silent**: an empty `*_IMAGE_TAG` makes `deploy.sh` fall back to the tag already running, so the job goes green having deployed nothing. Beta and prod get two explicit mutually-exclusive steps rather than a ternary over `secrets`, where an empty host would be worse still. ## Preserved, all load-bearing `fetch-depth: 0` and the domain-keyed 12-hex tag (the branch tip is usually another domain's commit); per-service per-ref concurrency with `cancel-in-progress: false`; separate `PROD_SSH_*` credentials so a beta leak can't reach prod; the `v*.*.*` both-images exception; `appleboy/ssh-action` by full URL. ## Verification Replayed the plan step against real history: | Push | backend | frontend | |---|---|---| | infra-only (#85) | `changed=false` | `changed=false` — nothing rolls | | backend-only (#83) | `changed=true` | `changed=false` — sibling untouched | | no usable before-sha | `changed=true` | `changed=true` — deploy rather than silently skip | The frontend tag computed for a backend-only push is `sha-ca84e0ce95f0` — **exactly the tag live on beta today** — so the derivation matches what the old workflows produced. All 9 workflow files parse. Docs naming the deleted files are updated in the same commit, per the rule the root `CLAUDE.md` now carries. ## Explicitly left alone `pr-build.yml` (its name and `gate` job *are* the required check), plus `secrets-guard.yml` and `shell-lint.yml` — the only other consolidation candidates that run on `pull_request`. The branch-protection API returned 401 without credentials, so I could not confirm whether their contexts are required. Merging two small files wasn't worth risking an unmergeable repo. That's a follow-up once the required-check list is confirmed. ## Follow-up for 4b `promotion.ts` hardcodes the monorepo's check name and applies it to **every** repo — it flagged centralis PR #14 as having an absent required check, yet the merge succeeded, proving centralis' protection doesn't require that context. `CENTRALIS_REQUIRED_CHECK` should become per-repo alongside the branch-model change.
admin_emi added 1 commit 2026-07-25 07:48:10 +00:00
ci: collapse the eight deploy and build-push workflows into two
All checks were successful
PR build (required check) / changes (pull_request) Successful in 9s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
secrets-guard / encrypted (pull_request) Successful in 7s
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 2s
fc455a0473
The six deploy workflows were one file written six times, differing only in a
branch name, a paths filter, a concurrency group, the service name, its
*_IMAGE_TAG variable and a secret prefix. The two build-push workflows were the
same file twice, differing only in the domain string. The contract into
infra/deploy/deploy.sh (SERVICE + BACKEND_IMAGE_TAG/FRONTEND_IMAGE_TAG) was
already fully parameterised, so the duplication bought nothing and cost eight
files to keep in step.

deploy.yml: branch selects the environment (main -> beta, release -> prod), a
matrix covers backend and frontend, and each leg decides whether this push
actually touched its domain before rolling anything. The workflow-level paths
filter only says backend OR frontend moved; without the per-leg refinement a
backend-only push would also roll the frontend and lose the independent-deploy
property the FE/BE split exists for.

build-push.yml: the same shape for images. Images stay separate and
independently deployable; nothing about the published artefacts changes.

The two *-deploy-dev.yml workflows are deleted rather than ported. They were
already documented as inert -- they call a monorepo path on the LAN box whose
~/thermograph-dev is still a split-era thermograph-infra checkout. LAN dev is a
local `make dev-up` concern, not a CI environment.

Deliberately boring expressions throughout. No dynamic matrix (fromJSON), and
the *_IMAGE_TAG selection is done in shell rather than with a
`matrix.service == 'x' && a || b` ternary. Those are GitHub idioms a Forgejo/act
runner may evaluate differently, and the failure mode is silent: an empty
*_IMAGE_TAG makes deploy.sh fall back to the tag already running, so the job
goes green having deployed nothing. Beta and prod get two explicit, mutually
exclusive steps rather than a ternary over secrets, where an empty host would be
worse still.

Everything load-bearing is preserved: fetch-depth 0 and the domain-keyed 12-hex
tag (the branch tip is often another domain's commit), per-service per-ref
concurrency with cancel-in-progress false, separate PROD_SSH_* credentials, the
v*.*.* both-images exception, and appleboy/ssh-action by full URL.

pr-build.yml is untouched. Its workflow name and `gate` job are the required
status check branch protection is configured against, and renaming that context
makes every PR unmergeable with fully green CI. secrets-guard.yml and
shell-lint.yml are likewise left alone: they are the only other consolidation
candidates that run on pull_request, and the branch-protection API needs
credentials this could not read, so merging them was not worth the risk for two
files.

Logic verified by replaying the plan step against real history: an infra-only
push rolls nothing, a backend-only push rolls backend and leaves frontend
alone, and a run with no usable before-sha defaults to deploying rather than
silently skipping. The computed frontend tag for a backend-only push is
sha-ca84e0ce95f0 -- exactly the tag live on beta -- so the derivation matches
what the old workflows produced.

15 workflows -> 9, 1365 lines -> 1019. Docs naming the deleted files are
updated in the same commit, per the rule the root CLAUDE.md now carries.
admin_emi merged commit d42a57a011 into dev 2026-07-25 07:48:50 +00:00
admin_emi deleted branch ci/consolidate-deploys 2026-07-25 07:48:51 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Jinemi/thermograph#87
No description provided.