diff --git a/.github/workflows/rc-breaking-changes.yaml b/.github/workflows/rc-breaking-changes.yaml new file mode 100644 index 000000000..a464321f2 --- /dev/null +++ b/.github/workflows/rc-breaking-changes.yaml @@ -0,0 +1,28 @@ +name: Main branch breaking changes check + +on: + push: + branches: + - main + +jobs: + breaking-changes: + name: Check "main" for breaking changes before release + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + fetch-tags: true + - name: Set up Go 1.22.6 + uses: actions/setup-go@v4 + with: + go-version: '1.22.6' + - name: Install gorelease tool + run: | + go install golang.org/x/exp/cmd/gorelease@latest + - name: Run Breaking Changes Script + run: | + go run ./tools/breakingchanges/cmd/main.go --ignore tools \ No newline at end of file diff --git a/.github/workflows/release-go-module.yml b/.github/workflows/release-go-module.yml index 9b393cf77..fc89a1620 100644 --- a/.github/workflows/release-go-module.yml +++ b/.github/workflows/release-go-module.yml @@ -119,7 +119,6 @@ jobs: fi - name: Create GitHub Release - if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/README.md b/README.md index 554bf2dfa..4434b14e6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ # Chainlink Testing Framework +[![Main branch breaking changes check](https://github.com/smartcontractkit/chainlink-testing-framework/actions/workflows/rc-breaking-changes.yaml/badge.svg)](https://github.com/smartcontractkit/chainlink-testing-framework/actions/workflows/rc-breaking-changes.yaml) [![Lib tag](https://img.shields.io/github/v/tag/smartcontractkit/chainlink-testing-framework?filter=%2Alib%2A)](https://github.com/smartcontractkit/chainlink-testing-framework/tags) [![WASP tag](https://img.shields.io/github/v/tag/smartcontractkit/chainlink-testing-framework?filter=%2Awasp%2A)](https://github.com/smartcontractkit/chainlink-testing-framework/tags) [![Seth tag](https://img.shields.io/github/v/tag/smartcontractkit/chainlink-testing-framework?filter=%2Aseth%2A)](https://github.com/smartcontractkit/chainlink-testing-framework/tags) diff --git a/RELEASE.md b/RELEASE.md index 52e091e1c..f2f76aee4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -41,19 +41,13 @@ module github.com/smartcontractkit/chainlink-testing-framework/wasp/v2 If your module have `cmd/main.go` we build binary automatically for various platforms and attach it to the release page. ## Debug Release Pipeline -Since some components of pipeline are relying on published Go modules index and Dependabot we have a test script to verify the release pipeline: +To test the release pipeline use `$pkg/$subpkg/v1.999.X-test-release` tags, they are retracted so consumers can't accidentally install them -To test release for any module use `$pkg/$subpkg/v1.999.X-test-release` tags, they are retracted so consumers can't accidentally install them +Create a test file inside `.changeset` with format `v1.999.X-test-release.md`, tag and push: ``` -nix develop -python ./scripts/test-package-release.py -tag k8s-test-runner/v1.999.0-test-release -package ./k8s-test-runner +git tag k8s-test-runner/v1.999.X-test-release && git push --tags ``` -To remove all the test tags use -``` -nix develop -python3 ./scripts/test-package-release.py -remove-test-tags -``` [Pipeline for releasing Go modules](.github/workflows/release-go-module.yml) [Dependabot summary pipeline](.github/workflows/dependabot-consumers-summary.yaml) @@ -61,5 +55,7 @@ python3 ./scripts/test-package-release.py -remove-test-tags ## Check breaking changes locally We have a simple wrapper to check breaking changes for all the packages. Commit all your changes and run: ``` -./scripts/breaking-changes.sh +go run ./tools/breakingchanges/cmd/main.go +go run ./tools/breakingchanges/cmd/main.go --subdir wasp # check recursively starting with subdir +go run ./tools/breakingchanges/cmd/main.go --ignore tools,wasp,havoc,seth # to ignore some packages ``` \ No newline at end of file diff --git a/scripts/breaking-changes.sh b/scripts/breaking-changes.sh deleted file mode 100755 index 1f63eb311..000000000 --- a/scripts/breaking-changes.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -cd tools/breakingchanges/ && go run cmd/main.go -path ../.. || cd - \ No newline at end of file diff --git a/scripts/test-package-release.py b/scripts/test-package-release.py deleted file mode 100644 index 2a57148e6..000000000 --- a/scripts/test-package-release.py +++ /dev/null @@ -1,102 +0,0 @@ -import subprocess -import argparse -import os - -def run_command(command): - """Run a shell command and capture its output.""" - try: - result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - return result.stdout.decode('utf-8').strip() - except subprocess.CalledProcessError as e: - print(f"Command '{command}' failed with error: {e.stderr.decode('utf-8').strip()}") - return None - -def remove_tag(tag): - """Remove the specified Git tag locally and remotely, if it exists.""" - # Check if the tag exists locally - print(f"Checking if tag '{tag}' exists locally...") - tag_exists = run_command(f"git tag -l {tag}") - - if tag_exists: - print(f"Tag '{tag}' found. Removing locally...") - run_command(f"git tag -d {tag}") - else: - print(f"Tag '{tag}' does not exist locally. Continuing...") - - # Attempt to remove the tag remotely - print(f"Removing tag '{tag}' from remote (if exists)...") - run_command(f"git push origin :refs/tags/{tag}") - -def remove_test_release_tags(): - """Remove all tags with '-test-release' suffix locally and remotely.""" - print("Finding all tags with '-test-release' suffix...") - tags_to_remove = run_command("git tag --list '*-test-release'").splitlines() - - if not tags_to_remove: - print("No tags found with '-test-release' suffix.") - return - - for tag in tags_to_remove: - print(f"Removing tag: {tag}") - remove_tag(tag) - -def add_release_file(package_dir, tag): - """Change directory to the package and create a release file.""" - # Extract the version part of the tag - version_part = tag.split('/')[-1] - filename = f".changeset/{version_part}.md" - - # Change directory to the package - os.chdir(package_dir) - print(f"Changed directory to {package_dir}. Creating file {filename}...") - - # Write example data to the release file - with open(filename, 'w') as f: - f.write(f"Test release of {package_dir} module\n\n") - f.write("Features added:\n") - f.write("- Test feature #1\n") - f.write("- Test feature #2\n") - - # Add and commit the new file - run_command(f"git add {filename}") - print("Yubikey signature might be required if the script is hanging for too long check your Yubikey...") - commit_message = f"Test release commit {version_part}" - run_command(f"git commit -m '{commit_message}' --no-verify") - print(f"Committed the release file: {filename}") - -def add_tag(tag): - """Create a new tag and push it.""" - print(f"Creating and adding new tag '{tag}' locally...") - run_command(f"git tag {tag}") - - print(f"Pushing new tag '{tag}' to remote...") - run_command(f"git push origin :refs/tags/{tag}") - -def push_changes(): - """Push changes to remote and push all tags.""" - print("Pushing changes to remote with --no-verify and --force...") - run_command("git push --no-verify --force") - - print("Pushing all tags to remote...") - run_command("git push --tags") - -def main(): - parser = argparse.ArgumentParser(description="Remove Git tags, add a release file, and push changes.") - parser.add_argument("-tag", required=False, help="The Git tag to remove and re-add.") - parser.add_argument("-package", required=False, help="The package directory to create the release file in.") - parser.add_argument("-remove-test-tags", required=False, action="store_true", help="Remove all Git tags with '-test-release' suffix.") - - args = parser.parse_args() - - if args.remove_test_tags: - remove_test_release_tags() - elif args.tag and args.package: - remove_tag(args.tag) - add_release_file(args.package, args.tag) - add_tag(args.tag) - push_changes() - else: - print("You must provide either '-remove-test-tags' or '-tag' and '-package' options.") - -if __name__ == "__main__": - main() diff --git a/tools/breakingchanges/breaking_changes.go b/tools/breakingchanges/breaking_changes.go deleted file mode 100644 index 89fe5020b..000000000 --- a/tools/breakingchanges/breaking_changes.go +++ /dev/null @@ -1,127 +0,0 @@ -package breakingchanges - -import ( - "bytes" - "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - - "golang.org/x/sync/errgroup" -) - -const ( - green = "\033[0;32m" - yellow = "\033[0;33m" - noColor = "\033[0m" -) - -func DetectBreakingChanges(rootPath string) { - // Check if gorelease is installed - if _, err := exec.LookPath("gorelease"); err != nil { - log.Fatalf("%sgorelease could not be found. Please install it with 'go install golang.org/x/exp/cmd/gorelease@latest'.%s\n", green, noColor) - } - - eg := &errgroup.Group{} - - // Function to process each directory - processDirectory := func(path string) error { - return runGorelease(path) - } - - // Check root directory for go.mod - if _, err := os.Stat(filepath.Join(rootPath, "go.mod")); err == nil { - eg.Go(func() error { - return processDirectory(rootPath) - }) - } - - // Walk through directories starting from rootPath - err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // Skip if not a directory or if it's the root directory (already processed) - if !info.IsDir() || path == rootPath { - return nil - } - - // Check if go.mod exists in the directory - goModPath := filepath.Join(path, "go.mod") - if _, err := os.Stat(goModPath); err == nil { - eg.Go(func() error { - return processDirectory(path) - }) - } - - return nil - }) - - if err != nil { - log.Fatalf("%sError walking through directories: %v%s\n", green, err, noColor) - } - - // Wait for all goroutines to finish and collect errors - if err := eg.Wait(); err != nil { - log.Fatalf("%sErrors occurred while running gorelease: %v%s\n", green, err, noColor) - } - - fmt.Printf("%sAll checks completed successfully.%s\n", green, noColor) -} - -func runGorelease(path string) error { - packageFolder := filepath.Base(path) - var output bytes.Buffer - - // Find the second-latest tag for the package - cmd := exec.Command("git", "tag", "--sort=-creatordate") - cmd.Dir = path - cmd.Stdout = &output - err := cmd.Run() - if err != nil { - fmt.Printf("%sFailed to retrieve git tags for package %s: %v%s\n", green, packageFolder, err, noColor) - return err - } - - tags := strings.Split(output.String(), "\n") - var previousTag string - for _, tag := range tags { - if strings.HasPrefix(tag, fmt.Sprintf("%s/v", packageFolder)) { - if previousTag != "" { - break - } - previousTag = tag - } - } - - if previousTag == "" { - fmt.Printf("%sNo previous tag found for package %s. Skipping.%s\n", green, packageFolder, noColor) - return nil - } - - versionTag := strings.Split(previousTag, "/")[1] - fmt.Printf("%sRunning gorelease for package %s with base tag %s%s\n", green, packageFolder, versionTag, noColor) - - cmd = exec.Command("gorelease", "-base", versionTag) - cmd.Dir = path - output.Reset() - cmd.Stdout = &output - cmd.Stderr = &output - err = cmd.Run() - - if err != nil { - fmt.Printf("%sgorelease command failed for package %s with error: %v%s\n%sOutput:%s\n%s%s%s", green, packageFolder, err, noColor, yellow, noColor, yellow, output.String(), noColor) - return err - } - - if strings.Contains(output.String(), "Breaking changes") { - fmt.Printf("%sBreaking changes found for package %s:%s\n%s%s%s", green, packageFolder, noColor, yellow, output.String(), noColor) - } else { - fmt.Printf("%sNo breaking changes found for package %s.%s\n", green, packageFolder, noColor) - } - - return nil -} diff --git a/tools/breakingchanges/cmd/main.go b/tools/breakingchanges/cmd/main.go index 7a06dbdcc..3c336ad4e 100644 --- a/tools/breakingchanges/cmd/main.go +++ b/tools/breakingchanges/cmd/main.go @@ -1,13 +1,174 @@ package main import ( + "bytes" "flag" - "github.com/smartcontractkit/chainlink-testing-framework/tools/breakingchanges" + "fmt" + "io/fs" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" ) +const ( + Yellow = "\033[33m" + Green = "\033[32m" + Red = "\033[31m" + Reset = "\033[0m" +) + +func findGoModDirs(rootFolder, subDir string) ([]string, error) { + var goModDirs []string + + err := filepath.WalkDir(filepath.Join(rootFolder, subDir), func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + if info.IsDir() { + goModPath := filepath.Join(path, "go.mod") + if _, err := os.Stat(goModPath); !os.IsNotExist(err) { + // Ensure we store absolute paths + absPath, err := filepath.Abs(path) + if err != nil { + return err + } + goModDirs = append(goModDirs, absPath) + } + } + return nil + }) + + if err != nil { + return nil, err + } + return goModDirs, nil +} + +func getLastTag(pathPrefix string) (string, error) { + cmd := exec.Command("sh", "-c", fmt.Sprintf("git tag | grep '%s' | tail -1", pathPrefix)) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("error fetching tags: %w", err) + } + + tag := strings.TrimSpace(out.String()) + if tag == "" { + return "", nil + } + + // Use regex to find the version tag starting with 'v' + re := regexp.MustCompile(`v\d+\.\d+\.\d+`) + matches := re.FindStringSubmatch(tag) + if len(matches) > 0 { + tag = matches[0] + } else { + return "", fmt.Errorf("no valid version tag found in '%s'", tag) + } + + return tag, nil +} + +func checkBreakingChanges(tag string) (string, string, error) { + var stdout, stderr bytes.Buffer + cmd := exec.Command("gorelease", "-base", tag) + fmt.Printf("%sExecuting command: %s %s\n", Yellow, cmd, Reset) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + return stdout.String(), stderr.String(), err +} + +func getIgnoredDirs(flag *string) []string { + ignoredDirs := make([]string, 0) + if flag != nil { + allDirs := strings.Split(*flag, ",") + for _, d := range allDirs { + ignoredDirs = append(ignoredDirs, d) + } + } + return ignoredDirs +} + +func isIgnoredDirPrefix(pathPrefix string, ignoredDirs []string) bool { + for _, d := range ignoredDirs { + if d == "" { + continue + } + fmt.Printf("Checking prefix: %s, path: %s\n", d, pathPrefix) + if strings.HasPrefix(pathPrefix, d) { + fmt.Printf("Path is ignored, skipping: %s\n", pathPrefix) + return true + } + } + return false +} + func main() { - pathFlag := flag.String("path", ".", "Path to start searching for go.mod files") + rootFolder := flag.String("root", ".", "The root folder to start scanning from") + subDir := flag.String("subdir", "", "The subdirectory inside the root folder to scan for modules") + ignoreDirs := flag.String("ignore", "", "Ignore directory paths starting with prefix") flag.Parse() - breakingchanges.DetectBreakingChanges(*pathFlag) + absRootFolder, err := filepath.Abs(*rootFolder) + if err != nil { + fmt.Printf("Error getting absolute path of root folder: %v\n", err) + return + } + + goModDirs, err := findGoModDirs(absRootFolder, *subDir) + if err != nil { + fmt.Printf("Error finding directories: %v\n", err) + return + } + + ignoredDirs := getIgnoredDirs(ignoreDirs) + + breakingChanges := false + for _, dirPath := range goModDirs { + // Convert the stripped path back to absolute + pathPrefix := strings.TrimPrefix(dirPath, absRootFolder+string(os.PathSeparator)) + + if isIgnoredDirPrefix(pathPrefix, ignoredDirs) { + continue + } + + lastTag, err := getLastTag(pathPrefix) + if err != nil { + fmt.Printf("Error finding last tag: %v\n", err) + continue + } + + if lastTag != "" { + fmt.Printf("%sProcessing directory: %s%s\n", Yellow, dirPath, Reset) + if err := os.Chdir(dirPath); err != nil { + fmt.Printf("Error changing directory: %v\n", err) + continue + } + + stdout, stderr, err := checkBreakingChanges(lastTag) + if err != nil { + fmt.Printf("Error running gorelease: %v\n", err) + breakingChanges = true + } + fmt.Printf("%sgorelease output:\n%s%s\n", Green, stdout, Reset) + if stderr != "" { + fmt.Printf("%sgorelease errors:\n%s%s\n", Red, stderr, Reset) + } + + if err := os.Chdir(absRootFolder); err != nil { + fmt.Printf("Error changing back to root directory: %v\n", err) + } + } else { + fmt.Printf("No valid tags found for path prefix: %s\n", pathPrefix) + } + } + if breakingChanges { + log.Fatalf("breaking changes detected!") + } } diff --git a/tools/breakingchanges/go.mod b/tools/breakingchanges/go.mod index 7378ddcf3..f36ea8be1 100644 --- a/tools/breakingchanges/go.mod +++ b/tools/breakingchanges/go.mod @@ -2,6 +2,4 @@ module github.com/smartcontractkit/chainlink-testing-framework/tools/breakingcha go 1.22.6 -require golang.org/x/sync v0.8.0 - retract [v1.999.0-test-release, v1.999.999-test-release] diff --git a/wasp/cluster.go b/wasp/cluster.go index 29ac50d20..f14a263bb 100644 --- a/wasp/cluster.go +++ b/wasp/cluster.go @@ -68,7 +68,7 @@ type ClusterConfig struct { tmpHelmFilePath string } -func (m *ClusterConfig) Defaults() error { +func (m *ClusterConfig) Defaults(a int) error { // TODO: will it be more clear if we move Helm values to a struct // TODO: or should it be like that for extensibility of a chart without reflection? m.HelmValues["namespace"] = m.Namespace @@ -165,7 +165,7 @@ func NewClusterProfile(cfg *ClusterConfig) (*ClusterProfile, error) { if err := cfg.Validate(); err != nil { return nil, err } - if err := cfg.Defaults(); err != nil { + if err := cfg.Defaults(0); err != nil { return nil, err } log.Info().Interface("Config", cfg).Msg("Cluster configuration") diff --git a/wasp/go.mod b/wasp/go.mod index c38fd6390..221305bf9 100644 --- a/wasp/go.mod +++ b/wasp/go.mod @@ -76,7 +76,7 @@ require ( github.com/gosimple/unidecode v1.0.1 // indirect github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect - github.com/hashicorp/consul/api v1.25.1 // indirect + github.com/hashicorp/consul/api v1.28.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect diff --git a/wasp/go.sum b/wasp/go.sum index 3d8ed891f..7fea2edf0 100644 --- a/wasp/go.sum +++ b/wasp/go.sum @@ -421,10 +421,10 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZ github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= -github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE= -github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= -github.com/hashicorp/consul/sdk v0.14.1 h1:ZiwE2bKb+zro68sWzZ1SgHF3kRMBZ94TwOCFRF4ylPs= -github.com/hashicorp/consul/sdk v0.14.1/go.mod h1:vFt03juSzocLRFo59NkeQHHmQa6+g7oU0pfzdI1mUhg= +github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= +github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= +github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8= +github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=