Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3e0875d
feat(ci): Add release CI job
kramaranya Aug 21, 2025
309db19
feat(ci): Add GitHub Release to CI
kramaranya Aug 21, 2025
03f3078
Change kubeflow sdk PyPI package name
kramaranya Aug 21, 2025
8cad32d
Add kubeflow sdk version verification to CI
kramaranya Aug 22, 2025
6796c33
Add proper changelog extraction
kramaranya Aug 26, 2025
107d448
Allow to reuse test-python workflow
kramaranya Aug 26, 2025
537df55
Add changelog generation script
kramaranya Aug 26, 2025
26852d3
Update uv.lock
kramaranya Aug 26, 2025
8fc6b4e
Remove blank lines in changelog script
kramaranya Aug 27, 2025
371c882
Add make release in Makefile
kramaranya Aug 27, 2025
f7a5856
Refactor gen_changelog
kramaranya Sep 3, 2025
6c51833
Delete CHANGELOG.md
kramaranya Sep 3, 2025
9dbecaa
Add prepare-release CI
kramaranya Sep 3, 2025
3592ce4
Allow manual trigger of release CI
kramaranya Sep 4, 2025
998e696
Add release readme
kramaranya Sep 4, 2025
e3740ac
Update chnagelog parser in CI
kramaranya Sep 4, 2025
fbf3bba
skip and check cherry pick for new release
kramaranya Sep 6, 2025
f3db98d
add script to update the version
kramaranya Sep 6, 2025
64045db
Update make release script
kramaranya Sep 11, 2025
7dea8e3
Use make test-python in release CI
kramaranya Sep 11, 2025
482ccb9
Move prepare release CI to one a single workflow
kramaranya Sep 13, 2025
0e200d9
Remove uv.sync from RELEASE.md
kramaranya Sep 14, 2025
8f56800
Upload artifacts to GitHub Release
kramaranya Sep 14, 2025
7302194
Run release CI against release-* branch
kramaranya Sep 15, 2025
366af93
Use X.Y.Z versioning in Kubeflow SDK
kramaranya Sep 15, 2025
8199dd1
Use single workflow for release
kramaranya Sep 15, 2025
d6d5974
Remove Release types from RELEASE.md
kramaranya Sep 15, 2025
cb17274
Add a note about older minor series patch release
kramaranya Sep 15, 2025
22d8cc8
Directly checkout release branch with ref input
kramaranya Sep 15, 2025
98b1a07
Checkout release branch with ref input
kramaranya Sep 15, 2025
04e4b9b
Update extract changelog job
kramaranya Sep 15, 2025
7a98c0c
Update the name of github release
kramaranya Sep 15, 2025
2057a66
Remove full changelog line
kramaranya Sep 15, 2025
74de8ca
Fix ruff issue
kramaranya Sep 15, 2025
ed4363d
Don't use link for authors in changelog
kramaranya Sep 15, 2025
290b2a4
Remove chnagelog for RCs
kramaranya Sep 16, 2025
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
211 changes: 211 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
name: Release

on:
push:
branches:
- main
- 'release-*'
paths:
- 'kubeflow/__init__.py'
workflow_dispatch: {}

permissions:
contents: write
id-token: write

