Skip to content

Commit 752e454

Browse files
committed
feat: Add Issue Dependencies API support
Add four new methods to IssuesService for the Issue Dependencies REST API (apiVersion 2026-03-10): - ListBlockedBy: list dependencies blocking an issue - AddBlockedBy: add a blocking dependency to an issue - RemoveBlockedBy: remove a blocking dependency - ListBlocking: list issues that an issue is blocking Includes IssueDependencyRequest type, full test coverage with testBadOptions, testNewRequestAndDoFailure, testURLParseError, and testJSONMarshal helpers.
1 parent f293a76 commit 752e454

2 files changed

Lines changed: 330 additions & 0 deletions

File tree

github/issues_dependencies.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2026 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
package github
7+
8+
import (
9+
"context"
10+
"fmt"
11+
)
12+
13+
// IssueDependencyRequest represents a request to add a dependency to an issue.
14+
type IssueDependencyRequest struct {
15+
IssueID *int64 `json:"issue_id,omitempty"`
16+
}
17+
18+
// ListBlockedBy lists the dependencies that block the specified issue.
19+
//
20+
// GitHub API docs: https://docs.github.com/rest/issues/issue-dependencies#list-dependencies-an-issue-is-blocked-by
21+
//
22+
//meta:operation GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by
23+
func (s *IssuesService) ListBlockedBy(ctx context.Context, owner, repo string, number int, opts *ListOptions) ([]*Issue, *Response, error) {
24+
u := fmt.Sprintf("repos/%v/%v/issues/%v/dependencies/blocked_by", owner, repo, number)
25+
u, err := addOptions(u, opts)
26+
if err != nil {
27+
return nil, nil, err
28+
}
29+
30+
req, err := s.client.NewRequest("GET", u, nil)
31+
if err != nil {
32+
return nil, nil, err
33+
}
34+
35+
var issues []*Issue
36+
resp, err := s.client.Do(ctx, req, &issues)
37+
if err != nil {
38+
return nil, resp, err
39+
}
40+
41+
return issues, resp, nil
42+
}
43+
44+
// AddBlockedBy adds a "blocked by" dependency to the specified issue.
45+
//
46+
// GitHub API docs: https://docs.github.com/rest/issues/issue-dependencies#add-a-dependency-an-issue-is-blocked-by
47+
//
48+
//meta:operation POST /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by
49+
func (s *IssuesService) AddBlockedBy(ctx context.Context, owner, repo string, number int, issueDepReq *IssueDependencyRequest) (*Issue, *Response, error) {
50+
u := fmt.Sprintf("repos/%v/%v/issues/%v/dependencies/blocked_by", owner, repo, number)
51+
req, err := s.client.NewRequest("POST", u, issueDepReq)
52+
if err != nil {
53+
return nil, nil, err
54+
}
55+
56+
var issue *Issue
57+
resp, err := s.client.Do(ctx, req, &issue)
58+
if err != nil {
59+
return nil, resp, err
60+
}
61+
62+
return issue, resp, nil
63+
}
64+
65+
// RemoveBlockedBy removes a "blocked by" dependency from the specified issue.
66+
//
67+
// GitHub API docs: https://docs.github.com/rest/issues/issue-dependencies#remove-dependency-an-issue-is-blocked-by
68+
//
69+
//meta:operation DELETE /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by/{issue_id}
70+
func (s *IssuesService) RemoveBlockedBy(ctx context.Context, owner, repo string, number int, issueID int64) (*Issue, *Response, error) {
71+
u := fmt.Sprintf("repos/%v/%v/issues/%v/dependencies/blocked_by/%v", owner, repo, number, issueID)
72+
req, err := s.client.NewRequest("DELETE", u, nil)
73+
if err != nil {
74+
return nil, nil, err
75+
}
76+
77+
var issue *Issue
78+
resp, err := s.client.Do(ctx, req, &issue)
79+
if err != nil {
80+
return nil, resp, err
81+
}
82+
83+
return issue, resp, nil
84+
}
85+
86+
// ListBlocking lists the issues that the specified issue is blocking.
87+
//
88+
// GitHub API docs: https://docs.github.com/rest/issues/issue-dependencies#list-dependencies-an-issue-is-blocking
89+
//
90+
//meta:operation GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocking
91+
func (s *IssuesService) ListBlocking(ctx context.Context, owner, repo string, number int, opts *ListOptions) ([]*Issue, *Response, error) {
92+
u := fmt.Sprintf("repos/%v/%v/issues/%v/dependencies/blocking", owner, repo, number)
93+
u, err := addOptions(u, opts)
94+
if err != nil {
95+
return nil, nil, err
96+
}
97+
98+
req, err := s.client.NewRequest("GET", u, nil)
99+
if err != nil {
100+
return nil, nil, err
101+
}
102+
103+
var issues []*Issue
104+
resp, err := s.client.Do(ctx, req, &issues)
105+
if err != nil {
106+
return nil, resp, err
107+
}
108+
109+
return issues, resp, nil
110+
}

