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