Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/docs/Capabilities/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and design relate and evolve.
| [Dependency Updates](dependency-updates/index.md) | How a repository's pinned dependencies are kept current and secure through automated, labelled update pull requests. |
| [Merge Automation](merge-automation/index.md) | How a pull request's required status checks become the machine-readable signal that drives automated approval and merge β€” green merges, red holds, nothing bypasses the gate. |
| [Downstream Release Propagation](downstream-release-propagation/index.md) | How a release in one repository propagates to the repositories that depend on it, via a delegated agent pull request. |
| [VS Code Extension Framework](vscode-extension-framework/index.md) | How a VS Code extension is built, tested, versioned, packaged, and published β€” one GitHub-native pipeline, opt-in from a template and a single settings file. |

<!-- INDEX:END -->

Expand Down
196 changes: 196 additions & 0 deletions src/docs/Capabilities/vscode-extension-framework/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
---
title: Design
description: How the VS Code extension framework is built β€” a shared reusable workflow that finds the version, builds one VSIX, tests it on a real host, and publishes it, from a template repository and a single settings file.
---

# VS Code Extension Framework β€” Design

The behaviour in the [spec](spec.md) is delivered by a **shared reusable
workflow** and a **template repository**, following the org's `Process-*` /
`Template-*` convention (as [Process-PSModule](https://github.com/PSModule/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:

```mermaid
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.Z` tag 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 `test` and `lint`.

## 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:

1. **Find version** decides `vX.Y.Z` from tags plus the PR label.
2. **Build** stamps it into `package.json` (and `package-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.
3. **Test** restores that bundle and manifest and tests them.
4. **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.vscode` and 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](../release-management/design.md) applied to a
VSIX artifact β€” this framework does not re-implement it:

- The bump is the PR label (`Major` / `Minor` / `Patch` / `NoRelease`,
defaulting to `Patch`); multiple SemVer labels, or a SemVer label with
`NoRelease`, are rejected.
- The version is computed once and stamped into the manifest; it is never
hand-edited.
- A prerelease is requested by a `Prerelease` label 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-release` and 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 with `code --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](../../Ways-of-Working/Documentation-Model.md)) β€”
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](../dependency-updates/design.md); 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](../../Coding-Standards/GitHub-Actions.md#concurrency).
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](../release-management/design.md)) |
| 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](https://github.com/PSModule/Template-PSModule):

- `src/` β€” the extension source (`extension.ts`) and its tests.
- `package.json` β€” the extension manifest: `publisher`, `engines.vscode`,
`contributes`, `activationEvents`, and `main` pointing 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`,
and `dependabot.yml`.

## Where this connects

- [Spec](spec.md) β€” the requirements this design delivers.
- [Release Management](../release-management/design.md) β€” the release mechanics this pipeline drives to ship a VSIX.
- [Dependency Updates](../dependency-updates/design.md) β€” how the extension's dependencies stay current.
- [Merge Automation](../merge-automation/spec.md) β€” how the pipeline's named status checks gate the merge.
- [GitHub Actions](../../Coding-Standards/GitHub-Actions.md) β€” how the reusable workflow is authored (SHA pins, least privilege, concurrency).
- [TypeScript](../../Coding-Standards/TypeScript.md) β€” how the extension source is written.
- [Testing](../../Coding-Standards/Testing.md) β€” the testing approach the host-based tests follow.
- [Security](../../Coding-Standards/Security.md#supply-chain) β€” why consumers install pinned, immutable versions.
24 changes: 24 additions & 0 deletions src/docs/Capabilities/vscode-extension-framework/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: VS Code Extension Framework
description: How a VS Code extension is built, tested, versioned, packaged, and published β€” one GitHub-native pipeline, opt-in from a template and a single settings file.
---

# VS Code Extension Framework

The end-to-end pipeline for a VS Code extension: compile and bundle the source,
test it on a real editor host, lint and type-check it, version it by
pull-request label, package a VSIX, and publish that VSIX to a GitHub Release
(and, optionally, a marketplace). One behaviour, defined once and consumed by
every extension repository β€” the VS Code counterpart to
[Process-PSModule](https://github.com/PSModule/Process-PSModule). A repository
opts in from a template and a single settings file; it never copies the
pipeline.

<!-- INDEX:START -->

| Page | Description |
| --- | --- |
| [Spec](spec.md) | Requirements for the VS Code extension framework β€” an opt-in, GitHub-native pipeline that builds, tests, versions, packages, and publishes a VS Code extension from a single settings file. |
| [Design](design.md) | How the VS Code extension framework is built β€” a shared reusable workflow that finds the version, builds one VSIX, tests it on a real host, and publishes it, from a template repository and a single settings file. |

<!-- INDEX:END -->
Loading