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

13 KiB
Raw Permalink Blame History

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:

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. 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

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.