Skip to content

Commit 41da075

Browse files
Automate Release workflow update (#741)
* Update auto-tagging * Update generate-release-notes * address co-pilot comments * Add todo * Enable patch to stable version release even if no commits are there * Add new contributor handle * Update regex to handle postN tags
1 parent 40a23f8 commit 41da075

6 files changed

Lines changed: 328 additions & 149 deletions

File tree

.github/workflows/release.yml

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ permissions:
2323
id-token: write # Required for Google Auth
2424

2525
jobs:
26-
promote:
27-
name: Promote Images
26+
tag-release:
27+
name: Tag Release
2828
runs-on: ubuntu-latest
2929
environment: release
30-
timeout-minutes: 180
3130
outputs:
32-
pr_url: ${{ steps.extract-pr.outputs.pr_url }}
3331
run_release: ${{ steps.set-tag.outputs.run_release || steps.auto-tag.outputs.run_release }}
3432
new_tag: ${{ steps.set-tag.outputs.new_tag || steps.auto-tag.outputs.new_tag }}
3533

@@ -47,6 +45,57 @@ jobs:
4745
with:
4846
go-version-file: "go.mod"
4947

48+
- name: Setup Git Identity
49+
run: |
50+
# Use the standard GitHub Actions Bot identity
51+
git config --global user.name "github-actions[bot]"
52+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
53+
54+
- name: Set Tag from Input
55+
if: github.event.inputs.tag != ''
56+
id: set-tag
57+
run: |
58+
echo "run_release=true" >> "$GITHUB_OUTPUT"
59+
echo "new_tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
60+
61+
- name: Auto Tag if New Commits
62+
if: github.event.inputs.tag == ''
63+
id: auto-tag
64+
run: |
65+
./dev/tools/auto-tag
66+
67+
- name: Create and Push Git Tag
68+
if: steps.set-tag.outputs.run_release == 'true' || steps.auto-tag.outputs.run_release == 'true'
69+
env:
70+
GH_TOKEN: ${{ secrets.K8S_IO_PAT }}
71+
run: |
72+
TAG="${{ steps.set-tag.outputs.new_tag || steps.auto-tag.outputs.new_tag }}"
73+
make release-promote TAG="$TAG" ONLY_TAGGING=true
74+
75+
promote-images:
76+
name: Promote Images
77+
needs: tag-release
78+
if: needs.tag-release.outputs.run_release == 'true'
79+
runs-on: ubuntu-latest
80+
environment: release
81+
timeout-minutes: 180
82+
outputs:
83+
pr_url: ${{ steps.extract-pr.outputs.pr_url }}
84+
run_release: ${{ needs.tag-release.outputs.run_release }}
85+
new_tag: ${{ needs.tag-release.outputs.new_tag }}
86+
87+
steps:
88+
- name: Checkout agent-sandbox
89+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet:actions/checkout@v4
90+
with:
91+
fetch-depth: 0
92+
token: ${{ secrets.GH_AUTOMATION_PAT }}
93+
94+
- name: Set up Go
95+
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # ratchet:actions/setup-go@v5
96+
with:
97+
go-version-file: "go.mod"
98+
5099
- name: Clone k8s.io
51100
env:
52101
# Use a Fine-Grained PAT scoped to kubernetes/k8s.io with contents:write and pull_requests:write
@@ -78,21 +127,7 @@ jobs:
78127
git config --global user.name "github-actions[bot]"
79128
git config --global user.email "github-actions[bot]@users.noreply.github.com"
80129
81-
- name: Set Tag from Input
82-
if: github.event.inputs.tag != ''
83-
id: set-tag
84-
run: |
85-
echo "run_release=true" >> "$GITHUB_OUTPUT"
86-
echo "new_tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
87-
88-
- name: Auto Tag if New Commits
89-
if: github.event.inputs.tag == ''
90-
id: auto-tag
91-
run: |
92-
./dev/tools/auto-tag
93-
94130
- name: Run Promotion Script
95-
if: steps.set-tag.outputs.run_release == 'true' || steps.auto-tag.outputs.run_release == 'true'
96131
id: extract-pr
97132
env:
98133
# Use a Fine-Grained PAT scoped to kubernetes/k8s.io with contents:write and pull_requests:write
@@ -103,10 +138,10 @@ jobs:
103138
# Update the git remote for the fork to use the FORK_TOKEN
104139
GH_USER=$(gh api user --jq .login)
105140
git -C ../k8s.io remote set-url origin "https://x-access-token:${FORK_TOKEN}@github.com/$GH_USER/k8s.io.git"
106-
107-
# Run promotion script
108-
TAG="${{ steps.set-tag.outputs.new_tag || steps.auto-tag.outputs.new_tag }}"
109-
make release-promote TAG="$TAG" K8S_IO_DIR=../k8s.io REMOTE_UPSTREAM=upstream REMOTE_FORK=origin
141+
142+
# Run promotion script skipping tag creation/push
143+
TAG="${{ needs.tag-release.outputs.new_tag }}"
144+
make release-promote TAG="$TAG" K8S_IO_DIR=../k8s.io REMOTE_UPSTREAM=upstream REMOTE_FORK=origin SKIP_TAGGING=true
110145
111146
# Read the PR URL from the file generated by the script
112147
if [ ! -f promote_pr.url ]; then
@@ -121,8 +156,8 @@ jobs:
121156
# Restart release from this step if the job times out
122157
poll-merge:
123158
name: Poll PR Status
124-
needs: promote
125-
if: needs.promote.outputs.run_release == 'true'
159+
needs: promote-images
160+
if: needs.promote-images.outputs.run_release == 'true'
126161
runs-on: ubuntu-latest
127162
environment: release
128163
timeout-minutes: ${{ fromJSON(github.event.inputs.poll_timeout_mins || '360') > 360 && 360 || fromJSON(github.event.inputs.poll_timeout_mins || '360') }}
@@ -132,7 +167,7 @@ jobs:
132167
env:
133168
# Use a Fine-Grained PAT scoped to kubernetes/k8s.io with contents:write and pull_requests:write
134169
GH_TOKEN: ${{ secrets.K8S_IO_PAT }}
135-
PR_URL: ${{ needs.promote.outputs.pr_url }}
170+
PR_URL: ${{ needs.promote-images.outputs.pr_url }}
136171
run: |
137172
echo "Waiting for $PR_URL to be merged..."
138173
@@ -167,16 +202,16 @@ jobs:
167202
168203
publish-draft:
169204
name: Publish Draft Release
170-
needs: [promote, poll-merge]
171-
if: needs.promote.outputs.run_release == 'true'
205+
needs: [promote-images, poll-merge]
206+
if: needs.promote-images.outputs.run_release == 'true'
172207
runs-on: ubuntu-latest
173208
environment: release
174209

175210
steps:
176211
- name: Checkout
177212
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2
178213
with:
179-
ref: refs/tags/${{ needs.promote.outputs.new_tag }}
214+
ref: refs/tags/${{ needs.promote-images.outputs.new_tag }}
180215
fetch-depth: 0
181216

182217
- name: Set up Go
@@ -196,4 +231,4 @@ jobs:
196231
GEMINI_MODEL: gemini-2.5-flash
197232
run: |
198233
# This finally publishes the draft for your review
199-
make release-publish TAG="${{ needs.promote.outputs.new_tag }}"
234+
make release-publish TAG="${{ needs.promote-images.outputs.new_tag }}"

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ GEMINI_MODEL ?= gemini-2.5-flash
8989
.PHONY: release-promote
9090
release-promote:
9191
@if [ -z "$(TAG)" ]; then echo "TAG is required (e.g., make release-promote TAG=vX.Y.Z)"; exit 1; fi
92-
./dev/tools/tag-promote-images --tag=${TAG} --k8s-io-dir=${K8S_IO_DIR} --upstream-remote=${REMOTE_UPSTREAM} --fork-remote=${REMOTE_FORK}
92+
./dev/tools/tag-promote-images --tag=${TAG} --k8s-io-dir=${K8S_IO_DIR} --upstream-remote=${REMOTE_UPSTREAM} --fork-remote=${REMOTE_FORK} $(if $(filter true,$(SKIP_TAGGING)),--skip-tagging) $(if $(filter true,$(ONLY_TAGGING)),--only-tagging)
9393

9494
# Publish a draft release to GitHub
9595
# Usage: make release-publish TAG=vX.Y.Z GEMINI_MODEL=gemini-2.5-flash

dev/tools/auto-tag

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,60 +20,87 @@ import re
2020
# The directory containing this script is automatically in sys.path
2121
from shared.git_ops import run_command
2222

23-
def increment_patch(version_str):
24-
"""Calculates the next release candidate tag.
25-
- If already an RC (e.g., v0.1.0rc1), increments the RC counter (v0.1.0rc2).
26-
- Otherwise, increments the patch version and starts at rc1 (e.g., v0.1.0 -> v0.1.1rc1).
27-
28-
TODO: Remove the rc suffix for official releases before switching to final patch versions.
23+
def parse_version(tag_str):
24+
"""Parses version tag into a sortable tuple: (major, minor, patch, rc_num, raw_tag).
25+
For official releases (non-RC), rc_num is set to 999999 to ensure it sorts higher than RC releases.
2926
"""
30-
rc_match = re.match(r'^v?(\d+\.\d+\.\d+)rc(\d+)$', version_str)
31-
if rc_match:
32-
version = rc_match.group(1)
33-
rc_num = int(rc_match.group(2))
34-
return f"v{version}rc{rc_num + 1}"
35-
36-
clean_version = re.match(r'^v?(\d+\.\d+\.\d+)', version_str)
37-
if clean_version:
38-
version_to_parse = f"v{clean_version.group(1)}"
39-
else:
40-
parts = re.findall(r'\d+', version_str)
41-
while len(parts) < 3:
42-
parts.append("0")
43-
version_to_parse = f"v{'.'.join(parts[:3])}"
44-
45-
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', version_to_parse)
46-
major, minor, patch = map(int, match.groups())
47-
patch += 1
48-
49-
return f"v{major}.{minor}.{patch}rc0"
50-
27+
match = re.match(r'^v?(\d+)\.(\d+)\.(\d+)(?:[-.]?rc\.?(\d+))?(?:\.post\d+)?$', tag_str)
28+
if not match:
29+
return None
30+
major, minor, patch = map(int, match.groups()[:3])
31+
rc_str = match.group(4)
32+
rc_num = int(rc_str) if rc_str is not None else 999999
33+
return (major, minor, patch, rc_num, tag_str)
5134

5235
def main():
36+
# 1. Check for Restart (HEAD already tagged with a valid version tag)
37+
head_tag = run_command(["git", "describe", "--tags", "--exact-match", "HEAD"], capture_output=True, allow_error=True)
38+
if head_tag:
39+
parsed_head = parse_version(head_tag)
40+
if parsed_head:
41+
print(f"⚠️ HEAD is already tagged as {head_tag}. Resuming/restarting release process...")
42+
if "GITHUB_OUTPUT" in os.environ:
43+
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
44+
f.write("run_release=true\n")
45+
f.write(f"new_tag={head_tag}\n")
46+
print(f"SUCCESS: Auto-patched to tag {head_tag}")
47+
sys.exit(0)
48+
5349
print("🔍 Checking for new commits since last release...")
5450

55-
# Get latest tag
56-
# We look for tags matching 'v*'
57-
latest_tag = run_command(["git", "describe", "--tags", "--abbrev=0", "--match=v*"], capture_output=True, allow_error=True)
58-
if not latest_tag:
59-
print("⚠️ Unable to determine latest tag from git describe. Defaulting to v0.1.0 base.")
51+
# 2. Get only the single latest tag reachable from HEAD
52+
latest_tag = run_command(
53+
["git", "describe", "--tags", "--abbrev=0", "--match=v*"],
54+
capture_output=True,
55+
allow_error=True
56+
)
57+
58+
has_latest_tag = bool(latest_tag)
59+
is_rc_to_stable_transition = False
60+
61+
if not has_latest_tag:
62+
print("⚠️ Unable to find any version tags in repository. Defaulting to v0.1.0 base.")
6063
latest_tag = "v0.1.0"
64+
new_tag = "v0.1.0"
65+
else:
66+
print(f"Found latest tag: {latest_tag}")
67+
parsed = parse_version(latest_tag)
68+
if not parsed:
69+
print(f"❌ Error: Failed to parse latest tag: {latest_tag}")
70+
sys.exit(1)
71+
72+
major, minor, patch, rc_num, raw = parsed
73+
74+
if rc_num < 999999: # Highest tag is an RC version (e.g. v0.4.5rc2)
75+
# Targeted check: Does the corresponding stable tag (e.g. v0.4.5) exist on HEAD?
76+
stable_tag_name = f"v{major}.{minor}.{patch}"
77+
stable_exists = run_command(
78+
["git", "tag", "--list", stable_tag_name, "--merged", "HEAD"],
79+
capture_output=True,
80+
allow_error=True
81+
)
82+
if not stable_exists:
83+
new_tag = stable_tag_name
84+
is_rc_to_stable_transition = True
85+
else:
86+
new_tag = f"v{major}.{minor}.{patch + 1}"
87+
else:
88+
new_tag = f"v{major}.{minor}.{patch + 1}"
89+
90+
# 3. Check for commits since latest tag
91+
if has_latest_tag:
92+
commits = run_command(["git", "log", f"{latest_tag}..HEAD", "--oneline"], capture_output=True)
93+
else:
94+
commits = run_command(["git", "log", "HEAD", "--oneline"], capture_output=True)
6195

62-
print(f"Found latest tag: {latest_tag}")
63-
64-
# Check for commits since latest tag
65-
commits = run_command(["git", "log", f"{latest_tag}..HEAD", "--oneline"], capture_output=True)
66-
67-
if not commits:
68-
print("ℹ️ No new commits since last release. Skipping release.")
96+
if not commits and not is_rc_to_stable_transition:
97+
print(f"ℹ️ No new commits since last release ({latest_tag}). Skipping release.")
6998
if "GITHUB_OUTPUT" in os.environ:
7099
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
71100
f.write("run_release=false\n")
72101
sys.exit(0)
73102

74103
print(f"Found new commits since {latest_tag}:\n{commits}")
75-
76-
new_tag = increment_patch(latest_tag)
77104
print(f"🚀 Proposing new tag: {new_tag}")
78105

79106
# Set output for GitHub Actions

0 commit comments

Comments
 (0)