1313// limitations under the License.
1414//////////////////////////////////////////////////////////////////////////////
1515
16- // A client interface wrapping the Github API for creating, listing, and closing
17- // issues on a single repository.
16+ // Package issues defines a client interface wrapping the Github API for
17+ // creating, listing, and closing issues on a single repository.
1818package issues
1919
2020import (
21+ "fmt"
2122 "log"
23+ "net/url"
24+ "strings"
25+ "time"
2226
2327 "github.com/google/go-github/github"
2428 "github.com/kr/pretty"
@@ -30,40 +34,44 @@ import (
3034type Client struct {
3135 // githubClient is an authenticated client for accessing the github API.
3236 GithubClient * github.Client
33- // owner is the github project (e.g. github.com/<owner>/<repo>).
34- owner string
35- // repo is the github repository under the above owner.
36- repo string
37+ // org is the github user or organization name (e.g. github.com/<org>/<repo>).
38+ org string
3739}
3840
3941// NewClient creates an Client authenticated using the Github authToken.
40- // Future operations are only performed on the given github "owner /repo".
41- func NewClient (owner , repo , authToken string ) * Client {
42+ // Future operations are only performed on the given github "org /repo".
43+ func NewClient (org , authToken string ) * Client {
4244 ctx := context .Background ()
4345 tokenSource := oauth2 .StaticTokenSource (
4446 & oauth2.Token {AccessToken : authToken },
4547 )
4648 client := & Client {
4749 GithubClient : github .NewClient (oauth2 .NewClient (ctx , tokenSource )),
48- owner : owner ,
49- repo : repo ,
50+ org : org ,
5051 }
5152 return client
5253}
5354
54- // CreateIssue creates a new Github issue. New issues are unassigned.
55- func (c * Client ) CreateIssue (title , body string ) (* github.Issue , error ) {
55+ // CreateIssue creates a new Github issue. New issues are unassigned. Issues are
56+ // labeled with with an alert named "alert:boom:". Labels are created automatically
57+ // if they do not already exist in a repo.
58+ func (c * Client ) CreateIssue (repo , title , body string ) (* github.Issue , error ) {
5659 // Construct a minimal github issue request.
5760 issueReq := github.IssueRequest {
58- Title : & title ,
59- Body : & body ,
61+ Title : & title ,
62+ Body : & body ,
63+ Labels : & ([]string {"alert:boom:" }), // Search using: label:"alert:boom:"
6064 }
6165
66+ // Enforce a timeout on the issue creation.
67+ ctx , cancel := context .WithTimeout (context .Background (), 15 * time .Second )
68+ defer cancel ()
69+
6270 // Create the issue.
6371 // See also: https://developer.github.com/v3/issues/#create-an-issue
6472 // See also: https://godoc.org/github.com/google/go-github/github#IssuesService.Create
6573 issue , resp , err := c .GithubClient .Issues .Create (
66- context . Background () , c .owner , c . repo , & issueReq )
74+ ctx , c .org , repo , & issueReq )
6775 if err != nil {
6876 log .Printf ("Error in CreateIssue: response: %v\n %s" ,
6977 err , pretty .Sprint (resp ))
@@ -72,30 +80,42 @@ func (c *Client) CreateIssue(title, body string) (*github.Issue, error) {
7280 return issue , nil
7381}
7482
75- // ListOpenIssues returns open issues from github Github issues are either
76- // "open" or "closed". Closed issues have either been resolved automatically or
77- // by a person. So, there will be an ever increasing number of "closed" issues .
78- // By only listing "open" issues we limit the number of issues returned.
83+ // ListOpenIssues returns open issues created by past alerts within the
84+ // client organization. Because ListOpenIssues uses the Github Search API,
85+ // the *github.Issue instances returned will contain partial information .
86+ // See also: https://developer.github.com/v3/search/#search-issues
7987func (c * Client ) ListOpenIssues () ([]* github.Issue , error ) {
8088 var allIssues []* github.Issue
8189
82- opts := & github.IssueListByRepoOptions { State : "open" }
90+ sopts := & github.SearchOptions { }
8391 for {
84- issues , resp , err := c .GithubClient .Issues .ListByRepo (
85- context .Background (), c .owner , c .repo , opts )
92+ // Enforce a timeout on the issue listing.
93+ ctx , cancel := context .WithTimeout (context .Background (), 15 * time .Second )
94+ defer cancel ()
95+
96+ // Github issues are either "open" or "closed". Closed issues have either been
97+ // resolved automatically or by a person. So, there will be an ever increasing
98+ // number of "closed" issues. By only listing "open" issues we limit the
99+ // number of issues returned.
100+ //
101+ // The search depends on all relevant issues including the "alert:boom:" label.
102+ issues , resp , err := c .GithubClient .Search .Issues (
103+ ctx , `is:issue in:title is:open org:` + c .org + ` label:"alert:boom:"` , sopts )
86104 if err != nil {
87- log .Printf ("Failed to list open github issues: %v\n %s" ,
88- err , pretty .Sprint (resp ))
105+ log .Printf ("Failed to list open github issues: %v\n " , err )
89106 return nil , err
90107 }
91108 // Collect 'em all.
92- allIssues = append (allIssues , issues ... )
109+ for i := range issues .Issues {
110+ log .Println ("ListOpenIssues:" , issues .Issues [i ].GetTitle ())
111+ allIssues = append (allIssues , & issues .Issues [i ])
112+ }
93113
94114 // Continue loading the next page until all issues are received.
95115 if resp .NextPage == 0 {
96116 break
97117 }
98- opts .ListOptions .Page = resp .NextPage
118+ sopts .ListOptions .Page = resp .NextPage
99119 }
100120 return allIssues , nil
101121}
@@ -107,14 +127,42 @@ func (c *Client) CloseIssue(issue *github.Issue) (*github.Issue, error) {
107127 State : github .String ("closed" ),
108128 }
109129
130+ org , repo , err := getOrgAndRepoFromIssue (issue )
131+ if err != nil {
132+ return nil , err
133+ }
134+ // Enforce a timeout on the issue edit.
135+ ctx , cancel := context .WithTimeout (context .Background (), 15 * time .Second )
136+ defer cancel ()
137+
110138 // Edits the issue to have "closed" state.
111139 // See also: https://developer.github.com/v3/issues/#edit-an-issue
112140 // See also: https://godoc.org/github.com/google/go-github/github#IssuesService.Edit
113- closedIssue , resp , err := c .GithubClient .Issues .Edit (
114- context . Background (), c . owner , c . repo , * issue .Number , & issueReq )
141+ closedIssue , _ , err := c .GithubClient .Issues .Edit (
142+ ctx , org , repo , * issue .Number , & issueReq )
115143 if err != nil {
116- log .Printf ("Failed to close issue: %v\n %s " , err , pretty . Sprint ( resp ) )
144+ log .Printf ("Failed to close issue: %v" , err )
117145 return nil , err
118146 }
119147 return closedIssue , nil
120148}
149+
150+ // getOrgAndRepoFromIssue reads the issue RepositoryURL and extracts the
151+ // owner and repo names. Issues returned by the Search API contain partial
152+ // records.
153+ func getOrgAndRepoFromIssue (issue * github.Issue ) (string , string , error ) {
154+ repoURL := issue .GetRepositoryURL ()
155+ if repoURL == "" {
156+ return "" , "" , fmt .Errorf ("Issue has invalid RepositoryURL value" )
157+ }
158+ u , err := url .Parse (repoURL )
159+ if err != nil {
160+ return "" , "" , err
161+ }
162+ fields := strings .Split (u .Path , "/" )
163+ if len (fields ) != 4 {
164+ return "" , "" , fmt .Errorf ("Issue has invalid RepositoryURL value" )
165+ }
166+ return fields [2 ], fields [3 ], nil
167+
168+ }
0 commit comments