├── .gitignore ├── .gitattributes ├── github ├── config.go ├── types.go └── github.go ├── config.go ├── Dockerfile ├── go.mod ├── types.go ├── .github └── workflows │ └── deploy.yml ├── time.go ├── listen.go ├── helper.go ├── main.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | *.out 3 | .idea 4 | .DS_Store 5 | artifacts -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /github/config.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | type Config struct { 4 | GithubToken string `env:"GH_TOKEN,required"` 5 | RepoOwner string `env:"REPO_OWNER,required"` 6 | RepoName string `env:"REPO_NAME,required"` 7 | DevelopmentBranch string `env:"DEV_BRANCH" envDefault:"dev"` 8 | } 9 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/flipper-zero/flipper-update-server/github" 4 | 5 | type config struct { 6 | Github github.Config 7 | Excluded []string `env:"EXCLUDED"` 8 | ArtifactsPath string `env:"ARTIFACTS_PATH" envDefault:"/artifacts"` 9 | BaseURL string `env:"BASE_URL"` 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine as builder 2 | 3 | WORKDIR /app 4 | COPY go.mod go.sum ./ 5 | 6 | RUN go mod download 7 | COPY . . 8 | 9 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -a -installsuffix cgo -o /go/bin/app . 10 | 11 | 12 | FROM alpine 13 | 14 | COPY --from=builder /go/bin/app /go/bin/app 15 | 16 | EXPOSE 8080 17 | ENTRYPOINT ["/go/bin/app"] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flipper-zero/flipper-update-server 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/caarlos0/env/v6 v6.6.2 7 | github.com/coreos/go-semver v0.3.0 8 | github.com/gin-gonic/gin v1.7.2 9 | github.com/google/go-github/v37 v37.0.0 10 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be 11 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /github/types.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "github.com/google/go-github/v37/github" 5 | "time" 6 | ) 7 | 8 | type Github struct { 9 | c *github.Client 10 | cfg *Config 11 | releases map[string]*Version 12 | branchesAndTags map[string]struct{} 13 | dev *Version 14 | } 15 | 16 | type Version struct { 17 | Version string 18 | Changelog string 19 | Date time.Time 20 | Rc bool 21 | } 22 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type directory struct { 4 | Channels []channel `json:"channels"` 5 | } 6 | 7 | type channel struct { 8 | ID string `json:"id"` 9 | Title string `json:"title"` 10 | Description string `json:"description"` 11 | Versions []version `json:"versions"` 12 | } 13 | 14 | type version struct { 15 | Version string `json:"version"` 16 | Changelog string `json:"changelog"` 17 | Timestamp Time `json:"timestamp"` 18 | Files []file `json:"files"` 19 | } 20 | 21 | type file struct { 22 | URL string `json:"url"` 23 | Target string `json:"target"` 24 | Type string `json:"type"` 25 | Sha256 string `json:"sha256"` 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: master 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v1 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | - name: Login to GitHub Container Registry 20 | uses: docker/login-action@v1 21 | with: 22 | registry: ghcr.io 23 | username: ${{ secrets.CR_USERNAME }} 24 | password: ${{ secrets.CR_PAT }} 25 | - name: Build and push 26 | id: docker_build 27 | uses: docker/build-push-action@v2 28 | with: 29 | context: . 30 | push: true 31 | tags: ghcr.io/flipperdevices/flipper-update-server:latest 32 | - name: Image digest 33 | run: echo ${{ steps.docker_build.outputs.digest }} -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // Time defines a timestamp encoded as epoch seconds in JSON 9 | type Time time.Time 10 | 11 | // MarshalJSON is used to convert the timestamp to JSON 12 | func (t Time) MarshalJSON() ([]byte, error) { 13 | return []byte(strconv.FormatInt(time.Time(t).Unix(), 10)), nil 14 | } 15 | 16 | // UnmarshalJSON is used to convert the timestamp from JSON 17 | func (t *Time) UnmarshalJSON(s []byte) (err error) { 18 | r := string(s) 19 | q, err := strconv.ParseInt(r, 10, 64) 20 | if err != nil { 21 | return err 22 | } 23 | *(*time.Time)(t) = time.Unix(q, 0) 24 | return nil 25 | } 26 | 27 | // Unix returns t as a Unix time, the number of seconds elapsed 28 | // since January 1, 1970 UTC. The result does not depend on the 29 | // location associated with t. 30 | func (t Time) Unix() int64 { 31 | return time.Time(t).Unix() 32 | } 33 | 34 | // Time returns the JSON time as a time.Time instance in UTC 35 | func (t Time) Time() time.Time { 36 | return time.Time(t).UTC() 37 | } 38 | 39 | // String returns t as a formatted string 40 | func (t Time) String() string { 41 | return t.Time().String() 42 | } 43 | -------------------------------------------------------------------------------- /listen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "log" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | func handleReindex(c *gin.Context) { 11 | if c.PostForm("key") != cfg.Github.GithubToken { 12 | c.String(http.StatusForbidden, "wrong key") 13 | return 14 | } 15 | 16 | go func() { 17 | err := regenDirectory() 18 | if err != nil { 19 | log.Println("Regen", err) 20 | } 21 | }() 22 | 23 | c.String(http.StatusOK, "ok") 24 | } 25 | 26 | func serveDirectory(c *gin.Context) { 27 | c.JSON(http.StatusOK, latestDirectory) 28 | } 29 | 30 | func serveLatest(c *gin.Context) { 31 | chID := c.Param("channel") 32 | var ch *channel 33 | for _, i := range latestDirectory.Channels { 34 | if i.ID == chID { 35 | ch = &i 36 | break 37 | } 38 | } 39 | if ch == nil { 40 | c.String(http.StatusNotFound, "no such channel") 41 | return 42 | } 43 | if len(ch.Versions) == 0 { 44 | c.String(http.StatusNotFound, "no versions in this channel") 45 | return 46 | } 47 | 48 | ver := ch.Versions[len(ch.Versions)-1] 49 | target := strings.ReplaceAll(c.Param("target"), "-", "/") 50 | t := c.Param("type") 51 | 52 | for _, f := range ver.Files { 53 | if f.Type == t && f.Target == target { 54 | c.Redirect(http.StatusFound, f.URL) 55 | return 56 | } 57 | } 58 | 59 | c.String(http.StatusNotFound, "no such target or type") 60 | return 61 | } -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | var regexes = []*regexp.Regexp{ 14 | regexp.MustCompile(`(?m)^flipper-z-(f[0-9]*|any)-(update|updater|bootloader|firmware|full|core2_firmware|scripts|resources|sdk|lib)-.*\.(dfu|bin|elf|hex|tgz|json|zip)$`), 15 | regexp.MustCompile(`(?m)^(f[0-9]*)_(bootloader|firmware|full)\.(dfu|bin|elf|hex)$`), 16 | } 17 | 18 | func isExistingDir(path string) bool { 19 | fi, err := os.Stat(path) 20 | if err != nil { 21 | return false 22 | } 23 | return fi.IsDir() 24 | } 25 | 26 | func removeWithParents(path string) error { 27 | err := os.RemoveAll(path) 28 | if err != nil { 29 | return err 30 | } 31 | parent := filepath.Dir(path) 32 | empty, err := isDirEmpty(parent) 33 | if err != nil { 34 | return err 35 | } 36 | if empty { 37 | return removeWithParents(parent) 38 | } 39 | return nil 40 | } 41 | 42 | func isDirEmpty(path string) (bool, error) { 43 | f, err := os.Open(path) 44 | if err != nil { 45 | return false, err 46 | } 47 | defer f.Close() 48 | 49 | _, err = f.Readdirnames(1) 50 | if err == io.EOF { 51 | return true, nil 52 | } 53 | return false, err 54 | } 55 | 56 | func arrayContains(stack []string, needle string) bool { 57 | for _, e := range stack { 58 | if e == needle { 59 | return true 60 | } 61 | } 62 | return false 63 | } 64 | 65 | func parseFilename(name string) *file { 66 | // TODO refactor this hardcoded crap 67 | if strings.HasPrefix(name, "qFlipper") { 68 | switch filepath.Ext(name) { 69 | case ".dmg": 70 | return &file{ 71 | Target: "macos/amd64", 72 | Type: "dmg", 73 | } 74 | case ".AppImage": 75 | return &file{ 76 | Target: "linux/amd64", 77 | Type: "AppImage", 78 | } 79 | case ".zip": 80 | return &file{ 81 | Target: "windows/amd64", 82 | Type: "portable", 83 | } 84 | case ".exe": 85 | return &file{ 86 | Target: "windows/amd64", 87 | Type: "installer", 88 | } 89 | } 90 | } 91 | 92 | for _, re := range regexes { 93 | m := re.FindAllStringSubmatch(name, -1) 94 | if len(m) != 1 || len(m[0]) != 4 { 95 | continue 96 | } 97 | return &file{ 98 | Type: m[0][2] + "_" + m[0][3], 99 | Target: m[0][1], 100 | } 101 | } 102 | return nil 103 | } 104 | 105 | func calculateSha256(data []byte) string { 106 | hash := sha256.Sum256(data) 107 | return hex.EncodeToString(hash[:]) 108 | } 109 | -------------------------------------------------------------------------------- /github/github.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/google/go-github/v37/github" 7 | "golang.org/x/oauth2" 8 | ) 9 | 10 | var ctx = context.Background() 11 | 12 | func New(cfg Config) (*Github, error) { 13 | ts := oauth2.StaticTokenSource( 14 | &oauth2.Token{AccessToken: cfg.GithubToken}, 15 | ) 16 | tc := oauth2.NewClient(ctx, ts) 17 | c := github.NewClient(tc) 18 | gh := &Github{ 19 | c: c, 20 | cfg: &cfg, 21 | } 22 | 23 | err := gh.Sync() 24 | if err != nil { 25 | return nil, err 26 | } 27 | return gh, nil 28 | } 29 | 30 | func (gh *Github) Sync() error { 31 | releases, err := gh.fetchReleases() 32 | if err != nil { 33 | return errors.New("releases: " + err.Error()) 34 | } 35 | gh.releases = releases 36 | 37 | branchesAndTags, err := gh.fetchBranchesAndTags() 38 | if err != nil { 39 | return errors.New("branches: " + err.Error()) 40 | } 41 | gh.branchesAndTags = branchesAndTags 42 | 43 | dev, err := gh.fetchDev() 44 | if err != nil { 45 | return errors.New("dev: " + err.Error()) 46 | } 47 | gh.dev = dev 48 | 49 | return nil 50 | } 51 | 52 | func (gh *Github) Lookup(ref string) (*Version, bool) { 53 | if ref == gh.cfg.DevelopmentBranch { 54 | return gh.dev, false 55 | } 56 | r, ok := gh.releases[ref] 57 | if ok { 58 | return r, false 59 | } 60 | _, ok = gh.branchesAndTags[ref] 61 | return nil, ok 62 | } 63 | 64 | func (gh *Github) fetchReleases() (map[string]*Version, error) { 65 | releases, _, err := gh.c.Repositories.ListReleases(ctx, gh.cfg.RepoOwner, gh.cfg.RepoName, &github.ListOptions{ 66 | Page: 1, 67 | PerPage: 100, 68 | }) 69 | if err != nil { 70 | return nil, err 71 | } 72 | m := make(map[string]*Version) 73 | for _, r := range releases { 74 | m[r.GetTagName()] = &Version{ 75 | Version: r.GetTagName(), 76 | Changelog: r.GetBody(), 77 | Date: r.GetCreatedAt().Time, 78 | Rc: r.GetPrerelease(), 79 | } 80 | } 81 | return m, nil 82 | } 83 | 84 | func (gh *Github) fetchBranchesAndTags() (map[string]struct{}, error) { 85 | branches, _, err := gh.c.Repositories.ListBranches(ctx, gh.cfg.RepoOwner, gh.cfg.RepoName, &github.BranchListOptions{ 86 | ListOptions: github.ListOptions{ 87 | Page: 1, 88 | PerPage: 100, 89 | }, 90 | }) 91 | if err != nil { 92 | return nil, err 93 | } 94 | tags, _, err := gh.c.Repositories.ListTags(ctx, gh.cfg.RepoOwner, gh.cfg.RepoName, &github.ListOptions{ 95 | Page: 1, 96 | PerPage: 100, 97 | }) 98 | if err != nil { 99 | return nil, err 100 | } 101 | m := make(map[string]struct{}) 102 | for _, b := range branches { 103 | m[b.GetName()] = struct{}{} 104 | } 105 | for _, t := range tags { 106 | m[t.GetName()] = struct{}{} 107 | } 108 | return m, nil 109 | } 110 | 111 | func (gh *Github) fetchDev() (*Version, error) { 112 | commit, _, err := gh.c.Repositories.GetCommit(ctx, gh.cfg.RepoOwner, gh.cfg.RepoName, "HEAD") 113 | if err != nil { 114 | return nil, err 115 | } 116 | return &Version{ 117 | Version: commit.GetSHA()[0:8], 118 | Changelog: "Last commit: " + commit.Commit.GetMessage(), 119 | Date: commit.Commit.Author.GetDate(), 120 | }, nil 121 | } 122 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/caarlos0/env/v6" 12 | "github.com/coreos/go-semver/semver" 13 | "github.com/flipper-zero/flipper-update-server/github" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | var cfg config 18 | var gh *github.Github 19 | var latestDirectory directory 20 | 21 | func main() { 22 | if err := env.Parse(&cfg); err != nil { 23 | log.Fatalln("Config", err) 24 | } 25 | 26 | if !isExistingDir(cfg.ArtifactsPath) { 27 | log.Fatalln(cfg.ArtifactsPath, "is not an existing directory") 28 | } 29 | 30 | var err error 31 | 32 | gh, err = github.New(cfg.Github) 33 | if err != nil { 34 | log.Fatalln("GitHub", err) 35 | } 36 | 37 | err = regenDirectory() 38 | if err != nil { 39 | log.Fatalln("Regen", err) 40 | } 41 | 42 | log.Println("Server started") 43 | 44 | r := gin.New() 45 | 46 | r.GET("/directory.json", serveDirectory) 47 | r.GET("/:channel/:target/:type", serveLatest) 48 | r.POST("/reindex", handleReindex) 49 | 50 | log.Fatal(r.Run(":8080")) 51 | } 52 | 53 | func regenDirectory() error { 54 | err := gh.Sync() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | devChannel := channel{ 60 | ID: "development", 61 | Title: "Development Channel", 62 | Description: "Latest builds, not yet tested by Flipper QA, be careful", 63 | } 64 | rcChannel := channel{ 65 | ID: "release-candidate", 66 | Title: "Release Candidate Channel", 67 | Description: "This is going to be released soon, undergoing QA tests now", 68 | } 69 | releaseChannel := channel{ 70 | ID: "release", 71 | Title: "Stable Release Channel", 72 | Description: "Stable releases, tested by Flipper QA", 73 | } 74 | 75 | dirs := make(map[string]struct{}) 76 | err = filepath.Walk(cfg.ArtifactsPath, func(path string, c os.FileInfo, err error) error { 77 | if !c.IsDir() { 78 | return nil 79 | } 80 | delete(dirs, filepath.Dir(path)) 81 | dirs[path] = struct{}{} 82 | return nil 83 | }) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | for path := range dirs { 89 | name := strings.TrimPrefix(path, strings.TrimPrefix(cfg.ArtifactsPath, "./")) 90 | name = strings.TrimLeft(name, "/") 91 | if arrayContains(cfg.Excluded, name) { 92 | continue 93 | } 94 | 95 | ver, isBranch := gh.Lookup(name) 96 | if isBranch { 97 | continue 98 | } 99 | if ver == nil { 100 | log.Println("Deleting", name) 101 | err = removeWithParents(filepath.Join(cfg.ArtifactsPath, name)) 102 | if err != nil { 103 | log.Println("Can't delete", name, err) 104 | } 105 | continue 106 | } 107 | 108 | v := version{ 109 | Version: ver.Version, 110 | Changelog: ver.Changelog, 111 | Timestamp: Time(ver.Date), 112 | Files: scanFiles(name), 113 | } 114 | 115 | if name == cfg.Github.DevelopmentBranch { 116 | devChannel.Versions = append(devChannel.Versions, v) 117 | continue 118 | } 119 | if ver.Rc { 120 | rcChannel.Versions = append(rcChannel.Versions, v) 121 | } else { 122 | releaseChannel.Versions = append(releaseChannel.Versions, v) 123 | } 124 | } 125 | 126 | latestDirectory = directory{ 127 | Channels: []channel{devChannel, rcChannel, releaseChannel}, 128 | } 129 | for k := range latestDirectory.Channels { 130 | c := &latestDirectory.Channels[k] 131 | sort.Slice(c.Versions, func(i, j int) bool { 132 | v1, err := semver.NewVersion(c.Versions[i].Version) 133 | v2, err := semver.NewVersion(c.Versions[j].Version) 134 | if err != nil { 135 | return c.Versions[i].Timestamp.Time().Before(c.Versions[j].Timestamp.Time()) 136 | } 137 | return !v1.LessThan(*v2) 138 | }) 139 | c.Versions = c.Versions[:1] 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func scanFiles(folder string) (files []file) { 146 | content, err := ioutil.ReadDir(filepath.Join(cfg.ArtifactsPath, folder)) 147 | if err != nil { 148 | return 149 | } 150 | for _, c := range content { 151 | if c.IsDir() { 152 | continue 153 | } 154 | f := parseFilename(c.Name()) 155 | if f == nil { 156 | continue 157 | } 158 | f.URL = cfg.BaseURL + filepath.Join(folder, c.Name()) 159 | bin, err := ioutil.ReadFile(filepath.Join(cfg.ArtifactsPath, folder, c.Name())) 160 | if err == nil { 161 | f.Sha256 = calculateSha256(bin) 162 | } 163 | files = append(files, *f) 164 | } 165 | return 166 | } 167 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/caarlos0/env/v6 v6.6.2 h1:BypLXDWQTA32rS4UM7pBz+/0BOuvs6C7LSeQAxMwyvI= 2 | github.com/caarlos0/env/v6 v6.6.2/go.mod h1:P0BVSgU9zfkxfSpFUs6KsO3uWR4k3Ac0P66ibAGTybM= 3 | github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= 4 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 9 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 10 | github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= 11 | github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 12 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 13 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 14 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 15 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 16 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 17 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 18 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 19 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 20 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 22 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 23 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 24 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/go-github/v37 v37.0.0 h1:rCspN8/6kB1BAJWZfuafvHhyfIo5fkAulaP/3bOQ/tM= 26 | github.com/google/go-github/v37 v37.0.0/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4= 27 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 28 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 30 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 31 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 32 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 33 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 34 | github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= 35 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 36 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 37 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 38 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 39 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 40 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 41 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 46 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 47 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 48 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 49 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 50 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 51 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 52 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 53 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 54 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 55 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 56 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 57 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 58 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= 59 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 60 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 61 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 63 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 65 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 70 | google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= 71 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 75 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 76 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 77 | --------------------------------------------------------------------------------