Skip to content
Open
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
23 changes: 23 additions & 0 deletions .github/workflows/components.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Update kn components

permissions:
contents: write
pull-requests: write

on:
schedule:
# every 4 hours
- cron: '0 */4 * * *'

jobs:
update:
name: Update components
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
- name: Run script
env:
GITHUB_TOKEN: ${{ GITHUB.TOKEN }}
run: make hack-generate-components

200 changes: 186 additions & 14 deletions hack/cmd/components/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"html/template"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"

github "github.com/google/go-github/v68/github"
Expand Down Expand Up @@ -72,6 +75,10 @@ type Component struct {
type ComponentList map[string]*Component

func main() {
// Parse flags
localMode := flag.Bool("local", false, "run in local mode (skip PR creation)")
flag.Parse()

// 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.
Expand All @@ -91,6 +98,7 @@ func main() {
fmt.Println("client with token")
return github.NewClient(nil).WithAuthToken(token)
}
fmt.Println("client without token")
return github.NewClient(nil)
}
client := getClient(os.Getenv("GITHUB_TOKEN"))
Expand All @@ -102,34 +110,67 @@ func main() {
os.Exit(1)
}

// update componentList in-situ
// update components in struct
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)
const (
branchName = "bot-auto-update-components"
owner = "knative"
repo = "func"
baseBranch = "main"
)

if !*localMode {
// pull main and checkout/rebase branch
if err := setupBranch(branchName, baseBranch); err != nil {
fmt.Fprintf(os.Stderr, "failed to setup and rebase branch: %v\n", err)
os.Exit(1)
}
fmt.Println("all good")
os.Exit(0)
}

// write files to disk
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!")
if updated {
fmt.Println("files updated!")
} else {
fmt.Println("no component updates; regenerated .sh just in case")
}

if *localMode {
fmt.Println("local update done")
os.Exit(0)
}

hasChanges, err := checkForChanges()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to check for changes: %v\n", err)
os.Exit(1)
}

if hasChanges {
if err := commitChanges(); err != nil {
fmt.Fprintf(os.Stderr, "failed to commit: %v\n", err)
os.Exit(1)
}
}

if err := pushChanges(branchName); err != nil {
fmt.Fprintf(os.Stderr, "failed to push: %v\n", err)
os.Exit(1)
}

if err := ensurePR(ctx, client, owner, repo, branchName, baseBranch); err != nil {
fmt.Fprintf(os.Stderr, "failed to create or update PR: %v\n", err)
os.Exit(1)
}
}

// do the update for each repo defined
Expand Down Expand Up @@ -176,7 +217,7 @@ func readVersions(file string) (c ComponentList, err error) {
// 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")
fmt.Println("writing files")
// write to json
err := writeSource(cl, json)
if err != nil {
Expand Down Expand Up @@ -225,6 +266,137 @@ func writeScript(cl ComponentList, file string) error {
return nil
}

// setupBranch configures git, checks out branch, and pulls with rebase
func setupBranch(branchName, baseBranch string) error {
fmt.Println("setting up git and rebasing branch on main...")
setupScript := fmt.Sprintf(`
git config user.email "[email protected]" && \
git config user.name "Knative Automation" && \
git fetch origin && \
(git switch %s || git switch -c %s origin/%s) && \
git pull --rebase -X theirs origin %s
`, branchName, branchName, baseBranch, baseBranch)

if err := runCommand("sh", "-c", setupScript); err != nil {
return fmt.Errorf("failed to setup and rebase branch: %w", err)
}
fmt.Printf("branch %s rebased on %s\n", branchName, baseBranch)
return nil
}

// checkForChanges checks if there are any uncommitted changes in the working directory
func checkForChanges() (bool, error) {
fmt.Println("checking for changes...")
cmd := exec.Command("git", "status", "--porcelain")
output, err := cmd.Output()
if err != nil {
return false, fmt.Errorf("failed to check git status: %w", err)
}

hasChanges := len(strings.TrimSpace(string(output))) > 0
if hasChanges {
fmt.Println("changes detected")
} else {
fmt.Println("no changes detected")
}
return hasChanges, nil
}

// commitChanges adds and commits changes
func commitChanges() error {
fmt.Println("committing changes...")

commitScript := fmt.Sprintf(`
git add %s %s && \
git commit -m "update components"
`, fileJson, fileScript)

if err := runCommand("sh", "-c", commitScript); err != nil {
return fmt.Errorf("failed to commit: %w", err)
}

fmt.Println("changes committed")
return nil
}

// pushChanges pushes changes to the remote branch (force push since we rebased)
func pushChanges(branchName string) error {
fmt.Println("pushing changes...")

pushScript := fmt.Sprintf(`git push -f --set-upstream origin %s`, branchName)

if err := runCommand("sh", "-c", pushScript); err != nil {
return fmt.Errorf("failed to push: %w", err)
}

fmt.Println("changes pushed")
return nil
}

// ensurePR checks for an existing PR or creates a new one
func ensurePR(ctx context.Context, client *github.Client, owner, repo, branchName, baseBranch string) error {
const prTitle = "chore: update kn components"

fmt.Println("checking for existing PR...")
pr, err := findPRByBranch(ctx, client, owner, repo, branchName)
if err != nil {
return fmt.Errorf("failed to check for existing PR: %w", err)
}

if pr != nil {
fmt.Printf("PR already exists: %s\n", pr.GetHTMLURL())
return nil
}

// Create new PR
fmt.Println("creating new PR...")
prBody := "you most likely need to close&re-open the PR for tests to run properly\n/assign gauron99"
newPR := &github.NewPullRequest{
Title: github.Ptr(prTitle),
Head: github.Ptr(branchName),
Base: github.Ptr(baseBranch),
Body: github.Ptr(prBody),
}

createdPR, _, err := client.PullRequests.Create(ctx, owner, repo, newPR)
if err != nil {
return fmt.Errorf("failed to create PR: %w", err)
}

fmt.Printf("PR created successfully: %s\n", createdPR.GetHTMLURL())
return nil
}

// findPRByBranch searches for an existing PR with the given head branch
func findPRByBranch(ctx context.Context, client *github.Client, owner, repo, branch string) (*github.PullRequest, error) {
opts := &github.PullRequestListOptions{
State: "open",
Head: fmt.Sprintf("%s:%s", owner, branch), // GitHub API requires owner:branch format
ListOptions: github.ListOptions{
PerPage: 100,
},
}

prs, _, err := client.PullRequests.List(ctx, owner, repo, opts)
if err != nil {
return nil, err
}

// Should only return 0 or 1 PR since head branch is unique
if len(prs) > 0 {
return prs[0], nil
}

return nil, nil
}

func runCommand(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

// 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)
Expand Down
3 changes: 0 additions & 3 deletions hack/update-deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,3 @@ set -o pipefail
source "$(go run knative.dev/hack/cmd/script library.sh)"

go_update_deps "$@"

# Update hack components
make hack-generate-components
Loading