Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GHA to update k3s-versions.json #7917

Merged
merged 1 commit into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/*"]
37 changes: 37 additions & 0 deletions .github/workflows/k3s-versions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 != '' }}
mook-as marked this conversation as resolved.
Show resolved Hide resolved

check-update-versions:
needs: check-for-token
if: needs.check-for-token.outputs.has-token == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# we may need to checkout an existing branch, so need the full history
fetch-depth: 0
# 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
jandubois marked this conversation as resolved.
Show resolved Hide resolved
- run: ./scripts/k3s-versions.sh
env:
GITHUB_TOKEN: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW }}
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=
182 changes: 182 additions & 0 deletions scripts/k3s-versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package main

import (
"encoding/json"
"fmt"
"io"
"maps"
"net/http"
"os"
"slices"
"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.
const 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, fmt.Errorf("failed to get k3s channels: %w", 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, fmt.Errorf("failed to read response body for k3s update channel: %w", err)
}

var channels Channels
if err := json.Unmarshal(body, &channels); err != nil {
return nil, fmt.Errorf("failed to unmarshal response from k3s update channel: %w", 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
}
// Turn "v1.31.3+k3s1" into "1.31.3"
latest := strings.TrimPrefix(channel.Latest, "v")
latest = strings.SplitN(latest, "+", 2)[0]
k3sChannels[channel.Name] = 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, fmt.Errorf("failed to create request for %q: %w", url, 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, fmt.Errorf("failed to make request for %q: %w", url, 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, fmt.Errorf("failed to read response body for %q: %w", url, err)
}

var releases []GithubRelease
if err := json.Unmarshal(body, &releases); err != nil {
return nil, fmt.Errorf("failed to unmarshal response for %q: %w", url, 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
}
}

return slices.SortedFunc(maps.Values(releaseMap), semver.Compare), nil
}

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

githubReleases, err := getGithubReleases()
if err != nil {
return "", 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 {
return "", fmt.Errorf("error marshalling result to JSON: %w", err)
}
return string(jsonResult), nil
}

func main() {
versions, err := getK3sVersions()
if err != nil {
panic(err)
}

fmt.Println(versions)
}
49 changes: 49 additions & 0 deletions scripts/k3s-versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/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
jandubois marked this conversation as resolved.
Show resolved Hide resolved
# 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_CONFIG_COUNT=2
export GIT_CONFIG_KEY_0=user.name
export GIT_CONFIG_VALUE_0="Rancher Desktop GitHub Action"
export GIT_CONFIG_KEY_1=user.email
export GIT_CONFIG_VALUE_1="donotuse@rancherdesktop.io"

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
Loading