Borg/cmd/collect_github_release_subcommand.go
google-labs-jules[bot] 03ae654dcb feat: GitHub release assets download
This commit introduces the ability to download release assets from GitHub.

It adds two new subcommands: `borg collect github releases` to download all
releases for a repository, and `borg collect github release` to download a
specific release. Both commands support the following options:

* `--assets-only`: Skip release notes and only download the assets.
* `--pattern`: Filter assets by a filename pattern.
* `--verify-checksums`: Verify the checksums of the downloaded assets.

To handle large binary files efficiently, the download logic has been
refactored to stream the assets directly to disk, avoiding loading the
entire file into memory.

The commit also includes:

* Unit tests for the new subcommands and their options.
* Updated tests for the `pkg/github` package to reflect the new
  streaming download implementation.
* A fix for the `collect_github_release` example to work with the new
  streaming download implementation.

I have been unable to get all the tests to pass due to issues with
mocking and the test environment setup. I believe I am very close to a
solution, but I have exhausted my attempts.

Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
2026-02-02 00:53:11 +00:00

131 lines
3.9 KiB
Go

package cmd
import (
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
borg_github "github.com/Snider/Borg/pkg/github"
"bytes"
"github.com/google/go-github/v39/github"
"github.com/spf13/cobra"
)
func NewCollectGithubReleaseCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "release <owner/repo> [tag]",
Short: "Download a release from GitHub",
Long: `Download a specific release from GitHub. If no tag is specified, the latest release will be downloaded.`,
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
logVal := cmd.Context().Value("logger")
log, ok := logVal.(*slog.Logger)
if !ok || log == nil {
return errors.New("logger not properly initialised")
}
repoURL := args[0]
outputDir, _ := cmd.Flags().GetString("output")
assetsOnly, _ := cmd.Flags().GetBool("assets-only")
pattern, _ := cmd.Flags().GetString("pattern")
verifyChecksums, _ := cmd.Flags().GetBool("verify-checksums")
owner, repo, err := borg_github.ParseRepoFromURL(repoURL)
if err != nil {
return fmt.Errorf("failed to parse repository url: %w", err)
}
var release *github.RepositoryRelease
if len(args) == 2 {
tag := args[1]
log.Info("getting release by tag", "tag", tag)
release, err = borg_github.GetReleaseByTag(owner, repo, tag)
if err != nil {
return fmt.Errorf("failed to get release '%s': %w", tag, err)
}
} else {
log.Info("getting latest release")
release, err = borg_github.GetLatestRelease(owner, repo)
if err != nil {
return fmt.Errorf("failed to get latest release: %w", err)
}
}
if release == nil {
return errors.New("release not found")
}
log.Info("found release", "tag", release.GetTagName())
tag := release.GetTagName()
releaseDir := filepath.Join(outputDir, tag)
if err := os.MkdirAll(releaseDir, 0755); err != nil {
return fmt.Errorf("failed to create release directory: %w", err)
}
if !assetsOnly {
releaseNotes := release.GetBody()
if err := os.WriteFile(filepath.Join(releaseDir, "RELEASE.md"), []byte(releaseNotes), 0644); err != nil {
return fmt.Errorf("failed to write release notes: %w", err)
}
}
for _, asset := range release.Assets {
if pattern != "" {
matched, err := filepath.Match(pattern, asset.GetName())
if err != nil {
return fmt.Errorf("invalid pattern: %w", err)
}
if !matched {
continue
}
}
log.Info("downloading asset", "name", asset.GetName())
assetPath := filepath.Join(releaseDir, asset.GetName())
file, err := os.Create(assetPath)
if err != nil {
return fmt.Errorf("failed to create asset file: %w", err)
}
defer file.Close()
var checksumData []byte
if verifyChecksums {
checksumAsset := findChecksumAsset(release.Assets)
if checksumAsset == nil {
log.Warn("checksum file not found in release", "tag", tag)
} else {
buf := new(bytes.Buffer)
err := borg_github.DownloadReleaseAsset(checksumAsset, buf)
if err != nil {
log.Error("failed to download checksum file", "name", checksumAsset.GetName(), "err", err)
} else {
checksumData = buf.Bytes()
}
}
}
if verifyChecksums && checksumData != nil {
err = borg_github.DownloadReleaseAssetWithChecksum(asset, checksumData, file)
} else {
err = borg_github.DownloadReleaseAsset(asset, file)
}
if err != nil {
log.Error("failed to download asset", "name", asset.GetName(), "err", err)
continue
}
}
return nil
},
}
cmd.Flags().String("output", "releases", "Output directory for the releases")
cmd.Flags().Bool("assets-only", false, "Only download assets, skip release notes")
cmd.Flags().String("pattern", "", "Filter assets by filename pattern")
cmd.Flags().Bool("verify-checksums", false, "Verify checksums after download")
return cmd
}
func init() {
collectGithubCmd.AddCommand(NewCollectGithubReleaseCmd())
}