1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package main
22
23 import (
24 "bufio"
25 "bytes"
26 "context"
27 "flag"
28 "fmt"
29 "os"
30 "strings"
31
32 "github.com/google/go-github/v47/github"
33 "github.com/peterbourgon/ff/v3"
34 "github.com/peterbourgon/ff/v3/ffcli"
35 "golang.org/x/oauth2"
36
37 "edge-infra.dev/pkg/lib/build/bazel"
38 "edge-infra.dev/pkg/lib/cli/commands"
39 )
40
41 var (
42 fs = flag.NewFlagSet("hiss", flag.ContinueOnError)
43
44 token = fs.String("github-token", "", "github personal access token")
45 repo = fs.String("repo", "edge-roadmap", "target repo")
46 owner = fs.String("owner", "ncrvoyix-swt-retail", "target repo owner")
47 moLabels = fs.String("labels", "", "comma separated list of labels to apply to all issues")
48 file = fs.String("f", "", "path to markdown file containing issues to create")
49 )
50
51 const (
52 delimiter = "---"
53 title = "#"
54 labels = "labels:"
55 parent = "parent:"
56 )
57
58 func main() {
59 cmd := &ffcli.Command{
60 Name: "hiss",
61 ShortUsage: "hiss -f issues.md <args>",
62 ShortHelp: "creates github issues from a markdown file",
63 FlagSet: fs,
64 Options: []ff.Option{
65 ff.WithEnvVarNoPrefix(),
66 },
67 Subcommands: []*ffcli.Command{
68 commands.Version(),
69 },
70 Exec: hiss,
71 }
72
73 if err := os.Chdir(bazel.ResolveWdOrDie()); err != nil {
74 fmt.Fprintln(os.Stderr, "failed to set cwd: ", err)
75 os.Exit(1)
76 }
77
78 if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
79 fmt.Fprintln(os.Stderr, "error: ", err)
80 os.Exit(1)
81 }
82 }
83
84 func hiss(_ context.Context, _ []string) error {
85 ctx := context.Background()
86 httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(
87 &oauth2.Token{AccessToken: *token},
88 ))
89 client := github.NewClient(httpClient)
90
91 issues, err := parseIssues(*file)
92 if err != nil {
93 return err
94 }
95
96 fmt.Println("going to create:")
97 for _, issue := range issues {
98 fmt.Printf("- %s (%s)\n", issue.title, issue.labels)
99 }
100
101 parents := map[string]int{}
102 for i, issue := range issues {
103
104 l := issue.labels
105 if *moLabels != "" {
106 l = append(issue.labels, strings.Split(*moLabels, ",")...)
107 }
108
109 for _, parent := range issue.parents {
110 if parents[parent] == 0 {
111 return fmt.Errorf("parent referenced that doesnt exist earlier in the file: '%s'", parent)
112 }
113 issue.body = issue.body + fmt.Sprintf("\n/parent #%d", parents[parent])
114 }
115
116 created, _, err := client.Issues.Create(ctx, *owner, *repo, &github.IssueRequest{
117 Title: &issues[i].title,
118 Body: &issues[i].body,
119 Labels: &l,
120 })
121 if err != nil {
122 return fmt.Errorf("failed to create %s: %w", issue.title, err)
123 }
124
125
126 parents[issue.title] = *created.Number
127 fmt.Printf("created #%d: %s\n", *created.Number, issue.title)
128 }
129
130 return nil
131 }
132
133 func parseIssues(path string) ([]issue, error) {
134 data, err := os.ReadFile(path)
135 if err != nil {
136 return nil, err
137 }
138
139 issues := []issue{}
140
141 scanner := bufio.NewScanner(bytes.NewReader(data))
142 i := issue{labels: []string{}}
143 body := []string{}
144 for scanner.Scan() {
145 text := scanner.Text()
146 switch {
147 case strings.HasPrefix(text, title):
148 if i.title != "" {
149 return nil, fmt.Errorf("encountered title ('#') twice in one issue body")
150 }
151 i.title = strings.TrimSpace(strings.TrimPrefix(text, title))
152 case strings.HasPrefix(text, labels):
153 if len(i.labels) != 0 {
154 return nil, fmt.Errorf("encountered key '%s' twice in one issue body", labels)
155 }
156 i.labels = strings.Split(strings.TrimSpace(strings.TrimPrefix(text, labels)), " ")
157 case strings.HasPrefix(text, parent):
158 i.parents = append(i.parents,
159 strings.TrimSpace(strings.TrimPrefix(text, parent)),
160 )
161 case text == delimiter:
162 i.body = strings.TrimSpace(strings.Join(body, "\n"))
163 issues = append(issues, i)
164
165 i = issue{}
166 body = []string{}
167 default:
168 body = append(body, text)
169 }
170 }
171
172 return issues, nil
173 }
174
175 type issue struct {
176 title string
177 body string
178 labels []string
179 parents []string
180 }
181
View as plain text