diff --git a/.github/actions/job-prerequisites/action.yaml b/.github/actions/job-prerequisites/action.yaml new file mode 100755 index 0000000..0b080d3 --- /dev/null +++ b/.github/actions/job-prerequisites/action.yaml @@ -0,0 +1,20 @@ +name: Job prerequisites + +runs: + using: "composite" + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@v4 + with: + ssh-key: ${{ secrets.BOT_SSH_KEY }} + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - name: Install Python dependencies + run: pip3 install -r ./scripts/requirements.txt diff --git a/.github/workflows/create-release-branch.yaml b/.github/workflows/create-release-branch.yaml index d2004cc..31b84bb 100644 --- a/.github/workflows/create-release-branch.yaml +++ b/.github/workflows/create-release-branch.yaml @@ -8,6 +8,13 @@ on: required: false default: "" # defaults to all matching branches (main and release branches) description: Run on which k8s-snap branches (space separated). If empty, it will run on all matching branches (main and release branches). + workflow_call: + inputs: + branches: + type: string + required: false + default: "" # defaults to all matching branches (main and release branches) + description: Run on which k8s-snap branches (space separated). If empty, it will run on all matching branches (main and release branches). permissions: contents: read diff --git a/.github/workflows/update-pre-release-branches.yaml b/.github/workflows/update-pre-release-branches.yaml new file mode 100755 index 0000000..9514e3d --- /dev/null +++ b/.github/workflows/update-pre-release-branches.yaml @@ -0,0 +1,76 @@ +name: Auto-update pre-release branches + +on: + pull_request: + paths: + - .github/workflows/update-pre-release-branches.yaml + schedule: + # Run 20 minutes after midnight, giving the k8s-snap nightly job + # enough time to pick up new k8s releases and setup the git branches. + - cron: "20 0 * * *" + +permissions: + contents: read + +jobs: + determine: + runs-on: ubuntu-latest + outputs: + preRelease: ${{ steps.determine.outputs.preRelease }} + gitBranch: ${{ steps.determine.outputs.gitBranch }} + steps: + - name: Prerequisites + uses: ./.github/actions/job-prerequisites + - name: Determine outstanding pre-release + id: determine + run: | + preRelease=`python3 ./scripts/k8s_releases.py get_outstanding_prerelease` + echo "preRelease=$preRelease" >> "$GITHUB_OUTPUT" + + if [[ -n "$preRelease" ]]; then + gitBranch="autoupdate/$preRelease" + fi + echo "gitBranch=$gitBranch" >> "$GITHUB_OUTPUT" + - name: Create pre-release branch ${{ steps.determine.outputs.gitBranch }} + if: ${{ steps.determine.outputs.preRelease }} != '' + uses: ./.github/workflows/create-release-branch.yaml + with: + branches: ${{ steps.determine.outputs.gitBranch }} + - name: Clean obsolete branches + run: | + git fetch origin + + # Log the latest release for reference. + latestRelease=`python3 ./scripts/k8s_releases.py get_latest_release` + echo "Latest k8s release: $latestRelease" + + for outstandingPreRelease in `python3 ./scripts/k8s_releases.py get_obsolete_prereleases`; do + gitBranch="autoupdate/${outstandingPreRelease}" + echo "Cleaning up obsolete pre-release branch: $gitBranch" + # TODO + done + handle-pre-release: + name: Handle pre-release ${{ needs.determine.outputs.preRelease }} + needs: [determine] + uses: ./.github/workflows/create-release-branch.yaml + if: ${{ needs.determine.outputs.preRelease }} != '' + with: + branches: ${{ needs.determine.outputs.gitBranch }} + clean-obsolete: + runs-on: ubuntu-latest + steps: + - name: Prerequisites + uses: ./.github/actions/job-prerequisites + - name: Clean obsolete branches + run: | + git fetch origin + + # Log the latest release for reference. + latestRelease=`python3 ./scripts/k8s_releases.py get_latest_release` + echo "Latest k8s release: $latestRelease" + + for outstandingPreRelease in `python3 ./scripts/k8s_releases.py get_obsolete_prereleases`; do + gitBranch="autoupdate/${outstandingPreRelease}" + echo "Cleaning up obsolete pre-release branch: $gitBranch" + # TODO + done diff --git a/scripts/k8s_releases.py b/scripts/k8s_releases.py new file mode 100755 index 0000000..ddec88b --- /dev/null +++ b/scripts/k8s_releases.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +import json +import sys +from typing import List, Optional + +import requests +from packaging.version import Version + +K8S_TAGS_URL = "https://api.github.com/repos/kubernetes/kubernetes/tags" + + +def _url_get(url: str) -> str: + r = requests.get(url, timeout=5) + r.raise_for_status() + return r.text + + +def get_k8s_tags() -> List[str]: + """Retrieve semantically ordered k8s releases, newest to oldest.""" + response = _url_get(K8S_TAGS_URL) + tags_json = json.loads(response) + if len(tags_json) == 0: + raise Exception("No k8s tags retrieved.") + tag_names = [tag['name'] for tag in tags_json] + # Github already sorts the tags semantically but let's not rely on that. + tag_names.sort(key=lambda x: Version(x), reverse=True) + return tag_names + + +# k8s release naming: +# * alpha: v{major}.{minor}.{patch}-alpha.{version} +# * beta: v{major}.{minor}.{patch}-beta.{version} +# * rc: v{major}.{minor}.{patch}-rc.{version} +# * stable: v{major}.{minor}.{patch} +def is_stable_release(release: str): + return "-" not in release + + +def get_latest_stable() -> str: + k8s_tags = get_k8s_tags() + for tag in k8s_tags: + if is_stable_release(tag): + return tag + raise Exception( + "Couldn't find stable release, received tags: %s" % k8s_tags) + + +def get_latest_release() -> str: + k8s_tags = get_k8s_tags() + return k8s_tags[0] + + +def get_outstanding_prerelease() -> Optional[str]: + latest_release = get_latest_release() + if not is_stable_release(latest_release): + return latest_release + # The latest release is a stable release, no outstanding pre-release. + return None + + +def get_obsolete_prereleases() -> List[str]: + """Return obsolete K8s pre-releases. + + We only keep the latest pre-release if there is no corresponding stable + release. All previous pre-releases are discarded. + """ + k8s_tags = get_k8s_tags() + if not is_stable_release(k8s_tags[0]): + # Valid pre-release + k8s_tags = k8s_tags[1:] + # Discard all other pre-releases. + return [tag for tag in k8s_tags if not is_stable_release(tag)] + + +# Rudimentary CLI that exposes these functions to shell scripts or GH actions. +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.stderr.write(f"Usage: {sys.argv[0]} \n") + sys.exit(1) + f = locals()[sys.argv[1]] + out = f() + if isinstance(out, (list, tuple)): + for item in out: + print(item) + else: + print(out or "") diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 9261e98..db6c9e9 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -4,6 +4,8 @@ httplib2==0.22.0 launchpadlib==1.11.0 lazr-restfulclient==0.14.6 lazr-uri==1.0.6 +packaging==24.2 +requests==2.32.3 semver==3.0.2 six==1.16.0 tox==4.20.0 diff --git a/test_requirements.txt b/test_requirements.txt index 031ed92..920b206 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,2 +1,3 @@ freezegun pytest +types-requests