diff --git a/gitea/releases.go b/gitea/releases.go index 167ed15..7db5b36 100644 --- a/gitea/releases.go +++ b/gitea/releases.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "strconv" "code.gitea.io/sdk/gitea" ) @@ -24,6 +25,7 @@ func (f *giteaForge) Releases() forge.ReleaseService { func convertGiteaRelease(r *gitea.Release) forge.Release { result := forge.Release{ + ID: r.ID, TagName: r.TagName, Title: r.Title, Body: r.Note, @@ -88,10 +90,31 @@ func (s *giteaReleaseService) List(ctx context.Context, owner, repo string, opts return all, nil } -func (s *giteaReleaseService) Get(ctx context.Context, owner, repo, tag string) (*forge.Release, error) { +// getReleaseByTagOrID resolves a release by tag, falling back to a numeric +// release ID when the tag lookup returns 404. +func (s *giteaReleaseService) getReleaseByTagOrID(owner, repo, tag, op string) (*gitea.Release, error) { r, resp, err := s.client.GetReleaseByTag(owner, repo, tag) + if err == nil { + return r, nil + } + if resp == nil || resp.StatusCode != http.StatusNotFound { + return nil, wrapErr(op, resp, err) + } + id, parseErr := strconv.ParseInt(tag, 10, 64) + if parseErr != nil { + return nil, forge.ErrNotFound + } + r, resp, err = s.client.GetRelease(owner, repo, id) + if err != nil { + return nil, wrapErr(op, resp, err) + } + return r, nil +} + +func (s *giteaReleaseService) Get(ctx context.Context, owner, repo, tag string) (*forge.Release, error) { + r, err := s.getReleaseByTagOrID(owner, repo, tag, "get release") if err != nil { - return nil, wrapErr("get release", resp, err) + return nil, err } result := convertGiteaRelease(r) return &result, nil @@ -142,9 +165,9 @@ func (s *giteaReleaseService) Create(ctx context.Context, owner, repo string, op } func (s *giteaReleaseService) Update(ctx context.Context, owner, repo, tag string, opts forge.UpdateReleaseOpts) (*forge.Release, error) { - existing, resp, err := s.client.GetReleaseByTag(owner, repo, tag) + existing, err := s.getReleaseByTagOrID(owner, repo, tag, "update release") if err != nil { - return nil, wrapErr("update release", resp, err) + return nil, err } gOpts := gitea.EditReleaseOption{} @@ -189,10 +212,11 @@ func (s *giteaReleaseService) Update(ctx context.Context, owner, repo, tag strin } func (s *giteaReleaseService) Delete(ctx context.Context, owner, repo, tag string) error { - existing, resp, err := s.client.GetReleaseByTag(owner, repo, tag) + existing, err := s.getReleaseByTagOrID(owner, repo, tag, "delete release") if err != nil { - return wrapErr("delete release", resp, err) + return err } + var resp *gitea.Response resp, err = s.client.DeleteRelease(owner, repo, existing.ID) if err != nil { @@ -202,10 +226,11 @@ func (s *giteaReleaseService) Delete(ctx context.Context, owner, repo, tag strin } func (s *giteaReleaseService) UploadAsset(ctx context.Context, owner, repo, tag string, file *os.File) (*forge.ReleaseAsset, error) { - existing, resp, err := s.client.GetReleaseByTag(owner, repo, tag) + existing, err := s.getReleaseByTagOrID(owner, repo, tag, "upload release asset") if err != nil { - return nil, wrapErr("upload release asset", resp, err) + return nil, err } + var resp *gitea.Response name := filepath.Base(file.Name()) a, resp, err := s.client.CreateReleaseAttachment(owner, repo, existing.ID, file, name) diff --git a/gitea/releases_test.go b/gitea/releases_test.go index 9cb35f2..b4b4d1e 100644 --- a/gitea/releases_test.go +++ b/gitea/releases_test.go @@ -6,8 +6,12 @@ import ( forge "github.com/git-pkgs/forge" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" + + "code.gitea.io/sdk/gitea" ) func TestCreateReleaseAlreadyExists(t *testing.T) { @@ -124,3 +128,181 @@ func TestCreateReleaseNotFound(t *testing.T) { t.Fatalf("expected ErrNotFound, got %v", err) } } + +func TestConvertGiteaReleasePopulatesID(t *testing.T) { + rel := convertGiteaRelease(&gitea.Release{ + ID: 777, + TagName: "v7.7.7", + Title: "Release 7.7.7", + IsDraft: true, + }) + if rel.ID != 777 { + t.Errorf("ID: want 777, got %d", rel.ID) + } + if rel.TagName != "v7.7.7" { + t.Errorf("TagName: want %q, got %q", "v7.7.7", rel.TagName) + } +} + +func TestGiteaReleaseGetByIDFallback(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/version", giteaVersionHandler) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/tags/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"release not found"}`)) + }) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":9,"tag_name":"v0.9.0-draft","name":"Draft 9","draft":true}`)) + }) + srv := httptest.NewServer(mux) + defer srv.Close() + + f := New(srv.URL, "", nil) + rel, err := f.Releases().Get(context.Background(), "o", "r", "9") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rel.ID != 9 { + t.Errorf("ID: want 9, got %d", rel.ID) + } + if rel.TagName != "v0.9.0-draft" { + t.Errorf("TagName: want %q, got %q", "v0.9.0-draft", rel.TagName) + } + if !rel.Draft { + t.Errorf("Draft: want true, got false") + } +} + +func TestGiteaReleaseUpdateByIDFallback(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/version", giteaVersionHandler) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/tags/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"release not found"}`)) + }) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":9,"tag_name":"v0.9.0-draft","name":"Draft 9","draft":true}`)) + }) + mux.HandleFunc("PATCH /api/v1/repos/o/r/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":9,"tag_name":"v0.9.0-draft","name":"Draft 9 renamed","draft":true}`)) + }) + srv := httptest.NewServer(mux) + defer srv.Close() + + f := New(srv.URL, "", nil) + title := "Draft 9 renamed" + rel, err := f.Releases().Update(context.Background(), "o", "r", "9", forge.UpdateReleaseOpts{ + Title: &title, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rel.ID != 9 { + t.Errorf("ID: want 9, got %d", rel.ID) + } + if rel.Title != "Draft 9 renamed" { + t.Errorf("Title: want %q, got %q", "Draft 9 renamed", rel.Title) + } +} + +func TestGiteaReleaseDeleteByIDFallback(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/version", giteaVersionHandler) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/tags/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"release not found"}`)) + }) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":9,"tag_name":"v0.9.0-draft","draft":true}`)) + }) + mux.HandleFunc("DELETE /api/v1/repos/o/r/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + srv := httptest.NewServer(mux) + defer srv.Close() + + f := New(srv.URL, "", nil) + if err := f.Releases().Delete(context.Background(), "o", "r", "9"); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGiteaReleaseUploadAssetByIDFallback(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/version", giteaVersionHandler) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/tags/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"release not found"}`)) + }) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":9,"tag_name":"v0.9.0-draft","draft":true}`)) + }) + mux.HandleFunc("POST /api/v1/repos/o/r/releases/9/assets", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(`{"id":100,"name":"notes.txt","size":11,"browser_download_url":"https://example.com/o/r/releases/download/9/notes.txt"}`)) + }) + srv := httptest.NewServer(mux) + defer srv.Close() + + tmp := filepath.Join(t.TempDir(), "notes.txt") + if err := os.WriteFile(tmp, []byte("hello world"), 0o600); err != nil { + t.Fatalf("writing temp file: %v", err) + } + file, err := os.Open(tmp) + if err != nil { + t.Fatalf("opening temp file: %v", err) + } + defer func() { _ = file.Close() }() + + f := New(srv.URL, "", nil) + asset, err := f.Releases().UploadAsset(context.Background(), "o", "r", "9", file) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if asset.Name != "notes.txt" { + t.Errorf("Name: want %q, got %q", "notes.txt", asset.Name) + } + if asset.Size != 11 { + t.Errorf("Size: want 11, got %d", asset.Size) + } +} + +func TestGiteaReleaseGetMissingTagNotFound(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/version", giteaVersionHandler) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/tags/nope", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"release not found"}`)) + }) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/tags/4242", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"release not found"}`)) + }) + mux.HandleFunc("GET /api/v1/repos/o/r/releases/4242", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"release not found"}`)) + }) + srv := httptest.NewServer(mux) + defer srv.Close() + + f := New(srv.URL, "", nil) + if _, err := f.Releases().Get(context.Background(), "o", "r", "nope"); !errors.Is(err, forge.ErrNotFound) { + t.Errorf("non-numeric missing tag: expected forge.ErrNotFound, got %v", err) + } + if _, err := f.Releases().Get(context.Background(), "o", "r", "4242"); !errors.Is(err, forge.ErrNotFound) { + t.Errorf("numeric missing tag: expected forge.ErrNotFound, got %v", err) + } +} diff --git a/github/helpers_test.go b/github/helpers_test.go index bc9e063..f1e7928 100644 --- a/github/helpers_test.go +++ b/github/helpers_test.go @@ -36,6 +36,13 @@ func assertEqualInt(t *testing.T, field string, want, got int) { } } +func assertEqualInt64(t *testing.T, field string, want, got int64) { + t.Helper() + if want != got { + t.Errorf("%s: want %d, got %d", field, want, got) + } +} + func assertSliceEqual(t *testing.T, field string, want, got []string) { t.Helper() if len(want) != len(got) { diff --git a/github/releases.go b/github/releases.go index 81373d4..f906ec8 100644 --- a/github/releases.go +++ b/github/releases.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path/filepath" + "strconv" "github.com/google/go-github/v82/github" ) @@ -21,6 +22,7 @@ func (f *gitHubForge) Releases() forge.ReleaseService { func convertGitHubRelease(r *github.RepositoryRelease) forge.Release { result := forge.Release{ + ID: r.GetID(), TagName: r.GetTagName(), Title: r.GetName(), Body: r.GetBody(), @@ -100,14 +102,36 @@ func (s *gitHubReleaseService) List(ctx context.Context, owner, repo string, opt return all, nil } -func (s *gitHubReleaseService) Get(ctx context.Context, owner, repo, tag string) (*forge.Release, error) { +// Draft releases 404 by tag on GitHub, so fall back to treating tag as a release ID. +func (s *gitHubReleaseService) getReleaseByTagOrID(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, error) { r, resp, err := s.client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) + if err == nil { + return r, nil + } + if resp == nil || resp.StatusCode != http.StatusNotFound { + return nil, err + } + + id, parseErr := strconv.ParseInt(tag, 10, 64) + if parseErr != nil { + return nil, forge.ErrNotFound + } + + r, resp, err = s.client.Repositories.GetRelease(ctx, owner, repo, id) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { return nil, forge.ErrNotFound } return nil, err } + return r, nil +} + +func (s *gitHubReleaseService) Get(ctx context.Context, owner, repo, tag string) (*forge.Release, error) { + r, err := s.getReleaseByTagOrID(ctx, owner, repo, tag) + if err != nil { + return nil, err + } result := convertGitHubRelease(r) return &result, nil } @@ -152,11 +176,8 @@ func (s *gitHubReleaseService) Create(ctx context.Context, owner, repo string, o func (s *gitHubReleaseService) Update(ctx context.Context, owner, repo, tag string, opts forge.UpdateReleaseOpts) (*forge.Release, error) { // Get existing release to find its ID - existing, resp, err := s.client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) + existing, err := s.getReleaseByTagOrID(ctx, owner, repo, tag) if err != nil { - if resp != nil && resp.StatusCode == http.StatusNotFound { - return nil, forge.ErrNotFound - } return nil, err } @@ -205,15 +226,12 @@ func (s *gitHubReleaseService) Update(ctx context.Context, owner, repo, tag stri } func (s *gitHubReleaseService) Delete(ctx context.Context, owner, repo, tag string) error { - existing, resp, err := s.client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) + existing, err := s.getReleaseByTagOrID(ctx, owner, repo, tag) if err != nil { - if resp != nil && resp.StatusCode == http.StatusNotFound { - return forge.ErrNotFound - } return err } - resp, err = s.client.Repositories.DeleteRelease(ctx, owner, repo, existing.GetID()) + resp, err := s.client.Repositories.DeleteRelease(ctx, owner, repo, existing.GetID()) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { return forge.ErrNotFound @@ -224,11 +242,8 @@ func (s *gitHubReleaseService) Delete(ctx context.Context, owner, repo, tag stri } func (s *gitHubReleaseService) UploadAsset(ctx context.Context, owner, repo, tag string, file *os.File) (*forge.ReleaseAsset, error) { - existing, resp, err := s.client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) + existing, err := s.getReleaseByTagOrID(ctx, owner, repo, tag) if err != nil { - if resp != nil && resp.StatusCode == http.StatusNotFound { - return nil, forge.ErrNotFound - } return nil, err } diff --git a/github/releases_id_test.go b/github/releases_id_test.go new file mode 100644 index 0000000..bd0ee12 --- /dev/null +++ b/github/releases_id_test.go @@ -0,0 +1,181 @@ +package github + +import ( + "context" + "fmt" + forge "github.com/git-pkgs/forge" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/google/go-github/v82/github" +) + +func TestGitHubReleaseGetByIDFallback(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/tags/9", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + }) + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{ + "id": 9, + "tag_name": "v0.9.0-draft", + "name": "Draft 9", + "body": "unpublished notes", + "draft": true, + "prerelease": false + }`) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + svc := newTestGitHubReleaseService(srv) + rel, err := svc.Get(context.Background(), "octocat", "hello", "9") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertEqual(t, "TagName", "v0.9.0-draft", rel.TagName) + assertEqualBool(t, "Draft", true, rel.Draft) + assertEqualInt64(t, "ID", 9, rel.ID) +} + +func TestGitHubReleaseUploadAssetByIDFallback(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/tags/9", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + }) + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"id": 9, "tag_name": "v0.9.0-draft", "draft": true}`) + }) + // go-github posts uploads to /api/uploads/... on enterprise URLs, so the stub must match that path. + mux.HandleFunc("POST /api/v3/api/uploads/repos/octocat/hello/releases/9/assets", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = fmt.Fprint(w, `{ + "id": 100, + "name": "notes.txt", + "size": 11, + "download_count": 0, + "browser_download_url": "https://github.com/octocat/hello/releases/download/9/notes.txt" + }`) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + tmp := filepath.Join(t.TempDir(), "notes.txt") + if err := os.WriteFile(tmp, []byte("hello world"), 0o600); err != nil { + t.Fatalf("writing temp file: %v", err) + } + f, err := os.Open(tmp) + if err != nil { + t.Fatalf("opening temp file: %v", err) + } + defer func() { _ = f.Close() }() + + svc := newTestGitHubReleaseService(srv) + asset, err := svc.UploadAsset(context.Background(), "octocat", "hello", "9", f) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertEqual(t, "Asset.Name", "notes.txt", asset.Name) + assertEqualInt(t, "Asset.Size", 11, asset.Size) +} + +func TestGitHubReleaseUpdateByIDFallback(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/tags/9", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + }) + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"id": 9, "tag_name": "v0.9.0-draft", "draft": true}`) + }) + mux.HandleFunc("PATCH /api/v3/repos/octocat/hello/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"id": 9, "tag_name": "v0.9.0-draft", "name": "Draft 9 renamed", "draft": true}`) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + svc := newTestGitHubReleaseService(srv) + rel, err := svc.Update(context.Background(), "octocat", "hello", "9", forge.UpdateReleaseOpts{ + Title: ptr("Draft 9 renamed"), + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertEqualInt64(t, "ID", 9, rel.ID) + assertEqual(t, "Title", "Draft 9 renamed", rel.Title) +} + +func TestGitHubReleaseDeleteByIDFallback(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/tags/9", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + }) + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, `{"id": 9, "tag_name": "v0.9.0-draft", "draft": true}`) + }) + mux.HandleFunc("DELETE /api/v3/repos/octocat/hello/releases/9", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + svc := newTestGitHubReleaseService(srv) + if err := svc.Delete(context.Background(), "octocat", "hello", "9"); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGitHubReleaseGetMissingTagNotFound(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/tags/nope", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + }) + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/tags/4242", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + }) + mux.HandleFunc("GET /api/v3/repos/octocat/hello/releases/4242", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + svc := newTestGitHubReleaseService(srv) + if _, err := svc.Get(context.Background(), "octocat", "hello", "nope"); err != forge.ErrNotFound { + t.Errorf("non-numeric missing tag: expected forge.ErrNotFound, got %v", err) + } + if _, err := svc.Get(context.Background(), "octocat", "hello", "4242"); err != forge.ErrNotFound { + t.Errorf("numeric missing tag: expected forge.ErrNotFound, got %v", err) + } +} + +func TestConvertGitHubReleasePopulatesID(t *testing.T) { + rel := convertGitHubRelease(&github.RepositoryRelease{ + ID: ptrInt64(12345), + TagName: ptr("v9.9.9"), + Name: ptr("Draft 9"), + Draft: ptrBool(true), + }) + assertEqualInt64(t, "ID", 12345, rel.ID) + assertEqual(t, "TagName", "v9.9.9", rel.TagName) + assertEqualBool(t, "Draft", true, rel.Draft) +} diff --git a/internal/cli/release.go b/internal/cli/release.go index c10e245..4155818 100644 --- a/internal/cli/release.go +++ b/internal/cli/release.go @@ -94,6 +94,7 @@ func releaseViewCmd() *cobra.Command { return &cobra.Command{ Use: "view ", Short: "View a release", + Long: "View a release. Pass the release ID instead of the tag to target a draft release, since drafts cannot be looked up by tag.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { tag := args[0] @@ -225,6 +226,7 @@ func releaseEditCmd() *cobra.Command { cmd := &cobra.Command{ Use: "edit ", Short: "Edit a release", + Long: "Edit a release. Pass the release ID instead of the tag to target a draft release, since drafts cannot be looked up by tag.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { tag := args[0] @@ -284,6 +286,7 @@ func releaseDeleteCmd() *cobra.Command { cmd := &cobra.Command{ Use: "delete ", Short: "Delete a release", + Long: "Delete a release. Pass the release ID instead of the tag to target a draft release, since drafts cannot be looked up by tag.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { tag := args[0] @@ -338,6 +341,7 @@ func releaseUploadCmd() *cobra.Command { cmd := &cobra.Command{ Use: "upload ( | --tag )", Short: "Upload an asset to a release", + Long: "Upload an asset to a release. Pass the release ID instead of the tag to target a draft release, since drafts cannot be looked up by tag.", Args: func(cmd *cobra.Command, args []string) error { _, _, err := uploadTagAndFile(flagTag, cmd.Flags().Changed("tag"), args) return err @@ -387,7 +391,7 @@ func releaseDownloadCmd() *cobra.Command { cmd := &cobra.Command{ Use: "download ", Short: "Download a release asset", - Long: "Downloads a release asset by first listing assets to find the matching name, then downloading by ID.", + Long: "Downloads a release asset by first listing assets to find the matching name, then downloading by ID. Pass the release ID instead of the tag to target a draft release, since drafts cannot be looked up by tag.", Args: cobra.ExactArgs(releaseAssetArgs), RunE: func(cmd *cobra.Command, args []string) error { tag := args[0] diff --git a/types.go b/types.go index 2220bb9..b81e905 100644 --- a/types.go +++ b/types.go @@ -372,6 +372,7 @@ type UpdateMilestoneOpts struct { // Release holds normalized metadata about a release. type Release struct { + ID int64 `json:"id"` TagName string `json:"tag_name"` Title string `json:"title"` Body string `json:"body,omitempty"`