From 9eaf1ce8303699e46ad1d80cf5f09cd615b3a193 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 12 Jul 2026 12:20:23 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=93=96=20[Docs]:=20Add=20VS=20Code=20?= =?UTF-8?q?extension=20framework=20capability=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/Capabilities/index.md | 1 + .../vscode-extension-framework/design.md | 179 ++++++++++++++++++ .../vscode-extension-framework/index.md | 24 +++ .../vscode-extension-framework/spec.md | 90 +++++++++ src/zensical.toml | 5 + 5 files changed, 299 insertions(+) create mode 100644 src/docs/Capabilities/vscode-extension-framework/design.md create mode 100644 src/docs/Capabilities/vscode-extension-framework/index.md create mode 100644 src/docs/Capabilities/vscode-extension-framework/spec.md diff --git a/src/docs/Capabilities/index.md b/src/docs/Capabilities/index.md index de150d8..91c8d0e 100644 --- a/src/docs/Capabilities/index.md +++ b/src/docs/Capabilities/index.md @@ -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. | diff --git a/src/docs/Capabilities/vscode-extension-framework/design.md b/src/docs/Capabilities/vscode-extension-framework/design.md new file mode 100644 index 0000000..7f6616f --- /dev/null +++ b/src/docs/Capabilities/vscode-extension-framework/design.md @@ -0,0 +1,179 @@ +--- +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 — code in code +files. + +## 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 + +The TypeScript source is bundled into a single self-contained CommonJS module +with a fast bundler (esbuild), with `vscode` marked external (the host provides +it), minified for release and source-mapped for development. The bundle is the +extension entry point (`dist/extension.js`); packaging with the editor's +packaging CLI (`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, optionally Insiders; 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. + +## 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 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 (`vsce publish`) and/or Open + VSX (`ovsx publish`), gated behind a GitHub environment holding the publish + token. 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. 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). +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. diff --git a/src/docs/Capabilities/vscode-extension-framework/index.md b/src/docs/Capabilities/vscode-extension-framework/index.md new file mode 100644 index 0000000..749f693 --- /dev/null +++ b/src/docs/Capabilities/vscode-extension-framework/index.md @@ -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. + + + +| 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. | + + diff --git a/src/docs/Capabilities/vscode-extension-framework/spec.md b/src/docs/Capabilities/vscode-extension-framework/spec.md new file mode 100644 index 0000000..55bcae1 --- /dev/null +++ b/src/docs/Capabilities/vscode-extension-framework/spec.md @@ -0,0 +1,90 @@ +--- +title: Spec +description: 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. +--- + +# VS Code Extension Framework — Spec + +## Premise + +A VS Code extension has a lifecycle that is the same for every extension: +compile the source into a bundle, test it against a real editor, lint and +type-check it, decide its version, package it into a VSIX, and publish that VSIX +so a user can install it. This framework automates that lifecycle end to end. A +contributor focuses on the extension's own code and tests; the framework builds, +tests, versions, packages, and ships it — driven entirely on the GitHub +platform, with no local release ritual. A repository *opts in* to the framework; +it does not re-invent the pipeline. Merging a pull request *is* releasing. + +The framework is the VS Code counterpart to the module and container pipelines: +one behaviour, defined once and consumed by every extension repository, so a new +extension inherits a working build-test-release path on day one. + +### Principles + +This capability rests on the [Principles](../../Ways-of-Working/Principles/index.md): + +- **[Everything as Code](../../Ways-of-Working/Principles/Engineering-Practices.md#everything-as-code).** The pipeline, the version decision, and the extension's configuration are version-controlled workflow and settings — never a GUI action or a hand-run packaging command. +- **[Decision before change](../../Ways-of-Working/Principles/AI-First-Development.md#decision-before-change).** The pull request is the decision point; its review gate approves the code *and* the release it produces, and the bump label records the versioning decision explicitly. +- **[Extensible by default](../../Ways-of-Working/Principles/Software-Design.md#extensible-by-default).** The lifecycle is the stable core; host versions, the operating-system matrix, and publish targets are extension-specific settings that slot in. A new publish destination is a configured step, not a new pipeline. +- **[Least-privilege](../../Ways-of-Working/Principles/Purpose-and-Direction.md#least-privilege).** The pipeline runs read-only by default; only the stage that cuts the release holds write, and only the scope it needs. + +## Scope + +Applies to any repository whose released artifact is a **VS Code extension** — a +VSIX package for VS Code and compatible hosts (VS Code Insiders, Cursor, +VSCodium). One test decides applicability: **does merging produce an installable +VS Code extension that something else consumes by version?** If yes, this +capability governs how it is built and shipped. + +**In scope:** compiling and bundling the extension, testing it on a real editor +host, linting and type-checking, computing its version, packaging the VSIX, +publishing and distributing that VSIX, keeping the extension's dependencies +current, and building the extension's own documentation. + +**Out of scope:** the extension's *features and behaviour* — those belong to the +extension's own spec, in the extension's repository. The generic mechanics of +versioning and cutting a release are governed by +[Release Management](../release-management/spec.md); *which* checks gate a merge +by [Merge Automation](../merge-automation/spec.md); keeping dependencies current +by [Dependency Updates](../dependency-updates/spec.md). A plain Node package that +is not a VS Code extension is not in scope. Documentation for a specific +extension lives in that extension's repository; this capability documents the +framework itself. + +## Requirements + +- **Opt-in from a template.** A new extension starts from a template repository and inherits the whole pipeline. Adopting the framework is adding a short caller and a settings file, never copying a pipeline into the repo. +- **A single settings file with secure, working defaults.** Behaviour is configured in one version-controlled settings file. Zero configuration MUST produce a correct build, test, and VSIX; every setting defaults to a secure, sensible value and is overridable in that one place. +- **A reproducible, self-contained bundle.** The extension is compiled into a single self-contained bundle with no unbundled runtime dependencies; the same commit always produces the same output. +- **Tested on a real host.** The extension is tested against a real VS Code host, across the host versions and operating systems the extension declares as supported. Tests exercise the exact bundle that ships — never a separately compiled copy. +- **A static quality gate.** Every change is linted and type-checked, and the pipeline holds on any error. Quality is validated at pull-request time, not after merge. +- **Built once, shipped once.** The version is computed once, stamped into the manifest, and the same packaged VSIX is what is tested and what is published. Build, test, and release MUST NOT diverge. +- **Label-driven, semantic versioning.** Versioning follows [Release Management](../release-management/spec.md): the bump is a pull-request label (`Major` / `Minor` / `Patch` / `NoRelease`, defaulting to `Patch`), the version is [SemVer](https://semver.org/), and it is derived automatically — never hand-edited in the manifest. +- **An installable artifact on every release.** Each release produces an installable VSIX attached to its [GitHub Release](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases), together with an immutable reference. A user MUST be able to install a specific released version without a marketplace account. +- **Optional marketplace publication.** Where configured, the same VSIX is also published to an extension marketplace (the VS Code Marketplace and/or Open VSX). Marketplace publication is opt-in and MUST NOT be a prerequisite for the GitHub-Release install path. +- **A prerelease from an open pull request.** A prerelease VSIX MUST be obtainable from an open pull request for testing before merge, without being promoted to the latest stable version. +- **A named status check per merge-blocking stage.** Every stage that must hold a pull request surfaces a named status check, so [Merge Automation](../merge-automation/spec.md) can gate the merge on it. +- **Documentation travels with the extension.** Each extension's user documentation lives in its own repository and is built and published by the framework. There is no separate internal-versus-user split; the framework's own documentation is this capability. +- **Dependencies kept current.** The extension's pinned dependencies — npm packages and pinned Actions — are kept current and secure through [Dependency Updates](../dependency-updates/spec.md). +- **Standard GitHub primitives only.** Pull requests, labels, releases, environments, and GitHub Actions — no external release tooling beyond `gh` and the editor's own packaging and publishing CLIs. + +## Success criteria + +- Creating a repository from the template and pushing a first change yields a green build, a passing test run, and a packaged VSIX with no configuration written. +- A labelled pull request merged to a release branch produces a GitHub Release carrying an installable VSIX whose version matches the label's bump — a conflicting or ambiguous label set is rejected, never guessed. +- The tests that gate the release exercise the exact VSIX that is released, on every supported host version and operating system. +- A documentation-only or CI-only change runs its checks but produces no new version. +- A user installs any released version straight from its GitHub Release with no marketplace account; where marketplace publishing is enabled, that same version also appears in the marketplace. +- An open pull request can publish a prerelease VSIX for testing that never becomes the latest stable version, and it is cleaned up when the pull request closes. +- Adopting the framework in a new extension is a short caller plus a settings file — the pipeline itself is never copied into the repository. + +## Where this connects + +- [Design](design.md) — how these requirements are delivered. +- [Release Management](../release-management/spec.md) — the versioning and release mechanics this framework drives. +- [Dependency Updates](../dependency-updates/spec.md) — how the extension's dependencies stay current. +- [Merge Automation](../merge-automation/spec.md) — how the framework's status checks gate the merge. +- [Documentation Model](../../Ways-of-Working/Documentation-Model.md) — why this spec holds only the why and the what, and why an extension's docs live in its own repo. +- [TypeScript](../../Coding-Standards/TypeScript.md) — the language standard extensions are written to. +- [GitHub Actions](../../Coding-Standards/GitHub-Actions.md) — how the pipeline that delivers this is authored. diff --git a/src/zensical.toml b/src/zensical.toml index 3bcee3b..bc8db38 100644 --- a/src/zensical.toml +++ b/src/zensical.toml @@ -111,6 +111,11 @@ nav = [ {"Spec" = "Capabilities/downstream-release-propagation/spec.md"}, {"Design" = "Capabilities/downstream-release-propagation/design.md"}, ]}, + {"VS Code Extension Framework" = [ + "Capabilities/vscode-extension-framework/index.md", + {"Spec" = "Capabilities/vscode-extension-framework/spec.md"}, + {"Design" = "Capabilities/vscode-extension-framework/design.md"}, + ]}, ]}, {"Dictionary" = "Dictionary/index.md"}, ] From df947931aff6080fdc9feb93f7f77eea820c2390 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 12 Jul 2026 13:46:48 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=96=20[Docs]:=20Use=20end-to-end?= =?UTF-8?q?=20(terminology=20linter)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/Capabilities/vscode-extension-framework/spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/Capabilities/vscode-extension-framework/spec.md b/src/docs/Capabilities/vscode-extension-framework/spec.md index 55bcae1..a522c01 100644 --- a/src/docs/Capabilities/vscode-extension-framework/spec.md +++ b/src/docs/Capabilities/vscode-extension-framework/spec.md @@ -10,7 +10,7 @@ description: Requirements for the VS Code extension framework — an opt-in, Git A VS Code extension has a lifecycle that is the same for every extension: compile the source into a bundle, test it against a real editor, lint and type-check it, decide its version, package it into a VSIX, and publish that VSIX -so a user can install it. This framework automates that lifecycle end to end. A +so a user can install it. This framework automates that lifecycle end-to-end. A contributor focuses on the extension's own code and tests; the framework builds, tests, versions, packages, and ships it — driven entirely on the GitHub platform, with no local release ritual. A repository *opts in* to the framework; From bab5ead741eb9aedde27ddee1bb5ce69d07e8c79 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 12 Jul 2026 14:25:11 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=96=20[Docs]:=20Sharpen=20extensio?= =?UTF-8?q?n=20framework=20design=20after=20local=20review=20(council=20+?= =?UTF-8?q?=20claude)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vscode-extension-framework/design.md | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/docs/Capabilities/vscode-extension-framework/design.md b/src/docs/Capabilities/vscode-extension-framework/design.md index 7f6616f..dcb0a8a 100644 --- a/src/docs/Capabilities/vscode-extension-framework/design.md +++ b/src/docs/Capabilities/vscode-extension-framework/design.md @@ -11,8 +11,7 @@ workflow** and a **template repository**, following the org's `Process-*` / 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 — code in code -files. +lives in versioned scripts the workflow calls, never inline shell. ## The pipeline @@ -57,12 +56,19 @@ artifact forward. ## Build -The TypeScript source is bundled into a single self-contained CommonJS module -with a fast bundler (esbuild), with `vscode` marked external (the host provides -it), minified for release and source-mapped for development. The bundle is the -extension entry point (`dist/extension.js`); packaging with the editor's -packaging CLI (`vsce package --no-dependencies`) produces the VSIX. Bundling -first is what lets the VSIX omit `node_modules` and stay small. +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 @@ -71,7 +77,9 @@ A real editor is downloaded and driven by the VS Code test harness 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, optionally Insiders; and + 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 @@ -97,7 +105,9 @@ VSIX artifact — this framework does not re-implement it: 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. + 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 @@ -110,13 +120,16 @@ 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 and installs it with - `code --install-extension`, honouring a host override so it targets Insiders, - Cursor, or VSCodium. This is always available. + 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 (`vsce publish`) and/or Open - VSX (`ovsx publish`), gated behind a GitHub environment holding the publish - token. It is opt-in and never blocks the GitHub-Release install path. + 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. @@ -136,11 +149,15 @@ request closes; stable releases are never touched. ## 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. 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 +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