Skip to content

Commit e6d469b

Browse files
hyangahgopherbot
authored andcommitted
extension/tools/release: rewrite subcommand handling
We are going to add extra subcommands that may have extra flags and args. Rewrite the subcommand handling logic to be more easily extensible. Now we can define a different set of flag for each subcommand. Add "-out" flag for the package subcommand, and "-in" flag for the publish subcommand. Change-Id: I4c2bf80397229b9c379448dcf62abdc8af8773cc Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/595375 Auto-Submit: Hyang-Ah Hana Kim <hyangah@gmail.com> Commit-Queue: Hyang-Ah Hana Kim <hyangah@gmail.com> kokoro-CI: kokoro <noreply+kokoro@google.com> Reviewed-by: Robert Findley <rfindley@google.com>
1 parent 0099728 commit e6d469b

File tree

6 files changed

+267
-64
lines changed

6 files changed

+267
-64
lines changed

extension/tools/release/release.go

Lines changed: 246 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
// Usage:
2020
//
2121
// // package the extension (based on TAG_NAME).
22-
// go run build/release.go package
22+
// go run ./tools/release package
2323
// // publish the extension.
24-
// go run build/release.go publish
24+
// go run ./tools/release publish
2525
package main
2626

2727
import (
@@ -35,39 +35,182 @@ import (
3535
"strings"
3636
)
3737

38-
var flagN = flag.Bool("n", false, "print the underlying commands but do not run them")
38+
var (
39+
flagN = false
40+
)
41+
42+
var (
43+
cmdPackage = &command{
44+
usage: "package",
45+
short: "package the extension to vsix file",
46+
long: `package command builds the extension and produces .vsix file in -out`,
47+
run: runPackage,
48+
}
49+
cmdPublish = &command{
50+
usage: "publish",
51+
short: "publish the packaged extension (vsix) to the Visual Studio Code marketplace",
52+
long: `publish command publishes all the extension files in -in to the Visual Studio Code marketplace`,
53+
run: runPublish,
54+
}
55+
56+
allCommands = []*command{cmdPackage, cmdPublish}
57+
)
58+
59+
func init() {
60+
cmdPackage.flags.String("out", ".", "directory where the artifacts are written")
61+
cmdPublish.flags.String("in", ".", "directory where the artifacts to be published are")
62+
63+
addCommonFlags := func(cmd *command) {
64+
cmd.flags.BoolVar(&flagN, "n", flagN, "print the underlying commands but do not run them")
65+
}
66+
for _, cmd := range allCommands {
67+
addCommonFlags(cmd)
68+
name := cmd.name()
69+
cmd.flags.Usage = func() {
70+
help(name)
71+
}
72+
}
73+
}
3974

4075
func main() {
76+
flag.Usage = usage
4177
flag.Parse()
42-
if flag.NArg() != 1 {
43-
usage()
44-
os.Exit(1)
78+
79+
args := flag.Args()
80+
if flag.NArg() == 0 {
81+
flag.Usage()
82+
os.Exit(2)
83+
}
84+
// len(args) > 0
85+
86+
if args[0] == "help" {
87+
flag.CommandLine.SetOutput(os.Stdout)
88+
switch len(args) {
89+
case 1:
90+
flag.Usage()
91+
case 2:
92+
help(args[1])
93+
default:
94+
flag.Usage()
95+
fatalf(`too many arguments to "help"`)
96+
}
97+
os.Exit(0)
98+
}
99+
100+
cmd := findCommand(args[0])
101+
if cmd == nil {
102+
flag.Usage()
103+
os.Exit(2)
104+
}
105+
106+
cmd.run(cmd, args[1:])
107+
}
108+
109+
func usage() {
110+
printCommand := func(cmd *command) {
111+
output(fmt.Sprintf("\t%s\t%s", cmd.name(), cmd.short))
45112
}
46-
cmd := flag.Arg(0)
113+
output("go run release.go [command]")
114+
output("The commands are:")
115+
output()
116+
for _, cmd := range allCommands {
117+
printCommand(cmd)
118+
}
119+
output()
120+
}
121+
122+
func output(msgs ...any) {
123+
fmt.Fprintln(flag.CommandLine.Output(), msgs...)
124+
}
125+
126+
func findCommand(name string) *command {
127+
for _, cmd := range allCommands {
128+
if cmd.name() == name {
129+
return cmd
130+
}
131+
}
132+
return nil
133+
}
134+
135+
func help(name string) {
136+
cmd := findCommand(name)
137+
if cmd == nil {
138+
fatalf("unknown command %q", name)
139+
}
140+
output(fmt.Sprintf("Usage: release %s", cmd.usage))
141+
output()
142+
if cmd.long != "" {
143+
output(cmd.long)
144+
} else {
145+
output(fmt.Sprintf("release %s is used to %s.", cmd.name(), cmd.short))
146+
}
147+
anyflags := false
148+
cmd.flags.VisitAll(func(*flag.Flag) {
149+
anyflags = true
150+
})
151+
if anyflags {
152+
output()
153+
output("Flags:")
154+
output()
155+
cmd.flags.PrintDefaults()
156+
}
157+
}
158+
159+
type command struct {
160+
usage string
161+
short string
162+
long string
163+
flags flag.FlagSet
164+
run func(cmd *command, args []string)
165+
}
166+
167+
func (c command) name() string {
168+
name, _, _ := strings.Cut(c.usage, " ")
169+
return name
170+
}
171+
172+
func (c command) lookupFlag(name string) flag.Value {
173+
f := c.flags.Lookup(name)
174+
if f == nil {
175+
fatalf("flag %q not found", name)
176+
}
177+
return f.Value
178+
}
179+
180+
// runPackage implements the "package" subcommand.
181+
func runPackage(cmd *command, args []string) {
182+
cmd.flags.Parse(args) // will exit on error
47183

48184
checkWD()
185+
49186
requireTools("jq", "npx", "gh", "git")
50-
requireEnvVars("TAG_NAME")
51187

52-
tagName, version, isRC := releaseVersionInfo()
53-
vsix := fmt.Sprintf("go-%s.vsix", version)
188+
tagName := requireEnv("TAG_NAME")
54189

55-
switch cmd {
56-
case "package":
57-
buildPackage(version, tagName, vsix)
58-
case "publish":
59-
requireEnvVars("VSCE_PAT", "GITHUB_TOKEN")
60-
publish(tagName, vsix, isRC)
61-
default:
62-
usage()
63-
os.Exit(1)
64-
}
190+
version, _ := releaseVersionInfo(tagName)
191+
checkPackageJSON(tagName)
192+
outDir := prepareOutputDir(cmd.lookupFlag("out").String())
193+
vsix := filepath.Join(outDir, fmt.Sprintf("go-%s.vsix", version))
194+
buildPackage(version, tagName, vsix)
65195
}
66196

67-
func usage() {
68-
fmt.Fprintf(os.Stderr, "Usage: %s <flags> [package|publish]\n\n", os.Args[0])
69-
fmt.Fprintln(os.Stderr, "Flags:")
70-
flag.PrintDefaults()
197+
// runPublish implements the "publish" subcommand.
198+
func runPublish(cmd *command, args []string) {
199+
cmd.flags.Parse(args) // will exit on error
200+
201+
checkWD()
202+
203+
requireTools("jq", "npx", "gh", "git")
204+
205+
requireEnv("VSCE_PAT")
206+
requireEnv("GITHUB_TOKEN")
207+
tagName := requireEnv("TAG_NAME")
208+
209+
version, isRC := releaseVersionInfo(tagName)
210+
checkPackageJSON(tagName)
211+
inDir := prepareInputDir(cmd.lookupFlag("in").String())
212+
vsix := filepath.Join(inDir, fmt.Sprintf("go-%s.vsix", version))
213+
publish(tagName, vsix, isRC)
71214
}
72215

73216
func fatalf(format string, args ...any) {
@@ -76,6 +219,50 @@ func fatalf(format string, args ...any) {
76219
os.Exit(1)
77220
}
78221

222+
// prepareOutputDir normalizes --output-dir. If the directory doesn't exist,
223+
// prepareOutputDir creates it.
224+
func prepareOutputDir(outDir string) string {
225+
if outDir == "" {
226+
outDir = "."
227+
}
228+
229+
if flagN {
230+
// -n used for testing. don't create the directory nor try to resolve.
231+
return outDir
232+
}
233+
234+
// resolve to absolute path so output dir can be consitent
235+
// even when child processes accessing it need to run in a different directory.
236+
dir, err := filepath.Abs(outDir)
237+
if err != nil {
238+
fatalf("failed to get absolute path of output directory: %v", err)
239+
}
240+
241+
if err := os.MkdirAll(dir, 0755); err != nil {
242+
fatalf("failed to create output directory: %v", err)
243+
}
244+
return dir
245+
}
246+
247+
// prepareInputDir normalizes --input-dir.
248+
func prepareInputDir(inDir string) string {
249+
if inDir == "" {
250+
inDir = "."
251+
}
252+
if flagN {
253+
// -n used for testing. don't create the directory nor try to resolve.
254+
return inDir
255+
}
256+
257+
// resolve to absolute path so input dir can be consitent
258+
// even when child processes accessing it need to run in a different directory.
259+
dir, err := filepath.Abs(inDir)
260+
if err != nil {
261+
fatalf("failed to get absolute path of output directory: %v", err)
262+
}
263+
return dir
264+
}
265+
79266
func requireTools(tools ...string) {
80267
for _, tool := range tools {
81268
if _, err := exec.LookPath(tool); err != nil {
@@ -84,12 +271,12 @@ func requireTools(tools ...string) {
84271
}
85272
}
86273

87-
func requireEnvVars(vars ...string) {
88-
for _, v := range vars {
89-
if os.Getenv(v) == "" {
90-
fatalf("required environment variable %q not set", v)
91-
}
274+
func requireEnv(name string) string {
275+
v := os.Getenv(name)
276+
if v == "" {
277+
fatalf("required environment variable %q not set", v)
92278
}
279+
return v
93280
}
94281

95282
// checkWD checks if the working directory is the extension directory where package.json is located.
@@ -106,30 +293,37 @@ func checkWD() {
106293

107294
// releaseVersionInfo computes the version and label information for this release.
108295
// It requires the TAG_NAME environment variable to be set and the tag matches the version info embedded in package.json.
109-
func releaseVersionInfo() (tagName, version string, isPrerelease bool) {
110-
tagName = os.Getenv("TAG_NAME")
111-
if tagName == "" {
112-
fatalf("TAG_NAME environment variable is not set")
113-
}
296+
func releaseVersionInfo(tagName string) (version string, isPrerelease bool) {
114297
// versionTag should be of the form vMajor.Minor.Patch[-rc.N].
115298
// e.g. v1.1.0-rc.1, v1.1.0
116299
// The MajorMinorPatch part should match the version in package.json.
117300
// The optional `-rc.N` part is captured as the `Label` group
118301
// and the validity is checked below.
119-
versionTagRE := regexp.MustCompile(`^v(?P<MajorMinorPatch>\d+\.\d+\.\d+)(?P<Label>\S*)$`)
120-
m := versionTagRE.FindStringSubmatch(tagName)
121-
if m == nil {
122-
fatalf("TAG_NAME environment variable %q is not a valid version", tagName)
123-
}
124-
mmp := m[versionTagRE.SubexpIndex("MajorMinorPatch")]
125-
label := m[versionTagRE.SubexpIndex("Label")]
302+
mmp, label := parseVersionTagName(tagName)
126303
if label != "" {
127304
if !strings.HasPrefix(label, "-rc.") {
128305
fatalf("TAG_NAME environment variable %q is not a valid release candidate version", tagName)
129306
}
130307
isPrerelease = true
131308
}
309+
return mmp + label, isPrerelease
310+
}
311+
312+
func parseVersionTagName(tagName string) (majorMinorPatch, label string) {
313+
versionTagRE := regexp.MustCompile(`^v(?P<MajorMinorPatch>\d+\.\d+\.\d+)(?P<Label>\S*)$`)
314+
m := versionTagRE.FindStringSubmatch(tagName)
315+
if m == nil {
316+
fatalf("TAG_NAME environment variable %q is not a valid version", tagName)
317+
}
318+
return m[versionTagRE.SubexpIndex("MajorMinorPatch")], m[versionTagRE.SubexpIndex("Label")]
319+
}
132320

321+
func checkPackageJSON(tagName string) {
322+
if flagN {
323+
tracef("jq -r .version package.json")
324+
return
325+
}
326+
mmp, _ := parseVersionTagName(tagName)
133327
cmd := exec.Command("jq", "-r", ".version", "package.json")
134328
cmd.Stderr = os.Stderr
135329
var buf bytes.Buffer
@@ -138,19 +332,15 @@ func releaseVersionInfo() (tagName, version string, isPrerelease bool) {
138332
fatalf("failed to read package.json version")
139333
}
140334
versionInPackageJSON := buf.Bytes()
141-
if *flagN {
142-
return tagName, mmp + label, isPrerelease
143-
}
144335
if got := string(bytes.TrimSpace(versionInPackageJSON)); got != mmp {
145336
fatalf("package.json version %q does not match TAG_NAME %q", got, tagName)
146337
}
147-
return tagName, mmp + label, isPrerelease
148338
}
149339

150340
func commandRun(cmd *exec.Cmd) error {
151-
if *flagN {
341+
if flagN {
152342
if cmd.Dir != "" {
153-
fmt.Fprintf(os.Stderr, "cd %v\n", cmd.Dir)
343+
tracef("cd %v", cmd.Dir)
154344
}
155345
fmt.Fprintf(os.Stderr, "%v\n", strings.Join(cmd.Args, " "))
156346
return nil
@@ -159,8 +349,8 @@ func commandRun(cmd *exec.Cmd) error {
159349
}
160350

161351
func copy(dst, src string) error {
162-
if *flagN {
163-
fmt.Fprintf(os.Stderr, "cp %s %s\n", src, dst)
352+
if flagN {
353+
tracef("cp %s %s", src, dst)
164354
return nil
165355
}
166356
data, err := os.ReadFile(src)
@@ -193,8 +383,8 @@ func buildPackage(version, tagName, output string) {
193383
// publish publishes the extension to the VS Code Marketplace and GitHub, using npx vsce and gh release create.
194384
func publish(tagName, packageFile string, isPrerelease bool) {
195385
// check if the package file exists.
196-
if *flagN {
197-
fmt.Fprintf(os.Stderr, "stat %s\n", packageFile)
386+
if flagN {
387+
tracef("stat %s", packageFile)
198388
} else {
199389
if _, err := os.Stat(packageFile); os.IsNotExist(err) {
200390
fatalf("package file %q does not exist. Did you run 'go run build/release.go package'?", packageFile)
@@ -242,3 +432,8 @@ func commitSHA() string {
242432
}
243433
return strings.TrimSpace(string(commitSHA))
244434
}
435+
436+
func tracef(format string, args ...any) {
437+
str := fmt.Sprintf(format, args...)
438+
fmt.Fprintln(os.Stderr, str)
439+
}

0 commit comments

Comments
 (0)