diff --git a/.github/workflows/build-skip.yml b/.github/workflows/build-skip.yml index 40bd755..0e9b196 100644 --- a/.github/workflows/build-skip.yml +++ b/.github/workflows/build-skip.yml @@ -13,10 +13,11 @@ # success under the same `ubuntu-latest` context — satisfying the rule without paying # for a real build. # -# Two things must stay aligned with build.yml, which is generated from +# Three things must stay aligned with build.yml, which is generated from # build/Build.CI.GitHubActions.cs: # - the job name (`ubuntu-latest`), because that IS the status-check context; -# - the path set, as the exact complement of that file's OnPullRequestExcludePaths. +# - the path set, as the exact complement of that file's OnPullRequestExcludePaths; +# - the branch list, as the same set of long-lived branches a PR can target. # A PR touching both a .md and a source file runs both workflows; both report the same # context and both pass, which is fine. @@ -25,7 +26,9 @@ name: build on: pull_request: branches: + - develop - main + - 'support/*' paths: - '**/*.md' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87157e6..fb528cd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,9 @@ name: build on: pull_request: branches: + - develop - main + - 'support/*' paths-ignore: - '**/*.md' diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 381fbed..d6b8a3a 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -1,4 +1,4 @@ -# Hand-written (not auto-generated). The "preview" channel — per-commit builds of main. +# Hand-written (not auto-generated). The "preview" channel — per-commit builds of develop. # # Counterpart to the framework repo's publish-packages-preview.yml, which pushes a # -preview prerelease to GitHub Packages on every commit to main. The extension can't @@ -22,13 +22,18 @@ # The .vsix is marked as a marketplace pre-release, so if it is ever sideloaded next to # a stable build VS Code shows it as pre-release rather than silently as a release. # +# See docs/ci.md for how this fits with the other workflows. +# # NOTE: this publishes NOTHING to any marketplace. It is the GitHub pre-stage only. name: preview on: push: branches: - - main + # develop is the integration trunk under GitFlow, so it is what "latest" means. + # main only ever receives release and hotfix merges, and those are tagged, so a + # preview build of main would duplicate a release build of the same commit. + - develop paths-ignore: - '**/*.md' workflow_dispatch: @@ -108,7 +113,7 @@ jobs: set -euo pipefail NOTES=$(cat < tag), not its entire dependency tree or the code-split ESM build. diff --git a/README.md b/README.md index c0786c8..a1b68e0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,11 @@ Explore, run, and visualize your [Fallout](https://github.com/Fallout-build/Fall The extension's `major.minor` track the Fallout framework release line it targets — 10.4.x builds against Fallout 10.4 — while the patch moves independently. A mismatch between the extension and the framework your workspace builds with surfaces as a non-blocking warning. -Versions are computed by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) from `version.json`, the same as the framework itself; the build fails if the declared line drifts from the Fallout version it actually references. Release candidates are published as GitHub pre-releases only — see [RELEASING.md](RELEASING.md). +Versions are computed by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) from `version.json`, the same as the framework itself; the build fails if the declared line drifts from the Fallout version it actually references. Release candidates are published as GitHub pre-releases only. + +## Contributing + +This repository uses GitFlow: branch from `develop`, PR back into `develop`. See [docs/branching-and-release.md](docs/branching-and-release.md), [docs/ci.md](docs/ci.md) and [docs/releasing.md](docs/releasing.md). ## License diff --git a/RELEASING.md b/RELEASING.md deleted file mode 100644 index 41f17d9..0000000 --- a/RELEASING.md +++ /dev/null @@ -1,121 +0,0 @@ -# Releasing - -Maintainer reference. Mirrors the Fallout repo's [branching-and-release](https://github.com/Fallout-build/Fallout/blob/main/docs/branching-and-release.md) model, adapted to extension marketplaces. - -## Versioning - -[Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) computes the version from `version.json`, as in the framework repo. - -- `version.json`'s `version` pins the **Fallout release line** this extension targets (`10.4`). The third component is the git height and moves on its own. -- The build asserts the declared line still matches the referenced `Fallout.Common`. Bumping one without the other fails the build rather than shipping a version that misstates its target. -- `versionHeightOffset` exists because height restarts when `version.json` is introduced or its `version` changes. Marketplace versions must increase monotonically, so the offset keeps the sequence moving forward. - -Check what will be produced: - -```bash -dotnet nbgv get-version -``` - -### Why release candidates aren't `-rc.N` - -Both registries take **exactly three integers**. `vsce` refuses a semver prerelease outright: - -> The VS Marketplace doesn't support prerelease versions - -So an RC is an ordinary triple carrying a pre-release bit in the VSIX manifest (`Microsoft.VisualStudio.Code.PreRelease`), and the `-rc.N` suffix lives only on the git tag and the GitHub release. - -The consequence that shapes the whole pipeline: **a version is pre-release or stable, never both.** Publishing `10.4.16` as a marketplace pre-release would burn that number, forcing GA to `10.4.17`. Release candidates therefore never reach a marketplace — they stop at GitHub, and the number stays free. - -## Channels - -GitHub is the pre-stage; each marketplace is a promotion target with its own environment. - -| Channel | Trigger | Gating | -|---|---|---| -| `preview` (rolling) | every push to `main` | none | -| `github-releases` | 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 deliberate: set the flag, then approve the environment — two independent layers, matching how Fallout gates nuget.org. - -## The preview channel - -Every push to `main` builds a `.vsix` and replaces the asset on a rolling `preview` GitHub pre-release (`preview.yml`). It's the counterpart to the framework's per-commit `-preview` packages — reshaped because GitHub Packages doesn't speak the VS Code gallery protocol and neither marketplace accepts a semver prerelease. - -The `preview` tag deliberately doesn't match `v*`, so it neither triggers `publish.yml` nor falls under the `v*` tag-protection ruleset — which matters, because the workflow force-moves it on every push. - -### Installing a preview - -```bash -gh release download preview -R Fallout-build/Fallout.Extensions.VSCode -p '*.vsix' --clobber -code --install-extension fallout.vsix --force -``` - -The download URL is stable, so that pair of commands is the whole update story — no run IDs to hunt, no expiry. Each run also uploads the same `.vsix` as a workflow artifact, which is the fixed record of what a given commit produced; the rolling asset can't be, since the next push replaces it. - -Building locally, `dotnet fallout PackVsix` produces the `.vsix` and you install it the same way. Installing is deliberately a manual step — the build never touches your editor. - -### Why this isn't auto-updating - -VS Code will not auto-update a manually installed extension — it only tracks versions for extensions it got from a gallery, and this channel has no gallery. Manual download-and-install is the deliberate trade-off; the build never touches your editor. - -Two ways to get real automatic updates, if that ever becomes worth the cost: - -- **Marketplace pre-release channel.** The native mechanism: VS Code offers *"Switch to Pre-Release Version"* and updates it like anything else. Requires an actual marketplace presence. Note the version-burning concern doesn't apply to a preview stream — the patch is a git height, so every build has a unique number and stable is always a later height. -- **A self-hosted gallery at `gallery.fallout.build`** — tracked as [#6](https://github.com/Fallout-build/Fallout.Extensions.VSCode/issues/6), with hosting options and trade-offs in [Chrison-Homelab/Homelab#409](https://github.com/Chrison-Homelab/Homelab/issues/409). Candidates are Microsoft's own [Private Marketplace](https://github.com/microsoft/vsmarketplace/blob/main/privatemarketplace/latest/README.md) (stateless container, configured by supported device-management policy, but every consumer needs a Copilot Business/Enterprise seat), `coder/code-marketplace`, or self-hosted Open VSX. Since registries are already modelled as data in `IPublishVsix`, adding one is a target entry rather than a new pipeline. - -## Cutting a release candidate - -```bash -git switch main && git pull --ff-only -dotnet nbgv get-version # the number this release will carry -gh release create v10.4.16-rc.1 --target main --prerelease --generate-notes -``` - -The tag must match the version `nbgv` computes for that commit — the workflow packages from the checked-out tag, not from the tag name, so a mismatch ships a `.vsix` whose version disagrees with its release. - -The tag push builds the `.vsix` with the pre-release bit set and attaches it to a GitHub pre-release. Install it with **Extensions: Install from VSIX…** to dogfood. - -## Cutting a stable release - -```bash -gh release create v10.4.16 --target main --generate-notes -``` - -That publishes to GitHub Releases only. To then promote to the marketplaces: - -```bash -gh workflow run publish.yml -f tag=v10.4.16 -f publish-to-marketplaces=true -``` - -Both `vs-marketplace` and `open-vsx` pause for approval on the run page (*Review deployments*). Approve each. The job publishes the exact artifact the `pack` job produced (`--skip PackVsix`), not a rebuild. - -You can rehearse the wiring without burning a release: set the flag, wait for the approval prompt, then cancel without approving. - -## If a publish fails partway - -Both CLIs are invoked with `--skip-duplicate`, so re-running is idempotent on whatever already landed: - -```bash -gh workflow run publish.yml -f tag=v10.4.16 -f publish-to-marketplaces=true -``` - -## Release notes - -Generated by GitHub from merged PR labels — the taxonomy lives in [`.github/release.yml`](.github/release.yml) and matches the Fallout repo's. Apply one category label per PR (`enhancement`, `bug`, `breaking-change`, `security`, `documentation`, `dependencies`), or `skip-changelog` for housekeeping. - -There is no `CHANGELOG.md`. The labels are the changelog — a hand-maintained file would duplicate them, and its version heading could not be written correctly in advance since the patch is a git height. If the marketplace page ever needs a rendered changelog, it should be **generated into the `.vsix` at pack time** from these notes, not reinstated as a tracked file. - -## Tokens - -| Secret | Used by | Scope | -|---|---|---| -| `VSCE_PAT` | `vs-marketplace` | Azure DevOps PAT, Marketplace > Manage | -| `OVSX_TOKEN` | `open-vsx` | Open VSX access token (mapped to `OVSX_PAT`) | - -Both are read from the environment by the CLIs, never passed as process arguments. Verify them without publishing: - -```bash -dotnet fallout VerifyVsixCredentials -``` diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs index e3c5039..e677780 100644 --- a/build/Build.CI.GitHubActions.cs +++ b/build/Build.CI.GitHubActions.cs @@ -28,7 +28,10 @@ ConcurrencyGroup = "${{ github.workflow }}-${{ github.ref }}", ConcurrencyCancelInProgress = true, CheckoutRef = "${{ github.head_ref }}", - OnPullRequestBranches = [MainBranch], + // Every long-lived branch a PR can target (docs/branching-and-release.md): the trunk, + // production, and any support line. Feature branches themselves run nothing until a PR + // is opened — there is no value in gating work that isn't asking to land yet. + OnPullRequestBranches = [DevelopBranch, MainBranch, SupportBranchPattern], // Kept to a single pattern so build-skip.yml can express the exact inverse. Anything // more clever here (negations, directory carve-outs) makes the two sets drift, and a // gap between them means a PR blocked forever on a required check that never fires. @@ -37,5 +40,12 @@ PublishArtifacts = false)] partial class Build { + /// Integration trunk and default branch. All feature work lands here first. + const string DevelopBranch = "develop"; + + /// Production. Only release and hotfix merges land here, and each is tagged. const string MainBranch = "main"; + + /// Long-lived maintenance line for an older Fallout release line, e.g. support/v10.4. + const string SupportBranchPattern = "support/*"; } diff --git a/docs/branching-and-release.md b/docs/branching-and-release.md new file mode 100644 index 0000000..c95f1c0 --- /dev/null +++ b/docs/branching-and-release.md @@ -0,0 +1,131 @@ +# Branching and release flow + +How this repository branches, ships, and maintains older lines. The model is **GitFlow** as originally defined, plus long-lived `support/*` lines because the extension has to keep serving Fallout release lines that `main` has moved past. + +> **Audience.** Anyone opening a PR needs the [Branches](#branches) table and [Where work lands](#where-work-lands). The rest is maintainer material — see [releasing.md](releasing.md) for the runbooks and [ci.md](ci.md) for what CI does at each step. + +## Branches + +| Branch | Purpose | Lifetime | Tagged? | +|---|---|---|---| +| `develop` | **Integration trunk. Default branch.** All finished work lands here first. Every push builds the [preview channel](releasing.md#the-preview-channel). | Permanent | No | +| `main` | **Production.** Only receives merges from `release/*` and `hotfix/*`, and every one of those is tagged. Never committed to directly. | Permanent | **Yes** | +| `release/*` | **Stabilisation window** for a release being prepared. Cut from `develop`; takes only fixes and release prep. Merges to `main` *and back to* `develop`, then deleted. | Short-lived | No (the merge into `main` is) | +| `hotfix/*` | **Urgent production fix.** Cut from `main`. Merges to `main` *and* `develop`, then deleted. | Short-lived | No (the merge into `main` is) | +| `support/vX.Y` | **Maintenance line** for an older Fallout release line still being served after `main` moved on — e.g. `support/v10.4` once `main` targets 10.5. Fixes only. | Permanent once cut | **Yes** | +| `feature/*`, `bugfix/*`, `chore/*`, `docs/*` | Working branches. | PR-and-merge, then deleted | No | + +The two permanent branches are the point of the whole model: **`main` answers "what is in production" and `develop` answers "what is next"**, and they are always two refs you can diff. + +`master` is not used. `support/*` is deliberately *not* called `release/*` — in GitFlow that prefix already means the temporary stabilisation branch, and overloading it makes every sentence about "the release branch" ambiguous. + +## The flow + +```mermaid +gitGraph + commit id: "initial" + branch develop + checkout develop + commit id: "trunk" + branch feature/targets-view + checkout feature/targets-view + commit id: "feature work" + checkout develop + merge feature/targets-view + commit id: "more trunk" + branch release/10.4.30 + checkout release/10.4.30 + commit id: "stabilise" + checkout main + merge release/10.4.30 tag: "v10.4.30" + checkout develop + merge release/10.4.30 + checkout main + branch hotfix/10.4.31 + checkout hotfix/10.4.31 + commit id: "urgent fix" + checkout main + merge hotfix/10.4.31 tag: "v10.4.31" + checkout develop + merge hotfix/10.4.31 +``` + +Note both merge-backs into `develop`. **Skipping either is the classic GitFlow mistake** — the fix ships to users and then vanishes on the next release, because the trunk never learned about it. + +## Where work lands + +Everything routine starts from `develop` and goes back to `develop`: + +```bash +git switch develop && git pull --ff-only +git switch -c feature/my-change +# … work … +gh pr create --base develop --label enhancement +``` + +`develop` is the default branch, so `gh pr create` targets it without `--base`. Pass `--base` explicitly anyway when you mean `main` or a support line, so the intent is on the record. + +One category label per PR (`enhancement`, `bug`, `breaking-change`, `security`, `documentation`, `dependencies`) or `skip-changelog` — the labels *are* the changelog, see [releasing.md](releasing.md#release-notes). + +## Which branch does a fix belong on? + +```mermaid +flowchart TD + A["A change needs to ship"] --> B{"Is production
broken right now?"} + B -->|No| C["feature/* or bugfix/*
off develop"] + C --> D["PR → develop"] + D --> E["Ships on the next release"] + B -->|Yes| F{"Does it affect the
line main is on?"} + F -->|Yes| G["hotfix/* off main"] + G --> H["PR → main, tag,
then merge back to develop"] + F -->|"No — only an
older Fallout line"| I["bugfix/* off support/vX.Y"] + I --> J["PR → support/vX.Y,
then tag that line"] + J --> K{"Does the trunk
have the same bug?"} + K -->|Yes| L["Also forward-port
to develop"] + K -->|No| M["Done — the code
has moved on"] +``` + +A `support/*` fix is **not** automatically forward-ported: those lines exist precisely because the trunk has moved on, so the same bug often doesn't exist there. Check rather than assume. + +## Cutting a support line + +`support/vX.Y` is cut **on demand, not preemptively** — the same rule Fallout applies to its production lines ([ADR-0007](https://github.com/Fallout-build/Fallout/blob/main/docs/adr/0007-cut-release-branch-on-demand.md)). A branch is created at the moment there is real work for it, not in anticipation. + +For this repo the trigger is specific: **`main` retargets a newer Fallout release line while the previous one still needs serving.** The extension's `major.minor` track the framework line it targets ([releasing.md](releasing.md#versioning)), so moving `main` from 10.4 to 10.5 would otherwise abandon every user still on Fallout 10.4. + +```bash +# main is about to move from Fallout 10.4 to the next line — preserve the 10.4 line first +git switch main && git pull --ff-only +git switch -c support/v10.4 +git push -u origin support/v10.4 +# then, on develop, retarget version.json + the pinned Fallout.Common together +``` + +There are **no support lines today**: `main` targets Fallout 10.4 and there is no newer line to move to. Nothing to cut yet. + +## Protection + +| | `develop` | `main` | `support/*` | +|---|---|---|---| +| PR required | yes | yes | yes | +| `ubuntu-latest` check | required | required | required | +| Code-owner review | `src/`, `plugins/` | same | same | +| Linear history | required | required | required | +| Force-push / delete | blocked | blocked | blocked | +| Admins exempt | yes | yes | yes | + +Tags matching `v*` are covered by a repository ruleset: creation, deletion and updates are blocked for non-admins. That matters more here than in most repos — **every release channel is tag-triggered**, so an accidental tag is an accidental release. + +Admins are deliberately exempt (`enforce_admins: false`), which keeps an escape hatch when a required check is stuck or production needs an urgent fix. + +## Merging + +Merge commits are disabled. **Squash or rebase only**, and linear history is enforced on all protected branches. + +One consequence specific to GitFlow: a `release/*` or `hotfix/*` branch has to merge into **two** branches. Squashing into `main` and then squashing the same work into `develop` produces two unrelated commits with the same content, which is fine here — we don't rely on `git branch --contains` for anything except [tag validation](ci.md#publishyml), which checks reachability from `main` and `support/*` only. + +## See also + +- [ci.md](ci.md) — what runs, when, and why the publish workflow is hand-written +- [releasing.md](releasing.md) — versioning, channels, and every release runbook +- [Fallout's branching-and-release](https://github.com/Fallout-build/Fallout/blob/main/docs/branching-and-release.md) — the framework repo's model, which differs: it has no `develop`, and its `release/YYYY` lines are permanent production branches rather than stabilisation windows diff --git a/docs/ci.md b/docs/ci.md new file mode 100644 index 0000000..6db7b75 --- /dev/null +++ b/docs/ci.md @@ -0,0 +1,107 @@ +# CI + +What runs, when, and why it's shaped this way. + +The governing principle: **the build is defined in C#, not in YAML.** Every workflow here provisions toolchains and routes channels; every step that actually does something invokes a Fallout target from [`build/Build.cs`](../build/Build.cs) driving the [`Fallout.Vsce`](../plugins/Fallout.Vsce) plugin. That's why the same commands work identically on a laptop and on a runner. + +## The workflows + +| File | Trigger | Does | Generated? | +|---|---|---|---| +| `build.yml` | PR to `develop`, `main`, `support/*` | `PackVsix` — the required check | **Yes**, from `[GitHubActions]` | +| `build-skip.yml` | PR touching only `**/*.md` | Nothing; reports the same check | No | +| `preview.yml` | push to `develop` | Packs, publishes the rolling `preview` release | No | +| `publish.yml` | `v*` tag push, or dispatch | Packs, releases, optionally promotes to marketplaces | No | + +```mermaid +flowchart LR + PR["Pull request"] --> G{"Only
*.md?"} + G -->|No| B["build.yml
PackVsix"] + G -->|Yes| S["build-skip.yml
no-op"] + B --> C(["ubuntu-latest ✓"]) + S --> C + + D["Push to develop"] --> P["preview.yml"] + P --> PR2["rolling 'preview'
pre-release"] + + T["Tag v*"] --> V["publish.yml"] + V --> R["GitHub release"] + V -.->|"dispatch flag
+ approval"| M["marketplaces"] + + style C fill:#2d6a4f,color:#fff + style M fill:#7f4f24,color:#fff +``` + +## build.yml — the required check + +Generated from the `[GitHubActions]` attribute in [`build/Build.CI.GitHubActions.cs`](../build/Build.CI.GitHubActions.cs). **Never hand-edit `.github/workflows/build.yml`** — edit the attribute and run `./build.ps1` (or `./build.sh`), which regenerates it. The file carries an `` header saying so. + +Branch protection requires a status check named **`ubuntu-latest`**. That's the *job* name, not the workflow name — protection keys on jobs, which is why the job is named after the image. + +### build-skip.yml, and the trap it exists for + +`build.yml` ignores `**/*.md`, so a docs-only PR doesn't burn CI on a build that can't be affected. But a required check that never reports leaves the PR **blocked forever** — GitHub waits for a check that will never arrive. + +`build-skip.yml` fires on the exact inverse path set, does nothing, and reports success under the same `ubuntu-latest` context. Three things must stay in lockstep between the two files: + +1. the job name (`ubuntu-latest`) — it *is* the status-check context; +2. the path sets, exact complements of one another; +3. the branch lists. + +This is why `OnPullRequestExcludePaths` is a single `**/*.md` pattern. An earlier version used a negation carve-out (`!.github/workflows/**`), which made the complement impossible to state — and a gap between the two sets is exactly how a PR ends up permanently blocked. + +A PR touching both a `.md` and a source file runs **both** workflows. Both report `ubuntu-latest`, both pass. That's fine. + +## preview.yml + +Every push to `develop` packs a `.vsix`, uploads it as a per-run artifact, and replaces the asset on a rolling `preview` GitHub pre-release. Details and rationale in [releasing.md](releasing.md#the-preview-channel). + +Two deliberate choices worth knowing when reading it: + +- **Concurrency queues, never cancels.** A cancelled run could leave the release holding a half-uploaded asset. +- **The tag is `preview`, which does not match `v*`.** So it neither triggers `publish.yml` nor falls under the `v*` tag-protection ruleset — necessary, because the workflow force-moves that tag on every push. + +## publish.yml + +Hand-written, and it stays that way for two reasons the generator can't address: + +1. It needs `actions/setup-node` with npm caching. The generator emits checkout, cache and setup-dotnet, with **no hook for extra steps**. +2. It fans out into per-channel jobs bound to GitHub Environments with approval gates, passing one artifact between them. The generator emits **one job per image** and has no concept of that. + +The framework repo splits it the same way and for the same category of reason — generated CI, hand-written publish. + +```mermaid +flowchart TD + T["v* tag push"] --> VR["validate-ref"] + D["workflow_dispatch"] -.->|skipped| VR + VR --> PK["pack
PackVsix + artifact"] + PK --> GR["publish → github-releases
ungated"] + PK -.->|"flag + approval"| VM["publish → vs-marketplace"] + PK -.->|"flag + approval"| OV["publish → open-vsx"] + + style GR fill:#2d6a4f,color:#fff + style VM fill:#7f4f24,color:#fff + style OV fill:#7f4f24,color:#fff +``` + +**`validate-ref`** confirms the tag is reachable from `main` or a `support/vX.Y` branch. Tags on `develop` are rejected: under GitFlow the trunk is never tagged for release — it ships through the preview channel instead. + +It runs only on tag pushes. On `workflow_dispatch` it's skipped by design, which is why every downstream job is guarded with `always() && needs..result == 'success'` rather than a bare dependency — a skipped job otherwise propagates through the graph and silently skips everything after it *while the run still reports success*. + +The promotion jobs publish the exact artifact `pack` produced (`--skip PackVsix`) rather than rebuilding, so what gets approved is what ships. + +## Running CI locally + +There is no separate CI script. The runner invokes the same targets you do: + +```bash +./build.ps1 PackVsix # what the PR gate runs +dotnet fallout --plan # what would run, without running it +dotnet fallout VerifyVsixCredentials # proves marketplace tokens, publishes nothing +``` + +## Gotchas + +- **`PublicRelease: true`** is set on any job that checks out a tag. A detached HEAD matches none of `version.json`'s branch refspecs, so without it Nerdbank.GitVersioning appends a git-height suffix and the marketplace version stops being a clean triple. +- **`fetch-depth: 0`** is required wherever the version is computed — NB.GV needs full history. +- **Boolean workflow inputs arrive as strings** from the REST API, and therefore from `gh workflow run`. Conditions compare against both `true` and `'true'`; a bare `== true` silently skips while reporting success. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..9b7415c --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,189 @@ +# Releasing + +Versioning, channels, and the runbook for every kind of release. Branch model in [branching-and-release.md](branching-and-release.md); what CI does at each step in [ci.md](ci.md). + +## Versioning + +[Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) computes the version from `version.json`, the same as the framework repo. + +- `version.json`'s `version` pins the **Fallout release line** this extension targets (`10.4`). The third component is the git height and moves on its own. +- The build **asserts** the declared line still matches the referenced `Fallout.Common`. Bumping one without the other fails the build rather than shipping a version that misstates what it targets. +- `versionHeightOffset` exists because height restarts when `version.json`'s `version` changes. Marketplace versions must increase monotonically, so the offset keeps the sequence moving forward. + +```bash +dotnet nbgv get-version # the number this commit would ship as +``` + +### Why release candidates aren't `-rc.N` + +Both registries take **exactly three integers**. `vsce` refuses a semver prerelease outright: + +> The VS Marketplace doesn't support prerelease versions + +So an RC is an ordinary triple carrying a pre-release bit in the VSIX manifest (`Microsoft.VisualStudio.Code.PreRelease`), and `-rc.N` lives only on the git tag and the GitHub release. + +**A version is pre-release or stable, never both.** Publishing `10.4.30` as a marketplace pre-release burns that number, forcing GA to `10.4.31`. Release candidates therefore stop at GitHub. + +That argument does *not* apply to the [preview channel](#the-preview-channel): the patch is a git height, so every build already has a unique number and stable is always a later height. Nothing gets consumed that a stable release wants. + +## Channels + +```mermaid +flowchart LR + DEV["develop"] -->|every push| PREV["preview
rolling pre-release"] + MAIN["main / support/*"] -->|"v* tag"| GH["github-releases"] + GH -.->|"opt-in flag
+ approval"| VSM["vs-marketplace"] + GH -.->|"opt-in flag
+ approval"| OVSX["open-vsx"] + + style PREV fill:#1d4e6f,color:#fff + style GH fill:#2d6a4f,color:#fff + style VSM fill:#7f4f24,color:#fff + style OVSX fill:#7f4f24,color:#fff +``` + +| Channel | Trigger | Gating | +|---|---|---| +| `preview` (rolling) | every push to `develop` | none | +| `github-releases` | any `v*` tag | none | +| `vs-marketplace` | dispatch opt-in flag | flag + approval | +| `open-vsx` | dispatch opt-in flag | flag + approval | + +**A tag push never reaches a marketplace.** Promotion is deliberate: set the flag, then approve the environment — two independent layers, matching how Fallout gates nuget.org. + +## The preview channel + +Every push to `develop` builds a `.vsix`, uploads it as a per-run workflow artifact, and replaces the asset on a rolling `preview` GitHub pre-release. + +Both, deliberately: the release asset has a **stable URL** and is what you install from, but the next push replaces it — so the per-run artifact is the fixed record of what a given commit produced. + +```bash +gh release download preview -R Fallout-build/Fallout.Extensions.VSCode -p '*.vsix' --clobber +code --install-extension fallout.vsix --force +``` + +Installing is a manual step by design; the build never touches your editor. VS Code only auto-updates extensions it obtained from a gallery, so this is a one-shot install — see [#6](https://github.com/Fallout-build/Fallout.Extensions.VSCode/issues/6) for the self-hosted-gallery idea that would change that. + +## Cutting a release + +```mermaid +flowchart TD + A["develop is where you want it"] --> B{"Needs a
stabilisation window?"} + B -->|"No — ship develop as-is"| C["PR develop → main"] + B -->|"Yes"| D["Cut release/X.Y.Z from develop"] + D --> E["Fix only on the release branch"] + E --> F["PR release/X.Y.Z → main"] + C --> G["Rebase-merge into main"] + F --> G + G --> H["Tag main → publish.yml fires"] + H --> I["GitHub release created"] + F -.->|"then"| J["Port stabilisation commits
back to develop"] + I -.->|"optional, later"| K["Promote to marketplaces"] +``` + +### Simple release — nothing to stabilise + +```bash +git switch develop && git pull --ff-only +gh pr create --base main --title "Release" --label skip-changelog +# merge with REBASE (see below), then: +git switch main && git pull --ff-only +dotnet nbgv get-version # confirm the number +gh release create v10.4.30 --target main --generate-notes +``` + +### With a stabilisation window + +```bash +git switch -c release/10.4.30 develop +git push -u origin release/10.4.30 +# … fixes land here by PR, feature work continues on develop … +gh pr create --base main +# merge, tag as above, then port the stabilisation commits back: +git switch -c chore/port-10.4.30 develop +git cherry-pick … +gh pr create --base develop +``` + +### Release candidate + +```bash +gh release create v10.4.30-rc.1 --target main --prerelease --generate-notes +``` + +The tag must match the version `nbgv` computes for that commit — the workflow packages from the checked-out tag, not from the tag name, so a mismatch ships a `.vsix` whose version disagrees with its release. + +> **Merge with rebase, not squash, for `develop` → `main`.** Squashing collapses a whole release into one commit, which loses the per-change history on the production branch and makes later comparisons between the two branches useless. Merge commits are disabled repo-wide and linear history is enforced, so rebase is the only option that keeps commits intact. + +> **"Merge back to develop" is a cherry-pick or a second PR here**, not a literal merge. GitFlow assumes merge commits; this repo enforces linear history. The effect is the same — the fix must reach `develop`, or it ships to users and then disappears on the next release — but the mechanism is a PR carrying the same changes. + +## Hotfix + +Production is broken and it can't wait for the next release. + +```bash +git switch main && git pull --ff-only +git switch -c hotfix/10.4.31 +# … fix … +gh pr create --base main --label bug +# merge, then: +gh release create v10.4.31 --target main --generate-notes +``` + +Then get it onto the trunk — **this step is not optional**: + +```bash +git switch -c bugfix/port-10.4.31 develop +git cherry-pick +gh pr create --base develop --label skip-changelog +``` + +## Releasing from a support line + +`support/vX.Y` serves an older Fallout line. Tags on it fire the same pipeline — `validate-ref` accepts `main` and `support/vX.Y`. + +```bash +git switch support/v10.4 && git pull --ff-only +# … fix lands by PR … +gh release create v10.4.99 --target support/v10.4 --generate-notes +``` + +Whether to forward-port is a judgement call: those lines exist because the trunk moved on, so the same bug often doesn't exist there. Check, don't assume. + +## Promoting to the marketplaces + +Nothing reaches a marketplace from a tag. Promotion is an explicit act: + +```bash +gh workflow run publish.yml -f tag=v10.4.30 -f publish-to-marketplaces=true +``` + +Both `vs-marketplace` and `open-vsx` then pause for approval on the run page (*Review deployments*). Approve each. The job publishes the exact artifact the `pack` job produced (`--skip PackVsix`), not a rebuild. + +You can rehearse the wiring without burning a release: set the flag, wait for the approval prompt, then cancel without approving. + +## If a publish fails partway + +Both CLIs are invoked with `--skip-duplicate`, so re-running is idempotent on whatever already landed: + +```bash +gh workflow run publish.yml -f tag=v10.4.30 -f publish-to-marketplaces=true +``` + +## Release notes + +Generated by GitHub from merged PR labels — the taxonomy lives in [`.github/release.yml`](../.github/release.yml) and matches the framework repo's. Apply one category label per PR (`enhancement`, `bug`, `breaking-change`, `security`, `documentation`, `dependencies`), or `skip-changelog` for housekeeping. + +There is **no `CHANGELOG.md`**. The labels are the changelog: a hand-maintained file would duplicate them, and its version heading can't be written correctly in advance since the patch is a git height not settled until the release is cut. If the marketplace page ever needs a rendered changelog, generate it into the `.vsix` at pack time rather than reinstating a tracked file. + +## Tokens + +| Secret | Used by | Scope | +|---|---|---| +| `VSCE_PAT` | `vs-marketplace` | Azure DevOps PAT, Marketplace > Manage | +| `OVSX_TOKEN` | `open-vsx` | Open VSX access token (mapped to `OVSX_PAT`) | + +Both are read from the environment by the CLIs, never passed as process arguments. Verify without publishing: + +```bash +dotnet fallout VerifyVsixCredentials +``` diff --git a/version.json b/version.json index d599743..b5d7484 100644 --- a/version.json +++ b/version.json @@ -10,11 +10,15 @@ // pre-release bit in the VSIX manifest, requested per-build via --pre-release. "version": "10.4", - // main produces the releasable version directly; there is no separate preview lane, because - // the pre-stage is a GitHub pre-release of the same artifact rather than a different feed. + // Every branch that can produce a distributable build (docs/branching-and-release.md). + // A non-public ref makes NB.GV append -g, which MarketplaceVersion strips anyway — + // but listing them keeps `nbgv get-version` honest, so the number you read before tagging + // is the number that ships. "publicReleaseRefSpec": [ "^refs/heads/main$", - "^refs/heads/release/v\\d+\\.\\d+$" + "^refs/heads/develop$", + "^refs/heads/release/.*$", + "^refs/heads/support/v\\d+\\.\\d+$" ], // The extension shipped its changelog entry as 10.4.15 before versioning moved to NB.GV.