diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 4543a82..a8483d3 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -2,8 +2,8 @@
"version": 1,
"isRoot": true,
"tools": {
- "fallout.globaltools": {
- "version": "2026.1.0-preview.104.g6583d78d2c",
+ "fallout.globaltool": {
+ "version": "10.4.0",
"commands": [
"fallout"
]
diff --git a/.fallout/build.schema.json b/.fallout/build.schema.json
index 46496eb..05c35ae 100644
--- a/.fallout/build.schema.json
+++ b/.fallout/build.schema.json
@@ -24,12 +24,11 @@
"ExecutableTarget": {
"type": "string",
"enum": [
- "Compile",
- "Pack",
- "Publish",
- "PublishMarketplace",
- "PublishOpenVsx",
- "Restore"
+ "CompileVsix",
+ "PackVsix",
+ "PublishVsix",
+ "RestoreVsix",
+ "VerifyVsixCredentials"
]
},
"Verbosity": {
@@ -108,7 +107,21 @@
}
},
"allOf": [
- {},
+ {
+ "properties": {
+ "PreRelease": {
+ "type": "boolean",
+ "description": "Mark the packaged extension as a marketplace pre-release"
+ },
+ "PublishVsixTo": {
+ "type": "array",
+ "description": "Publish only to these named registries (default: all configured VsixPublishTargets)",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
{
"$ref": "#/definitions/FalloutBuild"
}
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..d43304e
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/release.yml b/.github/release.yml
new file mode 100644
index 0000000..adf9141
--- /dev/null
+++ b/.github/release.yml
@@ -0,0 +1,54 @@
+# Source of truth for the PR label taxonomy, mirroring the Fallout repo's own
+# .github/release.yml so contributors and tooling meet one convention across both.
+#
+# GitHub reads this to auto-generate release notes, grouping merged PRs into the
+# sections below by label. Releases are cut with `gh release create --generate-notes`,
+# so the label you put on a PR is what appears in the release.
+#
+# When raising a PR: apply the one category label that matches the change. A PR with
+# no category label falls through to "Other Changes" — prefer a real category.
+#
+# This is the ONLY changelog. There is no CHANGELOG.md — a hand-maintained file would
+# duplicate what these labels already record, and its version heading is unknowable in
+# advance anyway (the patch component is a Nerdbank.GitVersioning git height, so the
+# number is not settled until the release is cut). If the marketplace page ever needs
+# a rendered changelog, generate it into the .vsix at pack time from these notes.
+changelog:
+ exclude:
+ labels:
+ # skip-changelog: housekeeping with no consumer-facing note (CI, tooling,
+ # internal refactors). Keeps the notes signal-heavy.
+ - skip-changelog
+ # dependencies: dependency bumps — folded out of the notes as noise. Also a
+ # category below, so the label is documented once; the exclude wins.
+ - dependencies
+ authors:
+ - dependabot
+ categories:
+ # breaking-change: removes/renames a command ID, setting, or view ID; drops
+ # support for a VS Code or Fallout version; changes the build-graph contract.
+ - title: "⚠️ Breaking Changes"
+ labels:
+ - breaking-change
+ # enhancement: a new feature or user-facing capability.
+ - title: "✨ New Features"
+ labels:
+ - enhancement
+ # bug: fixes incorrect behavior.
+ - title: "🐛 Bug Fixes"
+ labels:
+ - bug
+ # security: fixes a vulnerability or hardens a security-sensitive surface.
+ - title: "🔒 Security"
+ labels:
+ - security
+ # dependencies: dependency version bumps (excluded above — kept for completeness).
+ - title: "📦 Dependencies"
+ labels:
+ - dependencies
+ # documentation: docs, comments, and agent-instruction changes only.
+ - title: "📖 Documentation"
+ labels:
+ - documentation
+ # Fallback for PRs carrying none of the labels above — don't rely on it.
+ - title: "Other Changes"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..623691c
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,56 @@
+# ------------------------------------------------------------------------------
+#
+#
+# This code was generated.
+#
+# - To turn off auto-generation set:
+#
+# [GitHubActions (AutoGenerate = false)]
+#
+# - To trigger manual generation invoke:
+#
+# fallout --generate-configuration GitHubActions_build --host GitHubActions
+#
+#
+# ------------------------------------------------------------------------------
+
+name: build
+
+on:
+ pull_request:
+ branches:
+ - main
+ paths-ignore:
+ - '**/*.md'
+ - '.github/**'
+ - '!.github/workflows/**'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ ubuntu-latest:
+ name: ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
+ ref: ${{ github.head_ref }}
+ - name: 'Cache: .fallout/temp, ~/.nuget/packages'
+ uses: actions/cache@v6
+ with:
+ path: |
+ .fallout/temp
+ ~/.nuget/packages
+ key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
+ - name: 'Setup: .NET SDK'
+ uses: actions/setup-dotnet@v6
+ with:
+ global-json-file: global.json
+ - name: 'Restore: dotnet tools'
+ run: dotnet tool restore
+ - name: 'Run: PackVsix'
+ run: dotnet fallout PackVsix
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d4565d1..b03051e 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,61 +1,250 @@
-# Publishes the extension to the VS Marketplace and Open VSX, and attaches the
-# .vsix to the GitHub Release. Fires on a version tag (e.g. v2026.1.0).
+# Release pipeline for the extension. Shape deliberately mirrors the Fallout repo's
+# publish-packages-release.yml, so the two repos are read with one mental model.
#
-# Dogfood: the actual build/package/publish is a Fallout build (build/Build.cs),
-# invoked here via ./build.sh. This workflow only provisions the toolchains and
-# passes the marketplace tokens through the environment.
+# Dogfood: the build, package and publish steps are a Fallout build (build/Build.cs)
+# driving the Fallout.Vsce plugin (plugins/Fallout.Vsce). This workflow only provisions
+# toolchains, routes channels, and carries the tokens.
+#
+# validate-ref -> pack -> fan out to three channel jobs, each its own GH Environment
+#
+# Channel routing — GitHub is the pre-stage, the marketplaces are promoted into:
+#
+# | Channel | Trigger | Gating |
+# |--------------------------------|----------------------------------|---------------------|
+# | github-releases (pre-stage) | any release tag | none |
+# | vs-marketplace | workflow_dispatch opt-in flag | flag + approval |
+# | open-vsx | workflow_dispatch opt-in flag | flag + approval |
+#
+# A tag push NEVER reaches a marketplace. Promotion is a conscious act: dispatch with
+# publish-to-marketplaces=true, then approve the environment. Two independent layers,
+# matching how Fallout gates nuget.org.
+#
+# gh workflow run publish.yml -f tag=v10.4.16 -f publish-to-marketplaces=true
+#
+# On release candidates: both registries reject semver prerelease versions, so an rc tag
+# produces an ordinary three-integer .vsix carrying a pre-release bit in its manifest, and
+# is published only as a GitHub pre-release. The -rc.N suffix lives on the tag alone.
+# Because an rc never reaches a marketplace, its version number stays free for the GA.
#
# Required repository secrets:
-# VSCE_PAT - Azure DevOps PAT, scope Marketplace > Manage (VS Marketplace)
-# OVSX_TOKEN - Open VSX access token (Eclipse Foundation account)
-# FALLOUT_PACKAGES_TOKEN - token with read:packages, to restore Fallout -preview
-# packages (the build framework + CLI) from GitHub Packages
+# VSCE_PAT - Azure DevOps PAT, scope Marketplace > Manage (VS Marketplace)
+# OVSX_TOKEN - Open VSX access token (Eclipse Foundation account)
name: publish
on:
push:
tags:
- - 'v*'
- # Manual dry-run: restore (incl. Fallout -preview from GitHub Packages) + compile +
- # package, WITHOUT publishing. Validates the toolchain and secrets before tagging.
+ # Patterns must match the whole ref, so a stable tag cannot match the rc pattern
+ # or vice versa. Anything else (v10.4, v10.4.16-beta) triggers nothing.
+ - 'v[0-9]+.[0-9]+.[0-9]+'
+ - 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'
+ # Manual path, for two scenarios:
+ # 1. Re-running a partial release after a transient failure (pick the tag, leave
+ # publish-to-marketplaces at its default).
+ # 2. Promoting a released version to the marketplaces (pick the tag, set the flag).
workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Existing tag to (re-)release (e.g. v10.4.16)'
+ required: true
+ publish-to-marketplaces:
+ description: 'Publish to VS Marketplace + Open VSX? Default false — opt-in for promotion only.'
+ required: false
+ type: boolean
+ default: false
permissions:
- contents: write
+ contents: read
jobs:
- publish:
+ validate-ref:
+ name: validate ref
runs-on: ubuntu-latest
+ if: github.event_name == 'push'
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ - name: 'Verify tag is reachable from a release branch'
+ run: |
+ set -euo pipefail
+ REACHABLE=$(git branch -r --contains "${{ github.sha }}" \
+ | grep -E 'origin/(main|release/v[0-9]+\.[0-9]+)$' || true)
+ if [ -z "$REACHABLE" ]; then
+ echo "::error::Tag ${GITHUB_REF_NAME} is not reachable from main or a release/vX.Y branch."
+ exit 1
+ fi
+ echo "Tag ${GITHUB_REF_NAME} validated. Reachable from:"
+ echo "$REACHABLE"
+
+ pack:
+ name: pack
+ runs-on: ubuntu-latest
+ needs: [validate-ref]
+ # Runs when validate-ref succeeded (tag push) OR was skipped (workflow_dispatch).
+ if: always() && (needs.validate-ref.result == 'success' || needs.validate-ref.result == 'skipped')
+ outputs:
+ is-rc: ${{ steps.classify.outputs.is-rc }}
+ tag: ${{ steps.classify.outputs.tag }}
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ ref: ${{ inputs.tag || github.ref }}
+ fetch-depth: 0 # Nerdbank.GitVersioning needs full history
+ - uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
- - uses: actions/setup-dotnet@v4
+ - uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- # Tag push -> Publish (both registries). Manual dispatch -> Pack only (dry-run).
- - name: 'Fallout build'
- run: ./build.sh ${{ github.event_name == 'push' && 'Publish' || 'Pack' }}
+
+ - name: Classify the trigger
+ id: classify
+ run: |
+ set -euo pipefail
+ TAG="${{ inputs.tag || github.ref_name }}"
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+ if [[ "$TAG" == *-rc.* ]]; then
+ echo 'is-rc=true' >> "$GITHUB_OUTPUT"
+ else
+ echo 'is-rc=false' >> "$GITHUB_OUTPUT"
+ fi
+
+ # An rc marks the package as a marketplace pre-release. Fallout resolves parameters
+ # from the environment, so PreRelease is passed that way.
+ #
+ # PublicRelease: we check out a tag above, so HEAD is detached and matches none of
+ # version.json's publicReleaseRefSpec entries (all branch refs — nbgv never matches
+ # that spec against refs/tags/*). Without this, NB.GV stamps a git-height suffix and
+ # the marketplace version would not be a clean triple.
+ - name: 'Fallout build (PackVsix)'
+ run: ./build.sh PackVsix
env:
- # Restores Fallout -preview (framework + CLI tool) from GitHub Packages.
- FALLOUT_PACKAGES_TOKEN: ${{ secrets.FALLOUT_PACKAGES_TOKEN }}
- VSCE_PAT: ${{ secrets.VSCE_PAT }}
- # ovsx reads OVSX_PAT from the environment; our secret is named OVSX_TOKEN.
- OVSX_PAT: ${{ secrets.OVSX_TOKEN }}
- # Dry-run only: prove the marketplace credentials + publisher/namespace align
- # before a real tag, without publishing anything.
- - name: 'Verify publish credentials'
+ PreRelease: ${{ steps.classify.outputs.is-rc }}
+ PublicRelease: true
+
+ # Prove the tokens still work before a promotion needs them. Publishes nothing.
+ - name: 'Verify marketplace credentials'
if: github.event_name == 'workflow_dispatch'
+ run: ./build.sh VerifyVsixCredentials
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_TOKEN }}
- run: |
- npx vsce verify-pat fallout
- npx ovsx verify-pat fallout
- - name: Attach .vsix to the GitHub Release
- if: github.event_name == 'push'
+
+ - uses: actions/upload-artifact@v7
+ with:
+ name: fallout-vsix
+ path: fallout.vsix
+ retention-days: 7
+ if-no-files-found: error
+
+ # Pre-stage. Every release tag lands here first, and this is the only channel a
+ # release candidate ever reaches.
+ publish-github-releases:
+ name: publish → GitHub Releases
+ runs-on: ubuntu-latest
+ needs: [pack]
+ if: always() && needs.pack.result == 'success'
+ permissions:
+ contents: write
+ environment:
+ name: github-releases
+ url: https://github.com/Fallout-build/Fallout.Extensions.VSCode/releases
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/download-artifact@v8
+ with:
+ name: fallout-vsix
+ - name: 'Create or update the GitHub release'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: gh release upload "${GITHUB_REF_NAME}" fallout.vsix --clobber
+ TAG: ${{ needs.pack.outputs.tag }}
+ IS_RC: ${{ needs.pack.outputs.is-rc }}
+ run: |
+ set -euo pipefail
+ if gh release view "$TAG" > /dev/null 2>&1; then
+ echo "Release $TAG already exists — uploading asset with --clobber."
+ gh release upload "$TAG" fallout.vsix --clobber
+ else
+ # --generate-notes groups merged PRs by the label taxonomy in .github/release.yml.
+ ARGS=(--title "$TAG" --generate-notes --verify-tag)
+ if [ "$IS_RC" = "true" ]; then
+ ARGS+=(--prerelease)
+ fi
+ gh release create "$TAG" "${ARGS[@]}" fallout.vsix
+ fi
+
+ # Promotion tier. Opt-in flag AND environment approval, and never for a release
+ # candidate. See the header for why the condition tolerates a string 'true': a
+ # type: boolean input only arrives as a real boolean from the Actions UI, while the
+ # REST API — and therefore `gh workflow run` — can only send strings. Comparing a
+ # string to a boolean casts both to numbers, so a bare `== true` would silently skip
+ # every CLI-triggered promotion while still reporting success.
+ publish-vs-marketplace:
+ name: publish → VS Marketplace
+ runs-on: ubuntu-latest
+ needs: [pack]
+ if: >-
+ always() && needs.pack.result == 'success'
+ && needs.pack.outputs.is-rc == 'false'
+ && github.event_name == 'workflow_dispatch'
+ && (inputs.publish-to-marketplaces == true || inputs.publish-to-marketplaces == 'true')
+ environment:
+ name: vs-marketplace
+ url: https://marketplace.visualstudio.com/items?itemName=fallout.fallout
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ ref: ${{ inputs.tag }}
+ fetch-depth: 0
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 20
+ cache: npm
+ - uses: actions/setup-dotnet@v6
+ with:
+ global-json-file: global.json
+ - uses: actions/download-artifact@v8
+ with:
+ name: fallout-vsix
+ # --skip PackVsix publishes the exact artifact that was packed and validated,
+ # rather than rebuilding it here and publishing bits nothing has seen.
+ - name: 'Fallout build (PublishVsix → vs-marketplace)'
+ run: ./build.sh PublishVsix --publish-vsix-to vs-marketplace --skip PackVsix
+ env:
+ VSCE_PAT: ${{ secrets.VSCE_PAT }}
+ PublicRelease: true
+
+ publish-open-vsx:
+ name: publish → Open VSX
+ runs-on: ubuntu-latest
+ needs: [pack]
+ if: >-
+ always() && needs.pack.result == 'success'
+ && needs.pack.outputs.is-rc == 'false'
+ && github.event_name == 'workflow_dispatch'
+ && (inputs.publish-to-marketplaces == true || inputs.publish-to-marketplaces == 'true')
+ environment:
+ name: open-vsx
+ url: https://open-vsx.org/extension/fallout/fallout
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ ref: ${{ inputs.tag }}
+ fetch-depth: 0
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 20
+ cache: npm
+ - uses: actions/setup-dotnet@v6
+ with:
+ global-json-file: global.json
+ - uses: actions/download-artifact@v8
+ with:
+ name: fallout-vsix
+ - name: 'Fallout build (PublishVsix → open-vsx)'
+ run: ./build.sh PublishVsix --publish-vsix-to open-vsx --skip PackVsix
+ env:
+ OVSX_PAT: ${{ secrets.OVSX_TOKEN }}
+ PublicRelease: true
diff --git a/.gitignore b/.gitignore
index ec73a56..a0343ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,7 @@ out/
*.vsix
.vscode-test/
-# Fallout build
+# Fallout build (build/ is the build project, plugins/ the Fallout plugins it consumes)
.fallout/temp/
-build/bin/
-build/obj/
+bin/
+obj/
diff --git a/.vscodeignore b/.vscodeignore
index d1357a5..303531f 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -11,11 +11,15 @@ tsconfig.json
# Fallout build scaffold — build/publish tooling, not part of the shipped extension.
.fallout/**
build/**
+plugins/**
.config/**
global.json
nuget.config
+version.json
build.ps1
build.sh
+# Maintainer-facing; the marketplace page renders README.md + CHANGELOG.md only.
+RELEASING.md
# Ship only Mermaid's self-contained UMD bundle (loaded by the graph webview via a
# classic