Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions openspec/changes/trunk-based-ci-cd/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## Context

See `proposal.md` for motivation. Today's model, implemented in `release-train.yml` + `.github/actions/release-train-detect/detect.sh`, treats git tags as the single source of truth for versioning and hardcodes a branch → channel mapping (`develop`→alpha, `staging`→beta, `main`→stable). `docker-release.yml` (called both by `release-train.yml` and by the manual `release.yml` in `legacy` mode) always rebuilds the image from source at release time — there is no existing mechanism to promote an already-built artifact.

No consuming repo has real deploy infrastructure yet, so this design defines the pipeline's shape and the exact point where real deploys will plug in, without implementing those deploys.

## Goals / Non-Goals

**Goals:**

- Decouple git branch topology (single `main`) from release/environment topology (`dev` → `pre` → `prod`).
- Guarantee "build once, promote the same artifact" for the path from `pre` to `prod`.
- Ship this as a purely additive change to `sisques-labs/workflows` so no existing consumer breaks or needs to act.
- Define where real deploy steps will plug in later, without requiring that infra to exist now.

**Non-Goals:**

- Implementing actual deployment logic to real dev/pre/prod infrastructure (no consuming repo has environments provisioned).
- Migrating any specific consuming repo — beacon-api will pilot this as a separate change in its own repository.
- Deprecating or modifying `release-train.yml` for repos that keep using it — that is a later, separate decision once the pilot is validated.
- Auditing which repos have consumers pinned to `:alpha`/`:beta` image tags — required before any given repo migrates, but tracked per-repo, not here.

## Decisions

### D1 — Two-phase versioning: continuous (unversioned) vs. release (versioned)

Every merge to `main` builds and publishes `image:sha-<shortsha>` (+ floating `image:edge`). No `npm version` bump, no git tag, no GitHub Release happens on merge. `package.json` is untouched until a release is explicitly cut.

**Alternatives considered:** computing a semver pre-release (`X.Y.Z-rc.N`) on every merge, mirroring today's alpha/beta channels — rejected because it produces version churn nobody consumes once there is only one branch, and it reintroduces the "which pre-release tag is actually deployed" ambiguity this change is meant to remove.

### D2 — `prod` releases promote an existing image by digest, never rebuild

`docker-release.yml` gains `bump_mode: promote`, which takes the digest of the already-built `sha-*` image (the one validated in `pre`) and runs `docker buildx imagetools create --tag <image>:X.Y.Z --tag <image>:latest <image>@sha256:<digest>`. The existing `legacy` and `release-train` modes (which build-then-push) are untouched — `promote` is a new branch in the existing mode dispatch, not a rewrite.

**Risk to validate during implementation:** `imagetools create` must correctly copy the full multi-arch manifest list (the repo builds `linux/amd64,linux/arm64`), not just a single-platform digest — confirm this before relying on it for a real release.

### D3 — Job graph: `build` → `deploy-dev` → `deploy-pre`, sequential

New reusable workflow `trunk-ci-cd.yml` (`workflow_call`), consumed via a repo's own `push: [main]` trigger:

```
build-and-publish (lint, test, build, push sha-*/edge tags)
deploy-dev (needs: build-and-publish; no gate)
deploy-pre (needs: deploy-dev; gate optional per consumer input)
```

`prod` is deliberately **not** a job in this workflow — it is only reachable via the manual `release.yml` dispatch (D2), which promotes the artifact that has already gone through `dev` and `pre`.

**Alternatives considered:** deploying `dev` and `pre` in parallel from `build` — rejected per the org's explicit decision to keep DEV as an early-feedback gate before promoting to the QA-facing `pre` environment.

### D4 — `deploy-dev`/`deploy-pre` ship as placeholders

Since no consuming repo has dev/pre/prod infrastructure yet, both jobs are defined with a placeholder step (log-only) in this iteration. The job graph, `needs:` ordering, and environment names are the real deliverable; wiring an actual deploy target is follow-up work once a repo provisions real infrastructure.

### D5 — Environments are configured per-repo, not in this shared repo

