Skip to content

Commit d0976cc

Browse files
feat: Add support for GitHub-hosted runner API endpoints (#3487)
1 parent e9d8a58 commit d0976cc

8 files changed

+2928
-7
lines changed

github/actions_hosted_runners.go

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
// Copyright 2025 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+
"errors"
11+
"fmt"
12+
)
13+
14+
// HostedRunnerPublicIP represents the details of a public IP for GitHub-hosted runner.
15+
type HostedRunnerPublicIP struct {
16+
Enabled bool `json:"enabled"` // Whether public IP is enabled.
17+
Prefix string `json:"prefix"` // The prefix for the public IP. Example: 20.80.208.150
18+
Length int `json:"length"` // The length of the IP prefix. Example: 28
19+
}
20+
21+
// HostedRunnerMachineSpec represents the details of a particular machine specification for GitHub-hosted runner.
22+
type HostedRunnerMachineSpec struct {
23+
ID string `json:"id"` // The ID used for the `size` parameter when creating a new runner. Example: 8-core
24+
CPUCores int `json:"cpu_cores"` // The number of cores. Example: 8
25+
MemoryGB int `json:"memory_gb"` // The available RAM for the machine spec. Example: 32
26+
StorageGB int `json:"storage_gb"` // The available SSD storage for the machine spec. Example: 300
27+
}
28+
29+
// HostedRunner represents a single GitHub-hosted runner with additional details.
30+
type HostedRunner struct {
31+
ID *int64 `json:"id,omitempty"`
32+
Name *string `json:"name,omitempty"`
33+
RunnerGroupID *int64 `json:"runner_group_id,omitempty"`
34+
Platform *string `json:"platform,omitempty"`
35+
ImageDetails *HostedRunnerImageDetail `json:"image_details,omitempty"`
36+
MachineSizeDetails *HostedRunnerMachineSpec `json:"machine_size_details,omitempty"`
37+
Status *string `json:"status,omitempty"`
38+
MaximumRunners *int64 `json:"maximum_runners,omitempty"`
39+
PublicIPEnabled *bool `json:"public_ip_enabled,omitempty"`
40+
PublicIPs []*HostedRunnerPublicIP `json:"public_ips,omitempty"`
41+
LastActiveOn *Timestamp `json:"last_active_on,omitempty"`
42+
}
43+
44+
// HostedRunnerImageDetail represents the image details of a GitHub-hosted runners.
45+
type HostedRunnerImageDetail struct {
46+
ID *string `json:"id"` // The ID of the image. Use this ID for the `image` parameter when creating a new larger runner. Example: ubuntu-20.04
47+
SizeGB *int64 `json:"size_gb"` // Image size in GB. Example: 86
48+
DisplayName *string `json:"display_name"` // Display name for this image. Example: 20.04
49+
Source *string `json:"source"` // The image provider. Example: github, partner, custom
50+
Version *string `json:"version"` // The image version of the hosted runner pool. Example: latest
51+
}
52+
53+
// HostedRunners represents a collection of GitHub-hosted runners for an organization.
54+
type HostedRunners struct {
55+
TotalCount int `json:"total_count"`
56+
Runners []*HostedRunner `json:"runners"`
57+
}
58+
59+
// ListHostedRunners lists all the GitHub-hosted runners for an organization.
60+
//
61+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#list-github-hosted-runners-for-an-organization
62+
//
63+
//meta:operation GET /orgs/{org}/actions/hosted-runners
64+
func (s *ActionsService) ListHostedRunners(ctx context.Context, org string, opts *ListOptions) (*HostedRunners, *Response, error) {
65+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners", org)
66+
u, err := addOptions(u, opts)
67+
if err != nil {
68+
return nil, nil, err
69+
}
70+
71+
req, err := s.client.NewRequest("GET", u, nil)
72+
if err != nil {
73+
return nil, nil, err
74+
}
75+
76+
runners := &HostedRunners{}
77+
resp, err := s.client.Do(ctx, req, &runners)
78+
if err != nil {
79+
return nil, resp, err
80+
}
81+
82+
return runners, resp, nil
83+
}
84+
85+
// HostedRunnerImage represents the image of GitHub-hosted runners.
86+
// To list all available images, use GET /actions/hosted-runners/images/github-owned or GET /actions/hosted-runners/images/partner.
87+
type HostedRunnerImage struct {
88+
ID string `json:"id"`
89+
Source string `json:"source"`
90+
Version string `json:"version"`
91+
}
92+
93+
// HostedRunnerRequest specifies body parameters to Hosted Runner configuration.
94+
type HostedRunnerRequest struct {
95+
Name string `json:"name,omitempty"`
96+
Image HostedRunnerImage `json:"image,omitempty"`
97+
RunnerGroupID int64 `json:"runner_group_id,omitempty"`
98+
Size string `json:"size,omitempty"`
99+
MaximumRunners int64 `json:"maximum_runners,omitempty"`
100+
EnableStaticIP bool `json:"enable_static_ip,omitempty"`
101+
ImageVersion string `json:"image_version,omitempty"`
102+
}
103+
104+
// validateCreateHostedRunnerRequest validates the provided HostedRunnerRequest to ensure
105+
// that all required fields are properly set and that no invalid fields are present for hosted runner create request.
106+
//
107+
// If any of these conditions are violated, an appropriate error message is returned.
108+
// Otherwise, nil is returned, indicating the request is valid.
109+
func validateCreateHostedRunnerRequest(request *HostedRunnerRequest) error {
110+
if request.Size == "" {
111+
return errors.New("size is required for creating a hosted runner")
112+
}
113+
if request.Image == (HostedRunnerImage{}) {
114+
return errors.New("image is required for creating a hosted runner")
115+
}
116+
if request.Name == "" {
117+
return errors.New("name is required for creating a hosted runner")
118+
}
119+
if request.RunnerGroupID == 0 {
120+
return errors.New("runner group ID is required for creating a hosted runner")
121+
}
122+
if request.ImageVersion != "" {
123+
return errors.New("imageVersion should not be set directly; use the Image struct to specify image details")
124+
}
125+
return nil
126+
}
127+
128+
// CreateHostedRunner creates a GitHub-hosted runner for an organization.
129+
//
130+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#create-a-github-hosted-runner-for-an-organization
131+
//
132+
//meta:operation POST /orgs/{org}/actions/hosted-runners
133+
func (s *ActionsService) CreateHostedRunner(ctx context.Context, org string, request *HostedRunnerRequest) (*HostedRunner, *Response, error) {
134+
if err := validateCreateHostedRunnerRequest(request); err != nil {
135+
return nil, nil, fmt.Errorf("validation failed: %w", err)
136+
}
137+
138+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners", org)
139+
req, err := s.client.NewRequest("POST", u, request)
140+
if err != nil {
141+
return nil, nil, err
142+
}
143+
144+
hostedRunner := new(HostedRunner)
145+
resp, err := s.client.Do(ctx, req, hostedRunner)
146+
if err != nil {
147+
return nil, resp, err
148+
}
149+
150+
return hostedRunner, resp, nil
151+
}
152+
153+
// HostedRunnerImageSpecs represents the details of a GitHub-hosted runner image.
154+
type HostedRunnerImageSpecs struct {
155+
ID string `json:"id"`
156+
Platform string `json:"platform"`
157+
SizeGB int `json:"size_gb"`
158+
DisplayName string `json:"display_name"`
159+
Source string `json:"source"`
160+
}
161+
162+
// HostedRunnerImages represents the response containing the total count and details of runner images.
163+
type HostedRunnerImages struct {
164+
TotalCount int `json:"total_count"`
165+
Images []*HostedRunnerImageSpecs `json:"images"`
166+
}
167+
168+
// GetHostedRunnerGitHubOwnedImages gets the list of GitHub-owned images available for GitHub-hosted runners for an organization.
169+
//
170+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#get-github-owned-images-for-github-hosted-runners-in-an-organization
171+
//
172+
//meta:operation GET /orgs/{org}/actions/hosted-runners/images/github-owned
173+
func (s *ActionsService) GetHostedRunnerGitHubOwnedImages(ctx context.Context, org string) (*HostedRunnerImages, *Response, error) {
174+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners/images/github-owned", org)
175+
req, err := s.client.NewRequest("GET", u, nil)
176+
if err != nil {
177+
return nil, nil, err
178+
}
179+
180+
hostedRunnerImages := new(HostedRunnerImages)
181+
resp, err := s.client.Do(ctx, req, hostedRunnerImages)
182+
if err != nil {
183+
return nil, resp, err
184+
}
185+
186+
return hostedRunnerImages, resp, nil
187+
}
188+
189+
// GetHostedRunnerPartnerImages gets the list of partner images available for GitHub-hosted runners for an organization.
190+
//
191+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#get-partner-images-for-github-hosted-runners-in-an-organization
192+
//
193+
//meta:operation GET /orgs/{org}/actions/hosted-runners/images/partner
194+
func (s *ActionsService) GetHostedRunnerPartnerImages(ctx context.Context, org string) (*HostedRunnerImages, *Response, error) {
195+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners/images/partner", org)
196+
req, err := s.client.NewRequest("GET", u, nil)
197+
if err != nil {
198+
return nil, nil, err
199+
}
200+
201+
hostedRunnerImages := new(HostedRunnerImages)
202+
resp, err := s.client.Do(ctx, req, hostedRunnerImages)
203+
if err != nil {
204+
return nil, resp, err
205+
}
206+
207+
return hostedRunnerImages, resp, nil
208+
}
209+
210+
// HostedRunnerPublicIPLimits represents the static public IP limits for GitHub-hosted runners.
211+
type HostedRunnerPublicIPLimits struct {
212+
PublicIPs *PublicIPUsage `json:"public_ips"`
213+
}
214+
215+
// PublicIPUsage provides details of static public IP limits for GitHub-hosted runners.
216+
type PublicIPUsage struct {
217+
Maximum int64 `json:"maximum"` // The maximum number of static public IP addresses that can be used for Hosted Runners. Example: 50
218+
CurrentUsage int64 `json:"current_usage"` // The current number of static public IP addresses in use by Hosted Runners. Example: 17
219+
}
220+
221+
// GetHostedRunnerLimits gets the GitHub-hosted runners Static public IP Limits for an organization.
222+
//
223+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#get-limits-on-github-hosted-runners-for-an-organization
224+
//
225+
//meta:operation GET /orgs/{org}/actions/hosted-runners/limits
226+
func (s *ActionsService) GetHostedRunnerLimits(ctx context.Context, org string) (*HostedRunnerPublicIPLimits, *Response, error) {
227+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners/limits", org)
228+
req, err := s.client.NewRequest("GET", u, nil)
229+
if err != nil {
230+
return nil, nil, err
231+
}
232+
233+
publicIPLimits := new(HostedRunnerPublicIPLimits)
234+
resp, err := s.client.Do(ctx, req, publicIPLimits)
235+
if err != nil {
236+
return nil, resp, err
237+
}
238+
239+
return publicIPLimits, resp, nil
240+
}
241+
242+
// HostedRunnerMachineSpecs represents the response containing the total count and details of machine specs for GitHub-hosted runners.
243+
type HostedRunnerMachineSpecs struct {
244+
TotalCount int `json:"total_count"`
245+
MachineSpecs []*HostedRunnerMachineSpec `json:"machine_specs"`
246+
}
247+
248+
// GetHostedRunnerMachineSpecs gets the list of machine specs available for GitHub-hosted runners for an organization.
249+
//
250+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#get-github-hosted-runners-machine-specs-for-an-organization
251+
//
252+
//meta:operation GET /orgs/{org}/actions/hosted-runners/machine-sizes
253+
func (s *ActionsService) GetHostedRunnerMachineSpecs(ctx context.Context, org string) (*HostedRunnerMachineSpecs, *Response, error) {
254+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners/machine-sizes", org)
255+
req, err := s.client.NewRequest("GET", u, nil)
256+
if err != nil {
257+
return nil, nil, err
258+
}
259+
260+
machineSpecs := new(HostedRunnerMachineSpecs)
261+
resp, err := s.client.Do(ctx, req, machineSpecs)
262+
if err != nil {
263+
return nil, resp, err
264+
}
265+
266+
return machineSpecs, resp, nil
267+
}
268+
269+
// HostedRunnerPlatforms represents the response containing the total count and platforms for GitHub-hosted runners.
270+
type HostedRunnerPlatforms struct {
271+
TotalCount int `json:"total_count"`
272+
Platforms []string `json:"platforms"`
273+
}
274+
275+
// GetHostedRunnerPlatforms gets list of platforms available for GitHub-hosted runners for an organization.
276+
//
277+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#get-platforms-for-github-hosted-runners-in-an-organization
278+
//
279+
//meta:operation GET /orgs/{org}/actions/hosted-runners/platforms
280+
func (s *ActionsService) GetHostedRunnerPlatforms(ctx context.Context, org string) (*HostedRunnerPlatforms, *Response, error) {
281+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners/platforms", org)
282+
req, err := s.client.NewRequest("GET", u, nil)
283+
if err != nil {
284+
return nil, nil, err
285+
}
286+
287+
platforms := new(HostedRunnerPlatforms)
288+
resp, err := s.client.Do(ctx, req, platforms)
289+
if err != nil {
290+
return nil, resp, err
291+
}
292+
293+
return platforms, resp, nil
294+
}
295+
296+
// GetHostedRunner gets a GitHub-hosted runner in an organization.
297+
//
298+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#get-a-github-hosted-runner-for-an-organization
299+
//
300+
//meta:operation GET /orgs/{org}/actions/hosted-runners/{hosted_runner_id}
301+
func (s *ActionsService) GetHostedRunner(ctx context.Context, org string, runnerID int64) (*HostedRunner, *Response, error) {
302+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners/%v", org, runnerID)
303+
req, err := s.client.NewRequest("GET", u, nil)
304+
if err != nil {
305+
return nil, nil, err
306+
}
307+
308+
hostedRunner := new(HostedRunner)
309+
resp, err := s.client.Do(ctx, req, hostedRunner)
310+
if err != nil {
311+
return nil, resp, err
312+
}
313+
314+
return hostedRunner, resp, nil
315+
}
316+
317+
// validateUpdateHostedRunnerRequest validates the provided HostedRunnerRequest to ensure
318+
// that no disallowed updates are made for a hosted runner update request.
319+
//
320+
// If any of these conditions are violated, an appropriate error message is returned.
321+
// Otherwise, nil is returned, indicating the request is valid for an update.
322+
func validateUpdateHostedRunnerRequest(request *HostedRunnerRequest) error {
323+
if request.Size != "" {
324+
return errors.New("size cannot be updated, API does not support updating size")
325+
}
326+
if request.Image != (HostedRunnerImage{}) {
327+
return errors.New("image struct should not be set directly; use the ImageVersion to specify version details")
328+
}
329+
return nil
330+
}
331+
332+
// UpdateHostedRunner updates a GitHub-hosted runner for an organization.
333+
//
334+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#update-a-github-hosted-runner-for-an-organization
335+
//
336+
//meta:operation PATCH /orgs/{org}/actions/hosted-runners/{hosted_runner_id}
337+
func (s *ActionsService) UpdateHostedRunner(ctx context.Context, org string, runnerID int64, updateReq HostedRunnerRequest) (*HostedRunner, *Response, error) {
338+
if err := validateUpdateHostedRunnerRequest(&updateReq); err != nil {
339+
return nil, nil, fmt.Errorf("validation failed: %w", err)
340+
}
341+
342+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners/%v", org, runnerID)
343+
req, err := s.client.NewRequest("PATCH", u, updateReq)
344+
if err != nil {
345+
return nil, nil, err
346+
}
347+
348+
hostedRunner := new(HostedRunner)
349+
resp, err := s.client.Do(ctx, req, hostedRunner)
350+
if err != nil {
351+
return nil, resp, err
352+
}
353+
354+
return hostedRunner, resp, nil
355+
}
356+
357+
// DeleteHostedRunner deletes GitHub-hosted runner from an organization.
358+
//
359+
// GitHub API docs: https://docs.github.com/rest/actions/hosted-runners#delete-a-github-hosted-runner-for-an-organization
360+
//
361+
//meta:operation DELETE /orgs/{org}/actions/hosted-runners/{hosted_runner_id}
362+
func (s *ActionsService) DeleteHostedRunner(ctx context.Context, org string, runnerID int64) (*HostedRunner, *Response, error) {
363+
u := fmt.Sprintf("orgs/%v/actions/hosted-runners/%v", org, runnerID)
364+
req, err := s.client.NewRequest("DELETE", u, nil)
365+
if err != nil {
366+
return nil, nil, err
367+
}
368+
369+
hostedRunner := new(HostedRunner)
370+
resp, err := s.client.Do(ctx, req, hostedRunner)
371+
if err != nil {
372+
return nil, resp, err
373+
}
374+
375+
return hostedRunner, resp, nil
376+
}

0 commit comments

Comments
 (0)