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