-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
381 additions
and
340 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package main | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"log" | ||
"os" | ||
"strings" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/google/go-github/github" | ||
"golang.org/x/oauth2" | ||
"golang.org/x/sync/errgroup" | ||
yaml "gopkg.in/yaml.v2" | ||
) | ||
|
||
func (c *CLI) Run(args []string) error { | ||
token := os.Getenv("GITHUB_TOKEN") | ||
if token == "" { | ||
return errors.New("GITHUB_TOKEN is missing") | ||
} | ||
|
||
ts := oauth2.StaticTokenSource(&oauth2.Token{ | ||
AccessToken: token, | ||
}) | ||
tc := oauth2.NewClient(oauth2.NoContext, ts) | ||
client := github.NewClient(tc) | ||
|
||
m, err := loadManifest(c.Option.Config) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
gc := &githubClient{ | ||
github: client, | ||
dryRun: c.Option.DryRun, | ||
logger: log.New(os.Stdout, "labeler: ", log.Ldate|log.Ltime), | ||
} | ||
|
||
if c.Option.DryRun { | ||
gc.logger.SetPrefix("labeler (dry-run): ") | ||
} | ||
|
||
gc.common.client = gc | ||
gc.Label = (*LabelService)(&gc.common) | ||
|
||
c.Client = gc | ||
c.Config = m | ||
|
||
if len(c.Config.Repos) == 0 { | ||
return fmt.Errorf("no repos found in %s", c.Option.Config) | ||
} | ||
|
||
if c.Option.Import { | ||
m := c.CurrentLabels() | ||
f, err := os.Create(c.Option.Config) | ||
if err != nil { | ||
return err | ||
} | ||
defer f.Close() | ||
return yaml.NewEncoder(f).Encode(&m) | ||
} | ||
|
||
if cmp.Equal(c.CurrentLabels(), c.Config) { | ||
// no need to sync | ||
return nil | ||
} | ||
|
||
eg := errgroup.Group{} | ||
for _, repo := range c.Config.Repos { | ||
repo := repo | ||
eg.Go(func() error { | ||
return c.Sync(repo) | ||
}) | ||
} | ||
|
||
return eg.Wait() | ||
} | ||
|
||
// applyLabels creates/edits labels described in YAML | ||
func (c *CLI) applyLabels(owner, repo string, label Label) error { | ||
ghLabel, err := c.Client.Label.Get(owner, repo, label) | ||
if err != nil { | ||
return c.Client.Label.Create(owner, repo, label) | ||
} | ||
|
||
if ghLabel.Description != label.Description || ghLabel.Color != label.Color { | ||
return c.Client.Label.Edit(owner, repo, label) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// deleteLabels deletes the label not described in YAML but exists on GitHub | ||
func (c *CLI) deleteLabels(owner, repo string) error { | ||
labels, err := c.Client.Label.List(owner, repo) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for _, label := range labels { | ||
if c.Config.checkIfRepoHasLabel(owner+"/"+repo, label.Name) { | ||
// no need to delete | ||
continue | ||
} | ||
err := c.Client.Label.Delete(owner, repo, label) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Sync syncs labels based on YAML | ||
func (c *CLI) Sync(repo Repo) error { | ||
slugs := strings.Split(repo.Name, "/") | ||
if len(slugs) != 2 { | ||
return fmt.Errorf("repository name %q is invalid", repo.Name) | ||
} | ||
for _, labelName := range repo.Labels { | ||
label, err := c.Config.getDefinedLabel(labelName) | ||
if err != nil { | ||
return err | ||
} | ||
err = c.applyLabels(slugs[0], slugs[1], label) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
return c.deleteLabels(slugs[0], slugs[1]) | ||
} | ||
|
||
func (c *CLI) CurrentLabels() Manifest { | ||
var m Manifest | ||
for _, repo := range c.Config.Repos { | ||
e := strings.Split(repo.Name, "/") | ||
if len(e) != 2 { | ||
// TODO: handle error | ||
continue | ||
} | ||
labels, err := c.Client.Label.List(e[0], e[1]) | ||
if err != nil { | ||
// TODO: handle error | ||
continue | ||
} | ||
var ls []string | ||
for _, label := range labels { | ||
ls = append(ls, label.Name) | ||
} | ||
repo.Labels = ls | ||
m.Repos = append(m.Repos, repo) | ||
m.Labels = append(m.Labels, labels...) | ||
} | ||
return m | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
|
||
yaml "gopkg.in/yaml.v2" | ||
) | ||
|
||
func loadManifest(path string) (Manifest, error) { | ||
var m Manifest | ||
buf, err := ioutil.ReadFile(path) | ||
if err != nil { | ||
return m, err | ||
} | ||
err = yaml.Unmarshal(buf, &m) | ||
return m, err | ||
} | ||
|
||
func (m Manifest) getDefinedLabel(name string) (Label, error) { | ||
for _, label := range m.Labels { | ||
if label.Name == name { | ||
return label, nil | ||
} | ||
} | ||
return Label{}, fmt.Errorf("%s: no such defined label in manifest YAML", name) | ||
} | ||
|
||
func (m Manifest) checkIfRepoHasLabel(repoName, labelName string) bool { | ||
var labels []string | ||
for _, repo := range m.Repos { | ||
if repo.Name == repoName { | ||
labels = repo.Labels | ||
break | ||
} | ||
} | ||
for _, label := range labels { | ||
if label == labelName { | ||
return true | ||
} | ||
} | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"log" | ||
|
||
"github.com/google/go-github/github" | ||
) | ||
|
||
// Manifest represents the YAML file described about labels and repos | ||
type Manifest struct { | ||
Labels Labels `yaml:"labels"` | ||
Repos Repos `yaml:"repos"` | ||
} | ||
|
||
// Label represents GitHub label | ||
type Label struct { | ||
Name string `yaml:"name"` | ||
Description string `yaml:"description"` | ||
Color string `yaml:"color"` | ||
PreviousName string `yaml:"previous_name,omitempty"` | ||
} | ||
|
||
// Labels represents a collection of Label | ||
type Labels []Label | ||
|
||
// Repo represents GitHub repository | ||
type Repo struct { | ||
Name string `yaml:"name"` | ||
Labels []string `yaml:"labels"` | ||
} | ||
|
||
// Repos represents a collection of Repo | ||
type Repos []Repo | ||
|
||
type CLI struct { | ||
Stdout io.Writer | ||
Stderr io.Writer | ||
Option Option | ||
|
||
Client *githubClient | ||
Config Manifest | ||
} | ||
|
||
type Option struct { | ||
DryRun bool `long:"dry-run" description:"Just dry run"` | ||
Config string `short:"c" long:"config" description:"Path to YAML file that labels are defined" default:"labels.yaml"` | ||
Import bool `long:"import" description:"Import existing labels if enabled"` | ||
Version bool `long:"version" description:"Show version"` | ||
} | ||
|
||
type githubClient struct { | ||
// github *github.Client | ||
github *github.Client | ||
|
||
dryRun bool | ||
logger *log.Logger | ||
|
||
common service | ||
|
||
Label *LabelService | ||
} | ||
|
||
type Labeler interface { | ||
// Get(owner, repo string, label Label) (Label, error) | ||
// Create(owner, repo string, label Label) error | ||
// Edit(owner, repo string, label Label) error | ||
// List(owner, repo string) ([]Label, error) | ||
// Delete(owner, repo string, label Label) error | ||
|
||
GetLabel(ctx context.Context, owner string, repo string, name string) (*github.Label, *github.Response, error) | ||
EditLabel(ctx context.Context, owner string, repo string, name string, label *github.Label) (*github.Label, *github.Response, error) | ||
CreateLabel(ctx context.Context, owner string, repo string, label *github.Label) (*github.Label, *github.Response, error) | ||
ListLabels(ctx context.Context, owner string, repo string, opt *github.ListOptions) ([]*github.Label, *github.Response, error) | ||
DeleteLabel(ctx context.Context, owner string, repo string, name string) (*github.Response, error) | ||
} | ||
|
||
// LabelService handles communication with the label related | ||
// methods of GitHub API | ||
type LabelService service | ||
|
||
type service struct { | ||
client *githubClient | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/google/go-github/github" | ||
) | ||
|
||
// Get gets GitHub labels | ||
func (g *LabelService) Get(owner, repo string, label Label) (Label, error) { | ||
ctx := context.Background() | ||
ghLabel, _, err := g.client.github.Issues.GetLabel(ctx, owner, repo, label.Name) | ||
if err != nil { | ||
return Label{}, err | ||
} | ||
return Label{ | ||
Name: ghLabel.GetName(), | ||
Description: ghLabel.GetDescription(), | ||
Color: ghLabel.GetName(), | ||
}, nil | ||
} | ||
|
||
// Create creates GitHub labels | ||
func (g *LabelService) Create(owner, repo string, label Label) error { | ||
ctx := context.Background() | ||
ghLabel := &github.Label{ | ||
Name: github.String(label.Name), | ||
Description: github.String(label.Description), | ||
Color: github.String(label.Color), | ||
} | ||
if len(label.PreviousName) > 0 { | ||
g.client.logger.Printf("rename %q in %s/%s to %q", label.PreviousName, owner, repo, label.Name) | ||
if g.client.dryRun { | ||
return nil | ||
} | ||
_, _, err := g.client.github.Issues.EditLabel(ctx, owner, repo, label.PreviousName, ghLabel) | ||
return err | ||
} | ||
g.client.logger.Printf("create %q in %s/%s", label.Name, owner, repo) | ||
if g.client.dryRun { | ||
return nil | ||
} | ||
_, _, err := g.client.github.Issues.CreateLabel(ctx, owner, repo, ghLabel) | ||
return err | ||
} | ||
|
||
// Edit edits GitHub labels | ||
func (g *LabelService) Edit(owner, repo string, label Label) error { | ||
ctx := context.Background() | ||
ghLabel := &github.Label{ | ||
Name: github.String(label.Name), | ||
Description: github.String(label.Description), | ||
Color: github.String(label.Color), | ||
} | ||
g.client.logger.Printf("edit %q in %s/%s", label.Name, owner, repo) | ||
if g.client.dryRun { | ||
return nil | ||
} | ||
_, _, err := g.client.github.Issues.EditLabel(ctx, owner, repo, label.Name, ghLabel) | ||
return err | ||
} | ||
|
||
// List lists GitHub labels | ||
func (g *LabelService) List(owner, repo string) ([]Label, error) { | ||
ctx := context.Background() | ||
opt := &github.ListOptions{PerPage: 10} | ||
var labels []Label | ||
for { | ||
ghLabels, resp, err := g.client.github.Issues.ListLabels(ctx, owner, repo, opt) | ||
if err != nil { | ||
return labels, err | ||
} | ||
for _, ghLabel := range ghLabels { | ||
labels = append(labels, Label{ | ||
Name: ghLabel.GetName(), | ||
Description: ghLabel.GetDescription(), | ||
Color: ghLabel.GetColor(), | ||
}) | ||
} | ||
if resp.NextPage == 0 { | ||
break | ||
} | ||
opt.Page = resp.NextPage | ||
} | ||
return labels, nil | ||
} | ||
|
||
// Delete deletes GitHub labels | ||
func (g *LabelService) Delete(owner, repo string, label Label) error { | ||
ctx := context.Background() | ||
g.client.logger.Printf("delete %q in %s/%s", label.Name, owner, repo) | ||
if g.client.dryRun { | ||
return nil | ||
} | ||
_, err := g.client.github.Issues.DeleteLabel(ctx, owner, repo, label.Name) | ||
return err | ||
} |
Oops, something went wrong.