feat(ci): layered promotion-gated test pipeline (build once → dev → rc → prod) [DRAFT] #6

Open
jmiller wants to merge 1 commits from feat/tiered-test-pipeline into main
Owner

First draft for review. Structural draft of the tiered test pipeline for Template-Go#4. Env-specific wiring is left as TODO in the workflow header; the goal is a well-structured, reviewable design (template repo, not production).

Adds .mokogitea/workflows/test-pipeline.yml — a single workflow implementing the build once → test at dev → promote the tested artifact to rc → gate prod on RC health model from #4. Pairs with the deploy workflows in PR #5 (deploy-dev.yml / deploy-rc.yml / deploy-prod.yml) and reuses their DEV_*/RC_*/PROD_* variables and shared secrets.DEPLOY_SSH_KEY + secrets.MOKOGITEA_TOKEN.

Tier model

Tier Trigger What runs Gate to promote
dev push to dev build the image once + run the full suite (vars.TEST_CMD, default go test ./...) → push the tested artifact → deploy tested image to dev tests green → move dev-tested pointer → eligible for rc
rc push to rc (promote-rc) pull the dev-tested artifact (no rebuild), promote it to the RC registry, deploy that exact digest, run suite + smoke vs the live RC instance suite + smoke green → move rc-healthy pointer → eligible for main
prod push to main verify an rc-healthy artifact exists (fail closed), promote the same digest to prod :version + :latest, deploy RC healthy → deploy prod

Build once, promote the artifact

The promotable artifact is a content-addressed image tag sha-<shortsha> built only at the dev tier. Promotion never rebuilds: rc and prod docker pull the exact tested image and re-tag it, so the bits that ship to prod are the bits that passed dev and rc.

Durable, registry-native promotion pointers carry state across workflow runs:

  • <IMAGE>:dev-tested → digest that passed the dev suite (moved by the dev job)
  • <IMAGE>:rc-healthy → digest that passed rc suite + smoke (moved by the rc job)

