Skip to content

Commit 809ef3e

Browse files
committed
tekton: automate releases with Pipelines-as-Code
- Add .tekton/release.yaml for initial releases (branch creation trigger) - Add .tekton/release-patch.yaml for patch releases (incoming webhook trigger) - Add .github/workflows/patch-release.yaml (cron + manual dispatch) - Update tekton/release-cheat-sheet.md to reflect automated workflow Signed-off-by: Shubham Bhardwaj <shubbhar@redhat.com>
1 parent 7f9658b commit 809ef3e

File tree

4 files changed

+445
-123
lines changed

4 files changed

+445
-123
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
name: Patch Release
2+
3+
"on":
4+
workflow_dispatch:
5+
inputs:
6+
branch:
7+
description: "Release branch (e.g. release-v0.3.x)"
8+
required: true
9+
type: string
10+
version:
11+
description: "Version to release (e.g. v0.3.6)"
12+
required: true
13+
type: string
14+
release_as_latest:
15+
description: "Publish as latest release"
16+
required: false
17+
type: boolean
18+
default: true
19+
schedule:
20+
# Weekly on Thursday at 10:00 UTC
21+
- cron: "0 10 * * 4"
22+
23+
env:
24+
PAC_CONTROLLER_URL: "https://pac.infra.tekton.dev"
25+
PAC_REPOSITORY_NAME: "tektoncd-pruner"
26+
# Ignore release branches older than this (major.minor)
27+
MIN_RELEASE_VERSION: "0.3"
28+
29+
jobs:
30+
scan-release-branches:
31+
name: Scan for unreleased commits
32+
if: github.event_name == 'schedule' && github.repository_owner == 'tektoncd'
33+
runs-on: ubuntu-latest
34+
outputs:
35+
matrix: ${{ steps.scan.outputs.matrix }}
36+
has_releases: ${{ steps.scan.outputs.has_releases }}
37+
steps:
38+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
39+
with:
40+
fetch-depth: 0
41+
42+
- name: Scan release branches for new commits
43+
id: scan
44+
run: |
45+
# Determine which release branch is the latest (highest version)
46+
latest_branch=""
47+
latest_major=0
48+
latest_minor=0
49+
for ref in $(git branch -r --list 'origin/release-v*'); do
50+
branch="${ref#origin/}"
51+
if [[ "$branch" =~ release-v([0-9]+)\.([0-9]+)\.x ]]; then
52+
major="${BASH_REMATCH[1]}"
53+
minor="${BASH_REMATCH[2]}"
54+
if [ "$major" -gt "$latest_major" ] || { [ "$major" -eq "$latest_major" ] && [ "$minor" -gt "$latest_minor" ]; }; then
55+
latest_major=$major
56+
latest_minor=$minor
57+
latest_branch=$branch
58+
fi
59+
fi
60+
done
61+
echo "::notice::Latest release branch: ${latest_branch}"
62+
63+
MIN_MAJOR="${MIN_RELEASE_VERSION%%.*}"
64+
MIN_MINOR="${MIN_RELEASE_VERSION##*.}"
65+
66+
releases=()
67+
for ref in $(git branch -r --list 'origin/release-v*'); do
68+
branch="${ref#origin/}"
69+
70+
# Skip branches older than MIN_RELEASE_VERSION
71+
if [[ "$branch" =~ release-v([0-9]+)\.([0-9]+)\.x ]]; then
72+
major="${BASH_REMATCH[1]}"
73+
minor="${BASH_REMATCH[2]}"
74+
if [ "$major" -lt "$MIN_MAJOR" ] || { [ "$major" -eq "$MIN_MAJOR" ] && [ "$minor" -lt "$MIN_MINOR" ]; }; then
75+
echo "::notice::Branch ${branch} is older than v${MIN_RELEASE_VERSION} — skipping"
76+
continue
77+
fi
78+
fi
79+
80+
# Find the latest tag on this branch
81+
last_tag=$(git describe --tags --abbrev=0 --match 'v*' "$ref" 2>/dev/null || echo "")
82+
if [ -z "$last_tag" ]; then
83+
echo "::notice::Branch ${branch} has no tags — skipping (initial release handled by branch creation)"
84+
continue
85+
fi
86+
87+
# Count commits since last tag
88+
new_commits=$(git rev-list "${last_tag}..${ref}" --count)
89+
if [ "$new_commits" -eq 0 ]; then
90+
echo "::notice::Branch ${branch} has no new commits since ${last_tag}"
91+
continue
92+
fi
93+
94+
# Calculate next patch version: v0.3.5 → v0.3.6
95+
next_version=$(echo "$last_tag" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}')
96+
97+
# Only the latest release branch publishes as latest
98+
is_latest="false"
99+
if [ "$branch" = "$latest_branch" ]; then
100+
is_latest="true"
101+
fi
102+
103+
echo "::notice::Branch ${branch}: ${new_commits} new commits since ${last_tag} → ${next_version} (latest=${is_latest})"
104+
releases+=("{\"branch\":\"${branch}\",\"version\":\"${next_version}\",\"release_as_latest\":\"${is_latest}\"}")
105+
done
106+
107+
if [ ${#releases[@]} -eq 0 ]; then
108+
echo "matrix=[]" >> "$GITHUB_OUTPUT"
109+
echo "has_releases=false" >> "$GITHUB_OUTPUT"
110+
else
111+
echo "matrix=[$(IFS=,; echo "${releases[*]}")]" >> "$GITHUB_OUTPUT"
112+
echo "has_releases=true" >> "$GITHUB_OUTPUT"
113+
fi
114+
115+
trigger-scanned-releases:
116+
name: "Trigger ${{ matrix.release.version }} (${{ matrix.release.branch }})"
117+
needs: scan-release-branches
118+
if: needs.scan-release-branches.outputs.has_releases == 'true'
119+
runs-on: ubuntu-latest
120+
strategy:
121+
matrix:
122+
release: ${{ fromJson(needs.scan-release-branches.outputs.matrix) }}
123+
max-parallel: 1
124+
steps:
125+
- name: Trigger PAC incoming webhook
126+
env:
127+
PAC_INCOMING_SECRET: ${{ secrets.PAC_INCOMING_SECRET }}
128+
run: |
129+
echo "::notice::Triggering release ${{ matrix.release.version }} on ${{ matrix.release.branch }} (latest=${{ matrix.release.release_as_latest }})"
130+
curl -sf -X POST "${PAC_CONTROLLER_URL}/incoming" \
131+
-H "Content-Type: application/json" \
132+
-d '{
133+
"repository": "'"${PAC_REPOSITORY_NAME}"'",
134+
"branch": "${{ matrix.release.branch }}",
135+
"pipelinerun": "release-patch",
136+
"secret": "'"${PAC_INCOMING_SECRET}"'",
137+
"params": {
138+
"version": "${{ matrix.release.version }}",
139+
"release_as_latest": "${{ matrix.release.release_as_latest }}"
140+
}
141+
}'
142+
echo "Release triggered successfully"
143+
144+
trigger-manual-release:
145+
name: "Trigger ${{ inputs.version }} (${{ inputs.branch }})"
146+
if: github.event_name == 'workflow_dispatch' && github.repository_owner == 'tektoncd'
147+
runs-on: ubuntu-latest
148+
steps:
149+
- name: Validate inputs
150+
run: |
151+
if [[ ! "${{ inputs.branch }}" =~ ^release-v[0-9]+\.[0-9]+\.x$ ]]; then
152+
echo "::error::Invalid branch format: ${{ inputs.branch }}. Expected: release-vX.Y.x"
153+
exit 1
154+
fi
155+
if [[ ! "${{ inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
156+
echo "::error::Invalid version format: ${{ inputs.version }}. Expected: vX.Y.Z"
157+
exit 1
158+
fi
159+
160+
- name: Trigger PAC incoming webhook
161+
env:
162+
PAC_INCOMING_SECRET: ${{ secrets.PAC_INCOMING_SECRET }}
163+
run: |
164+
echo "::notice::Triggering release ${{ inputs.version }} on ${{ inputs.branch }} (latest=${{ inputs.release_as_latest }})"
165+
curl -sf -X POST "${PAC_CONTROLLER_URL}/incoming" \
166+
-H "Content-Type: application/json" \
167+
-d '{
168+
"repository": "'"${PAC_REPOSITORY_NAME}"'",
169+
"branch": "${{ inputs.branch }}",
170+
"pipelinerun": "release-patch",
171+
"secret": "'"${PAC_INCOMING_SECRET}"'",
172+
"params": {
173+
"version": "${{ inputs.version }}",
174+
"release_as_latest": "${{ inputs.release_as_latest }}"
175+
}
176+
}'
177+
echo "Release triggered successfully"

.tekton/release-patch.yaml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright 2026 The Tekton Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# This PipelineRun is triggered via PAC incoming webhooks for patch
16+
# releases on existing release branches. It is invoked either manually
17+
# (via GitHub Actions workflow_dispatch) or on a cron schedule when
18+
# new commits are detected on a release branch since the last tag.
19+
#
20+
# The version and release_as_latest parameters are passed dynamically
21+
# through the incoming webhook payload.
22+
---
23+
apiVersion: tekton.dev/v1
24+
kind: PipelineRun
25+
metadata:
26+
name: release-patch
27+
annotations:
28+
pipelinesascode.tekton.dev/on-event: "[incoming]"
29+
pipelinesascode.tekton.dev/on-target-branch: "[release-v*]"
30+
pipelinesascode.tekton.dev/pipeline: "tekton/release-pipeline.yaml"
31+
pipelinesascode.tekton.dev/max-keep-runs: "5"
32+
spec:
33+
pipelineRef:
34+
name: pruner-release
35+
params:
36+
- name: package
37+
value: github.com/tektoncd/pruner
38+
- name: repoName
39+
value: pruner
40+
- name: gitRevision
41+
value: "{{ revision }}"
42+
- name: imageRegistry
43+
value: ghcr.io
44+
- name: imageRegistryPath
45+
value: tektoncd/pruner
46+
- name: imageRegistryRegions
47+
value: ""
48+
- name: imageRegistryUser
49+
value: tekton-robot
50+
- name: versionTag
51+
value: "{{ version }}"
52+
- name: releaseBucket
53+
value: tekton-releases
54+
- name: releaseAsLatest
55+
value: "{{ release_as_latest }}"
56+
- name: buildPlatforms
57+
value: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
58+
- name: publishPlatforms
59+
value: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le,windows/amd64
60+
- name: koExtraArgs
61+
value: ""
62+
- name: serviceAccountImagesPath
63+
value: credentials
64+
- name: runTests
65+
value: "true"
66+
timeouts:
67+
pipeline: 3h0m0s
68+
workspaces:
69+
- name: workarea
70+
volumeClaimTemplate:
71+
spec:
72+
accessModes:
73+
- ReadWriteOnce
74+
resources:
75+
requests:
76+
storage: 1Gi
77+
- name: release-secret
78+
secret:
79+
secretName: oci-release-secret
80+
- name: release-images-secret
81+
secret:
82+
secretName: ghcr-creds

.tekton/release.yaml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright 2026 The Tekton Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# This PipelineRun is triggered by Pipelines-as-Code when a release
16+
# branch (release-v*) is first created. It runs the pruner release
17+
# pipeline defined in tekton/release-pipeline.yaml.
18+
#
19+
# The release pipeline builds, publishes multi-arch images, uploads
20+
# artifacts to the release bucket, and reports the release URLs.
21+
---
22+
apiVersion: tekton.dev/v1
23+
kind: PipelineRun
24+
metadata:
25+
name: release-pipeline
26+
annotations:
27+
# Trigger on push events to release branches, but ONLY when the branch
28+
# is first created (body.created == true). This means it fires exactly
29+
# once per release branch, not on every commit pushed to it.
30+
# NOTE: on-cel-expression takes precedence over on-event/on-target-branch,
31+
# so the branch filter MUST be included in the CEL expression.
32+
pipelinesascode.tekton.dev/on-event: "[push]"
33+
pipelinesascode.tekton.dev/on-target-branch: "[refs/heads/release-v*]"
34+
pipelinesascode.tekton.dev/on-cel-expression: |
35+
event == "push" && has(body.created) && body.created == true &&
36+
target_branch.startsWith("release-v")
37+
pipelinesascode.tekton.dev/pipeline: "tekton/release-pipeline.yaml"
38+
pipelinesascode.tekton.dev/max-keep-runs: "5"
39+
spec:
40+
pipelineRef:
41+
name: pruner-release
42+
params:
43+
- name: package
44+
value: github.com/tektoncd/pruner
45+
- name: repoName
46+
value: pruner
47+
- name: gitRevision
48+
value: "{{ revision }}"
49+
- name: imageRegistry
50+
value: ghcr.io
51+
- name: imageRegistryPath
52+
value: tektoncd/pruner
53+
- name: imageRegistryRegions
54+
value: ""
55+
- name: imageRegistryUser
56+
value: tekton-robot
57+
# For initial releases, the version is derived from the branch name:
58+
# release-v0.3.x → v0.3.0
59+
- name: versionTag
60+
value: '{{ cel: pac.target_branch.replace("release-", "").replace(".x", ".0") }}'
61+
- name: releaseBucket
62+
value: tekton-releases
63+
- name: releaseAsLatest
64+
value: "true"
65+
- name: buildPlatforms
66+
value: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
67+
- name: publishPlatforms
68+
value: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le,windows/amd64
69+
- name: koExtraArgs
70+
value: ""
71+
- name: serviceAccountImagesPath
72+
value: credentials
73+
- name: runTests
74+
value: "true"
75+
timeouts:
76+
pipeline: 3h0m0s
77+
workspaces:
78+
- name: workarea
79+
volumeClaimTemplate:
80+
spec:
81+
accessModes:
82+
- ReadWriteOnce
83+
resources:
84+
requests:
85+
storage: 1Gi
86+
- name: release-secret
87+
secret:
88+
secretName: oci-release-secret
89+
- name: release-images-secret
90+
secret:
91+
secretName: ghcr-creds

0 commit comments

Comments
 (0)