// Command Hiss is a gitHub ISSue creator utility and how you feel when you are // creating issues. // // Hiss parses a markdown file containing multiple GitHub issue bodies and // creates individual issues. // // # File Format // // - `---` separates issues // - `#` indicates issue title. There can only be a single level 1 header per issue. // - A line starting with `labels:` can be provided to add labels to the issue. // - A line starting with `parent: title of another issue` can be used to reference issues defined earlier in the file. // - The standard /parent can still be used to reference existing issues. // // # Usage // // Create issues from `issues.md` and add the `team/waterboys` + `area/oci` labels to all created issues. // // export GITHUB_TOKEN=${YOUR_ACTUAL_GITHUB_TOKEN} // bazel run hack/tools/hiss --action_env=GITHUB_TOKEN -- -f issues.md --labels team/waterboys,area/oci package main import ( "bufio" "bytes" "context" "flag" "fmt" "os" "strings" "github.com/google/go-github/v47/github" "github.com/peterbourgon/ff/v3" "github.com/peterbourgon/ff/v3/ffcli" "golang.org/x/oauth2" "edge-infra.dev/pkg/lib/build/bazel" "edge-infra.dev/pkg/lib/cli/commands" ) var ( fs = flag.NewFlagSet("hiss", flag.ContinueOnError) token = fs.String("github-token", "", "github personal access token") repo = fs.String("repo", "edge-roadmap", "target repo") owner = fs.String("owner", "ncrvoyix-swt-retail", "target repo owner") moLabels = fs.String("labels", "", "comma separated list of labels to apply to all issues") file = fs.String("f", "", "path to markdown file containing issues to create") ) const ( delimiter = "---" title = "#" labels = "labels:" parent = "parent:" ) func main() { cmd := &ffcli.Command{ Name: "hiss", ShortUsage: "hiss -f issues.md ", ShortHelp: "creates github issues from a markdown file", FlagSet: fs, Options: []ff.Option{ ff.WithEnvVarNoPrefix(), }, Subcommands: []*ffcli.Command{ commands.Version(), }, Exec: hiss, } if err := os.Chdir(bazel.ResolveWdOrDie()); err != nil { fmt.Fprintln(os.Stderr, "failed to set cwd: ", err) os.Exit(1) } if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, "error: ", err) os.Exit(1) } } func hiss(_ context.Context, _ []string) error { ctx := context.Background() httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource( &oauth2.Token{AccessToken: *token}, )) client := github.NewClient(httpClient) issues, err := parseIssues(*file) if err != nil { return err } fmt.Println("going to create:") for _, issue := range issues { fmt.Printf("- %s (%s)\n", issue.title, issue.labels) } parents := map[string]int{} for i, issue := range issues { // compute labels l := issue.labels if *moLabels != "" { l = append(issue.labels, strings.Split(*moLabels, ",")...) } // replace parents for _, parent := range issue.parents { if parents[parent] == 0 { return fmt.Errorf("parent referenced that doesnt exist earlier in the file: '%s'", parent) } issue.body = issue.body + fmt.Sprintf("\n/parent #%d", parents[parent]) } created, _, err := client.Issues.Create(ctx, *owner, *repo, &github.IssueRequest{ Title: &issues[i].title, Body: &issues[i].body, Labels: &l, }) if err != nil { return fmt.Errorf("failed to create %s: %w", issue.title, err) } // store issue number in case this issue is a parent later on parents[issue.title] = *created.Number fmt.Printf("created #%d: %s\n", *created.Number, issue.title) } return nil } func parseIssues(path string) ([]issue, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } issues := []issue{} scanner := bufio.NewScanner(bytes.NewReader(data)) i := issue{labels: []string{}} body := []string{} for scanner.Scan() { text := scanner.Text() switch { case strings.HasPrefix(text, title): if i.title != "" { return nil, fmt.Errorf("encountered title ('#') twice in one issue body") } i.title = strings.TrimSpace(strings.TrimPrefix(text, title)) case strings.HasPrefix(text, labels): if len(i.labels) != 0 { return nil, fmt.Errorf("encountered key '%s' twice in one issue body", labels) } i.labels = strings.Split(strings.TrimSpace(strings.TrimPrefix(text, labels)), " ") case strings.HasPrefix(text, parent): i.parents = append(i.parents, strings.TrimSpace(strings.TrimPrefix(text, parent)), ) case text == delimiter: i.body = strings.TrimSpace(strings.Join(body, "\n")) issues = append(issues, i) // reset state for next issue i = issue{} body = []string{} default: body = append(body, text) } } return issues, nil } type issue struct { title string body string labels []string parents []string }