// Package main implements a tool for automatically updating component // versions for use in the hack/* scripts. // // Files interacted with: // 1. The source-of-truth file at hack/component-versions.json // 2. Autogenerated script at hack/component-versions.sh // // USAGE: // // This is running on semi-auto basis where versions are being auto bumped via // PRs sent to main. Semi-auto because if repo is missing 'owner' or 'repo' fields // in the .json it will not be automatically bumped (it has no repo to look). // This is intentional for components we dont want autobumped like this. // The source-of-truth file is found in this repo @root/hack/component-versions.json // // ADD NEW/MODIFY COMPONENTS // // 1. Edit source-of-truth .json file // ! If a component is missing "owner" or "repo" it will not be auto-bumped // 2. If new component was added: // - Edit the autogenerated text just below here 'versionsScriptTemplate' // 3. Regenerate using Makefile - find target 'hack-generate-components' package main import ( "context" "encoding/json" "fmt" "html/template" "os" "os/signal" "syscall" github "github.com/google/go-github/v68/github" ) const ( fileScript string = "component-versions.sh" fileJson string = "component-versions.json" versionsScriptTemplate string = `#!/usr/bin/env bash # AUTOGENERATED FILE - edit versions in ./component-versions.json. # If you want to add/modify these components, please read the how-to steps in # ./cmd/components/main.go. # You can regenerate with "make hack-generate-components". set_versions() { # Note: Kubernetes Version node image per Kind releases (full hash is suggested): # https://github.com/kubernetes-sigs/kind/releases kind_node_version={{.KindNode.Version}} # find source-of-truth in component-versions.json to add/modify components knative_serving_version="{{.Serving.Version}}" knative_eventing_version="{{.Eventing.Version}}" contour_version="{{.Contour.Version}}" tekton_version="{{.Tekton.Version}}" pac_version="{{.Pac.Version}}" } ` ) // Individual component info like for "Serving" or "Eventing" // If you want to add new component, read the comment at the top of the file! type Component struct { Version string `json:"version"` Owner string `json:"owner,omitempty"` Repo string `json:"repo,omitempty"` } // make iterable struct type ComponentList map[string]*Component func main() { // Set up context for possible signal inputs to not disrupt cleanup process. // This is not gonna do much for workflows since they finish and shutdown // but in case of local testing - dont leave left over resources on disk/RAM. ctx, cancel := context.WithCancel(context.Background()) defer cancel() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigs cancel() <-sigs os.Exit(130) }() getClient := func(token string) *github.Client { if token != "" { fmt.Println("client with token") return github.NewClient(nil).WithAuthToken(token) } return github.NewClient(nil) } client := getClient(os.Getenv("GITHUB_TOKEN")) // Read source-of-truth .json componentList, err := readVersions(fileJson) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } // update componentList in-situ updated, err := update(ctx, client, &componentList) if err != nil { fmt.Fprintf(os.Stderr, "failed to update %v\n", err) os.Exit(1) } if !updated { // nothing was updated, nothing to do fmt.Println("no newer versions found, re-generating .sh just in case") // regenerate .sh to keep up to date if changed err = writeScript(componentList, fileScript) if err != nil { err = fmt.Errorf("failed to re-generate script: %v", err) fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("all good") os.Exit(0) } if err := writeFiles(componentList, fileScript, fileJson); err != nil { err = fmt.Errorf("failed to write files: %v", err) fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("files updated!") } // do the update for each repo defined func update(ctx context.Context, client *github.Client, cl *ComponentList) (bool, error) { fmt.Println("Getting latest releases") updated := false for _, c := range *cl { if c.Owner == "" || c.Repo == "" { //skipping auto updates continue } newV, err := getLatestVersion(ctx, client, c.Owner, c.Repo) if err != nil { err = fmt.Errorf("error while getting latest v of %s/%s: %v", c.Owner, c.Repo, err) return false, err } if c.Version != newV { fmt.Printf("bump %v: %v --> %v\n", fmt.Sprintf("%s/%s", c.Owner, c.Repo), c.Version, newV) c.Version = newV updated = true } } return updated, nil } // read (unmarshal) component versions from .json func readVersions(file string) (c ComponentList, err error) { fmt.Println("Reading versions from source-of-truth") data, err := os.ReadFile(file) if err != nil { return } err = json.Unmarshal(data, &c) if err != nil { return } return } // Overwrite the 'source of truth' file - .json and regenerate new script // with new versions from 'v'. // Arguments 'script' & 'json' are paths to files for autogenerated script and // source (json) file respectively. func writeFiles(cl ComponentList, script, json string) error { fmt.Print("Writing files") // write to json err := writeSource(cl, json) if err != nil { return fmt.Errorf("failed to write to json: %v", err) } // write to script file err = writeScript(cl, script) if err != nil { return fmt.Errorf("failed to generate script: %v", err) } return nil } // write to 'source of truth' .json with updated versions (if pulled latest) func writeSource(cl ComponentList, file string) error { vB, err := json.MarshalIndent(cl, "", " ") if err != nil { return fmt.Errorf("cant Marshal versions: %v", err) } f, err := os.Create(file) if err != nil { return err } defer f.Close() _, err = f.Write(append(vB, '\n')) // append newline for reviewdog return err } // write the autogenerated script based on 'cl' func writeScript(cl ComponentList, file string) error { tmpl, err := template.New("versions").Parse(versionsScriptTemplate) if err != nil { return err } f, err := os.Create(file) if err != nil { return err } defer f.Close() if err := tmpl.Execute(f, cl); err != nil { return err } return nil } // get latest version of owner/repo via GH API func getLatestVersion(ctx context.Context, client *github.Client, owner string, repo string) (v string, err error) { rr, res, err := client.Repositories.GetLatestRelease(ctx, owner, repo) if err != nil { err = fmt.Errorf("error: request for latest %s release: %v", owner+"/"+repo, err) return } if res.StatusCode < 200 && res.StatusCode > 299 { err = fmt.Errorf("error: Return status code of request for latest %s release is %d", owner+"/"+repo, res.StatusCode) return } v = *rr.Name if v == "" { return "", fmt.Errorf("internal error: returned latest release name is empty for '%s'", repo) } return v, nil }