Skip to content

Instantly share code, notes, and snippets.

@houstonhaynes
Last active February 14, 2025 13:52
Show Gist options
  • Save houstonhaynes/384b035490361508f8a2646640aa7f1b to your computer and use it in GitHub Desktop.
Save houstonhaynes/384b035490361508f8a2646640aa7f1b to your computer and use it in GitHub Desktop.
An F# script to extract open GitHub issues from a repo as Markdown
#r "nuget: Octokit"
open Octokit
open System
open System.IO
open System.Text.RegularExpressions
// Configuration
type Config = {
Token: string
RepoUrl: string
OutputDir: string
}
// URL parsing
let parseGitHubUrl (url: string) =
let pattern = @"github\.com/([^/]+)/([^/]+)"
let m = Regex.Match(url, pattern)
if m.Success then
Some(m.Groups.[1].Value, m.Groups.[2].Value)
else
None
// Helper functions to format markdown
let formatUser (user: User) =
sprintf "[@%s](https://github.com/%s)" user.Login user.Login
let formatDateTime (dt: DateTimeOffset) =
dt.ToString("yyyy-MM-dd HH:mm:ss UTC")
let formatLabels (labels: Label seq) =
labels
|> Seq.map (fun label -> sprintf "`%s`" label.Name)
|> String.concat " "
let formatBody (body: string) =
if String.IsNullOrEmpty(body) then ""
else sprintf "\n\n%s" body
// Format a single issue as markdown
let formatIssue (issue: Issue) (comments: IssueComment seq) =
let header = sprintf "# %s\n\n" issue.Title
let metadata =
sprintf "**Issue #%d** | Created by %s | %s | %s\n\n"
issue.Number
(formatUser issue.User)
(formatDateTime issue.CreatedAt)
(formatLabels issue.Labels)
let issueBody = formatBody issue.Body
let comments =
comments
|> Seq.map (fun comment ->
sprintf "\n\n---\n\n**Comment by %s** | %s%s"
(formatUser comment.User)
(formatDateTime comment.CreatedAt)
(formatBody comment.Body))
|> String.concat ""
let state =
sprintf "\n\n---\n\n**Status**: %s"
(if issue.State.Value = ItemState.Closed then
sprintf "Closed on %s" (formatDateTime issue.ClosedAt.Value)
else "Open")
header + metadata + issueBody + comments + state
// Main function to extract issues
let extractIssues (config: Config) = async {
match parseGitHubUrl config.RepoUrl with
| None ->
printfn "Invalid GitHub URL. Please provide a URL in the format: https://github.com/owner/repo"
return Error "Invalid URL"
| Some (owner, repo) ->
try
// Initialize GitHub client
let client = new GitHubClient(new ProductHeaderValue("IssueExtractor"))
client.Credentials <- Credentials(config.Token)
// Get open issues
let! issues =
client.Issue.GetAllForRepository(
owner,
repo,
new RepositoryIssueRequest(State = ItemStateFilter.Open))
|> Async.AwaitTask
// Create output directory if it doesn't exist
Directory.CreateDirectory(config.OutputDir) |> ignore
// Process each issue
for issue in issues do
// Get comments for the issue
let! comments =
client.Issue.Comment.GetAllForIssue(
owner,
repo,
issue.Number)
|> Async.AwaitTask
// Format issue and comments as markdown
let markdown = formatIssue issue comments
// Save to file
let filename =
Path.Combine(
config.OutputDir,
sprintf "issue-%d.md" issue.Number)
File.WriteAllText(filename, markdown)
printfn "Processed issue #%d" issue.Number
// Add small delay to avoid rate limiting
do! Async.Sleep 1000
return Ok()
with ex ->
printfn "Error: %s" ex.Message
return Error ex.Message
}
// Example usage
let config = {
Token = "[YOUR_GITHUB_TOKEN_HERE]" // Replace with your GitHub token
RepoUrl = "https://github.com/account/project" // Just paste the full GitHub repo URL here
OutputDir = "issues"
}
// Run the extractor
match Async.RunSynchronously (extractIssues config) with
| Ok () -> printfn "Successfully extracted all open issues"
| Error msg -> printfn "Failed to extract issues: %s" msg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment