github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/release/create/create_test.go (about)

     1  package create
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  	"testing"
    12  
    13  	"github.com/ungtb10d/cli/v2/git"
    14  	"github.com/ungtb10d/cli/v2/internal/config"
    15  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    16  	"github.com/ungtb10d/cli/v2/internal/run"
    17  	"github.com/ungtb10d/cli/v2/pkg/cmd/release/shared"
    18  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    19  	"github.com/ungtb10d/cli/v2/pkg/httpmock"
    20  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    21  	"github.com/ungtb10d/cli/v2/pkg/prompt"
    22  	"github.com/google/shlex"
    23  	"github.com/stretchr/testify/assert"
    24  	"github.com/stretchr/testify/require"
    25  )
    26  
    27  func Test_NewCmdCreate(t *testing.T) {
    28  	tempDir := t.TempDir()
    29  	tf, err := os.CreateTemp(tempDir, "release-create")
    30  	require.NoError(t, err)
    31  	fmt.Fprint(tf, "MY NOTES")
    32  	tf.Close()
    33  	af1, err := os.Create(filepath.Join(tempDir, "windows.zip"))
    34  	require.NoError(t, err)
    35  	af1.Close()
    36  	af2, err := os.Create(filepath.Join(tempDir, "linux.tgz"))
    37  	require.NoError(t, err)
    38  	af2.Close()
    39  
    40  	tests := []struct {
    41  		name    string
    42  		args    string
    43  		isTTY   bool
    44  		stdin   string
    45  		want    CreateOptions
    46  		wantErr string
    47  	}{
    48  		{
    49  			name:  "no arguments tty",
    50  			args:  "",
    51  			isTTY: true,
    52  			want: CreateOptions{
    53  				TagName:      "",
    54  				Target:       "",
    55  				Name:         "",
    56  				Body:         "",
    57  				BodyProvided: false,
    58  				Draft:        false,
    59  				Prerelease:   false,
    60  				RepoOverride: "",
    61  				Concurrency:  5,
    62  				Assets:       []*shared.AssetForUpload(nil),
    63  			},
    64  		},
    65  		{
    66  			name:    "no arguments notty",
    67  			args:    "",
    68  			isTTY:   false,
    69  			wantErr: "tag required when not running interactively",
    70  		},
    71  		{
    72  			name:  "only tag name",
    73  			args:  "v1.2.3",
    74  			isTTY: true,
    75  			want: CreateOptions{
    76  				TagName:      "v1.2.3",
    77  				Target:       "",
    78  				Name:         "",
    79  				Body:         "",
    80  				BodyProvided: false,
    81  				Draft:        false,
    82  				Prerelease:   false,
    83  				RepoOverride: "",
    84  				Concurrency:  5,
    85  				Assets:       []*shared.AssetForUpload(nil),
    86  			},
    87  		},
    88  		{
    89  			name:  "asset files",
    90  			args:  fmt.Sprintf("v1.2.3 '%s' '%s#Linux build'", af1.Name(), af2.Name()),
    91  			isTTY: true,
    92  			want: CreateOptions{
    93  				TagName:      "v1.2.3",
    94  				Target:       "",
    95  				Name:         "",
    96  				Body:         "",
    97  				BodyProvided: false,
    98  				Draft:        false,
    99  				Prerelease:   false,
   100  				RepoOverride: "",
   101  				Concurrency:  5,
   102  				Assets: []*shared.AssetForUpload{
   103  					{
   104  						Name:  "windows.zip",
   105  						Label: "",
   106  					},
   107  					{
   108  						Name:  "linux.tgz",
   109  						Label: "Linux build",
   110  					},
   111  				},
   112  			},
   113  		},
   114  		{
   115  			name:  "provide title and body",
   116  			args:  "v1.2.3 -t mytitle -n mynotes",
   117  			isTTY: true,
   118  			want: CreateOptions{
   119  				TagName:      "v1.2.3",
   120  				Target:       "",
   121  				Name:         "mytitle",
   122  				Body:         "mynotes",
   123  				BodyProvided: true,
   124  				Draft:        false,
   125  				Prerelease:   false,
   126  				RepoOverride: "",
   127  				Concurrency:  5,
   128  				Assets:       []*shared.AssetForUpload(nil),
   129  			},
   130  		},
   131  		{
   132  			name:  "notes from file",
   133  			args:  fmt.Sprintf(`v1.2.3 -F '%s'`, tf.Name()),
   134  			isTTY: true,
   135  			want: CreateOptions{
   136  				TagName:      "v1.2.3",
   137  				Target:       "",
   138  				Name:         "",
   139  				Body:         "MY NOTES",
   140  				BodyProvided: true,
   141  				Draft:        false,
   142  				Prerelease:   false,
   143  				RepoOverride: "",
   144  				Concurrency:  5,
   145  				Assets:       []*shared.AssetForUpload(nil),
   146  			},
   147  		},
   148  		{
   149  			name:  "notes from stdin",
   150  			args:  "v1.2.3 -F -",
   151  			isTTY: true,
   152  			stdin: "MY NOTES",
   153  			want: CreateOptions{
   154  				TagName:      "v1.2.3",
   155  				Target:       "",
   156  				Name:         "",
   157  				Body:         "MY NOTES",
   158  				BodyProvided: true,
   159  				Draft:        false,
   160  				Prerelease:   false,
   161  				RepoOverride: "",
   162  				Concurrency:  5,
   163  				Assets:       []*shared.AssetForUpload(nil),
   164  			},
   165  		},
   166  		{
   167  			name:  "set draft and prerelease",
   168  			args:  "v1.2.3 -d -p",
   169  			isTTY: true,
   170  			want: CreateOptions{
   171  				TagName:      "v1.2.3",
   172  				Target:       "",
   173  				Name:         "",
   174  				Body:         "",
   175  				BodyProvided: false,
   176  				Draft:        true,
   177  				Prerelease:   true,
   178  				RepoOverride: "",
   179  				Concurrency:  5,
   180  				Assets:       []*shared.AssetForUpload(nil),
   181  			},
   182  		},
   183  		{
   184  			name:  "discussion category",
   185  			args:  "v1.2.3 --discussion-category 'General'",
   186  			isTTY: true,
   187  			want: CreateOptions{
   188  				TagName:            "v1.2.3",
   189  				Target:             "",
   190  				Name:               "",
   191  				Body:               "",
   192  				BodyProvided:       false,
   193  				Draft:              false,
   194  				Prerelease:         false,
   195  				RepoOverride:       "",
   196  				Concurrency:        5,
   197  				Assets:             []*shared.AssetForUpload(nil),
   198  				DiscussionCategory: "General",
   199  			},
   200  		},
   201  		{
   202  			name:    "discussion category for draft release",
   203  			args:    "v1.2.3 -d --discussion-category 'General'",
   204  			isTTY:   true,
   205  			wantErr: "discussions for draft releases not supported",
   206  		},
   207  		{
   208  			name:  "generate release notes",
   209  			args:  "v1.2.3 --generate-notes",
   210  			isTTY: true,
   211  			want: CreateOptions{
   212  				TagName:       "v1.2.3",
   213  				Target:        "",
   214  				Name:          "",
   215  				Body:          "",
   216  				BodyProvided:  true,
   217  				Draft:         false,
   218  				Prerelease:    false,
   219  				RepoOverride:  "",
   220  				Concurrency:   5,
   221  				Assets:        []*shared.AssetForUpload(nil),
   222  				GenerateNotes: true,
   223  			},
   224  		},
   225  		{
   226  			name:  "generate release notes with notes tag",
   227  			args:  "v1.2.3 --generate-notes --notes-start-tag v1.1.0",
   228  			isTTY: true,
   229  			want: CreateOptions{
   230  				TagName:       "v1.2.3",
   231  				Target:        "",
   232  				Name:          "",
   233  				Body:          "",
   234  				BodyProvided:  true,
   235  				Draft:         false,
   236  				Prerelease:    false,
   237  				RepoOverride:  "",
   238  				Concurrency:   5,
   239  				Assets:        []*shared.AssetForUpload(nil),
   240  				GenerateNotes: true,
   241  				NotesStartTag: "v1.1.0",
   242  			},
   243  		},
   244  		{
   245  			name:  "notes tag",
   246  			args:  "--notes-start-tag v1.1.0",
   247  			isTTY: true,
   248  			want: CreateOptions{
   249  				TagName:       "",
   250  				Target:        "",
   251  				Name:          "",
   252  				Body:          "",
   253  				BodyProvided:  false,
   254  				Draft:         false,
   255  				Prerelease:    false,
   256  				RepoOverride:  "",
   257  				Concurrency:   5,
   258  				Assets:        []*shared.AssetForUpload(nil),
   259  				GenerateNotes: false,
   260  				NotesStartTag: "v1.1.0",
   261  			},
   262  		},
   263  		{
   264  			name:  "latest",
   265  			args:  "--latest v1.1.0",
   266  			isTTY: false,
   267  			want: CreateOptions{
   268  				TagName:       "v1.1.0",
   269  				Target:        "",
   270  				Name:          "",
   271  				Body:          "",
   272  				BodyProvided:  false,
   273  				Draft:         false,
   274  				Prerelease:    false,
   275  				IsLatest:      boolPtr(true),
   276  				RepoOverride:  "",
   277  				Concurrency:   5,
   278  				Assets:        []*shared.AssetForUpload(nil),
   279  				GenerateNotes: false,
   280  				NotesStartTag: "",
   281  			},
   282  		},
   283  		{
   284  			name:  "not latest",
   285  			args:  "--latest=false v1.1.0",
   286  			isTTY: false,
   287  			want: CreateOptions{
   288  				TagName:       "v1.1.0",
   289  				Target:        "",
   290  				Name:          "",
   291  				Body:          "",
   292  				BodyProvided:  false,
   293  				Draft:         false,
   294  				Prerelease:    false,
   295  				IsLatest:      boolPtr(false),
   296  				RepoOverride:  "",
   297  				Concurrency:   5,
   298  				Assets:        []*shared.AssetForUpload(nil),
   299  				GenerateNotes: false,
   300  				NotesStartTag: "",
   301  			},
   302  		},
   303  	}
   304  	for _, tt := range tests {
   305  		t.Run(tt.name, func(t *testing.T) {
   306  			ios, stdin, _, _ := iostreams.Test()
   307  			if tt.stdin == "" {
   308  				ios.SetStdinTTY(tt.isTTY)
   309  			} else {
   310  				ios.SetStdinTTY(false)
   311  				fmt.Fprint(stdin, tt.stdin)
   312  			}
   313  			ios.SetStdoutTTY(tt.isTTY)
   314  			ios.SetStderrTTY(tt.isTTY)
   315  
   316  			f := &cmdutil.Factory{
   317  				IOStreams: ios,
   318  			}
   319  
   320  			var opts *CreateOptions
   321  			cmd := NewCmdCreate(f, func(o *CreateOptions) error {
   322  				opts = o
   323  				return nil
   324  			})
   325  			cmd.PersistentFlags().StringP("repo", "R", "", "")
   326  
   327  			argv, err := shlex.Split(tt.args)
   328  			require.NoError(t, err)
   329  			cmd.SetArgs(argv)
   330  
   331  			cmd.SetIn(&bytes.Buffer{})
   332  			cmd.SetOut(io.Discard)
   333  			cmd.SetErr(io.Discard)
   334  
   335  			_, err = cmd.ExecuteC()
   336  			if tt.wantErr != "" {
   337  				require.EqualError(t, err, tt.wantErr)
   338  				return
   339  			} else {
   340  				require.NoError(t, err)
   341  			}
   342  
   343  			assert.Equal(t, tt.want.TagName, opts.TagName)
   344  			assert.Equal(t, tt.want.Target, opts.Target)
   345  			assert.Equal(t, tt.want.Name, opts.Name)
   346  			assert.Equal(t, tt.want.Body, opts.Body)
   347  			assert.Equal(t, tt.want.BodyProvided, opts.BodyProvided)
   348  			assert.Equal(t, tt.want.Draft, opts.Draft)
   349  			assert.Equal(t, tt.want.Prerelease, opts.Prerelease)
   350  			assert.Equal(t, tt.want.Concurrency, opts.Concurrency)
   351  			assert.Equal(t, tt.want.RepoOverride, opts.RepoOverride)
   352  			assert.Equal(t, tt.want.DiscussionCategory, opts.DiscussionCategory)
   353  			assert.Equal(t, tt.want.GenerateNotes, opts.GenerateNotes)
   354  			assert.Equal(t, tt.want.NotesStartTag, opts.NotesStartTag)
   355  			assert.Equal(t, tt.want.IsLatest, opts.IsLatest)
   356  
   357  			require.Equal(t, len(tt.want.Assets), len(opts.Assets))
   358  			for i := range tt.want.Assets {
   359  				assert.Equal(t, tt.want.Assets[i].Name, opts.Assets[i].Name)
   360  				assert.Equal(t, tt.want.Assets[i].Label, opts.Assets[i].Label)
   361  			}
   362  		})
   363  	}
   364  }
   365  
   366  func Test_createRun(t *testing.T) {
   367  	tests := []struct {
   368  		name       string
   369  		isTTY      bool
   370  		opts       CreateOptions
   371  		httpStubs  func(t *testing.T, reg *httpmock.Registry)
   372  		wantErr    string
   373  		wantStdout string
   374  		wantStderr string
   375  	}{
   376  		{
   377  			name:  "create a release",
   378  			isTTY: true,
   379  			opts: CreateOptions{
   380  				TagName:      "v1.2.3",
   381  				Name:         "The Big 1.2",
   382  				Body:         "* Fixed bugs",
   383  				BodyProvided: true,
   384  				Target:       "",
   385  			},
   386  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   387  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   388  					"url": "https://api.github.com/releases/123",
   389  					"upload_url": "https://api.github.com/assets/upload",
   390  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   391  				}`, func(params map[string]interface{}) {
   392  					assert.Equal(t, map[string]interface{}{
   393  						"tag_name":   "v1.2.3",
   394  						"name":       "The Big 1.2",
   395  						"body":       "* Fixed bugs",
   396  						"draft":      false,
   397  						"prerelease": false,
   398  					}, params)
   399  				}))
   400  			},
   401  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   402  			wantStderr: ``,
   403  		},
   404  		{
   405  			name:  "with discussion category",
   406  			isTTY: true,
   407  			opts: CreateOptions{
   408  				TagName:            "v1.2.3",
   409  				Name:               "The Big 1.2",
   410  				Body:               "* Fixed bugs",
   411  				BodyProvided:       true,
   412  				Target:             "",
   413  				DiscussionCategory: "General",
   414  			},
   415  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   416  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   417  					"url": "https://api.github.com/releases/123",
   418  					"upload_url": "https://api.github.com/assets/upload",
   419  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   420  				}`, func(params map[string]interface{}) {
   421  					assert.Equal(t, map[string]interface{}{
   422  						"tag_name":                 "v1.2.3",
   423  						"name":                     "The Big 1.2",
   424  						"body":                     "* Fixed bugs",
   425  						"draft":                    false,
   426  						"prerelease":               false,
   427  						"discussion_category_name": "General",
   428  					}, params)
   429  				}))
   430  			},
   431  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   432  			wantStderr: ``,
   433  		},
   434  		{
   435  			name:  "with target commitish",
   436  			isTTY: true,
   437  			opts: CreateOptions{
   438  				TagName:      "v1.2.3",
   439  				Name:         "",
   440  				Body:         "",
   441  				BodyProvided: true,
   442  				Target:       "main",
   443  			},
   444  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   445  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   446  					"url": "https://api.github.com/releases/123",
   447  					"upload_url": "https://api.github.com/assets/upload",
   448  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   449  				}`, func(params map[string]interface{}) {
   450  					assert.Equal(t, map[string]interface{}{
   451  						"tag_name":         "v1.2.3",
   452  						"draft":            false,
   453  						"prerelease":       false,
   454  						"target_commitish": "main",
   455  					}, params)
   456  				}))
   457  			},
   458  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   459  			wantStderr: ``,
   460  		},
   461  		{
   462  			name:  "as draft",
   463  			isTTY: true,
   464  			opts: CreateOptions{
   465  				TagName:      "v1.2.3",
   466  				Name:         "",
   467  				Body:         "",
   468  				BodyProvided: true,
   469  				Draft:        true,
   470  				Target:       "",
   471  			},
   472  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   473  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   474  					"url": "https://api.github.com/releases/123",
   475  					"upload_url": "https://api.github.com/assets/upload",
   476  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   477  				}`, func(params map[string]interface{}) {
   478  					assert.Equal(t, map[string]interface{}{
   479  						"tag_name":   "v1.2.3",
   480  						"draft":      true,
   481  						"prerelease": false,
   482  					}, params)
   483  				}))
   484  			},
   485  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   486  			wantStderr: ``,
   487  		},
   488  		{
   489  			name:  "with latest",
   490  			isTTY: false,
   491  			opts: CreateOptions{
   492  				TagName:       "v1.2.3",
   493  				Name:          "",
   494  				Body:          "",
   495  				Target:        "",
   496  				IsLatest:      boolPtr(true),
   497  				BodyProvided:  true,
   498  				GenerateNotes: false,
   499  			},
   500  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   501  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   502  					"url": "https://api.github.com/releases/123",
   503  					"upload_url": "https://api.github.com/assets/upload",
   504  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   505  				}`, func(params map[string]interface{}) {
   506  					assert.Equal(t, map[string]interface{}{
   507  						"tag_name":    "v1.2.3",
   508  						"draft":       false,
   509  						"prerelease":  false,
   510  						"make_latest": "true",
   511  					}, params)
   512  				}))
   513  			},
   514  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   515  			wantErr:    "",
   516  		},
   517  		{
   518  			name:  "with generate notes",
   519  			isTTY: true,
   520  			opts: CreateOptions{
   521  				TagName:       "v1.2.3",
   522  				Name:          "",
   523  				Body:          "",
   524  				Target:        "",
   525  				BodyProvided:  true,
   526  				GenerateNotes: true,
   527  			},
   528  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   529  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   530  					"url": "https://api.github.com/releases/123",
   531  					"upload_url": "https://api.github.com/assets/upload",
   532  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   533  				}`, func(params map[string]interface{}) {
   534  					assert.Equal(t, map[string]interface{}{
   535  						"tag_name":               "v1.2.3",
   536  						"draft":                  false,
   537  						"prerelease":             false,
   538  						"generate_release_notes": true,
   539  					}, params)
   540  				}))
   541  			},
   542  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   543  			wantErr:    "",
   544  		},
   545  		{
   546  			name:  "with generate notes and notes tag",
   547  			isTTY: true,
   548  			opts: CreateOptions{
   549  				TagName:       "v1.2.3",
   550  				Name:          "",
   551  				Body:          "",
   552  				Target:        "",
   553  				BodyProvided:  true,
   554  				GenerateNotes: true,
   555  				NotesStartTag: "v1.1.0",
   556  			},
   557  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   558  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
   559  					httpmock.RESTPayload(200, `{
   560  						"name": "generated name",
   561  						"body": "generated body"
   562  				}`, func(params map[string]interface{}) {
   563  						assert.Equal(t, map[string]interface{}{
   564  							"tag_name":          "v1.2.3",
   565  							"previous_tag_name": "v1.1.0",
   566  						}, params)
   567  					}))
   568  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   569  					"url": "https://api.github.com/releases/123",
   570  					"upload_url": "https://api.github.com/assets/upload",
   571  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   572  				}`, func(params map[string]interface{}) {
   573  					assert.Equal(t, map[string]interface{}{
   574  						"tag_name":   "v1.2.3",
   575  						"draft":      false,
   576  						"prerelease": false,
   577  						"body":       "generated body",
   578  						"name":       "generated name",
   579  					}, params)
   580  				}))
   581  			},
   582  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   583  			wantErr:    "",
   584  		},
   585  		{
   586  			name:  "with generate notes and notes tag and body and name",
   587  			isTTY: true,
   588  			opts: CreateOptions{
   589  				TagName:       "v1.2.3",
   590  				Name:          "name",
   591  				Body:          "body",
   592  				Target:        "",
   593  				BodyProvided:  true,
   594  				GenerateNotes: true,
   595  				NotesStartTag: "v1.1.0",
   596  			},
   597  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   598  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
   599  					httpmock.RESTPayload(200, `{
   600  						"name": "generated name",
   601  						"body": "generated body"
   602  				}`, func(params map[string]interface{}) {
   603  						assert.Equal(t, map[string]interface{}{
   604  							"tag_name":          "v1.2.3",
   605  							"previous_tag_name": "v1.1.0",
   606  						}, params)
   607  					}))
   608  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   609  					"url": "https://api.github.com/releases/123",
   610  					"upload_url": "https://api.github.com/assets/upload",
   611  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   612  				}`, func(params map[string]interface{}) {
   613  					assert.Equal(t, map[string]interface{}{
   614  						"tag_name":   "v1.2.3",
   615  						"draft":      false,
   616  						"prerelease": false,
   617  						"body":       "body\ngenerated body",
   618  						"name":       "name",
   619  					}, params)
   620  				}))
   621  			},
   622  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   623  			wantErr:    "",
   624  		},
   625  		{
   626  			name:  "publish after uploading files",
   627  			isTTY: true,
   628  			opts: CreateOptions{
   629  				TagName:      "v1.2.3",
   630  				Name:         "",
   631  				Body:         "",
   632  				BodyProvided: true,
   633  				Draft:        false,
   634  				Target:       "",
   635  				Assets: []*shared.AssetForUpload{
   636  					{
   637  						Name: "ball.tgz",
   638  						Open: func() (io.ReadCloser, error) {
   639  							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
   640  						},
   641  					},
   642  				},
   643  				Concurrency: 1,
   644  			},
   645  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   646  				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
   647  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   648  					"url": "https://api.github.com/releases/123",
   649  					"upload_url": "https://api.github.com/assets/upload",
   650  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   651  				}`, func(params map[string]interface{}) {
   652  					assert.Equal(t, map[string]interface{}{
   653  						"tag_name":   "v1.2.3",
   654  						"draft":      true,
   655  						"prerelease": false,
   656  					}, params)
   657  				}))
   658  				reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
   659  					q := req.URL.Query()
   660  					assert.Equal(t, "ball.tgz", q.Get("name"))
   661  					assert.Equal(t, "", q.Get("label"))
   662  					return &http.Response{
   663  						StatusCode: 201,
   664  						Request:    req,
   665  						Body:       io.NopCloser(bytes.NewBufferString(`{}`)),
   666  						Header: map[string][]string{
   667  							"Content-Type": {"application/json"},
   668  						},
   669  					}, nil
   670  				})
   671  				reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
   672  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
   673  				}`, func(params map[string]interface{}) {
   674  					assert.Equal(t, map[string]interface{}{
   675  						"draft": false,
   676  					}, params)
   677  				}))
   678  			},
   679  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
   680  			wantStderr: ``,
   681  		},
   682  		{
   683  			name:  "upload files but release already exists",
   684  			isTTY: true,
   685  			opts: CreateOptions{
   686  				TagName:      "v1.2.3",
   687  				Name:         "",
   688  				Body:         "",
   689  				BodyProvided: true,
   690  				Draft:        false,
   691  				Target:       "",
   692  				Assets: []*shared.AssetForUpload{
   693  					{
   694  						Name: "ball.tgz",
   695  						Open: func() (io.ReadCloser, error) {
   696  							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
   697  						},
   698  					},
   699  				},
   700  				Concurrency: 1,
   701  			},
   702  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   703  				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(200, ``))
   704  			},
   705  			wantStdout: ``,
   706  			wantStderr: ``,
   707  			wantErr:    `a release with the same tag name already exists: v1.2.3`,
   708  		},
   709  		{
   710  			name:  "upload files and create discussion",
   711  			isTTY: true,
   712  			opts: CreateOptions{
   713  				TagName:      "v1.2.3",
   714  				Name:         "",
   715  				Body:         "",
   716  				BodyProvided: true,
   717  				Draft:        false,
   718  				Target:       "",
   719  				Assets: []*shared.AssetForUpload{
   720  					{
   721  						Name: "ball.tgz",
   722  						Open: func() (io.ReadCloser, error) {
   723  							return io.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
   724  						},
   725  					},
   726  				},
   727  				DiscussionCategory: "general",
   728  				Concurrency:        1,
   729  			},
   730  			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
   731  				reg.Register(httpmock.REST("HEAD", "repos/OWNER/REPO/releases/tags/v1.2.3"), httpmock.StatusStringResponse(404, ``))
   732  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
   733  					"url": "https://api.github.com/releases/123",
   734  					"upload_url": "https://api.github.com/assets/upload",
   735  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   736  				}`, func(params map[string]interface{}) {
   737  					assert.Equal(t, map[string]interface{}{
   738  						"tag_name":                 "v1.2.3",
   739  						"draft":                    true,
   740  						"prerelease":               false,
   741  						"discussion_category_name": "general",
   742  					}, params)
   743  				}))
   744  				reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
   745  					q := req.URL.Query()
   746  					assert.Equal(t, "ball.tgz", q.Get("name"))
   747  					assert.Equal(t, "", q.Get("label"))
   748  					return &http.Response{
   749  						StatusCode: 201,
   750  						Request:    req,
   751  						Body:       io.NopCloser(bytes.NewBufferString(`{}`)),
   752  						Header: map[string][]string{
   753  							"Content-Type": {"application/json"},
   754  						},
   755  					}, nil
   756  				})
   757  				reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
   758  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
   759  				}`, func(params map[string]interface{}) {
   760  					assert.Equal(t, map[string]interface{}{
   761  						"draft":                    false,
   762  						"discussion_category_name": "general",
   763  					}, params)
   764  				}))
   765  			},
   766  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
   767  			wantStderr: ``,
   768  		},
   769  	}
   770  	for _, tt := range tests {
   771  		t.Run(tt.name, func(t *testing.T) {
   772  			ios, _, stdout, stderr := iostreams.Test()
   773  			ios.SetStdoutTTY(tt.isTTY)
   774  			ios.SetStdinTTY(tt.isTTY)
   775  			ios.SetStderrTTY(tt.isTTY)
   776  
   777  			fakeHTTP := &httpmock.Registry{}
   778  			if tt.httpStubs != nil {
   779  				tt.httpStubs(t, fakeHTTP)
   780  			}
   781  			defer fakeHTTP.Verify(t)
   782  
   783  			tt.opts.IO = ios
   784  			tt.opts.HttpClient = func() (*http.Client, error) {
   785  				return &http.Client{Transport: fakeHTTP}, nil
   786  			}
   787  			tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
   788  				return ghrepo.FromFullName("OWNER/REPO")
   789  			}
   790  
   791  			tt.opts.GitClient = &git.Client{GitPath: "some/path/git"}
   792  
   793  			err := createRun(&tt.opts)
   794  			if tt.wantErr != "" {
   795  				require.EqualError(t, err, tt.wantErr)
   796  				return
   797  			} else {
   798  				require.NoError(t, err)
   799  			}
   800  
   801  			assert.Equal(t, tt.wantStdout, stdout.String())
   802  			assert.Equal(t, tt.wantStderr, stderr.String())
   803  		})
   804  	}
   805  }
   806  
   807  func Test_createRun_interactive(t *testing.T) {
   808  	tests := []struct {
   809  		name       string
   810  		httpStubs  func(*httpmock.Registry)
   811  		askStubs   func(*prompt.AskStubber)
   812  		runStubs   func(*run.CommandStubber)
   813  		opts       *CreateOptions
   814  		wantParams map[string]interface{}
   815  		wantOut    string
   816  		wantErr    string
   817  	}{
   818  		{
   819  			name: "create a release from existing tag",
   820  			opts: &CreateOptions{},
   821  			askStubs: func(as *prompt.AskStubber) {
   822  				as.StubPrompt("Choose a tag").
   823  					AssertOptions([]string{"v1.2.3", "v1.2.2", "v1.0.0", "v0.1.2", "Create a new tag"}).
   824  					AnswerWith("v1.2.3")
   825  				as.StubPrompt("Title (optional)").AnswerWith("")
   826  				as.StubPrompt("Release notes").
   827  					AssertOptions([]string{"Write my own", "Write using generated notes as template", "Leave blank"}).
   828  					AnswerWith("Leave blank")
   829  				as.StubPrompt("Is this a prerelease?").AnswerWith(false)
   830  				as.StubPrompt("Submit?").
   831  					AssertOptions([]string{"Publish release", "Save as draft", "Cancel"}).AnswerWith("Publish release")
   832  			},
   833  			runStubs: func(rs *run.CommandStubber) {
   834  				rs.Register(`git tag --list`, 1, "")
   835  			},
   836  			httpStubs: func(reg *httpmock.Registry) {
   837  				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/tags"), httpmock.StatusStringResponse(200, `[
   838  					{ "name": "v1.2.3" }, { "name": "v1.2.2" }, { "name": "v1.0.0" }, { "name": "v0.1.2" }
   839  				]`))
   840  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
   841  					httpmock.StatusStringResponse(200, `{
   842  						"name": "generated name",
   843  						"body": "generated body"
   844  					}`))
   845  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
   846  					"url": "https://api.github.com/releases/123",
   847  					"upload_url": "https://api.github.com/assets/upload",
   848  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   849  				}`))
   850  			},
   851  			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   852  		},
   853  		{
   854  			name: "create a release from new tag",
   855  			opts: &CreateOptions{},
   856  			askStubs: func(as *prompt.AskStubber) {
   857  				as.StubPrompt("Choose a tag").AnswerWith("Create a new tag")
   858  				as.StubPrompt("Tag name").AnswerWith("v1.2.3")
   859  				as.StubPrompt("Title (optional)").AnswerWith("")
   860  				as.StubPrompt("Release notes").
   861  					AssertOptions([]string{"Write my own", "Write using generated notes as template", "Leave blank"}).
   862  					AnswerWith("Leave blank")
   863  				as.StubPrompt("Is this a prerelease?").AnswerWith(false)
   864  				as.StubPrompt("Submit?").AnswerWith("Publish release")
   865  			},
   866  			runStubs: func(rs *run.CommandStubber) {
   867  				rs.Register(`git tag --list`, 1, "")
   868  			},
   869  			httpStubs: func(reg *httpmock.Registry) {
   870  				reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/tags"), httpmock.StatusStringResponse(200, `[
   871  					{ "name": "v1.2.2" }, { "name": "v1.0.0" }, { "name": "v0.1.2" }
   872  				]`))
   873  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
   874  					httpmock.StatusStringResponse(200, `{
   875  						"name": "generated name",
   876  						"body": "generated body"
   877  					}`))
   878  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
   879  					"url": "https://api.github.com/releases/123",
   880  					"upload_url": "https://api.github.com/assets/upload",
   881  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   882  				}`))
   883  			},
   884  			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   885  		},
   886  		{
   887  			name: "create a release using generated notes",
   888  			opts: &CreateOptions{
   889  				TagName: "v1.2.3",
   890  			},
   891  			askStubs: func(as *prompt.AskStubber) {
   892  				as.StubPrompt("Title (optional)").AnswerDefault()
   893  				as.StubPrompt("Release notes").
   894  					AssertOptions([]string{"Write my own", "Write using generated notes as template", "Leave blank"}).
   895  					AnswerWith("Write using generated notes as template")
   896  				as.StubPrompt("Is this a prerelease?").AnswerWith(false)
   897  				as.StubPrompt("Submit?").AnswerWith("Publish release")
   898  			},
   899  			runStubs: func(rs *run.CommandStubber) {
   900  				rs.Register(`git tag --list`, 1, "")
   901  			},
   902  			httpStubs: func(reg *httpmock.Registry) {
   903  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
   904  					httpmock.StatusStringResponse(200, `{
   905  						"name": "generated name",
   906  						"body": "generated body"
   907  					}`))
   908  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
   909  					httpmock.StatusStringResponse(201, `{
   910  						"url": "https://api.github.com/releases/123",
   911  						"upload_url": "https://api.github.com/assets/upload",
   912  						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   913  					}`))
   914  			},
   915  			wantParams: map[string]interface{}{
   916  				"body":       "generated body",
   917  				"draft":      false,
   918  				"name":       "generated name",
   919  				"prerelease": false,
   920  				"tag_name":   "v1.2.3",
   921  			},
   922  			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   923  		},
   924  		{
   925  			name: "create a release using commit log as notes",
   926  			opts: &CreateOptions{
   927  				TagName: "v1.2.3",
   928  			},
   929  			askStubs: func(as *prompt.AskStubber) {
   930  				as.StubPrompt("Title (optional)").AnswerDefault()
   931  				as.StubPrompt("Release notes").
   932  					AssertOptions([]string{"Write my own", "Write using commit log as template", "Leave blank"}).
   933  					AnswerWith("Write using commit log as template")
   934  				as.StubPrompt("Is this a prerelease?").AnswerWith(false)
   935  				as.StubPrompt("Submit?").AnswerWith("Publish release")
   936  			},
   937  			runStubs: func(rs *run.CommandStubber) {
   938  				rs.Register(`git tag --list`, 1, "")
   939  				rs.Register(`git describe --tags --abbrev=0 HEAD\^`, 0, "v1.2.2\n")
   940  				rs.Register(`git .+log .+v1\.2\.2\.\.HEAD$`, 0, "commit subject\n\ncommit body\n")
   941  			},
   942  			httpStubs: func(reg *httpmock.Registry) {
   943  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
   944  					httpmock.StatusStringResponse(404, `{}`))
   945  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
   946  					httpmock.StatusStringResponse(201, `{
   947  						"url": "https://api.github.com/releases/123",
   948  						"upload_url": "https://api.github.com/assets/upload",
   949  						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   950  					}`))
   951  			},
   952  			wantParams: map[string]interface{}{
   953  				"body":       "* commit subject\n\n  commit body\n  ",
   954  				"draft":      false,
   955  				"prerelease": false,
   956  				"tag_name":   "v1.2.3",
   957  			},
   958  			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   959  		},
   960  		{
   961  			name: "create using annotated tag as notes",
   962  			opts: &CreateOptions{
   963  				TagName: "v1.2.3",
   964  			},
   965  			askStubs: func(as *prompt.AskStubber) {
   966  				as.StubPrompt("Title (optional)").AnswerDefault()
   967  				as.StubPrompt("Release notes").
   968  					AssertOptions([]string{"Write my own", "Write using git tag message as template", "Leave blank"}).
   969  					AnswerWith("Write using git tag message as template")
   970  				as.StubPrompt("Is this a prerelease?").AnswerWith(false)
   971  				as.StubPrompt("Submit?").AnswerWith("Publish release")
   972  			},
   973  			runStubs: func(rs *run.CommandStubber) {
   974  				rs.Register(`git tag --list`, 0, "hello from annotated tag")
   975  				rs.Register(`git describe --tags --abbrev=0 v1\.2\.3\^`, 1, "")
   976  			},
   977  			httpStubs: func(reg *httpmock.Registry) {
   978  				reg.Register(httpmock.GraphQL("RepositoryFindRef"),
   979  					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`))
   980  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
   981  					httpmock.StatusStringResponse(404, `{}`))
   982  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
   983  					httpmock.StatusStringResponse(201, `{
   984  						"url": "https://api.github.com/releases/123",
   985  						"upload_url": "https://api.github.com/assets/upload",
   986  						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   987  					}`))
   988  			},
   989  			wantParams: map[string]interface{}{
   990  				"body":       "hello from annotated tag",
   991  				"draft":      false,
   992  				"prerelease": false,
   993  				"tag_name":   "v1.2.3",
   994  			},
   995  			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   996  		},
   997  		{
   998  			name: "error when unpublished local tag and target not specified",
   999  			opts: &CreateOptions{
  1000  				TagName: "v1.2.3",
  1001  			},
  1002  			runStubs: func(rs *run.CommandStubber) {
  1003  				rs.Register(`git tag --list`, 0, "tag exists")
  1004  			},
  1005  			httpStubs: func(reg *httpmock.Registry) {
  1006  				reg.Register(httpmock.GraphQL("RepositoryFindRef"),
  1007  					httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": ""}}}}`))
  1008  			},
  1009  			wantErr: "tag v1.2.3 exists locally but has not been pushed to OWNER/REPO, please push it before continuing or specify the `--target` flag to create a new tag",
  1010  		},
  1011  		{
  1012  			name: "create a release when unpublished local tag and target specified",
  1013  			opts: &CreateOptions{
  1014  				TagName: "v1.2.3",
  1015  				Target:  "main",
  1016  			},
  1017  			askStubs: func(as *prompt.AskStubber) {
  1018  				as.StubPrompt("Title (optional)").AnswerWith("")
  1019  				as.StubPrompt("Release notes").
  1020  					AssertOptions([]string{"Write my own", "Write using generated notes as template", "Write using git tag message as template", "Leave blank"}).
  1021  					AnswerWith("Leave blank")
  1022  				as.StubPrompt("Is this a prerelease?").AnswerWith(false)
  1023  				as.StubPrompt("Submit?").AnswerWith("Publish release")
  1024  			},
  1025  			runStubs: func(rs *run.CommandStubber) {
  1026  				rs.Register(`git tag --list`, 0, "tag exists")
  1027  			},
  1028  			httpStubs: func(reg *httpmock.Registry) {
  1029  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
  1030  					httpmock.StatusStringResponse(200, `{
  1031  						"name": "generated name",
  1032  						"body": "generated body"
  1033  					}`))
  1034  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
  1035  					"url": "https://api.github.com/releases/123",
  1036  					"upload_url": "https://api.github.com/assets/upload",
  1037  					"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
  1038  				}`))
  1039  			},
  1040  			wantParams: map[string]interface{}{
  1041  				"draft":            false,
  1042  				"prerelease":       false,
  1043  				"tag_name":         "v1.2.3",
  1044  				"target_commitish": "main",
  1045  			},
  1046  			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
  1047  		},
  1048  		{
  1049  			name: "create a release using generated notes with previous tag",
  1050  			opts: &CreateOptions{
  1051  				TagName:       "v1.2.3",
  1052  				NotesStartTag: "v1.1.0",
  1053  			},
  1054  			askStubs: func(as *prompt.AskStubber) {
  1055  				as.StubPrompt("Title (optional)").AnswerDefault()
  1056  				as.StubPrompt("Release notes").
  1057  					AssertOptions([]string{"Write my own", "Write using generated notes as template", "Leave blank"}).
  1058  					AnswerWith("Write using generated notes as template")
  1059  				as.StubPrompt("Is this a prerelease?").AnswerWith(false)
  1060  				as.StubPrompt("Submit?").AnswerWith("Publish release")
  1061  			},
  1062  			runStubs: func(rs *run.CommandStubber) {
  1063  				rs.Register(`git tag --list`, 1, "")
  1064  			},
  1065  			httpStubs: func(reg *httpmock.Registry) {
  1066  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
  1067  					httpmock.RESTPayload(200, `{
  1068  						"name": "generated name",
  1069  						"body": "generated body"
  1070  				}`, func(params map[string]interface{}) {
  1071  						assert.Equal(t, map[string]interface{}{
  1072  							"tag_name":          "v1.2.3",
  1073  							"previous_tag_name": "v1.1.0",
  1074  						}, params)
  1075  					}))
  1076  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
  1077  					httpmock.StatusStringResponse(201, `{
  1078  						"url": "https://api.github.com/releases/123",
  1079  						"upload_url": "https://api.github.com/assets/upload",
  1080  						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
  1081  					}`))
  1082  			},
  1083  			wantParams: map[string]interface{}{
  1084  				"body":       "generated body",
  1085  				"draft":      false,
  1086  				"name":       "generated name",
  1087  				"prerelease": false,
  1088  				"tag_name":   "v1.2.3",
  1089  			},
  1090  			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
  1091  		},
  1092  		{
  1093  			name: "create a release using commit log as notes with previous tag",
  1094  			opts: &CreateOptions{
  1095  				TagName:       "v1.2.3",
  1096  				NotesStartTag: "v1.1.0",
  1097  			},
  1098  			askStubs: func(as *prompt.AskStubber) {
  1099  				as.StubPrompt("Title (optional)").AnswerDefault()
  1100  				as.StubPrompt("Release notes").
  1101  					AssertOptions([]string{"Write my own", "Write using commit log as template", "Leave blank"}).
  1102  					AnswerWith("Write using commit log as template")
  1103  				as.StubPrompt("Is this a prerelease?").AnswerWith(false)
  1104  				as.StubPrompt("Submit?").AnswerWith("Publish release")
  1105  			},
  1106  			runStubs: func(rs *run.CommandStubber) {
  1107  				rs.Register(`git tag --list`, 1, "")
  1108  				rs.Register(`git .+log .+v1\.1\.0\.\.HEAD$`, 0, "commit subject\n\ncommit body\n")
  1109  			},
  1110  			httpStubs: func(reg *httpmock.Registry) {
  1111  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
  1112  					httpmock.StatusStringResponse(404, `{}`))
  1113  				reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
  1114  					httpmock.StatusStringResponse(201, `{
  1115  						"url": "https://api.github.com/releases/123",
  1116  						"upload_url": "https://api.github.com/assets/upload",
  1117  						"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
  1118  					}`))
  1119  			},
  1120  			wantParams: map[string]interface{}{
  1121  				"body":       "* commit subject\n\n  commit body\n  ",
  1122  				"draft":      false,
  1123  				"prerelease": false,
  1124  				"tag_name":   "v1.2.3",
  1125  			},
  1126  			wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
  1127  		},
  1128  	}
  1129  	for _, tt := range tests {
  1130  		ios, _, stdout, stderr := iostreams.Test()
  1131  		ios.SetStdoutTTY(true)
  1132  		ios.SetStdinTTY(true)
  1133  		ios.SetStderrTTY(true)
  1134  		tt.opts.IO = ios
  1135  
  1136  		reg := &httpmock.Registry{}
  1137  		defer reg.Verify(t)
  1138  		tt.httpStubs(reg)
  1139  		tt.opts.HttpClient = func() (*http.Client, error) {
  1140  			return &http.Client{Transport: reg}, nil
  1141  		}
  1142  
  1143  		tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
  1144  			return ghrepo.FromFullName("OWNER/REPO")
  1145  		}
  1146  
  1147  		tt.opts.Config = func() (config.Config, error) {
  1148  			return config.NewBlankConfig(), nil
  1149  		}
  1150  
  1151  		tt.opts.Edit = func(_, _, val string, _ io.Reader, _, _ io.Writer) (string, error) {
  1152  			return val, nil
  1153  		}
  1154  
  1155  		tt.opts.GitClient = &git.Client{GitPath: "some/path/git"}
  1156  
  1157  		t.Run(tt.name, func(t *testing.T) {
  1158  			//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
  1159  			as := prompt.NewAskStubber(t)
  1160  			if tt.askStubs != nil {
  1161  				tt.askStubs(as)
  1162  			}
  1163  
  1164  			rs, teardown := run.Stub()
  1165  			defer teardown(t)
  1166  			if tt.runStubs != nil {
  1167  				tt.runStubs(rs)
  1168  			}
  1169  
  1170  			err := createRun(tt.opts)
  1171  
  1172  			if tt.wantErr != "" {
  1173  				require.EqualError(t, err, tt.wantErr)
  1174  				return
  1175  			} else {
  1176  				require.NoError(t, err)
  1177  			}
  1178  
  1179  			if tt.wantParams != nil {
  1180  				var r *http.Request
  1181  				for _, req := range reg.Requests {
  1182  					if req.URL.Path == "/repos/OWNER/REPO/releases" {
  1183  						r = req
  1184  						break
  1185  					}
  1186  				}
  1187  				if r == nil {
  1188  					t.Fatalf("no http requests for creating a release found")
  1189  				}
  1190  				bb, err := io.ReadAll(r.Body)
  1191  				assert.NoError(t, err)
  1192  				var params map[string]interface{}
  1193  				err = json.Unmarshal(bb, &params)
  1194  				assert.NoError(t, err)
  1195  				assert.Equal(t, tt.wantParams, params)
  1196  			}
  1197  
  1198  			assert.Equal(t, tt.wantOut, stdout.String())
  1199  			assert.Equal(t, "", stderr.String())
  1200  		})
  1201  	}
  1202  }
  1203  
  1204  func boolPtr(b bool) *bool {
  1205  	return &b
  1206  }