Skip to content

Commit

Permalink
build: add GCB release workflow
Browse files Browse the repository at this point in the history
This workflow is meant to replace the GitHub action workflow defined in
https://github.com/golang/vscode-go/blob/master/.github/workflows/release.yml

The GH workflow was a collection of command line tools invocation and
some bash scripting to parse the version tag and adjust the workflow
depending on whether it's for rc or not. I figured it's easier to work
with go, so ported the logic to build/release.go.

TODO: convert most part of build/all.bash and the nightly release workflow
with go code.

release.go uses `gh` (GitHub CLI tool) to post the release note and
the artifacts (vsix) to GitHub. Dockerfile is updated to include `gh`.
Reference: https://stackoverflow.com/a/69477930

The workflow needs to access GitHub personal access token.
To access the release API, write permission for Content & Workflow is
necessary.
https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens?apiVersion=2022-11-28#repository-permissions-for-contents

Tested it by triggering the GCB workflow against a forked repo.

For b/216321767

Change-Id: I1eec6b4b896fe1f1565bbf9e160f0368cade36c7
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/553115
Reviewed-by: Robert Findley <rfindley@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
Commit-Queue: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
  • Loading branch information
hyangah committed Jan 4, 2024
1 parent 4d53cdc commit 481f2b5
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 4 deletions.
9 changes: 9 additions & 0 deletions build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ ENV DEBIAN_FRONTEND noninteractive
# TODO(hyangah): remove this when the platform works with ipv6.
ENV NODE_OPTIONS --dns-result-order=ipv4first

# Install xvfb jq
RUN apt-get -qq update && apt-get install -qq -y libnss3 libgtk-3-dev libxss1 libasound2 xvfb libsecret-1-0 jq > /dev/null

# Install gh https://stackoverflow.com/a/69477930
RUN apt update && apt install -y \
curl \
gpg
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg;
RUN echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null;
RUN apt update && apt install -y gh;

USER node
WORKDIR /workspace
ENTRYPOINT ["build/all.bash"]
8 changes: 4 additions & 4 deletions build/release-nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ steps:
entrypoint: npm
- name: us-docker.pkg.dev/$PROJECT_ID/vscode-go-docker-repo/ci-image
args:
- build/all.bash
- build/all.bash // TODO: replace all.bash with release.go
- prepare_nightly
dir: vscode-go
id: prepare nightly release
Expand Down Expand Up @@ -61,14 +61,14 @@ steps:
args:
- '-c'
- >
npx vsce publish -i $(cat /workspace/vsix_name.txt) -p $$VSCE_TOKEN
npx vsce publish -i $(cat /workspace/vsix_name.txt) -p $$VSCE_PAT
--baseContentUrl=https://github.com/golang/vscode-go
--baseImagesUrl=https://github.com/golang/vscode-go
dir: vscode-go
id: publish nightly extension
entrypoint: bash
secretEnv:
- VSCE_TOKEN
- VSCE_PAT
timeout: 1800s
options:
machineType: E2_HIGHCPU_8
Expand All @@ -81,4 +81,4 @@ artifacts:
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/$_VSCE_TOKEN/versions/latest
env: VSCE_TOKEN
env: VSCE_PAT
205 changes: 205 additions & 0 deletions build/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// 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 (
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
)

func main() {
if len(os.Args) != 2 {
fatalf("usage: %s [package|publish]", os.Args[0])
}
cmd := os.Args[1]

checkWD()
requireTools("jq", "npx", "gh", "git")
requireEnvVars("TAG_NAME")

tagName, version, isRC := releaseVersionInfo()
vsix := fmt.Sprintf("go-%s.vsix", version)

switch cmd {
case "package":
buildPackage(version, vsix)
case "publish":
requireEnvVars("VSCE_PAT", "GITHUB_TOKEN")
publish(tagName, vsix, isRC)
default:
fatalf("usage: %s [package|publish]", os.Args[0])
}
}

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 root of the repository 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
}

cmd := exec.Command("jq", "-r", ".version", "package.json")
cmd.Stderr = os.Stderr
versionInPackageJSON, err := cmd.Output()
if err != nil {
fatalf("failed to read package.json version")
}
if got := string(bytes.TrimSpace(versionInPackageJSON)); got != mmp {
fatalf("package.json version %q does not match TAG_NAME %q", got, tagName)
}

return tagName, mmp + label, isPrerelease
}

// buildPackage builds the extension of the given version, using npx vsce package.
func buildPackage(version, output string) {
cmd := exec.Command("npx", "vsce", "package",
"-o", output,
"--baseContentUrl", "https://github.com/golang/vscode-go",
"--baseImagesUrl", "https://github.com/golang/vscode-go",
"--no-update-package-json",
"--no-git-tag-version",
version)

cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fatalf("failed to build package")
}

cmd = exec.Command("git", "add", output)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fatalf("failed to build package")
}
}

// 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 _, 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 := cmd.Run(); err != nil {
fatalf("failed to publish release")
}

if isPrerelease {
return // TODO: release with the -pre-release flag if isPrerelease is set.
}

/* TODO(hyangah): uncomment this to finalize the release workflow migration.
npxVsceArgs := []string{"vsce", "publish", "-i", packageFile}
cmd2 := exec.Command("npx", npxVsceArgs...)
cmd2.Stderr = os.Stderr
if err := cmd2.Run(); 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))
}
65 changes: 65 additions & 0 deletions build/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# This workflow will be triggered when a new release tag (of the form vX.Y.Z) is
# pushed to the vscode-go repo.)
# For local testing, run:
# gcloud builds submit --config release.yaml --no-source --substitutions=_TAG_NAME="v0.40.1-rc.1",_GITHUB_TOKEN="alias1",_VSCE_TOKEN="alias2"
#
# WARNING: this will publish the extension.
#
# This will check out the vscode-go repo on the specified tag, build the extension,
# and publish it to the VS Code Marketplace and the GitHub Releases page if not published already.
steps:
# TODO: check build/test status
# TODO: configure failure notification https://cloud.google.com/build/docs/configuring-notifications/notifiers
- name: gcr.io/cloud-builders/git
args:
- clone
- '--branch=$TAG_NAME'
- '--single-branch'
- '--depth=1'
- 'https://go.googlesource.com/vscode-go'
- vscode-go
id: clone vscode-go repo
- name: gcr.io/cloud-builders/docker
args:
- '-R'
- '1000:1000'
- /workspace
- /builder/home
dir: /
id: adjust file permissions
entrypoint: chown
- name: us-docker.pkg.dev/$PROJECT_ID/vscode-go-docker-repo/ci-image
args:
- ci
dir: vscode-go
id: install npm dependencies
entrypoint: npm
- name: us-docker.pkg.dev/$PROJECT_ID/vscode-go-docker-repo/ci-image
args:
- -c
- |
go run build/release.go package &&
go run build/release.go publish
dir: vscode-go
id: package and publish the extension
entrypoint: bash
env:
- 'TAG_NAME=$TAG_NAME'
- 'COMMIT_SHA=$COMMIT_SHA'
secretEnv:
- VSCE_PAT
- GITHUB_TOKEN
timeout: 1800s
options:
substitutionOption: ALLOW_LOOSE
artifacts:
objects:
location: 'gs://$PROJECT_ID/releases/$TAG_NAME'
paths:
- vscode-go/*.vsix
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/$_VSCE_TOKEN/versions/latest
env: VSCE_PAT
- versionName: projects/$PROJECT_ID/secrets/$_GITHUB_TOKEN/versions/latest
env: GITHUB_TOKEN

0 comments on commit 481f2b5

Please sign in to comment.