Add automated per-plugin versioning (NBGV) with /version-bump + weekly backstop - #813
Conversation
…y backstop WHY Tools that surface skills (Copilot CLI, Claude Code, Codex, Cursor) read a plugin's version directly from its checked-in manifest. With no versioning discipline, a plugin's behavior can change while its advertised version stays flat, so clients never learn to re-pull, and there is no human-readable signal of what changed. We want correct, current versions in the repo with minimal manual work and without bloating the marketplace clone. WHAT - Per-plugin semantic versioning via Nerdbank.GitVersioning (NBGV). Each plugin owns a version.json whose pathFilters exclude the generated manifests and the version.json itself, so version height tracks real content changes only. - The computed version is materialized into the checked-in manifests (plugin.json and .codex-plugin/plugin.json) so every consumer reads a current value with no build step on their side. - eng/version/Sync-PluginVersions.ps1 is the single workhorse. It resolves the set of changed plugins from a git diff, computes each version with nbgv (predicting the squash-merge height for PRs), and either reports or stamps. AUTOMATIONS (two, low-touch by design) - /version-bump: an admin/maintainer comments the command on a PR and the affected plugins are stamped on the PR branch. Gated on collaborator permission (admin/write/maintain); forks are rejected before any privileged step. No other PRs are auto-modified. - weekly-version-sync: a Monday backstop (and workflow_dispatch) that stamps any drift on main, opens/updates a single bot PR, and explains the per-plugin reason. This self-heals anything that merged without a bump. We deliberately did NOT auto-edit contributor PRs or add a noisy advisory comment bot; maintainers stay in control and the signal stays clean. SECURITY (multi-model adversarial review: GPT-5.5 + Gemini 3.1 Pro) - Supply chain (High, both models): dotnet tool restore would have honored a nuget.config authored in the PR tree, letting an attacker remap the nbgv package source to a malicious feed and run code in the privileged contents:write context. Mitigated with a trusted eng/version/nuget.config (clear + nuget.org-only + packageSourceMapping), overlaid from main and used via --configfile so PR-supplied configs are ignored. No nuget.config is tracked in the repo today, so this path was genuinely exploitable. - TOCTOU (Medium): /version-bump now checks out the authorized head SHA rather than the mutable branch name; a racing push fails non-fast-forward, which is the safe outcome. - Injection: Set-ManifestVersion uses a MatchEvaluator (not a replacement string) so a "$"-bearing version cannot re-expand, plus a strict major.minor.patch guard that throws on a malformed base, leaving manifests untouched. - A base-only version.json bump (0.1 -> 0.2) is correctly detected and stamped. VERIFIED End-to-end against a real NBGV git harness: content-scoped predict, base-only bump -> x.y.0, docs-only -> [], weekly drift stamping, malformed-base guard, and --configfile restore (exit 0). actionlint passes on both workflows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skill Coverage Report
Uncovered:
|
|
Curious to hear thoughts on this versioning strategy. Goal was to keep this with as less ceremony as possible. At the moment Claude code users do not really get updates if they've already pulled down a plugin earlier because we haven't changed our versions. |
There was a problem hiding this comment.
Pull request overview
This PR introduces automated, per-plugin semantic versioning for the plugins/* tree using Nerdbank.GitVersioning (NBGV), and adds two GitHub Actions automations to materialize the computed versions into the checked-in plugin manifests (so downstream consumers read current versions directly from the repo).
Changes:
- Add per-plugin
plugins/<plugin>/version.jsonfiles to scope NBGV version height to each plugin subtree. - Add
eng/version/Sync-PluginVersions.ps1plus a locked-downeng/version/nuget.configand a local tool manifest fornbgv. - Add two workflows:
/version-bump(issue_comment-driven, same-repo PRs) and a scheduled weekly backstop PR that stamps any drift onmain.
Show a summary per file
| File | Description |
|---|---|
| plugins/dotnet11/version.json | Adds NBGV per-plugin version configuration for dotnet11. |
| plugins/dotnet/version.json | Adds NBGV per-plugin version configuration for dotnet. |
| plugins/dotnet-upgrade/version.json | Adds NBGV per-plugin version configuration for dotnet-upgrade. |
| plugins/dotnet-test/version.json | Adds NBGV per-plugin version configuration for dotnet-test. |
| plugins/dotnet-template-engine/version.json | Adds NBGV per-plugin version configuration for dotnet-template-engine. |
| plugins/dotnet-nuget/version.json | Adds NBGV per-plugin version configuration for dotnet-nuget. |
| plugins/dotnet-msbuild/version.json | Adds NBGV per-plugin version configuration for dotnet-msbuild. |
| plugins/dotnet-maui/version.json | Adds NBGV per-plugin version configuration for dotnet-maui. |
| plugins/dotnet-experimental/version.json | Adds NBGV per-plugin version configuration for dotnet-experimental. |
| plugins/dotnet-diag/version.json | Adds NBGV per-plugin version configuration for dotnet-diag. |
| plugins/dotnet-data/version.json | Adds NBGV per-plugin version configuration for dotnet-data. |
| plugins/dotnet-blazor/version.json | Adds NBGV per-plugin version configuration for dotnet-blazor. |
| plugins/dotnet-aspnetcore/version.json | Adds NBGV per-plugin version configuration for dotnet-aspnetcore. |
| plugins/dotnet-ai/version.json | Adds NBGV per-plugin version configuration for dotnet-ai. |
| eng/version/Sync-PluginVersions.ps1 | Core PowerShell implementation to compute/stamp versions and emit a JSON report. |
| eng/version/nuget.config | Locked-down NuGet config used by workflows to prevent PR-controlled feed remapping during tool restore. |
| .config/dotnet-tools.json | Adds local nbgv tool dependency for consistent version computation in CI. |
| .github/workflows/version-bump-command.yml | Implements the maintainer-triggered /version-bump workflow. |
| .github/workflows/weekly-version-sync.yml | Implements the scheduled backstop that opens/updates a single “weekly sync” PR on drift. |
| CONTRIBUTING.md | Documents the new per-plugin versioning model and contributor expectations. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 20/20 changed files
- Comments generated: 4
|
👋 @AbhitejJohn — this PR has 4 unresolved review thread(s). When you're ready, please address the feedback and push an update; the triage bot will pick up the next state automatically. (Add the |
- Add missing plugins/dotnet-test-migration/version.json so it participates in versioning (it was the only plugin without one; manifests are at 0.1.0). - CONTRIBUTING: the two manifests are not byte-identical; say the version is duplicated across two manifest files instead. - weekly-version-sync: include version.json in commit attribution so a base-only bump is explained rather than showing 'no attributable commits'. - Get-NbgvInfo: capture nbgv stderr and include it in the thrown error so CI failures are diagnosable, while keeping stdout clean for JSON parsing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
✅ Evaluation passed for |
Evangelink
left a comment
There was a problem hiding this comment.
Review: per-plugin NBGV versioning
Really like the overall design — per-plugin pathFilters, materializing into the checked-in manifests, the opt-in /version-bump + weekly backstop split, and the security hardening on the privileged workflow (SHA-pinned actions, fail-closed permission gate, fork rejection before any privileged step, --configfile source-mapped NuGet, trusted-tooling overlay from main, injection-safe MatchEvaluator). The security posture of version-bump-command.yml looks sound to me.
My concern is that the privileged workflow's git plumbing and the tool runtime pin look like they'll prevent /version-bump (and likely the weekly sync) from actually running. Because both workflows are issue_comment/schedule-triggered they run from main and get no CI signal on this PR, so these can only be caught by review. Two of the inline findings below were reproduced in a scratch repo.
This was a combined pass (two independent reviewers); we converged on the same set with no contradictions. Inline comments have details + suggested fixes. Summary:
Blocking / CI-breaking
git fetch --depth=1 origin mainmakes the whole repo shallow → NBGV refuses to compute height. Reproduced: a depth-limited fetch writes.git/shallowand flips the entire repo to shallow even though the head was checked out withfetch-depth: 0, so everynbgv get-versionin the ordinary-content path fails.git merge-base $BASE_SHA $headreturns empty when the PR is behindmain. Reproduced (PR 3 commits behind → empty merge-base, exit 1 → script throws-PredictSquashMerge requires -BaseCommit). Fixed by the same full-fetch change, plus an empty-merge-base guard.nbgv(net8) underrollForward: falseon a net11-preview-only runtime.setup-dotnetinstalls only theglobal.jsonruntime; a net8-targeted tool won't run, and evenrollForward: truewon't pick a preview runtime withoutDOTNET_ROLL_FORWARD_TO_PRERELEASE=1. Best fix: explicitly install an 8.0.x runtime in both workflows.
Correctness
- Squash prediction over-bumps on
version.json-only (non-base) edits, then the weekly backstop computes the true (lower) height and corrects downward — a non-monotonic version regression visible to consumers.
Minor
- Format guard validates the computed value but not the 2-part base shape in
version.json(a malformed 3-part base slips through the weekly path). - Concurrent
/version-bumpon the same plugin can stamp colliding patch numbers until the weekly sync reconciles — worth a doc note. - PR description says
version.json×14; there are 15 (dotnet-test-migrationwas added in commit 2).
|
👋 @AbhitejJohn — this PR has 5 unresolved review thread(s). When you're ready, please address the feedback and push an update; the triage bot will pick up the next state automatically. (Add the |
Address @Evangelink's review on the per-plugin versioning workflows. These issue_comment/schedule workflows run from main and get no CI signal on the PR, so each fix is validated with a local NBGV git harness instead. Workflow runtime/git plumbing (version-bump-command.yml, weekly-version-sync.yml): - Drop --depth=1 from the "pin trusted tooling from main" fetch. A shallow fetch writes .git/shallow and flips the whole repo shallow (proven locally), and NBGV refuses to compute height on a shallow repo. fetch-depth: 0 already fetched main, so the full fetch is cheap and keeps the supply-chain pin intact. - Add a guard that throws if `git merge-base BASE_SHA head` is empty, instead of letting nbgv fail with an opaque "-PredictSquashMerge requires -BaseCommit". - setup-dotnet: install dotnet-version 8.0.x alongside global-json-file. nbgv targets net8.0; global.json pins an SDK that ships only a net11 preview runtime, onto which a release-targeted tool will not roll forward. setup-dotnet installs both inputs, giving nbgv a runtime to run on. Prediction correctness (Sync-PluginVersions.ps1): - Only add +1 to the predicted height when a height-bearing file actually changed. A version.json-only non-base edit is height-neutral, so predicting +1 over-bumps and the weekly sync would later correct it downward — a visible version regression. New Test-HeightBearingChange checks the diff with the canonical exclusions. - Validate the version.json base is major.minor; a 3-part base (e.g. 0.1.0) was silently normalized by NBGV into a passing SimpleVersion on the weekly path. - Reject non-canonical pathFilters (raised in cross-model review). The predict height math assumes version.json and the two stamped manifests are excluded and that the filter set is stable between base and head; a hand-edited pathFilters would silently diverge the post-merge height from the prediction. The height-excluded file list is now a single source of truth shared by the guard and Test-HeightBearingChange. CONTRIBUTING.md: note that concurrently bumped PRs can predict the same patch and that the weekly sync reconciles the collision. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
✅ Evaluation passed for |
|
👋 @AbhitejJohn — this PR has 6 unresolved review thread(s). When you're ready, please address the feedback and push an update; the triage bot will pick up the next state automatically. (Add the |
The main merge bumped dotnet, dotnet-test, and dotnet-advanced to 0.2.0 in their manifests, but their NBGV bases hadn't followed — a mismatch the weekly sync would have "corrected" downward into a version regression. Addresses the Copilot review on the merge commit. - plugins/dotnet, plugins/dotnet-test: version.json base 0.1 -> 0.2 to match the manifests. Verified with an NBGV harness: base 0.1 + manifest 0.2.0 computes 0.1.1 (a downgrade); base 0.2 computes 0.2.0 (the base change resets git height to 0), so no regression. - Add plugins/dotnet-advanced/version.json (base 0.2, canonical pathFilters); it shipped a plugin.json but had no version base, so automation skipped it. Sync-PluginVersions.ps1: - Require -HeadCommit whenever -PredictSquashMerge is set. Without it the script fell back to all plugins and defaulted bumps=1, predicting a +1 patch for every plugin. Also simplifies the now-unconditional bumps calculation. - Set-Location to the repo root before running git, so the repo-root-relative pathspecs resolve correctly regardless of the caller's working directory. - Weekly path now enumerates by plugin.json and fails fast if a plugin has no version.json, instead of silently skipping it. The predict path is made symmetric: a changed shipped plugin (has plugin.json) missing version.json throws, while a deleted/non-plugin dir is still skipped. CONTRIBUTING: document that every plugin must carry its own version.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fail-fast Resolves the 3 findings from the Copilot re-review of ebd957e. version-bump-command.yml: - After overlaying trusted tooling from main via `git checkout FETCH_HEAD -- <paths>`, that checkout also STAGES those paths. Add `git reset -q HEAD -- <same paths>` so the index returns to HEAD's copies while the working tree keeps the overlay: the script still runs main's tooling, but a later commit can never include the overlaid tooling even if cleanup is skipped/fails. - Make the worktree-hygiene cleanup `git checkout HEAD -- <paths>` non-fatal (`|| true`) so an older PR branch whose HEAD predates a file (e.g. no global.json) doesn't fail on "pathspec did not match". Rewrote the comment to clarify the unstage — not this cleanup — is what prevents a tooling leak. Sync-PluginVersions.ps1: - Treat the Codex manifest (.codex-plugin/plugin.json) as required: throw fast if a shipped plugin is missing it, rather than silently stamping only plugin.json and shipping mismatched versions across consumers. Placed after the version.json/deleted-plugin guard so deletions still skip cleanly. - Simplify $currentCodex to an unconditional read and $changed accordingly, since existence is now guaranteed. Validated: actionlint clean; script parses; harness scenarios prove codex-missing throws, both manifests stamp equally, and manifest drift is detected. Cross-model (GPT-5.5) review found no issues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rden /version-bump permission check Resolves the 3 findings from the Copilot re-review of ebfc958. Optional third manifest (.claude-plugin/plugin.json): - One plugin (dotnet-msbuild, via #844) also carries plugins/<name>/.claude-plugin/plugin.json — Claude Code needs an inline mcpServers block where the Codex manifest uses a reference. It has its own "version" field, but the script only stamped plugin.json and .codex-plugin/plugin.json, so it would drift. The top-level marketplace.json files carry no version fields, so they stay out of scope. - Sync-PluginVersions.ps1: add .claude-plugin/plugin.json to the universal $HeightExcludedFiles (so it's height-excluded and part of the canonical pathFilters set) and to Get-ChangedPlugins' output-manifest excludes. In the main loop, treat it as optional (stamp-if-present, keyed off Test-Path so a present-but-malformed manifest fails loudly instead of being skipped); codex stays required. Include it in the drift/$changed check and the write path. - All 16 plugins/*/version.json: add ":!.claude-plugin/plugin.json" so they still match the now-4-entry canonical set. Excluding a manifest a plugin doesn't have is a harmless no-op and future-proofs any plugin that later adds one. - CONTRIBUTING.md: document the optional third manifest. version-bump-command.yml: - The actor permission lookup returned a 404 for non-collaborators, which made `gh api` exit non-zero and (under the default `set -eo pipefail` shell) aborted the step before the friendly "requires write access" denial comment. Default to "none" on any API failure so the denial path always runs. This can only ever deny more, never grant. - Add a non-fatal `git add plugins/*/.claude-plugin/plugin.json` so the new manifest is committed, and an empty glob (if no plugin has one in future) can't fail the step. Validated: actionlint clean; script parses; harness scenarios prove all three manifests stamp equally, the Claude manifest is height-excluded (no self-bump), claude-only drift is detected, a plugin without one stamps only its two, a present-but-malformed Claude manifest fails loudly, and the canonical guard accepts the 4-exclusion set. Earlier codex/regression guard suites still pass. Cross-model (GPT-5.5) review drove the Test-Path presence fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…weekly sync Resolves the 2 findings from the Copilot re-review of 65d79ce, both in the weekly sync workflow (the /version-bump command was already updated in 65d79ce but the weekly sync was missed). weekly-version-sync.yml: - The stamping script updates the optional .claude-plugin/plugin.json when a plugin has one, but the commit step only staged plugin.json and .codex-plugin/plugin.json — so for dotnet-msbuild the stamped Claude manifest would never be committed, causing persistent drift and a repeat sync PR every week. Add a non-fatal `git add plugins/*/.claude-plugin/plugin.json` (mirrors the /version-bump fix; empty glob can't fail the step). - Update the generated PR body to say the sync also stamps .claude-plugin/plugin.json where present, so reviewers of the bot PR aren't confused. - Exclude .claude-plugin/plugin.json from the per-plugin commit attribution (git log) too, consistent with the other two output manifests, so a manifest stamp is never listed as a reason for the bump. actionlint clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
👋 @AbhitejJohn — this PR has 1 unresolved review thread(s). When you're ready, please address the feedback and push an update; the triage bot will pick up the next state automatically. (Add the |
The header comment contained `--configfile`, i.e. a doubled hyphen `--`, which XML forbids inside a comment. That made eng/version/nuget.config invalid XML, so `dotnet tool restore --configfile eng/version/nuget.config` failed with "NuGet.Config is not valid XML" on every run of BOTH versioning workflows (weekly-version-sync and version-bump-command). As a result no automated version bumps have ever been produced since the infra merged in dotnet#813. Reword the comment to avoid any doubled-hyphen sequence and move it inside <configuration>. Verified: the file now parses as XML and `dotnet tool restore` succeeds; nbgv get-version computes correctly against the plugins. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 70b3727e-c27d-4a3e-aff9-f4eb65eaf25c
Why
Every client that surfaces these skills — Copilot CLI, Claude Code, Codex, Cursor — reads a plugin's version directly from its checked‑in manifest (
plugin.json/.codex-plugin/plugin.json). Today those versions are static, so:We want correct, current versions in the repo with minimal manual work, without bloating the marketplace clone and without taking on third‑party/external plugins (out of scope for now).
What this adds
version.jsonwhosepathFiltersexclude the generated manifests andversion.jsonitself, so the computed version height tracks real content changes only (editing a manifest or the version file doesn't inflate the height).eng/version/Sync-PluginVersions.ps1— resolves the changed‑plugin set from a git diff, computes each version withnbgv(predicting the squash‑merge height for PRs), and either reports or stamps.Automations (two, low‑touch by design)
/version-bump/version-bumpon a PRweekly-version-syncworkflow_dispatchmain, opens/updates a single bot PR, and explains the per‑plugin reason. Self‑heals anything merged without a bump.We deliberately did not auto‑edit contributor PRs or add a noisy advisory‑comment bot — maintainers stay in control and the signal stays clean. (No official skills repo we surveyed auto‑mutates contributor PRs for versioning; the opt‑in command + scheduled backstop pattern is the conservative norm.)
Security — multi‑model adversarial review (GPT‑5.5 + Gemini 3.1 Pro)
dotnet tool restorewould have honored anuget.configauthored in the PR tree, letting an attacker remap thenbgvpackage source to a malicious feed and execute code in the privilegedcontents: writecontext. This was genuinely reachable — nonuget.configis tracked anywhere in the repo today, so a PR could add one. Fix: a trustedeng/version/nuget.config(<clear/>+ nuget.org‑only +packageSourceMapping), overlaid frommainand consumed via--configfileso PR‑supplied configs are ignored entirely./version-bumpnow checks out the authorized head SHA rather than the mutable branch name; if the branch advances after authorization, the push fails non‑fast‑forward — the safe outcome.Set-ManifestVersionuses aMatchEvaluator(not a replacement string), so a$‑bearing version can't re‑expand, plus a strict^\d+\.\d+\.\d+$guard that throws on a malformed base and leaves manifests untouched.version.jsonbump (0.1 → 0.2) is now detected and stamped.Verification
Exercised end‑to‑end against a real NBGV git harness (the repo
global.jsonpins an SDK that isn't installable locally, so testing uses an isolated harness):x.y.0;[](no bump);-OnlyChanged→ stamps drift authoritatively;dotnet tool restore --configfile→ exit 0.actionlintpasses on both workflows.Files
eng/version/Sync-PluginVersions.ps1,eng/version/nuget.config,.config/dotnet-tools.json.github/workflows/version-bump-command.yml,.github/workflows/weekly-version-sync.ymlplugins/*/version.json(×15),CONTRIBUTING.mdNotes for reviewers
issue_comment/scheduled workflows run the YAML frommain, so/version-bumpand the weekly job only take effect once this is merged — expected, not a bootstrap bug.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com