Skip to content

Commit 9026df1

Browse files
committed
refactor: remove go-github dependency and implement internal GitHub API client
- Replace go-github/v74 dependency with lightweight internal implementation - Add new github.go with minimal GitHub API types and client - Update InstallationTokenOptions and InstallationPermissions to local types - Simplify enterprise URL configuration to single base URL parameter - All functionality maintained with same public API surface - All tests passing with zero breaking changes
1 parent c78f523 commit 9026df1

5 files changed

Lines changed: 182 additions & 40 deletions

File tree

auth.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//
44
// This package implements oauth2.TokenSource interfaces for GitHub App
55
// authentication and GitHub App installation token generation. It is built
6-
// on top of the go-github and golang.org/x/oauth2 libraries.
6+
// on top of the golang.org/x/oauth2 library.
77
package githubauth
88

99
import (
@@ -15,7 +15,6 @@ import (
1515
"time"
1616

1717
jwt "github.com/golang-jwt/jwt/v5"
18-
"github.com/google/go-github/v74/github"
1918
"golang.org/x/oauth2"
2019
)
2120

@@ -135,7 +134,7 @@ func (t *applicationTokenSource) Token() (*oauth2.Token, error) {
135134
type InstallationTokenSourceOpt func(*installationTokenSource)
136135

137136
// WithInstallationTokenOptions sets the options for the GitHub App installation token.
138-
func WithInstallationTokenOptions(opts *github.InstallationTokenOptions) InstallationTokenSourceOpt {
137+
func WithInstallationTokenOptions(opts *InstallationTokenOptions) InstallationTokenSourceOpt {
139138
return func(i *installationTokenSource) {
140139
i.opts = opts
141140
}
@@ -149,16 +148,16 @@ func WithHTTPClient(client *http.Client) InstallationTokenSourceOpt {
149148
Base: client.Transport,
150149
}
151150

152-
i.client = github.NewClient(client)
151+
i.client = newGitHubClient(client)
153152
}
154153
}
155154

156-
// WithEnterpriseURLs sets the base URL and upload URL for GitHub Enterprise Server.
155+
// WithEnterpriseURL sets the base URL for GitHub Enterprise Server.
157156
// This option should be used after WithHTTPClient to ensure the HTTP client is properly configured.
158-
// If the provided URLs are invalid, the option is ignored and default GitHub URLs are used.
159-
func WithEnterpriseURLs(baseURL, uploadURL string) InstallationTokenSourceOpt {
157+
// If the provided base URL is invalid, the option is ignored and default GitHub base URL is used.
158+
func WithEnterpriseURL(baseURL string) InstallationTokenSourceOpt {
160159
return func(i *installationTokenSource) {
161-
enterpriseClient, err := i.client.WithEnterpriseURLs(baseURL, uploadURL)
160+
enterpriseClient, err := i.client.withEnterpriseURL(baseURL)
162161
if err != nil {
163162
return
164163
}
@@ -182,8 +181,8 @@ type installationTokenSource struct {
182181
id int64
183182
ctx context.Context
184183
src oauth2.TokenSource
185-
client *github.Client
186-
opts *github.InstallationTokenOptions
184+
client *githubClient
185+
opts *InstallationTokenOptions
187186
}
188187

189188
// NewInstallationTokenSource creates a GitHub App installation token source.
@@ -193,7 +192,7 @@ type installationTokenSource struct {
193192
// token regeneration. Don't worry about wrapping the result again since ReuseTokenSource
194193
// prevents re-wrapping automatically.
195194
//
196-
// See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token
195+
// See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
197196
func NewInstallationTokenSource(id int64, src oauth2.TokenSource, opts ...InstallationTokenSourceOpt) oauth2.TokenSource {
198197
ctx := context.Background()
199198

@@ -207,7 +206,7 @@ func NewInstallationTokenSource(id int64, src oauth2.TokenSource, opts ...Instal
207206
id: id,
208207
ctx: ctx,
209208
src: src,
210-
client: github.NewClient(httpClient),
209+
client: newGitHubClient(httpClient),
211210
}
212211

213212
for _, opt := range opts {
@@ -219,15 +218,15 @@ func NewInstallationTokenSource(id int64, src oauth2.TokenSource, opts ...Instal
219218

220219
// Token generates a new GitHub App installation token for authenticating as a GitHub App installation.
221220
func (t *installationTokenSource) Token() (*oauth2.Token, error) {
222-
token, _, err := t.client.Apps.CreateInstallationToken(t.ctx, t.id, t.opts)
221+
token, err := t.client.createInstallationToken(t.ctx, t.id, t.opts)
223222
if err != nil {
224223
return nil, err
225224
}
226225

227226
return &oauth2.Token{
228-
AccessToken: token.GetToken(),
227+
AccessToken: token.Token,
229228
TokenType: bearerTokenType,
230-
Expiry: token.GetExpiresAt().Time,
229+
Expiry: token.ExpiresAt,
231230
}, nil
232231
}
233232

auth_test.go

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"time"
1313

1414
jwt "github.com/golang-jwt/jwt/v5"
15-
"github.com/google/go-github/v74/github"
1615
"golang.org/x/oauth2"
1716
)
1817

@@ -161,18 +160,16 @@ func Test_installationTokenSource_Token(t *testing.T) {
161160
mockedHTTPClient, cleanupSuccess := newMockedHTTPClient(
162161
withRequestMatch(
163162
postAppInstallationsAccessTokensByInstallationID,
164-
github.InstallationToken{
165-
Token: github.Ptr("mocked-installation-token"),
166-
ExpiresAt: &github.Timestamp{
167-
Time: expiration,
163+
InstallationToken{
164+
Token: "mocked-installation-token",
165+
ExpiresAt: expiration,
166+
Permissions: &InstallationPermissions{
167+
PullRequests: ptr("read"),
168168
},
169-
Permissions: &github.InstallationPermissions{
170-
PullRequests: github.Ptr("read"),
171-
},
172-
Repositories: []*github.Repository{
169+
Repositories: []Repository{
173170
{
174-
Name: github.Ptr("mocked-repo-1"),
175-
ID: github.Ptr(int64(1)),
171+
Name: ptr("mocked-repo-1"),
172+
ID: ptr(int64(1)),
176173
},
177174
},
178175
},
@@ -217,7 +214,7 @@ func Test_installationTokenSource_Token(t *testing.T) {
217214
id: 1,
218215
src: appSrc,
219216
opts: []InstallationTokenSourceOpt{
220-
WithInstallationTokenOptions(&github.InstallationTokenOptions{}),
217+
WithInstallationTokenOptions(&InstallationTokenOptions{}),
221218
WithHTTPClient(errMockedHTTPClient),
222219
},
223220
},
@@ -229,9 +226,9 @@ func Test_installationTokenSource_Token(t *testing.T) {
229226
id: 1,
230227
src: appSrc,
231228
opts: []InstallationTokenSourceOpt{
232-
WithInstallationTokenOptions(&github.InstallationTokenOptions{}),
229+
WithInstallationTokenOptions(&InstallationTokenOptions{}),
233230
WithContext(context.Background()),
234-
WithEnterpriseURLs("https://github.example.com", "https://github.example.com"),
231+
WithEnterpriseURL("https://github.example.com"),
235232
WithHTTPClient(mockedHTTPClient),
236233
},
237234
},

github.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package githubauth
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
"time"
12+
)
13+
14+
const (
15+
// defaultBaseURL is the default GitHub API base URL.
16+
defaultBaseURL = "https://api.github.com/"
17+
)
18+
19+
// InstallationTokenOptions specifies options for creating an installation token.
20+
type InstallationTokenOptions struct {
21+
// Repositories is a list of repository names that the token should have access to.
22+
Repositories []string `json:"repositories,omitempty"`
23+
// RepositoryIDs is a list of repository IDs that the token should have access to.
24+
RepositoryIDs []int64 `json:"repository_ids,omitempty"`
25+
// Permissions are the permissions granted to the access token.
26+
Permissions *InstallationPermissions `json:"permissions,omitempty"`
27+
}
28+
29+
// InstallationPermissions represents the permissions granted to an installation token.
30+
type InstallationPermissions struct {
31+
Actions *string `json:"actions,omitempty"`
32+
Administration *string `json:"administration,omitempty"`
33+
Checks *string `json:"checks,omitempty"`
34+
Contents *string `json:"contents,omitempty"`
35+
ContentReferences *string `json:"content_references,omitempty"`
36+
Deployments *string `json:"deployments,omitempty"`
37+
Environments *string `json:"environments,omitempty"`
38+
Issues *string `json:"issues,omitempty"`
39+
Metadata *string `json:"metadata,omitempty"`
40+
Packages *string `json:"packages,omitempty"`
41+
Pages *string `json:"pages,omitempty"`
42+
PullRequests *string `json:"pull_requests,omitempty"`
43+
RepositoryAnnouncementBanners *string `json:"repository_announcement_banners,omitempty"`
44+
RepositoryHooks *string `json:"repository_hooks,omitempty"`
45+
RepositoryProjects *string `json:"repository_projects,omitempty"`
46+
SecretScanningAlerts *string `json:"secret_scanning_alerts,omitempty"`
47+
Secrets *string `json:"secrets,omitempty"`
48+
SecurityEvents *string `json:"security_events,omitempty"`
49+
SingleFile *string `json:"single_file,omitempty"`
50+
Statuses *string `json:"statuses,omitempty"`
51+
VulnerabilityAlerts *string `json:"vulnerability_alerts,omitempty"`
52+
Workflows *string `json:"workflows,omitempty"`
53+
Members *string `json:"members,omitempty"`
54+
OrganizationAdministration *string `json:"organization_administration,omitempty"`
55+
OrganizationCustomRoles *string `json:"organization_custom_roles,omitempty"`
56+
OrganizationAnnouncementBanners *string `json:"organization_announcement_banners,omitempty"`
57+
OrganizationHooks *string `json:"organization_hooks,omitempty"`
58+
OrganizationPlan *string `json:"organization_plan,omitempty"`
59+
OrganizationProjects *string `json:"organization_projects,omitempty"`
60+
OrganizationPackages *string `json:"organization_packages,omitempty"`
61+
OrganizationSecrets *string `json:"organization_secrets,omitempty"`
62+
OrganizationSelfHostedRunners *string `json:"organization_self_hosted_runners,omitempty"`
63+
OrganizationUserBlocking *string `json:"organization_user_blocking,omitempty"`
64+
TeamDiscussions *string `json:"team_discussions,omitempty"`
65+
}
66+
67+
// InstallationToken represents a GitHub App installation token.
68+
type InstallationToken struct {
69+
Token string `json:"token"`
70+
ExpiresAt time.Time `json:"expires_at"`
71+
Permissions *InstallationPermissions `json:"permissions,omitempty"`
72+
Repositories []Repository `json:"repositories,omitempty"`
73+
}
74+
75+
// Repository represents a GitHub repository.
76+
type Repository struct {
77+
ID *int64 `json:"id,omitempty"`
78+
Name *string `json:"name,omitempty"`
79+
}
80+
81+
// githubClient is a simple GitHub API client for creating installation tokens.
82+
type githubClient struct {
83+
baseURL *url.URL
84+
client *http.Client
85+
}
86+
87+
// newGitHubClient creates a new GitHub API client.
88+
func newGitHubClient(httpClient *http.Client) *githubClient {
89+
baseURL, _ := url.Parse(defaultBaseURL)
90+
91+
return &githubClient{
92+
baseURL: baseURL,
93+
client: httpClient,
94+
}
95+
}
96+
97+
// withEnterpriseURL sets the base URL for GitHub Enterprise Server.
98+
func (c *githubClient) withEnterpriseURL(baseURL string) (*githubClient, error) {
99+
base, err := url.Parse(baseURL)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed to parse base URL: %w", err)
102+
}
103+
104+
c.baseURL = base
105+
106+
return c, nil
107+
}
108+
109+
// createInstallationToken creates an installation access token for a GitHub App.
110+
// API documentation: https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app
111+
func (c *githubClient) createInstallationToken(ctx context.Context, installationID int64, opts *InstallationTokenOptions) (*InstallationToken, error) {
112+
endpoint := fmt.Sprintf("app/installations/%d/access_tokens", installationID)
113+
u, err := c.baseURL.Parse(endpoint)
114+
if err != nil {
115+
return nil, fmt.Errorf("failed to parse endpoint URL: %w", err)
116+
}
117+
118+
var body io.Reader
119+
if opts != nil {
120+
jsonBody, err := json.Marshal(opts)
121+
if err != nil {
122+
return nil, fmt.Errorf("failed to marshal request body: %w", err)
123+
}
124+
body = bytes.NewReader(jsonBody)
125+
}
126+
127+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), body)
128+
if err != nil {
129+
return nil, fmt.Errorf("failed to create request: %w", err)
130+
}
131+
132+
req.Header.Set("Accept", "application/vnd.github+json")
133+
req.Header.Set("Content-Type", "application/json")
134+
135+
resp, err := c.client.Do(req)
136+
if err != nil {
137+
return nil, fmt.Errorf("failed to execute request: %w", err)
138+
}
139+
defer resp.Body.Close()
140+
141+
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
142+
bodyBytes, _ := io.ReadAll(resp.Body)
143+
return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(bodyBytes))
144+
}
145+
146+
var token InstallationToken
147+
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
148+
return nil, fmt.Errorf("failed to decode response: %w", err)
149+
}
150+
151+
return &token, nil
152+
}
153+
154+
// ptr is a helper function to create a pointer to a value.
155+
func ptr[T any](v T) *T {
156+
return &v
157+
}

go.mod

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,5 @@ go 1.25
44

55
require (
66
github.com/golang-jwt/jwt/v5 v5.3.0
7-
github.com/google/go-github/v74 v74.0.0
87
golang.org/x/oauth2 v0.32.0
98
)
10-
11-
require github.com/google/go-querystring v1.1.0 // indirect

go.sum

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
11
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
22
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
3-
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4-
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
5-
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
6-
github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=
7-
github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak=
8-
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
9-
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
103
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
114
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
12-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

0 commit comments

Comments
 (0)