- 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
277 lines
13 KiB
Markdown
277 lines
13 KiB
Markdown
# 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.
|