forked from tcnksm/ghr
/
cli.go
335 lines (272 loc) · 8.97 KB
/
cli.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Command ghr is a tool to create a Github Release and upload your
// artifacts in parallel.
package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"os"
"runtime"
"time"
"github.com/google/go-github/github"
"github.com/mitchellh/colorstring"
"github.com/tcnksm/go-gitconfig"
)
const (
// EnvGitHubToken is environmental var to set GitHub API token
EnvGitHubToken = "GITHUB_TOKEN"
// EnvGitHubAPI is environmental var to set GitHub API base endpoint.
// This is used mainly by GitHub Enterprise user.
EnvGitHubAPI = "GITHUB_API"
// EnvDebug is environmental var to handle debug mode
EnvDebug = "GHR_DEBUG"
EnvStackTrace = "GHR_TRACE"
)
// Exit codes are in value that represnet an exit code for a paticular error.
const (
ExitCodeOK int = 0
// Errors start at 10
ExitCodeError = 10 + iota
ExitCodeParseFlagsError
ExitCodeBadArgs
ExitCodeInvalidURL
ExitCodeTokenNotFound
ExitCodeOwnerNotFound
ExitCodeRepoNotFound
ExitCodeRleaseError
)
const (
defaultCheckTimeout = 2 * time.Second
defaultBaseURL = "https://api.github.com/"
DefaultParallel = -1
)
// Debugf prints debug output when EnvDebug is given
func Debugf(format string, args ...interface{}) {
if env := os.Getenv(EnvDebug); len(env) != 0 {
log.Printf("[DEBUG] "+format+"\n", args...)
}
}
// PrintErrorf prints red error message on console.
func PrintRedf(w io.Writer, format string, args ...interface{}) {
fmt.Fprint(w,
colorstring.Color(fmt.Sprintf("[red]%s[reset]"+format, args...)))
}
// CLI is the command line object
type CLI struct {
// outStream and errStream are the stdout and stderr
// to write message from the CLI.
outStream, errStream io.Writer
}
// Run invokes the CLI with the given arguments.
func (cli *CLI) Run(args []string) int {
var (
owner string
repo string
token string
commitish string
draft bool
prerelease bool
parallel int
recreate bool
replace bool
stat bool
version bool
debug bool
)
flags := flag.NewFlagSet(Name, flag.ContinueOnError)
flags.SetOutput(cli.errStream)
flags.Usage = func() {
fmt.Fprint(cli.errStream, helpText)
}
flags.StringVar(&owner, "username", "", "")
flags.StringVar(&owner, "owner", "", "")
flags.StringVar(&owner, "u", "", "")
flags.StringVar(&repo, "repository", "", "")
flags.StringVar(&repo, "r", "", "")
flags.StringVar(&token, "token", os.Getenv(EnvGitHubToken), "")
flags.StringVar(&token, "t", os.Getenv(EnvGitHubToken), "")
flags.StringVar(&commitish, "commitish", "", "")
flags.StringVar(&commitish, "c", "", "")
flags.BoolVar(&draft, "draft", false, "")
flags.BoolVar(&prerelease, "prerelease", false, "")
flags.IntVar(¶llel, "parallel", DefaultParallel, "")
flags.IntVar(¶llel, "p", DefaultParallel, "")
flags.BoolVar(&recreate, "delete", false, "")
flags.BoolVar(&recreate, "recreate", false, "")
flags.BoolVar(&replace, "replace", false, "")
flags.BoolVar(&version, "version", false, "")
flags.BoolVar(&version, "v", false, "")
flags.BoolVar(&debug, "debug", false, "")
// Deprecated
flags.BoolVar(&stat, "stat", false, "")
// Parse flag
if err := flags.Parse(args[1:]); err != nil {
return ExitCodeParseFlagsError
}
if debug {
os.Setenv(EnvDebug, "1")
Debugf("Run as DEBUG mode")
}
// Show version and check latest version release
if version {
fmt.Fprintf(cli.outStream, OutputVersion())
return ExitCodeOK
}
parsedArgs := flags.Args()
if len(parsedArgs) != 2 {
PrintRedf(cli.errStream,
"Invalid argument: you must set TAG and PATH name.")
return ExitCodeBadArgs
}
tag, path := parsedArgs[0], parsedArgs[1]
// Extract github repository owner name.
// If it's not provided via command line flag, read it from .gitconfig
// (github user or git user).
if len(owner) == 0 {
var err error
owner, err = gitconfig.GithubUser()
if err != nil {
owner, err = gitconfig.Username()
}
if err != nil {
PrintRedf(cli.errStream,
"Failed to set up ghr: repository owner name not found\n")
fmt.Fprintf(cli.errStream,
"Please set it via `-u` option.\n\n"+
"You can set default owner name in `github.username` or `user.name`\n"+
"in `~/.gitconfig` file")
return ExitCodeOwnerNotFound
}
}
Debugf("Owner: %s", owner)
// Extract repository name from files.
// If not provided, read it from .git/config file.
if len(repo) == 0 {
var err error
repo, err = gitconfig.Repository()
if err != nil {
PrintRedf(cli.errStream,
"Failed to set up ghr: repository name not found\n")
fmt.Fprintf(cli.errStream,
"ghr reads it from `.git/config` file. Change directory to \n"+
"repository root directory or setup git repository.\n"+
"Or set it via `-r` option.\n")
return ExitCodeOwnerNotFound
}
}
Debugf("Repository: %s", repo)
// If GitHub api token is not provided via command line flag
// or env var then read it from .gitconfig file.
if len(token) == 0 {
var err error
token, err = gitconfig.GithubToken()
if err != nil {
PrintRedf(cli.errStream, "Failed to set up ghr: token not found\n")
fmt.Fprintf(cli.errStream,
"To use ghr, you need a GitHub API token.\n"+
"Please set it via `%s` env var or `-t` option.\n\n"+
"If you don't have one, visit official doc (goo.gl/jSnoI)\n"+
"and get it first.\n",
EnvGitHubToken)
return ExitCodeTokenNotFound
}
}
Debugf("Github API Token: %s", maskString(token))
// Set Base GitHub API. Base URL can be provided via env var. This is for GHE.
baseURLStr := defaultBaseURL
if urlStr := os.Getenv(EnvGitHubAPI); len(urlStr) != 0 {
baseURLStr = urlStr
}
Debugf("Base GitHub API URL: %s", baseURLStr)
if parallel <= 0 {
parallel = runtime.NumCPU()
}
Debugf("Parallel factor: %d", parallel)
localAssets, err := LocalAssets(path)
if err != nil {
PrintRedf(cli.errStream,
"Failed to find assets from %s: %s\n", path, err)
return ExitCodeError
}
Debugf("Number of file to upload: %d", len(localAssets))
// Create a GitHub client
gitHubClient, err := NewGitHubClient(owner, repo, token, baseURLStr)
if err != nil {
PrintRedf(cli.errStream, "Failed to construct GitHub client: %s", err)
return ExitCodeError
}
ghr := GHR{
GitHub: gitHubClient,
outStream: cli.outStream,
}
// Prepare create release request
req := &github.RepositoryRelease{
TagName: github.String(tag),
Prerelease: github.Bool(prerelease),
Draft: github.Bool(draft),
TargetCommitish: github.String(commitish),
}
ctx := context.TODO()
release, err := ghr.CreateRelease(ctx, req, recreate)
if err != nil {
PrintRedf(cli.errStream, "Failed to create GitHub release page: %s", err)
return ExitCodeError
}
if replace {
err := ghr.DeleteAssets(ctx, *release.ID, localAssets, parallel)
if err != nil {
PrintRedf(cli.errStream, "Failed to delete existing assets: %s", err)
return ExitCodeError
}
}
// FIXME(tcnksm): More ideal way to change this
// This is for Github enterprise
if err := ghr.GitHub.SetUploadURL(*release.UploadURL); err != nil {
fmt.Fprintf(cli.errStream, "Failed to set upload URL %s: %s", *release.UploadURL, err)
return ExitCodeError
}
err = ghr.UploadAssets(ctx, *release.ID, localAssets, parallel)
if err != nil {
PrintRedf(cli.errStream, "Failed to upload one of assets: %s", err)
return ExitCodeError
}
return ExitCodeOK
}
// maskString is used to mask string which should not be displayed
// directly like auth token
func maskString(s string) string {
if len(s) < 5 {
return "**** (masked)"
}
return s[:5] + "**** (masked)"
}
var helpText = `Usage: ghr [options...] TAG PATH
ghr is a tool to create Release on Github and upload your
artifacts to it. ghr parallelizes upload of multiple artifacts.
You must specify tag (e.g., v1.0.0) and PATH to local artifacts.
If PATH is directory, ghr globs all files in the directory and
upload it. If PATH is a file then, upload only it.
And you also must provide GitHub API token which has enough permission
(For a private repository you need the 'repo' scope and for a public
repository need 'public_repo' scope). You can get token from GitHub's
account setting page.
You can use ghr on GitHub Enterprise. Set base URL via GITHUB_API
environment variable.
Options:
-username, -u Github repository onwer name. By default, ghr
extracts it from global gitconfig value.
-repository, -r GitHub repository name. By default, ghr extracts
repository name from current directory's .git/config.
-token, -t GitHub API Token. By default, ghr reads it from
'GITHUB_TOKEN' env var.
-parallel=-1 Parallelization factor. This option limits amount
of parallelism of uploading. By default, ghr uses
number of logic CPU.
-recreate Recreate release if it already exists. If want to
upload to same release and replace use '-replace'.
-replace Replace artifacts if it is already uploaded. ghr
thinks it's same when local artifact base name
and uploaded file name are same.
`