Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import (
)

const (
runnerEndpoint = "_apis/distributedtask/pools/0/agents"
scaleSetEndpoint = "_apis/runtime/runnerscalesets"
runnerEndpoint = "_apis/distributedtask/pools/0/agents"
runnerGroupEndpoint = "_apis/runtime/runnergroups/"
scaleSetEndpoint = "_apis/runtime/runnerscalesets"
)

var buildInfo clientBuildInfo
Expand Down Expand Up @@ -443,7 +444,7 @@ func (c *Client) GetRunnerScaleSetByID(ctx context.Context, runnerScaleSetID int
// GetRunnerGroupByName fetches a runner group by its name.
func (c *Client) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*RunnerGroup, error) {
query := url.Values{"groupName": []string{runnerGroup}}
req, err := c.newActionsServiceRequestWithQuery(ctx, http.MethodGet, "/_apis/runtime/runnergroups/", query, nil)
req, err := c.newActionsServiceRequestWithQuery(ctx, http.MethodGet, runnerGroupEndpoint, query, nil)
if err != nil {
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
Expand Down Expand Up @@ -472,6 +473,34 @@ func (c *Client) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (
}
}

// ListRunnerGroups returns every runner group available to the client's configured GitHub scope.
// The repository, organization, or enterprise is selected by the GitHubConfigURL used to create
// the client. Results preserve the API response order; request, HTTP status, and decoding failures
// are returned as errors.
func (c *Client) ListRunnerGroups(ctx context.Context) ([]RunnerGroup, error) {
req, err := c.newActionsServiceRequest(ctx, http.MethodGet, runnerGroupEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}

resp, err := c.do(req)
if err != nil {
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}

var runnerGroupList RunnerGroupList
if err := json.NewDecoder(resp.Body).Decode(&runnerGroupList); err != nil {
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode runner group list: %w", err))
}

return runnerGroupList.RunnerGroups, nil
}

// applyDefaultLabelTypes ensures that each label in the runner scale set has a Type set,
// defaulting to "System" when the field is empty. This encapsulates the legacy API detail
// so that callers do not need to manage label types explicitly.
Expand Down
72 changes: 72 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,78 @@ func TestGetRunnerGroupByName(t *testing.T) {
})
}

func TestListRunnerGroups(t *testing.T) {
ctx := context.Background()
auth := actionsAuth{token: "token"}

t.Run("returns every runner group for the configured repository", func(t *testing.T) {
expected := []RunnerGroup{
{ID: 1, Name: "default", Size: 2, IsDefault: true},
{ID: 2, Name: "production", Size: 3},
}

server := newActionsServer(t, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
assert.Equal(t, "/tenant/123/_apis/runtime/runnergroups/", request.URL.Path)
assert.Empty(t, request.URL.Query().Get("groupName"))
writer.Header().Set("Content-Type", "application/json")
require.NoError(t, json.NewEncoder(writer).Encode(RunnerGroupList{
Count: len(expected),
RunnerGroups: expected,
}))
}))

client, err := newClient(testSystemInfo, server.URL+"/my-org/my-repo", auth)
require.NoError(t, err)

actual, err := client.ListRunnerGroups(ctx)
require.NoError(t, err)
assert.Equal(t, expected, actual)
})

t.Run("returns an empty list", func(t *testing.T) {
server := newActionsServer(t, http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.Header().Set("Content-Type", "application/json")
_, err := writer.Write([]byte(`{"count":0,"value":[]}`))
require.NoError(t, err)
}))

client, err := newClient(testSystemInfo, server.URL+"/my-org/my-repo", auth)
require.NoError(t, err)

actual, err := client.ListRunnerGroups(ctx)
require.NoError(t, err)
assert.Empty(t, actual)
})

t.Run("returns an error for a non-success response", func(t *testing.T) {
server := newActionsServer(t, http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.WriteHeader(http.StatusTeapot)
}))

client, err := newClient(testSystemInfo, server.URL+"/my-org/my-repo", auth)
require.NoError(t, err)

actual, err := client.ListRunnerGroups(ctx)
assert.ErrorContains(t, err, "unexpected status code: 418")
assert.Nil(t, actual)
})

t.Run("returns an error for a malformed response", func(t *testing.T) {
server := newActionsServer(t, http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.Header().Set("Content-Type", "application/json")
_, err := writer.Write([]byte(`{"count":`))
require.NoError(t, err)
}))

client, err := newClient(testSystemInfo, server.URL+"/my-org/my-repo", auth)
require.NoError(t, err)

actual, err := client.ListRunnerGroups(ctx)
assert.ErrorContains(t, err, "failed to decode runner group list")
assert.Nil(t, actual)
})
}

func TestGetRunnerScaleSet(t *testing.T) {
ctx := context.Background()
auth := actionsAuth{
Expand Down