Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dev: clean gen_github_action_config #4847

Merged
merged 1 commit into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/gen_github_action_config/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/golangci/golangci-lint/scripts/gen_github_action_config
go 1.21

require (
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
github.com/shurcooL/githubv4 v0.0.0-20240429030203-be2daab69064
golang.org/x/oauth2 v0.21.0
)

Expand Down
4 changes: 2 additions & 2 deletions scripts/gen_github_action_config/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

226 changes: 124 additions & 102 deletions scripts/gen_github_action_config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,7 @@ import (
"golang.org/x/oauth2"
)

func main() {
if err := generate(context.Background()); err != nil {
log.Fatal(err)
}
}

func generate(ctx context.Context) error {
allReleases, err := fetchAllReleases(ctx)
if err != nil {
return fmt.Errorf("failed to fetch all releases: %w", err)
}

cfg, err := buildConfig(allReleases)
if err != nil {
return fmt.Errorf("failed to build config: %w", err)
}

if len(os.Args) != 2 { //nolint:gomnd
return fmt.Errorf("usage: go run .../main.go out-path.json")
}
outFile, err := os.Create(os.Args[1])
if err != nil {
return fmt.Errorf("failed to create output config file: %w", err)
}
defer outFile.Close()
enc := json.NewEncoder(outFile)
enc.SetIndent("", " ")
if err = enc.Encode(cfg); err != nil {
return fmt.Errorf("failed to json encode config: %w", err)
}

return nil
}
const noPatch = -1

type logInfo struct {
Warning string `json:",omitempty"`
Expand All @@ -72,79 +40,142 @@ type version struct {

func (v version) String() string {
ret := fmt.Sprintf("v%d.%d", v.major, v.minor)

if v.patch != noPatch {
ret += fmt.Sprintf(".%d", v.patch)
}

return ret
}

func (v *version) isAfterOrEq(vv *version) bool {
func (v version) isAfterOrEq(vv *version) bool {
if v.major != vv.major {
return v.major >= vv.major
}

if v.minor != vv.minor {
return v.minor >= vv.minor
}

return v.patch >= vv.patch
}

const noPatch = -1
type release struct {
TagName string
ReleaseAssets struct {
Nodes []releaseAsset
} `graphql:"releaseAssets(first: 50)"`
}

func parseVersion(s string) (*version, error) {
const vPrefix = "v"
if !strings.HasPrefix(s, vPrefix) {
return nil, fmt.Errorf("version should start with %q", vPrefix)
type releaseAsset struct {
DownloadURL string
}

func main() {
if err := generate(context.Background()); err != nil {
log.Fatal(err)
}
s = strings.TrimPrefix(s, vPrefix)
}

parts := strings.Split(s, ".")
func generate(ctx context.Context) error {
if len(os.Args) != 2 {
return fmt.Errorf("usage: go run .../main.go out-path.json")
}

var nums []int
for _, part := range parts {
num, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("failed to parse version part: %w", err)
}
nums = append(nums, num)
allReleases, err := fetchAllReleases(ctx)
if err != nil {
return fmt.Errorf("failed to fetch all releases: %w", err)
}

if len(nums) == 2 { //nolint:gomnd
return &version{major: nums[0], minor: nums[1], patch: noPatch}, nil
cfg, err := buildConfig(allReleases)
if err != nil {
return fmt.Errorf("failed to build config: %w", err)
}
if len(nums) == 3 { //nolint:gomnd
return &version{major: nums[0], minor: nums[1], patch: nums[2]}, nil

outFile, err := os.Create(os.Args[1])
if err != nil {
return fmt.Errorf("failed to create output config file: %w", err)
}

return nil, errors.New("invalid version format")
defer outFile.Close()

enc := json.NewEncoder(outFile)
enc.SetIndent("", " ")

if err = enc.Encode(cfg); err != nil {
return fmt.Errorf("failed to json encode config: %w", err)
}

return nil
}

func findLinuxAssetURL(ver *version, releaseAssets []releaseAsset) (string, error) {
pattern := fmt.Sprintf("golangci-lint-%d.%d.%d-linux-amd64.tar.gz", ver.major, ver.minor, ver.patch)
for _, relAsset := range releaseAssets {
if strings.HasSuffix(relAsset.DownloadURL, pattern) {
return relAsset.DownloadURL, nil
func fetchAllReleases(ctx context.Context) ([]release, error) {
githubToken := os.Getenv("GITHUB_TOKEN")
if githubToken == "" {
return nil, errors.New("no GITHUB_TOKEN environment variable")
}

client := githubv4.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken})))

var q struct {
Repository struct {
Releases struct {
Nodes []release
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"releases(first: 100, orderBy: { field: CREATED_AT, direction: DESC }, after: $releasesCursor)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

vars := map[string]any{
"owner": githubv4.String("golangci"),
"name": githubv4.String("golangci-lint"),
"releasesCursor": (*githubv4.String)(nil),
}

var allReleases []release
for {
err := client.Query(ctx, &q, vars)
if err != nil {
return nil, fmt.Errorf("failed to fetch releases page from GitHub: %w", err)
}

releases := q.Repository.Releases
allReleases = append(allReleases, releases.Nodes...)

if !releases.PageInfo.HasNextPage {
break
}

vars["releasesCursor"] = githubv4.NewString(releases.PageInfo.EndCursor)
}
return "", fmt.Errorf("no matched asset url for pattern %q", pattern)

return allReleases, nil
}

func buildConfig(releases []release) (*actionConfig, error) {
versionToRelease := map[version]release{}

for _, rel := range releases {
ver, err := parseVersion(rel.TagName)
if err != nil {
return nil, fmt.Errorf("failed to parse release %s version: %w", rel.TagName, err)
}

if _, ok := versionToRelease[*ver]; ok {
return nil, fmt.Errorf("duplicate release %s", rel.TagName)
}

versionToRelease[*ver] = rel
}

maxPatchReleases := map[string]version{}

for ver := range versionToRelease {
key := fmt.Sprintf("v%d.%d", ver.major, ver.minor)

if mapVer, ok := maxPatchReleases[key]; !ok || ver.isAfterOrEq(&mapVer) {
maxPatchReleases[key] = ver
}
Expand All @@ -155,6 +186,7 @@ func buildConfig(releases []release) (*actionConfig, error) {

latestVersion := version{}
latestVersionConfig := versionConfig{}

for minorVersionedStr, maxPatchVersion := range maxPatchReleases {
if !maxPatchVersion.isAfterOrEq(&minAllowedVersion) {
minorVersionToConfig[minorVersionedStr] = versionConfig{
Expand All @@ -163,77 +195,67 @@ func buildConfig(releases []release) (*actionConfig, error) {
}
continue
}

maxPatchVersion := maxPatchVersion

assetURL, err := findLinuxAssetURL(&maxPatchVersion, versionToRelease[maxPatchVersion].ReleaseAssets.Nodes)
if err != nil {
return nil, fmt.Errorf("failed to find linux asset url for release %s: %w", maxPatchVersion, err)
}

minorVersionToConfig[minorVersionedStr] = versionConfig{
TargetVersion: maxPatchVersion.String(),
AssetURL: assetURL,
}

if maxPatchVersion.isAfterOrEq(&latestVersion) {
latestVersion = maxPatchVersion
latestVersionConfig.TargetVersion = maxPatchVersion.String()
latestVersionConfig.AssetURL = assetURL
}
}

minorVersionToConfig["latest"] = latestVersionConfig

return &actionConfig{MinorVersionToConfig: minorVersionToConfig}, nil
}

type release struct {
TagName string
ReleaseAssets struct {
Nodes []releaseAsset
} `graphql:"releaseAssets(first: 50)"`
}

type releaseAsset struct {
DownloadURL string
}
func findLinuxAssetURL(ver *version, releaseAssets []releaseAsset) (string, error) {
pattern := fmt.Sprintf("golangci-lint-%d.%d.%d-linux-amd64.tar.gz", ver.major, ver.minor, ver.patch)

func fetchAllReleases(ctx context.Context) ([]release, error) {
githubToken := os.Getenv("GITHUB_TOKEN")
if githubToken == "" {
return nil, errors.New("no GITHUB_TOKEN environment variable")
for _, relAsset := range releaseAssets {
if strings.HasSuffix(relAsset.DownloadURL, pattern) {
return relAsset.DownloadURL, nil
}
}
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken})
httpClient := oauth2.NewClient(ctx, src)
client := githubv4.NewClient(httpClient)

var q struct {
Repository struct {
Releases struct {
Nodes []release
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"releases(first: 100, orderBy: { field: CREATED_AT, direction: DESC }, after: $releasesCursor)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}
return "", fmt.Errorf("no matched asset url for pattern %q", pattern)
}

vars := map[string]any{
"owner": githubv4.String("golangci"),
"name": githubv4.String("golangci-lint"),
"releasesCursor": (*githubv4.String)(nil),
func parseVersion(s string) (*version, error) {
const vPrefix = "v"
if !strings.HasPrefix(s, vPrefix) {
return nil, fmt.Errorf("version %q should start with %q", s, vPrefix)
}

var allReleases []release
for {
err := client.Query(ctx, &q, vars)
parts := strings.Split(strings.TrimPrefix(s, vPrefix), ".")

var nums []int
for _, part := range parts {
num, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("failed to fetch releases page from GitHub: %w", err)
return nil, fmt.Errorf("failed to parse version %q: %w", s, err)
}
releases := q.Repository.Releases
allReleases = append(allReleases, releases.Nodes...)
if !releases.PageInfo.HasNextPage {
break
}
vars["releasesCursor"] = githubv4.NewString(releases.PageInfo.EndCursor)

nums = append(nums, num)
}

return allReleases, nil
switch len(nums) {
case 2:
return &version{major: nums[0], minor: nums[1], patch: noPatch}, nil
case 3:
return &version{major: nums[0], minor: nums[1], patch: nums[2]}, nil
default:
return nil, fmt.Errorf("invalid version format: %s", s)
}
}
Loading