|
| 1 | +// Package main provides a tool to detect and resolve JFrog module dependencies |
| 2 | +// by parsing go.mod using `go mod edit -json` and checking for branch availability |
| 3 | +// in forked repositories. |
| 4 | +package main |
| 5 | + |
| 6 | +import ( |
| 7 | + "encoding/json" |
| 8 | + "fmt" |
| 9 | + "os" |
| 10 | + "os/exec" |
| 11 | + "strings" |
| 12 | +) |
| 13 | + |
| 14 | +// GoMod represents the JSON structure from `go mod edit -json` |
| 15 | +type GoMod struct { |
| 16 | + Module Module `json:"Module"` |
| 17 | + Go string `json:"Go"` |
| 18 | + Require []Require `json:"Require"` |
| 19 | + Replace []Replace `json:"Replace"` |
| 20 | +} |
| 21 | + |
| 22 | +// Module represents the module path |
| 23 | +type Module struct { |
| 24 | + Path string `json:"Path"` |
| 25 | +} |
| 26 | + |
| 27 | +// Require represents a required module |
| 28 | +type Require struct { |
| 29 | + Path string `json:"Path"` |
| 30 | + Version string `json:"Version"` |
| 31 | + Indirect bool `json:"Indirect"` |
| 32 | +} |
| 33 | + |
| 34 | +// Replace represents a replace directive |
| 35 | +type Replace struct { |
| 36 | + Old ModuleVersion `json:"Old"` |
| 37 | + New ModuleVersion `json:"New"` |
| 38 | +} |
| 39 | + |
| 40 | +// ModuleVersion represents a module with optional version |
| 41 | +type ModuleVersion struct { |
| 42 | + Path string `json:"Path"` |
| 43 | + Version string `json:"Version,omitempty"` |
| 44 | +} |
| 45 | + |
| 46 | +// DependencyInfo holds information about a detected dependency |
| 47 | +type DependencyInfo struct { |
| 48 | + Name string // Short name like "build-info-go" |
| 49 | + ModulePath string // Full module path like "github.com/jfrog/build-info-go" |
| 50 | + Repo string // Repository like "jfrog/build-info-go" |
| 51 | + Ref string // Branch or tag reference |
| 52 | +} |
| 53 | + |
| 54 | +// jfrogDependencies maps short names to their module paths |
| 55 | +var jfrogDependencies = map[string]string{ |
| 56 | + "build-info-go": "github.com/jfrog/build-info-go", |
| 57 | + "jfrog-client-go": "github.com/jfrog/jfrog-client-go", |
| 58 | + "jfrog-cli-core": "github.com/jfrog/jfrog-cli-core/v2", |
| 59 | +} |
| 60 | + |
| 61 | +func main() { |
| 62 | + // Get current branch from environment |
| 63 | + currentBranch := os.Getenv("CURRENT_BRANCH") |
| 64 | + if currentBranch == "" { |
| 65 | + currentBranch = "main" |
| 66 | + } |
| 67 | + |
| 68 | + // Parse go.mod |
| 69 | + goMod, err := parseGoMod() |
| 70 | + if err != nil { |
| 71 | + fmt.Fprintf(os.Stderr, "Error parsing go.mod: %v\n", err) |
| 72 | + os.Exit(1) |
| 73 | + } |
| 74 | + |
| 75 | + // Build a map of replace directives |
| 76 | + replaces := make(map[string]Replace) |
| 77 | + for _, r := range goMod.Replace { |
| 78 | + replaces[r.Old.Path] = r |
| 79 | + } |
| 80 | + |
| 81 | + // Open GITHUB_OUTPUT file for writing outputs |
| 82 | + outputFile := os.Getenv("GITHUB_OUTPUT") |
| 83 | + var output *os.File |
| 84 | + if outputFile != "" { |
| 85 | + var err error |
| 86 | + output, err = os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) |
| 87 | + if err != nil { |
| 88 | + fmt.Fprintf(os.Stderr, "Error opening GITHUB_OUTPUT: %v\n", err) |
| 89 | + os.Exit(1) |
| 90 | + } |
| 91 | + defer output.Close() |
| 92 | + } |
| 93 | + |
| 94 | + fmt.Println("============================================") |
| 95 | + fmt.Println("Detecting JFrog dependencies...") |
| 96 | + fmt.Println("============================================") |
| 97 | + fmt.Printf("Current branch: %s\n\n", currentBranch) |
| 98 | + |
| 99 | + // Process each dependency |
| 100 | + for name, modulePath := range jfrogDependencies { |
| 101 | + info := detectDependency(name, modulePath, replaces, currentBranch) |
| 102 | + writeOutput(output, name, info) |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +// parseGoMod runs `go mod edit -json` and parses the output |
| 107 | +func parseGoMod() (*GoMod, error) { |
| 108 | + cmd := exec.Command("go", "mod", "edit", "-json") |
| 109 | + out, err := cmd.Output() |
| 110 | + if err != nil { |
| 111 | + return nil, fmt.Errorf("failed to run go mod edit -json: %w", err) |
| 112 | + } |
| 113 | + |
| 114 | + var goMod GoMod |
| 115 | + if err := json.Unmarshal(out, &goMod); err != nil { |
| 116 | + return nil, fmt.Errorf("failed to parse go.mod JSON: %w", err) |
| 117 | + } |
| 118 | + |
| 119 | + return &goMod, nil |
| 120 | +} |
| 121 | + |
| 122 | +// detectDependency determines the repository and ref for a dependency |
| 123 | +func detectDependency(name, modulePath string, replaces map[string]Replace, currentBranch string) *DependencyInfo { |
| 124 | + // Check if there's a replace directive for this module |
| 125 | + if replace, ok := replaces[modulePath]; ok { |
| 126 | + // Parse the replace target |
| 127 | + newPath := replace.New.Path |
| 128 | + if strings.HasPrefix(newPath, "github.com/") { |
| 129 | + // Extract repo and version/ref |
| 130 | + parts := strings.TrimPrefix(newPath, "github.com/") |
| 131 | + repo := parts |
| 132 | + ref := replace.New.Version |
| 133 | + |
| 134 | + // Handle version format like "v1.2.3-0.20240101-abc123" |
| 135 | + // The ref might be a pseudo-version containing a commit hash |
| 136 | + if ref != "" { |
| 137 | + fmt.Printf("Found replace directive: %s => %s @ %s\n", name, repo, ref) |
| 138 | + return &DependencyInfo{ |
| 139 | + Name: name, |
| 140 | + ModulePath: modulePath, |
| 141 | + Repo: repo, |
| 142 | + Ref: ref, |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + // No replace directive found, check if current branch exists in the dependency repo |
| 149 | + repo := fmt.Sprintf("jfrog/%s", name) |
| 150 | + if name == "jfrog-cli-core" { |
| 151 | + repo = "jfrog/jfrog-cli-core" |
| 152 | + } |
| 153 | + |
| 154 | + // Check if the current branch exists in the repo |
| 155 | + if branchExists(repo, currentBranch) { |
| 156 | + fmt.Printf("Branch '%s' exists in %s, using it\n", currentBranch, repo) |
| 157 | + return &DependencyInfo{ |
| 158 | + Name: name, |
| 159 | + ModulePath: modulePath, |
| 160 | + Repo: repo, |
| 161 | + Ref: currentBranch, |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + // Fall back to master |
| 166 | + fmt.Printf("No matching branch for %s, will use default (master)\n", name) |
| 167 | + return nil |
| 168 | +} |
| 169 | + |
| 170 | +// branchExists checks if a branch exists in a GitHub repository |
| 171 | +func branchExists(repo, branch string) bool { |
| 172 | + if branch == "" || branch == "main" || branch == "master" { |
| 173 | + return false // Don't try to match main/master, use defaults |
| 174 | + } |
| 175 | + |
| 176 | + url := fmt.Sprintf("https://github.com/%s.git", repo) |
| 177 | + cmd := exec.Command("git", "ls-remote", "--heads", url, branch) |
| 178 | + out, err := cmd.Output() |
| 179 | + if err != nil { |
| 180 | + return false |
| 181 | + } |
| 182 | + |
| 183 | + // If output contains the branch name, it exists |
| 184 | + return strings.Contains(string(out), fmt.Sprintf("refs/heads/%s", branch)) |
| 185 | +} |
| 186 | + |
| 187 | +// writeOutput writes the dependency info to GITHUB_OUTPUT |
| 188 | +func writeOutput(output *os.File, name string, info *DependencyInfo) { |
| 189 | + // Convert name to output key format (e.g., "build-info-go" -> "build_info_go") |
| 190 | + keyName := strings.ReplaceAll(name, "-", "_") |
| 191 | + |
| 192 | + var repo, ref string |
| 193 | + if info != nil { |
| 194 | + repo = info.Repo |
| 195 | + ref = info.Ref |
| 196 | + } |
| 197 | + |
| 198 | + // Write to GITHUB_OUTPUT if available |
| 199 | + if output != nil { |
| 200 | + fmt.Fprintf(output, "%s_repo=%s\n", keyName, repo) |
| 201 | + fmt.Fprintf(output, "%s_ref=%s\n", keyName, ref) |
| 202 | + } |
| 203 | + |
| 204 | + // Also print for visibility |
| 205 | + if info != nil { |
| 206 | + fmt.Printf(" %s_repo=%s\n", keyName, repo) |
| 207 | + fmt.Printf(" %s_ref=%s\n", keyName, ref) |
| 208 | + } else { |
| 209 | + fmt.Printf(" %s: using default\n", keyName) |
| 210 | + } |
| 211 | +} |
0 commit comments