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
174 changes: 174 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: Scheduled Release Automation
Copy link
Copy Markdown
Member

@janetkuo janetkuo Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested the new scripts and the workflow? What tests/verification steps have you done? Would you provide an example release note that would be generated from main?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Added the info in the PR description.

Testing:
Tested the workflow by running it manually on my forked repository with hardcoded image names.

Promote Image job - https://github.com/shrutiyam-glitch/agent-sandbox-trial/actions/runs/24554049773/job/71786142560
Promote PR that was created in k8s repo - kubernetes/k8s.io#9354
Ran make release-publish script on local. Draft release generated - https://github.com/shrutiyam-glitch/agent-sandbox-trial/releases/tag/untagged-d5948cb2c139173f52b7

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see drafts given that it's not public - would you publish https://github.com/shrutiyam-glitch/agent-sandbox-trial/releases/tag/untagged-d5948cb2c139173f52b7?


on:
# Version tag is incremented by patch update
# schedule:
# - cron: '0 6 * * 5' # Every Friday at 06:00 UTC
# For Major/Minor version updates, enter tag manually
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this comment, would you clarify?

Copy link
Copy Markdown
Contributor Author

@shrutiyam-glitch shrutiyam-glitch Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no manual tag is provided, the dev/tools/auto-tag script calculates the next version by incrementing the patch number of the latest existing tag (e.g., v0.1.0 becomes v0.1.1). The workflow then creates and pushes this new tag to the repository.

For Major/Minor version updates (0.1.0->0.2.0 or 1.0.0) - need to enter the tag version in the input field during manual run of the workflow.

workflow_dispatch:
inputs:
tag:
description: "Tag for release (e.g., v0.2.0). Leave blank for auto-patch."
required: false
type: string

permissions:
contents: write # Allows to create and push tags
pull-requests: write
id-token: write # Required for Google Auth

jobs:
promote:
name: Promote Images
runs-on: ubuntu-latest
timeout-minutes: 180
outputs:
pr_url: ${{ steps.extract-pr.outputs.pr_url }}
run_release: ${{ steps.set-tag.outputs.run_release || steps.auto-tag.outputs.run_release }}
new_tag: ${{ steps.set-tag.outputs.new_tag || steps.auto-tag.outputs.new_tag }}

steps:
- name: Checkout agent-sandbox
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet:actions/checkout@v4
with:
fetch-depth: 0
# To ensure pushing the tag during the workflow has the permission to trigger the PyPI publish workflow.
# Github secret has repo access.
token: ${{ secrets.GH_AUTOMATION_PAT }}

- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # ratchet:actions/setup-go@v5
with:
go-version-file: "go.mod"