Gitea commit statuses pipeline/dev and pipeline/rc are also published per tier (the "status/label the promotion checks read" from #4), so branch-protection / the UI get a green check per tier.

Prod gates on RC, not dev

The prod job fails closed if no rc-healthy artifact exists — an image that never passed the RC tier can't reach prod. This replaces the old "verify dev healthy" gate removed in MokoGitea-Fork#758.

Avoids the Gitea needs-chain scheduler bug

Three independent jobs (dev, rc, prod), each gated by if: on the pushed branch, with no needs: between them. The Gitea Actions scheduler doesn't reliably start the dependent 2nd job of a needs: chain (it stalls in waiting — see MokoGitea-Fork#756, which made the Tests job independent). Each tier is one self-contained job whose steps run sequentially (build → test → promote → deploy); cross-tier ordering is the natural git flow (dev → rc → main), not an in-workflow dependency.

Config

All tier config from vars.*/secrets.*, consistent with PR #5's naming. Pipeline-specific (optional) vars: TEST_CMD (default go test ./...), SMOKE_CMD (default curls RC_HEALTH_URL), GO_VERSION (default: reads go.mod, else stable).

Open questions / assumptions

  • Runner-side promotion vs registry-native: cross-run state uses moving registry tags (dev-tested / rc-healthy). These are last-wins pointers, not per-commit — fine for a linear dev→rc→main flow, but concurrent branches could race. A per-commit coupling (commit-status keyed by SHA) is the alternative; happy to switch if preferred.
  • Deploy handoff: the tiers currently inline the deploy (same ssh/compose/health pattern as PR #5 but docker pull instead of docker build). Alternative is to hand off to deploy-*.yml — but those rebuild from source, which would defeat "promote the tested artifact." Flagged in the header TODO.
  • Where tests run: suite runs on the runner via setup-go (TEST_CMD). Could instead run inside the built image (docker run … go test) for true artifact-parity; noted as a header TODO.
  • Assumes each repo ships a root Dockerfile and the DEV_*/RC_*/PROD_* vars are populated.

Refs #4

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT

> **First draft for review.** Structural draft of the tiered test pipeline for Template-Go#4. Env-specific wiring is left as `TODO` in the workflow header; the goal is a well-structured, reviewable design (template repo, not production). Adds `.mokogitea/workflows/test-pipeline.yml` — a single workflow implementing the **build once → test at dev → promote the tested artifact to rc → gate prod on RC health** model from #4. Pairs with the deploy workflows in **PR #5** (`deploy-dev.yml` / `deploy-rc.yml` / `deploy-prod.yml`) and reuses their `DEV_*`/`RC_*`/`PROD_*` variables and shared `secrets.DEPLOY_SSH_KEY` + `secrets.MOKOGITEA_TOKEN`. ## Tier model | Tier | Trigger | What runs | Gate to promote | |---|---|---|---| | **dev** | push to `dev` | build the image **once** + run the full suite (`vars.TEST_CMD`, default `go test ./...`) → push the tested artifact → deploy tested image to dev | tests green → move `dev-tested` pointer → eligible for rc | | **rc** | push to `rc` (promote-rc) | **pull** the `dev-tested` artifact (no rebuild), promote it to the RC registry, deploy that exact digest, run suite + **smoke** vs the live RC instance | suite + smoke green → move `rc-healthy` pointer → eligible for main | | **prod** | push to `main` | verify an `rc-healthy` artifact exists (**fail closed**), promote the **same digest** to prod `:version` + `:latest`, deploy | RC healthy → deploy prod | ## Build once, promote the artifact The promotable artifact is a content-addressed image tag `sha-<shortsha>` built **only** at the dev tier. Promotion **never rebuilds**: `rc` and `prod` `docker pull` the exact tested image and re-tag it, so the bits that ship to prod are the bits that passed dev **and** rc. Durable, registry-native promotion pointers carry state across workflow runs: - `<IMAGE>:dev-tested` → digest that passed the dev suite (moved by the dev job) - `<IMAGE>:rc-healthy` → digest that passed rc suite + smoke (moved by the rc job) Gitea commit statuses `pipeline/dev` and `pipeline/rc` are also published per tier (the "status/label the promotion checks read" from #4), so branch-protection / the UI get a green check per tier. ## Prod gates on RC, not dev The prod job **fails closed** if no `rc-healthy` artifact exists — an image that never passed the RC tier can't reach prod. This replaces the old "verify dev healthy" gate removed in MokoGitea-Fork#758. ## Avoids the Gitea needs-chain scheduler bug Three **independent** jobs (`dev`, `rc`, `prod`), each gated by `if:` on the pushed branch, with **no `needs:` between them**. The Gitea Actions scheduler doesn't reliably start the dependent 2nd job of a `needs:` chain (it stalls in `waiting` — see MokoGitea-Fork#756, which made the Tests job independent). Each tier is one self-contained job whose steps run sequentially (build → test → promote → deploy); cross-tier ordering is the natural git flow (dev → rc → main), not an in-workflow dependency. ## Config All tier config from `vars.*`/`secrets.*`, consistent with PR #5's naming. Pipeline-specific (optional) vars: `TEST_CMD` (default `go test ./...`), `SMOKE_CMD` (default curls `RC_HEALTH_URL`), `GO_VERSION` (default: reads `go.mod`, else `stable`). ## Open questions / assumptions - **Runner-side promotion vs registry-native:** cross-run state uses moving registry tags (`dev-tested` / `rc-healthy`). These are last-wins pointers, not per-commit — fine for a linear dev→rc→main flow, but concurrent branches could race. A per-commit coupling (commit-status keyed by SHA) is the alternative; happy to switch if preferred. - **Deploy handoff:** the tiers currently inline the deploy (same ssh/compose/health pattern as PR #5 but `docker pull` instead of `docker build`). Alternative is to hand off to `deploy-*.yml` — but those rebuild from source, which would defeat "promote the tested artifact." Flagged in the header TODO. - **Where tests run:** suite runs on the runner via `setup-go` (`TEST_CMD`). Could instead run inside the built image (`docker run … go test`) for true artifact-parity; noted as a header TODO. - Assumes each repo ships a root `Dockerfile` and the `DEV_*`/`RC_*`/`PROD_*` vars are populated. Refs #4 Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
jmiller added 1 commit 2026-07-06 21:18:38 +00:00
feat(ci): layered promotion-gated test pipeline (build once → dev → rc → prod)
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 12s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
0f4b6848a2
Add .mokogitea/workflows/test-pipeline.yml implementing the tiered
"build once, test, promote the artifact" model from Template-Go#4:

- dev  (push dev):  build the image ONCE, run the suite, push the tested
                    artifact, move the `dev-tested` pointer, deploy to dev.
- rc   (push rc):   PULL the dev-tested artifact (no rebuild), promote it to
                    the RC registry, deploy that exact digest, run suite +
                    smoke vs the live RC instance, move `rc-healthy`.
- prod (push main): gate on an `rc-healthy` artifact existing (fail closed),
                    then promote the SAME digest to prod :version + :latest
                    and deploy — replacing the removed "verify dev healthy"
                    gate (MokoGitea-Fork#758).

Three independent `if:`-gated jobs, no `needs:` chain, so it avoids the Gitea
scheduler stall on dependent 2nd jobs (MokoGitea-Fork#756). All tier config is
driven from repo vars.*/secrets.*, reusing the DEV_*/RC_*/PROD_* variables and
shared DEPLOY_SSH_KEY/MOKOGITEA_TOKEN secrets from the deploy workflows (PR#5).
Gitea commit statuses pipeline/dev + pipeline/rc are published per tier.

First draft for review; env-specific wiring left as TODO in the header.

Refs #4

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
Some required checks failed
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 12s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/tiered-test-pipeline:feat/tiered-test-pipeline
git checkout feat/tiered-test-pipeline
Sign in to join this conversation.
No Reviewers
Priority -
Type -
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: MokoConsulting/Template-Go#6