Skip to content

Commit 4d5ad3d

Browse files
committed
feat: Support self-hosted GitHub Enterprise servers
1 parent a1196a7 commit 4d5ad3d

File tree

13 files changed

+243
-100
lines changed

13 files changed

+243
-100
lines changed

assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestRelease.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# `gitHubLatestRelease` *owner-repo*
1+
# `gitHubLatestRelease` *host-owner-repo*
22

33
`gitHubLatestRelease` calls the GitHub API to retrieve the latest release about
4-
the given *owner-repo*, returning structured data as defined by the [GitHub Go
5-
API
4+
the given *host-owner-repo*, returning structured data as defined by the [GitHub
5+
Go API
66
bindings](https://pkg.go.dev/github.com/google/go-github/v57/github#RepositoryRelease).
77

88
Calls to `gitHubLatestRelease` are cached so calling `gitHubLatestRelease` with
9-
the same *owner-repo* will only result in one call to the GitHub API.
9+
the same *host-owner-repo* will only result in one call to the GitHub API.
1010

1111
!!! example
1212

assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
# `gitHubLatestReleaseAssetURL` *owner-repo* *pattern*
1+
# `gitHubLatestReleaseAssetURL` *host-owner-repo* *pattern*
22

33
`gitHubLatestReleaseAssetURL` calls the GitHub API to retrieve the latest
4-
release about the given *owner-repo*, returning structured data as defined by
5-
the [GitHub Go API
4+
release about the given *host-owner-repo*, returning structured data as defined
5+
by the [GitHub Go API
66
bindings](https://pkg.go.dev/github.com/google/go-github/v61/github#RepositoryRelease).
77
It then iterates through all the release's assets, returning the first one that
88
matches *pattern*. *pattern* is a shell pattern as [described in
99
`path.Match`](https://pkg.go.dev/path#Match).
1010

1111
Calls to `gitHubLatestReleaseAssetURL` are cached so calling
12-
`gitHubLatestReleaseAssetURL` with the same *owner-repo* will only result in one
13-
call to the GitHub API.
12+
`gitHubLatestReleaseAssetURL` with the same *host-owner-repo* will only result
13+
in one call to the GitHub API.
1414

1515
!!! example
1616

assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestTag.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# `gitHubLatestTag` *owner-repo*
1+
# `gitHubLatestTag` *host-owner-repo*
22

33
`gitHubLatestTag` calls the GitHub API to retrieve the latest tag for the given
4-
*owner-repo*, returning structured data as defined by the [GitHub Go API
4+
*host-owner-repo*, returning structured data as defined by the [GitHub Go API
55
bindings](https://pkg.go.dev/github.com/google/go-github/v57/github#RepositoryTag).
66

77
Calls to `gitHubLatestTag` are cached the same as [`githubTags`](gitHubTags.md),
8-
so calling `gitHubLatestTag` with the same *owner-repo* will only result in one
9-
call to the GitHub API.
8+
so calling `gitHubLatestTag` with the same *host-owner-repo* will only result in
9+
one call to the GitHub API.
1010

1111
!!! example
1212

assets/chezmoi.io/docs/reference/templates/github-functions/gitHubReleases.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# `gitHubReleases` *owner-repo*
1+
# `gitHubReleases` *host-owner-repo*
22

33
`gitHubReleases` calls the GitHub API to retrieve the first page of releases for
4-
the given *owner-repo*, returning structured data as defined by the [GitHub Go
5-
API
4+
the given *host-owner-repo*, returning structured data as defined by the [GitHub
5+
Go API
66
bindings](https://pkg.go.dev/github.com/google/go-github/v57/github#RepositoryRelease).
77

88
Calls to `gitHubReleases` are cached so calling `gitHubReleases` with the same
9-
*owner-repo* will only result in one call to the GitHub API.
9+
*host-owner-repo* will only result in one call to the GitHub API.
1010

1111
!!! example
1212

assets/chezmoi.io/docs/reference/templates/github-functions/gitHubTags.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# `gitHubTags` *owner-repo*
1+
# `gitHubTags` *host-owner-repo*
22

3-
`gitHubTags` calls the GitHub API to retrieve the first page of tags for
4-
the given *owner-repo*, returning structured data as defined by the [GitHub Go
3+
`gitHubTags` calls the GitHub API to retrieve the first page of tags for the
4+
given *host-owner-repo*, returning structured data as defined by the [GitHub Go
55
API
66
bindings](https://pkg.go.dev/github.com/google/go-github/v57/github#RepositoryTag).
77

88
Calls to `gitHubTags` are cached so calling `gitHubTags` with the
9-
same *owner-repo* will only result in one call to the GitHub API.
9+
same *host-owner-repo* will only result in one call to the GitHub API.
1010

1111
!!! example
1212

assets/chezmoi.io/docs/reference/templates/github-functions/index.md

+35-6
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,49 @@
22

33
The `gitHub*` template functions return data from the GitHub API.
44

5+
All functions take a *host-owner-repo* argument of the form:
6+
7+
[host/]owner/repo
8+
9+
The optional `host` specifies the host and defaults to `github.com` if omitted.
10+
`owner` and `repo` specify the repository owner and name respectively.
11+
512
By default, chezmoi makes anonymous GitHub API requests, which are subject to
613
[GitHub's rate
714
limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting)
815
(currently 60 requests per hour per source IP address). chezmoi caches results
916
from identical GitHub API requests for the period defined in
1017
`gitHub.refreshPeriod` (default one minute).
1118

12-
If any of the environment variables `$CHEZMOI_GITHUB_ACCESS_TOKEN`,
13-
`$GITHUB_ACCESS_TOKEN`, or `$GITHUB_TOKEN` are found, then the first one found
14-
will be used to authenticate the GitHub API requests which have a higher rate
15-
limit (currently 5,000 requests per hour per user).
19+
For `github.com` repos, if any of the environment variables
20+
21+
* `$CHEZMOI_GITHUB_ACCESS_TOKEN`
22+
* `$CHEZMOI_GITHUB_TOKEN`
23+
* `$GITHUB_ACCESS_TOKEN`
24+
* `$GITHUB_TOKEN`
25+
26+
are found, then the first one found will be used to
27+
authenticate the GitHub API requests which have a higher rate limit (currently
28+
5,000 requests per hour per user).
1629

1730
In practice, GitHub API rate limits are high enough chezmoi's caching of results
1831
mean that you should rarely need to set a token, unless you are sharing a source
19-
IP address with many other GitHub users. If needed, the GitHub documentation
20-
describes how to [create a personal access
32+
IP address with many other GitHub users or accessing a private repo. If needed,
33+
the GitHub documentation describes how to [create a personal access
2134
token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
35+
36+
For non-`github.com` repos, e.g. self-hosted GitHub Enterprise repos, if any of
37+
the environment variables
38+
39+
* `$CHEZMOI_`*`HOST`*`_ACCESS_TOKEN`
40+
* `$`*`HOST`*`_ACCESS_TOKEN`
41+
42+
are found then the first one will be used to authenticate requests, where
43+
*`HOST`* is the host converted to uppercase and with all non-letter characters
44+
replaced with underscores.
45+
46+
!!! example
47+
48+
Given the host `git.example.com`, chezmoi will look for the
49+
`$CHEZMOI_GIT_EXAMPLE_COM_ACCESS_TOKEN` and `$GIT_EXAMPLE_COM_ACCESS_TOKEN`
50+
environment variables.

internal/chezmoi/github.go

+41-8
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,8 @@ import (
1111

1212
// NewGitHubClient returns a new github.Client configured with an access token
1313
// and a http client, if available.
14-
func NewGitHubClient(ctx context.Context, httpClient *http.Client) *github.Client {
15-
for _, key := range []string{
16-
"CHEZMOI_GITHUB_ACCESS_TOKEN",
17-
"CHEZMOI_GITHUB_TOKEN",
18-
"GITHUB_ACCESS_TOKEN",
19-
"GITHUB_TOKEN",
20-
} {
14+
func NewGitHubClient(ctx context.Context, httpClient *http.Client, host string) (*github.Client, error) {
15+
for _, key := range accessTokenEnvKeys(host) {
2116
if accessToken := os.Getenv(key); accessToken != "" {
2217
httpClient = oauth2.NewClient(
2318
context.WithValue(ctx, oauth2.HTTPClient, httpClient),
@@ -27,5 +22,43 @@ func NewGitHubClient(ctx context.Context, httpClient *http.Client) *github.Clien
2722
break
2823
}
2924
}
30-
return github.NewClient(httpClient)
25+
gitHubClient := github.NewClient(httpClient)
26+
if host == "github.com" {
27+
return gitHubClient, nil
28+
}
29+
return gitHubClient.WithEnterpriseURLs(
30+
"https://"+host+"/api/v3/",
31+
"https://"+host+"/api/uploads/",
32+
)
33+
}
34+
35+
func accessTokenEnvKeys(host string) []string {
36+
if host == "github.com" {
37+
return []string{
38+
"CHEZMOI_GITHUB_ACCESS_TOKEN",
39+
"CHEZMOI_GITHUB_TOKEN",
40+
"GITHUB_ACCESS_TOKEN",
41+
"GITHUB_TOKEN",
42+
}
43+
}
44+
hostKey := makeHostKey(host)
45+
return []string{
46+
"CHEZMOI_" + hostKey + "_ACCESS_TOKEN",
47+
hostKey + "_ACCESS_TOKEN",
48+
}
49+
}
50+
51+
func makeHostKey(host string) string {
52+
hostKey := make([]byte, 0, len(host))
53+
for _, b := range []byte(host) {
54+
switch {
55+
case 'A' <= b && b <= 'Z':
56+
hostKey = append(hostKey, b)
57+
case 'a' <= b && b <= 'z':
58+
hostKey = append(hostKey, b-'a'+'A')
59+
default:
60+
hostKey = append(hostKey, '_')
61+
}
62+
}
63+
return string(hostKey)
3164
}

internal/chezmoi/github_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package chezmoi
2+
3+
import (
4+
"testing"
5+
6+
"github.com/alecthomas/assert/v2"
7+
)
8+
9+
func TestAccessTokenEnvKeys(t *testing.T) {
10+
for _, tc := range []struct {
11+
host string
12+
expected []string
13+
}{
14+
{
15+
host: "github.com",
16+
expected: []string{
17+
"CHEZMOI_GITHUB_ACCESS_TOKEN",
18+
"CHEZMOI_GITHUB_TOKEN",
19+
"GITHUB_ACCESS_TOKEN",
20+
"GITHUB_TOKEN",
21+
},
22+
},
23+
{
24+
host: "git.example.com",
25+
expected: []string{
26+
"CHEZMOI_GIT_EXAMPLE_COM_ACCESS_TOKEN",
27+
"GIT_EXAMPLE_COM_ACCESS_TOKEN",
28+
},
29+
},
30+
} {
31+
t.Run(tc.host, func(t *testing.T) {
32+
assert.Equal(t, tc.expected, accessTokenEnvKeys(tc.host))
33+
})
34+
}
35+
}

internal/cmd/config.go

+5
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,11 @@ func newConfig(options ...configOption) (*Config, error) {
340340
homeDir: userHomeDir,
341341
templateFuncs: sprig.TxtFuncMap(),
342342

343+
// Password manager data.
344+
gitHub: gitHubData{
345+
clientsByHost: make(map[string]gitHubClientResult),
346+
},
347+
343348
// Command configurations.
344349
apply: applyCmdConfig{
345350
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),

internal/cmd/doctorcmd.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,10 @@ func (c *latestVersionCheck) Run(system chezmoi.System, homeDirAbsPath chezmoi.A
659659

660660
ctx := context.Background()
661661

662-
gitHubClient := chezmoi.NewGitHubClient(ctx, c.httpClient)
662+
gitHubClient, err := chezmoi.NewGitHubClient(ctx, c.httpClient, "github.com")
663+
if err != nil {
664+
return checkResultFailed, err.Error()
665+
}
663666
rr, _, err := gitHubClient.Repositories.GetLatestRelease(ctx, "twpayne", "chezmoi")
664667
var rateLimitErr *github.RateLimitError
665668
var abuseRateLimitErr *github.AbuseRateLimitError

0 commit comments

Comments
 (0)