`dev`/`pre`/`prod` GitHub Environments (and any required-reviewer gates on them) are Settings-level config owned by each consuming repo. `sisques-labs/workflows` only defines the job names/order that reference environment names by convention.

### D6 — Rollout is additive and opt-in per repo

No file this change touches is edited in place for its existing behavior: `trunk-ci-cd.yml` is a new file nothing currently references, and `docker-release.yml`'s new `bump_mode: promote` branch is unreachable unless a caller explicitly passes it. A repo migrates by changing its *own* workflow files to reference the new pieces — `sisques-labs/workflows` never forces a migration.

### D7 — Changelog/release generation is reused, simplified

`docker-release.yml`'s existing git-cliff + GitHub Release steps apply unchanged when `release_type: stable`. Because there is only one channel once a repo migrates, the alpha/beta/rc tag-ignoring logic in the changelog range computation becomes dead code for that repo's future releases but is not removed here (other repos still exercise it via `legacy`/`release-train` modes).

## Risks / Trade-offs

- **[Risk] Multi-arch digest promotion is unproven** → Validate `imagetools create` against a real multi-platform image before beacon-api's pilot relies on it for an actual prod release.
- **[Risk] Downstream consumers pinned to `:alpha`/`:beta` tags** → Each repo must audit this before migrating; out of scope for this shared-workflow change.
- **[Trade-off] `deploy-dev`/`deploy-pre` are placeholders** → The pipeline shape ships now; real deploy logic is deferred until a repo has infrastructure to target.
- **[Trade-off] Org-wide standardization without a forced cutover** → Slower convergence (repos migrate on their own schedule) in exchange for zero blast radius on this change.

## Migration Plan

1. Merge this proposal/design into `sisques-labs/workflows` as documentation — no consumer is affected by merging it.
2. Implement `trunk-ci-cd.yml` and `docker-release.yml`'s `promote` mode per `tasks.md`.
3. Beacon-api pilots the migration in its own repository/change: drop `release-train.yml`, adopt `trunk-ci-cd.yml`, switch `release.yml` to `bump_mode: promote`.
4. Validate the pilot (including the multi-arch promotion risk in D2) before any other repo migrates.
5. Other repos opt in individually, on their own schedule.
6. Deprecating `release-train.yml` org-wide is a separate future decision, made only after enough repos have migrated.

## Open Questions

- Should `deploy-pre` carry a mandatory approval gate by default, or is that left to each repo's own GitHub Environment configuration?
- What is the audit process for repos with external consumers pinned to `:alpha`/`:beta` image tags before they migrate?
29 changes: 29 additions & 0 deletions openspec/changes/trunk-based-ci-cd/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Why

Every consuming repo releases through `release-train.yml`, which maps git branch to release channel (`develop` → alpha, `staging` → beta, `main` → stable) via `release-train-detect/detect.sh`. This couples the org's git topology to its release topology. Today no consuming repo has real environments provisioned, so `develop`/`staging` gate a version channel but not an actual deployment — the three-branch model carries the cost of environment branches (merge overhead, drift risk between branches) without their benefit.

Standardizing on a single long-lived `main` branch (trunk-based development) lets environment promotion be driven by the CI/CD pipeline instead of by branch merges: one artifact is built per merge to `main` and the *same* artifact is promoted through `dev` → `pre` → `prod`, rather than being rebuilt per branch. This removes an entire class of "works in staging, different bytes in prod" risk.

## What Changes

