1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "log" 9 "os" 10 "strconv" 11 "strings" 12 13 "github.com/shurcooL/githubv4" 14 "golang.org/x/oauth2" 15) 16 17func main() { 18 if err := generate(context.Background()); err != nil { 19 log.Fatal(err) 20 } 21} 22 23func generate(ctx context.Context) error { 24 allReleases, err := fetchAllReleases(ctx) 25 if err != nil { 26 return fmt.Errorf("failed to fetch all releases: %w", err) 27 } 28 29 cfg, err := buildConfig(allReleases) 30 if err != nil { 31 return fmt.Errorf("failed to build config: %w", err) 32 } 33 34 if len(os.Args) != 2 { //nolint:gomnd 35 return fmt.Errorf("usage: go run .../main.go out-path.json") 36 } 37 outFile, err := os.Create(os.Args[1]) 38 if err != nil { 39 return fmt.Errorf("failed to create output config file: %w", err) 40 } 41 defer outFile.Close() 42 enc := json.NewEncoder(outFile) 43 enc.SetIndent("", " ") 44 if err = enc.Encode(cfg); err != nil { 45 return fmt.Errorf("failed to json encode config: %w", err) 46 } 47 48 return nil 49} 50 51type logInfo struct { 52 Warning string `json:",omitempty"` 53 Info string `json:",omitempty"` 54} 55 56type versionConfig struct { 57 Error string `json:",omitempty"` 58 59 Log *logInfo `json:",omitempty"` 60 61 TargetVersion string `json:",omitempty"` 62 AssetURL string `json:",omitempty"` 63} 64 65type actionConfig struct { 66 MinorVersionToConfig map[string]versionConfig 67} 68 69type version struct { 70 major, minor, patch int 71} 72 73func (v version) String() string { 74 ret := fmt.Sprintf("v%d.%d", v.major, v.minor) 75 if v.patch != noPatch { 76 ret += fmt.Sprintf(".%d", v.patch) 77 } 78 return ret 79} 80 81func (v *version) isAfterOrEq(vv *version) bool { 82 if v.major != vv.major { 83 return v.major >= vv.major 84 } 85 if v.minor != vv.minor { 86 return v.minor >= vv.minor 87 } 88 89 return v.patch >= vv.patch 90} 91 92const noPatch = -1 93 94func parseVersion(s string) (*version, error) { 95 const vPrefix = "v" 96 if !strings.HasPrefix(s, vPrefix) { 97 return nil, fmt.Errorf("version should start with %q", vPrefix) 98 } 99 s = strings.TrimPrefix(s, vPrefix) 100 101 parts := strings.Split(s, ".") 102 103 var nums []int 104 for _, part := range parts { 105 num, err := strconv.Atoi(part) 106 if err != nil { 107 return nil, fmt.Errorf("failed to parse version part: %w", err) 108 } 109 nums = append(nums, num) 110 } 111 112 if len(nums) == 2 { //nolint:gomnd 113 return &version{major: nums[0], minor: nums[1], patch: noPatch}, nil 114 } 115 if len(nums) == 3 { //nolint:gomnd 116 return &version{major: nums[0], minor: nums[1], patch: nums[2]}, nil 117 } 118 119 return nil, errors.New("invalid version format") 120} 121 122func findLinuxAssetURL(ver *version, releaseAssets []releaseAsset) (string, error) { 123 pattern := fmt.Sprintf("golangci-lint-%d.%d.%d-linux-amd64.tar.gz", ver.major, ver.minor, ver.patch) 124 for _, relAsset := range releaseAssets { 125 if strings.HasSuffix(relAsset.DownloadURL, pattern) { 126 return relAsset.DownloadURL, nil 127 } 128 } 129 return "", fmt.Errorf("no matched asset url for pattern %q", pattern) 130} 131 132func buildConfig(releases []release) (*actionConfig, error) { 133 versionToRelease := map[version]release{} 134 for _, rel := range releases { 135 ver, err := parseVersion(rel.TagName) 136 if err != nil { 137 return nil, fmt.Errorf("failed to parse release %s version: %w", rel.TagName, err) 138 } 139 if _, ok := versionToRelease[*ver]; ok { 140 return nil, fmt.Errorf("duplicate release %s", rel.TagName) 141 } 142 versionToRelease[*ver] = rel 143 } 144 145 maxPatchReleases := map[string]version{} 146 for ver := range versionToRelease { 147 key := fmt.Sprintf("v%d.%d", ver.major, ver.minor) 148 if mapVer, ok := maxPatchReleases[key]; !ok || ver.isAfterOrEq(&mapVer) { 149 maxPatchReleases[key] = ver 150 } 151 } 152 153 minorVersionToConfig := map[string]versionConfig{} 154 minAllowedVersion := version{major: 1, minor: 14, patch: 0} 155 156 latestVersion := version{} 157 latestVersionConfig := versionConfig{} 158 for minorVersionedStr, maxPatchVersion := range maxPatchReleases { 159 if !maxPatchVersion.isAfterOrEq(&minAllowedVersion) { 160 minorVersionToConfig[minorVersionedStr] = versionConfig{ 161 Error: fmt.Sprintf("golangci-lint version '%s' isn't supported: we support only %s and later versions", 162 minorVersionedStr, minAllowedVersion), 163 } 164 continue 165 } 166 maxPatchVersion := maxPatchVersion 167 assetURL, err := findLinuxAssetURL(&maxPatchVersion, versionToRelease[maxPatchVersion].ReleaseAssets.Nodes) 168 if err != nil { 169 return nil, fmt.Errorf("failed to find linux asset url for release %s: %w", maxPatchVersion, err) 170 } 171 minorVersionToConfig[minorVersionedStr] = versionConfig{ 172 TargetVersion: maxPatchVersion.String(), 173 AssetURL: assetURL, 174 } 175 if maxPatchVersion.isAfterOrEq(&latestVersion) { 176 latestVersion = maxPatchVersion 177 latestVersionConfig.TargetVersion = maxPatchVersion.String() 178 latestVersionConfig.AssetURL = assetURL 179 } 180 } 181 minorVersionToConfig["latest"] = latestVersionConfig 182 183 return &actionConfig{MinorVersionToConfig: minorVersionToConfig}, nil 184} 185 186type release struct { 187 TagName string 188 ReleaseAssets struct { 189 Nodes []releaseAsset 190 } `graphql:"releaseAssets(first: 50)"` 191} 192 193type releaseAsset struct { 194 DownloadURL string 195} 196 197func fetchAllReleases(ctx context.Context) ([]release, error) { 198 githubToken := os.Getenv("GITHUB_TOKEN") 199 if githubToken == "" { 200 return nil, errors.New("no GITHUB_TOKEN environment variable") 201 } 202 src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken}) 203 httpClient := oauth2.NewClient(ctx, src) 204 client := githubv4.NewClient(httpClient) 205 206 var q struct { 207 Repository struct { 208 Releases struct { 209 Nodes []release 210 PageInfo struct { 211 EndCursor githubv4.String 212 HasNextPage bool 213 } 214 } `graphql:"releases(first: 100, after: $releasesCursor)"` 215 } `graphql:"repository(owner: $owner, name: $name)"` 216 } 217 218 vars := map[string]interface{}{ 219 "owner": githubv4.String("golangci"), 220 "name": githubv4.String("golangci-lint"), 221 "releasesCursor": (*githubv4.String)(nil), 222 } 223 224 var allReleases []release 225 for { 226 err := client.Query(ctx, &q, vars) 227 if err != nil { 228 return nil, fmt.Errorf("failed to fetch releases page from github: %w", err) 229 } 230 releases := q.Repository.Releases 231 allReleases = append(allReleases, releases.Nodes...) 232 if !releases.PageInfo.HasNextPage { 233 break 234 } 235 vars["releasesCursor"] = githubv4.NewString(releases.PageInfo.EndCursor) 236 } 237 238 return allReleases, nil 239} 240