To automate VS Code extension releases, run your tests in a GitHub Actions workflow on every push, then publish from a version tag using the vsce tool and a marketplace personal access token stored as an encrypted secret. Keep the token scoped, pin your actions and require review before tagging.
What does the pipeline need to do?
Two jobs. A test job runs on every push and pull request. A release job runs only when you push a tag like v1.4.0, builds the package and publishes it.
The VS Code documentation on continuous integration covers this flow, including a note that extension tests need a display on Linux runners, which is why workflows often wrap the test command in a virtual display tool such as xvfb.
What goes in the test job?
Check out the code, set up Node with a pinned version, install with npm ci, compile, lint and run the extension tests. Extension tests launch a copy of VS Code, so they are slower than unit tests. Keep pure logic in separate modules that unit tests can cover quickly, and leave the integration tests for activation and commands.
Use a matrix if you support more than one operating system. Path handling and shell behaviour differ, and extensions often break on Windows first.
What goes in the release job?
- Trigger on tags matching a version pattern.
- Check out and install exactly what the lockfile says.
- Run the tests again. A tag can be pushed from a stale branch.
- Package the extension into a vsix file and keep it as a workflow artifact, so you can inspect what shipped.
- Publish with the vsce tool, reading the token from a secret named by your workflow, never from the repository.
If you also publish to Open VSX for editors such as Cursor, add a second publish step with its own token. Treat that token the same way.
How do you secure the token and the workflow?
The marketplace token can publish new code to every installed copy of your extension, so it deserves the care of a production credential.
- Create the token with the narrowest scope the marketplace allows, and set an expiry.
- Store it in a GitHub environment secret and require a reviewer for that environment, so a release needs a human click.
- Give the workflow token the minimum permissions, usually read access to contents.
- Pin third party actions to a full commit SHA. GitHub's hardening guide recommends this because tags can move.
- Do not run release steps on pull requests from forks.
- Protect the tag pattern so only maintainers can create release tags.
What does a mistake cost?
A constructed example. A maintainer stores a publish token in a repository file for convenience. A contributor's pull request workflow prints environment variables for debugging. The token appears in a log, someone copies it, and a malicious update goes out to thousands of editors. VS Code's own documentation notes that extensions run with the same permissions as the editor, so a bad release can read files and run commands on every user's machine. That is the blast radius to design for.
How to check or apply this
- Split the workflow into a test job and a tag-triggered release job.
- Use npm ci and a pinned Node version.
- Keep the vsix file as an artifact for each release.
- Store tokens as environment secrets with required reviewers.
- Pin actions by commit SHA and set minimal workflow permissions.
- Restrict who can push release tags.
- Scan your own dependencies before each release, since users inherit them. Safeguard's CLI and IDE extensions can run that check locally and in CI.
Quick check
Why run tests again in the release job?
A tag can point to a commit that never passed CI. Re-running tests ensures what ships was tested.
Where should the publish token live?
In an encrypted secret, ideally in a protected GitHub environment with required reviewers, and never in the repository.
Sources
- Continuous Integration, Visual Studio Code Extension API
- Extension runtime security, Visual Studio Code
- Security hardening for GitHub Actions, GitHub Docs
Last reviewed: 18 October 2026.