From a396e3a394b72137b619a16d1a9f08d853bd4c9b Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Mon, 29 Apr 2024 20:08:54 +0200 Subject: [PATCH] feat: Add gitHubLatestReleaseAssetURL template function --- .../gitHubLatestReleaseAssetURL.md | 20 +++ assets/chezmoi.io/mkdocs.yml | 1 + internal/cmd/config.go | 163 +++++++++--------- internal/cmd/githubtemplatefuncs.go | 44 ++++- 4 files changed, 139 insertions(+), 89 deletions(-) create mode 100644 assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md new file mode 100644 index 00000000000..a36d9a15fc4 --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md @@ -0,0 +1,20 @@ +# `gitHubLatestReleaseAssetURL` *owner-repo* *pattern* + +`gitHubLatestReleaseAssetURL` calls the GitHub API to retrieve the latest +release about the given *owner-repo*, returning structured data as defined by +the [GitHub Go API +bindings](https://pkg.go.dev/github.com/google/go-github/v61/github#RepositoryRelease). +It then iterates through all the release's assets, returning the first one that +matches *pattern*. *pattern* is a shell pattern as [described in +`path.Match`](https://pkg.go.dev/path#Match). + +Calls to `gitHubLatestReleaseAssetURL` are cached so calling +`gitHubLatestReleaseAssetURL` with the same *owner-repo* will only result in one +call to the GitHub API. + +!!! example + + ``` + {{ gitHubLatestReleaseAssetURL "FiloSottile/age" (printf "age-*-%s-%s.tar.gz" .chezmoi.os .chezmoi.arch) }} + {{ gitHubLatestReleaseAssetURL "twpayne/chezmoi" (printf "chezmoi-%s-%s" .chezmoi.os .chezmoi.arch) }} + ``` diff --git a/assets/chezmoi.io/mkdocs.yml b/assets/chezmoi.io/mkdocs.yml index 026d6f121a4..0bce14cf6ce 100644 --- a/assets/chezmoi.io/mkdocs.yml +++ b/assets/chezmoi.io/mkdocs.yml @@ -224,6 +224,7 @@ nav: - reference/templates/github-functions/index.md - gitHubKeys: reference/templates/github-functions/gitHubKeys.md - gitHubLatestRelease: reference/templates/github-functions/gitHubLatestRelease.md + - gitHubLatestReleaseAssetURL: reference/templates/github-functions/gitHubLatestReleaseAssetURL.md - gitHubLatestTag: reference/templates/github-functions/gitHubLatestTag.md - gitHubReleases: reference/templates/github-functions/gitHubReleases.md - gitHubTags: reference/templates/github-functions/gitHubTags.md diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 3d07ffa12bd..6bc4525bff1 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -405,87 +405,88 @@ func newConfig(options ...configOption) (*Config, error) { // The completion template function is added in persistentPreRunRootE as // it needs a *cobra.Command, which we don't yet have. for key, value := range map[string]any{ - "awsSecretsManager": c.awsSecretsManagerTemplateFunc, - "awsSecretsManagerRaw": c.awsSecretsManagerRawTemplateFunc, - "azureKeyVault": c.azureKeyVaultTemplateFunc, - "bitwarden": c.bitwardenTemplateFunc, - "bitwardenAttachment": c.bitwardenAttachmentTemplateFunc, - "bitwardenAttachmentByRef": c.bitwardenAttachmentByRefTemplateFunc, - "bitwardenFields": c.bitwardenFieldsTemplateFunc, - "bitwardenSecrets": c.bitwardenSecretsTemplateFunc, - "comment": c.commentTemplateFunc, - "dashlaneNote": c.dashlaneNoteTemplateFunc, - "dashlanePassword": c.dashlanePasswordTemplateFunc, - "decrypt": c.decryptTemplateFunc, - "deleteValueAtPath": c.deleteValueAtPathTemplateFunc, - "doppler": c.dopplerTemplateFunc, - "dopplerProjectJson": c.dopplerProjectJSONTemplateFunc, - "ejsonDecrypt": c.ejsonDecryptTemplateFunc, - "ejsonDecryptWithKey": c.ejsonDecryptWithKeyTemplateFunc, - "encrypt": c.encryptTemplateFunc, - "eqFold": c.eqFoldTemplateFunc, - "findExecutable": c.findExecutableTemplateFunc, - "findOneExecutable": c.findOneExecutableTemplateFunc, - "fromIni": c.fromIniTemplateFunc, - "fromJson": c.fromJsonTemplateFunc, - "fromJsonc": c.fromJsoncTemplateFunc, - "fromToml": c.fromTomlTemplateFunc, - "fromYaml": c.fromYamlTemplateFunc, - "gitHubKeys": c.gitHubKeysTemplateFunc, - "gitHubLatestRelease": c.gitHubLatestReleaseTemplateFunc, - "gitHubLatestTag": c.gitHubLatestTagTemplateFunc, - "gitHubReleases": c.gitHubReleasesTemplateFunc, - "gitHubTags": c.gitHubTagsTemplateFunc, - "glob": c.globTemplateFunc, - "gopass": c.gopassTemplateFunc, - "gopassRaw": c.gopassRawTemplateFunc, - "hcpVaultSecret": c.hcpVaultSecretTemplateFunc, - "hcpVaultSecretJson": c.hcpVaultSecretJSONTemplateFunc, - "hexDecode": c.hexDecodeTemplateFunc, - "hexEncode": c.hexEncodeTemplateFunc, - "include": c.includeTemplateFunc, - "includeTemplate": c.includeTemplateTemplateFunc, - "ioreg": c.ioregTemplateFunc, - "isExecutable": c.isExecutableTemplateFunc, - "joinPath": c.joinPathTemplateFunc, - "jq": c.jqTemplateFunc, - "keepassxc": c.keepassxcTemplateFunc, - "keepassxcAttachment": c.keepassxcAttachmentTemplateFunc, - "keepassxcAttribute": c.keepassxcAttributeTemplateFunc, - "keeper": c.keeperTemplateFunc, - "keeperDataFields": c.keeperDataFieldsTemplateFunc, - "keeperFindPassword": c.keeperFindPasswordTemplateFunc, - "keyring": c.keyringTemplateFunc, - "lastpass": c.lastpassTemplateFunc, - "lastpassRaw": c.lastpassRawTemplateFunc, - "lookPath": c.lookPathTemplateFunc, - "lstat": c.lstatTemplateFunc, - "mozillaInstallHash": c.mozillaInstallHashTemplateFunc, - "onepassword": c.onepasswordTemplateFunc, - "onepasswordDetailsFields": c.onepasswordDetailsFieldsTemplateFunc, - "onepasswordDocument": c.onepasswordDocumentTemplateFunc, - "onepasswordItemFields": c.onepasswordItemFieldsTemplateFunc, - "onepasswordRead": c.onepasswordReadTemplateFunc, - "output": c.outputTemplateFunc, - "pass": c.passTemplateFunc, - "passFields": c.passFieldsTemplateFunc, - "passhole": c.passholeTemplateFunc, - "passRaw": c.passRawTemplateFunc, - "pruneEmptyDicts": c.pruneEmptyDictsTemplateFunc, - "quoteList": c.quoteListTemplateFunc, - "rbw": c.rbwTemplateFunc, - "rbwFields": c.rbwFieldsTemplateFunc, - "replaceAllRegex": c.replaceAllRegexTemplateFunc, - "secret": c.secretTemplateFunc, - "secretJSON": c.secretJSONTemplateFunc, - "setValueAtPath": c.setValueAtPathTemplateFunc, - "splitList": c.splitListTemplateFunc, - "stat": c.statTemplateFunc, - "toIni": c.toIniTemplateFunc, - "toPrettyJson": c.toPrettyJsonTemplateFunc, - "toToml": c.toTomlTemplateFunc, - "toYaml": c.toYamlTemplateFunc, - "vault": c.vaultTemplateFunc, + "awsSecretsManager": c.awsSecretsManagerTemplateFunc, + "awsSecretsManagerRaw": c.awsSecretsManagerRawTemplateFunc, + "azureKeyVault": c.azureKeyVaultTemplateFunc, + "bitwarden": c.bitwardenTemplateFunc, + "bitwardenAttachment": c.bitwardenAttachmentTemplateFunc, + "bitwardenAttachmentByRef": c.bitwardenAttachmentByRefTemplateFunc, + "bitwardenFields": c.bitwardenFieldsTemplateFunc, + "bitwardenSecrets": c.bitwardenSecretsTemplateFunc, + "comment": c.commentTemplateFunc, + "dashlaneNote": c.dashlaneNoteTemplateFunc, + "dashlanePassword": c.dashlanePasswordTemplateFunc, + "decrypt": c.decryptTemplateFunc, + "deleteValueAtPath": c.deleteValueAtPathTemplateFunc, + "doppler": c.dopplerTemplateFunc, + "dopplerProjectJson": c.dopplerProjectJSONTemplateFunc, + "ejsonDecrypt": c.ejsonDecryptTemplateFunc, + "ejsonDecryptWithKey": c.ejsonDecryptWithKeyTemplateFunc, + "encrypt": c.encryptTemplateFunc, + "eqFold": c.eqFoldTemplateFunc, + "findExecutable": c.findExecutableTemplateFunc, + "findOneExecutable": c.findOneExecutableTemplateFunc, + "fromIni": c.fromIniTemplateFunc, + "fromJson": c.fromJsonTemplateFunc, + "fromJsonc": c.fromJsoncTemplateFunc, + "fromToml": c.fromTomlTemplateFunc, + "fromYaml": c.fromYamlTemplateFunc, + "gitHubKeys": c.gitHubKeysTemplateFunc, + "gitHubLatestRelease": c.gitHubLatestReleaseTemplateFunc, + "gitHubLatestReleaseAssetURL": c.gitHubLatestReleaseAssetURLTemplateFunc, + "gitHubLatestTag": c.gitHubLatestTagTemplateFunc, + "gitHubReleases": c.gitHubReleasesTemplateFunc, + "gitHubTags": c.gitHubTagsTemplateFunc, + "glob": c.globTemplateFunc, + "gopass": c.gopassTemplateFunc, + "gopassRaw": c.gopassRawTemplateFunc, + "hcpVaultSecret": c.hcpVaultSecretTemplateFunc, + "hcpVaultSecretJson": c.hcpVaultSecretJSONTemplateFunc, + "hexDecode": c.hexDecodeTemplateFunc, + "hexEncode": c.hexEncodeTemplateFunc, + "include": c.includeTemplateFunc, + "includeTemplate": c.includeTemplateTemplateFunc, + "ioreg": c.ioregTemplateFunc, + "isExecutable": c.isExecutableTemplateFunc, + "joinPath": c.joinPathTemplateFunc, + "jq": c.jqTemplateFunc, + "keepassxc": c.keepassxcTemplateFunc, + "keepassxcAttachment": c.keepassxcAttachmentTemplateFunc, + "keepassxcAttribute": c.keepassxcAttributeTemplateFunc, + "keeper": c.keeperTemplateFunc, + "keeperDataFields": c.keeperDataFieldsTemplateFunc, + "keeperFindPassword": c.keeperFindPasswordTemplateFunc, + "keyring": c.keyringTemplateFunc, + "lastpass": c.lastpassTemplateFunc, + "lastpassRaw": c.lastpassRawTemplateFunc, + "lookPath": c.lookPathTemplateFunc, + "lstat": c.lstatTemplateFunc, + "mozillaInstallHash": c.mozillaInstallHashTemplateFunc, + "onepassword": c.onepasswordTemplateFunc, + "onepasswordDetailsFields": c.onepasswordDetailsFieldsTemplateFunc, + "onepasswordDocument": c.onepasswordDocumentTemplateFunc, + "onepasswordItemFields": c.onepasswordItemFieldsTemplateFunc, + "onepasswordRead": c.onepasswordReadTemplateFunc, + "output": c.outputTemplateFunc, + "pass": c.passTemplateFunc, + "passFields": c.passFieldsTemplateFunc, + "passhole": c.passholeTemplateFunc, + "passRaw": c.passRawTemplateFunc, + "pruneEmptyDicts": c.pruneEmptyDictsTemplateFunc, + "quoteList": c.quoteListTemplateFunc, + "rbw": c.rbwTemplateFunc, + "rbwFields": c.rbwFieldsTemplateFunc, + "replaceAllRegex": c.replaceAllRegexTemplateFunc, + "secret": c.secretTemplateFunc, + "secretJSON": c.secretJSONTemplateFunc, + "setValueAtPath": c.setValueAtPathTemplateFunc, + "splitList": c.splitListTemplateFunc, + "stat": c.statTemplateFunc, + "toIni": c.toIniTemplateFunc, + "toPrettyJson": c.toPrettyJsonTemplateFunc, + "toToml": c.toTomlTemplateFunc, + "toYaml": c.toYamlTemplateFunc, + "vault": c.vaultTemplateFunc, } { c.addTemplateFunc(key, value) } diff --git a/internal/cmd/githubtemplatefuncs.go b/internal/cmd/githubtemplatefuncs.go index 5dd703bb470..9d9a52d4075 100644 --- a/internal/cmd/githubtemplatefuncs.go +++ b/internal/cmd/githubtemplatefuncs.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "path" "strings" "time" @@ -107,14 +108,33 @@ func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { return allKeys } -func (c *Config) gitHubLatestReleaseTemplateFunc(ownerRepo string) *github.RepositoryRelease { - owner, repo, err := gitHubSplitOwnerRepo(ownerRepo) +func (c *Config) gitHubLatestReleaseAssetURLTemplateFunc(ownerRepo, pattern string) string { + release, err := c.gitHubLatestRelease(ownerRepo) if err != nil { panic(err) } + for _, asset := range release.Assets { + if asset.Name == nil { + continue + } + switch ok, err := path.Match(pattern, *asset.Name); { + case err != nil: + panic(err) + case ok: + return *asset.BrowserDownloadURL + } + } + return "" +} + +func (c *Config) gitHubLatestRelease(ownerRepo string) (*github.RepositoryRelease, error) { + owner, repo, err := gitHubSplitOwnerRepo(ownerRepo) + if err != nil { + return nil, err + } if release := c.gitHub.latestReleaseCache[owner][repo]; release != nil { - return release + return release, nil } now := time.Now() @@ -123,9 +143,9 @@ func (c *Config) gitHubLatestReleaseTemplateFunc(ownerRepo string) *github.Repos var gitHubLatestReleaseStateValue gitHubLatestReleaseState switch ok, err := chezmoi.PersistentStateGet(c.persistentState, gitHubLatestReleaseStateBucket, gitHubLatestReleaseKey, &gitHubLatestReleaseStateValue); { case err != nil: - panic(err) + return nil, err case ok && now.Before(gitHubLatestReleaseStateValue.RequestedAt.Add(c.GitHub.RefreshPeriod)): - return gitHubLatestReleaseStateValue.Release + return gitHubLatestReleaseStateValue.Release, nil } } @@ -134,19 +154,19 @@ func (c *Config) gitHubLatestReleaseTemplateFunc(ownerRepo string) *github.Repos gitHubClient, err := c.getGitHubClient(ctx) if err != nil { - panic(err) + return nil, err } release, _, err := gitHubClient.Repositories.GetLatestRelease(ctx, owner, repo) if err != nil { - panic(err) + return nil, err } if err := chezmoi.PersistentStateSet(c.persistentState, gitHubLatestReleaseStateBucket, gitHubLatestReleaseKey, &gitHubLatestReleaseState{ RequestedAt: now, Release: release, }); err != nil { - panic(err) + return nil, err } if c.gitHub.latestReleaseCache == nil { @@ -157,6 +177,14 @@ func (c *Config) gitHubLatestReleaseTemplateFunc(ownerRepo string) *github.Repos } c.gitHub.latestReleaseCache[owner][repo] = release + return release, nil +} + +func (c *Config) gitHubLatestReleaseTemplateFunc(ownerRepo string) *github.RepositoryRelease { + release, err := c.gitHubLatestRelease(ownerRepo) + if err != nil { + panic(err) + } return release }