From 5c161009fbffa45051379ce4b51bb580632d1e9c Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Sun, 10 May 2026 17:31:40 +0530 Subject: [PATCH 1/4] Add automation commands for updating platform versions and CA bundle - Introduced new Makefile targets: `update-quarkus-platform`, `update-springboot-platform`, and `update-ca-bundle` to automate the process of updating respective platform versions in templates. - Updated GitHub Actions workflows for `update-ca-bundle`, `update-quarkus-platform`, and `update-springboot-platform` to utilize Go scripts instead of Node.js for executing updates. - Added new Go scripts for handling the update logic for CA bundle, Quarkus platform, and Spring Boot platform, including PR creation and version checks. - Implemented shared GitHub client functionality for PR management and version retrieval. This change enhances the automation of platform updates and streamlines the CI/CD process for dependency management. --- .github/workflows/update-ca-bundle.yaml | 10 +- .../workflows/update-quarkus-platform.yaml | 9 +- .../workflows/update-springboot-platform.yaml | 9 +- Makefile | 12 + hack/cmd/shared/github.go | 111 +++++++ hack/cmd/update-ca-bundle/main.go | 100 +++++++ hack/cmd/update-quarkus-platform/main.go | 173 +++++++++++ hack/cmd/update-springboot-platform/main.go | 270 ++++++++++++++++++ 8 files changed, 673 insertions(+), 21 deletions(-) create mode 100644 hack/cmd/shared/github.go create mode 100644 hack/cmd/update-ca-bundle/main.go create mode 100644 hack/cmd/update-quarkus-platform/main.go create mode 100644 hack/cmd/update-springboot-platform/main.go diff --git a/.github/workflows/update-ca-bundle.yaml b/.github/workflows/update-ca-bundle.yaml index 56bb9bcbdf..a434a86c59 100644 --- a/.github/workflows/update-ca-bundle.yaml +++ b/.github/workflows/update-ca-bundle.yaml @@ -20,13 +20,9 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "20" - - name: Install NPM deps. - run: npm install octokit@3.2.1 + - uses: knative/actions/setup-go@main - name: Create PR env: GITHUB_TOKEN: ${{ github.token }} - run: node ./hack/update-ca-bundle.js - + GITHUB_REPOSITORY: ${{ github.repository }} + run: cd hack && go run ./cmd/update-ca-bundle diff --git a/.github/workflows/update-quarkus-platform.yaml b/.github/workflows/update-quarkus-platform.yaml index 11e4284fa0..eb0fe69741 100644 --- a/.github/workflows/update-quarkus-platform.yaml +++ b/.github/workflows/update-quarkus-platform.yaml @@ -21,17 +21,12 @@ jobs: steps: - uses: actions/checkout@v4 - uses: knative/actions/setup-go@main - - uses: actions/setup-node@v4 - with: - node-version: "20" - uses: actions/setup-java@v4 with: java-version: 21 distribution: 'temurin' - - name: Install NPM deps. - run: npm install xml2js@0.6.2 octokit@3.2.1 - name: Create PR env: GITHUB_TOKEN: ${{ github.token }} - run: node ./hack/update-quarkus-platform.js - + GITHUB_REPOSITORY: ${{ github.repository }} + run: cd hack && go run ./cmd/update-quarkus-platform diff --git a/.github/workflows/update-springboot-platform.yaml b/.github/workflows/update-springboot-platform.yaml index feb0b50c5d..6144fe209a 100644 --- a/.github/workflows/update-springboot-platform.yaml +++ b/.github/workflows/update-springboot-platform.yaml @@ -21,17 +21,12 @@ jobs: steps: - uses: actions/checkout@v4 - uses: knative/actions/setup-go@main - - uses: actions/setup-node@v4 - with: - node-version: "20" - uses: actions/setup-java@v4 with: java-version: 21 distribution: 'temurin' - - name: Install NPM deps. - run: npm install xml2js@0.6.2 octokit@3.2.1 yaml@2.4.5 semver@7.6.3 - name: Create PR env: GITHUB_TOKEN: ${{ github.token }} - run: node ./hack/update-springboot-platform.js - + GITHUB_REPOSITORY: ${{ github.repository }} + run: cd hack && go run ./cmd/update-springboot-platform diff --git a/Makefile b/Makefile index e35743e2b0..8a30f172f2 100644 --- a/Makefile +++ b/Makefile @@ -422,6 +422,18 @@ test-hack: __update-builder: # Used in automation cd hack && go run ./cmd/update-builder +.PHONY: update-quarkus-platform +update-quarkus-platform: ## Update Quarkus platform version in templates + cd hack && go run ./cmd/update-quarkus-platform + +.PHONY: update-springboot-platform +update-springboot-platform: ## Update Spring Boot platform version in templates + cd hack && go run ./cmd/update-springboot-platform + +.PHONY: update-ca-bundle +update-ca-bundle: ## Update CA bundle in templates + cd hack && go run ./cmd/update-ca-bundle + .PHONY: setup-githooks setup-githooks: git config --local core.hooksPath .githooks/ diff --git a/hack/cmd/shared/github.go b/hack/cmd/shared/github.go new file mode 100644 index 0000000000..b506986547 --- /dev/null +++ b/hack/cmd/shared/github.go @@ -0,0 +1,111 @@ +package shared + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/google/go-github/v68/github" + "golang.org/x/oauth2" +) + +// NewGHClient constructs an authenticated GitHub client using GITHUB_TOKEN. +func NewGHClient(ctx context.Context) *github.Client { + return github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: os.Getenv("GITHUB_TOKEN"), + }))) +} + +// RepoFromEnv splits GITHUB_REPOSITORY ("owner/repo") into owner and repo. +func RepoFromEnv() (owner, repo string, err error) { + v := os.Getenv("GITHUB_REPOSITORY") + if v == "" { + return "", "", fmt.Errorf("GITHUB_REPOSITORY is not set") + } + parts := strings.SplitN(v, "/", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("GITHUB_REPOSITORY has unexpected format: %q", v) + } + return parts[0], parts[1], nil +} + +// PRExists returns true if any open pull request satisfies pred(pr.GetTitle()). +func PRExists(ctx context.Context, client *github.Client, owner, repo string, pred func(string) bool) (bool, error) { + opts := &github.PullRequestListOptions{ + State: "open", + ListOptions: github.ListOptions{ + PerPage: 10, + }, + } + for { + prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts) + if err != nil { + return false, fmt.Errorf("cannot list pull requests: %w", err) + } + for _, pr := range prs { + if pred(pr.GetTitle()) { + return true, nil + } + } + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + return false, nil +} + +// PrepareBranch configures git identity, creates branchName, runs codegen, +// stages filesToAdd, commits with prTitle as the message, and pushes. +func PrepareBranch(ctx context.Context, branchName, prTitle string, filesToAdd []string) error { + steps := [][]string{ + {"git", "config", "user.email", "automation@knative.team"}, + {"git", "config", "user.name", "Knative Automation"}, + {"git", "checkout", "-b", branchName}, + {"make", "generate/zz_filesystem_generated.go"}, + } + for _, args := range steps { + if err := RunCmd(ctx, args[0], args[1:]...); err != nil { + return err + } + } + + addArgs := append([]string{"add"}, filesToAdd...) + if err := RunCmd(ctx, "git", addArgs...); err != nil { + return err + } + + if err := RunCmd(ctx, "git", "commit", "-m", prTitle); err != nil { + return err + } + + return RunCmd(ctx, "git", "push", "--set-upstream", "origin", branchName) +} + +// CreatePR opens a pull request against main with the given title. +// head should be in the form "owner:branchName". +func CreatePR(ctx context.Context, client *github.Client, owner, repo, title, head string) error { + _, _, err := client.PullRequests.Create(ctx, owner, repo, &github.NewPullRequest{ + Title: github.Ptr(title), + Body: github.Ptr(title), + Base: github.Ptr("main"), + Head: github.Ptr(head), + }) + if err != nil { + return fmt.Errorf("cannot create pull request: %w", err) + } + return nil +} + +// RunCmd runs name with args, streaming stdout/stderr to the process output. +func RunCmd(ctx context.Context, name string, args ...string) error { + cmd := exec.CommandContext(ctx, name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("command %q failed: %w", name+" "+strings.Join(args, " "), err) + } + return nil +} diff --git a/hack/cmd/update-ca-bundle/main.go b/hack/cmd/update-ca-bundle/main.go new file mode 100644 index 0000000000..7c453e8df7 --- /dev/null +++ b/hack/cmd/update-ca-bundle/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "os/signal" + "syscall" + "time" + + "knative.dev/func/hack/cmd/shared" +) + +const ( + caBundlePath = "templates/certs/ca-certificates.crt" + prTitle = "chore: update CA bundle" +) + +func main() { + 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) + }() + + if err := run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } + fmt.Println("OK!") +} + +func run(ctx context.Context) error { + owner, repo, err := shared.RepoFromEnv() + if err != nil { + return err + } + ghClient := shared.NewGHClient(ctx) + + exists, err := shared.PRExists(ctx, ghClient, owner, repo, func(title string) bool { + return title == prTitle + }) + if err != nil { + return fmt.Errorf("cannot check for existing PR: %w", err) + } + if exists { + fmt.Println("The PR already exists!") + return nil + } + + if err := shared.RunCmd(ctx, "make", caBundlePath); err != nil { + return fmt.Errorf("cannot update CA bundle: %w", err) + } + + changed, err := hasChanges(ctx) + if err != nil { + return err + } + if !changed { + fmt.Println("The CA bundle is up to date. Nothing to be done.") + return nil + } + + branchName := fmt.Sprintf("update-ca-bundle-%s", time.Now().UTC().Format("2006-01-02")) + + if err := shared.PrepareBranch(ctx, branchName, prTitle, []string{ + "generate/zz_filesystem_generated.go", caBundlePath, + }); err != nil { + return fmt.Errorf("cannot prepare branch: %w", err) + } + + if err := shared.CreatePR(ctx, ghClient, owner, repo, prTitle, fmt.Sprintf("%s:%s", owner, branchName)); err != nil { + return err + } + fmt.Println("The PR has been created!") + return nil +} + +// hasChanges reports whether caBundlePath has uncommitted changes. +// git diff --exit-code exits with 0 when there are no changes, 1 when there are. +func hasChanges(ctx context.Context) (bool, error) { + cmd := exec.CommandContext(ctx, "git", "diff", "--exit-code", "--", caBundlePath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err == nil { + return false, nil + } + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return true, nil + } + return false, fmt.Errorf("git diff failed unexpectedly: %w", err) +} diff --git a/hack/cmd/update-quarkus-platform/main.go b/hack/cmd/update-quarkus-platform/main.go new file mode 100644 index 0000000000..8f4e022000 --- /dev/null +++ b/hack/cmd/update-quarkus-platform/main.go @@ -0,0 +1,173 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "regexp" + "syscall" + + "knative.dev/func/hack/cmd/shared" +) + +const ( + cePomPath = "templates/quarkus/cloudevents/pom.xml" + httpPomPath = "templates/quarkus/http/pom.xml" + + quarkusPlatformAPI = "https://code.quarkus.io/api/platforms" + + quarkusVersionTag = "quarkus.platform.version" + quarkusVersionExpr = `([^<]+)` +) + +var quarkusVersionRe = regexp.MustCompile(quarkusVersionExpr) + +type quarkusPlatformResponse struct { + Platforms []struct { + Streams []struct { + Releases []struct { + QuarkusCoreVersion string `json:"quarkusCoreVersion"` + } `json:"releases"` + } `json:"streams"` + } `json:"platforms"` +} + +func main() { + 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) + }() + + if err := run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } + fmt.Println("OK!") +} + +func run(ctx context.Context) error { + latestVersion, err := getLatestQuarkusVersion(ctx) + if err != nil { + return fmt.Errorf("cannot get latest Quarkus platform version: %w", err) + } + fmt.Printf("Latest Quarkus platform version: %s\n", latestVersion) + + ceVersion, err := versionFromPOM(cePomPath) + if err != nil { + return fmt.Errorf("cannot read version from %s: %w", cePomPath, err) + } + httpVersion, err := versionFromPOM(httpPomPath) + if err != nil { + return fmt.Errorf("cannot read version from %s: %w", httpPomPath, err) + } + + if ceVersion == latestVersion && httpVersion == latestVersion { + fmt.Println("Quarkus platform is up-to-date!") + return nil + } + + owner, repo, err := shared.RepoFromEnv() + if err != nil { + return err + } + ghClient := shared.NewGHClient(ctx) + + prTitle := fmt.Sprintf("chore: update Quarkus platform version to %s", latestVersion) + exists, err := shared.PRExists(ctx, ghClient, owner, repo, func(title string) bool { + return title == prTitle + }) + if err != nil { + return fmt.Errorf("cannot check for existing PR: %w", err) + } + if exists { + fmt.Println("The PR already exists!") + return nil + } + + for _, pomPath := range []string{cePomPath, httpPomPath} { + if err := updatePOM(pomPath, latestVersion); err != nil { + return fmt.Errorf("cannot update %s: %w", pomPath, err) + } + } + + smokeCmd := []string{"make", "test-quarkus"} + if err := shared.RunCmd(ctx, smokeCmd[0], smokeCmd[1:]...); err != nil { + return fmt.Errorf("smoke test failed: %w", err) + } + + branchName := fmt.Sprintf("update-quarkus-platform-%s", latestVersion) + if err := shared.PrepareBranch(ctx, branchName, prTitle, []string{ + cePomPath, httpPomPath, "generate/zz_filesystem_generated.go", + }); err != nil { + return fmt.Errorf("cannot prepare branch: %w", err) + } + + if err := shared.CreatePR(ctx, ghClient, owner, repo, prTitle, fmt.Sprintf("%s:%s", owner, branchName)); err != nil { + return err + } + fmt.Println("The PR has been created!") + return nil +} + +func getLatestQuarkusVersion(ctx context.Context) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, quarkusPlatformAPI, nil) + if err != nil { + return "", err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status %d from %s", resp.StatusCode, quarkusPlatformAPI) + } + + var data quarkusPlatformResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return "", err + } + if len(data.Platforms) == 0 || + len(data.Platforms[0].Streams) == 0 || + len(data.Platforms[0].Streams[0].Releases) == 0 { + return "", fmt.Errorf("unexpected response structure from Quarkus platform API") + } + v := data.Platforms[0].Streams[0].Releases[0].QuarkusCoreVersion + if v == "" { + return "", fmt.Errorf("quarkusCoreVersion is empty in API response") + } + return v, nil +} + +func versionFromPOM(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + m := quarkusVersionRe.FindSubmatch(data) + if len(m) < 2 { + return "", fmt.Errorf("cannot find <%s> in %s", quarkusVersionTag, path) + } + return string(m[1]), nil +} + +func updatePOM(path, newVersion string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + updated := quarkusVersionRe.ReplaceAll(data, + []byte(fmt.Sprintf("<%s>%s", quarkusVersionTag, newVersion, quarkusVersionTag))) + return os.WriteFile(path, updated, 0644) +} diff --git a/hack/cmd/update-springboot-platform/main.go b/hack/cmd/update-springboot-platform/main.go new file mode 100644 index 0000000000..ee1b5a28d5 --- /dev/null +++ b/hack/cmd/update-springboot-platform/main.go @@ -0,0 +1,270 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "regexp" + "strings" + "syscall" + + "github.com/blang/semver/v4" + "gopkg.in/yaml.v3" + + "knative.dev/func/hack/cmd/shared" +) + +const ( + cePomPath = "templates/springboot/cloudevents/pom.xml" + httpPomPath = "templates/springboot/http/pom.xml" + + springBootReleasesAPI = "https://api.github.com/repos/spring-projects/spring-boot/releases/latest" + springCloudBOMURL = "https://raw.githubusercontent.com/spring-io/start.spring.io/main/start-site/src/main/resources/application.yml" + + springCloudVersionTag = "spring-cloud.version" + springCloudVersionExpr = `([^<]+)` +) + +// parentVersionRe matches the version inside the spring-boot-starter-parent block. +var ( + parentVersionRe = regexp.MustCompile(`(spring-boot-starter-parent\s*)([^<]+)()`) + springCloudVersionRe = regexp.MustCompile(springCloudVersionExpr) +) + +type springBootRelease struct { + TagName string `json:"tag_name"` + Draft bool `json:"draft"` +} + +func main() { + 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) + }() + + if err := run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) + } + fmt.Println("OK!") +} + +func run(ctx context.Context) error { + latestVersion, err := getLatestSpringBootVersion(ctx) + if err != nil { + return fmt.Errorf("cannot get latest Spring Boot version: %w", err) + } + if latestVersion == "" { + fmt.Println("Spring Boot platform latest version is not ready to use!") + return nil + } + fmt.Printf("Latest Spring Boot version: %s\n", latestVersion) + + ceVersion, err := parentVersionFromPOM(cePomPath) + if err != nil { + return fmt.Errorf("cannot read version from %s: %w", cePomPath, err) + } + httpVersion, err := parentVersionFromPOM(httpPomPath) + if err != nil { + return fmt.Errorf("cannot read version from %s: %w", httpPomPath, err) + } + + if ceVersion == latestVersion && httpVersion == latestVersion { + fmt.Println("Spring Boot platform is up-to-date!") + return nil + } + + owner, repo, err := shared.RepoFromEnv() + if err != nil { + return err + } + ghClient := shared.NewGHClient(ctx) + + prTitle := fmt.Sprintf("chore: update Springboot platform version to %s", latestVersion) + exists, err := shared.PRExists(ctx, ghClient, owner, repo, func(title string) bool { + return title == prTitle + }) + if err != nil { + return fmt.Errorf("cannot check for existing PR: %w", err) + } + if exists { + fmt.Println("The PR already exists!") + return nil + } + + springCloudVersion, err := getCompatibleSpringCloudVersion(ctx, latestVersion) + if err != nil { + return fmt.Errorf("cannot find compatible spring-cloud version: %w", err) + } + fmt.Printf("Compatible spring-cloud version: %s\n", springCloudVersion) + + for _, pomPath := range []string{cePomPath, httpPomPath} { + if err := updatePOM(pomPath, latestVersion, springCloudVersion); err != nil { + return fmt.Errorf("cannot update %s: %w", pomPath, err) + } + } + + if err := shared.RunCmd(ctx, "make", "test-springboot"); err != nil { + return fmt.Errorf("smoke test failed: %w", err) + } + + branchName := fmt.Sprintf("update-springboot-platform-%s", latestVersion) + if err := shared.PrepareBranch(ctx, branchName, prTitle, []string{ + cePomPath, httpPomPath, "generate/zz_filesystem_generated.go", + }); err != nil { + return fmt.Errorf("cannot prepare branch: %w", err) + } + + if err := shared.CreatePR(ctx, ghClient, owner, repo, prTitle, fmt.Sprintf("%s:%s", owner, branchName)); err != nil { + return err + } + fmt.Println("The PR has been created!") + return nil +} + +func getLatestSpringBootVersion(ctx context.Context) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, springBootReleasesAPI, nil) + if err != nil { + return "", err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status %d from %s", resp.StatusCode, springBootReleasesAPI) + } + + var release springBootRelease + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return "", err + } + if release.Draft { + return "", nil + } + // Strip any leading alphabetic characters (e.g. "v3.5.12" → "3.5.12") + return strings.TrimLeft(release.TagName, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), nil +} + +func parentVersionFromPOM(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + m := parentVersionRe.FindSubmatch(data) + if len(m) < 4 { + return "", fmt.Errorf("cannot find spring-boot-starter-parent version in %s", path) + } + return string(m[2]), nil +} + +func updatePOM(path, newVersion, newSpringCloudVersion string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + // Replace parent version + updated := parentVersionRe.ReplaceAll(data, []byte("${1}"+newVersion+"${3}")) + + // Replace spring-cloud.version + updated = springCloudVersionRe.ReplaceAll(updated, + []byte(fmt.Sprintf("<%s>%s", springCloudVersionTag, newSpringCloudVersion, springCloudVersionTag))) + + return os.WriteFile(path, updated, 0644) +} + +// springCloudBOM is the minimal structure we need from start.spring.io's application.yml. +type springCloudBOM struct { + Initializr struct { + Env struct { + Boms map[string]struct { + Mappings []struct { + CompatibilityRange string `yaml:"compatibilityRange"` + Version string `yaml:"version"` + } `yaml:"mappings"` + } `yaml:"boms"` + } `yaml:"env"` + } `yaml:"initializr"` +} + +func getCompatibleSpringCloudVersion(ctx context.Context, springBootVersion string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, springCloudBOMURL, nil) + if err != nil { + return "", err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status %d from %s", resp.StatusCode, springCloudBOMURL) + } + + var bom springCloudBOM + if err := yaml.NewDecoder(resp.Body).Decode(&bom); err != nil { + return "", fmt.Errorf("cannot decode spring-cloud BOM YAML: %w", err) + } + + sc, ok := bom.Initializr.Env.Boms["spring-cloud"] + if !ok { + return "", fmt.Errorf("spring-cloud entry not found in BOM") + } + + target, err := semver.ParseTolerant(springBootVersion) + if err != nil { + return "", fmt.Errorf("cannot parse Spring Boot version %q: %w", springBootVersion, err) + } + + for _, m := range sc.Mappings { + var begin, end semver.Version + r := m.CompatibilityRange + + if strings.HasPrefix(r, "[") { + // Format: [begin,end) + inner := strings.Trim(r, "[]") + parts := strings.SplitN(inner, ",", 2) + if len(parts) != 2 { + continue + } + begin, err = semver.ParseTolerant(strings.TrimSpace(parts[0])) + if err != nil { + continue + } + end, err = semver.ParseTolerant(strings.TrimSpace(parts[1])) + if err != nil { + continue + } + } else { + // Format: begin (open-ended) + begin, err = semver.ParseTolerant(r) + if err != nil { + continue + } + end, err = semver.ParseTolerant("999.999.999") + if err != nil { + continue + } + } + + if target.GTE(begin) && target.LT(end) { + return m.Version, nil + } + } + + return "", fmt.Errorf("no compatible spring-cloud version found for Spring Boot %s", springBootVersion) +} From a40ba410e606f03996337d299aef64b89492444e Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Sun, 10 May 2026 18:21:21 +0530 Subject: [PATCH 2/4] Refactor automation scripts for platform updates and CA bundle management - Updated Makefile targets to streamline the execution of platform updates for Quarkus, Spring Boot, and CA bundle. - Replaced Node.js scripts with Go scripts for better performance and maintainability in GitHub Actions workflows. - Enhanced the update logic for CA bundle, Quarkus platform, and Spring Boot platform, including automated PR creation and version checks. - Removed outdated JavaScript files related to platform updates, consolidating functionality within Go. These changes improve the automation process for dependency management and enhance the CI/CD workflow. --- .github/workflows/update-ca-bundle.yaml | 25 ++- .../workflows/update-quarkus-platform.yaml | 27 ++- .../workflows/update-springboot-platform.yaml | 27 ++- Makefile | 6 +- hack/cmd/shared/github.go | 94 +-------- hack/cmd/update-ca-bundle/main.go | 68 +------ hack/cmd/update-quarkus-platform/main.go | 38 +--- hack/cmd/update-springboot-platform/main.go | 125 +++++------- hack/update-ca-bundle.js | 110 ----------- hack/update-quarkus-platform.js | 137 -------------- hack/update-springboot-platform.js | 179 ------------------ 11 files changed, 125 insertions(+), 711 deletions(-) delete mode 100644 hack/update-ca-bundle.js delete mode 100644 hack/update-quarkus-platform.js delete mode 100644 hack/update-springboot-platform.js diff --git a/.github/workflows/update-ca-bundle.yaml b/.github/workflows/update-ca-bundle.yaml index a434a86c59..5135368931 100644 --- a/.github/workflows/update-ca-bundle.yaml +++ b/.github/workflows/update-ca-bundle.yaml @@ -21,8 +21,23 @@ jobs: steps: - uses: actions/checkout@v4 - uses: knative/actions/setup-go@main - - name: Create PR - env: - GITHUB_TOKEN: ${{ github.token }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: cd hack && go run ./cmd/update-ca-bundle + - name: Update CA bundle + run: go run ./hack/cmd/update-ca-bundle/main.go + - name: Run make generate + run: make generate/zz_filesystem_generated.go + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ github.token }} + commit-message: 'chore: update CA bundle' + title: 'chore: update CA bundle' + body: | + This PR updates the CA bundle in the embedded templates. + + This PR was automatically generated by the [update-ca-bundle workflow](https://github.com/${{ github.repository }}/actions/workflows/update-ca-bundle.yaml). + branch: update-ca-bundle + delete-branch: true + committer: Knative Automation + author: Knative Automation + assignees: gauron99, matejvasek, lkingland + base: main diff --git a/.github/workflows/update-quarkus-platform.yaml b/.github/workflows/update-quarkus-platform.yaml index eb0fe69741..5e860e919f 100644 --- a/.github/workflows/update-quarkus-platform.yaml +++ b/.github/workflows/update-quarkus-platform.yaml @@ -25,8 +25,25 @@ jobs: with: java-version: 21 distribution: 'temurin' - - name: Create PR - env: - GITHUB_TOKEN: ${{ github.token }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: cd hack && go run ./cmd/update-quarkus-platform + - name: Update Quarkus platform version + run: go run ./hack/cmd/update-quarkus-platform/main.go + - name: Run make generate + run: make generate/zz_filesystem_generated.go + - name: Run smoke tests + run: make test-quarkus + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ github.token }} + commit-message: 'chore: update Quarkus platform version' + title: 'chore: update Quarkus platform version' + body: | + This PR updates the Quarkus platform version in the Quarkus scaffolding templates to the latest version. + + This PR was automatically generated by the [update-quarkus-platform workflow](https://github.com/${{ github.repository }}/actions/workflows/update-quarkus-platform.yaml). + branch: update-quarkus-platform + delete-branch: true + committer: Knative Automation + author: Knative Automation + assignees: gauron99, matejvasek, lkingland + base: main diff --git a/.github/workflows/update-springboot-platform.yaml b/.github/workflows/update-springboot-platform.yaml index 6144fe209a..6329c8bfc8 100644 --- a/.github/workflows/update-springboot-platform.yaml +++ b/.github/workflows/update-springboot-platform.yaml @@ -25,8 +25,25 @@ jobs: with: java-version: 21 distribution: 'temurin' - - name: Create PR - env: - GITHUB_TOKEN: ${{ github.token }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: cd hack && go run ./cmd/update-springboot-platform + - name: Update Spring Boot platform version + run: go run ./hack/cmd/update-springboot-platform/main.go + - name: Run make generate + run: make generate/zz_filesystem_generated.go + - name: Run smoke tests + run: make test-springboot + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ github.token }} + commit-message: 'chore: update Spring Boot platform version' + title: 'chore: update Spring Boot platform version' + body: | + This PR updates the Spring Boot platform version in the Spring Boot scaffolding templates to the latest version. + + This PR was automatically generated by the [update-springboot-platform workflow](https://github.com/${{ github.repository }}/actions/workflows/update-springboot-platform.yaml). + branch: update-springboot-platform + delete-branch: true + committer: Knative Automation + author: Knative Automation + assignees: gauron99, matejvasek, lkingland + base: main diff --git a/Makefile b/Makefile index 8a30f172f2..a3a7467d89 100644 --- a/Makefile +++ b/Makefile @@ -424,15 +424,15 @@ __update-builder: # Used in automation .PHONY: update-quarkus-platform update-quarkus-platform: ## Update Quarkus platform version in templates - cd hack && go run ./cmd/update-quarkus-platform + go run ./hack/cmd/update-quarkus-platform/main.go .PHONY: update-springboot-platform update-springboot-platform: ## Update Spring Boot platform version in templates - cd hack && go run ./cmd/update-springboot-platform + go run ./hack/cmd/update-springboot-platform/main.go .PHONY: update-ca-bundle update-ca-bundle: ## Update CA bundle in templates - cd hack && go run ./cmd/update-ca-bundle + go run ./hack/cmd/update-ca-bundle/main.go .PHONY: setup-githooks setup-githooks: diff --git a/hack/cmd/shared/github.go b/hack/cmd/shared/github.go index b506986547..596e8534cd 100644 --- a/hack/cmd/shared/github.go +++ b/hack/cmd/shared/github.go @@ -3,101 +3,15 @@ package shared import ( "context" "fmt" + "net/http" "os" "os/exec" "strings" - - "github.com/google/go-github/v68/github" - "golang.org/x/oauth2" + "time" ) -// NewGHClient constructs an authenticated GitHub client using GITHUB_TOKEN. -func NewGHClient(ctx context.Context) *github.Client { - return github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: os.Getenv("GITHUB_TOKEN"), - }))) -} - -// RepoFromEnv splits GITHUB_REPOSITORY ("owner/repo") into owner and repo. -func RepoFromEnv() (owner, repo string, err error) { - v := os.Getenv("GITHUB_REPOSITORY") - if v == "" { - return "", "", fmt.Errorf("GITHUB_REPOSITORY is not set") - } - parts := strings.SplitN(v, "/", 2) - if len(parts) != 2 { - return "", "", fmt.Errorf("GITHUB_REPOSITORY has unexpected format: %q", v) - } - return parts[0], parts[1], nil -} - -// PRExists returns true if any open pull request satisfies pred(pr.GetTitle()). -func PRExists(ctx context.Context, client *github.Client, owner, repo string, pred func(string) bool) (bool, error) { - opts := &github.PullRequestListOptions{ - State: "open", - ListOptions: github.ListOptions{ - PerPage: 10, - }, - } - for { - prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts) - if err != nil { - return false, fmt.Errorf("cannot list pull requests: %w", err) - } - for _, pr := range prs { - if pred(pr.GetTitle()) { - return true, nil - } - } - if resp.NextPage == 0 { - break - } - opts.Page = resp.NextPage - } - return false, nil -} - -// PrepareBranch configures git identity, creates branchName, runs codegen, -// stages filesToAdd, commits with prTitle as the message, and pushes. -func PrepareBranch(ctx context.Context, branchName, prTitle string, filesToAdd []string) error { - steps := [][]string{ - {"git", "config", "user.email", "automation@knative.team"}, - {"git", "config", "user.name", "Knative Automation"}, - {"git", "checkout", "-b", branchName}, - {"make", "generate/zz_filesystem_generated.go"}, - } - for _, args := range steps { - if err := RunCmd(ctx, args[0], args[1:]...); err != nil { - return err - } - } - - addArgs := append([]string{"add"}, filesToAdd...) - if err := RunCmd(ctx, "git", addArgs...); err != nil { - return err - } - - if err := RunCmd(ctx, "git", "commit", "-m", prTitle); err != nil { - return err - } - - return RunCmd(ctx, "git", "push", "--set-upstream", "origin", branchName) -} - -// CreatePR opens a pull request against main with the given title. -// head should be in the form "owner:branchName". -func CreatePR(ctx context.Context, client *github.Client, owner, repo, title, head string) error { - _, _, err := client.PullRequests.Create(ctx, owner, repo, &github.NewPullRequest{ - Title: github.Ptr(title), - Body: github.Ptr(title), - Base: github.Ptr("main"), - Head: github.Ptr(head), - }) - if err != nil { - return fmt.Errorf("cannot create pull request: %w", err) - } - return nil -} +// HTTPClient is used for all outbound HTTP requests; has a 30-second timeout. +var HTTPClient = &http.Client{Timeout: 30 * time.Second} // RunCmd runs name with args, streaming stdout/stderr to the process output. func RunCmd(ctx context.Context, name string, args ...string) error { diff --git a/hack/cmd/update-ca-bundle/main.go b/hack/cmd/update-ca-bundle/main.go index 7c453e8df7..002ab11ff0 100644 --- a/hack/cmd/update-ca-bundle/main.go +++ b/hack/cmd/update-ca-bundle/main.go @@ -4,18 +4,13 @@ import ( "context" "fmt" "os" - "os/exec" "os/signal" "syscall" - "time" "knative.dev/func/hack/cmd/shared" ) -const ( - caBundlePath = "templates/certs/ca-certificates.crt" - prTitle = "chore: update CA bundle" -) +const caBundleMakeTarget = "templates/certs/ca-certificates.crt" func main() { ctx, cancel := context.WithCancel(context.Background()) @@ -34,67 +29,8 @@ func main() { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } - fmt.Println("OK!") } func run(ctx context.Context) error { - owner, repo, err := shared.RepoFromEnv() - if err != nil { - return err - } - ghClient := shared.NewGHClient(ctx) - - exists, err := shared.PRExists(ctx, ghClient, owner, repo, func(title string) bool { - return title == prTitle - }) - if err != nil { - return fmt.Errorf("cannot check for existing PR: %w", err) - } - if exists { - fmt.Println("The PR already exists!") - return nil - } - - if err := shared.RunCmd(ctx, "make", caBundlePath); err != nil { - return fmt.Errorf("cannot update CA bundle: %w", err) - } - - changed, err := hasChanges(ctx) - if err != nil { - return err - } - if !changed { - fmt.Println("The CA bundle is up to date. Nothing to be done.") - return nil - } - - branchName := fmt.Sprintf("update-ca-bundle-%s", time.Now().UTC().Format("2006-01-02")) - - if err := shared.PrepareBranch(ctx, branchName, prTitle, []string{ - "generate/zz_filesystem_generated.go", caBundlePath, - }); err != nil { - return fmt.Errorf("cannot prepare branch: %w", err) - } - - if err := shared.CreatePR(ctx, ghClient, owner, repo, prTitle, fmt.Sprintf("%s:%s", owner, branchName)); err != nil { - return err - } - fmt.Println("The PR has been created!") - return nil -} - -// hasChanges reports whether caBundlePath has uncommitted changes. -// git diff --exit-code exits with 0 when there are no changes, 1 when there are. -func hasChanges(ctx context.Context) (bool, error) { - cmd := exec.CommandContext(ctx, "git", "diff", "--exit-code", "--", caBundlePath) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err == nil { - return false, nil - } - if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { - return true, nil - } - return false, fmt.Errorf("git diff failed unexpectedly: %w", err) + return shared.RunCmd(ctx, "make", caBundleMakeTarget) } diff --git a/hack/cmd/update-quarkus-platform/main.go b/hack/cmd/update-quarkus-platform/main.go index 8f4e022000..f81bbc9adc 100644 --- a/hack/cmd/update-quarkus-platform/main.go +++ b/hack/cmd/update-quarkus-platform/main.go @@ -52,7 +52,6 @@ func main() { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } - fmt.Println("OK!") } func run(ctx context.Context) error { @@ -76,46 +75,11 @@ func run(ctx context.Context) error { return nil } - owner, repo, err := shared.RepoFromEnv() - if err != nil { - return err - } - ghClient := shared.NewGHClient(ctx) - - prTitle := fmt.Sprintf("chore: update Quarkus platform version to %s", latestVersion) - exists, err := shared.PRExists(ctx, ghClient, owner, repo, func(title string) bool { - return title == prTitle - }) - if err != nil { - return fmt.Errorf("cannot check for existing PR: %w", err) - } - if exists { - fmt.Println("The PR already exists!") - return nil - } - for _, pomPath := range []string{cePomPath, httpPomPath} { if err := updatePOM(pomPath, latestVersion); err != nil { return fmt.Errorf("cannot update %s: %w", pomPath, err) } } - - smokeCmd := []string{"make", "test-quarkus"} - if err := shared.RunCmd(ctx, smokeCmd[0], smokeCmd[1:]...); err != nil { - return fmt.Errorf("smoke test failed: %w", err) - } - - branchName := fmt.Sprintf("update-quarkus-platform-%s", latestVersion) - if err := shared.PrepareBranch(ctx, branchName, prTitle, []string{ - cePomPath, httpPomPath, "generate/zz_filesystem_generated.go", - }); err != nil { - return fmt.Errorf("cannot prepare branch: %w", err) - } - - if err := shared.CreatePR(ctx, ghClient, owner, repo, prTitle, fmt.Sprintf("%s:%s", owner, branchName)); err != nil { - return err - } - fmt.Println("The PR has been created!") return nil } @@ -124,7 +88,7 @@ func getLatestQuarkusVersion(ctx context.Context) (string, error) { if err != nil { return "", err } - resp, err := http.DefaultClient.Do(req) + resp, err := shared.HTTPClient.Do(req) if err != nil { return "", err } diff --git a/hack/cmd/update-springboot-platform/main.go b/hack/cmd/update-springboot-platform/main.go index ee1b5a28d5..9762f21cca 100644 --- a/hack/cmd/update-springboot-platform/main.go +++ b/hack/cmd/update-springboot-platform/main.go @@ -30,7 +30,7 @@ const ( // parentVersionRe matches the version inside the spring-boot-starter-parent block. var ( - parentVersionRe = regexp.MustCompile(`(spring-boot-starter-parent\s*)([^<]+)()`) + parentVersionRe = regexp.MustCompile(`(spring-boot-starter-parent\s*)([^<]+)()`) springCloudVersionRe = regexp.MustCompile(springCloudVersionExpr) ) @@ -39,6 +39,23 @@ type springBootRelease struct { Draft bool `json:"draft"` } +// springCloudMapping is one entry from the start.spring.io BOM. +type springCloudMapping struct { + CompatibilityRange string `yaml:"compatibilityRange"` + Version string `yaml:"version"` +} + +// springCloudBOM is the minimal structure we need from start.spring.io's application.yml. +type springCloudBOM struct { + Initializr struct { + Env struct { + Boms map[string]struct { + Mappings []springCloudMapping `yaml:"mappings"` + } `yaml:"boms"` + } `yaml:"env"` + } `yaml:"initializr"` +} + func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -56,7 +73,6 @@ func main() { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } - fmt.Println("OK!") } func run(ctx context.Context) error { @@ -84,24 +100,6 @@ func run(ctx context.Context) error { return nil } - owner, repo, err := shared.RepoFromEnv() - if err != nil { - return err - } - ghClient := shared.NewGHClient(ctx) - - prTitle := fmt.Sprintf("chore: update Springboot platform version to %s", latestVersion) - exists, err := shared.PRExists(ctx, ghClient, owner, repo, func(title string) bool { - return title == prTitle - }) - if err != nil { - return fmt.Errorf("cannot check for existing PR: %w", err) - } - if exists { - fmt.Println("The PR already exists!") - return nil - } - springCloudVersion, err := getCompatibleSpringCloudVersion(ctx, latestVersion) if err != nil { return fmt.Errorf("cannot find compatible spring-cloud version: %w", err) @@ -113,22 +111,6 @@ func run(ctx context.Context) error { return fmt.Errorf("cannot update %s: %w", pomPath, err) } } - - if err := shared.RunCmd(ctx, "make", "test-springboot"); err != nil { - return fmt.Errorf("smoke test failed: %w", err) - } - - branchName := fmt.Sprintf("update-springboot-platform-%s", latestVersion) - if err := shared.PrepareBranch(ctx, branchName, prTitle, []string{ - cePomPath, httpPomPath, "generate/zz_filesystem_generated.go", - }); err != nil { - return fmt.Errorf("cannot prepare branch: %w", err) - } - - if err := shared.CreatePR(ctx, ghClient, owner, repo, prTitle, fmt.Sprintf("%s:%s", owner, branchName)); err != nil { - return err - } - fmt.Println("The PR has been created!") return nil } @@ -137,7 +119,10 @@ func getLatestSpringBootVersion(ctx context.Context) (string, error) { if err != nil { return "", err } - resp, err := http.DefaultClient.Do(req) + if token := os.Getenv("GITHUB_TOKEN"); token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + resp, err := shared.HTTPClient.Do(req) if err != nil { return "", err } @@ -154,8 +139,7 @@ func getLatestSpringBootVersion(ctx context.Context) (string, error) { if release.Draft { return "", nil } - // Strip any leading alphabetic characters (e.g. "v3.5.12" → "3.5.12") - return strings.TrimLeft(release.TagName, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), nil + return strings.TrimPrefix(release.TagName, "v"), nil } func parentVersionFromPOM(path string) (string, error) { @@ -175,37 +159,18 @@ func updatePOM(path, newVersion, newSpringCloudVersion string) error { if err != nil { return err } - - // Replace parent version updated := parentVersionRe.ReplaceAll(data, []byte("${1}"+newVersion+"${3}")) - - // Replace spring-cloud.version updated = springCloudVersionRe.ReplaceAll(updated, []byte(fmt.Sprintf("<%s>%s", springCloudVersionTag, newSpringCloudVersion, springCloudVersionTag))) - return os.WriteFile(path, updated, 0644) } -// springCloudBOM is the minimal structure we need from start.spring.io's application.yml. -type springCloudBOM struct { - Initializr struct { - Env struct { - Boms map[string]struct { - Mappings []struct { - CompatibilityRange string `yaml:"compatibilityRange"` - Version string `yaml:"version"` - } `yaml:"mappings"` - } `yaml:"boms"` - } `yaml:"env"` - } `yaml:"initializr"` -} - func getCompatibleSpringCloudVersion(ctx context.Context, springBootVersion string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, springCloudBOMURL, nil) if err != nil { return "", err } - resp, err := http.DefaultClient.Do(req) + resp, err := shared.HTTPClient.Do(req) if err != nil { return "", err } @@ -225,45 +190,57 @@ func getCompatibleSpringCloudVersion(ctx context.Context, springBootVersion stri return "", fmt.Errorf("spring-cloud entry not found in BOM") } + return resolveSpringCloudVersion(springBootVersion, sc.Mappings) +} + +// resolveSpringCloudVersion finds the spring-cloud version compatible with the +// given springBootVersion by evaluating each mapping's compatibilityRange. +// +// Range format mirrors Maven version ranges: +// - "[begin,end)" — begin inclusive, end exclusive +// - "begin" — begin inclusive, no upper bound +func resolveSpringCloudVersion(springBootVersion string, mappings []springCloudMapping) (string, error) { target, err := semver.ParseTolerant(springBootVersion) if err != nil { return "", fmt.Errorf("cannot parse Spring Boot version %q: %w", springBootVersion, err) } - for _, m := range sc.Mappings { - var begin, end semver.Version + for _, m := range mappings { r := m.CompatibilityRange if strings.HasPrefix(r, "[") { - // Format: [begin,end) - inner := strings.Trim(r, "[]") + // "[begin,end)" — strip the surrounding brackets by index, matching + // the JS original's slice(1,-1), so that the closing ")" is removed + // correctly (strings.Trim would only remove "[" and "]", leaving ")"). + if len(r) < 2 { + continue + } + inner := r[1 : len(r)-1] parts := strings.SplitN(inner, ",", 2) if len(parts) != 2 { continue } - begin, err = semver.ParseTolerant(strings.TrimSpace(parts[0])) + begin, err := semver.ParseTolerant(strings.TrimSpace(parts[0])) if err != nil { continue } - end, err = semver.ParseTolerant(strings.TrimSpace(parts[1])) + end, err := semver.ParseTolerant(strings.TrimSpace(parts[1])) if err != nil { continue } + if target.GTE(begin) && target.LT(end) { + return m.Version, nil + } } else { - // Format: begin (open-ended) - begin, err = semver.ParseTolerant(r) + // open-ended lower bound + begin, err := semver.ParseTolerant(r) if err != nil { continue } - end, err = semver.ParseTolerant("999.999.999") - if err != nil { - continue + if target.GTE(begin) { + return m.Version, nil } } - - if target.GTE(begin) && target.LT(end) { - return m.Version, nil - } } return "", fmt.Errorf("no compatible spring-cloud version found for Spring Boot %s", springBootVersion) diff --git a/hack/update-ca-bundle.js b/hack/update-ca-bundle.js deleted file mode 100644 index dd7b5eee57..0000000000 --- a/hack/update-ca-bundle.js +++ /dev/null @@ -1,110 +0,0 @@ -// const xml2js = require('xml2js'); -const {Octokit} = require("octokit"); -// const {readFile,writeFile} = require('fs/promises'); -const {spawn} = require('node:child_process'); - -const octokit = new Octokit({auth: process.env.GITHUB_TOKEN}); -const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/') - -const prExists = async (pred) => { - let page = 1 - const perPage = 10; - - while (true) { - const resp = await octokit.rest.pulls.list({ - owner: owner, - repo: repo, - state: 'open', - per_page: perPage, - page: page - }) - - for (const e of resp.data) { - if (pred(e)) { - return true - } - } - if (resp.data.length < perPage) { - return false - } - page++ - } -} - -/** - * @param script - * @return {Promise} - */ -const runScript = async (script) => { - const subproc = spawn("sh", ["-c", script], {stdio: ['inherit', 'inherit', 'inherit']}) - return new Promise((resolve, reject) => { - subproc.on('exit', code => { - resolve(code) - }) - if (typeof subproc.exitCode === 'number') { - resolve(subproc.exitCode) - } - }) -} - -/** - * @return {Promise} - */ -const updateCA = async () => { - let ec = await runScript('make templates/certs/ca-certificates.crt') - if (ec !== 0) { - throw new Error('cannot update CA bundle') - } - return (await runScript('git diff --exit-code -- templates/certs/ca-certificates.crt')) !== 0 -} - -const prepareBranch = async (branchName, prTitle) => { - const script = `git config user.email "automation@knative.team" && \\ - git config user.name "Knative Automation" && \\ - git checkout -b "${branchName}" && \\ - make generate/zz_filesystem_generated.go && \\ - git add generate/zz_filesystem_generated.go templates/certs/ca-certificates.crt && \\ - git commit -m "${prTitle}" && \\ - git push --set-upstream origin "${branchName}" -` - const ec = await runScript(script) - if (ec !== 0) { - throw new Error("cannot prepare branch: non-zero exit code") - } -} - -const main = async () => { - const prTitle = `chore: update CA bundle` - if (await prExists(({title}) => title === prTitle)) { - console.log("The PR already exists!") - return - } - - const hasUpdated = await updateCA() - if (!hasUpdated) { - console.log('The CA bundle is up to date. Nothing to be done.') - return - } - - const branchName = `update-ca-bundle-${(new Date()).toISOString().split('T')[0]}` - - await prepareBranch(branchName, prTitle) - - await octokit.rest.pulls.create({ - owner: owner, - repo: repo, - title: prTitle, - body: prTitle, - base: 'main', - head: `${owner}:${branchName}`, - }) - console.log("The PR has been created!") - -} - -main().then(value => { - console.log("OK!") -}).catch(reason => { - console.log("ERROR: ", reason) - process.exit(1) -}) diff --git a/hack/update-quarkus-platform.js b/hack/update-quarkus-platform.js deleted file mode 100644 index 1763ed9f6a..0000000000 --- a/hack/update-quarkus-platform.js +++ /dev/null @@ -1,137 +0,0 @@ -const xml2js = require('xml2js'); -const {Octokit} = require("octokit"); -const {readFile,writeFile} = require('fs/promises'); -const {spawn} = require('node:child_process'); - -const cePomPath = "templates/quarkus/cloudevents/pom.xml" -const httpPomPath = "templates/quarkus/http/pom.xml" -const octokit = new Octokit({auth: process.env.GITHUB_TOKEN}); -const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/') - -const getLatestPlatform = async () => { - const data = await (await fetch("https://code.quarkus.io/api/platforms")).json() - return data.platforms[0].streams[0].releases[0].quarkusCoreVersion -} - -const prExists = async (pred) => { - - let page = 1 - const perPage = 10; - - while (true) { - const resp = await octokit.rest.pulls.list({ - owner: owner, - repo: repo, - state: 'open', - per_page: perPage, - page: page - }) - - for (const e of resp.data) { - if (pred(e)) { - return true - } - } - if (resp.data.length < perPage) { - return false - } - page++ - } -} - -const parseXML = (text) => new Promise((resolve, reject) => { - xml2js.parseString(text, {}, (err, res) => { - if (err) { - reject(err) - } - resolve(res) - }) -}) - -const platformFromPom = async (pomPath) => { - const pomData = await readFile(pomPath, {encoding: 'utf8'}); - const pom = await parseXML(pomData) - return pom.project.properties[0]['quarkus.platform.version'][0] -} - -const prepareBranch = async (branchName, prTitle) => { - const script = `git config user.email "automation@knative.team" && \\ - git config user.name "Knative Automation" && \\ - git checkout -b "${branchName}" && \\ - make generate/zz_filesystem_generated.go && \\ - git add "${cePomPath}" "${httpPomPath}" generate/zz_filesystem_generated.go && \\ - git commit -m "${prTitle}" && \\ - git push --set-upstream origin "${branchName}" -` - const subproc = spawn("sh", ["-c", script], {stdio: ['inherit', 'inherit', 'inherit']}) - - return new Promise((resolve, reject) => { - subproc.on('exit', code => { - if (code === 0) { - resolve() - return - } - reject(new Error("cannot prepare branch: non-zero exit code")) - }) - }) -} - -const updatePlatformInPom = async (pomPath, newPlatform) => { - const pomData = await readFile(pomPath, {encoding: 'utf8'}); - const newPomData = pomData.replace(new RegExp('[\\w.]+', 'i'), - `${newPlatform}`) - await writeFile(pomPath, newPomData) -} - -const smokeTest = () => { - const subproc = spawn("make", ["test-quarkus"], {stdio: ['inherit', 'inherit', 'inherit']}) - return new Promise((resolve, reject) => { - subproc.on('exit', code => { - if (code === 0) { - resolve() - return - } - reject(new Error("smoke test failed: non-zero exit code")) - }) - }) -} - -const main = async () => { - const latestPlatform = await getLatestPlatform() - const prTitle = `chore: update Quarkus platform version to ${latestPlatform}` - const branchName = `update-quarkus-platform-${latestPlatform}` - const cePlatform = await platformFromPom(cePomPath) - const httpPlatform = await platformFromPom(httpPomPath) - - if (cePlatform === latestPlatform && httpPlatform === latestPlatform) { - console.log("Quarkus platform is up-to-date!") - return - } - - if (await prExists(({title}) => title === prTitle)) { - console.log("The PR already exists!") - return - } - - await updatePlatformInPom(cePomPath, latestPlatform) - await updatePlatformInPom(httpPomPath, latestPlatform) - await smokeTest() - await prepareBranch(branchName, prTitle) - await octokit.rest.pulls.create({ - owner: owner, - repo: repo, - title: prTitle, - body: prTitle, - base: 'main', - head: `${owner}:${branchName}`, - }) - console.log("The PR has been created!") - -} - -main().then(value => { - console.log("OK!") -}).catch(reason => { - console.log("ERROR: ", reason) - process.exit(1) -}) diff --git a/hack/update-springboot-platform.js b/hack/update-springboot-platform.js deleted file mode 100644 index a612341aec..0000000000 --- a/hack/update-springboot-platform.js +++ /dev/null @@ -1,179 +0,0 @@ -const xml2js = require('xml2js'); -const yaml = require('yaml') -const semver = require('semver') -const {Octokit} = require("octokit"); -const {readFile,writeFile} = require('fs/promises'); -const {spawn} = require('node:child_process'); - -const cePomPath = "templates/springboot/cloudevents/pom.xml" -const httpPomPath = "templates/springboot/http/pom.xml" -const octokit = new Octokit({auth: process.env.GITHUB_TOKEN}); -const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/') - -const getLatestPlatform = async () => { - const data = await (await fetch("https://api.github.com/repos/spring-projects/spring-boot/releases/latest")).json() - return (data.draft === false) ? data.tag_name.replace(/[A-Za-z]/g, "") : null; -} - -const prExists = async (pred) => { - - let page = 1 - const perPage = 10; - - while (true) { - const resp = await octokit.rest.pulls.list({ - owner: owner, - repo: repo, - state: 'open', - per_page: perPage, - page: page - }) - - for (const e of resp.data) { - if (pred(e)) { - return true - } - } - if (resp.data.length < perPage) { - return false - } - page++ - } -} - -const parseXML = (text) => new Promise((resolve, reject) => { - xml2js.parseString(text, {}, (err, res) => { - if (err) { - reject(err) - } - resolve(res) - }) -}) - -const platformFromPom = async (pomPath) => { - const pomData = await readFile(pomPath, {encoding: 'utf8'}); - const pom = await parseXML(pomData) - return pom.project.parent[0].version[0] -} - -const prepareBranch = async (branchName, prTitle) => { - const script = `git config user.email "automation@knative.team" && \\ - git config user.name "Knative Automation" && \\ - git checkout -b "${branchName}" && \\ - make generate/zz_filesystem_generated.go && \\ - git add "${cePomPath}" "${httpPomPath}" generate/zz_filesystem_generated.go && \\ - git commit -m "${prTitle}" && \\ - git push --set-upstream origin "${branchName}" -` - const subproc = spawn("sh", ["-c", script], {stdio: ['inherit', 'inherit', 'inherit']}) - - return new Promise((resolve, reject) => { - subproc.on('exit', code => { - if (code === 0) { - resolve() - return - } - reject(new Error("cannot prepare branch: non-zero exit code")) - }) - }) -} - -const updatePlatformInPom = async (pomPath, newPlatform) => { - const pomData = await readFile(pomPath, {encoding: 'utf8'}); - const pom = await parseXML(pomData) - pom.project.parent[0].version[0] = newPlatform - - const compatibleSpringCloudVersion = await getCompatibleSpringCloudVersion(newPlatform) - pom.project.properties[0]['spring-cloud.version'] = [compatibleSpringCloudVersion] - - const builder = new xml2js.Builder( { headless: false, renderOpts: { pretty: true } }) - const newPomData = builder.buildObject(pom) + "\n" - await writeFile(pomPath, newPomData) -} - -const getCompatibleSpringCloudVersion = async (newPlatform) => { - const bomUrl = "https://raw.githubusercontent.com/spring-io/start.spring.io/main/start-site/src/main/resources/application.yml" - const data = await (await fetch(bomUrl)).text() - const mappings = yaml.parseAllDocuments(data)[0].toJS() - .initializr - .env - .boms['spring-cloud'] - .mappings - - const newPlatformVersion = semver.parse(newPlatform, {}, true) - for (const {compatibilityRange, version} of mappings) { - let begin, end - if (compatibilityRange.startsWith('[')) { - let [b, e] = compatibilityRange.slice(1, -1).split(',') - begin = semver.parse(b, {}, true) - end = semver.parse(e, {}, true) - } else { - begin = semver.parse(compatibilityRange, {}, true) - end = semver.parse("999.999.999", {}, true) - } - - if (newPlatformVersion.compare(begin) >= 0 && newPlatformVersion.compare(end) < 0) { - return version - } - } - throw new Error("cannot get latest compatible spring-cloud version") -} - -const smokeTest = () => { - const subproc = spawn("make", ["test-springboot"], {stdio: ['inherit', 'inherit', 'inherit']}) - return new Promise((resolve, reject) => { - subproc.on('exit', code => { - if (code === 0) { - resolve() - return - } - reject(new Error("smoke test failed: non-zero exit code")) - }) - }) -} - -const main = async () => { - const latestPlatform = await getLatestPlatform() - - if(latestPlatform === null) { - console.log("Spring Boot platform latest version is not ready to use!") - return - } - - const prTitle = `chore: update Springboot platform version to ${latestPlatform}` - const branchName = `update-springboot-platform-${latestPlatform}` - const cePlatform = await platformFromPom(cePomPath) - const httpPlatform = await platformFromPom(httpPomPath) - - if (cePlatform === latestPlatform && httpPlatform === latestPlatform) { - console.log("Spring Boot platform is up-to-date!") - return - } - - if (await prExists(({title}) => title === prTitle)) { - console.log("The PR already exists!") - return - } - - await updatePlatformInPom(cePomPath, latestPlatform) - await updatePlatformInPom(httpPomPath, latestPlatform) - await smokeTest() - await prepareBranch(branchName, prTitle) - await octokit.rest.pulls.create({ - owner: owner, - repo: repo, - title: prTitle, - body: prTitle, - base: 'main', - head: `${owner}:${branchName}`, - }) - console.log("The PR has been created!") - -} - -main().then(value => { - console.log("OK!") -}).catch(reason => { - console.log("ERROR: ", reason) - process.exit(1) -}) From 8ed0f93687f3b9e9965b0934aaad80041b3b6c68 Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Sun, 10 May 2026 18:27:21 +0530 Subject: [PATCH 3/4] fix(workflows): gate smoke tests and PR creation on actual file changes - Pass GITHUB_TOKEN to the Spring Boot update step so GitHub API calls are authenticated (unauthenticated rate limit is 60 req/hr, shared across all Actions runners, causing intermittent 403s on the 4-hour cron) - Add a git-diff check after each Go tool + make generate step; skip smoke tests and the create-pull-request action when no files changed, restoring the early-exit behaviour the original JS scripts had --- .github/workflows/update-ca-bundle.yaml | 9 +++++++++ .github/workflows/update-quarkus-platform.yaml | 10 ++++++++++ .github/workflows/update-springboot-platform.yaml | 12 ++++++++++++ 3 files changed, 31 insertions(+) diff --git a/.github/workflows/update-ca-bundle.yaml b/.github/workflows/update-ca-bundle.yaml index 5135368931..9478c7ccfb 100644 --- a/.github/workflows/update-ca-bundle.yaml +++ b/.github/workflows/update-ca-bundle.yaml @@ -25,7 +25,16 @@ jobs: run: go run ./hack/cmd/update-ca-bundle/main.go - name: Run make generate run: make generate/zz_filesystem_generated.go + - name: Check for changes + id: changes + run: | + if git diff --quiet; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' uses: peter-evans/create-pull-request@v7 with: token: ${{ github.token }} diff --git a/.github/workflows/update-quarkus-platform.yaml b/.github/workflows/update-quarkus-platform.yaml index 5e860e919f..08a7334b9c 100644 --- a/.github/workflows/update-quarkus-platform.yaml +++ b/.github/workflows/update-quarkus-platform.yaml @@ -29,9 +29,19 @@ jobs: run: go run ./hack/cmd/update-quarkus-platform/main.go - name: Run make generate run: make generate/zz_filesystem_generated.go + - name: Check for changes + id: changes + run: | + if git diff --quiet; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi - name: Run smoke tests + if: steps.changes.outputs.changed == 'true' run: make test-quarkus - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' uses: peter-evans/create-pull-request@v7 with: token: ${{ github.token }} diff --git a/.github/workflows/update-springboot-platform.yaml b/.github/workflows/update-springboot-platform.yaml index 6329c8bfc8..3f9301a0c4 100644 --- a/.github/workflows/update-springboot-platform.yaml +++ b/.github/workflows/update-springboot-platform.yaml @@ -26,12 +26,24 @@ jobs: java-version: 21 distribution: 'temurin' - name: Update Spring Boot platform version + env: + GITHUB_TOKEN: ${{ github.token }} run: go run ./hack/cmd/update-springboot-platform/main.go - name: Run make generate run: make generate/zz_filesystem_generated.go + - name: Check for changes + id: changes + run: | + if git diff --quiet; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi - name: Run smoke tests + if: steps.changes.outputs.changed == 'true' run: make test-springboot - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' uses: peter-evans/create-pull-request@v7 with: token: ${{ github.token }} From 4f9a8390af6761241e7e0e31ce483977d0ff8f42 Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Sun, 10 May 2026 22:42:58 +0530 Subject: [PATCH 4/4] fix: use package dir in go run and rename shared/github.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch all `go run .../main.go` invocations to `go run ./pkg` in Makefile and the three CI workflows so additional files added to a command package are automatically included - Rename hack/cmd/shared/github.go → shared.go; the file contains a generic HTTP client and command runner, not GitHub-specific helpers --- .github/workflows/update-ca-bundle.yaml | 2 +- .github/workflows/update-quarkus-platform.yaml | 2 +- .github/workflows/update-springboot-platform.yaml | 2 +- Makefile | 6 +++--- hack/cmd/shared/{github.go => shared.go} | 0 5 files changed, 6 insertions(+), 6 deletions(-) rename hack/cmd/shared/{github.go => shared.go} (100%) diff --git a/.github/workflows/update-ca-bundle.yaml b/.github/workflows/update-ca-bundle.yaml index 9478c7ccfb..3f62a986db 100644 --- a/.github/workflows/update-ca-bundle.yaml +++ b/.github/workflows/update-ca-bundle.yaml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v4 - uses: knative/actions/setup-go@main - name: Update CA bundle - run: go run ./hack/cmd/update-ca-bundle/main.go + run: go run ./hack/cmd/update-ca-bundle - name: Run make generate run: make generate/zz_filesystem_generated.go - name: Check for changes diff --git a/.github/workflows/update-quarkus-platform.yaml b/.github/workflows/update-quarkus-platform.yaml index 08a7334b9c..61f60899ea 100644 --- a/.github/workflows/update-quarkus-platform.yaml +++ b/.github/workflows/update-quarkus-platform.yaml @@ -26,7 +26,7 @@ jobs: java-version: 21 distribution: 'temurin' - name: Update Quarkus platform version - run: go run ./hack/cmd/update-quarkus-platform/main.go + run: go run ./hack/cmd/update-quarkus-platform - name: Run make generate run: make generate/zz_filesystem_generated.go - name: Check for changes diff --git a/.github/workflows/update-springboot-platform.yaml b/.github/workflows/update-springboot-platform.yaml index 3f9301a0c4..132cbff167 100644 --- a/.github/workflows/update-springboot-platform.yaml +++ b/.github/workflows/update-springboot-platform.yaml @@ -28,7 +28,7 @@ jobs: - name: Update Spring Boot platform version env: GITHUB_TOKEN: ${{ github.token }} - run: go run ./hack/cmd/update-springboot-platform/main.go + run: go run ./hack/cmd/update-springboot-platform - name: Run make generate run: make generate/zz_filesystem_generated.go - name: Check for changes diff --git a/Makefile b/Makefile index a3a7467d89..7149fbb0a8 100644 --- a/Makefile +++ b/Makefile @@ -424,15 +424,15 @@ __update-builder: # Used in automation .PHONY: update-quarkus-platform update-quarkus-platform: ## Update Quarkus platform version in templates - go run ./hack/cmd/update-quarkus-platform/main.go + go run ./hack/cmd/update-quarkus-platform .PHONY: update-springboot-platform update-springboot-platform: ## Update Spring Boot platform version in templates - go run ./hack/cmd/update-springboot-platform/main.go + go run ./hack/cmd/update-springboot-platform .PHONY: update-ca-bundle update-ca-bundle: ## Update CA bundle in templates - go run ./hack/cmd/update-ca-bundle/main.go + go run ./hack/cmd/update-ca-bundle .PHONY: setup-githooks setup-githooks: diff --git a/hack/cmd/shared/github.go b/hack/cmd/shared/shared.go similarity index 100% rename from hack/cmd/shared/github.go rename to hack/cmd/shared/shared.go