From 1c366b1e828802f71e4cf019b544a1ef65a6ad4c Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Wed, 24 Dec 2025 12:51:43 +0000 Subject: [PATCH 1/6] add support for codespaces org --- github/codespaces_orgs.go | 173 +++++++++++++++++++++++++ github/codespaces_orgs_test.go | 227 +++++++++++++++++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 github/codespaces_orgs.go create mode 100644 github/codespaces_orgs_test.go diff --git a/github/codespaces_orgs.go b/github/codespaces_orgs.go new file mode 100644 index 00000000000..024e4a7b176 --- /dev/null +++ b/github/codespaces_orgs.go @@ -0,0 +1,173 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" +) + +// CodespaceOrgAccessControlRequest represent request for SetOrgAccessControl. +type CodespaceOrgAccessControlRequest struct { + // Visibility represent which users can access codespaces in the organization. + // Can be one of: disabled, selected_members, all_members, all_members_and_outside_collaborators. + Visibility string `json:"visibility"` + // SelectedUsernames represent the usernames of the organization members who should have access to codespaces in the organization. + // Required when visibility is selected_members. + SelectedUsernames []string `json:"selected_usernames,omitempty"` +} + +// ListInOrg lists the codespaces associated to a specified organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#list-codespaces-for-the-organization +// +//meta:operation GET /orgs/{org}/codespaces +func (s *CodespacesService) ListInOrg(ctx context.Context, org string, opts *ListOptions) (*ListCodespaces, *Response, error) { + u := fmt.Sprintf("orgs/%v/codespaces", org) + 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 + } + + var codespaces *ListCodespaces + resp, err := s.client.Do(ctx, req, &codespaces) + if err != nil { + return nil, resp, err + } + + return codespaces, resp, nil +} + +// SetOrgAccessControl sets which users can access codespaces in an organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#manage-access-control-for-organization-codespaces +// +//meta:operation PUT /orgs/{org}/codespaces/access +func (s *CodespacesService) SetOrgAccessControl(ctx context.Context, org string, request CodespaceOrgAccessControlRequest) (*Response, error) { + u := fmt.Sprintf("orgs/%v/codespaces/access", org) + req, err := s.client.NewRequest("PUT", u, request) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// AddUsersToOrgAccess adds users to Codespaces access for an organization +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#add-users-to-codespaces-access-for-an-organization +// +//meta:operation POST /orgs/{org}/codespaces/access/selected_users +func (s *CodespacesService) AddUsersToOrgAccess(ctx context.Context, org string, usernames []string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/codespaces/access/selected_users", org) + req, err := s.client.NewRequest("POST", u, map[string][]string{"selected_usernames": usernames}) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// RemoveUsersFromOrgAccess removes users from Codespaces access for an organization +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#remove-users-from-codespaces-access-for-an-organization +// +//meta:operation DELETE /orgs/{org}/codespaces/access/selected_users +func (s *CodespacesService) RemoveUsersFromOrgAccess(ctx context.Context, org string, usernames []string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/codespaces/access/selected_users", org) + req, err := s.client.NewRequest("DELETE", u, map[string][]string{"selected_usernames": usernames}) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// ListUserCodespacesInOrg lists the codespaces that a member of an organization has for repositories in that organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#list-codespaces-for-a-user-in-organization +// +//meta:operation GET /orgs/{org}/members/{username}/codespaces +func (s *CodespacesService) ListUserCodespacesInOrg(ctx context.Context, org, username string, opts *ListOptions) (*ListCodespaces, *Response, error) { + u := fmt.Sprintf("orgs/%v/members/%v/codespaces", org, username) + 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 + } + + var codespaces *ListCodespaces + resp, err := s.client.Do(ctx, req, &codespaces) + if err != nil { + return nil, resp, err + } + + return codespaces, resp, nil +} + +// DeleteUserCodespaceInOrg deletes a user's codespace from the organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#delete-a-codespace-from-the-organization +// +//meta:operation DELETE /orgs/{org}/members/{username}/codespaces/{codespace_name} +func (s *CodespacesService) DeleteUserCodespaceInOrg(ctx context.Context, org, username, codespaceName string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/members/%v/codespaces/%v", org, username, codespaceName) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// StopUserCodespaceInOrg stops a codespace for an organization user. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#stop-a-codespace-for-an-organization-user +// +//meta:operation POST /orgs/{org}/members/{username}/codespaces/{codespace_name}/stop +func (s *CodespacesService) StopUserCodespaceInOrg(ctx context.Context, org, username, codespaceName string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/members/%v/codespaces/%v/stop", org, username, codespaceName) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} diff --git a/github/codespaces_orgs_test.go b/github/codespaces_orgs_test.go new file mode 100644 index 00000000000..836d7c877c4 --- /dev/null +++ b/github/codespaces_orgs_test.go @@ -0,0 +1,227 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestCodespacesService_ListInOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/codespaces", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"total_count":1,"codespaces":[{"id":1}]}`) + }) + + opts := &ListOptions{Page: 1, PerPage: 10} + ctx := t.Context() + got, _, err := client.Codespaces.ListInOrg(ctx, "o1", opts) + if err != nil { + t.Fatalf("Codespaces.ListInOrg returned error: %v", err) + } + + want := &ListCodespaces{ + TotalCount: Ptr(1), + Codespaces: []*Codespace{ + {ID: Ptr(int64(1))}, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Codespaces.ListInOrg = %+v, want %+v", got, want) + } + const methodName = "ListInOrg" + testBadOptions(t, methodName, func() error { + _, _, err := client.Codespaces.ListInOrg(ctx, "\n", opts) + return err + }) +} + +func TestCodespacesService_SetOrgAccessControl(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/codespaces/access", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testBody(t, r, `{"visibility":"selected_members","selected_usernames":["u1","u2"]}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + req := CodespaceOrgAccessControlRequest{ + Visibility: "selected_members", + SelectedUsernames: []string{"u2", "u2"}, + } + + _, err := client.Codespaces.SetOrgAccessControl(ctx, "o1", req) + if err != nil { + t.Fatalf("Codespaces.SetOrgAccessControl returned error: %v", err) + } + + const methodName = "SetOrgAccessControl" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.SetOrgAccessControl(ctx, "\n", req) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.SetOrgAccessControl(ctx, "o1", req) + }) +} + +func TestEnterpriseService_AddUsersToOrgAccess(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/codespaces/access/selected_users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"selected_usernames":["u1"]}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + req := []string{"u1"} + resp, err := client.Codespaces.AddUsersToOrgAccess(ctx, "o1", req) + if err != nil { + t.Fatalf("AddUsersToOrgAccess returned error: %v", err) + } + if resp == nil { + t.Fatal("AddUsersToOrgAccess returned nil Response") + } + + const methodName = "AddUsersToOrgAccess" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.AddUsersToOrgAccess(ctx, "\n", req) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.AddUsersToOrgAccess(ctx, "o1", req) + }) +} + +func TestEnterpriseService_RemoveUsersFromOrgAccess(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/codespaces/access/selected_users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testBody(t, r, `{"selected_usernames":["u1"]}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + req := []string{"u1"} + resp, err := client.Codespaces.RemoveUsersFromOrgAccess(ctx, "o1", req) + if err != nil { + t.Fatalf("RemoveUsersFromOrgAccess returned error: %v", err) + } + if resp == nil { + t.Fatal("RemoveUsersFromOrgAccess returned nil Response") + } + + const methodName = "RemoveUsersFromOrgAccess" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.RemoveUsersFromOrgAccess(ctx, "\n", req) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.RemoveUsersFromOrgAccess(ctx, "o1", req) + }) +} + +func TestCodespacesService_ListUserCodespacesInOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/members/u1/codespaces", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"total_count":1,"codespaces":[{"id":1}]}`) + }) + + opts := &ListOptions{Page: 1, PerPage: 10} + ctx := t.Context() + got, _, err := client.Codespaces.ListUserCodespacesInOrg(ctx, "o1", "u1", opts) + if err != nil { + t.Fatalf("Codespaces.ListUserCodespacesInOrg returned error: %v", err) + } + + want := &ListCodespaces{ + TotalCount: Ptr(1), + Codespaces: []*Codespace{ + {ID: Ptr(int64(1))}, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Codespaces.ListUserCodespacesInOrg = %+v, want %+v", got, want) + } + const methodName = "ListUserCodespacesInOrg" + testBadOptions(t, methodName, func() error { + _, _, err := client.Codespaces.ListUserCodespacesInOrg(ctx, "\n", "\n", opts) + return err + }) +} + +func TestEnterpriseService_DeleteUserCodespaceInOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/members/u1/codespaces/c1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Codespaces.DeleteUserCodespaceInOrg(ctx, "o1", "u1", "c1") + if err != nil { + t.Fatalf("DeleteUserCodespaceInOrg returned error: %v", err) + } + if resp == nil { + t.Fatal("DeleteUserCodespaceInOrg returned nil Response") + } + + const methodName = "DeleteUserCodespaceInOrg" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.DeleteUserCodespaceInOrg(ctx, "\n", "u1", "c1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.DeleteUserCodespaceInOrg(ctx, "o1", "u1", "c1") + }) +} + +func TestEnterpriseService_StopUserCodespaceInOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/members/u1/codespaces/c1/stop", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Codespaces.StopUserCodespaceInOrg(ctx, "o1", "u1", "c1") + if err != nil { + t.Fatalf("StopUserCodespaceInOrg returned error: %v", err) + } + if resp == nil { + t.Fatal("StopUserCodespaceInOrg returned nil Response") + } + + const methodName = "StopUserCodespaceInOrg" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.StopUserCodespaceInOrg(ctx, "\n", "u1", "c1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.StopUserCodespaceInOrg(ctx, "o1", "u1", "c1") + }) +} From ca8e60b378790d00ad36f3e5f27f29353dcf68c4 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Wed, 24 Dec 2025 12:58:07 +0000 Subject: [PATCH 2/6] update copyright year to 2025 --- github/codespaces_orgs.go | 2 +- github/codespaces_orgs_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/github/codespaces_orgs.go b/github/codespaces_orgs.go index 024e4a7b176..5117e4b4d20 100644 --- a/github/codespaces_orgs.go +++ b/github/codespaces_orgs.go @@ -1,4 +1,4 @@ -// Copyright 2023 The go-github AUTHORS. All rights reserved. +// Copyright 2025 The go-github AUTHORS. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/github/codespaces_orgs_test.go b/github/codespaces_orgs_test.go index 836d7c877c4..2bc21a72fde 100644 --- a/github/codespaces_orgs_test.go +++ b/github/codespaces_orgs_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 The go-github AUTHORS. All rights reserved. +// Copyright 2025 The go-github AUTHORS. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. From b2372c6630f3379f28fb7f71936c76da041ce620 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Wed, 24 Dec 2025 13:04:22 +0000 Subject: [PATCH 3/6] fix test --- github/codespaces_orgs_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/codespaces_orgs_test.go b/github/codespaces_orgs_test.go index 2bc21a72fde..3c2704f5897 100644 --- a/github/codespaces_orgs_test.go +++ b/github/codespaces_orgs_test.go @@ -59,7 +59,7 @@ func TestCodespacesService_SetOrgAccessControl(t *testing.T) { ctx := t.Context() req := CodespaceOrgAccessControlRequest{ Visibility: "selected_members", - SelectedUsernames: []string{"u2", "u2"}, + SelectedUsernames: []string{"u1", "u2"}, } _, err := client.Codespaces.SetOrgAccessControl(ctx, "o1", req) From 6f151ccf5b24cf365471cba9a5a06ae6dc798f10 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Wed, 24 Dec 2025 13:56:18 +0000 Subject: [PATCH 4/6] inc coverage --- github/codespaces_orgs_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/github/codespaces_orgs_test.go b/github/codespaces_orgs_test.go index 3c2704f5897..cffed8d2113 100644 --- a/github/codespaces_orgs_test.go +++ b/github/codespaces_orgs_test.go @@ -44,6 +44,13 @@ func TestCodespacesService_ListInOrg(t *testing.T) { _, _, err := client.Codespaces.ListInOrg(ctx, "\n", opts) return err }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.ListInOrg(ctx, "o1", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) } func TestCodespacesService_SetOrgAccessControl(t *testing.T) { @@ -168,6 +175,13 @@ func TestCodespacesService_ListUserCodespacesInOrg(t *testing.T) { _, _, err := client.Codespaces.ListUserCodespacesInOrg(ctx, "\n", "\n", opts) return err }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.ListUserCodespacesInOrg(ctx, "o1", "u1", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) } func TestEnterpriseService_DeleteUserCodespaceInOrg(t *testing.T) { From 232367bcf30d54246e2928b3b577eb4ad0f12ca0 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Wed, 24 Dec 2025 13:59:46 +0000 Subject: [PATCH 5/6] feedback --- github/codespaces_orgs.go | 10 +++++----- github/codespaces_orgs_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/github/codespaces_orgs.go b/github/codespaces_orgs.go index 5117e4b4d20..2b478de4bd1 100644 --- a/github/codespaces_orgs.go +++ b/github/codespaces_orgs.go @@ -10,8 +10,8 @@ import ( "fmt" ) -// CodespaceOrgAccessControlRequest represent request for SetOrgAccessControl. -type CodespaceOrgAccessControlRequest struct { +// CodespacesOrgAccessControlRequest represent request for SetOrgAccessControl. +type CodespacesOrgAccessControlRequest struct { // Visibility represent which users can access codespaces in the organization. // Can be one of: disabled, selected_members, all_members, all_members_and_outside_collaborators. Visibility string `json:"visibility"` @@ -51,7 +51,7 @@ func (s *CodespacesService) ListInOrg(ctx context.Context, org string, opts *Lis // GitHub API docs: https://docs.github.com/rest/codespaces/organizations#manage-access-control-for-organization-codespaces // //meta:operation PUT /orgs/{org}/codespaces/access -func (s *CodespacesService) SetOrgAccessControl(ctx context.Context, org string, request CodespaceOrgAccessControlRequest) (*Response, error) { +func (s *CodespacesService) SetOrgAccessControl(ctx context.Context, org string, request CodespacesOrgAccessControlRequest) (*Response, error) { u := fmt.Sprintf("orgs/%v/codespaces/access", org) req, err := s.client.NewRequest("PUT", u, request) if err != nil { @@ -66,7 +66,7 @@ func (s *CodespacesService) SetOrgAccessControl(ctx context.Context, org string, return resp, nil } -// AddUsersToOrgAccess adds users to Codespaces access for an organization +// AddUsersToOrgAccess adds users to Codespaces access for an organization. // // GitHub API docs: https://docs.github.com/rest/codespaces/organizations#add-users-to-codespaces-access-for-an-organization // @@ -86,7 +86,7 @@ func (s *CodespacesService) AddUsersToOrgAccess(ctx context.Context, org string, return resp, nil } -// RemoveUsersFromOrgAccess removes users from Codespaces access for an organization +// RemoveUsersFromOrgAccess removes users from Codespaces access for an organization. // // GitHub API docs: https://docs.github.com/rest/codespaces/organizations#remove-users-from-codespaces-access-for-an-organization // diff --git a/github/codespaces_orgs_test.go b/github/codespaces_orgs_test.go index cffed8d2113..c9b674b7215 100644 --- a/github/codespaces_orgs_test.go +++ b/github/codespaces_orgs_test.go @@ -64,7 +64,7 @@ func TestCodespacesService_SetOrgAccessControl(t *testing.T) { }) ctx := t.Context() - req := CodespaceOrgAccessControlRequest{ + req := CodespacesOrgAccessControlRequest{ Visibility: "selected_members", SelectedUsernames: []string{"u1", "u2"}, } From 381db677f794f8f923926f24fe34e01bad95d430 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Thu, 25 Dec 2025 11:17:44 +0000 Subject: [PATCH 6/6] use omitzero with SelectedUsernames --- github/codespaces_orgs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/codespaces_orgs.go b/github/codespaces_orgs.go index 2b478de4bd1..3aeb05f86cb 100644 --- a/github/codespaces_orgs.go +++ b/github/codespaces_orgs.go @@ -17,7 +17,7 @@ type CodespacesOrgAccessControlRequest struct { Visibility string `json:"visibility"` // SelectedUsernames represent the usernames of the organization members who should have access to codespaces in the organization. // Required when visibility is selected_members. - SelectedUsernames []string `json:"selected_usernames,omitempty"` + SelectedUsernames []string `json:"selected_usernames,omitzero"` } // ListInOrg lists the codespaces associated to a specified organization.