From 9c12138d6273ba8feb6e7578833cc330c0cd6461 Mon Sep 17 00:00:00 2001 From: ongolk <238961+ongolk@noreply.gitea.com> Date: Thu, 10 Sep 2026 04:29:39 +0000 Subject: [PATCH] Make `tea issues create` honor `--output json` (#1114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Follow-up to #1111, covering the issues side of the same hole: `tea issues create` accepts `--output` (it parses through the urfave/cli v3 ancestor-flag cascade — `issues` carries the flag via `AllDefaultFlags`, `create` never declares it) but the action never reads it. Without this fix, `tea issues create --output json | jq .url` feeds jq a markdown document. The default output is doubly hostile to consumers: glamour renders the details as markdown (with OSC 8 hyperlinks around the URL when piped), and a second bare `fmt.Println(issue.HTMLURL)` line follows it. ## What this changes - `task.CreateIssue` now returns the created `*gitea.Issue` instead of printing it. - `runIssuesCreate` switches on `--output`, mirroring the detail-command precedent and the merged create-PR behavior from #1111: `--output json` emits compact lean JSON; any other value (or no flag) falls through to the previous rendering, byte-identical to before. - Lean JSON shape: `index`, `title`, `url`, `state` — matching `createdPullJSON` in `cmd/pulls/create.go`, including its post-review compact encoding. - The interactive path is untouched — it only triggers when zero flags are set, so `--output` can never be active there. Example: ``` $ tea issues create --output json --title "bug: thing" | jq -r .url https://gitea.example.com/owner/repo/issues/42 ``` There is no agit-flow equivalent on issues, so no extra guard is needed — unlike the pulls side, every creation path produces an `*gitea.Issue`. --------- Co-authored-by: Danilo Sousa Reviewed-on: https://gitea.com/gitea/tea/pulls/1114 Reviewed-by: Lunny Xiao Co-authored-by: ongolk <238961+ongolk@noreply.gitea.com> --- cmd/issues/create.go | 46 +++++++++++++++++++++++++++++--- cmd/issues/create_test.go | 41 ++++++++++++++++++++++++++++ modules/interact/issue_create.go | 13 ++++++++- modules/task/issue_create.go | 15 ++++------- 4 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 cmd/issues/create_test.go diff --git a/cmd/issues/create.go b/cmd/issues/create.go index 8b0d7d53..9cfd72bd 100644 --- a/cmd/issues/create.go +++ b/cmd/issues/create.go @@ -5,13 +5,18 @@ package issues import ( stdctx "context" + "encoding/json" + "fmt" + "io" + + gitea "gitea.dev/sdk" + "github.com/urfave/cli/v3" "gitea.dev/tea/cmd/flags" "gitea.dev/tea/modules/context" "gitea.dev/tea/modules/interact" + "gitea.dev/tea/modules/print" "gitea.dev/tea/modules/task" - - "github.com/urfave/cli/v3" ) // CmdIssuesCreate represents a sub command of issues to create issue @@ -47,9 +52,44 @@ func runIssuesCreate(requestCtx stdctx.Context, cmd *cli.Command) error { return err } - return task.CreateIssue(requestCtx, ctx.Login, + issue, err := task.CreateIssue(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, *opts, ) + if err != nil { + return err + } + + if ctx.IsSet("output") { + switch ctx.String("output") { + case "json": + return writeCreatedIssueAsJSON(ctx.Writer, issue) + } + } + + print.IssueDetails(issue, nil) + + fmt.Println(issue.HTMLURL) + + return nil +} + +// createdIssueJSON is the machine-readable representation of a freshly +// created issue, mirroring the create-PR equivalent in cmd/pulls/create.go +// (createdPullJSON). +type createdIssueJSON struct { + Index int64 `json:"index"` + Title string `json:"title"` + URL string `json:"url"` + State gitea.StateType `json:"state"` +} + +func writeCreatedIssueAsJSON(w io.Writer, issue *gitea.Issue) error { + return json.NewEncoder(w).Encode(createdIssueJSON{ + Index: issue.Index, + Title: issue.Title, + URL: issue.HTMLURL, + State: issue.State, + }) } diff --git a/cmd/issues/create_test.go b/cmd/issues/create_test.go new file mode 100644 index 00000000..274b312a --- /dev/null +++ b/cmd/issues/create_test.go @@ -0,0 +1,41 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "bytes" + "encoding/json" + "testing" + + gitea "gitea.dev/sdk" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteCreatedIssueAsJSON(t *testing.T) { + issue := &gitea.Issue{ + Index: 42, + Title: "test title", + HTMLURL: "https://gitea.example.com/owner/repo/issues/42", + State: gitea.StateOpen, + } + + var buf bytes.Buffer + require.NoError(t, writeCreatedIssueAsJSON(&buf, issue)) + + var got map[string]any + require.NoError(t, json.Unmarshal(buf.Bytes(), &got)) + + assert.Equal(t, float64(42), got["index"]) + assert.Equal(t, "test title", got["title"]) + assert.Equal(t, "https://gitea.example.com/owner/repo/issues/42", got["url"]) + assert.Equal(t, "open", got["state"]) + + // exactly the lean field set, nothing extra + assert.Len(t, got, 4) + + // machine-readable output must not contain terminal escape sequences + assert.NotContains(t, buf.String(), "\x1b") +} diff --git a/modules/interact/issue_create.go b/modules/interact/issue_create.go index 3dee358e..c1202499 100644 --- a/modules/interact/issue_create.go +++ b/modules/interact/issue_create.go @@ -5,11 +5,13 @@ package interact import ( "context" + "fmt" "strings" gitea "gitea.dev/sdk" "gitea.dev/tea/modules/config" + "gitea.dev/tea/modules/print" "gitea.dev/tea/modules/task" "gitea.dev/tea/modules/theme" @@ -34,7 +36,16 @@ func CreateIssue(ctx context.Context, login *config.Login, owner, repo string) e return err } - return task.CreateIssue(ctx, login, owner, repo, opts) + issue, err := task.CreateIssue(ctx, login, owner, repo, opts) + if err != nil { + return err + } + + print.IssueDetails(issue, nil) + + fmt.Println(issue.HTMLURL) + + return nil } func promptIssueProperties(ctx context.Context, login *config.Login, owner, repo string, o *gitea.CreateIssueOption) error { diff --git a/modules/task/issue_create.go b/modules/task/issue_create.go index 35b9c245..9d326c37 100644 --- a/modules/task/issue_create.go +++ b/modules/task/issue_create.go @@ -10,24 +10,19 @@ import ( gitea "gitea.dev/sdk" "gitea.dev/tea/modules/config" - "gitea.dev/tea/modules/print" ) -// CreateIssue creates an issue in the given repo and prints the result -func CreateIssue(requestCtx stdctx.Context, rlogin *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) error { +// CreateIssue creates an issue in the given repo and returns the created issue +func CreateIssue(requestCtx stdctx.Context, rlogin *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) (*gitea.Issue, error) { // title is required if len(opts.Title) == 0 { - return fmt.Errorf("title is required") + return nil, fmt.Errorf("title is required") } issue, _, err := rlogin.Client().Issues.CreateIssue(requestCtx, repoOwner, repoName, opts) if err != nil { - return fmt.Errorf("could not create issue: %s", err) + return nil, fmt.Errorf("could not create issue: %s", err) } - print.IssueDetails(issue, nil) - - fmt.Println(issue.HTMLURL) - - return nil + return issue, nil }