- Add a new reusable workflow `trunk-ci-cd.yml`: triggered by a consumer's `push: [main]`, it builds and publishes an immutable, commit-addressed image (`image:sha-<shortsha>` + floating `image:edge`) with no version bump, then runs `deploy-dev` followed by `deploy-pre` (`needs: deploy-dev`).
- Add a new `bump_mode: promote` to the existing `docker-release.yml` reusable workflow. When set, the release job skips the build step entirely and promotes an already-published image by digest (`docker buildx imagetools create`) to the release tag(s) instead of rebuilding from source — guaranteeing the exact bytes validated in `pre` are what ships to `prod`.
- Both changes are strictly additive: `release-train.yml`, `release-train-detect/detect.sh`, and the existing `legacy`/`release-train` bump modes in `docker-release.yml` are untouched. No consuming repo is affected until it edits its own workflow file to reference `trunk-ci-cd.yml` and pass `bump_mode: promote`.
- **Out of scope for this change**: implementing real deploy steps for `dev`/`pre`/`prod` (no consuming repo has provisioned environments yet — the jobs are defined as the pipeline's shape with a placeholder deploy step); migrating any specific repo (beacon-api will pilot this in its own repo as a follow-up change); deprecating `release-train.yml` for repos that don't migrate (a later decision, once the pilot is validated).

## Capabilities

### New Capabilities

- `trunk-based-ci-cd`: A continuous integration/delivery pipeline that builds an artifact once per merge to `main` and promotes that same artifact through `dev` → `pre` → `prod`, decoupling release/environment topology from git branch topology.

### Modified Capabilities

- `docker-release`: gains a `promote` bump mode that retags an existing image by digest instead of rebuilding, for use when cutting a `prod` release from an already-validated artifact.

## Impact

- **Code**: new file `.github/workflows/trunk-ci-cd.yml`; additive changes to `.github/workflows/docker-release.yml` (new `bump_mode` branch, no existing code path touched). No other file is modified.
- **Consumers**: zero impact on existing repos using `release-train.yml` — nothing here is wired into any consumer's workflow yet.
- **Environments**: `dev`/`pre`/`prod` GitHub Environments are configured per-repo (not part of this shared-workflows change) when a repo migrates and provisions real infrastructure.
- **Rollback**: revert this PR; nothing references the new workflow or bump mode until a consumer opts in.
24 changes: 24 additions & 0 deletions openspec/changes/trunk-based-ci-cd/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## 1. `trunk-ci-cd.yml` reusable workflow

- [ ] 1.1 Add `.github/workflows/trunk-ci-cd.yml` (`workflow_call`) with a `build-and-publish` job: lint, test, build, push `image:sha-<shortsha>` + floating `image:edge` — no version bump, no git tag
- [ ] 1.2 Add `deploy-dev` job (`needs: build-and-publish`) with a placeholder deploy step (no real target yet) — verify it runs unconditionally after a successful build
- [ ] 1.3 Add `deploy-pre` job (`needs: deploy-dev`) with a placeholder deploy step and an optional consumer-provided gate input — verify it never runs before `deploy-dev` succeeds

## 2. `docker-release.yml` promote mode

- [ ] 2.1 Add `bump_mode: promote` as a new accepted value alongside `legacy`/`release-train` — verify existing modes' code paths are untouched
- [ ] 2.2 When `bump_mode: promote`, accept the source image digest as an input and skip the existing build steps entirely
- [ ] 2.3 Implement the promotion step with `docker buildx imagetools create` retagging the digest to the computed release tag(s) + `:latest`
- [ ] 2.4 Verify multi-arch manifest lists (`linux/amd64,linux/arm64`) survive the promotion intact, not just a single-platform digest — this is the flagged risk in `design.md` D2
- [ ] 2.5 Confirm changelog (git-cliff) and GitHub Release creation steps run unchanged for `promote` mode when `release_type: stable`

## 3. Documentation

- [ ] 3.1 Document the new `trunk-ci-cd.yml` inputs/outputs and the `promote` bump mode in this repo's README, alongside the existing `release-train.yml`/`docker-release.yml` docs
- [ ] 3.2 Note in the README that `trunk-ci-cd.yml` is opt-in per repo and does not affect existing `release-train.yml` consumers

## 4. Verification

- [ ] 4.1 Dry-run `trunk-ci-cd.yml` against a disposable test repo/branch to confirm the `build-and-publish` → `deploy-dev` → `deploy-pre` ordering holds
- [ ] 4.2 Dry-run `docker-release.yml` with `bump_mode: promote` against a real multi-arch image and confirm the promoted tag pulls correctly on both architectures
- [ ] 4.3 Confirm no existing repo's `release-train.yml`-based pipeline changed behavior after this PR merges