Merge pull request 'feat/releases' (#20) from feat/releases into main
Reviewed-on: https://codeberg.org/romaintb/fgj/pulls/20
This commit is contained in:
commit
2c27823e18
5 changed files with 605 additions and 1 deletions
47
.editorconfig
Normal file
47
.editorconfig
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# Go files - use tabs (Go standard)
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Go mod files
|
||||||
|
[go.{mod,sum}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Makefiles - must use tabs
|
||||||
|
[{Makefile,*.mk}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# YAML files
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# JSON files
|
||||||
|
[*.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Markdown files
|
||||||
|
[*.md]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
# Shell scripts
|
||||||
|
[*.sh]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
6
Makefile
6
Makefile
|
|
@ -1,8 +1,9 @@
|
||||||
.PHONY: help build run test clean lint lint-fix
|
.PHONY: help build run test clean lint lint-fix install
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Available commands:"
|
@echo "Available commands:"
|
||||||
@echo " make build - Build the application"
|
@echo " make build - Build the application"
|
||||||
|
@echo " make install - Install the binary to /usr/bin"
|
||||||
@echo " make run - Run the application"
|
@echo " make run - Run the application"
|
||||||
@echo " make test - Run tests"
|
@echo " make test - Run tests"
|
||||||
@echo " make lint - Run golangci-lint"
|
@echo " make lint - Run golangci-lint"
|
||||||
|
|
@ -12,6 +13,9 @@ help:
|
||||||
build:
|
build:
|
||||||
go build -o bin/fgj .
|
go build -o bin/fgj .
|
||||||
|
|
||||||
|
install: build
|
||||||
|
install -Dm755 bin/fgj /usr/bin/fgj
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go run .
|
go run .
|
||||||
|
|
||||||
|
|
|
||||||
19
README.md
19
README.md
|
|
@ -155,6 +155,25 @@ fgj repo clone owner/repo -p ssh
|
||||||
fgj repo fork owner/repo
|
fgj repo fork owner/repo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Releases
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List releases
|
||||||
|
fgj release list
|
||||||
|
|
||||||
|
# View a release (or use "latest")
|
||||||
|
fgj release view v1.2.3
|
||||||
|
|
||||||
|
# Create a release with notes and optional assets
|
||||||
|
fgj release create v1.2.3 -t "v1.2.3" -n "Release notes" ./dist/app.tar.gz
|
||||||
|
|
||||||
|
# Upload assets to an existing release
|
||||||
|
fgj release upload v1.2.3 ./dist/app.tar.gz --clobber
|
||||||
|
|
||||||
|
# Delete a release (keeps the Git tag)
|
||||||
|
fgj release delete v1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
### Forgejo Actions
|
### Forgejo Actions
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
450
cmd/release.go
Normal file
450
cmd/release.go
Normal file
|
|
@ -0,0 +1,450 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"codeberg.org/romaintb/fgj/internal/api"
|
||||||
|
"codeberg.org/romaintb/fgj/internal/config"
|
||||||
|
"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.",
|
||||||
|
RunE: runReleaseList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var releaseViewCmd = &cobra.Command{
|
||||||
|
Use: "view <tag|latest>",
|
||||||
|
Short: "View a release",
|
||||||
|
Long: "Display detailed information about a release.",
|
||||||
|
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.",
|
||||||
|
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.",
|
||||||
|
Args: cobra.MinimumNArgs(2),
|
||||||
|
RunE: runReleaseUpload,
|
||||||
|
}
|
||||||
|
|
||||||
|
var releaseDeleteCmd = &cobra.Command{
|
||||||
|
Use: "delete <tag|latest>",
|
||||||
|
Short: "Delete a release",
|
||||||
|
Long: "Delete a release by tag, keeping its Git tag intact.",
|
||||||
|
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(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")
|
||||||
|
|
||||||
|
releaseViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
releaseDeleteCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf("failed to list releases: %w", err)
|
||||||
|
}
|
||||||
|
if len(batch) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
releases = append(releases, batch...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(releases) > limit {
|
||||||
|
releases = releases[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(releases) == 0 {
|
||||||
|
fmt.Printf("No releases in %s/%s\n", owner, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
_, _ = fmt.Fprintf(w, "TAG\tTITLE\tTYPE\tPUBLISHED\n")
|
||||||
|
for _, rel := range releases {
|
||||||
|
published := releaseTimestamp(rel).Format("2006-01-02")
|
||||||
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.TagName, rel.Title, releaseType(rel), published)
|
||||||
|
}
|
||||||
|
_ = w.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := getReleaseByTagOrLatest(client, owner, name, tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Release %s\n", release.TagName)
|
||||||
|
fmt.Printf("Title: %s\n", release.Title)
|
||||||
|
fmt.Printf("Type: %s\n", releaseType(release))
|
||||||
|
if release.Target != "" {
|
||||||
|
fmt.Printf("Target: %s\n", release.Target)
|
||||||
|
}
|
||||||
|
if release.Publisher != nil {
|
||||||
|
fmt.Printf("Author: %s\n", release.Publisher.UserName)
|
||||||
|
}
|
||||||
|
fmt.Printf("Created: %s\n", release.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
if !release.PublishedAt.IsZero() {
|
||||||
|
fmt.Printf("Published: %s\n", release.PublishedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
if release.HTMLURL != "" {
|
||||||
|
fmt.Printf("URL: %s\n", release.HTMLURL)
|
||||||
|
}
|
||||||
|
if release.Note != "" {
|
||||||
|
fmt.Printf("\n%s\n", release.Note)
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments, err := listReleaseAttachments(client, owner, name, release.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(attachments) > 0 {
|
||||||
|
fmt.Printf("\nAssets (%d):\n", len(attachments))
|
||||||
|
for _, asset := range attachments {
|
||||||
|
fmt.Printf("- %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, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
release, _, err := client.CreateRelease(owner, name, gitea.CreateReleaseOption{
|
||||||
|
TagName: tag,
|
||||||
|
Target: target,
|
||||||
|
Title: title,
|
||||||
|
Note: notes,
|
||||||
|
IsDraft: draft,
|
||||||
|
IsPrerelease: prerelease,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create release: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Release created: %s\n", release.TagName)
|
||||||
|
if release.HTMLURL != "" {
|
||||||
|
fmt.Printf("View at: %s\n", release.HTMLURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := uploadReleaseAssets(client, owner, name, release.ID, files, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Uploaded %d asset(s)\n", len(files))
|
||||||
|
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, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := getReleaseByTagOrLatest(client, owner, name, tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := uploadReleaseAssets(client, owner, name, release.ID, files, clobber); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Uploaded %d asset(s)\n", len(files))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runReleaseDelete(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, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := getReleaseByTagOrLatest(client, owner, name, tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.DeleteRelease(owner, name, release.ID); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete release: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Release %s deleted\n", 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
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build functional
|
||||||
// +build functional
|
// +build functional
|
||||||
|
|
||||||
package functional
|
package functional
|
||||||
|
|
@ -7,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
)
|
)
|
||||||
|
|
@ -620,3 +622,85 @@ func TestCLIRepoClone(t *testing.T) {
|
||||||
|
|
||||||
t.Logf("Successfully cloned repository to %s via CLI", clonePath)
|
t.Logf("Successfully cloned repository to %s via CLI", clonePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCLIReleaseCreateUploadDelete verifies release create/upload/delete via CLI.
|
||||||
|
func TestCLIReleaseCreateUploadDelete(t *testing.T) {
|
||||||
|
env := NewTestEnv(t)
|
||||||
|
|
||||||
|
tag := fmt.Sprintf("fgj-test-%d", time.Now().UnixNano())
|
||||||
|
title := "FGJ CLI Release Test"
|
||||||
|
notes := "Release created by functional tests"
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
assetPath := fmt.Sprintf("%s/asset.txt", tmpDir)
|
||||||
|
if err := os.WriteFile(assetPath, []byte("fgj release asset"), 0600); err != nil {
|
||||||
|
t.Fatalf("failed to create asset file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := env.RunCLI(
|
||||||
|
"--hostname", env.Hostname,
|
||||||
|
"release", "create", tag,
|
||||||
|
"-t", title,
|
||||||
|
"-n", notes,
|
||||||
|
assetPath,
|
||||||
|
)
|
||||||
|
if result.ExitCode != 0 {
|
||||||
|
t.Fatalf("release create failed with exit code %d: %s", result.ExitCode, result.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
release, _, err := env.Client.GetReleaseByTag(env.Owner, env.RepoName, tag)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to fetch created release: %v", err)
|
||||||
|
}
|
||||||
|
if release.Title != title {
|
||||||
|
t.Fatalf("expected release title %q, got %q", title, release.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments, _, err := env.Client.ListReleaseAttachments(env.Owner, env.RepoName, release.ID, gitea.ListReleaseAttachmentsOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to list release assets: %v", err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
if attachment.Name == "asset.txt" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("uploaded asset not found in release")
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteResult := env.RunCLI(
|
||||||
|
"--hostname", env.Hostname,
|
||||||
|
"release", "delete", tag,
|
||||||
|
)
|
||||||
|
if deleteResult.ExitCode != 0 {
|
||||||
|
t.Fatalf("release delete failed with exit code %d: %s", deleteResult.ExitCode, deleteResult.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = env.Client.GetReleaseByTag(env.Owner, env.RepoName, tag)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected release %s to be deleted, but it still exists", tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCLIReleaseList verifies the `fgj release list` command works.
|
||||||
|
func TestCLIReleaseList(t *testing.T) {
|
||||||
|
env := NewTestEnv(t)
|
||||||
|
|
||||||
|
result := env.RunCLI(
|
||||||
|
"--hostname", env.Hostname,
|
||||||
|
"release", "list",
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.ExitCode != 0 {
|
||||||
|
t.Fatalf("release list failed with exit code %d: %s", result.ExitCode, result.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Stdout == "" && result.Stderr == "" {
|
||||||
|
t.Logf("Note: release list produced no output")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Successfully listed releases via CLI")
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue