diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8430a2f7cd..d1e2b0b421 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -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: >- diff --git a/docs/release-procedure.md b/docs/release-procedure.md index a23f1ca6b5..4cd50b1f68 100644 --- a/docs/release-procedure.md +++ b/docs/release-procedure.md @@ -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 @@ -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. @@ -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) @@ -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`). diff --git a/hack/bump-release.py b/hack/bump-release.py index 80ed1f280f..cf9309d752 100755 --- a/hack/bump-release.py +++ b/hack/bump-release.py @@ -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. """ @@ -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 @@ -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) @@ -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}") @@ -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(