Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions .github/workflows/patch-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
name: Patch Release

"on":
workflow_dispatch:
inputs:
branch:
description: "Release branch (e.g. release-v0.26.x)"
required: true
type: string
version:
description: "Version to release (e.g. v0.26.4)"
required: true
type: string
release_as_latest:
description: "Publish as latest release"
required: false
type: boolean
default: true
schedule:
# Weekly on Thursday at 10:00 UTC
- cron: "0 10 * * 4"

env:
PAC_CONTROLLER_URL: "https://pac.infra.tekton.dev"
PAC_REPOSITORY_NAME: "tektoncd-chains"
# Ignore release branches older than this (major.minor)
MIN_RELEASE_VERSION: "0.20"

jobs:
scan-release-branches:
name: Scan for unreleased commits
if: github.event_name == 'schedule' && github.repository_owner == 'tektoncd'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.scan.outputs.matrix }}
has_releases: ${{ steps.scan.outputs.has_releases }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

Check notice

Code scanning / zizmor

credential persistence through GitHub Actions artifacts Note

credential persistence through GitHub Actions artifacts
Comment on lines +38 to +40

- name: Scan release branches for new commits
id: scan
run: |
# Determine which release branch is the latest (highest version)
latest_branch=""
latest_major=0
latest_minor=0
for ref in $(git branch -r --list 'origin/release-v*'); do
branch="${ref#origin/}"
if [[ "$branch" =~ release-v([0-9]+)\.([0-9]+)\.x ]]; then
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
if [ "$major" -gt "$latest_major" ] || { [ "$major" -eq "$latest_major" ] && [ "$minor" -gt "$latest_minor" ]; }; then
latest_major=$major
latest_minor=$minor
latest_branch=$branch
fi
fi
done
echo "::notice::Latest release branch: ${latest_branch}"

MIN_MAJOR="${MIN_RELEASE_VERSION%%.*}"
MIN_MINOR="${MIN_RELEASE_VERSION##*.}"

releases=()
for ref in $(git branch -r --list 'origin/release-v*'); do
branch="${ref#origin/}"

# Skip branches older than MIN_RELEASE_VERSION
if [[ "$branch" =~ release-v([0-9]+)\.([0-9]+)\.x ]]; then
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
if [ "$major" -lt "$MIN_MAJOR" ] || { [ "$major" -eq "$MIN_MAJOR" ] && [ "$minor" -lt "$MIN_MINOR" ]; }; then
echo "::notice::Branch ${branch} is older than v${MIN_RELEASE_VERSION} — skipping"
continue
fi
fi

# Find the latest tag on this branch
last_tag=$(git describe --tags --abbrev=0 --match 'v*' "$ref" 2>/dev/null || echo "")
if [ -z "$last_tag" ]; then
echo "::notice::Branch ${branch} has no tags — skipping (initial release handled by branch creation)"
continue
fi

# Count commits since last tag
new_commits=$(git rev-list "${last_tag}..${ref}" --count)
if [ "$new_commits" -eq 0 ]; then
echo "::notice::Branch ${branch} has no new commits since ${last_tag}"
continue
fi

# Calculate next patch version: v0.26.3 → v0.26.4
next_version=$(echo "$last_tag" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}')

# Only the latest release branch publishes as latest
is_latest="false"
if [ "$branch" = "$latest_branch" ]; then
is_latest="true"
fi

echo "::notice::Branch ${branch}: ${new_commits} new commits since ${last_tag} → ${next_version} (latest=${is_latest})"
releases+=("{\"branch\":\"${branch}\",\"version\":\"${next_version}\",\"release_as_latest\":\"${is_latest}\"}")
done

if [ ${#releases[@]} -eq 0 ]; then
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_releases=false" >> "$GITHUB_OUTPUT"
else
echo "matrix=[$(IFS=,; echo "${releases[*]}")]" >> "$GITHUB_OUTPUT"
echo "has_releases=true" >> "$GITHUB_OUTPUT"
fi

Check warning

Code scanning / zizmor

overly broad permissions Warning

overly broad permissions
Comment on lines +30 to +113

trigger-scanned-releases:
name: "Trigger ${{ matrix.release.version }} (${{ matrix.release.branch }})"
needs: scan-release-branches
if: needs.scan-release-branches.outputs.has_releases == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
release: ${{ fromJson(needs.scan-release-branches.outputs.matrix) }}
max-parallel: 1
steps:
- name: Trigger PAC incoming webhook
env:
PAC_INCOMING_SECRET: ${{ secrets.PAC_INCOMING_SECRET }}
run: |
echo "::notice::Triggering release ${{ matrix.release.version }} on ${{ matrix.release.branch }} (latest=${{ matrix.release.release_as_latest }})"
curl -sf -X POST "${PAC_CONTROLLER_URL}/incoming" \
-H "Content-Type: application/json" \
-d '{
"repository": "'"${PAC_REPOSITORY_NAME}"'",
"branch": "${{ matrix.release.branch }}",
"pipelinerun": "release-patch",
"secret": "'"${PAC_INCOMING_SECRET}"'",
"params": {
"version": "${{ matrix.release.version }}",
"release_as_latest": "${{ matrix.release.release_as_latest }}"
}
}'
echo "Release triggered successfully"

Check warning

Code scanning / zizmor

overly broad permissions Warning

overly broad permissions
Comment on lines +115 to +142

trigger-manual-release:
name: "Trigger ${{ inputs.version }} (${{ inputs.branch }})"
if: github.event_name == 'workflow_dispatch' && github.repository_owner == 'tektoncd'
runs-on: ubuntu-latest
steps:
- name: Validate inputs
run: |
if [[ ! "${{ inputs.branch }}" =~ ^release-v[0-9]+\.[0-9]+\.x$ ]]; then

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion
echo "::error::Invalid branch format: ${{ inputs.branch }}. Expected: release-vX.Y.x"

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion
exit 1
fi
if [[ ! "${{ inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion
echo "::error::Invalid version format: ${{ inputs.version }}. Expected: vX.Y.Z"

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion
exit 1
fi

- name: Trigger PAC incoming webhook
env:
PAC_INCOMING_SECRET: ${{ secrets.PAC_INCOMING_SECRET }}
run: |
echo "::notice::Triggering release ${{ inputs.version }} on ${{ inputs.branch }} (latest=${{ inputs.release_as_latest }})"

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion
curl -sf -X POST "${PAC_CONTROLLER_URL}/incoming" \
-H "Content-Type: application/json" \
-d '{
"repository": "'"${PAC_REPOSITORY_NAME}"'",
"branch": "${{ inputs.branch }}",

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion
"pipelinerun": "release-patch",
"secret": "'"${PAC_INCOMING_SECRET}"'",
"params": {
"version": "${{ inputs.version }}",

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion
"release_as_latest": "${{ inputs.release_as_latest }}"
}
}'
echo "Release triggered successfully"
83 changes: 83 additions & 0 deletions .tekton/release-patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2026 The Tekton Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This PipelineRun is triggered via PAC incoming webhooks for patch
# releases on existing release branches. It is invoked either manually
# (via GitHub Actions workflow_dispatch) or on a cron schedule when
# new commits are detected on a release branch since the last tag.
#
# The version and release_as_latest parameters are passed dynamically
# through the incoming webhook payload.
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: release-patch
annotations:
pipelinesascode.tekton.dev/on-event: "[incoming]"
pipelinesascode.tekton.dev/on-target-branch: "[release-v*]"
pipelinesascode.tekton.dev/pipeline: "release/release-pipeline.yaml"
pipelinesascode.tekton.dev/max-keep-runs: "5"
spec:
taskRunTemplate:
serviceAccountName: release
pipelineRef:
name: chains-release
params:
- name: package
value: github.com/tektoncd/chains
- name: repoName
value: chains
- name: gitRevision
value: "{{ revision }}"
- name: imageRegistry
value: ghcr.io
- name: imageRegistryPath
value: tektoncd/chains
- name: imageRegistryRegions
value: ""
- name: imageRegistryUser
value: tekton-robot
- name: versionTag
value: "{{ version }}"
- name: releaseBucket
value: tekton-releases
- name: releaseAsLatest
value: "{{ release_as_latest }}"
- name: buildPlatforms
value: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
- name: publishPlatforms
value: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le,windows/amd64
- name: koExtraArgs
value: "--preserve-import-paths"
- name: serviceAccountImagesPath
value: credentials
- name: runTests
value: "true"
timeouts:
pipeline: 3h0m0s
workspaces:
- name: workarea
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
- name: release-secret
secret:
secretName: oci-release-secret
- name: release-images-secret
secret:
secretName: ghcr-creds
92 changes: 92 additions & 0 deletions .tekton/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copyright 2026 The Tekton Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This PipelineRun is triggered by Pipelines-as-Code when a release
# branch (release-v*) is first created. It runs the chains release
# pipeline defined in release/release-pipeline.yaml.
#
# The release pipeline builds, publishes multi-arch images, uploads
# artifacts to the release bucket, and reports the release URLs.
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: release-pipeline
annotations:
# Trigger on push events to release branches, but ONLY when the branch
# is first created (body.created == true). This means it fires exactly
# once per release branch, not on every commit pushed to it.
# NOTE: on-cel-expression takes precedence over on-event/on-target-branch,
# so the branch filter MUST be included in the CEL expression.
pipelinesascode.tekton.dev/on-event: "[push]"
pipelinesascode.tekton.dev/on-target-branch: "[refs/heads/release-v*]"
pipelinesascode.tekton.dev/on-cel-expression: |
event == "push" && has(body.created) && body.created == true &&
target_branch.startsWith("release-v")
pipelinesascode.tekton.dev/pipeline: "release/release-pipeline.yaml"
pipelinesascode.tekton.dev/max-keep-runs: "5"
spec:
taskRunTemplate:
serviceAccountName: release
pipelineRef:
name: chains-release
params:
- name: package
value: github.com/tektoncd/chains
- name: repoName
value: chains
- name: gitRevision
value: "{{ revision }}"
- name: imageRegistry
value: ghcr.io
- name: imageRegistryPath
value: tektoncd/chains
- name: imageRegistryRegions
value: ""
- name: imageRegistryUser
value: tekton-robot
# For initial releases, the version is derived from the branch name:
# release-v0.26.x → v0.26.0
- name: versionTag
value: '{{ cel: pac.target_branch.replace("release-", "").replace(".x", ".0") }}'
- name: releaseBucket
value: tekton-releases
- name: releaseAsLatest
value: "true"
- name: buildPlatforms
value: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
- name: publishPlatforms
value: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le,windows/amd64
- name: koExtraArgs
value: "--preserve-import-paths"
- name: serviceAccountImagesPath
value: credentials
- name: runTests
value: "true"
timeouts:
pipeline: 3h0m0s
workspaces:
- name: workarea
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
- name: release-secret
secret:
secretName: oci-release-secret
- name: release-images-secret
secret:
secretName: ghcr-creds
Loading
Loading