jobs:
prepare:
name: Prepare release branch
if: ${{ !(github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release-') && github.actor == 'github-actions[bot]') }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.vars.outputs.version }}
branch: ${{ steps.vars.outputs.branch }}
is-prerelease: ${{ steps.vars.outputs.is-prerelease }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Configure git user
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Check version and branch
id: vars
run: |
VERSION=$(sed -n 's/^__version__ = "\(.*\)"/\1/p' kubeflow/__init__.py)
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
BRANCH=release-$MAJOR_MINOR
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
if [[ "$VERSION" =~ rc[0-9]+$ ]]; then
echo "is-prerelease=true" >> $GITHUB_OUTPUT
else
echo "is-prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Ensure release branch exists and contains version bump
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.version }}"
BRANCH="${{ steps.vars.outputs.branch }}"
MAIN_SHA="${{ github.sha }}"
if [[ "${GITHUB_REF_NAME}" == "$BRANCH" ]]; then
echo "Triggered on $BRANCH. Skipping cherry-pick from main."
exit 0
fi
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
echo "Using existing branch: $BRANCH"
git fetch origin "$BRANCH":"$BRANCH"
git checkout "$BRANCH"
if git merge-base --is-ancestor "$MAIN_SHA" "$BRANCH"; then
echo "Commit $MAIN_SHA already present in $BRANCH. Skipping cherry-pick."
else
if ! git cherry-pick -x "$MAIN_SHA"; then
echo "Cherry-pick failed. Please resolve manually on $BRANCH." >&2
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a use-case when we should manually resolve cherry-pick ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Generally no, but if that happens it shouldn't be a problem cause we can always rerun the workflow

exit 1
fi
fi
Comment on lines +67 to +74
Copy link
Member

Choose a reason for hiding this comment

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

Would that work if we want to create a new patch release for previous releases ?
E.g. if the main branch has the latest 0.3.0 release, but we want to make 0.1.4 release at release-0.1 branch.

I imagine a use-case when we want to make a new patch release in release branch, and we should not update the SDK version in the main branch.

Also, I can see that in Kueue they manually update the changelog in the main branch after the new patch release: kubernetes-sigs/kueue#6810

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IIUC we never update the version on the main branch for patch releases, is that right? So whenever we do a patch release, we update the version and changelog on the release branch and then cherry-pick the changelog to main? If so, I'll update the CI accordingly

Copy link
Member

@andreyvelich andreyvelich Sep 14, 2025

Choose a reason for hiding this comment

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

I think the main branch should always correspond to the latest official release (including the most recent patch to the latest release).
The only exception is when we need to publish a patch release for a previous minor version (e.g., X.Y-1). In that case, we should update the __version__ in the corresponding release-X.Y-1 branch.

Does it make sense @kramaranya ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I see what you mean, thank you for the explanation! Let me update the CI for this case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I addressed this in 7302194

else
echo "Creating new branch: $BRANCH from main@$MAIN_SHA"
git checkout -B "$BRANCH" "$MAIN_SHA"
fi
git push origin "$BRANCH"
build:
name: Build package
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.prepare.outputs.branch }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Setup build environment
run: |
make verify
Copy link
Member

Choose a reason for hiding this comment

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

Is that fine that we install SDK in the dev mode before running uv build and uv twine check ?

sdk/Makefile

Line 54 in 482ccb9

verify: install-dev ## install all required tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That shouldn't cause any issues since uv build ignores dev dependencies anyway. We're already using test-python with uv sync. If needed we could duplicate the makefile commands with uv sync --no-dev, but since we can't introduce breaking changes to the trainer API models, we should be fine just using uv sync, WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

If uv build ignores dev dependencies that should be fine.

- name: Run unit tests
run: |
make test-python
- name: Verify version
run: |
TAG_VERSION="${{ needs.prepare.outputs.version }}"
CODE_VERSION="$(python -c "import kubeflow; print(kubeflow.__version__)")"
echo "Tag version: $TAG_VERSION"
echo "Code version: $CODE_VERSION"
if [[ "$TAG_VERSION" != "$CODE_VERSION" ]]; then
echo "Version mismatch"; exit 1; fi
echo "Version verified: $TAG_VERSION"
- name: Build and validate package
run: |
uv build
uvx twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ needs.prepare.outputs.version }}
path: dist/
Comment on lines +119 to +123
Copy link
Member

Choose a reason for hiding this comment

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

Why do want to upload artifacts here ?
Maybe we can directly publish to pypi as part of this step ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept it in separate steps to allow manual approval before publishing it to PyPI

Copy link
Member

Choose a reason for hiding this comment

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

