From ca1c68fe3ba4ebd69e80108733995d6de3c78743 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Mon, 16 Mar 2026 18:20:43 -0400 Subject: [PATCH 1/2] Add ListRunnerGroupHostedRunners for org runner groups Implement GET /orgs/{org}/actions/runner-groups/{runner_group_id}/hosted-runners endpoint to list GitHub-hosted runners in an organization runner group. This was the only missing endpoint from the self-hosted runner groups API. --- github/actions_runner_groups.go | 26 +++++ github/actions_runner_groups_test.go | 144 +++++++++++++++++++++++++++ github/github-iterators.go | 35 +++++++ github/github-iterators_test.go | 72 ++++++++++++++ 4 files changed, 277 insertions(+) diff --git a/github/actions_runner_groups.go b/github/actions_runner_groups.go index dc54a84853d..2954ecd3912 100644 --- a/github/actions_runner_groups.go +++ b/github/actions_runner_groups.go @@ -265,6 +265,32 @@ func (s *ActionsService) RemoveRepositoryAccessRunnerGroup(ctx context.Context, return s.client.Do(ctx, req, nil) } +// ListRunnerGroupHostedRunners lists the GitHub-hosted runners in an organization runner group. +// +// GitHub API docs: https://docs.github.com/rest/actions/self-hosted-runner-groups#list-github-hosted-runners-in-a-group-for-an-organization +// +//meta:operation GET /orgs/{org}/actions/runner-groups/{runner_group_id}/hosted-runners +func (s *ActionsService) ListRunnerGroupHostedRunners(ctx context.Context, org string, groupID int64, opts *ListOptions) (*HostedRunners, *Response, error) { + u := fmt.Sprintf("orgs/%v/actions/runner-groups/%v/hosted-runners", org, groupID) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + runners := &HostedRunners{} + resp, err := s.client.Do(ctx, req, &runners) + if err != nil { + return nil, resp, err + } + + return runners, resp, nil +} + // ListRunnerGroupRunners lists self-hosted runners that are in a specific organization group. // // GitHub API docs: https://docs.github.com/rest/actions/self-hosted-runner-groups#list-self-hosted-runners-in-a-group-for-an-organization diff --git a/github/actions_runner_groups_test.go b/github/actions_runner_groups_test.go index 994e2716b4c..8d7354d430f 100644 --- a/github/actions_runner_groups_test.go +++ b/github/actions_runner_groups_test.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/google/go-cmp/cmp" ) @@ -413,6 +414,149 @@ func TestActionsService_RemoveRepositoryAccessRunnerGroup(t *testing.T) { }) } +func TestActionsService_ListRunnerGroupHostedRunners(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/actions/runner-groups/2/hosted-runners", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "2", "page": "2"}) + fmt.Fprint(w, `{ + "total_count": 2, + "runners": [ + { + "id": 5, + "name": "My hosted ubuntu runner", + "runner_group_id": 2, + "platform": "linux-x64", + "image_details": { + "id": "ubuntu-20.04", + "size_gb": 86 + }, + "machine_size_details": { + "id": "4-core", + "cpu_cores": 4, + "memory_gb": 16, + "storage_gb": 150 + }, + "status": "Ready", + "maximum_runners": 10, + "public_ip_enabled": true, + "public_ips": [ + { + "enabled": true, + "prefix": "20.80.208.150", + "length": 31 + } + ], + "last_active_on": "2023-04-26T15:23:37Z" + }, + { + "id": 7, + "name": "My hosted Windows runner", + "runner_group_id": 2, + "platform": "win-x64", + "image_details": { + "id": "windows-latest", + "size_gb": 256 + }, + "machine_size_details": { + "id": "8-core", + "cpu_cores": 8, + "memory_gb": 32, + "storage_gb": 300 + }, + "status": "Ready", + "maximum_runners": 20, + "public_ip_enabled": false, + "public_ips": [], + "last_active_on": "2023-04-26T15:23:37Z" + } + ] + }`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + ctx := t.Context() + runners, _, err := client.Actions.ListRunnerGroupHostedRunners(ctx, "o", 2, opts) + if err != nil { + t.Errorf("Actions.ListRunnerGroupHostedRunners returned error: %v", err) + } + + lastActiveOn := Timestamp{time.Date(2023, 4, 26, 15, 23, 37, 0, time.UTC)} + + want := &HostedRunners{ + TotalCount: 2, + Runners: []*HostedRunner{ + { + ID: Ptr(int64(5)), + Name: Ptr("My hosted ubuntu runner"), + RunnerGroupID: Ptr(int64(2)), + Platform: Ptr("linux-x64"), + ImageDetails: &HostedRunnerImageDetail{ + ID: Ptr("ubuntu-20.04"), + SizeGB: Ptr(int64(86)), + }, + MachineSizeDetails: &HostedRunnerMachineSpec{ + ID: "4-core", + CPUCores: 4, + MemoryGB: 16, + StorageGB: 150, + }, + Status: Ptr("Ready"), + MaximumRunners: Ptr(int64(10)), + PublicIPEnabled: Ptr(true), + PublicIPs: []*HostedRunnerPublicIP{ + { + Enabled: true, + Prefix: "20.80.208.150", + Length: 31, + }, + }, + LastActiveOn: Ptr(lastActiveOn), + }, + { + ID: Ptr(int64(7)), + Name: Ptr("My hosted Windows runner"), + RunnerGroupID: Ptr(int64(2)), + Platform: Ptr("win-x64"), + ImageDetails: &HostedRunnerImageDetail{ + ID: Ptr("windows-latest"), + SizeGB: Ptr(int64(256)), + }, + MachineSizeDetails: &HostedRunnerMachineSpec{ + ID: "8-core", + CPUCores: 8, + MemoryGB: 32, + StorageGB: 300, + }, + Status: Ptr("Ready"), + MaximumRunners: Ptr(int64(20)), + PublicIPEnabled: Ptr(false), + PublicIPs: []*HostedRunnerPublicIP{}, + LastActiveOn: Ptr(lastActiveOn), + }, + }, + } + if !cmp.Equal(runners, want) { + t.Errorf("Actions.ListRunnerGroupHostedRunners returned %+v, want %+v", runners, want) + } + + const methodName = "ListRunnerGroupHostedRunners" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Actions.ListRunnerGroupHostedRunners(ctx, "\n", 2, opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Actions.ListRunnerGroupHostedRunners(ctx, "o", 2, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestActionsService_ListRunnerGroupRunners(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/github-iterators.go b/github/github-iterators.go index f7b1bc6e97a..f33c9edc47c 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -679,6 +679,41 @@ func (s *ActionsService) ListRepositoryWorkflowRunsIter(ctx context.Context, own } } +// ListRunnerGroupHostedRunnersIter returns an iterator that paginates through all results of ListRunnerGroupHostedRunners. +func (s *ActionsService) ListRunnerGroupHostedRunnersIter(ctx context.Context, org string, groupID int64, opts *ListOptions) iter.Seq2[*HostedRunner, error] { + return func(yield func(*HostedRunner, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &ListOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListRunnerGroupHostedRunners(ctx, org, groupID, opts) + if err != nil { + yield(nil, err) + return + } + + var iterItems []*HostedRunner + if results != nil { + iterItems = results.Runners + } + for _, item := range iterItems { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + } +} + // ListRunnerGroupRunnersIter returns an iterator that paginates through all results of ListRunnerGroupRunners. func (s *ActionsService) ListRunnerGroupRunnersIter(ctx context.Context, org string, groupID int64, opts *ListOptions) iter.Seq2[*Runner, error] { return func(yield func(*Runner, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index a6ef9ef41d8..c48f02bcefc 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -1383,6 +1383,78 @@ func TestActionsService_ListRepositoryWorkflowRunsIter(t *testing.T) { } } +func TestActionsService_ListRunnerGroupHostedRunnersIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `{"runners": [{},{},{}]}`) + case 2: + fmt.Fprint(w, `{"runners": [{},{},{},{}]}`) + case 3: + fmt.Fprint(w, `{"runners": [{},{}]}`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `{"runners": [{},{}]}`) + } + }) + + iter := client.Actions.ListRunnerGroupHostedRunnersIter(t.Context(), "", 0, nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.Actions.ListRunnerGroupHostedRunnersIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListOptions{} + iter = client.Actions.ListRunnerGroupHostedRunnersIter(t.Context(), "", 0, opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.Actions.ListRunnerGroupHostedRunnersIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Actions.ListRunnerGroupHostedRunnersIter(t.Context(), "", 0, nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Actions.ListRunnerGroupHostedRunnersIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Actions.ListRunnerGroupHostedRunnersIter(t.Context(), "", 0, nil) + gotItems = 0 + iter(func(item *HostedRunner, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Actions.ListRunnerGroupHostedRunnersIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestActionsService_ListRunnerGroupRunnersIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) From 4c46ef38f1ad5d415ed2260c58a705f90f6d7ecf Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Tue, 17 Mar 2026 10:47:40 -0400 Subject: [PATCH 2/2] Use var declaration style for runners variable --- github/actions_runner_groups.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/actions_runner_groups.go b/github/actions_runner_groups.go index 2954ecd3912..2d673b77f40 100644 --- a/github/actions_runner_groups.go +++ b/github/actions_runner_groups.go @@ -282,7 +282,7 @@ func (s *ActionsService) ListRunnerGroupHostedRunners(ctx context.Context, org s return nil, nil, err } - runners := &HostedRunners{} + var runners *HostedRunners resp, err := s.client.Do(ctx, req, &runners) if err != nil { return nil, resp, err