VS Code Extension Framework — Design#
The behaviour in the spec is delivered by a shared reusable
workflow and a template repository, following the org's Process-* /
Template-* convention (as Process-PSModule
does for PowerShell modules). A repository opts in with a short caller workflow
and a single .github/vscode-extension.yml settings file; everything else has a
secure, working default, so a minimal caller is enough to adopt it. Step logic
lives in versioned scripts the workflow calls, never inline shell.
The pipeline#
The pipeline is a DAG of single-purpose stages, so each stage does one thing and every downstream stage reuses the exact output of the stages before it:
flowchart LR
find["Find version\ntags + PR label"] --> build["Build\nstamp · bundle · package VSIX"]
build --> test["Test\nreal VS Code host · matrix"]
lint["Lint & type-check"] --> release
test --> release["Release\npublish the built VSIX"]
- Find version — compute the version once, from the latest
vX.Y.Ztag or release plus the pull request's bump label. Independent of the other stages. - Lint & type-check — static analysis and the type checker. Independent, so it fails fast in parallel with the build.
- Build — stamp the computed version into the manifest, compile the production bundle, and package one VSIX. Uploads the bundle, the VSIX, and the stamped manifest as artifacts.
- Test — download a real editor host and run the extension test suite against the exact bundle and manifest that were built.
- Release — publish the exact VSIX that was built and tested. Depends on
both
testandlint.
The built-once contract#
The version is computed once and flows through the pipeline as artifacts, so the thing that ships is the thing that was tested:
- Find version decides
vX.Y.Zfrom tags plus the PR label. - Build stamps it into
package.json(andpackage-lock.json), compiles the bundle, and runs the packaging CLI to produce a single VSIX. The stamped manifest is uploaded alongside the VSIX so the manifest under test — including the version the running extension reports — matches the packaged artifact. - Test restores that bundle and manifest and tests them.
- Release restores and publishes that same VSIX.
Build, test, and release never re-compile or re-package; they pass the built artifact forward.
Build#
Dependencies are installed with npm ci so the lockfile is authoritative and the
install is reproducible. The TypeScript source is then bundled into a single
self-contained file with esbuild, with vscode marked external (the host
provides it). The output format is CommonJS by default — esbuild folds even
pure-ESM dependencies into that output; an extension whose own entry point
requires ECMAScript-module semantics (top-level await, or a deliberate
ESM-first choice) overrides the bundler format to esm, on a host new enough to
load ESM extension bundles. The pipeline produces one build: the release
bundle, always minified, with a source map emitted as a side artifact for
debugging rather than a separate development build. The bundle is the extension entry point
(dist/extension.js); packaging it with @vscode/vsce package --no-dependencies
produces the VSIX. Bundling first is what lets the VSIX omit node_modules and
stay small.
Test#
A real editor is downloaded and driven by the VS Code test harness
(@vscode/test-cli), which runs the extension test suite inside an Extension
Development Host. The test job runs as a matrix over:
- the host versions the extension supports — at least the minimum declared
in
engines.vscodeand the current stable; Insiders runs as an optional, non-blocking job, because it updates daily and can fail on VS Code regressions unrelated to the extension; and - the operating systems the extension declares — Linux, macOS, Windows.
Framework-level tests (the counterpart to the PSModule source-code tests) assert repository conventions — that the manifest is well-formed, contributions are declared, and the activation path is sound — independent of the extension's own feature tests.
Lint and type-check#
ESLint and the TypeScript compiler in no-emit mode run as one independent stage so a style or typing error fails fast, in parallel with the build. Both must pass before the release stage runs, alongside a green test result.
Versioning#
Versioning is Release Management applied to a VSIX artifact — this framework does not re-implement it:
- The bump is the PR label (
Major/Minor/Patch/NoRelease, defaulting toPatch); multiple SemVer labels, or a SemVer label withNoRelease, are rejected. - The version is computed once and stamped into the manifest; it is never hand-edited.
- A prerelease is requested by a
Prereleaselabel on an open pull request (or a prerelease branch), producing a prerelease VSIX that is never promoted to latest. When such a build is also published to the VS Code Marketplace, it goes out with@vscode/vsce publish --pre-releaseand an odd minor-version number, the Marketplace's pre-release-channel convention.
Publishing and distribution#
Every release attaches the VSIX to a GitHub Release whose name is the
version, and creates the git tag through the Releases API against an explicit
commit SHA — the merge commit for a merged pull request, the head commit for a
prerelease — rather than by pushing a tag over git. This keeps the release stage
at contents: write with no workflows scope, which is both least-privilege and
required on GitHub Enterprise Cloud with data residency, where that scope is not
grantable.
- Install without a marketplace. A hosted one-liner install script fetches
the latest release's VSIX asset through the authenticated GitHub CLI
(
gh api), so it works even where unauthenticated asset access is disabled, and installs it withcode --install-extension, honouring a host override so it targets Insiders, Cursor, or VSCodium. This is always available. - Marketplace publication (optional). Where the repository configures it, the
same VSIX is published to the VS Code Marketplace (
@vscode/vsce publish) and/or Open VSX (ovsx publish). Neither registry supports OIDC, so each publish token is a long-lived, publish-scoped personal access token held in a protected GitHub environment. It is opt-in and never blocks the GitHub-Release install path.
Prerelease tags, releases, and their VSIX assets are cleaned up when the pull request closes; stable releases are never touched.
Documentation and dependencies#
- Documentation. The extension's README and user docs live in the extension's own repository and are built and published like any repository's documentation (Documentation Model) — there is no internal-versus-user split. This framework's own documentation is this capability.
- Dependencies. The template ships a Dependabot configuration that keeps npm packages and pinned Actions current through Dependency Updates; those update pull requests are artifact-affecting and release through the same pipeline.
Least privilege and serialisation#
The workflow is contents: read at the top level; only the release job elevates
to contents: write, because it alone creates the tag and the release. The
separate pull-request-close job that cleans up a prerelease's tag, release, and
VSIX assets is the only other place that needs contents: write. Release runs
for the same ref are serialised and queue rather than cancel — an in-flight
release is never aborted mid-write — via a concurrency group keyed by workflow
and ref with cancel-in-progress disabled, inherited from the
GitHub Actions standard.
Pull-request validation runs, by contrast, use a per-branch concurrency group
with cancel-in-progress enabled, so a new push supersedes stale in-flight runs.
Every Action is pinned to a commit SHA.
Configuration surface#
| Surface | Where |
|---|---|
| Adoption (opt-in) | a short caller workflow that calls the reusable workflow |
| Host + OS matrix, marketplace toggle, extras | .github/vscode-extension.yml |
| Version bump / prerelease | pull-request label |
| Release branches + path filter | .github/release.config.yml (Release Management) |
| Marketplace publish tokens | a GitHub environment's secrets |
Extension manifest (engines.vscode, contributes, activation) |
package.json |
Repository structure#
The template repository lays down the whole shape, mirroring Template-PSModule:
src/— the extension source (extension.ts) and its tests.package.json— the extension manifest:publisher,engines.vscode,contributes,activationEvents, andmainpointing at the bundle.- the bundler config,
tsconfig.json, and.vscodeignore. install/— the hosted one-liner install scripts..github/— the caller workflow,vscode-extension.yml,release.config.yml, anddependabot.yml.
Where this connects#
- Spec — the requirements this design delivers.
- Release Management — the release mechanics this pipeline drives to ship a VSIX.
- Dependency Updates — how the extension's dependencies stay current.
- Merge Automation — how the pipeline's named status checks gate the merge.
- GitHub Actions — how the reusable workflow is authored (SHA pins, least privilege, concurrency).
- TypeScript — how the extension source is written.
- Testing — the testing approach the host-based tests follow.
- Security — why consumers install pinned, immutable versions.