Basic programmatic access to GitHub Issues
- 4 minutes read - 664 wordsIt’s been a while!
I’ve been spending time writing Bash scripts and a web site but neither has been sufficiently creative that I’ve felt worth a blog post.
As I’ve been finalizing the web site, I needed an Issue Tracker and decided to leverage GitHub(’s Issues).
As a former Googler, I’m familiar with Google’s (excellent) internal issue tracking tool (Buganizer) and it’s public manifestation Issue Tracker. Google documents Issue Tracker and its Issue type which I’ve mercilessly plagiarized in my implementation.
It turns out that the Go third-party library for GitHub is part of Google’s GitHub organization. I think I’ve hand-written my own pieces (structs) for this library and could have saved the time.
I have a basic HTML FORM to provide a way for customers to submit feedback:
<form method="POST" action="/submit">
<input type="text">
<input list="types">
<datalist id="types">
<option value="feature">
<option value="bug">
</datalist>
<input list="components">
<datalist id="components">
<option value="component-1">
<option value="component-2">
</datalist>
<input list="priorities">
<datalist id="priorities">
<option value="high">
<option value="medium">
<option value="low">
</datalist>
<input type="text">
<textarea name="content"></textarea>
<button type="submit">Submit</button>
</form>
NOTE The above is part of a Go template and is stripped of <fieldset>, <label>, <div> etc. for purposes of simplification.
There’s a Go handler (Submit
) mapped to /submit
. Its implementation is essentially:
func (h Handlers) Submit(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
issue := tracker.NewIssue(
assignee,
r.FormValue("reporter"),
r.FormValue("type"),
status,
r.FormValue("component"),
r.FormValue("priority"),
r.FormValue("title"),
r.FormValue("content"),
)
id, err := h.Client.Create(issue)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
w.Write([]byte("Thank you for the feedback\n"))
w.Write([]byte(fmt.Sprintf("Issue created:\n%s: %s", id, issue.Title)))
}
This allows us to create some intermediate (Issue
) format:
type Issue struct {
_Type IssueType
Status IssueStatus
Component ComponentType
Priority PriorityType
Assignee string
Reporter string
Title string
Content string
}
I’ll leave the implementations of the enums to you but, IssueType
is shown below. I’m following Google’s definitions for its Issue type for these.
type IssueStatus uint8
func (x IssueStatus) String() string {
switch x {
case New:
return "new"
case Assigned:
return "assigned"
case Accepted:
return "accepted"
case Fixed:
return "fixed"
case WontFix:
return "won't fix"
case Duplicate:
return "duplicate"
default:
return "undefined"
}
}
const (
New IssueStatus = 0
Assigned IssueStatus = 1
Accepted IssueStatus = 2
Fixed IssueStatus = 3
WontFix IssueStatus = 4
Duplicate IssueStatus = 5
)
var strToIssueStatus = map[string]IssueStatus{
"new": New,
"assigned": Assigned,
"accepted": Accepted,
"fixed": Fixed,
"wont-fix": WontFix,
"duplicate": Duplicate,
}
All that remains is (the simplest part) which is to POST
the Issue (in GitHub’s format) to a GitHub repo:
// Client is a type that represents a feedback client
type Client struct {
client *github.Client
owner string
repo string
}
// NewClient is a function that creates a new Client
func NewClient(owner, repo, token string) Client {
ctx := context.Background()
src := oauth2.StaticTokenSource(
&oauth2.Token{
AccessToken: token,
},
)
httpClient := oauth2.NewClient(ctx, src)
client := github.NewClient(httpClient)
return Client{
client: client,
owner: owner,
repo: repo,
}
}
// Create is a method that creates a new Issue using Client
func (c *Client) Create(issue Issue) (string, error) {
githubIssueRequest := issue.ToGitHubIssueRequest()
// Should Client hold Context?
githubIssue, _, err := c.client.Issues.Create(
context.Background(),
c.owner,
c.repo,
githubIssueRequest,
)
if err != nil {
return "", err
}
id := strconv.FormatInt(*githubIssue.ID, 10)
return id, nil
}
And some GitHub-specific methods on the Issue
type:
// GitHubType is a method that
// It maps _Type.FeatureRequest to GitHub enhancement
// GitHub enhancement is one of the default GitHub issue labels
func (i Issue) GitHubType() string {
switch i._Type {
case FeatureRequest:
return "enhancement"
default:
return i._Type.String()
}
}
// GitHubIssueRequest is a method
// It converts an Issue to a GitHub IssueRequest
func (i Issue) GitHubIssueRequest() *github.IssueRequest {
_type := i.GitHubType()
labels := &[]string{
_type,
i.Component.String(),
i.Priority.String(),
}
resp := &github.IssueRequest{
Title: &i.Title,
Body: &i.Content,
Labels: labels,
Assignee: &i.Assignee,
}
return resp
}
NOTE One omission with the current implementation is in ensuring that GitHub Issues are created on behalf of the requestor.