Add PR checks command, iostreams/text packages for colored table output, top-level run/workflow aliases matching gh CLI structure. Enhance actions, issues, PRs, releases, repos, labels, milestones, and wiki commands with improved flags, JSON output, and error handling.
673 lines
17 KiB
Go
673 lines
17 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.gitea.io/sdk/gitea"
|
|
"forgejo.zerova.net/sid/fgj-sid/internal/api"
|
|
"forgejo.zerova.net/sid/fgj-sid/internal/config"
|
|
"forgejo.zerova.net/sid/fgj-sid/internal/text"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var releaseCmd = &cobra.Command{
|
|
Use: "release",
|
|
Aliases: []string{"releases"},
|
|
Short: "Manage releases",
|
|
Long: "Create, view, list, upload assets, and delete releases.",
|
|
}
|
|
|
|
var releaseListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List releases",
|
|
Long: "List releases in a repository.",
|
|
Example: ` # List releases
|
|
fgj release list
|
|
|
|
# List only draft releases
|
|
fgj release list --draft
|
|
|
|
# Output as JSON with a custom limit
|
|
fgj release list --json --limit 10`,
|
|
RunE: runReleaseList,
|
|
}
|
|
|
|
var releaseViewCmd = &cobra.Command{
|
|
Use: "view <tag|latest>",
|
|
Short: "View a release",
|
|
Long: "Display detailed information about a release.",
|
|
Example: ` # View a release by tag
|
|
fgj release view v1.0.0
|
|
|
|
# View the latest release
|
|
fgj release view latest
|
|
|
|
# Open in browser
|
|
fgj release view v1.0.0 --web
|
|
|
|
# Output as JSON
|
|
fgj release view v1.0.0 --json`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runReleaseView,
|
|
}
|
|
|
|
var releaseCreateCmd = &cobra.Command{
|
|
Use: "create <tag> [files...]",
|
|
Short: "Create a release",
|
|
Long: "Create a new release and optionally upload assets.",
|
|
Example: ` # Create a release
|
|
fgj release create v1.0.0
|
|
|
|
# Create with title and notes
|
|
fgj release create v1.0.0 -t "First stable release" -n "Bug fixes and improvements"
|
|
|
|
# Create a draft prerelease with assets
|
|
fgj release create v2.0.0-rc1 --draft --prerelease dist/*.tar.gz
|
|
|
|
# Create from release notes file
|
|
fgj release create v1.0.0 -F CHANGELOG.md`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: runReleaseCreate,
|
|
}
|
|
|
|
var releaseUploadCmd = &cobra.Command{
|
|
Use: "upload <tag|latest> <files...>",
|
|
Short: "Upload release assets",
|
|
Long: "Upload assets to an existing release.",
|
|
Example: ` # Upload assets to a release
|
|
fgj release upload v1.0.0 dist/app-linux-amd64 dist/app-darwin-arm64
|
|
|
|
# Upload to the latest release, overwriting existing assets
|
|
fgj release upload latest build/output.zip --clobber`,
|
|
Args: cobra.MinimumNArgs(2),
|
|
RunE: runReleaseUpload,
|
|
}
|
|
|
|
var releaseDownloadCmd = &cobra.Command{
|
|
Use: "download <tag>",
|
|
Short: "Download release assets",
|
|
Long: "Download assets from a release.",
|
|
Example: ` # Download all assets from a release
|
|
fgj release download v1.0.0
|
|
|
|
# Download to a specific directory
|
|
fgj release download v1.0.0 -D ./downloads
|
|
|
|
# Download a specific asset by name pattern
|
|
fgj release download v1.0.0 -p "*.tar.gz"`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runReleaseDownload,
|
|
}
|
|
|
|
var releaseDeleteCmd = &cobra.Command{
|
|
Use: "delete <tag|latest>",
|
|
Short: "Delete a release",
|
|
Long: "Delete a release by tag, keeping its Git tag intact.",
|
|
Example: ` # Delete a release by tag
|
|
fgj release delete v1.0.0
|
|
|
|
# Delete the latest release
|
|
fgj release delete latest
|
|
|
|
# Delete without confirmation
|
|
fgj release delete v1.0.0 -y`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runReleaseDelete,
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(releaseCmd)
|
|
releaseCmd.AddCommand(releaseListCmd)
|
|
releaseCmd.AddCommand(releaseViewCmd)
|
|
releaseCmd.AddCommand(releaseCreateCmd)
|
|
releaseCmd.AddCommand(releaseUploadCmd)
|
|
releaseCmd.AddCommand(releaseDownloadCmd)
|
|
releaseCmd.AddCommand(releaseDeleteCmd)
|
|
|
|
releaseListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
releaseListCmd.Flags().Bool("draft", false, "Filter by draft status")
|
|
releaseListCmd.Flags().Bool("prerelease", false, "Filter by prerelease status")
|
|
releaseListCmd.Flags().Int("limit", 30, "Maximum number of releases to fetch")
|
|
addJSONFlags(releaseListCmd, "Output releases as JSON")
|
|
|
|
releaseViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
addJSONFlags(releaseViewCmd, "Output release as JSON")
|
|
releaseViewCmd.Flags().BoolP("web", "w", false, "Open in web browser")
|
|
|
|
releaseCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
releaseCreateCmd.Flags().StringP("title", "t", "", "Release title (defaults to tag)")
|
|
releaseCreateCmd.Flags().StringP("notes", "n", "", "Release notes")
|
|
releaseCreateCmd.Flags().StringP("notes-file", "F", "", "Read release notes from file")
|
|
releaseCreateCmd.Flags().Bool("draft", false, "Create a draft release")
|
|
releaseCreateCmd.Flags().Bool("prerelease", false, "Mark the release as prerelease")
|
|
releaseCreateCmd.Flags().String("target", "", "Target commitish (branch or SHA)")
|
|
|
|
releaseUploadCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
releaseUploadCmd.Flags().Bool("clobber", false, "Overwrite assets with the same name")
|
|
|
|
releaseDownloadCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
releaseDownloadCmd.Flags().StringP("dir", "D", ".", "Directory to download files into")
|
|
releaseDownloadCmd.Flags().StringP("pattern", "p", "", "Glob pattern to filter assets by name")
|
|
|
|
releaseDeleteCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
releaseDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
|
|
}
|
|
|
|
func runReleaseList(cmd *cobra.Command, args []string) error {
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
limit, _ := cmd.Flags().GetInt("limit")
|
|
draftValue, _ := cmd.Flags().GetBool("draft")
|
|
prereleaseValue, _ := cmd.Flags().GetBool("prerelease")
|
|
draftSet := cmd.Flags().Changed("draft")
|
|
prereleaseSet := cmd.Flags().Changed("prerelease")
|
|
|
|
if limit <= 0 {
|
|
return fmt.Errorf("limit must be greater than 0")
|
|
}
|
|
|
|
owner, name, err := parseRepo(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pageSize := limit
|
|
if pageSize > 50 {
|
|
pageSize = 50
|
|
}
|
|
|
|
opts := gitea.ListReleasesOptions{}
|
|
if draftSet {
|
|
opts.IsDraft = &draftValue
|
|
}
|
|
if prereleaseSet {
|
|
opts.IsPreRelease = &prereleaseValue
|
|
}
|
|
|
|
ios.StartSpinner("Fetching releases...")
|
|
var releases []*gitea.Release
|
|
for page := 1; len(releases) < limit; page++ {
|
|
opts.ListOptions = gitea.ListOptions{Page: page, PageSize: pageSize}
|
|
batch, _, err := client.ListReleases(owner, name, opts)
|
|
if err != nil {
|
|
ios.StopSpinner()
|
|
return fmt.Errorf("failed to list releases: %w", err)
|
|
}
|
|
if len(batch) == 0 {
|
|
break
|
|
}
|
|
releases = append(releases, batch...)
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
if len(releases) > limit {
|
|
releases = releases[:limit]
|
|
}
|
|
|
|
if wantJSON(cmd) {
|
|
return outputJSON(cmd, releases)
|
|
}
|
|
|
|
if len(releases) == 0 {
|
|
fmt.Fprintf(ios.Out, "No releases in %s/%s\n", owner, name)
|
|
return nil
|
|
}
|
|
|
|
isTTY := ios.IsStdoutTTY()
|
|
tp := ios.NewTablePrinter()
|
|
tp.AddHeader("TAG", "TITLE", "TYPE", "PUBLISHED")
|
|
for _, rel := range releases {
|
|
published := text.FormatDate(releaseTimestamp(rel), isTTY)
|
|
tp.AddRow(rel.TagName, rel.Title, releaseType(rel), published)
|
|
}
|
|
return tp.Render()
|
|
}
|
|
|
|
func runReleaseView(cmd *cobra.Command, args []string) error {
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
tag := args[0]
|
|
|
|
owner, name, err := parseRepo(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ios.StartSpinner("Fetching release...")
|
|
release, err := getReleaseByTagOrLatest(client, owner, name, tag)
|
|
if err != nil {
|
|
ios.StopSpinner()
|
|
return err
|
|
}
|
|
|
|
attachments, err := listReleaseAttachments(client, owner, name, release.ID)
|
|
ios.StopSpinner()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if web, _ := cmd.Flags().GetBool("web"); web {
|
|
if release.HTMLURL != "" {
|
|
return ios.OpenInBrowser(release.HTMLURL)
|
|
}
|
|
return fmt.Errorf("release has no HTML URL")
|
|
}
|
|
|
|
if wantJSON(cmd) {
|
|
payload := struct {
|
|
Release *gitea.Release `json:"release"`
|
|
Assets []*gitea.Attachment `json:"assets,omitempty"`
|
|
}{
|
|
Release: release,
|
|
Assets: attachments,
|
|
}
|
|
return outputJSON(cmd, payload)
|
|
}
|
|
|
|
if err := ios.StartPager(); err != nil {
|
|
fmt.Fprintf(ios.ErrOut, "warning: failed to start pager: %v\n", err)
|
|
}
|
|
defer ios.StopPager()
|
|
|
|
cs := ios.ColorScheme()
|
|
isTTY := ios.IsStdoutTTY()
|
|
|
|
fmt.Fprintf(ios.Out, "Release %s\n", cs.Bold(release.TagName))
|
|
fmt.Fprintf(ios.Out, "Title: %s\n", release.Title)
|
|
fmt.Fprintf(ios.Out, "Type: %s\n", releaseType(release))
|
|
if release.Target != "" {
|
|
fmt.Fprintf(ios.Out, "Target: %s\n", release.Target)
|
|
}
|
|
if release.Publisher != nil {
|
|
fmt.Fprintf(ios.Out, "Author: %s\n", release.Publisher.UserName)
|
|
}
|
|
fmt.Fprintf(ios.Out, "Created: %s\n", text.FormatDate(release.CreatedAt, isTTY))
|
|
if !release.PublishedAt.IsZero() {
|
|
fmt.Fprintf(ios.Out, "Published: %s\n", text.FormatDate(release.PublishedAt, isTTY))
|
|
}
|
|
if release.HTMLURL != "" {
|
|
fmt.Fprintf(ios.Out, "URL: %s\n", release.HTMLURL)
|
|
}
|
|
if release.Note != "" {
|
|
fmt.Fprintf(ios.Out, "\n%s\n", release.Note)
|
|
}
|
|
|
|
if len(attachments) > 0 {
|
|
fmt.Fprintf(ios.Out, "\nAssets (%d):\n", len(attachments))
|
|
for _, asset := range attachments {
|
|
fmt.Fprintf(ios.Out, "- %s (%d bytes) %s\n", asset.Name, asset.Size, asset.DownloadURL)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runReleaseCreate(cmd *cobra.Command, args []string) error {
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
title, _ := cmd.Flags().GetString("title")
|
|
notes, _ := cmd.Flags().GetString("notes")
|
|
notesFile, _ := cmd.Flags().GetString("notes-file")
|
|
draft, _ := cmd.Flags().GetBool("draft")
|
|
prerelease, _ := cmd.Flags().GetBool("prerelease")
|
|
target, _ := cmd.Flags().GetString("target")
|
|
|
|
if notes != "" && notesFile != "" {
|
|
return fmt.Errorf("use either --notes or --notes-file, not both")
|
|
}
|
|
|
|
tag := args[0]
|
|
files := args[1:]
|
|
|
|
if notesFile != "" {
|
|
content, err := os.ReadFile(notesFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read notes file: %w", err)
|
|
}
|
|
notes = string(content)
|
|
}
|
|
|
|
if title == "" {
|
|
title = tag
|
|
}
|
|
|
|
owner, name, err := parseRepo(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ios.StartSpinner("Creating release...")
|
|
release, _, err := client.CreateRelease(owner, name, gitea.CreateReleaseOption{
|
|
TagName: tag,
|
|
Target: target,
|
|
Title: title,
|
|
Note: notes,
|
|
IsDraft: draft,
|
|
IsPrerelease: prerelease,
|
|
})
|
|
ios.StopSpinner()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create release: %w", err)
|
|
}
|
|
|
|
cs := ios.ColorScheme()
|
|
fmt.Fprintf(ios.Out, "%s Release created: %s\n", cs.SuccessIcon(), release.TagName)
|
|
if release.HTMLURL != "" {
|
|
fmt.Fprintf(ios.Out, "View at: %s\n", release.HTMLURL)
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ios.StartSpinner("Uploading assets...")
|
|
if err := uploadReleaseAssets(client, owner, name, release.ID, files, false); err != nil {
|
|
ios.StopSpinner()
|
|
return err
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
fmt.Fprintf(ios.Out, "%s Uploaded %s\n", cs.SuccessIcon(), text.Pluralize(len(files), "asset"))
|
|
return nil
|
|
}
|
|
|
|
func runReleaseUpload(cmd *cobra.Command, args []string) error {
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
clobber, _ := cmd.Flags().GetBool("clobber")
|
|
|
|
tag := args[0]
|
|
files := args[1:]
|
|
|
|
owner, name, err := parseRepo(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ios.StartSpinner("Fetching release...")
|
|
release, err := getReleaseByTagOrLatest(client, owner, name, tag)
|
|
ios.StopSpinner()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ios.StartSpinner("Uploading assets...")
|
|
if err := uploadReleaseAssets(client, owner, name, release.ID, files, clobber); err != nil {
|
|
ios.StopSpinner()
|
|
return err
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
cs := ios.ColorScheme()
|
|
fmt.Fprintf(ios.Out, "%s Uploaded %s\n", cs.SuccessIcon(), text.Pluralize(len(files), "asset"))
|
|
return nil
|
|
}
|
|
|
|
func runReleaseDownload(cmd *cobra.Command, args []string) error {
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
dir, _ := cmd.Flags().GetString("dir")
|
|
pattern, _ := cmd.Flags().GetString("pattern")
|
|
tag := args[0]
|
|
|
|
owner, name, err := parseRepo(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ios.StartSpinner("Fetching release...")
|
|
release, err := getReleaseByTagOrLatest(client, owner, name, tag)
|
|
if err != nil {
|
|
ios.StopSpinner()
|
|
return err
|
|
}
|
|
|
|
attachments, err := listReleaseAttachments(client, owner, name, release.ID)
|
|
ios.StopSpinner()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(attachments) == 0 {
|
|
fmt.Fprintf(ios.Out, "No assets found for release %s\n", release.TagName)
|
|
return nil
|
|
}
|
|
|
|
// Filter by pattern if provided
|
|
var toDownload []*gitea.Attachment
|
|
for _, a := range attachments {
|
|
if pattern != "" {
|
|
matched, matchErr := path.Match(pattern, a.Name)
|
|
if matchErr != nil {
|
|
return fmt.Errorf("invalid glob pattern %q: %w", pattern, matchErr)
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
}
|
|
toDownload = append(toDownload, a)
|
|
}
|
|
|
|
if len(toDownload) == 0 {
|
|
fmt.Fprintf(ios.Out, "No assets matching pattern %q in release %s\n", pattern, release.TagName)
|
|
return nil
|
|
}
|
|
|
|
// Ensure download directory exists
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
|
}
|
|
|
|
for _, a := range toDownload {
|
|
destPath := filepath.Join(dir, a.Name)
|
|
f, createErr := os.Create(destPath)
|
|
if createErr != nil {
|
|
return fmt.Errorf("failed to create file %s: %w", destPath, createErr)
|
|
}
|
|
|
|
dlErr := client.DownloadFile(a.DownloadURL, f)
|
|
closeErr := f.Close()
|
|
if dlErr != nil {
|
|
return fmt.Errorf("failed to download %s: %w", a.Name, dlErr)
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("failed to close %s: %w", destPath, closeErr)
|
|
}
|
|
|
|
fmt.Fprintf(ios.Out, "Downloaded %s (%d bytes)\n", a.Name, a.Size)
|
|
}
|
|
|
|
cs := ios.ColorScheme()
|
|
fmt.Fprintf(ios.Out, "\n%s %s downloaded to %s\n", cs.SuccessIcon(), text.Pluralize(len(toDownload), "asset"), dir)
|
|
return nil
|
|
}
|
|
|
|
func runReleaseDelete(cmd *cobra.Command, args []string) error {
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
yes, _ := cmd.Flags().GetBool("yes")
|
|
tag := args[0]
|
|
|
|
owner, name, err := parseRepo(repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ios.StartSpinner("Fetching release...")
|
|
release, err := getReleaseByTagOrLatest(client, owner, name, tag)
|
|
ios.StopSpinner()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !yes {
|
|
confirmed, err := ios.ConfirmAction(fmt.Sprintf("Delete release %s?", release.TagName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !confirmed {
|
|
fmt.Fprintln(ios.ErrOut, "Aborted")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
ios.StartSpinner("Deleting release...")
|
|
if _, err := client.DeleteRelease(owner, name, release.ID); err != nil {
|
|
ios.StopSpinner()
|
|
return fmt.Errorf("failed to delete release: %w", err)
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
cs := ios.ColorScheme()
|
|
fmt.Fprintf(ios.Out, "%s Release %s deleted\n", cs.SuccessIcon(), release.TagName)
|
|
return nil
|
|
}
|
|
|
|
func getReleaseByTagOrLatest(client *api.Client, owner, name, tag string) (*gitea.Release, error) {
|
|
if strings.EqualFold(tag, "latest") {
|
|
release, _, err := client.GetLatestRelease(owner, name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get latest release: %w", err)
|
|
}
|
|
return release, nil
|
|
}
|
|
|
|
release, _, err := client.GetReleaseByTag(owner, name, tag)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get release: %w", err)
|
|
}
|
|
return release, nil
|
|
}
|
|
|
|
func uploadReleaseAssets(client *api.Client, owner, name string, releaseID int64, files []string, clobber bool) error {
|
|
existing := map[string]int64{}
|
|
if clobber {
|
|
attachments, err := listReleaseAttachments(client, owner, name, releaseID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, attachment := range attachments {
|
|
existing[attachment.Name] = attachment.ID
|
|
}
|
|
}
|
|
|
|
for _, file := range files {
|
|
filename := filepath.Base(file)
|
|
if clobber {
|
|
if attachmentID, ok := existing[filename]; ok {
|
|
if _, err := client.DeleteReleaseAttachment(owner, name, releaseID, attachmentID); err != nil {
|
|
return fmt.Errorf("failed to delete existing asset %s: %w", filename, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
handle, err := os.Open(file)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open %s: %w", file, err)
|
|
}
|
|
|
|
_, _, err = client.CreateReleaseAttachment(owner, name, releaseID, handle, filename)
|
|
closeErr := handle.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upload %s: %w", file, err)
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("failed to close %s: %w", file, closeErr)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func listReleaseAttachments(client *api.Client, owner, name string, releaseID int64) ([]*gitea.Attachment, error) {
|
|
var all []*gitea.Attachment
|
|
for page := 1; ; page++ {
|
|
attachments, _, err := client.ListReleaseAttachments(owner, name, releaseID, gitea.ListReleaseAttachmentsOptions{
|
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list release assets: %w", err)
|
|
}
|
|
if len(attachments) == 0 {
|
|
break
|
|
}
|
|
all = append(all, attachments...)
|
|
}
|
|
return all, nil
|
|
}
|
|
|
|
func releaseType(release *gitea.Release) string {
|
|
if release.IsDraft {
|
|
return "draft"
|
|
}
|
|
if release.IsPrerelease {
|
|
return "prerelease"
|
|
}
|
|
return "release"
|
|
}
|
|
|
|
func releaseTimestamp(release *gitea.Release) time.Time {
|
|
if !release.PublishedAt.IsZero() {
|
|
return release.PublishedAt
|
|
}
|
|
return release.CreatedAt
|
|
}
|