blob: 1d25b2c9b11ab19737aa7d4f45697f845d1239a7 [file] [log] [blame] [edit]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This script is used to build and publish VS Code Go extension.
// The script should be run from the root of the repository where package.json is located.
//
// The script requires the following environment variables to be set:
//
// TAG_NAME: the name of the tag to be released.
// COMMIT_SHA: the commit SHA to be released (optional. if not set, it will be retrieved from git)
// VSCE_PAT: the Personal Access Token for the VS Code Marketplace.
// GITHUB_TOKEN: the GitHub token for the Go repository.
//
// This script requires the following tools to be installed:
//
// jq, npx, gh, git
//
// Usage:
//
// // package the extension (based on TAG_NAME).
// go run build/release.go package
// // publish the extension.
// go run build/release.go publish
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
var flagN = flag.Bool("n", false, "print the underlying commands but do not run them")
func main() {
flag.Parse()
if flag.NArg() != 1 {
usage()
os.Exit(1)
}
cmd := flag.Arg(0)
checkWD()
requireEnvVars("TAG_NAME")
tagName, version, isRC := releaseVersionInfo()
vsix := fmt.Sprintf("go-%s.vsix", version)
switch cmd {
case "package":
requireTools("npx")
buildPackage(version, tagName, vsix)
case "publish":
requireTools("npx", "gh", "git")
requireEnvVars("VSCE_PAT", "GITHUB_TOKEN")
publish(tagName, vsix, isRC)
default:
usage()
os.Exit(1)
}
}
func usage() {
fmt.Fprintf(os.Stderr, "Usage: %s <flags> [package|publish]\n\n", os.Args[0])
fmt.Fprintln(os.Stderr, "Flags:")
flag.PrintDefaults()
}
func fatalf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintf(os.Stderr, "\n")
os.Exit(1)
}
func requireTools(tools ...string) {
for _, tool := range tools {
if _, err := exec.LookPath(tool); err != nil {
fatalf("required tool %q not found", tool)
}
}
}
func requireEnvVars(vars ...string) {
for _, v := range vars {
if os.Getenv(v) == "" {
fatalf("required environment variable %q not set", v)
}
}
}
// checkWD checks if the working directory is the extension directory where package.json is located.
func checkWD() {
wd, err := os.Getwd()
if err != nil {
fatalf("failed to get working directory")
}
// check if package.json is in the working directory
if _, err := os.Stat("package.json"); os.IsNotExist(err) {
fatalf("package.json not found in working directory %q", wd)
}
}
// releaseVersionInfo computes the version and label information for this release.
// It requires the TAG_NAME environment variable to be set and the tag matches the version info embedded in package.json.
func releaseVersionInfo() (tagName, version string, isPrerelease bool) {
tagName = os.Getenv("TAG_NAME")
if tagName == "" {
fatalf("TAG_NAME environment variable is not set")
}
// versionTag should be of the form vMajor.Minor.Patch[-rc.N].
// e.g. v1.1.0-rc.1, v1.1.0
// The MajorMinorPatch part should match the version in package.json.
// The optional `-rc.N` part is captured as the `Label` group
// and the validity is checked below.
versionTagRE := regexp.MustCompile(`^v(?P<MajorMinorPatch>\d+\.\d+\.\d+)(?P<Label>\S*)$`)
m := versionTagRE.FindStringSubmatch(tagName)
if m == nil {
fatalf("TAG_NAME environment variable %q is not a valid version", tagName)
}
mmp := m[versionTagRE.SubexpIndex("MajorMinorPatch")]
label := m[versionTagRE.SubexpIndex("Label")]
if label != "" {
if !strings.HasPrefix(label, "-rc.") {
fatalf("TAG_NAME environment variable %q is not a valid release candidate version", tagName)
}
isPrerelease = true
}
return tagName, mmp + label, isPrerelease
}
func commandRun(cmd *exec.Cmd) error {
if *flagN {
if cmd.Dir != "" {
fmt.Fprintf(os.Stderr, "cd %v\n", cmd.Dir)
}
fmt.Fprintf(os.Stderr, "%v\n", strings.Join(cmd.Args, " "))
return nil
}
return cmd.Run()
}
func copy(dst, src string) error {
if *flagN {
fmt.Fprintf(os.Stderr, "cp %s %s\n", src, dst)
return nil
}
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}
// buildPackage builds the extension of the given version, using npx vsce package.
func buildPackage(version, tagName, output string) {
if err := copy("README.md", filepath.Join("..", "README.md")); err != nil {
fatalf("failed to copy README.md: %v", err)
}
// build the package.
cmd := exec.Command("npx", "vsce", "package",
"-o", output,
"--baseContentUrl", "https://github.com/golang/vscode-go/raw/"+tagName,
"--baseImagesUrl", "https://github.com/golang/vscode-go/raw/"+tagName,
"--no-update-package-json",
"--no-git-tag-version",
version)
cmd.Stderr = os.Stderr
if err := commandRun(cmd); err != nil {
fatalf("failed to build package: %v", err)
}
}
// publish publishes the extension to the VS Code Marketplace and GitHub, using npx vsce and gh release create.
func publish(tagName, packageFile string, isPrerelease bool) {
// check if the package file exists.
if *flagN {
fmt.Fprintf(os.Stderr, "stat %s\n", packageFile)
} else {
if _, err := os.Stat(packageFile); os.IsNotExist(err) {
fatalf("package file %q does not exist. Did you run 'go run build/release.go package'?", packageFile)
}
}
// publish release to GitHub. This will create a draft release - manually publish it after reviewing the draft.
// TODO(hyangah): populate the changelog (the first section of CHANGELOG.md) and pass it using --notes-file instead of --generate-notes.
ghArgs := []string{"release", "create", "--generate-notes", "--target", commitSHA(), "--title", "Release " + tagName, "--draft"}
fmt.Printf("%s\n", strings.Join(ghArgs, " "))
if isPrerelease {
ghArgs = append(ghArgs, "--prerelease")
}
ghArgs = append(ghArgs, "-R", "github.com/golang/vscode-go")
ghArgs = append(ghArgs, tagName, packageFile)
cmd := exec.Command("gh", ghArgs...)
cmd.Stderr = os.Stderr
if err := commandRun(cmd); err != nil {
fatalf("failed to publish release: %v", err)
}
if isPrerelease {
return // TODO: release with the -pre-release flag if isPrerelease is set.
}
npxVsceArgs := []string{"vsce", "publish", "-i", packageFile}
cmd2 := exec.Command("npx", npxVsceArgs...)
cmd2.Stderr = os.Stderr
if err := commandRun(cmd2); err != nil {
fatalf("failed to publish release")
}
}
// commitSHA returns COMMIT_SHA environment variable, or the commit SHA of the current branch.
func commitSHA() string {
if commitSHA := os.Getenv("COMMIT_SHA"); commitSHA != "" {
return commitSHA
}
cmd := exec.Command("git", "rev-parse", "HEAD")
cmd.Stderr = os.Stderr
commitSHA, err := cmd.Output()
if err != nil {
fatalf("failed to get commit SHA")
}
return strings.TrimSpace(string(commitSHA))
}