The GitHub Action You Need to Publish VS Code Extensions
The term continuous integration and continuous delivery (short CI/CD) is a common best practice for software developers, including the ones building VS Code extensions. With the VS Code update from last November users have now access to pre-releases that allow developers to ship regular updates and offer testing out the latest cutting edge features from their extension to receive early feedback. In this blog post we’d like to share how Stateful releases its Marquee extension to the VS Code Marketplace and OpenVSX Registry through GitHub Actions. You can find a full example in the Marquee repository, feel free to copy and adapt it for your own extension.
While this release workflow doesn’t seem to look much different from other release pipelines and contains common steps like: setup ➡ build ➡ test ➡ compile ➡ push, you will see some interesting details we'd like to highlight that are very specific to VS Code developers.
The first section of the workflow definition contains, next to the workflow name, the trigger event for the workflow. In our case we’ve decided to have our maintainers manually trigger the release through the GitHub UI. This allows us to define a set of handy parameters to define the release type (e.g. patch, minor or major), the release channel and whether we release should be published to the marketplace.
The first workflow steps are pretty common, they checkout the repository, setup the environment and install all dependencies:
// ...
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Clone Repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node version
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install dependencies
run: yarn install --frozen-lockfile
// ...
Most of the steps that come next are dependent on whether we run a stable or edge release. Both are fairly similar but differ when it comes to compiling TypeScript and environment variables. This will become clear after looking at the next steps that build the extension code:
// ...
- name: Build Package
run: yarn build:dev
env:
NODE_ENV: development
MARQUEE_INSTRUMENTATION_KEY: ${{ secrets.MARQUEE_INSTRUMENTATION_KEY }}
if: ${{ github.event.inputs.releaseChannel == 'edge' }}
- name: Build Package
run: yarn build:prod
env:
NODE_ENV: production
MARQUEE_INSTRUMENTATION_KEY: ${{ secrets.MARQUEE_INSTRUMENTATION_KEY }}
if: ${{ github.event.inputs.releaseChannel == 'stable' }}
// ...
Behind the build command is a set of calls that compile our TypeScript code and runs Webpack either in development or production mode depending on the release channel we picked in the beginning. After this step you can either run your automated tests or jump directly into the release process. We first generate a changelog based on the commit messages between the last release and last commit. If you create pull requests for every bigger change set and squash these the changelog becomes fairly comprehensive:
// ...
- name: Create Changelog
run: |
git log $(git describe --tags --abbrev=0)..HEAD --oneline &> ${{ github.workspace }}-CHANGELOG.txt
cat ${{ github.workspace }}-CHANGELOG.txt
// ...
In the next steps we define the new release version. This version depends again on the release type, e.g. stable release: v1.2.3
or edge release: v1.2.3-edge.0
. Given that the VS Code Marketplace currently doesn’t support edge release versions, we’ve built a little script that updates the version to something like v1.2.1646405133
in which we replace the patch version number with a timestamp. This ensures that pre-releases will have a higher version than stable ones and that we can continuously make new pre-releases.
// ...
- name: Setup Git
run: |
git config --global user.name "stateful-wombot"
git config --global user.email "christian+github-bot@stateful.com"
- name: Get Current Version Number
run: |
CURRENT_VERSION=$(cat package.json | jq .version | cut -d'"' -f 2)
echo "CURRENT_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV
- name: Compile New Version (Edge)
run: |
RELEASE_VERSION=$(npx semver $CURRENT_VERSION -i pre${{ github.event.inputs.releaseType }} --preid edge)
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
echo "Bump to $RELEASE_VERSION"
if: ${{ github.event.inputs.releaseChannel == 'edge' && !contains(env.CURRENT_VERSION, 'edge') }}
- name: Compile New Version (Edge)
run: |
RELEASE_VERSION=$(npx semver $CURRENT_VERSION -i prerelease)
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
echo "Bump to $RELEASE_VERSION"
if: ${{ github.event.inputs.releaseChannel == 'edge' && contains(env.CURRENT_VERSION, 'edge') }}
- name: Compile New Version (Stable)
run: |
RELEASE_VERSION=$(npx semver $CURRENT_VERSION -i github.event.inputs.releaseType)
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
echo "Bump to $RELEASE_VERSION"
if: ${{ github.event.inputs.releaseChannel == 'stable' }}
- name: Version Package
run: |
npm version $RELEASE_VERSION
git tag -a $RELEASE_VERSION -m "$RELEASE_VERSION"
// ...
Lastly we will package and publish our extension using the Visual Studio Code Extension Manager and a GitHub Action called HaaLeo/publish-vscode-extension. The advantage of having the packaging and publishing step separated is that we can attach the compiled .vsix
file as an artifact to the workflow and offer it as download. Make sure to generate a token (named in the workflow as VSC_MKTP_PAT
and OPEN_VSX_TOKEN
) to allow GitHub to publish your extension.
// ...
- name: Package Extension (Edge)
if: ${{ github.event.inputs.releaseChannel == 'edge' }}
run: |
node .github/scripts/updateEdgeVersion.js
yarn vsce package --pre-release --yarn --no-git-tag-version --no-update-package-json -o "./marquee-$RELEASE_VERSION.vsix" ${{ github.event.inputs.additionalFlags }}
- name: Package Extension (Stable)
run: yarn vsce package $RELEASE_VERSION --yarn --no-git-tag-version --no-update-package-json -o "./marquee-$RELEASE_VERSION.vsix" ${{ github.event.inputs.additionalFlags }}
if: ${{ github.event.inputs.releaseChannel == 'stable' }}
- name: Publish to Visual Studio Marketplace (Edge)
run: yarn vsce publish --packagePath "./marquee-$RELEASE_VERSION.vsix" --pre-release --yarn --no-git-tag-version --no-update-package-json -p ${{ secrets.VSC_MKTP_PAT }} ${{ github.event.inputs.additionalFlags }}
if: ${{ github.event.inputs.publishMarketplace == 'yes' && github.event.inputs.releaseChannel == 'edge' }}
- name: Publish to Visual Studio Marketplace (Stable)
run: yarn vsce publish --packagePath "./marquee-$RELEASE_VERSION.vsix" --yarn --no-git-tag-version --no-update-package-json -p ${{ secrets.VSC_MKTP_PAT }} ${{ github.event.inputs.additionalFlags }}
if: ${{ github.event.inputs.publishMarketplace == 'yes' && github.event.inputs.releaseChannel == 'stable' }}
- name: Publish to Open VSX Registry (Edge)
uses: HaaLeo/publish-vscode-extension@v1
if: ${{ github.event.inputs.publishOpenVSX == 'yes' && github.event.inputs.releaseChannel == 'edge' }}
with:
preRelease: true
pat: ${{ secrets.OPEN_VSX_TOKEN }}
extensionFile: ./marquee-${{ env.RELEASE_VERSION }}.vsix
- name: Publish to Open VSX Registry (Stable)
uses: HaaLeo/publish-vscode-extension@v1
if: ${{ github.event.inputs.publishOpenVSX == 'yes' && github.event.inputs.releaseChannel == 'stable' }}
with:
preRelease: false
pat: ${{ secrets.OPEN_VSX_TOKEN }}
extensionFile: ./marquee-${{ env.RELEASE_VERSION }}.vsix
// ...
To conclude our release workflow we push our release commit and the new git tag back to GitHub, as well as attach the compiled extension file to the workflow using the ncipollo/release-action GitHub Action. This is intentionally done at the end of the workflow so that in case something went wrong during the process we don’t mark it as a new release:
// ...
- name: Push Tags
run: |
git log -1 --stat
git push origin main --tags
- run: |
export GIT_TAG=$(git describe --tags --abbrev=0)
echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV
- name: GitHub Release
uses: ncipollo/release-action@v1
with:
artifacts: "./marquee-*"
bodyFile: ${{ github.workspace }}-CHANGELOG.txt
tag: ${{ env.GIT_TAG }}
prerelease: ${{ github.event.inputs.releaseChannel == 'edge' }}
And that’s it! A fairly easy to adopt GitHub workflow that you can adapt to make continuous stable and pre-releases.