Skip to content

Commit

Permalink
Add GHA to update k3s-versions.json
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Dubois <[email protected]>
  • Loading branch information
jandubois committed Dec 12, 2024
1 parent 28e0d1a commit b81673c
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 3 deletions.
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ gcs
GENERALIZEDTIME
getfattr
getwindowid
gha
ghp
gitmodules
gitrepo
Expand Down
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,16 @@ updates:
patterns: ["golang.org/x/*"]
k8s:
patterns: ["k8s.io/*"]

- package-ecosystem: "gomod"
directory: "/scripts"
schedule:
interval: "daily"
open-pull-requests-limit: 12
labels: ["component/dependencies"]
reviewers: ["jandubois"]
groups:
golang-x:
patterns: ["golang.org/x/*"]
k8s:
patterns: ["k8s.io/*"]
38 changes: 38 additions & 0 deletions .github/workflows/k3s-versions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Update k3s-versions.json
on:
schedule:
- cron: '43 8 * * *'
workflow_dispatch: {}

permissions:
contents: write
pull-requests: write

jobs:
check-for-token:
outputs:
has-token: ${{ steps.calc.outputs.HAS_SECRET }}
runs-on: ubuntu-latest
steps:
- id: calc
run: echo "HAS_SECRET=${HAS_SECRET}" >> "${GITHUB_OUTPUT}"
env:
HAS_SECRET: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW != '' }}

check-update-versions:
needs: check-for-token
if: needs.check-for-token.outputs.has-token == 'true'
runs-on: ubuntu-latest
steps:
# Setup go to be able to run `go run ./scripts/k3s-version.go`
- uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version-file: go.work
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: ./scripts/k3s-versions.sh
env:
GITHUB_TOKEN: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW }}
# MINIMUM_VERSION **MUST** start with a 'v'
MINIMUM_VERSION: v1.21
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ go 1.23.0
toolchain go1.23.4

use (
./scripts
./src/go/docker-credential-none
./src/go/extension-proxy
./src/go/github-runner-monitor
Expand Down
5 changes: 5 additions & 0 deletions scripts/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/rancher-sandbox/rancher-desktop/scripts

go 1.23

require golang.org/x/mod v0.22.0
2 changes: 2 additions & 0 deletions scripts/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
187 changes: 187 additions & 0 deletions scripts/k3s-version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"slices"
"sort"
"strconv"
"strings"

"golang.org/x/mod/semver"
)

// golang.org/x/mod/semver *requires* a leading 'v' on versions, and will add missing minor/patch numbers.
var minimumVersion = "v1.21"

type Channels struct {
Data []Channel `json:"data"`
}
type Channel struct {
Name string `json:"name"`
Latest string `json:"latest"`
}

// getK3sChannels returns a map of all non-prerelease channels, plus "latest" and "stable".
// The values are the latest release for each channel.
func getK3sChannels() (map[string]string, error) {
resp, err := http.Get("https://update.k3s.io/v1-release/channels")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("update channel request failed with status: %s", resp.Status)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var channels Channels
if err := json.Unmarshal(body, &channels); err != nil {
return nil, err
}

k3sChannels := make(map[string]string)
for _, channel := range channels.Data {
switch {
case channel.Name == "latest" || channel.Name == "stable":
break
case semver.Prerelease(channel.Latest) != "":
continue
case semver.IsValid(channel.Latest) && semver.Compare(channel.Latest, minimumVersion) >= 0:
break
default:
continue
}
k3sChannels[channel.Name] = channel.Latest
}

return k3sChannels, nil
}

