Skip to content

feat!: Refactor client constructor to use options pattern#4201

Open
stevehipwell wants to merge 1 commit intogoogle:masterfrom
stevehipwell:client-options-pattern
Open

feat!: Refactor client constructor to use options pattern#4201
stevehipwell wants to merge 1 commit intogoogle:masterfrom
stevehipwell:client-options-pattern

Conversation

@stevehipwell
Copy link
Copy Markdown
Contributor

This PR refactors the client to be immutable and to use the with options pattern for construction. This is a significant change but it should reduce the overall complexity and support better UX around new capabilities and backwards compatibility (e.g. github.WithAPIVersion("2026-03-10") or github.WithGHESVersion("3.15")).

  • Client is immutable
    • NewClient is the only way to create a new Client
    • After a Client is created if you need to create a new one you can use Clone
    • You no longer need to create multiple clients and their associated child structs to chain helpers
  • Client configuration is explicit
    • Options are handled in a specific order to limit the potential for misconfiguration
    • Passing invalid values into NewClient causes an early error instead of waiting for a failure or silently doing nothing
  • Fixed implementation bugs
    • clientMu was unnecessary
    • Proxy from env pattern lost default transport configuration
  • Improved test coverage?

Closes #3870
Closes #3915

Signed-off-by: Steve Hipwell <steve.hipwell@gmail.com>
@gmlewis gmlewis changed the title feat!: refactor client constructor to use options pattern feat!: Refactor client constructor to use options pattern May 8, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

❌ Patch coverage is 67.16418% with 88 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.48%. Comparing base (6c58592) to head (7e6779b).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
github/github.go 93.04% 8 Missing and 5 partials ⚠️
test/fields/fields.go 0.00% 10 Missing ⚠️
example/otel/main.go 0.00% 7 Missing ⚠️
example/newfilewithappauth/main.go 0.00% 5 Missing ⚠️
example/basicauth/main.go 0.00% 4 Missing ⚠️
example/commitpr/main.go 0.00% 4 Missing ⚠️
example/contents/main.go 0.00% 4 Missing ⚠️
example/ratelimit/main.go 0.00% 4 Missing ⚠️
example/actionpermissions/main.go 0.00% 3 Missing ⚠️
...xample/codespaces/newreposecretwithxcrypto/main.go 0.00% 3 Missing ⚠️
... and 11 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4201      +/-   ##
==========================================
- Coverage   93.71%   93.48%   -0.23%     
==========================================
  Files         209      209              
  Lines       19772    19885     +113     
==========================================
+ Hits        18529    18590      +61     
- Misses       1046     1094      +48     
- Partials      197      201       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Collaborator

@gmlewis gmlewis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @stevehipwell.

Although I like the overall pattern, I have a few concerns with this PR.
First, the various fields within Client were originally exported individually, on a case-by-case basis, as users had need to access them. Suddenly unexporting all fields may wreak havoc.

Second, the clientMu was indeed needed to solve a race condition found when copying clients that were currently in-flight, if I'm remembering correctly.

Can we keep the nice builder pattern you've created here without unexporting all the fields and without removing the mutex that is protecting the client?

Comment thread example/basicauth/main.go
Comment on lines +44 to +45
fmt.Printf("\nerror: %v\n", err)
return
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("\nerror: %v\n", err)
return
log.Fatalf("error: %v", err)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally kept the existing pattern for the individual examples rather than standardising them all.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, let's please clean them all up and standardize them since you are touching every one of these in this PR.

Comment thread example/contents/main.go
Comment on lines +55 to +56
fmt.Printf("Error creating GitHub client: %v\n", err)
os.Exit(1)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("Error creating GitHub client: %v\n", err)
os.Exit(1)
log.Fatalf("Error creating GitHub client: %v", err)

Comment thread example/ratelimit/main.go
Comment on lines +42 to +43
fmt.Printf("Error creating GitHub client: %v\n", err)
return
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("Error creating GitHub client: %v\n", err)
return
log.Fatalf("Error creating GitHub client: %v", err)

