Skip to content
Merged
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
5 changes: 0 additions & 5 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,8 @@ jobs:
uses: helm/chart-testing-action@afea100a513515fbd68b0e72a7bb0ae34cb62aec

- name: Run chart-testing (lint)
if: github.base_ref == 'master'
run: ct lint --target-branch ${GITHUB_BASE_REF} --check-version-increment=false

- name: Run chart-testing (lint)
if: github.base_ref != 'master'
run: ct lint --target-branch ${GITHUB_BASE_REF}

# v1.0.3 is the latest release compatible with Helm v3.10.0.
- name: Install helm-unittest v1.0.3
run: >-
Expand Down
86 changes: 71 additions & 15 deletions docs/release-procedure.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ separate PRs:
`github.com/gophercloud/gophercloud` before bumping the version of
`k8s.io/kubernetes`.

2. Bump the version of `k8s.io/kubernetes` to the latest minor version.
3. Bump the version of `k8s.io/kubernetes` to the latest minor version.

```bash
go get -u k8s.io/kubernetes@latest
Expand All @@ -47,7 +47,7 @@ separate PRs:

Example: https://github.com/kubernetes/cloud-provider-openstack/pull/3010

3. Bump remaining dependencies.
4. Bump remaining dependencies.

Once again, pay close attention to any major version bumps of packages,
ensuring API changes are accounted for.
Expand Down Expand Up @@ -118,10 +118,17 @@ dependency or sidecar container.
$ git push origin release-X.Y
```

1. Reset the `version` field of the Helm Charts to `2.{X+1}.0-dev`
1. Reset the `version` field of the Helm Charts to `2.{X+1}.0-dev`.

Leave `appVersion` set to the version just released (e.g. `vX.Y.0`).
The `hack/bump-release.py` script relies on this value when computing
the next release version, reading it to determine the current minor and
validates that the chart `version` is already at the next minor's `dev`
placeholder.

