diff --git a/.github/workflows/supported-versions.yml b/.github/workflows/supported-versions.yml new file mode 100644 index 0000000000..7cf89d175b --- /dev/null +++ b/.github/workflows/supported-versions.yml @@ -0,0 +1,44 @@ +name: Supported Versions +on: + workflow_run: + workflows: ["Smoke Tests"] + branches: [main] + types: + - completed + +concurrency: + # Automatically cancel previous runs if a new one is triggered to conserve resources. + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + on-smoke-tests-success: + runs-on: ubuntu-latest + permissions: + actions: read + contents: write + pull-requests: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - uses: actions/checkout@v4 + + - run: echo 'Smoke Tests workflow passed' + + - run: go run parse_go_mod.go + + - run: git diff + + # note: This will only run when there *are* changes to integration versions + - name: Create Pull Request + id: pr + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: "update-latest-supported-versions" + commit-message: "Update supported versions" + base: main + title: "chore: update latest supported" + labels: changelog/no-changelog + body: "Test" diff --git a/parse_go_mod.go b/parse_go_mod.go new file mode 100644 index 0000000000..d218ad8bc1 --- /dev/null +++ b/parse_go_mod.go @@ -0,0 +1,163 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "os" + "os/exec" + "regexp" + "strings" + "sync" + "time" +) + +type ModuleVersion struct { + Name string + MinVersion string + MaxVersion string +} + +func parseGoMod(filePath string) ([]ModuleVersion, error) { + // This parses the go.mod file and extracts modules with their minimum versions. + var modules []ModuleVersion + + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("error: %s not found", filePath) + } + defer file.Close() + + regex := regexp.MustCompile(`^\s*([^\s]+)\s+v([0-9]+\.[0-9]+\.[0-9]+.*)`) + + scanner := bufio.NewScanner(file) + inRequireBlock := false + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if strings.HasPrefix(line, "require (") { + inRequireBlock = true + continue + } + + if inRequireBlock && line == ")" { + inRequireBlock = false + continue + } + + if inRequireBlock || strings.HasPrefix(line, "require ") { + match := regex.FindStringSubmatch(line) + if match != nil { + modules = append(modules, ModuleVersion{ + Name: match[1], + MinVersion: match[2], + }) + } + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading file: %v", err) + } + + return modules, nil +} + +func fetchLatestVersion(module string) (string, error) { + // Fetches latest version with `go list` + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "go", "list", "-m", "-versions", module) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("command timed out") + } + if err != nil { + return "", fmt.Errorf("command error: %s", stderr.String()) + } + + versions := strings.Fields(stdout.String()) + if len(versions) < 1 { + return "", fmt.Errorf("no versions found for module: %s", module) + } + + return versions[len(versions)-1], nil +} + +func fetchAllLatestVersions(modules []ModuleVersion) []ModuleVersion { + // Concurrently fetches the latest version of each module. + + var wg sync.WaitGroup + var mu sync.Mutex + + updatedModules := make([]ModuleVersion, 0, len(modules)) + + wg.Add(len(modules)) + for _, mod := range modules { + go func(mod ModuleVersion) { + defer wg.Done() + latestVersion, err := fetchLatestVersion(mod.Name) + if err != nil { + fmt.Printf("Error fetching latest version for %s: %v\n", mod.Name, err) + mu.Lock() + updatedModules = append(updatedModules, ModuleVersion{mod.Name, mod.MinVersion, "Error"}) + mu.Unlock() + return + } + mu.Lock() + updatedModules = append(updatedModules, ModuleVersion{mod.Name, mod.MinVersion, latestVersion}) + mu.Unlock() + }(mod) + } + + wg.Wait() + return updatedModules +} + +func outputVersionsAsMarkdown(modules []ModuleVersion, filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("error creating file: %v", err) + } + defer file.Close() + + fmt.Fprintln(file, "| Dependency | Minimum Version | Maximum Version |") + fmt.Fprintln(file, "|------------|-----------------|-----------------|") + for _, mod := range modules { + fmt.Fprintf(file, "| %s | v%s | %s |\n", mod.Name, mod.MinVersion, mod.MaxVersion) + } + return nil +} + +func main() { + goModPath := "integration_go.mod" // Modify path as needed + outputPath := "minimum_versions.md" + + modules, err := parseGoMod(goModPath) + if err != nil { + fmt.Println(err) + return + } + + modulesWithLatest := fetchAllLatestVersions(modules) + + err = outputVersionsAsMarkdown(modulesWithLatest, outputPath) + if err != nil { + fmt.Printf("Error writing output file: %v\n", err) + return + } + + fmt.Println("Version information written to", outputPath) +}