@kramaranya Can we just move this part to the publish-pypi step, so you. don't need to push and pull artifact?

      - name: Build and validate package
        run: |
          uv build
          uvx twine check dist/*
 

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, but we will need to set up python and checkout the release branch twice. Also if we push artifacts you can manually install them from github UI if needed - https://github.com/kramaranya/sdk/actions/runs/17457296258
Do we really want to avoid pushing and pulling artifacts?

Copy link
Member

Choose a reason for hiding this comment

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

Sure, make sense, in that case maybe we can upload the artifact to the GitHub release, so users can do something like:

pip install https://github.com/kubeflow/sdk/releases/download/<tag>/<artifact>.tar.gz
or
pip install https://github.com/kubeflow/sdk/releases/download/<tag>/<artifact>.whl

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, sounds good to me, I'll update GH release notes for that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 8f56800


create-tag:
name: Create and push tag
needs: [prepare, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
Comment on lines +130 to +132
Copy link
Member

Choose a reason for hiding this comment

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

Same here, can you checkout to the release branch here ?

Suggested change
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.prepare.outputs.branch }}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 22d8cc8

ref: ${{ needs.prepare.outputs.branch }}
- name: Create tag
run: |
VERSION="${{ needs.prepare.outputs.version }}"
if git ls-remote --tags origin "$VERSION" | grep -q "refs/tags/$VERSION"; then
echo "Tag $VERSION already exists. Skipping"; exit 0; fi
Comment on lines +137 to +138
Copy link
Member

Choose a reason for hiding this comment

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

Did you check the existence of tag here because publish-pypi or github-release job might fail ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I check if the tag exists first because sometimes we might have already published tag but the pypi publish failed. In those cases we want to be able to rerun the workflow to publish to pypi without trying to create the tag again, since pushing a duplicate tag would cause an error and block the workflow with pypi and github release steps

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good.

git tag "$VERSION"
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to create signed tag, I noticed that k8s does this ?

git tag -s $VERSION

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess for that we will need add GPG key to repo secrets, do we have this already?

Copy link
Member

Choose a reason for hiding this comment

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

No, we haven't done it yet.
We can do that in the future.

git push origin "$VERSION"
publish-pypi:
name: Publish to PyPI
needs: [prepare, build, create-tag]
runs-on: ubuntu-latest
environment:
name: release
url: https://pypi.org/project/kubeflow/
Comment on lines +146 to +148
Copy link
Member

Choose a reason for hiding this comment

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

What about secrets ? Do we need to generate Token to push to kubeflow PyPI ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, we'll use PyPI trusted publishing https://docs.pypi.org/trusted-publishers/adding-a-publisher/

Copy link
Member

Choose a reason for hiding this comment

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

I see, I guess I should update it since only I have access to the registry ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I was planning to explain you everything we need to configure before publishing this :)
We will need to add a trusted publishing to PyPI and create a github env

steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist-${{ needs.prepare.outputs.version }}
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true

github-release:
name: Create GitHub Release
needs: [prepare, build, create-tag, publish-pypi]
runs-on: ubuntu-latest
environment:
name: release
url: https://github.com/kubeflow/sdk/releases
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
Copy link
Member

Choose a reason for hiding this comment

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

do you need to checkout to the release branch ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, for patch releases! Updated in 98b1a07

ref: ${{ needs.prepare.outputs.branch }}
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist-${{ needs.prepare.outputs.version }}
path: dist/
- name: Extract changelog
if: needs.prepare.outputs.is-prerelease != 'true'
id: changelog
run: |
VERSION="${{ needs.prepare.outputs.version }}"
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
CHANGELOG_FILE="CHANGELOG/CHANGELOG-${MAJOR_MINOR}.md"
set -euo pipefail
[[ -f "$CHANGELOG_FILE" ]] || { echo "ERROR: $CHANGELOG_FILE not found" >&2; exit 1; }
HEADER_REGEX="^# \\[${VERSION//./\\.}\\]"
SECTION=$(sed -n "/$HEADER_REGEX/,\$p" "$CHANGELOG_FILE" | tail -n +2)
[[ -n "$SECTION" ]] || { echo "ERROR: No changelog section for $VERSION in $CHANGELOG_FILE" >&2; exit 1; }
NEXT_VERSION=$(echo "$SECTION" | grep -m1 "^# \\[[0-9]" || true)
if [[ -n "$NEXT_VERSION" ]]; then
CHANGELOG=$(echo "$SECTION" | sed -n "1,/^# \\[[0-9]/p" | sed '1d;$d')
else
CHANGELOG=$(echo "$SECTION" | sed '1d')
fi
[[ -n "$CHANGELOG" ]] || { echo "ERROR: Empty changelog body for $VERSION in $CHANGELOG_FILE" >&2; exit 1; }
{
echo "changelog<<EOF"
echo "$CHANGELOG"
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.prepare.outputs.version }}
name: ${{ needs.prepare.outputs.version }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ needs.prepare.outputs.is-prerelease == 'true' }}
generate_release_notes: false
files: |
dist/*
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ uv-venv:
echo "uv virtual environment already exists in $(VENV_DIR)."; \
fi

.PHONY: release
release: install-dev
@if [ -z "$(VERSION)" ]; then echo "Usage: make release VERSION=0.1.0"; exit 1; fi
@V_NO_V=$(VERSION); \
sed -i.bak "s/^__version__ = \".*\"/__version__ = \"$$V_NO_V\"/" kubeflow/__init__.py && \
rm -f kubeflow/__init__.py.bak
@uv run python scripts/gen-changelog.py --token=$${GITHUB_TOKEN} --version=$(VERSION)

# make test-python will produce html coverage by default. Run with `make test-python report=xml` to produce xml report.
.PHONY: test-python
test-python: uv-venv
Expand Down
105 changes: 105 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Releasing the Kubeflow SDK

## Prerequisites

- [Write](https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-permission-levels-for-an-organization#permission-levels-for-repositories-owned-by-an-organization)
permission for the Kubeflow SDK repository.

- Create a [GitHub Token](https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token) and set it as `GITHUB_TOKEN` environment variable.

## Versioning Policy

Kubeflow SDK version format follows Python's [PEP 440](https://peps.python.org/pep-0440/).
Kubeflow SDK versions are in the format of `X.Y.Z`, where `X` is the major version, `Y` is
the minor version, and `Z` is the patch version.
The patch version contains only bug fixes.

Additionally, Kubeflow SDK does pre-releases in this format: `X.Y.ZrcN` where `N` is a number
of the `Nth` release candidate (RC) before an upcoming public release named `X.Y.Z`.

## Release Branches and Tags

Kubeflow SDK releases are tagged with tags like `X.Y.Z`, for example `0.1.0`.

Release branches are in the format of `release-X.Y`, where `X.Y` stands for
the minor release.

`X.Y.Z` releases are released from the `release-X.Y` branch. For example,
`0.1.0` release should be on `release-0.1` branch.

If you want to push changes to the `release-X.Y` release branch, you have to
cherry pick your changes from the `main` branch and submit a PR.

## Changelog Structure

Kubeflow SDK uses a directory-based changelog structure under `CHANGELOG/`:

```
CHANGELOG/
├── CHANGELOG-0.1.md # All 0.1.x releases
├── CHANGELOG-0.2.md # All 0.2.x releases
└── CHANGELOG-0.3.md # All 0.3.x releases
```

Each file contains releases for that minor series, with the most recent releases at the top.

## Release Process

### Automated Release Workflow

The Kubeflow SDK uses an automated release process with GitHub Actions:

1. **Local Preparation**: Update version and generate changelog locally
2. **Automated CI**: GitHub Actions handles branch creation, tagging, building, and publishing
3. **Manual Approvals**: PyPI and GitHub releases require manual approval

### Step-by-Step Release Process

#### 1. Update Version and Changelog

1. Generate version and changelog locally (this will sync dependencies automatically):

```sh
export GITHUB_TOKEN=<your_github_token>
make release VERSION=X.Y.Z
```

This updates:
- `kubeflow/__init__.py` with `__version__ = "X.Y.Z"`
- `CHANGELOG/CHANGELOG-X.Y.md` with a new top entry `# [X.Y.Z] (YYYY-MM-DD)`

2. Open a PR:
- Review `kubeflow/__init__.py` and `CHANGELOG/CHANGELOG-X.Y.md`
- **For latest minor series**: Open a PR to `main` and get it reviewed and merged
- **For older minor series patch (e.g. 0.1.1 when main is at 0.2.x)**: Open a PR to the corresponding `release-X.Y` branch

#### 2. Automated Release Process

The `Release` GitHub Action automatically:

1. **Prepare**: Detects the version change in `kubeflow/__init__.py` and creates or updates the `release-X.Y` branch
2. **Build**: Runs tests and builds the package on the release branch
3. **Tag**: Creates and pushes the release tag
4. **Publish**: Publishes to PyPI (requires manual approval)
5. **Release**: Creates GitHub Release (requires manual approval)

**Verification**: Confirm the release branch and tag were created!

#### 3. Manual Approvals

1. **PyPI Publishing**: Go to [GitHub Actions](https://github.com/kubeflow/sdk/actions)`Release` workflow → Approve "Publish to PyPI"

2. **GitHub Release**: After PyPI approval → Approve "Create GitHub Release"

#### 4. Final Verification

1. Verify the release on [PyPI](https://pypi.org/project/kubeflow/)
2. Verify the release on [GitHub Releases](https://github.com/kubeflow/sdk/releases)
3. Test installation: `pip install kubeflow==X.Y.Z`


## Announcement

**Announce**: Post the announcement for the new Kubeflow SDK release in:
- [#kubeflow-ml-experience](https://www.kubeflow.org/docs/about/community/#slack-channels) Slack channel
- [kubeflow-discuss](https://www.kubeflow.org/docs/about/community/#kubeflow-mailing-list) mailing list
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dev = [
"kubeflow_trainer_api@git+https://github.com/kubeflow/trainer.git@master#subdirectory=api/python_api",
"ruff>=0.12.2",
"pre-commit>=4.2.0",
"PyGithub>=2.7.0",
]

[project.urls]
Expand Down
Loading