diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000..13236e5 --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,28 @@ +name: changelog + +on: + push: + tags: + - v* + +permissions: write-all + +jobs: + deploy: + name: Generate changelog and publish a release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: requarks/changelog-action@v1 + id: changelog + with: + token: ${{ github.token }} + tag: ${{ github.ref_name }} + - uses: ncipollo/release-action@v1.13.0 + with: + allowUpdates: true + draft: false + makeLatest: true + name: ${{ github.ref_name }} + body: ${{ steps.changelog.outputs.changes }} + token: ${{ github.token }} diff --git a/docs/.crd-ref-docs.yaml b/docs/.crd-ref-docs.yaml index ca1cd20..ab04824 100644 --- a/docs/.crd-ref-docs.yaml +++ b/docs/.crd-ref-docs.yaml @@ -10,4 +10,4 @@ processor: render: # Version of Kubernetes to use when generating links to Kubernetes API documentation. - kubernetesVersion: 'v1.30' + kubernetesVersion: '1.30' diff --git a/docs/reference/out.md b/docs/reference/out.md index b9392e9..16adcc4 100644 --- a/docs/reference/out.md +++ b/docs/reference/out.md @@ -27,7 +27,7 @@ LKEClusterConfig is the Schema for the lkeclusterconfigs API. | --- | --- | --- | --- | | `apiVersion` _string_ | `lke.anza-labs.dev/v1alpha1` | | | | `kind` _string_ | `LKEClusterConfig` | | | -| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/vv1.30/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `spec` _[LKEClusterConfigSpec](#lkeclusterconfigspec)_ | | | | diff --git a/hack/release.py b/hack/release.py index b183f83..2024499 100644 --- a/hack/release.py +++ b/hack/release.py @@ -13,111 +13,228 @@ # limitations under the License. import argparse +import os import subprocess import sys -import os -import requests -import yaml +import requests import semver +import yaml import hack.utils as utils logger = utils.setup(__name__) + def make(args: list[str]) -> None: - rc = subprocess.run( + """ + Runs the 'make' command with the provided arguments. + + Args: + args (list[str]): List of arguments to pass to the 'make' command. + """ + subprocess.run( ["make"] + args, stdout=sys.stdout, stderr=sys.stderr, + check=True, ) - if rc.returncode != 0: - raise Exception(rc.stderr) + def git(args: list[str]) -> None: - rc = subprocess.run( + """ + Runs the 'git' command with the provided arguments. + + Args: + args (list[str]): List of arguments to pass to the 'git' command. + """ + subprocess.run( ["git"] + args, stdout=sys.stdout, stderr=sys.stderr, + check=True, ) - if rc.returncode != 0: - raise Exception(rc.stderr) -def _parse_version(version: str) -> tuple[str, bool]: + +def _branch_prep(version: str, full_version: str): + """ + Prepares the branch for the release. + + If the branch exists, switches to it; otherwise, creates a new branch. + Merges the main branch into the release branch and pushes the changes. + + Args: + version (str): The short version string. + full_version (str): The full version string. + """ + branch = f"release-{version}" + if _branch_exists(branch): + _switch_to_branch(branch) + else: + _create_branch(branch) + git( + [ + "merge", + "main", + "-m", + f"chore({version}): merge changes for {full_version}", + "--signoff", + ] + ) + git(["push", "origin", branch]) + + +def _branch_exists(branch_name: str) -> bool: + """ + Checks if a Git branch exists. + + Args: + branch_name (str): The name of the branch to check. + + Returns: + bool: True if the branch exists, False otherwise. + """ + result = subprocess.run( + ["git", "branch", "--list", branch_name], capture_output=True, text=True + ) + return branch_name in result.stdout + + +def _switch_to_branch(branch_name: str) -> None: + """ + Switches to the specified Git branch. + + Args: + branch_name (str): The name of the branch to switch to. + """ + git(["checkout", branch_name]) + + +def _create_branch(branch_name: str) -> None: """ - Parses the given version string and returns a short version string and a boolean indicating whether it's a prerelease. + Creates and switches to a new Git branch. + + Args: + branch_name (str): The name of the branch to create. + """ + git(["checkout", "-b", branch_name]) + + +def _parse_version(version: str) -> str: + """ + Parses the given version string and returns a short version string. Args: version (str): The version string to parse. Returns: - Tuple[str, bool]: A tuple containing the short version string and a boolean indicating whether it's a prerelease. + str: The short version string. """ short_version = "" - is_prerelease = True if version == "main": short_version = version else: try: sv = semver.Version.parse(version.removeprefix("v")) - short_version = f"v{sv.major}.{sv.minor}" - is_prerelease = sv.prerelease is not None + short_version = f"{sv.major}.{sv.minor}" except ValueError as e: logger.warn(e) - logger.info("%s (is_prerelease: %s)", short_version, is_prerelease) - return short_version, is_prerelease + logger.info("%s", short_version) + return short_version def _get_latest_kubernetes_release() -> str: + """ + Fetches the latest Kubernetes release version from the GitHub API. + + Returns: + str: The latest Kubernetes release version. + + Raises: + Exception: If the API request fails. + """ url = "https://api.github.com/repos/kubernetes/kubernetes/releases/latest" response = requests.get(url) if response.status_code == 200: - latest_release = response.json()['tag_name'] + latest_release = response.json()["tag_name"] return latest_release else: - raise Exception(f"Failed to fetch the latest release. Status code: {response.status_code}") + raise Exception( + f"Failed to fetch the latest release. Status code: {response.status_code}" + ) + def _replace_kubernetes_version(file_path: str, new_version: str) -> None: - # Temporary file path - temp_file_path = file_path + '.tmp' + """ + Replaces the Kubernetes version in the specified configuration file. + + Args: + file_path (str): The path to the configuration file. + new_version (str): The new Kubernetes version to set. + """ + temp_file_path = file_path + ".tmp" - with open(file_path, 'r') as file, open(temp_file_path, 'w') as temp_file: + with open(file_path, "r") as file, open(temp_file_path, "w") as temp_file: for line in file: - # Check if the line contains the kubernetesVersion key - if 'kubernetesVersion' in line: - # Split the line at the colon and replace the version - key, _ = line.split(':') - # Preserve the formatting by adding back the key and the new version + if "kubernetesVersion" in line: + key, _ = line.split(":") temp_file.write(f"{key}: '{new_version}'\n") else: - # Write the original line if it doesn't contain kubernetesVersion temp_file.write(line) - # Replace the original file with the modified file os.replace(temp_file_path, file_path) + def _create_kustomization(resources, image_name, new_tag): + """ + Creates a kustomization dictionary. + + Args: + resources (list[str]): List of resource paths. + image_name (str): Name of the image to replace. + new_tag (str): New tag for the image. + + Returns: + dict: Kustomization dictionary. + """ kustomization = { - "apiVersion": "kustomize.config.k8s.io/v1beta1", - "kind": "Kustomization", "resources": resources, - "images": [ - { - "name": image_name, - "newTag": new_tag - } - ] + "images": [{"name": image_name, "newTag": new_tag}], } return kustomization -def _write_kustomization(kustomization, filepath): - with open(filepath, 'w') as file: + +def _release(version: str, full_version: str) -> None: + """ + Creates a release by committing changes, pushing to the remote branch, and tagging the release. + + Args: + version (str): The short version string. + full_version (str): The full version string. + """ + git(["add", "."]) + git(["commit", "-sm", f"chore({version}): create release commit {full_version}"]) + git(["push", "origin", f"release-{version}"]) + git(["tag", full_version]) + git(["push", "--tags"]) + + +def _write_kustomization(kustomization, filepath) -> None: + """ + Writes the kustomization dictionary to a file. + + Args: + kustomization (dict): The kustomization dictionary. + filepath (str): The path to the file where the kustomization will be written. + """ + with open(filepath, "w") as file: yaml.dump(kustomization, file) -def run(args=sys.argv): + +def run(args=sys.argv) -> None: """ Runs the release script. @@ -152,16 +269,18 @@ def run(args=sys.argv): args = parser.parse_args(args=args[1:]) - resources = [ - "./config/crd", - "./config/manager", - "./config/rbac" - ] + resources = ["./config/crd", "./config/manager", "./config/rbac"] - version, _ = _parse_version(version=args.version) - kube_version, _ = _parse_version(version=_get_latest_kubernetes_release()) + version = _parse_version(version=args.version) + kube_version = _parse_version(version=_get_latest_kubernetes_release()) _replace_kubernetes_version(args.config, kube_version) - git(["checkout", "-b", f"release-{version}"]) - make(["manifests", "api-docs"]) - kustomization = _create_kustomization(resources=resources, image_name=args.image, new_tag=args.version) + + _branch_prep(version, args.version) + + kustomization = _create_kustomization( + resources=resources, image_name=args.image, new_tag=args.version + ) _write_kustomization(kustomization=kustomization, filepath="./kustomization.yaml") + make(["manifests", "apidocs"]) + + _release(version, args.version)