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).

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.

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

// for testing
GithubURL string `kong:"hidden,default='https://api.github.com'"`
UploadURL string `kong:"hidden,default='https://uploads.github.com'"`
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.

This seems unrelated. Are you addressing two issues in this PR?

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.

The old pattern allowed you to leave the client in an invalid state and would just error when you used an empty URL. The new pattern checks for errors early so there is no uncertainty later as to why a call failed.

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question on DownloadReleaseAsset redirect logic Refactor WithEnterpriseURLs helper

2 participants