Skip to content

Instantly share code, notes, and snippets.

@iljavs
Created October 3, 2025 21:07
Show Gist options
  • Select an option

  • Save iljavs/17c51132de9ccf8ea10cbfba11ed38c4 to your computer and use it in GitHub Desktop.

Select an option

Save iljavs/17c51132de9ccf8ea10cbfba11ed38c4 to your computer and use it in GitHub Desktop.
Grab all open issues from a github repo and save them to disk locally.
package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/google/go-github/v57/github"
"golang.org/x/oauth2"
)
type config struct {
token string
owner string
repo string
outputDir string
labels []string
}
func main() {
cfg := parseFlags()
if err := os.MkdirAll(cfg.outputDir, 0755); err != nil {
fmt.Printf("Error creating output directory: %v\n", err)
os.Exit(1)
}
client := createGitHubClient(cfg.token)
issues, err := fetchOpenIssues(client, cfg.owner, cfg.repo, cfg.labels)
if err != nil {
fmt.Printf("Error fetching issues: %v\n", err)
os.Exit(1)
}
fmt.Printf("Found %d issues\n", len(issues))
printIssueTitles(issues)
savedCount := saveIssuesToFiles(issues, cfg.outputDir)
fmt.Printf("\nSuccessfully exported %d issues to %s\n",
savedCount, cfg.outputDir)
}
func parseFlags() config {
token := flag.String("token", "", "GitHub Personal Access Token (required)")
owner := flag.String("owner", "", "Repository owner (required)")
repo := flag.String("repo", "", "Repository name (required)")
labels := flag.String("labels", "",
"Comma-separated list of labels to filter issues (optional)")
outputDir := flag.String("output", "issues",
"Output directory for markdown files")
flag.Parse()
if *token == "" || *owner == "" || *repo == "" {
fmt.Println("Error: -token, -owner, and -repo flags are required")
flag.Usage()
os.Exit(1)
}
return config{
token: *token,
owner: *owner,
repo: *repo,
outputDir: *outputDir,
labels: strings.Split(*labels, ","),
}
}
func createGitHubClient(token string) *github.Client {
ctx := context.Background()
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(ctx, ts)
return github.NewClient(tc)
}
func fetchOpenIssues(
client *github.Client,
owner, repo string,
labels []string) ([]*github.Issue, error) {
ctx := context.Background()
opt := &github.IssueListByRepoOptions{
State: "open",
Labels: labels,
ListOptions: github.ListOptions{
PerPage: 100,
},
}
var allIssues []*github.Issue
for {
issues, resp, err := client.Issues.ListByRepo(ctx, owner, repo, opt)
if err != nil {
return nil, err
}
allIssues = append(allIssues, issues...)
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return allIssues, nil
}
func printIssueTitles(issues []*github.Issue) {
fmt.Println("\nOpen Issues:")
fmt.Println("------------")
i := 1
for _, issue := range issues {
if !issue.IsPullRequest() {
fmt.Printf("%d: %s\n", i, *issue.Title)
i++
}
}
fmt.Println()
}
func saveIssuesToFiles(issues []*github.Issue, outputDir string) int {
savedCount := 0
for _, issue := range issues {
if issue.IsPullRequest() {
continue
}
filename := sanitizeFilename(fmt.Sprintf("%s.md", *issue.Title))
filepath := filepath.Join(outputDir, filename)
content := ""
if issue.Body != nil {
content = *issue.Body
}
if err := os.WriteFile(filepath, []byte(content), 0644); err != nil {
fmt.Printf("Error writing issue #%d: %v\n", *issue.Number, err)
continue
}
fmt.Printf("Saved issue #%d: %s\n", *issue.Number, *issue.Title)
savedCount++
}
return savedCount
}
func sanitizeFilename(filename string) string {
sanitized := strings.ReplaceAll(filename, "`", "")
sanitized = strings.ReplaceAll(sanitized, "'", "")
reg := regexp.MustCompile(`[<>:"/\\|?*]`)
sanitized = reg.ReplaceAllString(sanitized, "-")
sanitized = strings.ReplaceAll(sanitized, " ", "_")
sanitized = strings.ReplaceAll(sanitized, "→", "-")
sanitized = strings.ReplaceAll(sanitized, "->", "-")
asciiOnly := strings.Map(
func(r rune) rune {
if r > 127 {
return -1
}
return r
},
sanitized)
if len(asciiOnly) > 200 {
asciiOnly = asciiOnly[:200]
}
if !strings.HasSuffix(asciiOnly, ".md") {
asciiOnly += ".md"
}
return asciiOnly
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment