From 6ea118c24fe55d7e85d24b8d2f9d3677ecae61c7 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Wed, 18 Mar 2026 21:47:47 +0200 Subject: [PATCH] feat: github release add archive_bin_name option --- docs/installer-configuration.md | 16 +++++++++ installer/github_release_installer.go | 23 +++++++++++-- installer/github_release_installer_test.go | 39 ++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/docs/installer-configuration.md b/docs/installer-configuration.md index dec8357..5cf75b3 100755 --- a/docs/installer-configuration.md +++ b/docs/installer-configuration.md @@ -315,6 +315,22 @@ These fields are shared by all installer types. Some fields may vary in behavior download_filename: myapp_{tag}_linux.tar.gz # outputs: myapp_v1.0.0_linux.tar.gz ``` + - `opts.archive_bin_name`: The name of the binary file inside the archive (tar/zip). + Use this when the filename inside the archive differs from the desired output `bin_name`. + If not set, falls back to `bin_name` (or the installer name). + + ```yaml + - name: cospend-cli + bin_name: cospend + type: github-release + opts: + repository: chenasraf/cospend-cli + destination: ~/.local/bin + strategy: tar + download_filename: cospend-cli-linux-{{ .Arch }}.tar.gz + archive_bin_name: cospend-cli # file inside the tar is "cospend-cli", output will be "cospend" + ``` + - `opts.github_token`: GitHub personal access token for authenticated API requests. Authenticated requests have a much higher rate limit (5,000/hour vs 60/hour for unauthenticated). diff --git a/installer/github_release_installer.go b/installer/github_release_installer.go index db068ef..e6437fd 100755 --- a/installer/github_release_installer.go +++ b/installer/github_release_installer.go @@ -40,6 +40,10 @@ type GitHubReleaseOpts struct { // GithubToken is the GitHub personal access token for authenticated API requests. // Supports environment variable expansion (e.g., "$GITHUB_TOKEN" or "${GITHUB_TOKEN}"). GithubToken *string + // ArchiveBinName is the name of the binary file inside the archive (tar/zip). + // Use this when the filename inside the archive differs from the desired output bin_name. + // If not set, falls back to bin_name (or the installer name). + ArchiveBinName *string } // GitHubReleaseInstallStrategy represents the installation strategy for a GitHub release. @@ -193,7 +197,7 @@ func (i *GitHubReleaseInstaller) Install() error { if err != nil { return err } - logger.Debug("Strategy 'tar': copying binary '%s' to destination", i.GetBinName()) + logger.Debug("Strategy 'tar': copying binary '%s' to destination", i.GetArchiveBinName()) success, err = i.CopyExtractedFile(out, tmpDir) if !success { return fmt.Errorf("failed to copy extracted file: %w", err) @@ -210,7 +214,7 @@ func (i *GitHubReleaseInstaller) Install() error { if err != nil { return err } - logger.Debug("Strategy 'zip': copying binary '%s' to destination", i.GetBinName()) + logger.Debug("Strategy 'zip': copying binary '%s' to destination", i.GetArchiveBinName()) success, err = i.CopyExtractedFile(out, tmpDir) if !success { return fmt.Errorf("failed to copy extracted file: %w", err) @@ -299,6 +303,16 @@ func (i *GitHubReleaseInstaller) GetBinName() string { return filepath.Base(*i.Info.Name) } +// GetArchiveBinName returns the name of the binary file inside the archive. +// It uses ArchiveBinName from opts if provided, otherwise falls back to GetBinName(). +func (i *GitHubReleaseInstaller) GetArchiveBinName() string { + opts := i.GetOpts() + if opts.ArchiveBinName != nil { + return *opts.ArchiveBinName + } + return i.GetBinName() +} + // CopyExtractedFile copies the extracted file from a temporary directory to the final destination. func (i *GitHubReleaseInstaller) CopyExtractedFile(out *os.File, tmpDir string) (bool, error) { binFile, err := os.Create(out.Name()) @@ -310,7 +324,7 @@ func (i *GitHubReleaseInstaller) CopyExtractedFile(out *os.File, tmpDir string) logger.Warn("failed to close binFile %s: %v", binFile.Name(), cerr) } }() - tmpBinFile, err := os.Open(filepath.Join(tmpDir, i.GetBinName())) + tmpBinFile, err := os.Open(filepath.Join(tmpDir, i.GetArchiveBinName())) if err != nil { return false, fmt.Errorf("failed to open temporary file: %w", err) } @@ -397,6 +411,9 @@ func (i *GitHubReleaseInstaller) GetOpts() *GitHubReleaseOpts { token = utils.GetRealPath(i.GetData().Environ(), token) opts.GithubToken = &token } + if archiveBinName, ok := (*info.Opts)["archive_bin_name"].(string); ok { + opts.ArchiveBinName = &archiveBinName + } } return opts } diff --git a/installer/github_release_installer_test.go b/installer/github_release_installer_test.go index cde0b1e..9d08bc2 100755 --- a/installer/github_release_installer_test.go +++ b/installer/github_release_installer_test.go @@ -175,6 +175,45 @@ func TestGitHubReleaseGetBinName(t *testing.T) { }) } +func TestGitHubReleaseGetArchiveBinName(t *testing.T) { + logger.InitLogger(false) + + t.Run("returns archive_bin_name when set", func(t *testing.T) { + binName := "cospend" + data := &appconfig.InstallerData{ + Name: strPtr("cospend-cli"), + Type: appconfig.InstallerTypeGitHubRelease, + BinName: &binName, + Opts: &map[string]any{ + "archive_bin_name": "cospend-cli", + }, + } + installer := newTestGitHubReleaseInstaller(data) + assert.Equal(t, "cospend-cli", installer.GetArchiveBinName()) + assert.Equal(t, "cospend", installer.GetBinName()) + }) + + t.Run("falls back to bin_name when archive_bin_name not set", func(t *testing.T) { + binName := "custom-bin" + data := &appconfig.InstallerData{ + Name: strPtr("my-app"), + Type: appconfig.InstallerTypeGitHubRelease, + BinName: &binName, + } + installer := newTestGitHubReleaseInstaller(data) + assert.Equal(t, "custom-bin", installer.GetArchiveBinName()) + }) + + t.Run("falls back to name when neither set", func(t *testing.T) { + data := &appconfig.InstallerData{ + Name: strPtr("my-app"), + Type: appconfig.InstallerTypeGitHubRelease, + } + installer := newTestGitHubReleaseInstaller(data) + assert.Equal(t, "my-app", installer.GetArchiveBinName()) + }) +} + func TestGitHubReleaseGetFilename(t *testing.T) { logger.InitLogger(false)