github/issues_dependencies_test.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright 2026 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
package github
7+
8+
import (
9+
"encoding/json"
10+
"fmt"
11+
"net/http"
12+
"testing"
13+
14+
"github.com/google/go-cmp/cmp"
15+
)
16+
17+
func TestIssuesService_ListBlockedBy(t *testing.T) {
18+
t.Parallel()
19+
client, mux, _ := setup(t)
20+
21+
mux.HandleFunc("/repos/o/r/issues/1/dependencies/blocked_by", func(w http.ResponseWriter, r *http.Request) {
22+
testMethod(t, r, "GET")
23+
testFormValues(t, r, values{"page": "2"})
24+
fmt.Fprint(w, `[{"number":1347,"title":"Found a bug"}]`)
25+
})
26+
27+
opt := &ListOptions{Page: 2}
28+
ctx := t.Context()
29+
issues, _, err := client.Issues.ListBlockedBy(ctx, "o", "r", 1, opt)
30+
if err != nil {
31+
t.Errorf("Issues.ListBlockedBy returned error: %v", err)
32+
}
33+
34+
want := []*Issue{{Number: Ptr(1347), Title: Ptr("Found a bug")}}
35+
if !cmp.Equal(issues, want) {
36+
t.Errorf("Issues.ListBlockedBy returned %+v, want %+v", issues, want)
37+
}
38+
39+
const methodName = "ListBlockedBy"
40+
testBadOptions(t, methodName, func() (err error) {
41+
_, _, err = client.Issues.ListBlockedBy(ctx, "\n", "\n", -1, opt)
42+
return err
43+
})
44+
45+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
46+
got, resp, err := client.Issues.ListBlockedBy(ctx, "o", "r", 1, opt)
47+
if got != nil {
48+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
49+
}
50+
return resp, err
51+
})
52+
}
53+
54+
func TestIssuesService_ListBlockedBy_invalidOwner(t *testing.T) {
55+
t.Parallel()
56+
client, _, _ := setup(t)
57+
58+
ctx := t.Context()
59+
_, _, err := client.Issues.ListBlockedBy(ctx, "%", "%", 1, nil)
60+
testURLParseError(t, err)
61+
}
62+
63+
func TestIssuesService_AddBlockedBy(t *testing.T) {
64+
t.Parallel()
65+
client, mux, _ := setup(t)
66+
67+
input := &IssueDependencyRequest{IssueID: Ptr(int64(42))}
68+
69+
mux.HandleFunc("/repos/o/r/issues/1/dependencies/blocked_by", func(w http.ResponseWriter, r *http.Request) {
70+
var v *IssueDependencyRequest
71+
assertNilError(t, json.NewDecoder(r.Body).Decode(&v))
72+
73+
testMethod(t, r, "POST")
74+
if !cmp.Equal(v, input) {
75+
t.Errorf("Request body = %+v, want %+v", v, input)
76+
}
77+
78+
w.WriteHeader(http.StatusCreated)
79+
fmt.Fprint(w, `{"number":42,"title":"Dependency issue"}`)
80+
})
81+
82+
ctx := t.Context()
83+
issue, _, err := client.Issues.AddBlockedBy(ctx, "o", "r", 1, input)
84+
if err != nil {
85+
t.Errorf("Issues.AddBlockedBy returned error: %v", err)
86+
}
87+
88+
want := &Issue{Number: Ptr(42), Title: Ptr("Dependency issue")}
89+
if !cmp.Equal(issue, want) {
90+
t.Errorf("Issues.AddBlockedBy returned %+v, want %+v", issue, want)
91+
}
92+
93+
const methodName = "AddBlockedBy"
94+
testBadOptions(t, methodName, func() (err error) {
95+
_, _, err = client.Issues.AddBlockedBy(ctx, "\n", "\n", -1, input)
96+
return err
97+
})
98+
99+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
100+
got, resp, err := client.Issues.AddBlockedBy(ctx, "o", "r", 1, input)
101+
if got != nil {
102+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
103+
}
104+
return resp, err
105+
})
106+
}
107+
108+
func TestIssuesService_AddBlockedBy_invalidOwner(t *testing.T) {
109+
t.Parallel()
110+
client, _, _ := setup(t)
111+
112+
ctx := t.Context()
113+
_, _, err := client.Issues.AddBlockedBy(ctx, "%", "%", 1, nil)
114+
testURLParseError(t, err)
115+
}
116+
117+
func TestIssuesService_RemoveBlockedBy(t *testing.T) {
118+
t.Parallel()
119+
client, mux, _ := setup(t)
120+
121+
mux.HandleFunc("/repos/o/r/issues/1/dependencies/blocked_by/42", func(w http.ResponseWriter, r *http.Request) {
122+
testMethod(t, r, "DELETE")
123+
fmt.Fprint(w, `{"number":1,"title":"Original issue"}`)
124+
})
125+
126+
ctx := t.Context()
127+
issue, _, err := client.Issues.RemoveBlockedBy(ctx, "o", "r", 1, 42)
128+
if err != nil {
129+
t.Errorf("Issues.RemoveBlockedBy returned error: %v", err)
130+
}
131+
132+
want := &Issue{Number: Ptr(1), Title: Ptr("Original issue")}
133+
if !cmp.Equal(issue, want) {
134+
t.Errorf("Issues.RemoveBlockedBy returned %+v, want %+v", issue, want)
135+
}
136+
137+
const methodName = "RemoveBlockedBy"
138+
testBadOptions(t, methodName, func() (err error) {
139+
_, _, err = client.Issues.RemoveBlockedBy(ctx, "\n", "\n", -1, 42)
140+
return err
141+
})
142+
143+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
144+
got, resp, err := client.Issues.RemoveBlockedBy(ctx, "o", "r", 1, 42)
145+
if got != nil {
146+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
147+
}
148+
return resp, err
149+
})
150+
}
151+
152+
func TestIssuesService_RemoveBlockedBy_invalidOwner(t *testing.T) {
153+
t.Parallel()
154+
client, _, _ := setup(t)
155+
156+
ctx := t.Context()
157+
_, _, err := client.Issues.RemoveBlockedBy(ctx, "%", "%", 1, 42)
158+
testURLParseError(t, err)
159+
}
160+
161+
func TestIssuesService_ListBlocking(t *testing.T) {
162+
t.Parallel()
163+
client, mux, _ := setup(t)
164+
165+
mux.HandleFunc("/repos/o/r/issues/1/dependencies/blocking", func(w http.ResponseWriter, r *http.Request) {
166+
testMethod(t, r, "GET")
167+
testFormValues(t, r, values{"page": "2"})
168+
fmt.Fprint(w, `[{"number":1348,"title":"Blocked issue"}]`)
169+
})
170+
171+
opt := &ListOptions{Page: 2}
172+
ctx := t.Context()
173+
issues, _, err := client.Issues.ListBlocking(ctx, "o", "r", 1, opt)
174+
if err != nil {
175+
t.Errorf("Issues.ListBlocking returned error: %v", err)
176+
}
177+
178+
want := []*Issue{{Number: Ptr(1348), Title: Ptr("Blocked issue")}}
179+
if !cmp.Equal(issues, want) {
180+
t.Errorf("Issues.ListBlocking returned %+v, want %+v", issues, want)
181+
}
182+
183+
const methodName = "ListBlocking"
184+
testBadOptions(t, methodName, func() (err error) {
185+
_, _, err = client.Issues.ListBlocking(ctx, "\n", "\n", -1, opt)
186+
return err
187+
})
188+
189+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
190+
got, resp, err := client.Issues.ListBlocking(ctx, "o", "r", 1, opt)
191+
if got != nil {
192+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
193+
}
194+
return resp, err
195+
})
196+
}
197+
198+
func TestIssuesService_ListBlocking_invalidOwner(t *testing.T) {
199+
t.Parallel()
200+
client, _, _ := setup(t)
201+
202+
ctx := t.Context()
203+
_, _, err := client.Issues.ListBlocking(ctx, "%", "%", 1, nil)
204+
testURLParseError(t, err)
205+
}
206+
207+
func TestIssueDependencyRequest_Marshal(t *testing.T) {
208+
t.Parallel()
209+
testJSONMarshal(t, &IssueDependencyRequest{}, "{}")
210+
211+
u := &IssueDependencyRequest{
212+
IssueID: Ptr(int64(1)),
213+
}
214+
215+
want := `{
216+
"issue_id": 1
217+
}`
218+
219+
testJSONMarshal(t, u, want)
220+
}

0 commit comments

Comments
 (0)