Any bugfixes for the Helm Charts must be backported to the `release-*`
stable branches and released from there.
stable branches and released from there. See [Helm Charts](#helm-charts)
below for details.

1. Make PR modifying
[images.yaml](https://github.com/kubernetes/k8s.io/blob/main/registry.k8s.io/images/k8s-staging-provider-os/images.yaml)
Expand Down Expand Up @@ -152,22 +159,71 @@ dependency or sidecar container.

### Minor releases (`X.Y.Z`, `Z` > 0)

The release process for a minor release is effectively the same as the release
process for major releases but with the following changes:
The release process for a minor release is the same as for major releases with
the following differences:

1. You must always bump the Helm Chart `appVersion` and `version` fields.
1. Check out the `release-X.Y` branch and run the `master` version of
`hack/bump-release.py`.

1. It is not necessary to create a new branch or add new jobs.
```bash
$ git fetch upstream
$ git checkout release-X.Y
$ git pull --rebase upstream release-X.Y
$ tmp=$(mktemp --suffix=.py)
$ git show upstream/master:hack/bump-release.py > "$tmp"
$ uv run "$tmp"
$ rm "$tmp"
```

The script must be run from the release branch so it can detect it is on a
stable branch. Running the `master` version ensures you always use the
latest version of the script.

The script automatically bumps both `appVersion` and `version` in all Helm
Charts (whereas a chart-only release on a stable branch bumps only the
`version` of affected charts).

1. It is not necessary to create a new release branch or add new CI jobs.

## Helm Charts

### Release tag types

There are four independent release artifacts, each with its own tag:

- The overall CPO binary release (e.g. `v1.36.0`)
- The CCM chart release (e.g. `openstack-cloud-controller-manager-2.36.0`)
- The Cinder CSI chart release (e.g. `openstack-cinder-csi-2.36.0`)
- The Manila CSI chart release (e.g. `openstack-manila-csi-2.36.0`)

A CPO binary release always triggers new versions of all three charts (because
all charts reference the CPO image via `appVersion`). A chart-only release —
where only chart files change with no changes to `cmd/` or `pkg/` — produces
new chart tags for the affected charts without a new CPO binary tag.

### Versioning on `master`

Chart versions on `master` use a `-dev` pre-release suffix (e.g.
`2.37.0-dev`) and are **not** bumped for individual PRs. Version bumps only
`2.37.0-dev`) and are **not** bumped for individual PRs. The `appVersion`
field reflects the most recently published CPO release. Version bumps only
happen at release time (see [Major releases](#major-releases-xy0) above).

On `release-*` branches the chart version (`version`) **must** be bumped for
every backported change to a chart(s) including changes to `appVersion`. A CI
job enforces this for PRs targeting those branches. Once version change is
merged, tags are automatically created for any charts whose version changed
(i.e. `openstack-cloud-controller-manager-X.Y.Z`, `openstack-cinder-csi-X.Y.Z`,
and `openstack-manila-csi-X.Y.Z`).
### Versioning on `release-*` branches

On `release-*` branches the chart `version` **should** be bumped for every
backported change to a chart or to CPO code (`cmd/`, `pkg/`). This can be done
in the backport PR itself or later via a separate PR.

Run `hack/bump-release.py` from the `release-*` branch to apply the correct
bump automatically. The script compares the branch tip against the most recent
CPO tag (e.g. `v1.36.0`) and determines what changed:

- **CPO code changed** (`cmd/` or `pkg/`): bumps `appVersion` in all charts,
bumps `version` in all charts, and updates image references in `docs/`,
`manifests/`, and `examples/`.
- **Only chart files changed**: bumps `version` only in the charts that
changed; `appVersion` and image references are left untouched.

Once a version change is merged, tags are automatically created for any charts
whose `version` changed (i.e. `openstack-cloud-controller-manager-X.Y.Z`,
`openstack-cinder-csi-X.Y.Z`, and `openstack-manila-csi-X.Y.Z`).
147 changes: 135 additions & 12 deletions hack/bump-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def git_check(*args: str) -> bool:


def find_base_branch() -> str | None:
"""Return 'master' or a 'release-N.N' name, whichever is the closest ancestor of HEAD.
"""Return 'master' or a 'release-N.N' name for the closest ancestor of HEAD.

Checks all remotes so the result is not tied to a specific remote name.
"""
Expand Down Expand Up @@ -95,6 +95,30 @@ def find_base_branch() -> str | None:
return best_name


def find_last_cpo_tag() -> str:
"""Return the most recent vX.Y.Z CPO tag reachable from HEAD."""
try:
return git(
"describe", "--tags", "--abbrev=0", "--match", "v[0-9]*.[0-9]*.[0-9]*"
)
except subprocess.CalledProcessError:
sys.exit("ERROR: No CPO release tag (vX.Y.Z) found reachable from HEAD.")


def find_last_chart_tag(chart_name: str) -> str | None:
"""Return the most recent release tag for a chart, or None if none exists."""
try:
return git(
"describe",
"--tags",
"--abbrev=0",
"--match",
f"{chart_name}-[0-9]*.[0-9]*.[0-9]*",
)
except subprocess.CalledProcessError:
return None


def load_chart(path: Path) -> tuple[YAML, dict]:
yaml = YAML()
yaml.preserve_quotes = True
Expand Down Expand Up @@ -190,18 +214,12 @@ def release_master() -> None:
update_files(app_version, new_app_version)


def release_stable(base_branch: str) -> None:
print(f"Base release branch: {base_branch}")
print()
print(
"If you are only bumping the Helm chart version independently of a CPO release, "
"manually update the 'version' field in each Chart.yaml instead."
)
if not Confirm.ask("Releasing a new CPO version?", default=False):
print("Exiting. Manually update 'version' in each Chart.yaml if needed.")
sys.exit(0)
print()
def changed_files(merge_base: str) -> list[str]:
return git("diff", "--name-only", merge_base, "HEAD").splitlines()


def release_stable_full(base_branch: str) -> None:
"""Full CPO release: bump appVersion, chart versions, and image references."""
app_version = read_app_version()

m_app = APP_VERSION_RE.match(app_version)
Expand All @@ -211,6 +229,17 @@ def release_stable(base_branch: str) -> None:
app_x, app_y = int(m_app.group("x")), int(m_app.group("y"))
new_app_version = f"v{m_app.group('major')}.{app_x}.{app_y + 1}"

console.print(f"[dim]Detected base branch:[/dim] [bold]{base_branch}[/bold]")
console.print(
f"[dim]Changes to CPO code detected. Continuing will release a new CPO patch version "
f"([bold]{new_app_version}[/bold]) and bump all Helm chart versions.[/dim]"
)
console.print()
if not Confirm.ask(f"Release CPO {new_app_version}?", default=False):
print("Exiting.")
sys.exit(0)
print()

print("Bumping versions:")
print(f" appVersion: {app_version} -> {new_app_version}")

Expand Down Expand Up @@ -238,6 +267,100 @@ def release_stable(base_branch: str) -> None:
update_files(app_version, new_app_version)


def release_stable_chart_only(base_branch: str, affected: list[Path]) -> None:
"""Chart-only release: bump chart versions without touching appVersion or manifests."""
app_version = read_app_version()

m_app = APP_VERSION_RE.match(app_version)
if not m_app:
sys.exit(f"ERROR: Expected appVersion to match 'v1.X.Y', got: {app_version!r}")

app_x = int(m_app.group("x"))

console.print(f"[dim]Detected base branch:[/dim] [bold]{base_branch}[/bold]")
console.print(
"[dim]No changes to CPO code detected. Continuing will bump only the Helm chart "
"versions for changed charts (appVersion and image references will not change).[/dim]"
)
console.print()
if not Confirm.ask("Release chart-only update?", default=False):
print("Exiting.")
sys.exit(0)
print()

print("Bumping chart versions:")

for path in affected:
_, data = load_chart(path)
current_version = data["version"]

m_ver = VERSION_RE.match(current_version)
if not m_ver:
sys.exit(
f"ERROR: {path.parent.name}: expected version 'MAJOR.X.Z' (no -dev suffix), "
f"got: {current_version!r}"
)
if int(m_ver.group("x")) != app_x:
sys.exit(
f"ERROR: {path.parent.name}: minor version mismatch with "
f"appVersion ({app_version}): {current_version!r}"
)

new_version = f"{m_ver.group('major')}.{app_x}.{int(m_ver.group('z')) + 1}"
print(f" {path.parent.name}: version {current_version} -> {new_version}")
update_chart(path, app_version, new_version)


def release_stable(base_branch: str) -> None:
last_cpo_tag = find_last_cpo_tag()
app_version = read_app_version()

# If appVersion is already ahead of the last CPO tag, a full release has
# been prepared but not yet tagged — don't double-bump.
if app_version != last_cpo_tag:
console.print(
f"[yellow]Warning:[/yellow] appVersion is already at {app_version!r} "
f"(last CPO tag: {last_cpo_tag!r}). "
f"A CPO release has already been prepared; create the tag to complete it."
)
sys.exit(0)

# Code changes are always measured against the last CPO tag.
has_code_changes = any(
f.startswith("cmd/") or f.startswith("pkg/")
for f in changed_files(last_cpo_tag)
)

if has_code_changes:
release_stable_full(base_branch)
return

# Chart changes are measured against each chart's own last tag so that
# previously released chart-only changes are not counted again.
# If a chart's version is already ahead of its last tag, a chart-only
# release has been prepared but not yet tagged — skip it.
affected: list[Path] = []
for chart_path in CHART_FILES:
chart_name = chart_path.parent.name
last_chart_tag = find_last_chart_tag(chart_name)
if last_chart_tag:
_, data = load_chart(chart_path)
if data["version"] != last_chart_tag.rsplit("-", 1)[-1]:
continue
ref = last_chart_tag if last_chart_tag else last_cpo_tag
if any(f.startswith(f"charts/{chart_name}/") for f in changed_files(ref)):
affected.append(chart_path)

if not affected:
print(
f"No changes to CPO code or charts detected since {last_cpo_tag}. "
"Nothing to release."
)
sys.exit(0)

release_stable_chart_only(base_branch, affected)


def main() -> None:
if not CHART_FILES:
sys.exit(
Expand Down
Loading