type GithubRelease struct {
TagName string `json:"tag_name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
}

// getGithubReleasesPage fetches a single page of GitHub releases and returns a list
// of all non-draft, non-prerelease releases higher than the minimumVersion.
func getGithubReleasesPage(page int) ([]GithubRelease, error) {
url := fmt.Sprintf("https://api.github.com/repos/k3s-io/k3s/releases?page=%d", page)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
token := os.Getenv("GH_TOKEN")
if token == "" {
token = os.Getenv("GITHUB_TOKEN")
}
if token != "" {
req.Header.Set("Authorization", "token "+token)
}

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
//nolint:revive // error-strings
return nil, fmt.Errorf("GitHub API request failed with status: %s", resp.Status)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var releases []GithubRelease
if err := json.Unmarshal(body, &releases); err != nil {
return nil, err
}

// Filter desired releases here, so caller will stop requesting additional pages if there are
// no more matches (heuristics, but releases are returned in reverse chronological order).
releases = slices.DeleteFunc(releases, func(release GithubRelease) bool {
return release.Draft || release.Prerelease || semver.Compare(release.TagName, minimumVersion) < 0
})
return releases, nil
}

// getGithubReleases returns a sorted list of all matching GitHub releases.
func getGithubReleases() ([]string, error) {
releaseMap := make(map[string]string)
for page := 1; ; page++ {
releases, err := getGithubReleasesPage(page)
if err != nil {
return nil, err
}
if len(releases) == 0 {
break
}
for _, release := range releases {
version := semver.Canonical(release.TagName)
// for each version we only keep the latest k3s patch, i.e. +k3s2 instead of +k3s1
if oldTag, ok := releaseMap[version]; ok {
oldPatch, _ := strconv.Atoi(strings.TrimPrefix(semver.Build(oldTag), "+k3s"))
patch, _ := strconv.Atoi(strings.TrimPrefix(semver.Build(release.TagName), "+k3s"))
if oldPatch > patch {
continue
}
}
releaseMap[version] = release.TagName
}
}

var versions []string
for _, version := range releaseMap {
versions = append(versions, version)
}

sort.Slice(versions, func(i, j int) bool {
return semver.Compare(versions[i], versions[j]) < 0
})

return versions, nil
}

func main() {
if len(os.Args) > 1 {
minimumVersion = os.Args[1]
}
if !semver.IsValid(minimumVersion) {
panic(fmt.Errorf("minimum version %q is not a valid version, e.g. needs to start with 'v'", minimumVersion))
}

k3sChannels, err := getK3sChannels()
if err != nil {
panic(fmt.Errorf("error fetching k3s channels: %w", err))
}

githubReleases, err := getGithubReleases()
if err != nil {
panic(fmt.Errorf("error fetching GitHub releases: %w", err))
}

result := map[string]interface{}{
"cacheVersion": 2,
"channels": k3sChannels,
"versions": githubReleases,
}

// json.Marshal will produce map keys in sort order
jsonResult, err := json.MarshalIndent(result, "", " ")
if err != nil {
panic(fmt.Errorf("error marshalling result to JSON: %w", err))
}

fmt.Println(string(jsonResult))
}
45 changes: 45 additions & 0 deletions scripts/k3s-versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/bin/bash

# This script expects to be called from the root of the repo.
# It will rebuild resources/k3s-versions.json from both the k3s update
# channel and the GitHub k3s releases list.
# Creates a pull request if the new version is different.

set -eu

K3S_VERSIONS="resources/k3s-versions.json"
BRANCH_NAME="gha-update-k3s-versions"
NEW_PR="true"

if git rev-parse --verify "origin/${BRANCH_NAME}" 2>/dev/null; then
# This logic relies on the fact that PR branches inside the repo get automatically
# deleted when the PR has been merged. We assume that if the branch exists, there
# is also a corresponding PR for it, so we just update the branch with a new commit.
git checkout "$BRANCH_NAME"
NEW_PR="false"
else
git checkout -b "$BRANCH_NAME"
fi

go run ./scripts/k3s-versions.go "$MINIMUM_VERSION" >"$K3S_VERSIONS"

# Exit if there are no changes
if git diff --exit-code; then
exit
fi

export GIT_AUTHOR_NAME="Rancher Desktop GitHub Action"
export GIT_AUTHOR_EMAIL="[email protected]"
git add "$K3S_VERSIONS"
git commit --signoff --message "Automated update: k3s-versions.json"
git push origin "$BRANCH_NAME"

if [ "$NEW_PR" = "false" ]; then
exit
fi

gh pr create \
--title "Update k3s-versions.json" \
--body "This pull request contains the latest update to k3s-versions.json." \
--head "$BRANCH_NAME" \
--base main
2 changes: 1 addition & 1 deletion src/go/networking/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ require (
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
golang.org/x/crypto v0.30.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.26.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
Expand Down
4 changes: 2 additions & 2 deletions src/go/networking/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
Expand Down

0 comments on commit b81673c

Please sign in to comment.