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