diff --git a/github/github-accessors.go b/github/github-accessors.go index 4371d0ca274..7d38c90500a 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -10886,6 +10886,30 @@ func (i *IDPGroup) GetGroupName() string { return *i.GroupName } +// GetEnforcedRepositories returns the EnforcedRepositories field if it's non-nil, zero value otherwise. +func (i *ImmutableReleaseRepository) GetEnforcedRepositories() string { + if i == nil || i.EnforcedRepositories == nil { + return "" + } + return *i.EnforcedRepositories +} + +// GetEnforcedRepositories returns the EnforcedRepositories field if it's non-nil, zero value otherwise. +func (i *ImmutableReleaseSettings) GetEnforcedRepositories() string { + if i == nil || i.EnforcedRepositories == nil { + return "" + } + return *i.EnforcedRepositories +} + +// GetSelectedRepositoriesURL returns the SelectedRepositoriesURL field if it's non-nil, zero value otherwise. +func (i *ImmutableReleaseSettings) GetSelectedRepositoriesURL() string { + if i == nil || i.SelectedRepositoriesURL == nil { + return "" + } + return *i.SelectedRepositoriesURL +} + // GetAuthorsCount returns the AuthorsCount field if it's non-nil, zero value otherwise. func (i *Import) GetAuthorsCount() int { if i == nil || i.AuthorsCount == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 2aa09771e4a..1b5963e7014 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -14121,6 +14121,39 @@ func TestIDPGroup_GetGroupName(tt *testing.T) { i.GetGroupName() } +func TestImmutableReleaseRepository_GetEnforcedRepositories(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &ImmutableReleaseRepository{EnforcedRepositories: &zeroValue} + i.GetEnforcedRepositories() + i = &ImmutableReleaseRepository{} + i.GetEnforcedRepositories() + i = nil + i.GetEnforcedRepositories() +} + +func TestImmutableReleaseSettings_GetEnforcedRepositories(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &ImmutableReleaseSettings{EnforcedRepositories: &zeroValue} + i.GetEnforcedRepositories() + i = &ImmutableReleaseSettings{} + i.GetEnforcedRepositories() + i = nil + i.GetEnforcedRepositories() +} + +func TestImmutableReleaseSettings_GetSelectedRepositoriesURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &ImmutableReleaseSettings{SelectedRepositoriesURL: &zeroValue} + i.GetSelectedRepositoriesURL() + i = &ImmutableReleaseSettings{} + i.GetSelectedRepositoriesURL() + i = nil + i.GetSelectedRepositoriesURL() +} + func TestImport_GetAuthorsCount(tt *testing.T) { tt.Parallel() var zeroValue int diff --git a/github/orgs_immutable_releases.go b/github/orgs_immutable_releases.go new file mode 100644 index 00000000000..968b9d2b9ad --- /dev/null +++ b/github/orgs_immutable_releases.go @@ -0,0 +1,181 @@ +// 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. + +package github + +import ( + "context" + "fmt" +) + +// ImmutableReleaseSettings represents the response from the immutable releases settings endpoint. +type ImmutableReleaseSettings struct { + // EnforcedRepositories specifies how immutable releases are enforced in the organization. Possible values include "all", "none", or "selected". + EnforcedRepositories *string `json:"enforced_repositories,omitempty"` + // SelectedRepositoriesURL provides the API URL for managing the repositories + // selected for immutable releases enforcement when EnforcedRepositories is set to "selected". + SelectedRepositoriesURL *string `json:"selected_repositories_url,omitempty"` +} + +// ImmutableReleaseRepository is for setting the immutable releases policy for repositories in an organization. +type ImmutableReleaseRepository struct { + // EnforcedRepositories specifies how immutable releases are enforced in the organization. Possible values include "all", "none", or "selected". + EnforcedRepositories *string `json:"enforced_repositories,omitempty"` + // An array of repository ids for which immutable releases enforcement should be applied. + // You can only provide a list of repository ids when the enforced_repositories is set to "selected" + SelectedRepositoryIDs []int64 `json:"selected_repository_ids,omitempty"` +} + +// setImmutableReleasesRepositoriesOptions represents the request body for setting repositories. +type setImmutableReleasesRepositoriesOptions struct { + SelectedRepositoryIDs []int64 `json:"selected_repository_ids"` +} + +// GetImmutableReleasesSettings gets immutable releases settings for an organization. +// +// This endpoint returns the immutable releases configuration that applies to repositories within the given organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/orgs#get-immutable-releases-settings-for-an-organization +// +//meta:operation GET /orgs/{org}/settings/immutable-releases +func (s *OrganizationsService) GetImmutableReleasesSettings(ctx context.Context, org string) (*ImmutableReleaseSettings, *Response, error) { + u := fmt.Sprintf("orgs/%v/settings/immutable-releases", org) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var settings *ImmutableReleaseSettings + resp, err := s.client.Do(ctx, req, &settings) + if err != nil { + return nil, resp, err + } + + return settings, resp, nil +} + +// UpdateImmutableReleasesSettings sets immutable releases settings for an organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/orgs#set-immutable-releases-settings-for-an-organization +// +//meta:operation PUT /orgs/{org}/settings/immutable-releases +func (s *OrganizationsService) UpdateImmutableReleasesSettings(ctx context.Context, org string, opts ImmutableReleaseRepository) (*Response, error) { + u := fmt.Sprintf("orgs/%v/settings/immutable-releases", org) + + req, err := s.client.NewRequest("PUT", u, opts) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// ListImmutableReleaseRepositories lists selected repositories for immutable releases enforcement. +// +// This endpoint gives a list of all the repositories that have been selected for immutable releases enforcement in an organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/orgs#list-selected-repositories-for-immutable-releases-enforcement +// +//meta:operation GET /orgs/{org}/settings/immutable-releases/repositories +func (s *OrganizationsService) ListImmutableReleaseRepositories(ctx context.Context, org string, opts *ListOptions) (*ListRepositories, *Response, error) { + u := fmt.Sprintf("orgs/%v/settings/immutable-releases/repositories", 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 repositories *ListRepositories + resp, err := s.client.Do(ctx, req, &repositories) + if err != nil { + return nil, resp, err + } + + return repositories, resp, nil +} + +// SetImmutableReleaseRepositories sets selected repositories for immutable releases enforcement. +// +// Replaces all repositories selected for immutable releases enforcement in an organization. Requires the organization's immutable releases policy for enforced_repositories to be set to "selected". +// +// GitHub API docs: https://docs.github.com/rest/orgs/orgs#set-selected-repositories-for-immutable-releases-enforcement +// +//meta:operation PUT /orgs/{org}/settings/immutable-releases/repositories +func (s *OrganizationsService) SetImmutableReleaseRepositories(ctx context.Context, org string, repositoryIDs []int64) (*Response, error) { + u := fmt.Sprintf("orgs/%v/settings/immutable-releases/repositories", org) + + body := &setImmutableReleasesRepositoriesOptions{ + SelectedRepositoryIDs: repositoryIDs, + } + + req, err := s.client.NewRequest("PUT", u, body) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// EnableRepositoryForImmutableRelease enables a selected repository for immutable releases in an organization. +// +// Adds a repository to the organization's selected list for immutable releases enforcement (requires enforced_repositories set to "selected"). +// +// GitHub API docs: https://docs.github.com/rest/orgs/orgs#enable-a-selected-repository-for-immutable-releases-in-an-organization +// +//meta:operation PUT /orgs/{org}/settings/immutable-releases/repositories/{repository_id} +func (s *OrganizationsService) EnableRepositoryForImmutableRelease(ctx context.Context, org string, repoID int64) (*Response, error) { + u := fmt.Sprintf("orgs/%v/settings/immutable-releases/repositories/%v", org, repoID) + + req, err := s.client.NewRequest("PUT", 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 +} + +// DisableRepositoryForImmutableRelease disables a selected repository for immutable releases in an organization. +// +// Removes a repository from the organization's selected list for immutable releases enforcement (requires enforced_repositories set to "selected"). +// +// GitHub API docs: https://docs.github.com/rest/orgs/orgs#disable-a-selected-repository-for-immutable-releases-in-an-organization +// +//meta:operation DELETE /orgs/{org}/settings/immutable-releases/repositories/{repository_id} +func (s *OrganizationsService) DisableRepositoryForImmutableRelease(ctx context.Context, org string, repoID int64) (*Response, error) { + u := fmt.Sprintf("orgs/%v/settings/immutable-releases/repositories/%v", org, repoID) + + 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 +} diff --git a/github/orgs_immutable_releases_test.go b/github/orgs_immutable_releases_test.go new file mode 100644 index 00000000000..eeb76e89f4f --- /dev/null +++ b/github/orgs_immutable_releases_test.go @@ -0,0 +1,266 @@ +// 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. + +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestOrganizationsService_GetImmutableReleasesSettings(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/settings/immutable-releases", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "enforced_repositories": "selected", + "selected_repositories_url": "https://api.github.com/orgs/o/r" + }`) + }) + + ctx := t.Context() + settings, _, err := client.Organizations.GetImmutableReleasesSettings(ctx, "o") + if err != nil { + t.Errorf("Organizations.GetImmutableReleasesSettings returned error: %v", err) + } + + wantURL := "https://api.github.com/orgs/o/r" + want := &ImmutableReleaseSettings{ + EnforcedRepositories: Ptr("selected"), + SelectedRepositoriesURL: &wantURL, + } + + if !cmp.Equal(settings, want) { + t.Errorf("Organizations.GetImmutableReleasesSettings returned %+v, want %+v", settings, want) + } + + const methodName = "GetImmutableReleasesSettings" + + testBadOptions(t, methodName, func() error { + _, _, err := client.Organizations.GetImmutableReleasesSettings(ctx, "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.GetImmutableReleasesSettings(ctx, "o") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestOrganizationsService_UpdateImmutableReleasesSettings(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := ImmutableReleaseRepository{ + EnforcedRepositories: Ptr("selected"), + } + + mux.HandleFunc("/orgs/o/settings/immutable-releases", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + + var gotBody map[string]any + assertNilError(t, json.NewDecoder(r.Body).Decode(&gotBody)) + + wantBody := map[string]any{ + "enforced_repositories": "selected", + } + + if !cmp.Equal(gotBody, wantBody) { + t.Errorf("Request body = %+v, want %+v", gotBody, wantBody) + } + + w.WriteHeader(http.StatusNoContent) + fmt.Fprint(w, `{"enforced_repositories":"selected"}`) + }) + + ctx := t.Context() + resp, err := client.Organizations.UpdateImmutableReleasesSettings(ctx, "o", input) + if err != nil { + t.Errorf("Organizations.UpdateImmutableReleasesSettings returned error: %v", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Expected status 204 No Content, got %v", resp.StatusCode) + } + + const methodName = "UpdateImmutableReleasesSettings" + + testBadOptions(t, methodName, func() error { + _, err := client.Organizations.UpdateImmutableReleasesSettings(ctx, "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + resp, err := client.Organizations.UpdateImmutableReleasesSettings(ctx, "o", input) + return resp, err + }) +} + +func TestOrganizationsService_ListImmutableReleaseRepositories(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + responseBody := `{ + "total_count": 2, + "repositories": [ + {"id": 1, "name": "repo1"}, + {"id": 2, "name": "repo2"} + ] + }` + + mux.HandleFunc("/orgs/o/settings/immutable-releases/repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, responseBody) + }) + + ctx := t.Context() + opts := &ListOptions{Page: 1, PerPage: 10} + repos, _, err := client.Organizations.ListImmutableReleaseRepositories(ctx, "o", opts) + if err != nil { + t.Errorf("Organizations.ListImmutableReleaseRepositories returned error: %v", err) + } + + want := &ListRepositories{ + TotalCount: Ptr(2), + Repositories: []*Repository{ + {ID: Ptr(int64(1)), Name: Ptr("repo1")}, + {ID: Ptr(int64(2)), Name: Ptr("repo2")}, + }, + } + + if !cmp.Equal(repos, want) { + t.Errorf("Organizations.ListImmutableReleaseRepositories returned %+v, want %+v", repos, want) + } + + const methodName = "ListImmutableReleaseRepositories" + + testBadOptions(t, methodName, func() error { + _, _, err := client.Organizations.ListImmutableReleaseRepositories(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.ListImmutableReleaseRepositories(ctx, "o", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestOrganizationsService_SetImmutableReleaseRepositories(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := []int64{1, 2, 3} + mux.HandleFunc("/orgs/o/settings/immutable-releases/repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + + var gotBody setImmutableReleasesRepositoriesOptions + if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { + t.Fatalf("Failed to decode request body: %v", err) + } + + if !cmp.Equal(gotBody.SelectedRepositoryIDs, input) { + t.Errorf("Request body = %+v, want %+v", gotBody.SelectedRepositoryIDs, input) + } + + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Organizations.SetImmutableReleaseRepositories(ctx, "o", input) + if err != nil { + t.Fatalf("SetImmutableReleaseRepositories returned error: %v", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Expected status 204 No Content, got %v", resp.StatusCode) + } + + const methodName = "SetImmutableReleaseRepositories" + + testBadOptions(t, methodName, func() error { + _, err := client.Organizations.SetImmutableReleaseRepositories(ctx, "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Organizations.SetImmutableReleaseRepositories(ctx, "o", input) + }) +} + +func TestOrganizationsService_EnableRepositoryForImmutableRelease(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + repoID := int64(42) + + mux.HandleFunc(fmt.Sprintf("/orgs/o/settings/immutable-releases/repositories/%v", repoID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Organizations.EnableRepositoryForImmutableRelease(ctx, "o", repoID) + if err != nil { + t.Errorf("EnableRepositoryForImmutableRelease returned error: %v", err) + } + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Expected status 204 No Content, got %v", resp.StatusCode) + } + + const methodName = "EnableRepositoryForImmutableRelease" + + testBadOptions(t, methodName, func() error { + _, err := client.Organizations.EnableRepositoryForImmutableRelease(ctx, "o", -1) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Organizations.EnableRepositoryForImmutableRelease(ctx, "o", repoID) + }) +} + +func TestOrganizationsService_DisableRepositoryForImmutableRelease(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + repoID := int64(42) + + mux.HandleFunc(fmt.Sprintf("/orgs/o/settings/immutable-releases/repositories/%v", repoID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Organizations.DisableRepositoryForImmutableRelease(ctx, "o", repoID) + if err != nil { + t.Errorf("DisableRepositoryForImmutableRelease returned error: %v", err) + } + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Expected status 204 No Content, got %v", resp.StatusCode) + } + + const methodName = "DisableRepositoryForImmutableRelease" + + testBadOptions(t, methodName, func() error { + _, err := client.Organizations.DisableRepositoryForImmutableRelease(ctx, "o", -1) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Organizations.DisableRepositoryForImmutableRelease(ctx, "o", repoID) + }) +}