diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 10bc62caebc..d1793e2a986 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -106,6 +106,7 @@ Manage your dotfiles across multiple machines, securely. * [`decrypt` *ciphertext*](#decrypt-ciphertext) * [`encrypt` *plaintext*](#encrypt-plaintext) * [`gitHubKeys` *user*](#githubkeys-user) + * [`gitHubLatestRelease` *user-repo*](#githubkeys-user-repo) * [`gopass` *gopass-name*](#gopass-gopass-name) * [`gopassRaw` *gopass-name*](#gopassraw-gopass-name) * [`include` *filename*](#include-filename) @@ -2044,7 +2045,7 @@ environment variables `$CHEZMOI_GITHUB_ACCESS_TOKEN`, `$GITHUB_ACCESS_TOKEN`, or the GitHub API request, with a higher rate limit (currently 5,000 requests per hour per user). -In practice, GitHub API rate limits are high enough that you should never need +In practice, GitHub API rate limits are high enough that you should rarely need to set a token, unless you are sharing a source IP address with many other GitHub users. If needed, the GitHub documentation describes how to [create a personal access @@ -2060,6 +2061,26 @@ token](https://docs.github.com/en/github/authenticating-to-github/creating-a-per --- +### `gitHubLatestRelease` *user-repo* + +`gitHubLatestRelease` calls the GitHub API to retrieve the latest release about the given +*user-repo*, returning structured data as defined by the [GitHub Go API +bindings](https://pkg.go.dev/github.com/google/go-github/v40/github#RepositoryRelease). + +Calls to `gitHubLatestRelease` are cached so calling `gitHubLatestRelease` with the same +*user-repo* will only result in one call to the GitHub API. + + +`gitHubLatestRelease` uses the same API request mechanism as `gitHubKeys`. + +#### `gitHubLatestRelease` examples + +``` +{{ (gitHubLatestRelease "docker/compose").TagName }} +``` + +--- + ### `gopass` *gopass-name* `gopass` returns passwords stored in [gopass](https://www.gopass.pw/) using the diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 51ba2bc2acf..efecb84ff17 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -404,6 +404,7 @@ func newConfig(options ...configOption) (*Config, error) { "decrypt": c.decryptTemplateFunc, "encrypt": c.encryptTemplateFunc, "gitHubKeys": c.gitHubKeysTemplateFunc, + "gitHubLatestRelease": c.gitHubLatestReleaseTemplateFunc, "gopass": c.gopassTemplateFunc, "gopassRaw": c.gopassRawTemplateFunc, "include": c.includeTemplateFunc, diff --git a/internal/cmd/githubtemplatefuncs.go b/internal/cmd/githubtemplatefuncs.go index da35518c42e..a35bc7c7b3d 100644 --- a/internal/cmd/githubtemplatefuncs.go +++ b/internal/cmd/githubtemplatefuncs.go @@ -2,12 +2,15 @@ package cmd import ( "context" + "fmt" + "strings" "github.com/google/go-github/v40/github" ) type gitHubData struct { - keysCache map[string][]*github.Key + keysCache map[string][]*github.Key + latestReleaseCache map[string]map[string]*github.RepositoryRelease } func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { @@ -43,3 +46,42 @@ func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { c.gitHub.keysCache[user] = allKeys return allKeys } + +func (c *Config) gitHubLatestReleaseTemplateFunc(userRepo string) *github.RepositoryRelease { + user, repo := parseGitHubUserRepo(userRepo) + + if release := c.gitHub.latestReleaseCache[user][repo]; release != nil { + return release + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + gitHubClient := newGitHubClient(ctx) + + release, _, err := gitHubClient.Repositories.GetLatestRelease(ctx, user, repo) + if err != nil { + returnTemplateError(err) + return nil + } + + if c.gitHub.latestReleaseCache == nil { + c.gitHub.latestReleaseCache = make(map[string]map[string]*github.RepositoryRelease) + } + if c.gitHub.latestReleaseCache[user] == nil { + c.gitHub.latestReleaseCache[user] = make(map[string]*github.RepositoryRelease) + } + c.gitHub.latestReleaseCache[user][repo] = release + + return release +} + +func parseGitHubUserRepo(userRepo string) (string, string) { + fields := strings.SplitN(userRepo, "/", 2) + if len(fields) != 2 || fields[0] == "" || fields[1] == "" { + returnTemplateError(fmt.Errorf("%s: not a user/repo", userRepo)) + return "", "" + } + user, repo := fields[0], fields[1] + return user, repo +}