Comment thread example/tokenauth/main.go
client := github.NewClient(nil).WithAuthToken(string(token))
client, err := github.NewClient(github.WithAuthToken(string(token)))
if err != nil {
log.Fatalf("Error creating GitHub client: %v\n", err)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Fatalf("Error creating GitHub client: %v\n", err)
log.Fatalf("Error creating GitHub client: %v", err)

client := github.NewClient(nil).WithAuthToken(token)
client, err := github.NewClient(github.WithAuthToken(token))
if err != nil {
log.Fatalf("Error creating GitHub client: %v\n", err)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Fatalf("Error creating GitHub client: %v\n", err)
log.Fatalf("Error creating GitHub client: %v", err)

Comment thread test/fields/fields.go
Comment on lines +45 to +46
fmt.Printf("Error creating GitHub client: %v\n", err)
os.Exit(1)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("Error creating GitHub client: %v\n", err)
os.Exit(1)
log.Fatalf("Error creating GitHub client: %v", err)

Comment thread test/fields/fields.go
Comment on lines +52 to +53
fmt.Printf("Error creating GitHub client with token: %v\n", err)
os.Exit(1)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("Error creating GitHub client with token: %v\n", err)
os.Exit(1)
log.Fatalf("Error creating GitHub client with token: %v", err)

Comment on lines +29 to +30
fmt.Printf("Error creating GitHub client: %v\n", err)
os.Exit(1)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("Error creating GitHub client: %v\n", err)
os.Exit(1)
log.Fatalf("Error creating GitHub client: %v", err)

Comment on lines +37 to +38
fmt.Printf("Error creating GitHub client with token: %v\n", err)
os.Exit(1)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("Error creating GitHub client with token: %v\n", err)
os.Exit(1)
log.Fatalf("Error creating GitHub client with token: %v", err)

Comment thread tools/metadata/main.go
@stevehipwell
Copy link
Copy Markdown
Contributor Author

@gmlewis if you look at the code you'll see that clientMu is doing nothing and doesn't even wrap the relevant concerns in the copy function. It used to technically serve a purpose when some of the functions mutated the client, but that no longer happens, although as I said above it wasn't "correct" even for that. I also double checked this with 2 different agents, which both agreed with my summary above.

Unexporting the values and putting them behind the options pattern to make the client immutable is core I the whole pattern. Could you provide some examples where these need changing and Clone is the wrong choice? For example I added the disable rate limit option to the client, it's only exported as there wasn't an existing pattern so that was the simplest approach. I'm happy to add getters for the remaining unexpired fields, but I wasn't sure they were actually needed.

@gmlewis
Copy link
Copy Markdown
Collaborator

gmlewis commented May 9, 2026

@gmlewis if you look at the code you'll see that clientMu is doing nothing and doesn't even wrap the relevant concerns in the copy function. It used to technically serve a purpose when some of the functions mutated the client, but that no longer happens, although as I said above it wasn't "correct" even for that. I also double checked this with 2 different agents, which both agreed with my summary above.

Unexporting the values and putting them behind the options pattern to make the client immutable is core I the whole pattern. Could you provide some examples where these need changing and Clone is the wrong choice? For example I added the disable rate limit option to the client, it's only exported as there wasn't an existing pattern so that was the simplest approach. I'm happy to add getters for the remaining unexpired fields, but I wasn't sure they were actually needed.

OK, I see. That sounds good to me.

If you wouldn't mind please cleaning up all the non-idiomatic failures in the examples where I have marked them, that would be greatly appreciated, then we should be ready for a second LGTM+Approval.

@gmlewis
Copy link
Copy Markdown
Collaborator

gmlewis commented May 9, 2026

If you could also please investigate the 8 missed lines in code coverage in github/github.go in this PR, that would also be great. Thanks!

@gmlewis gmlewis added NeedsReview PR is awaiting a review before merging. Breaking API Change PR will require a bump to the major version num in next release. Look here to see the change(s). labels May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Breaking API Change PR will require a bump to the major version num in next release. Look here to see the change(s). NeedsReview PR is awaiting a review before merging.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question on DownloadReleaseAsset redirect logic Refactor WithEnterpriseURLs helper

2 participants