Skip to content

Commit

Permalink
Automatically manage pre-release branches
Browse files Browse the repository at this point in the history
A k8s-snap PR automatically creates and cleans up git branches
for upstream k8s pre-releases:
canonical/k8s-snap#916

Here we're adding an almost identical job that picks up these
git branches and prepares launchpad recipes.

TODOs:
* clean up obsolete pre-releases that were superseeded by a new
  pre-release or stable release
* promote beta and rc pre-releases to the corresponding snap
  risk level
  • Loading branch information
petrutlucian94 committed Dec 19, 2024
1 parent a83cf81 commit 1d56a1c
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .github/actions/job-prerequisites/action.yaml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions .github/workflows/create-release-branch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions .github/workflows/update-pre-release-branches.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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: ${{ steps.determine.outputs.gitBranch }}
clean-obsolete:
runs-on: ubuntu-latest
outputs:
preRelease: ${{ steps.determine.outputs.preRelease }}
gitBranch: ${{ steps.determine.outputs.gitBranch }}
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
87 changes: 87 additions & 0 deletions scripts/k8s_releases.py
Original file line number Diff line number Diff line change
@@ -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]} <function>\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 "")
2 changes: 2 additions & 0 deletions scripts/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions test_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
freezegun
pytest
types-requests

0 comments on commit 1d56a1c

Please sign in to comment.