feat(release): +create 新增 --attachment-files 本地文件自动上传并附加
This commit is contained in:
parent
19209c5cea
commit
78aee0cf5c
|
|
@ -211,6 +211,7 @@
|
|||
"flag.profile.start_time": "Start time (Unix timestamp)",
|
||||
"flag.profile.user": "Target user login (defaults to the authenticated user)",
|
||||
"flag.profile.year": "Year for the contribution heatmap (e.g. 2025)",
|
||||
"flag.release.attachment_files": "Comma-separated local files to upload and attach",
|
||||
"flag.release.body": "Release notes",
|
||||
"flag.release.id": "Release ID",
|
||||
"flag.release.id_or_tag": "Release ID or tag",
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@
|
|||
"flag.profile.start_time": "开始时间(Unix 时间戳)",
|
||||
"flag.profile.user": "目标用户登录名(默认为当前认证用户)",
|
||||
"flag.profile.year": "贡献热力图的年份(如 2025)",
|
||||
"flag.release.attachment_files": "以逗号分隔的本地文件路径,自动上传并附加到发行版",
|
||||
"flag.release.body": "发布说明",
|
||||
"flag.release.id": "发布 ID",
|
||||
"flag.release.id_or_tag": "发布 ID 或标签",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package release
|
|||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -46,6 +47,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
{Name: "prerelease", Usage: tr.T("flag.release.prerelease"), Default: "false"},
|
||||
{Name: "draft", Usage: "Mark as draft (true/false)", Default: "false"},
|
||||
{Name: "attachment-ids", Usage: "Comma-separated attachment IDs"},
|
||||
{Name: "attachment-files", Usage: tr.T("flag.release.attachment_files")},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
|
|
@ -79,11 +81,21 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
if t := ctx.Arg("target"); t != "" {
|
||||
payload["target_commitish"] = t
|
||||
}
|
||||
var ids []string
|
||||
if attachmentIDs := ctx.Arg("attachment-ids"); attachmentIDs != "" {
|
||||
ids, err := parseReleaseAttachmentIDs(attachmentIDs)
|
||||
ids, err = parseReleaseAttachmentIDs(attachmentIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if files := ctx.Arg("attachment-files"); files != "" {
|
||||
uploaded, err := uploadReleaseAttachments(ctx, files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids = append(ids, uploaded...)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
payload["attachment_ids"] = ids
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/releases", payload)
|
||||
|
|
@ -424,3 +436,41 @@ func firstReleaseValue(values ...string) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// uploadReleaseAttachments uploads local files given as a comma-separated
|
||||
// list and returns their attachment ids for use in attachment_ids.
|
||||
func uploadReleaseAttachments(ctx *common.RuntimeContext, files string) ([]string, error) {
|
||||
var ids []string
|
||||
for _, part := range strings.Split(files, ",") {
|
||||
file := strings.TrimSpace(part)
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot access file %q: %w", file, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("%q is a directory, expected a file", file)
|
||||
}
|
||||
env, err := ctx.Client.PostMultipartFile("/attachments", file, "file", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload %q failed: %w", file, err)
|
||||
}
|
||||
data, _ := env.Data.(map[string]interface{})
|
||||
id, _ := data["id"].(string)
|
||||
if id == "" {
|
||||
if num, ok := data["id"].(float64); ok {
|
||||
id = strconv.FormatFloat(num, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("upload %q succeeded but no attachment id was returned", file)
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("--attachment-files must include at least one file")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
|
@ -453,3 +455,58 @@ func ExampleShortcuts() {
|
|||
// update
|
||||
// delete
|
||||
}
|
||||
|
||||
func TestReleaseCreateWithAttachmentFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
asset := filepath.Join(dir, "asset.bin")
|
||||
if err := os.WriteFile(asset, []byte("release asset data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
var uploads int
|
||||
server := newReleaseTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/attachments" || r.URL.Path == "/attachments.json" {
|
||||
uploads++
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
t.Fatalf("parse multipart: %v", err)
|
||||
}
|
||||
writeReleaseJSON(t, w, map[string]interface{}{"id": "uuid-from-upload", "title": "asset.bin"})
|
||||
return
|
||||
}
|
||||
assertReleaseRequest(t, r, "POST", "/owner/repo/releases.json")
|
||||
payload = decodeReleaseJSON(t, r)
|
||||
writeReleaseJSON(t, w, map[string]interface{}{"status": 0, "message": "created"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runReleaseShortcut(t, server, "create", map[string]string{
|
||||
"tag": "v1.0.0",
|
||||
"name": "v1.0.0",
|
||||
"attachment-ids": "12",
|
||||
"attachment-files": asset,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create shortcut failed: %v", err)
|
||||
}
|
||||
if uploads != 1 {
|
||||
t.Fatalf("uploads = %d, want 1", uploads)
|
||||
}
|
||||
assertReleaseStringSlice(t, payload["attachment_ids"], []string{"12", "uuid-from-upload"})
|
||||
}
|
||||
|
||||
func TestReleaseCreateAttachmentFilesMissing(t *testing.T) {
|
||||
server := newReleaseTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runReleaseShortcut(t, server, "create", map[string]string{
|
||||
"tag": "v1.0.0",
|
||||
"name": "v1.0.0",
|
||||
"attachment-files": "/nonexistent/path.bin",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing attachment file")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue