1// Copyright 2021 The Bazel Authors. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package main 16 17import ( 18 "bytes" 19 "context" 20 "errors" 21 "fmt" 22 "os" 23 "strings" 24 25 "github.com/google/go-github/v36/github" 26) 27 28type githubClient struct { 29 *github.Client 30} 31 32func (gh *githubClient) listTags(ctx context.Context, org, repo string) (_ []*github.RepositoryTag, err error) { 33 defer func() { 34 if err != nil { 35 err = fmt.Errorf("listing tags in github.com/%s/%s: %w", org, repo, err) 36 } 37 }() 38 39 var allTags []*github.RepositoryTag 40 err = gh.listPages(func(opts *github.ListOptions) (*github.Response, error) { 41 tags, resp, err := gh.Repositories.ListTags(ctx, org, repo, opts) 42 if err != nil { 43 return nil, err 44 } 45 allTags = append(allTags, tags...) 46 return resp, nil 47 }) 48 if err != nil { 49 return nil, err 50 } 51 return allTags, nil 52} 53 54func (gh *githubClient) listReleases(ctx context.Context, org, repo string) (_ []*github.RepositoryRelease, err error) { 55 defer func() { 56 if err != nil { 57 err = fmt.Errorf("listing releases in github.com/%s/%s: %w", org, repo, err) 58 } 59 }() 60 61 var allReleases []*github.RepositoryRelease 62 err = gh.listPages(func(opts *github.ListOptions) (*github.Response, error) { 63 releases, resp, err := gh.Repositories.ListReleases(ctx, org, repo, opts) 64 if err != nil { 65 return nil, err 66 } 67 allReleases = append(allReleases, releases...) 68 return resp, nil 69 }) 70 if err != nil { 71 return nil, err 72 } 73 return allReleases, nil 74} 75 76// getReleaseByTagIncludingDraft is like 77// github.RepositoriesService.GetReleaseByTag, but it also considers draft 78// releases that aren't tagged yet. 79func (gh *githubClient) getReleaseByTagIncludingDraft(ctx context.Context, org, repo, tag string) (*github.RepositoryRelease, error) { 80 releases, err := gh.listReleases(ctx, org, repo) 81 if err != nil { 82 return nil, err 83 } 84 for _, release := range releases { 85 if release.GetTagName() == tag { 86 return release, nil 87 } 88 } 89 return nil, errReleaseNotFound 90} 91 92var errReleaseNotFound = errors.New("release not found") 93 94// githubListPages calls fn repeatedly to get all pages of a large result. 95// This is useful for fetching all tags or all comments or something similar. 96func (gh *githubClient) listPages(fn func(opt *github.ListOptions) (*github.Response, error)) error { 97 opt := &github.ListOptions{PerPage: 50} 98 for { 99 resp, err := fn(opt) 100 if err != nil { 101 return err 102 } 103 if resp.NextPage == 0 { 104 return nil 105 } 106 opt.Page = resp.NextPage 107 } 108} 109 110// githubTokenFlag is used to find a GitHub personal access token on the 111// command line. It accepts a raw token or a path to a file containing a token. 112type githubTokenFlag string 113 114func (f *githubTokenFlag) Set(v string) error { 115 if strings.HasPrefix(v, "ghp_") { 116 *(*string)(f) = v 117 return nil 118 } 119 data, err := os.ReadFile(v) 120 if err != nil { 121 return fmt.Errorf("reading GitHub token: %w", err) 122 } 123 *(*string)(f) = string(bytes.TrimSpace(data)) 124 return nil 125} 126 127func (f *githubTokenFlag) String() string { 128 if f == nil { 129 return "" 130 } 131 return string(*f) 132} 133