forgejo-mcp/docs/design/release-pipeline-migration.md
Christoph Görn 3125eec711
feat: ✨ enable Tekton Chains SLSA v1.0 provenance for release-tools image
- add chains.tekton.dev/transparency-upload annotation to publish PipelineRun
- propagate IMAGE_URL + IMAGE_DIGEST as pipeline-level results for Chains binding
- add IMAGE_URL result to push-image-by-digest task (required for image→provenance bind)
- document cosign verify-attestation --type slsaprovenance in README
- update ADR (release-pipeline-migration.md) with Chains addendum; remove from out-of-scope

Closes forgejo-mcp-46j
2026-05-26 15:06:35 +02:00

277 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR: Migrate Release Workflow to op1st Tekton Pipeline
- Status: Accepted (hard cutover complete)
- Date: 2026-05-25 (initial) / 2026-05-26 (hard cutover)
- Tracking: [forgejo-mcp-d4b](#) (initial migration, closed) → [forgejo-mcp-n85](#) (soft-disable Forgejo trigger, closed) → [forgejo-mcp-gdz](#) (hard cutover, closed) → [forgejo-mcp-td8](#) (canonical CI gate decision, closed)
## Status history
| Date | Status | Change |
|------------|-------------------------|----------------------------------------------------------------------------------------------|
| 2026-05-25 | Proposed | PR #150 merged: Tekton release pipeline + Tasks + ADR + runbook + README verify chapter |
| 2026-05-25 | Accepted (soft-cutover) | `.forgejo/workflows/release.yml` trigger changed from `push: tags: v*` to `workflow_dispatch` only (bead forgejo-mcp-n85). Tekton is now the sole auto-firing release path; Forgejo workflow kept as manual fallback. |
| 2026-05-26 | Accepted (hard cutover) | `.forgejo/workflows/release.yml` and `ci.yml` deleted (commit 1777cca). Tekton is now the sole release and CI path. See addendum below. |
## Context
The forgejo-mcp release is driven by `.forgejo/workflows/release.yml`, a
Forgejo Actions workflow that runs on Codeberg's hosted runner whenever a
`v*` tag is pushed. It performs six jobs in a single linear job:
1. Checkout with full history (for changelog).
2. Install Go via `actions/setup-go`.
3. Install `syft` (CycloneDX SBOM generator) and `cosign` (artifact signer).
4. Run `goreleaser release` against the tag, producing platform archives,
per-archive SBOMs, and a `checksums.txt`.
5. Smoke-test the freshly-built `linux/amd64` binary (`forgejo-mcp version`
must contain the tag).
6. Cosign-sign `checksums.txt` (skipped if `COSIGN_PRIVATE_KEY` unset) and
upload the signature to the Codeberg release as an asset.
7. Build a `.mcpb` (Claude Desktop Extension) per `(goos, goarch)` pair
(linux/darwin × amd64/arm64), version-stamp the bundled `manifest.json`,
and upload each to the Codeberg release.
Parallel to this, the project already uses op1st Tekton (OpenShift
Pipelines + Pipelines-as-Code) for PR and push-to-main CI, registered in
namespace `op1st-pipelines` via the `Repository` CR
`.tekton/repository.yaml`. The open bead **forgejo-mcp-td8** captures the
follow-up decision of which CI gate (Forgejo Actions vs Tekton) becomes
canonical for PRs; release follows the same direction once that lands.
## Decision
Add a Tekton release `PipelineRun` (`.tekton/on-tag-push-release.yaml`)
that mirrors the seven Forgejo Actions steps as three reusable Tasks under
`.tekton/tasks/`:
| Task | Purpose |
|-------------------------------------|--------------------------------------------------|
| `goreleaser-release.yaml` | install syft + goreleaser, run release, smoke-test |
| `cosign-sign-release.yaml` | sign-blob `checksums.txt`, upload `.sig` to release |
| `mcpb-pack.yaml` | pack `.mcpb` per platform, upload to release |
PipelinesAsCode trigger is a tag push:
```yaml
pipelinesascode.tekton.dev/on-event: "[push]"
pipelinesascode.tekton.dev/on-target-branch: "[refs/tags/v*]"
```
The existing `.forgejo/workflows/release.yml` stays untouched. The two
pipelines run in parallel for at least two releases (~2–4 weeks), after
which a follow-up decision (see "Cutover criteria" below) selects the
canonical one.
## Why not …
- **Hard cutover in this PR.** First-time pipeline runs surface secret
wiring bugs, image incompatibilities, and PaC parameter quirks. Losing
a release to those is expensive — the package is consumed by Claude
Desktop users who pin to specific `.mcpb` builds. Parallel-run buys
cheap insurance: the Forgejo workflow still ships the release even if
Tekton fails.
- **Cross-repo `workflow_call` from Forgejo Actions into Tekton.**
Considered but rejected: doubles the moving parts (Forgejo orchestrator
+ Tekton executor), depends on Forgejo Actions remaining the entry
point indefinitely, and the C5 spike (`forgejo-mcp-33k`) for cross-repo
workflow_call on Codeberg's Forgejo v11.x is unverified.
- **Tekton Chains for artifact signing instead of explicit `cosign
sign-blob`.** Tekton Chains is deployed (see
`manifests/applications/op1st-pipelines/tekton-chains.yaml` in
`op1st-emea-b4mad`) and signs `TaskRun` provenance automatically. But
Chains signs the *run record*, not the GoReleaser artifact files
uploaded to the Codeberg release. Users verifying a downloaded archive
expect a `.sig` file matching the existing Forgejo workflow's contract;
preserving that contract requires explicit `sign-blob`. Chains-emitted
provenance is a complementary signal we can publish later — not a
replacement.
- **Single mega-image with all tooling.** Slower image rebuilds, no
cache reuse, and forces the maintainer to own a private registry path.
Per-Task images (`golang:1.25`, `cosign/cosign:v2.4.1`,
`node:22-bookworm`) are upstream-maintained and pinned.
## Secrets
Required in the `op1st-pipelines` namespace. Both already exist
(provisioned upstream — see "Source of truth" below):
| Secret | Type | Keys | Used by |
|-----------------------|---------|--------------------------------------------|-----------------------------------------------------|
| `op1st-release-token` | Opaque | `token`, `username` | GoReleaser publish + Codeberg release asset uploads |
| `cosign-signing-key` | Opaque | `cosign.key`, `cosign.password`, `cosign.pub` | `sign-blob` of `checksums.txt` |
### Source of truth
Neither Secret is created directly in `op1st-pipelines`:
- `op1st-release-token` is auto-mirrored from `op1st-gitops/op1st-release-token`
via the [emberstack reflector](https://github.com/emberstack/kubernetes-reflector).
Rotation happens once in `op1st-gitops`; the reflector propagates to every
consuming namespace within ~seconds.
- `cosign-signing-key` is provisioned alongside Tekton Chains tooling
(matches the `signing-secret` shape used by Chains, with `cosign.key`,
`cosign.password`, `cosign.pub` keys).
The pipeline references `cosign-signing-key` with `optional: true` — if
absent, the signing step warns and exits 0, matching
`.forgejo/workflows/release.yml`'s fail-open behavior.
### Operator-confirmable preconditions
These cannot be validated from inside the cluster (token scopes are not
visible from `oc get secret`). Operator must confirm once on Codeberg UI:
- The PAT stored in `op1st-release-token.token` carries
`write:repository` on `goern/forgejo-mcp` (covers release publish +
asset upload). Read-only PaC tokens like `codeberg-runner-openshift-pac`
do **not** suffice.
- The PAT identity (e.g. `b4mad-release-bot`) is whoever should appear as
release author in the Codeberg UI.
- `cosign-signing-key.cosign.pub` in `op1st-pipelines` matches the
normative public key at
`op1st-emea-b4mad/manifests/applications/op1st-pipelines-tokens/cosign-signing-key.pub`.
Mismatch means `cosign verify-blob` failures for every downstream user
following the README "Verifying Releases" chapter.
## Trigger semantics
PipelinesAsCode resolves the matched ref into `{{ target_branch }}`. For
a push of `refs/tags/v2.24.0` the value passed to the PipelineRun is the
full ref. The `resolve-tag` inline Task strips the `refs/tags/` prefix to
produce a bare tag name (`v2.24.0`) that downstream Tasks use to:
- locate the just-published release via the Codeberg API
(`GET /repos/{owner}/{repo}/releases/tags/{tag}`), and
- derive `VERSION="${TAG#v}"` for filename templating consistent with
`.goreleaser.yml`'s `name_template`.
## Cutover criteria
Promote Tekton to canonical (and remove `.forgejo/workflows/release.yml`)
when **all** of the following hold across two consecutive releases:
1. Tekton release pipeline succeeds end-to-end without manual
intervention.
2. All eight expected assets (4× archive, 4× `.mcpb`, `checksums.txt`,
optional `checksums.txt.sig`, SBOMs) are present on the Codeberg
release and match the Forgejo-produced equivalents byte-for-byte
(where deterministic) or in structure (SBOMs).
3. Wall-clock pipeline duration is within 1.5× of the Forgejo workflow,
measured from tag push to release-complete.
4. Cosign signature verification succeeds against the published
`cosign.pub` for both pipelines' outputs.
Failure modes that defer cutover: PaC tag-push trigger does not fire on
Codeberg's Forgejo v11.x (then we keep `.forgejo/workflows/release.yml`
and revisit when Forgejo lands the relevant fixes); image pulls blocked
from op1st-pipelines (then we mirror to an internal registry first).
## Rollback
Delete `.tekton/on-tag-push-release.yaml`. The three Task files are
inert without a PipelineRun referencing them; leaving them in tree
costs nothing. The Forgejo workflow remains the authoritative release
path until the file is removed in a follow-up commit.
## Addendum: Hard cutover — 2026-05-26
### Decision
Hard cutover executed after **one** fully successful Tekton release (v2.25.1)
rather than the two specified in the original cutover criteria.
`.forgejo/workflows/release.yml` and `.forgejo/workflows/ci.yml` deleted in
commit `1777cca`. Bead `forgejo-mcp-gdz` closed.
### Why one run instead of two
The two-run bar existed to catch infrastructure surprises: secret wiring bugs,
image incompatibilities, PaC parameter quirks. By 2026-05-26 all of those were
resolved:
| Run | Outcome | Blocker resolved by |
|---------|---------|---------------------|
| v2.24.0 | goreleaser exit 127 | release-tools image (forgejo-mcp-1b4) |
| v2.24.1 | goreleaser dirty-tree | GOCACHE/GOMODCACHE outside workspace |
| v2.24.2 | cosign step failed (distroless, no shell) | release-tools image (forgejo-mcp-1b4) |
| v2.25.0 | cosign step failed (`--output-signature` deprecated) | fix in commit `cee6465` |
| v2.25.1 | **all tasks succeeded**, `.sig` present | — first clean end-to-end run |
After v2.25.1 the pipeline was demonstrably stable. No infrastructure surprises
remained. Running a second release solely to satisfy the original count added
delay without adding safety.
Additionally, the security posture improved with this cutover:
- cosign signing key split into `cosign-signing-key-artifacts` (release
artifacts) and `cosign-signing-key-images` (release-tools image), so a
compromised release-tools image can no longer forge artifact signatures
(PR #164, bead forgejo-mcp-j52).
### Updated secrets table
| Secret | Keys | Used by |
|---------------------------------|-----------------------------------------------|------------------------------------------------|
| `op1st-release-token` | `token`, `username` | goreleaser publish + Codeberg release asset uploads |
| `cosign-signing-key-artifacts` | `cosign.key`, `cosign.password`, `cosign.pub` | `sign-blob` of `checksums.txt` in release pipeline |
| `cosign-signing-key-images` | `cosign.key`, `cosign.password`, `cosign.pub` | signing the release-tools container image |
The original `cosign-signing-key` Secret remains in `op1st-pipelines` as a
legacy artefact; it is no longer referenced by any Task.
### Rollback
The Forgejo Actions workflows are deleted. Rollback requires restoring them
from git history (`git show HEAD~1:.forgejo/workflows/release.yml`) and
re-adding the tag trigger. The Tekton pipeline remains in tree and can be
disabled by removing `.tekton/on-tag-push-release.yaml`.
## Out of scope
- Migrating CI (PR + push-to-main) Forgejo Actions to Tekton — tracked
in `forgejo-mcp-td8`. This ADR covers release only.
- Konflux integration for container builds — currently no container
release exists; revisit when one does.
## Addendum: Tekton Chains SLSA provenance — 2026-05-26
### Decision
Enable Tekton Chains SLSA v1.0 provenance attestation for the release-tools
publish PipelineRun (`forgejo-mcp-46j`).
Changes:
1. **`release-tools-on-tag-publish.yaml`** — added
`chains.tekton.dev/transparency-upload: "true"` annotation; added
`IMAGE_URL` and `IMAGE_DIGEST` as pipeline-level results propagated from
`push-by-digest`. Chains reads these results to bind image digest to
provenance.
2. **`push-image-by-digest.yaml`** — added `IMAGE_URL` result (plain
registry URL, no tag/digest) required for Chains image-binding.
3. **`README.md`** — new "Verify SLSA provenance" section under "Verifying
Releases".
### Why
A cosign signature alone attests *signer-holds-key*. Tekton Chains
in-toto SLSA provenance additionally binds image digest → PipelineRun →
git commit → builder identity. This closes the provenance gap identified in
the adversarial review of the release-tools image (L5 finding).
`chains.tekton.dev/transparency-upload: "true"` causes Chains to upload the
attestation envelope to Rekor, making it publicly auditable.
### Verify
```bash
cosign verify-attestation \
--type slsaprovenance \
--key cosign-images.pub \
"codeberg.org/operate-first/release-tools:${TAG}"
```
See README "Verifying Releases → Verify SLSA provenance" for the full
procedure including key fetch.