forgejo-mcp/.tekton/on-tag-push-release.yaml
Christoph Görn 748de5102d
fix: 🐛 push image version tag directly, never delete a staging tag
On Forgejo/Codeberg the container registry deletes the underlying
MANIFEST when any of its tags is deleted. promote-image-tag's
`skopeo delete :build-tmp` cleanup therefore wiped the :vX.Y.Z and
:latest tags that shared the same digest — only the separate cosign
.sig/.att manifests survived. This silently broke the v2.27.0 image
publish (first release to run the delete step added in f026f8c).

Fix (shared by the forgejo-mcp and release-tools publish pipelines):
- push-image-by-digest pushes :vX.Y.Z directly (new TAG param); no
  :build-tmp staging tag is ever created.
- promote-image-tag only ADDS :latest (stable releases); it no longer
  copies :vX.Y.Z or deletes any tag.
- new delete-tag-on-failure task runs in `finally`, gated on
  attach-sbom not succeeding, to remove the briefly-unsigned :vX.Y.Z
  so the fail-closed "never advertise unsigned" guarantee is kept.

Both PipelineRuns validated with `oc create --dry-run=server`.

Refs: forgejo-mcp-9r4
2026-06-02 17:37:45 +02:00

304 lines
12 KiB
YAML
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.

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: forgejo-mcp-on-tag-push-release
annotations:
# PipelinesAsCode: trigger on tag pushes matching v* refs.
# `on-target-branch` is overloaded to match refs/tags/* as well as
# refs/heads/* when on-event is "push" — PaC resolves the ref via
# the underlying provider payload (Codeberg/Forgejo).
pipelinesascode.tekton.dev/on-event: "[push]"
pipelinesascode.tekton.dev/on-target-branch: "[refs/tags/v*]"
# T11 defensive CEL (A1 tag-isolation): PaC's glob [refs/tags/v*] may
# match refs/tags/release-tools/vX.Y.Z on some PaC versions because the
# glob is applied after stripping refs/tags/ and "release-tools/v1.0.0"
# starts with a non-v prefix, but belt-and-suspenders ensures this pipeline
# never fires on a release-tools tag. The expression requires the ref to
# start with refs/tags/v followed by a digit, which excludes
# refs/tags/release-tools/... entirely.
pipelinesascode.tekton.dev/on-cel-expression: |
event == "push" && target_branch.matches("^refs/tags/v[0-9]")
pipelinesascode.tekton.dev/task: "[git-clone, .tekton/tasks/goreleaser-release.yaml, .tekton/tasks/cosign-sign-release.yaml, .tekton/tasks/mcpb-pack.yaml, .tekton/release-tools/tasks/build-image.yaml, .tekton/release-tools/tasks/push-image-by-digest.yaml, .tekton/release-tools/tasks/cosign-sign-image-by-digest.yaml, .tekton/release-tools/tasks/cosign-attach-sbom.yaml, .tekton/release-tools/tasks/promote-image-tag.yaml, .tekton/release-tools/tasks/delete-tag-on-failure.yaml]"
pipelinesascode.tekton.dev/max-keep-runs: "10"
# Releases are not cancellable — let in-flight runs finish even if
# another tag arrives.
pipelinesascode.tekton.dev/cancel-in-progress: "false"
# Tekton Chains: upload in-toto SLSA v1.0 provenance for the published app
# image to the Rekor transparency log. Chains reads the IMAGE_URL +
# IMAGE_DIGEST pipeline results below and attaches a cosign attestation
# binding the image digest → PipelineRun → git commit → builder identity.
# Verify with: cosign verify-attestation --type slsaprovenance \
# --key cosign-images.pub codeberg.org/goern/forgejo-mcp:<tag>
chains.tekton.dev/transparency-upload: "true"
# dn0 — App OCI image publish (decided 2026-06-01):
# Registry: codeberg.org/goern/forgejo-mcp (mirrors the signed-binary
# release namespace). Tags: vMAJOR.MINOR.PATCH + latest (promote-image-tag).
# Single-arch amd64 (reuses release-tools-build-image, which is not a
# manifest-list build). Image build runs in parallel with goreleaser — it
# compiles its own binary inside the Containerfile and only depends on
# resolve-tag.
# Required Secrets in op1st-pipelines (operator-confirmable preconditions):
# - codeberg-pusher (dockerconfigjson — push auth). Its PAT MUST
# carry `write:package` on goern/forgejo-mcp. The cluster cannot validate
# token scope; confirm once via the Codeberg UI before the first vX.Y.Z
# tag. The release-tools image proved this Secret can push to codeberg.org,
# but that auth targets the operate-first org — package-write on goern is
# a SEPARATE grant that must be present on the same PAT identity.
# - cosign-signing-key-images (cosign.key/.password — optional: false,
# fail-closed). A missing key fails the run; no tag is ever promoted.
spec:
params:
- name: repo_url
value: "{{ repo_url }}"
- name: revision
value: "{{ revision }}"
- name: target_branch
value: "{{ target_branch }}"
pipelineSpec:
params:
- name: repo_url
- name: revision
- name: target_branch
description: |
PaC passes the matched ref here (e.g. refs/tags/v2.24.0). Stripped
to the bare tag name (v2.24.0) for downstream tasks.
results:
# Tekton Chains reads IMAGE_URL + IMAGE_DIGEST at the Pipeline level to
# bind the published app image to the SLSA provenance attestation it
# generates. Without these, Chains produces subject-less provenance that
# cannot be verified against a specific image digest.
- name: IMAGE_URL
value: $(tasks.push-image-by-digest.results.IMAGE_URL)
- name: IMAGE_DIGEST
value: $(tasks.push-image-by-digest.results.IMAGE_DIGEST)
workspaces:
- name: source
- name: registry-credentials
description: |
Kubernetes Secret containing .dockerconfigjson (or config.json) with
push credentials for codeberg.org/goern/forgejo-mcp. Used by the
image push, cosign-sign, SBOM-attach, and tag-promote tasks.
tasks:
- name: fetch-source
taskRef:
name: git-clone
workspaces:
- name: output
workspace: source
params:
- name: url
value: $(params.repo_url)
- name: revision
value: $(params.revision)
- name: resolve-tag
runAfter:
- fetch-source
params:
- name: target_branch
value: $(params.target_branch)
taskSpec:
params:
- name: target_branch
results:
- name: tag
description: Bare tag name (e.g. v2.24.0).
steps:
- name: resolve
image: registry.access.redhat.com/ubi9/ubi-minimal:latest
script: |
#!/usr/bin/env sh
set -eu
TAG="$(params.target_branch)"
TAG="${TAG#refs/tags/}"
echo "Resolved tag: $TAG"
printf '%s' "$TAG" > "$(results.tag.path)"
- name: goreleaser
runAfter:
- resolve-tag
taskRef:
name: goreleaser-release
workspaces:
- name: source
workspace: source
params:
- name: TAG
value: $(tasks.resolve-tag.results.tag)
- name: cosign-sign
runAfter:
- goreleaser
taskRef:
name: cosign-sign-release
workspaces:
- name: source
workspace: source
params:
- name: TAG
value: $(tasks.resolve-tag.results.tag)
- name: REPO
value: goern/forgejo-mcp
- name: mcpb-pack
runAfter:
- goreleaser
taskRef:
name: mcpb-pack
workspaces:
- name: source
workspace: source
params:
- name: TAG
value: $(tasks.resolve-tag.results.tag)
- name: REPO
value: goern/forgejo-mcp
# dn0 — App OCI image (codeberg.org/goern/forgejo-mcp). Reuses the
# release-tools image tasks. The Containerfile compiles its own binary, so
# it is logically independent of goreleaser. It MUST NOT run in parallel
# with the goreleaser-family tasks, however: build-image runs buildah
# privileged-as-root and writes its ~1GB store (.buildah) into the SHARED
# `source` PVC, while goreleaser/cosign-sign/mcpb-pack read+write the same
# volume as uid 1000. On the single-node topolvm cluster this concurrency
# corrupted goreleaser's view of the workspace — the first release that
# added this task failed every time goreleaser ran beside buildah
# (compile fork/exec EACCES, then `error reading .git`). Gating on all
# three goreleaser-family tasks gives buildah the PVC to itself.
- name: build-image
runAfter:
- goreleaser
- cosign-sign
- mcpb-pack
taskRef:
name: release-tools-build-image
workspaces:
- name: source
workspace: source
params:
- name: IMAGE
value: "forgejo-mcp:$(tasks.resolve-tag.results.tag)"
- name: CONTEXT
value: "."
- name: CONTAINERFILE
value: Containerfile
# Inject the release version so the in-image binary reports the tag
# instead of the Containerfile's ARG VERSION=dev default.
- name: BUILD_ARGS
value: "VERSION=$(tasks.resolve-tag.results.tag)"
# bd forgejo-mcp-9r4: push the :vX.Y.Z tag directly (no :build-tmp
# staging tag). It is signed in place by cosign-sign next; the finally
# block deletes it if signing/attestation fail.
- name: push-image-by-digest
runAfter:
- build-image
taskRef:
name: release-tools-push-image-by-digest
workspaces:
- name: source
workspace: source
- name: registry-credentials
workspace: registry-credentials
params:
- name: IMAGE
value: "forgejo-mcp:$(tasks.resolve-tag.results.tag)"
- name: REGISTRY_IMAGE
value: "codeberg.org/goern/forgejo-mcp"
- name: TAG
value: $(tasks.resolve-tag.results.tag)
# Fail-closed: cosign-signing-key-images is optional: false in the task.
# If absent, this fails and no human-readable tag is promoted.
- name: cosign-sign-image
runAfter:
- push-image-by-digest
taskRef:
name: release-tools-cosign-sign-image-by-digest
workspaces:
- name: registry-credentials
workspace: registry-credentials
params:
- name: IMAGE_REPO_DIGEST
value: $(tasks.push-image-by-digest.results.IMAGE_REPO_DIGEST)
- name: attach-image-sbom
runAfter:
- cosign-sign-image
taskRef:
name: release-tools-cosign-attach-sbom
workspaces:
- name: source
workspace: source
- name: registry-credentials
workspace: registry-credentials
params:
- name: IMAGE_REPO_DIGEST
value: $(tasks.push-image-by-digest.results.IMAGE_REPO_DIGEST)
# Add :latest ONLY after sign + SBOM succeed (skipped for pre-releases).
# The :vX.Y.Z tag was already pushed + signed upstream; this no longer
# writes it or deletes any staging tag (bd forgejo-mcp-9r4).
- name: promote-image-tag
runAfter:
- push-image-by-digest
- cosign-sign-image
- attach-image-sbom
taskRef:
name: release-tools-promote-image-tag
workspaces:
- name: registry-credentials
workspace: registry-credentials
params:
- name: IMAGE_REPO_DIGEST
value: $(tasks.push-image-by-digest.results.IMAGE_REPO_DIGEST)
- name: TAG
value: $(tasks.resolve-tag.results.tag)
- name: REGISTRY_IMAGE
value: "codeberg.org/goern/forgejo-mcp"
# Fail-closed cleanup (bd forgejo-mcp-9r4): if attach-image-sbom did not
# succeed — which also covers a skipped attach when cosign-sign failed
# first — the :vX.Y.Z tag was pushed but never signed/attested. Delete it
# so no unsigned image stays advertised. :latest is not yet promoted at
# that point, so :vX.Y.Z is the manifest's only tag and the delete is safe.
finally:
- name: delete-unsigned-tag
when:
- input: "$(tasks.attach-image-sbom.status)"
operator: notin
values: ["Succeeded"]
taskRef:
name: release-tools-delete-tag-on-failure
workspaces:
- name: registry-credentials
workspace: registry-credentials
params:
- name: REGISTRY_IMAGE
value: "codeberg.org/goern/forgejo-mcp"
- name: TAG
value: $(tasks.resolve-tag.results.tag)
workspaces:
- name: source
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
# Release builds need ~6× CI: cross-compiled binaries +
# SBOMs + checksums + .mcpb bundles per platform. dn0 adds a
# container image build (buildah store + layers) and a syft
# image SBOM scan into the same workspace — bumped 8Gi → 12Gi.
storage: 12Gi
- name: registry-credentials
secret:
# dn0 (decided 2026-06-01): push to codeberg.org/goern/forgejo-mcp.
# Reuse the existing codeberg-pusher dockerconfigjson Secret in
# op1st-pipelines. Its auth covers codeberg.org, but for push to the
# goern namespace to succeed the underlying PAT MUST carry
# `write:package` on goern/forgejo-mcp — a separate grant from the
# operate-first package-write the release-tools image relies on.
# Operator confirms out-of-band (cluster cannot validate token scope).
secretName: codeberg-pusher