- name: Clone k8s.io
env:
# Use a Fine-Grained PAT scoped to kubernetes/k8s.io with contents:write and pull_requests:write
GH_TOKEN: ${{ secrets.K8S_IO_PAT }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the PAT uses Fine-Grained Permissions scoped exclusively to the kubernetes/k8s.io repository (or its fork) if possible. Limit access to contents: write and pull_requests: write.

run: |
cd ..
git clone https://github.com/kubernetes/k8s.io.git
cd k8s.io
git remote rename origin upstream
gh repo fork --clone=false || true # Ignore if already exists or fails to add remote if already there
# Ensure origin remote points to the fork
GH_USER=$(gh api user --jq .login)
git remote add origin "https://x-access-token:${{ secrets.K8S_IO_PAT }}@github.com/$GH_USER/k8s.io.git" || git remote set-url origin "https://x-access-token:${{ secrets.K8S_IO_PAT }}@github.com/$GH_USER/k8s.io.git"

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
# workload_identity_provider: '${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}'
# service_account: '${{ secrets.GCP_SERVICE_ACCOUNT }}'

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Setup Git Identity
run: |
# Use the standard GitHub Actions Bot identity
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Set Tag from Input
if: github.event.inputs.tag != ''
id: set-tag
run: |
echo "run_release=true" >> "$GITHUB_OUTPUT"
echo "new_tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"

- name: Auto Tag if New Commits
if: github.event.inputs.tag == ''
id: auto-tag
run: |
./dev/tools/auto-tag

- name: Run Promotion Script
if: steps.set-tag.outputs.run_release == 'true' || steps.auto-tag.outputs.run_release == 'true'
id: extract-pr
env:
# Use a Fine-Grained PAT scoped to kubernetes/k8s.io with contents:write and pull_requests:write
GH_TOKEN: ${{ secrets.K8S_IO_PAT }}
run: |
# Run promotion script
TAG="${{ steps.set-tag.outputs.new_tag || steps.auto-tag.outputs.new_tag }}"
make release-promote TAG="$TAG" K8S_IO_DIR=../k8s.io REMOTE_UPSTREAM=upstream REMOTE_FORK=origin

# Read the PR URL from the file generated by the script
if [ ! -f promote_pr.url ]; then
echo "❌ promote_pr.url file not found. Check make release-promote output."
exit 1
fi

PR_URL=$(cat promote_pr.url)
echo "Found PR: $PR_URL"
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"

# Restart release from this step if the job times out
poll-merge:
name: Poll PR Status
needs: promote
if: needs.promote.outputs.run_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 720 # 12-hour limit for long k8s.io merge cycles
steps:
- name: Wait for PR Merge
env:
# Use a Fine-Grained PAT scoped to kubernetes/k8s.io with contents:write and pull_requests:write
GH_TOKEN: ${{ secrets.K8S_IO_PAT }}
PR_URL: ${{ needs.promote.outputs.pr_url }}
run: |
echo "Waiting for $PR_URL to be merged..."

# 144 attempts, every 5 minutes = 12 hours total
for i in {1..144}; do
# Explicitly check the kubernetes/k8s.io repo context
STATE=$(gh pr view "$PR_URL" --repo kubernetes/k8s.io --json state --jq '.state')

echo "Attempt $i: PR is currently $STATE"

if [ "$STATE" == "MERGED" ]; then
echo "✅ PR merged. Proceeding."
exit 0
elif [ "$STATE" == "CLOSED" ]; then
echo "❌ PR was closed without merging."
exit 1
fi

sleep 300
done

echo "❌ Timed out waiting for merge."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the workflow be resumed manually if timed out, rather than restarting the entire promotion process?

Copy link
Copy Markdown
Contributor Author

@shrutiyam-glitch shrutiyam-glitch Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can be restarted from the job - "poll-merge".

exit 1

publish-draft:
name: Publish Draft Release
needs: [promote, poll-merge]
if: needs.promote.outputs.run_release == 'true'
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet:actions/checkout@v4
Comment thread
shrutiyam-glitch marked this conversation as resolved.
with:
ref: refs/tags/${{ needs.promote.outputs.new_tag }}
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # ratchet:actions/setup-go@v5
with:
go-version-file: "go.mod"

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # ratchet:actions/setup-python@v5
with:
python-version: "3.10"

- name: Generate Draft Release and Manifests
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
run: |
# This finally publishes the draft for your review
make release-publish TAG="${{ needs.promote.outputs.new_tag }}"
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,14 @@ K8S_IO_DIR ?= ../../kubernetes/k8s.io

# Default remote (can be overriden: make release-publish REMOTE=upstream ...)
REMOTE_UPSTREAM ?= upstream
REMOTE_FORK ?= origin

# Promote all staging images to registry.k8s.io
# Usage: make release-promote TAG=vX.Y.Z
.PHONY: release-promote
release-promote:
@if [ -z "$(TAG)" ]; then echo "TAG is required (e.g., make release-promote TAG=vX.Y.Z)"; exit 1; fi
./dev/tools/tag-promote-images --tag=${TAG} --k8s-io-dir=${K8S_IO_DIR}
./dev/tools/tag-promote-images --tag=${TAG} --k8s-io-dir=${K8S_IO_DIR} --upstream-remote=${REMOTE_UPSTREAM} --fork-remote=${REMOTE_FORK}

# Publish a draft release to GitHub
# Usage: make release-publish TAG=vX.Y.Z
Expand Down
68 changes: 68 additions & 0 deletions dev/tools/auto-tag
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
# Copyright 2026 The Kubernetes 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.

import os
import sys
import re

# The directory containing this script is automatically in sys.path
from shared.git_ops import run_command

def increment_patch(version_str):
"""Increments the patch version of a SemVer string (e.g., v0.1.0 -> v0.1.1)."""
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', version_str)
if not match:
print(f"❌ Cannot parse version {version_str} for auto-increment.")
sys.exit(1)
major, minor, patch = map(int, match.groups())
return f"v{major}.{minor}.{patch + 1}"

def main():

print("🔍 Checking for new commits since last release...")

# Get latest tag
# We look for tags matching 'v*'
latest_tag = run_command(["git", "describe", "--tags", "--abbrev=0", "--match=v*"], capture_output=True, allow_error=True)
if not latest_tag:
print("⚠️ Unable to determine latest tag from git describe. Defaulting to v0.1.0 base.")
latest_tag = "v0.1.0"

print(f"Found latest tag: {latest_tag}")

# Check for commits since latest tag
commits = run_command(["git", "log", f"{latest_tag}..HEAD", "--oneline"], capture_output=True)

if not commits:
print("ℹ️ No new commits since last release. Skipping release.")
if "GITHUB_OUTPUT" in os.environ:
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write("run_release=false\n")
sys.exit(0)

print(f"Found new commits since {latest_tag}:\n{commits}")

new_tag = increment_patch(latest_tag)
print(f"🚀 Proposing new tag: {new_tag}")

# Set output for GitHub Actions
if "GITHUB_OUTPUT" in os.environ:
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write("run_release=true\n")
f.write(f"new_tag={new_tag}\n")
print(f"SUCCESS: Auto-patched to tag {new_tag}")

if __name__ == '__main__':
main()
Loading