Repository: google/go-github
Pull request #3489 (2025-02-21 19:19:23)

fix: Resolve '400 Custom domains' error on GitHub Enterprise Server
This PR fixes an issue with the UpdatePages method, which caused the error: "400 Custom domains are not available for GitHub Pages" on GHES. The issue occurred because the update struct didn't specify the omitempty tag for JSON marshalling, which resulted in explicit null values being sent. As a result, GHES returned the error.

To resolve this, the fix adds a check for the client URL. If the URL differs from the public GitHub API, a new struct is created without the CNAME field to avoid sending null values.

Question: Should we add the omitempty tag directly to the CNAME field of the PagesUpdate struct instead of this workaround?

References:
- Issue: https://github.com/google/go-github/issues/3466

Fixes: #3466.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/google/go-github/pull/3489?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
Attention: Patch coverage is `22.58065%` with `24 lines` in your changes missing coverage. Please review.
> Project coverage is 90.89%. Comparing base [(`868f897`)](https://app.codecov.io/gh/google/go-github/commit/868f897f6d9dcfc74bb954a092a51e6384d84b49?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) to head [(`8767703`)](https://app.codecov.io/gh/google/go-github/commit/8767703f85a5f743a9317e2e6d8615d3b3c91541?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).

| [Files with missing lines](https://app.codecov.io/gh/google/go-github/pull/3489?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) | Patch % | Lines |
|---|---|---|
| [github/repos\_pages.go](https://app.codecov.io/gh/google/go-github/pull/3489?src=pr&el=tree&filepath=github%2Frepos_pages.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL3JlcG9zX3BhZ2VzLmdv) | 22.58% | [20 Missing and 4 partials :warning: ](https://app.codecov.io/gh/google/go-github/pull/3489?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3489      +/-   ##
==========================================
- Coverage   91.02%   90.89%   -0.14%     
==========================================
  Files         179      179              
  Lines       15561    15590      +29     
==========================================
+ Hits        14165    14170       +5     
- Misses       1223     1243      +20     
- Partials      173      177       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/google/go-github/pull/3489?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).

gmlewis: What happens when a person wishes to remove a custom domain?

When this happens for other endpoints (where sending a "null" is important and has a specific meaning), we often introduce a second endpoint that has serves the other usage, and whose name highlights the difference between the two use cases.

I think that might be appropriate here.  

If you look around, you should be able to find some endpoints like that, but if you can't, let me know and I'll dig around for examples of this.
atilsensalduz: > What happens when a person wishes to remove a custom domain?
> 
> When this happens for other endpoints (where sending a "null" is important and has a specific meaning), we often introduce a second endpoint that has serves the other usage, and whose name highlights the difference between the two use cases.
> 
> I think that might be appropriate here.
> 
> If you look around, you should be able to find some endpoints like that, but if you can't, let me know and I'll dig around for examples of this.

You're absolutely right. Thanks for guiding me toward the better approach. I've updated the PR to use a separate endpoint for updating pages specifically for GHES and to use a struct without the CNAME field to avoid the 400 error.
atilsensalduz: > Whups, I spoke too soon. Please fix the lint issues by following step 4 of CONTRIBUTING.md.

Ah, sorry about that! I must have missed it. 🤦  Thanks for the heads-up! I've fixed the lint issues now. I really appreciate your review and support! @gmlewis 

Patch:
diff --git a/example/go.sum b/example/go.sum
index 9fc9663b..d99bb0d3 100644
--- a/example/go.sum
+++ b/example/go.sum
@@ -154,8 +154,8 @@ github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGk
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
 github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
 github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
diff --git a/example/newreposecretwithlibsodium/go.sum b/example/newreposecretwithlibsodium/go.sum
index a1345cce..bb0a7c88 100644
--- a/example/newreposecretwithlibsodium/go.sum
+++ b/example/newreposecretwithlibsodium/go.sum
@@ -1,8 +1,8 @@
 github.com/GoKillers/libsodium-go v0.0.0-20171022220152-dd733721c3cb h1:ilqSFSbR1fq6x88heeHrvAqlg+ES+tZk2ZcaCmiH1gI=
 github.com/GoKillers/libsodium-go v0.0.0-20171022220152-dd733721c3cb/go.mod h1:72TQeEkiDH9QMXZa5nJJvZre0UjqqO67X2QEIoOwCRU=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 6270bc1f..085e6e59 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -16734,6 +16734,38 @@ func (p *PagesUpdate) GetSource() *PagesSource {
 	return p.Source
 }
 
+// GetBuildType returns the BuildType field if it's non-nil, zero value otherwise.
+func (p *PagesUpdateWithoutCNAME) GetBuildType() string {
+	if p == nil || p.BuildType == nil {
+		return ""
+	}
+	return *p.BuildType
+}
+
+// GetHTTPSEnforced returns the HTTPSEnforced field if it's non-nil, zero value otherwise.
+func (p *PagesUpdateWithoutCNAME) GetHTTPSEnforced() bool {
+	if p == nil || p.HTTPSEnforced == nil {
+		return false
+	}
+	return *p.HTTPSEnforced
+}
+
+// GetPublic returns the Public field if it's non-nil, zero value otherwise.
+func (p *PagesUpdateWithoutCNAME) GetPublic() bool {
+	if p == nil || p.Public == nil {
+		return false
+	}
+	return *p.Public
+}
+
+// GetSource returns the Source field.
+func (p *PagesUpdateWithoutCNAME) GetSource() *PagesSource {
+	if p == nil {
+		return nil
+	}
+	return p.Source
+}
+
 // GetName returns the Name field if it's non-nil, zero value otherwise.
 func (p *PatternRuleParameters) GetName() string {
 	if p == nil || p.Name == nil {
diff --git a/github/repos_pages.go b/github/repos_pages.go
index 6b9ba76e..930f6000 100644
--- a/github/repos_pages.go
+++ b/github/repos_pages.go
@@ -170,7 +170,36 @@ func (s *RepositoriesService) UpdatePages(ctx context.Context, owner, repo strin
 	if err != nil {
 		return resp, err
 	}
+	return resp, nil
+}
+
+// PagesUpdateWithoutCNAME defines parameters for updating a GitHub Pages site on GitHub Enterprise Servers.
+// Sending a request with a CNAME (any value, empty string, or null) results in a 400 error: "Custom domains are not available for GitHub Pages".
+type PagesUpdateWithoutCNAME struct {
+	BuildType     *string      `json:"build_type,omitempty"`
+	Source        *PagesSource `json:"source,omitempty"`
+	Public        *bool        `json:"public,omitempty"`
+	HTTPSEnforced *bool        `json:"https_enforced,omitempty"`
+}
+
+// UpdatePagesGHES updates GitHub Pages for the named repo in GitHub Enterprise Servers.
+//
+// GitHub API docs: https://docs.github.com/rest/pages/pages#update-information-about-a-github-pages-site
+//
+//meta:operation PUT /repos/{owner}/{repo}/pages
+func (s *RepositoriesService) UpdatePagesGHES(ctx context.Context, owner, repo string, opts *PagesUpdateWithoutCNAME) (*Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/pages", owner, repo)
 
+	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
 }
 
diff --git a/go.mod b/go.mod
index b08ff981..25e0e7b4 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,6 @@ module github.com/google/go-github/v69
 go 1.22.0
 
 require (
-	github.com/google/go-cmp v0.6.0
+	github.com/google/go-cmp v0.7.0
 	github.com/google/go-querystring v1.1.0
 )
diff --git a/go.sum b/go.sum
index b2abdf11..831bfd79 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/scrape/go.mod b/scrape/go.mod
index e3da742a..82db6307 100644
--- a/scrape/go.mod
+++ b/scrape/go.mod
@@ -4,7 +4,7 @@ go 1.22.0
 
 require (
 	github.com/PuerkitoBio/goquery v1.9.3
-	github.com/google/go-cmp v0.6.0
+	github.com/google/go-cmp v0.7.0
 	github.com/google/go-github/v69 v69.2.0
 	github.com/xlzd/gotp v0.1.0
 	golang.org/x/net v0.35.0
diff --git a/scrape/go.sum b/scrape/go.sum
index ca75cda0..f0eddaf0 100644
--- a/scrape/go.sum
+++ b/scrape/go.sum
@@ -3,8 +3,9 @@ github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G
 github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
 github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
 github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
diff --git a/tools/go.mod b/tools/go.mod
index 212311e0..c9ce43d3 100644
--- a/tools/go.mod
+++ b/tools/go.mod
@@ -5,7 +5,7 @@ go 1.22.0
 require (
 	github.com/alecthomas/kong v1.8.1
 	github.com/getkin/kin-openapi v0.128.0
-	github.com/google/go-cmp v0.6.0
+	github.com/google/go-cmp v0.7.0
 	github.com/google/go-github/v69 v69.2.0
 	golang.org/x/sync v0.11.0
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/tools/go.sum b/tools/go.sum
index 3f4813ed..ecbe0ac6 100644
--- a/tools/go.sum
+++ b/tools/go.sum
@@ -15,8 +15,8 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ
 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 16c021bc..d3125e80 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -54,7 +54,7 @@ jobs:
           echo "go-mod-cache=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
 
       - name: Cache go modules
-        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+        uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
         with:
           path: |
             ${{ steps.cache-paths.outputs.go-cache }}
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index b3be0c11..094387db 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -21709,6 +21709,47 @@ func TestPagesUpdate_GetSource(tt *testing.T) {
 	p.GetSource()
 }
 
+func TestPagesUpdateWithoutCNAME_GetBuildType(tt *testing.T) {
+	tt.Parallel()
+	var zeroValue string
+	p := &PagesUpdateWithoutCNAME{BuildType: &zeroValue}
+	p.GetBuildType()
+	p = &PagesUpdateWithoutCNAME{}
+	p.GetBuildType()
+	p = nil
+	p.GetBuildType()
+}
+
+func TestPagesUpdateWithoutCNAME_GetHTTPSEnforced(tt *testing.T) {
+	tt.Parallel()
+	var zeroValue bool
+	p := &PagesUpdateWithoutCNAME{HTTPSEnforced: &zeroValue}
+	p.GetHTTPSEnforced()
+	p = &PagesUpdateWithoutCNAME{}
+	p.GetHTTPSEnforced()
+	p = nil
+	p.GetHTTPSEnforced()
+}
+
+func TestPagesUpdateWithoutCNAME_GetPublic(tt *testing.T) {
+	tt.Parallel()
+	var zeroValue bool
+	p := &PagesUpdateWithoutCNAME{Public: &zeroValue}
+	p.GetPublic()
+	p = &PagesUpdateWithoutCNAME{}
+	p.GetPublic()
+	p = nil
+	p.GetPublic()
+}
+
+func TestPagesUpdateWithoutCNAME_GetSource(tt *testing.T) {
+	tt.Parallel()
+	p := &PagesUpdateWithoutCNAME{}
+	p.GetSource()
+	p = nil
+	p.GetSource()
+}
+
 func TestPatternRuleParameters_GetName(tt *testing.T) {
 	tt.Parallel()
 	var zeroValue string
diff --git a/github/repos_pages_test.go b/github/repos_pages_test.go
index 3c139d99..31a2ee1b 100644
--- a/github/repos_pages_test.go
+++ b/github/repos_pages_test.go
@@ -200,6 +200,44 @@ func TestRepositoriesService_UpdatePagesWorkflow(t *testing.T) {
 	})
 }
 
+func TestRepositoriesService_UpdatePagesGHES(t *testing.T) {
+	t.Parallel()
+	client, mux, _ := setup(t)
+
+	input := &PagesUpdateWithoutCNAME{
+		BuildType: Ptr("workflow"),
+	}
+
+	mux.HandleFunc("/repos/o/r/pages", func(w http.ResponseWriter, r *http.Request) {
+		v := new(PagesUpdate)
+		assertNilError(t, json.NewDecoder(r.Body).Decode(v))
+
+		testMethod(t, r, "PUT")
+		want := &PagesUpdate{BuildType: Ptr("workflow")}
+		if !cmp.Equal(v, want) {
+			t.Errorf("Request body = %+v, want %+v", v, want)
+		}
+
+		fmt.Fprint(w, `{"build_type":"workflow"}`)
+	})
+
+	ctx := context.Background()
+	_, err := client.Repositories.UpdatePagesGHES(ctx, "o", "r", input)
+	if err != nil {
+		t.Errorf("Repositories.UpdatePagesGHES returned error: %v", err)
+	}
+
+	const methodName = "UpdatePagesGHES"
+	testBadOptions(t, methodName, func() (err error) {
+		_, err = client.Repositories.UpdatePagesGHES(ctx, "\n", "\n", input)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		return client.Repositories.UpdatePagesGHES(ctx, "o", "r", input)
+	})
+}
+
 func TestRepositoriesService_UpdatePages_NullCNAME(t *testing.T) {
 	t.Parallel()
 	client, mux, _ := setup(t)