├── .gitignore ├── LICENSE ├── README.md ├── cmd └── github-sprinter │ └── main.go ├── go.mod ├── go.sum ├── manifest.go ├── milestone.go ├── screenshot.png ├── sprint.yaml └── sprinter.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .envrc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 micnncim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-sprinter 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/micnncim/github-sprinter)](https://goreportcard.com/report/github.com/micnncim/github-sprinter) 4 | [![CodeFactor](https://www.codefactor.io/repository/github/micnncim/github-sprinter/badge)](https://www.codefactor.io/repository/github/micnncim/github-sprinter) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/3ce77cb742824ffb9fb8610b0e4b1e25)](https://www.codacy.com/app/micnncim/github-sprinter?utm_source=github.com&utm_medium=referral&utm_content=micnncim/github-sprinter&utm_campaign=Badge_Grade) 6 | [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 7 | 8 | CLI managing GitHub milestones as sprints with YAML file. 9 | Strongly inspired by [b4b4r07/github-labeler](https://github.com/b4b4r07/github-labeler). 10 | 11 | ![screenshot](./screenshot.png) 12 | 13 | ## Installation 14 | 15 | ``` 16 | $ go get -u github.com/micnncim/github-sprinter/cmd/github-sprinter 17 | ``` 18 | 19 | ## Usage 20 | 21 | ``` 22 | $ github-sprinter 23 | ``` 24 | 25 | Specify manifest (default=`sprint.yaml`). 26 | 27 | ``` 28 | $ github-sprinter -manifest manifest.yaml 29 | ``` 30 | 31 | Support dry-run. 32 | 33 | ``` 34 | $ github-sprinter -dry-run 35 | ``` 36 | 37 | Update (including deletion) available. 38 | If not specify `-update`, `github-sprinter` will not delete any milestone and will create some milestones. 39 | Even though `github-sprinter` deletes some milestones, only `open` milestones will deleted. 40 | 41 | ``` 42 | $ github-sprinter -update 43 | ``` 44 | 45 | ## Manifest example 46 | 47 | ```yaml 48 | sprint: 49 | title_format: Sprint {{ .SID }} ({{ .StartOn }}-{{ .DueOn }}) 50 | duration: 168h # 1 week (24h * 7d = 168h) 51 | terms: 52 | - start_on: 2019/04/01 53 | due_on: 2020/03/31 54 | - start_on: 2020/04/01 55 | due_on: 2021/03/31 56 | ignore: 57 | terms: 58 | - start_on: 2019/08/01 59 | due_on: 2019/08/14 60 | - start_on: 2019/12/27 61 | due_on: 2020/01/01 62 | edge_weekdays: 63 | - Saturday 64 | - Sunday 65 | 66 | repos: 67 | - name: micnncim/github-sprinter 68 | ``` 69 | 70 | ### Use case 71 | 72 | #### `-update` 73 | 74 | - Change duration of each sprint 75 | 76 | Change `sprint.duration` (`sprint.title_format` `sprint.terms` `sprint.ignore`) 77 | :warning: This operation **DELETE**s all open milestone once and creates new milestones (it means the links between milestones and issues will be removed) 78 | 79 | ```diff 80 | sprint: 81 | - duration: 168h # 1 week (24h * 7d = 168h) 82 | + duration: 120h # 5 days (24h * 5d = 120h) 83 | ``` 84 | 85 | - Remove all milestones from some repository 86 | 87 | Remove all `sprint.terms` and stay `repos.name` 88 | 89 | ```diff 90 | sprint: 91 | terms: 92 | - - start_on: 2019/04/01 93 | - due_on: 2020/03/31 94 | - - start_on: 2020/04/01 95 | - due_on: 2021/03/31 96 | 97 | repos: 98 | - name: micnncim/github-sprinter 99 | ``` 100 | 101 | - Add repository for the existing milestones 102 | 103 | Remove the repositories where you don't want to change anything and add the repositories you want 104 | 105 | ```diff 106 | repos: 107 | - - name: micnncim/github-sprinter-1 108 | - - name: micnncim/github-sprinter-2 109 | + - name: micnncim/github-sprinter-3 110 | ``` 111 | 112 | 113 | ## LICENSE 114 | 115 | [MIT](./MIT) -------------------------------------------------------------------------------- /cmd/github-sprinter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "golang.org/x/sync/errgroup" 10 | 11 | sprinter "github.com/micnncim/github-sprinter" 12 | ) 13 | 14 | func main() { 15 | var ( 16 | manifest = flag.String("manifest", "sprint.yaml", "manifest yaml file") 17 | dryRun = flag.Bool("dry-run", false, "dry run flag") 18 | update = flag.Bool("update", false, "update flag (destructive change)") 19 | ) 20 | flag.Parse() 21 | 22 | ctx := context.Background() 23 | sprinter, err := sprinter.NewSprinter(ctx, *manifest, *dryRun, *update) 24 | if err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | eg := errgroup.Group{} 29 | for _, repo := range sprinter.Manifest.Repos { 30 | repo := repo 31 | eg.Go(func() error { 32 | return sprinter.ApplyManifest(ctx, repo) 33 | }) 34 | } 35 | 36 | if err := eg.Wait(); err != nil { 37 | fmt.Println(err) 38 | os.Exit(1) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/micnncim/github-sprinter 2 | 3 | require ( 4 | github.com/golang/protobuf v1.2.0 // indirect 5 | github.com/google/go-github v17.0.0+incompatible 6 | github.com/google/go-querystring v1.0.0 // indirect 7 | github.com/pkg/errors v0.8.1 8 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d // indirect 9 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be 10 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 11 | google.golang.org/appengine v1.1.0 // indirect 12 | gopkg.in/yaml.v2 v2.2.2 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 2 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 3 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 4 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 5 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 6 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 7 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 8 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= 10 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 11 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= 12 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 13 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 14 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= 16 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 20 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 21 | -------------------------------------------------------------------------------- /manifest.go: -------------------------------------------------------------------------------- 1 | package github_sprinter 2 | 3 | import ( 4 | "io/ioutil" 5 | "time" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | const ( 11 | timeFormat = "2006/01/02" 12 | ) 13 | 14 | var ( 15 | day = time.Hour * 24 16 | ) 17 | 18 | var daysOfWeek = map[string]time.Weekday{ 19 | "Sunday": time.Sunday, 20 | "Monday": time.Monday, 21 | "Tuesday": time.Tuesday, 22 | "Wednesday": time.Wednesday, 23 | "Thursday": time.Thursday, 24 | "Friday": time.Friday, 25 | "Saturday": time.Saturday, 26 | } 27 | 28 | func loadManifest(path string) (*Manifest, error) { 29 | var m Manifest 30 | buf, err := ioutil.ReadFile(path) 31 | if err != nil { 32 | return nil, err 33 | } 34 | if err := yaml.Unmarshal(buf, &m); err != nil { 35 | return nil, err 36 | } 37 | return &m, nil 38 | } 39 | 40 | type Manifest struct { 41 | Sprint *Sprint `yaml:"sprint"` 42 | Repos []*Repo `yaml:"repos"` 43 | } 44 | 45 | type Sprint struct { 46 | TitleFormat string `yaml:"title_format"` 47 | Duration Duration `yaml:"duration"` 48 | Terms []*Term `yaml:"terms"` 49 | Ignore *Ignore `yaml:"ignore"` 50 | } 51 | 52 | func (s *Sprint) GenerateMilestones() ([]*Milestone, error) { 53 | var milestones []*Milestone 54 | for _, term := range s.Terms { 55 | termStartOn, termDueOn, err := term.Parse() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | d, err := s.Duration.Parse() 61 | if err != nil { 62 | return nil, err 63 | } 64 | startOn, dueOn := termStartOn, termStartOn.Add(d).Add(-day) 65 | for sid := 1; ; sid++ { 66 | startOn, dueOn, err = s.Ignore.OmitIgnored(startOn, dueOn, d) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | if startOn.After(termDueOn) { 72 | break 73 | } 74 | if dueOn.After(termDueOn) { 75 | dueOn = termDueOn 76 | m, err := NewMilestone(sid, s.TitleFormat, startOn, dueOn) 77 | if err != nil { 78 | return nil, err 79 | } 80 | milestones = append(milestones, m) 81 | break 82 | } 83 | 84 | m, err := NewMilestone(sid, s.TitleFormat, startOn, dueOn) 85 | if err != nil { 86 | return nil, err 87 | } 88 | milestones = append(milestones, m) 89 | startOn = dueOn.Add(day) 90 | dueOn = startOn.Add(d).Add(-day) 91 | } 92 | 93 | } 94 | 95 | return milestones, nil 96 | } 97 | 98 | type Duration string 99 | 100 | func (d Duration) Parse() (time.Duration, error) { 101 | return time.ParseDuration(string(d)) 102 | } 103 | 104 | type Term struct { 105 | StartOn string `yaml:"start_on"` 106 | DueOn string `yaml:"due_on"` 107 | } 108 | 109 | func (t *Term) Parse() (startOn, dueOn time.Time, err error) { 110 | startOn, err = time.Parse(timeFormat, t.StartOn) 111 | if err != nil { 112 | return 113 | } 114 | dueOn, err = time.Parse(timeFormat, t.DueOn) 115 | if err != nil { 116 | return 117 | } 118 | return 119 | } 120 | 121 | type Ignore struct { 122 | Terms []*Term `yaml:"terms"` 123 | Weekdays []Weekday `yaml:"edge_weekdays"` 124 | } 125 | 126 | func (i *Ignore) OmitIgnored(startOn, dueOn time.Time, duration time.Duration) (validStartOn, validDueOn time.Time, err error) { 127 | validStartOn, validDueOn = startOn, dueOn 128 | for _, term := range i.Terms { 129 | var iso time.Time 130 | var ido time.Time 131 | iso, ido, err = term.Parse() 132 | if err != nil { 133 | return 134 | } 135 | // startOn in ignored term 136 | if (startOn.After(iso) && startOn.Before(ido)) || startOn.Equal(iso) || startOn.Equal(ido) { 137 | validStartOn = ido.Add(day) 138 | validDueOn = validStartOn.Add(duration).Add(-day) 139 | return 140 | } 141 | // dueOn in ignored term 142 | if (dueOn.After(iso) && dueOn.Before(ido)) || dueOn.Equal(iso) || dueOn.Equal(ido) { 143 | validDueOn = iso.Add(-day) 144 | return 145 | } 146 | // term includes ignored term 147 | if startOn.Before(iso) && dueOn.After(ido) { 148 | validDueOn = iso.Add(-day) 149 | return 150 | } 151 | // ignored term includes term 152 | if startOn.After(iso) && dueOn.Before(ido) { 153 | validStartOn = ido.Add(day) 154 | validDueOn = validStartOn.Add(duration).Add(-day) 155 | return 156 | } 157 | } 158 | 159 | for _, w := range i.Weekdays { 160 | // if ignored weekday, on++ 161 | if validStartOn.Weekday() == w.Parse() { 162 | validStartOn = validStartOn.Add(day) 163 | } 164 | if validDueOn.Weekday() == w.Parse() { 165 | validDueOn = validDueOn.Add(day) 166 | } 167 | } 168 | return 169 | } 170 | 171 | type Weekday string 172 | 173 | func (w Weekday) Parse() time.Weekday { 174 | return daysOfWeek[string(w)] 175 | } 176 | 177 | type Repo struct { 178 | Name string `yaml:"name"` 179 | } 180 | -------------------------------------------------------------------------------- /milestone.go: -------------------------------------------------------------------------------- 1 | package github_sprinter 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | "time" 7 | ) 8 | 9 | type Milestone struct { 10 | Number int 11 | SID int // Sprint ID 12 | Title string 13 | State string 14 | Description string 15 | StartOn string 16 | DueOn string 17 | } 18 | 19 | func NewMilestone(sid int, titleFmt string, startOn, dueOn time.Time) (*Milestone, error) { 20 | tmpl, err := template.New("title").Parse(titleFmt) 21 | if err != nil { 22 | return nil, err 23 | } 24 | m := &Milestone{ 25 | SID: sid, 26 | State: "open", 27 | StartOn: startOn.Format(timeFormat), 28 | DueOn: dueOn.Format(timeFormat), 29 | } 30 | buf := new(bytes.Buffer) 31 | if err := tmpl.Execute(buf, m); err != nil { 32 | return nil, err 33 | } 34 | m.Title = buf.String() 35 | return m, nil 36 | } 37 | 38 | func (m *Milestone) ParseDate() (startOn, dueOn time.Time, err error) { 39 | startOn, err = time.Parse(timeFormat, m.StartOn) 40 | if err != nil { 41 | return 42 | } 43 | dueOn, err = time.Parse(timeFormat, m.DueOn) 44 | if err != nil { 45 | return 46 | } 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micnncim/github-sprinter/d0f8027cc12b14bd25bfcf03bf8f7b8321df288b/screenshot.png -------------------------------------------------------------------------------- /sprint.yaml: -------------------------------------------------------------------------------- 1 | sprint: 2 | title_format: Sprint {{ .SID }} ({{ .StartOn }}-{{ .DueOn }}) 3 | duration: 168h # 1 week (24h * 7d = 168h) 4 | terms: 5 | - start_on: 2019/04/01 6 | due_on: 2019/10/31 7 | ignore: 8 | terms: 9 | - start_on: 2019/08/01 10 | due_on: 2019/08/14 11 | edge_weekdays: 12 | - Saturday 13 | - Sunday 14 | 15 | repos: 16 | - name: micnncim/github-sprinter 17 | -------------------------------------------------------------------------------- /sprinter.go: -------------------------------------------------------------------------------- 1 | package github_sprinter 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | "golang.org/x/oauth2" 12 | "golang.org/x/sync/errgroup" 13 | 14 | "github.com/google/go-github/github" 15 | ) 16 | 17 | type githubClient struct { 18 | *github.Client 19 | 20 | dryRun bool 21 | update bool 22 | 23 | common service 24 | 25 | Milestone *MilestoneService 26 | Issue *IssueService 27 | } 28 | 29 | type MilestoneService service 30 | 31 | type IssueService service 32 | 33 | type service struct { 34 | client *githubClient 35 | } 36 | 37 | type Sprinter struct { 38 | github *githubClient 39 | Manifest *Manifest 40 | } 41 | 42 | type Issue struct { 43 | Title string 44 | URL string 45 | } 46 | 47 | func (s *MilestoneService) Create(ctx context.Context, owner, repo string, milestone *Milestone) error { 48 | fmt.Printf("create %q in %s/%s\n", milestone.Title, owner, repo) 49 | if s.client.dryRun { 50 | return nil 51 | } 52 | _, dueOn, err := milestone.ParseDate() 53 | if err != nil { 54 | return err 55 | } 56 | dueOn = dueOn.Add(day) 57 | ghMilestone := &github.Milestone{ 58 | Title: github.String(milestone.Title), 59 | State: github.String(milestone.State), 60 | Description: github.String(milestone.Description), 61 | DueOn: &dueOn, 62 | } 63 | if _, _, err := s.client.Issues.CreateMilestone(ctx, owner, repo, ghMilestone); err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | func (s *MilestoneService) List(ctx context.Context, owner, repo string) ([]*Milestone, error) { 70 | opt := github.ListOptions{PerPage: 10} 71 | var milestones []*Milestone 72 | 73 | for { 74 | ghMilestones, resp, err := s.client.Issues.ListMilestones( 75 | ctx, 76 | owner, 77 | repo, 78 | &github.MilestoneListOptions{ 79 | ListOptions: opt, 80 | }, 81 | ) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | for _, ghMilestone := range ghMilestones { 87 | description := "" 88 | if ghMilestones != nil { 89 | description = *ghMilestone.Description 90 | } 91 | milestones = append(milestones, &Milestone{ 92 | Number: *ghMilestone.Number, 93 | Title: *ghMilestone.Title, 94 | State: *ghMilestone.State, 95 | Description: description, 96 | DueOn: ghMilestone.DueOn.Format(timeFormat), 97 | }) 98 | } 99 | 100 | if resp.NextPage == 0 { 101 | break 102 | } 103 | opt.Page = resp.NextPage 104 | } 105 | 106 | return milestones, nil 107 | } 108 | 109 | func (s *MilestoneService) Delete(ctx context.Context, owner, repo string, milestone *Milestone) error { 110 | fmt.Printf("delete %q in %s/%s\n", milestone.Title, owner, repo) 111 | if s.client.dryRun { 112 | return nil 113 | } 114 | 115 | if _, err := s.client.Issues.DeleteMilestone(ctx, owner, repo, milestone.Number); err != nil { 116 | return err 117 | } 118 | return nil 119 | } 120 | 121 | func (s *IssueService) ListByMilestone(ctx context.Context, owner, repo string, milestone *Milestone) ([]*Issue, error) { 122 | opt := github.ListOptions{PerPage: 10} 123 | var issues []*Issue 124 | 125 | for { 126 | ghIssues, resp, err := s.client.Issues.ListByRepo(ctx, owner, repo, &github.IssueListByRepoOptions{ 127 | Milestone: strconv.Itoa(milestone.Number), 128 | ListOptions: opt, 129 | }) 130 | if err != nil { 131 | return nil, err 132 | } 133 | for _, ghIssue := range ghIssues { 134 | issues = append(issues, &Issue{ 135 | Title: *ghIssue.Title, 136 | URL: *ghIssue.HTMLURL, 137 | }) 138 | } 139 | 140 | if resp.NextPage == 0 { 141 | break 142 | } 143 | opt.Page = resp.NextPage 144 | } 145 | 146 | return issues, nil 147 | } 148 | 149 | func NewSprinter(ctx context.Context, configPath string, dryRun, update bool) (*Sprinter, error) { 150 | token := os.Getenv("GITHUB_TOKEN") 151 | if token == "" { 152 | return nil, errors.New("GITHUB_TOKEN is missing") 153 | } 154 | 155 | ts := oauth2.StaticTokenSource(&oauth2.Token{ 156 | AccessToken: token, 157 | }) 158 | tc := oauth2.NewClient(ctx, ts) 159 | client := github.NewClient(tc) 160 | 161 | m, err := loadManifest(configPath) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | gc := &githubClient{ 167 | Client: client, 168 | dryRun: dryRun, 169 | update: update, 170 | } 171 | gc.common.client = gc 172 | gc.Milestone = (*MilestoneService)(&gc.common) 173 | gc.Issue = (*IssueService)(&gc.common) 174 | return &Sprinter{ 175 | github: gc, 176 | Manifest: m, 177 | }, nil 178 | } 179 | 180 | func (s *Sprinter) ApplyManifest(ctx context.Context, repository *Repo) error { 181 | slugs := strings.Split(repository.Name, "/") 182 | if len(slugs) != 2 { 183 | return fmt.Errorf("repository name %q is invalid", repository.Name) 184 | } 185 | owner, repo := slugs[0], slugs[1] 186 | 187 | if s.github.update { 188 | // delete all milestones (state="open") 189 | ms, err := s.github.Milestone.List(ctx, owner, repo) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | eg := errgroup.Group{} 195 | for _, m := range ms { 196 | m := m 197 | eg.Go(func() error { 198 | if err := s.github.Milestone.Delete(ctx, owner, repo, m); err != nil { 199 | return err 200 | } 201 | issues, err := s.github.Issue.ListByMilestone(ctx, owner, repo, m) 202 | if err != nil { 203 | return err 204 | } 205 | for _, issue := range issues { 206 | fmt.Printf(" - dislink %q url=%s\n", issue.Title, issue.URL) 207 | } 208 | 209 | return nil 210 | }) 211 | if err := eg.Wait(); err != nil { 212 | return err 213 | } 214 | } 215 | } 216 | 217 | milestones, err := s.Manifest.Sprint.GenerateMilestones() 218 | if err != nil { 219 | return err 220 | } 221 | eg := errgroup.Group{} 222 | for _, m := range milestones { 223 | eg.Go(func() error { 224 | return s.github.Milestone.Create(ctx, owner, repo, m) 225 | }) 226 | if err := eg.Wait(); err != nil { 227 | return err 228 | } 229 | } 230 | 231 | return nil 232 | } 233 | --------------------------------------------------------------------------------