| name | dependency-update |
|---|---|
| description | Sweep every version-pinned surface in the bitplatform repo (NuGet, npm, GitHub Actions, Azure DevOps tasks, devcontainers, dotnet-tools, global.json, container image tags), update what should move, leave what is deliberately held, and report each change with its release notes and any breaking behaviour. Use when asked to "update dependencies/packages/versions", refresh CI, or before a release. |
Update every version pin in the repo in one pass, then hand back a report the maintainer can review without opening the diff.
The sweep reads its pins from the working tree, so a checkout that is behind develop sweeps stale
pins: it proposes bumps that already landed upstream and misses packages added since the last pull.
Before reading a single pin, fetch and count:
up=$(git remote -v | awk '$2 ~ /bitfoundation\/bitplatform(\.git)?$/ && $3 == "(fetch)" { print $1; exit }')
git fetch "$up" "+refs/heads/develop:refs/remotes/$up/develop"
git rev-list --count HEAD.."$up"/develop # commits the checkout is missing$up is the remote that points at bitfoundation/bitplatform: upstream when origin is your
fork, origin in a plain clone. If the count is not 0:
- On
developwith a clean tree, fast-forward it:git merge --ff-only "$up"/develop. - Anywhere else (another branch, local changes, commits of its own), leave the checkout alone and run
the whole sweep, scan included, in a worktree on the latest
develop(git worktree add --detach ../bitplatform-sweep "$up"/develop). Once the issue exists, name the branch there withgit switch -c <n>instead of adding the worktree in Ship it step 2.
Open the report with the commit the sweep read.
Work the surfaces in this order. Each is independent; batch the network lookups.
- NuGet (
*.csproj,Directory.Packages.props) - npm (
package.json, thennpm installto refreshpackage-lock.json) - GitHub Actions (
.github/workflows/,.github/actions/,src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/) - Azure DevOps tasks (
src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/) - devcontainers (
.devcontainer/,src/Templates/Boilerplate/Bit.Boilerplate/.devcontainer/) dnx <package>@<version>calls in workflows and docs (vpk,dotnet-ef),global.json(3 files)- Container image tags in the Aspire AppHost
Most of the value here is knowing what NOT to move. A pin in one of these categories is a decision, not staleness — leave it, and say in the report that you left it and why.
Roslyn and MSBuild — never bump. An analyzer, source generator, or MSBuild task loads inside the
consumer's compiler, not ours. Referencing a Microsoft.CodeAnalysis.* newer than the oldest SDK we
support makes it fail to load there (CS8032 / AD0001). Same for Microsoft.Build.* and
System.Reflection.Metadata in a task assembly, which MSBuild supplies at run time. These are floors
for the oldest supported SDK:
| Package | Pinned | Where |
|---|---|---|
Microsoft.CodeAnalysis.Analyzers |
3.3.3 | Bit.BlazorUI.SourceGenerators, Bit.SourceGenerators |
Microsoft.CodeAnalysis.CSharp.Workspaces |
4.3.1 | Bit.BlazorUI.SourceGenerators, Bit.SourceGenerators |
Microsoft.CodeAnalysis.Workspaces.Common |
4.3.1 | Bit.CodeAnalyzers |
Microsoft.CodeAnalysis.CSharp |
4.8.0 | Bit.Brouter.Generators, Bit.Blex.Generators, Bit.BlazeDb.Generators + their test projects, and Bit.Blex.Demo.Server and Bit.BlazeDb.Demo.Server (run their generators for their MCP servers) (must match) |
Microsoft.CodeAnalysis.CSharp |
4.14.0 | Bit.CodeAnalyzers |
Microsoft.Build.Utilities.Core |
17.7.2 | Bit.Butil.Build, Bit.BlazorUI.Build |
System.Reflection.Metadata |
1.6.0 | Bit.BlazorUI.Build |
TFM-conditioned floors — only the highest band moves. A multi-targeted library pins the .0 of
each band it supports, because the pin is the consumer's minimum runtime. Raising 8.0.0 to 8.0.x
raises that minimum for no gain.
<PackageReference Include="Microsoft.JSInterop" Version="8.0.0" Condition="'$(TargetFramework)' == 'net8.0'" />Applies to Bit.Besql (JSInterop, Components.WebAssembly, EF Core Sqlite), Bit.BlazorUI /
.Assets / .Extras / .Legacy, Bit.Bswup and Bit.Butil (Components.Web), and the
BlazorEmpty template.
The rule keys on the .0 in a shipped library. A TFM-conditioned pin at a concrete patch —
Bit.BlazorUI.Tests on Components.WebView 8.0.x / 9.0.x / 10.0.x — is not a floor: nothing
consumes a test project, so every band takes the newest patch of its own band. Likewise a lone
10.0.0 sitting among 10.0.11 siblings in Directory.Packages.props is staleness, not intent.
Version ranges stay. [8.0.0,9.0.0) is a compatibility contract, not a pin.
Bit.* self-references track the in-development version, currently 10.7.0-pre-03. Never read
these off nuget.org — the published latest is behind the working tree by design, and unrelated
higher-numbered lines exist there.
TypeScript is held at 5.9.3 in the libraries. 7.x is the Go rewrite
(microsoft/typescript-go), not an incremental release. Boilerplate.Client.Core is the canary and
is already on 7.0.2 — if a bump is wanted, move one library project, build it, and only then the
rest. Do not move all twenty-one at once.
Vitest is held at ^3 in src/Utilities/Bmotion/Tests/bit-bmotion-js, src/Bswup/Tests/bit-bswup-js,
src/Utilities/Bsqlite/Tests/bit-bsqlite-js and src/Utilities/BevTools/Tests/bit-bevtools-js.
devops-actions/variable-substitution is held at v1.2 (ae7b1f36…). v2.0.0 committed 31
symlinks into its vendored node_modules; the runner cannot stage them and every job dies before
the action runs with Could not find file '.../helper-compilation-targets/node_modules/.bin/semver'.
v2.0.0 is the only v2 release and has been broken since 2026-05-23. Re-check with the tree API
before ever moving this pin again:
gh api "repos/<owner>/<repo>/git/trees/<sha>?recursive=1" --jq '[.tree[]|select(.mode=="120000")]|length'A non-zero count on a JavaScript action that vendors node_modules means it will not stage. This is
worth checking for any action bump, not just this one — it is invisible in the release notes.
A MAUI bump needs one MAUI build. MAUI's resizetizer bundles an Svg.Skia, and from 10.0.101 to
10.0.110 that Svg.Skia called a SkiaSharp overload missing from the SkiaSharp next to it, so any SVG
with a <filter> failed the build with MAUIR0001 /
MissingMethodException: SKImageFilter.CreateMatrixConvolution
(dotnet/maui#38319). The app icons of the Boilerplate
and the BlazorUI demo have one, so before moving MAUI, build a MAUI app with the Boilerplate's
appicon.svg as its icon, for any target.
Xamarin.Firebase.Messaging is held at 125.1.1. 125.1.2 pulls Xamarin.AndroidX.Fragment 1.9.0
(through GooglePlayServices.Base 118.10.1), which absorbed the ktx classes, while the rest of the
graph still brings Fragment.Ktx 1.8.9.x. R8 then fails the Android build with
Type androidx.fragment.app.FragmentKt is defined multiple times. Move it only once something else in
the graph already brings Fragment.Ktx 1.9.0 (an empty stub), rather than pinning Ktx in the template.
Xamarin.GooglePlayServices.Auth.Api.Phone is held at 118.3.1 for the same reason: 118.3.1.1 pulls
GooglePlayServices.Basement 118.10.0.3, which brings Fragment 1.9.0.
Linux runners are held at ubuntu-24.04, in the GitHub workflows and the Azure DevOps vmImage.
ubuntu-26.04 defaults to JDK 25 ((default) in the image readme's Java table), and .NET for Android
10 accepts up to 21 (LatestSupportedJavaVersion 21.0.99), so every Android build there fails. .NET 11
raises the limit to 25.0.99; move the runners together with the .NET 11 move.
When a held pin's rationale no longer holds — the oldest supported SDK moved, TS 7 was adopted repo-wide — say so in the report rather than acting on it.
Extract every pin, attribute order varies, so do not assume Include precedes Version:
grep -rhoE '<Package(Reference|Version)[^>]*/?>' --include=*.csproj --include=*.props src/ \
| grep 'Version="' \
| sed -E 's/.*Include="([^"]+)".*/\1|&/' | sed -E 's/\|.*Version="([^"]+)".*/|\1/' \
| sort -uA Condition="..." attribute sits between PackageVersion and Include on many
Directory.Packages.props lines — a naive Include="X" Version="Y" regex silently drops ~60
packages, including everything Aspire, FusionCache, and Agents.AI.
Then per package (https://api.nuget.org/v3-flatcontainer/<lowercased-id>/index.json returns
versions ascending):
- latest stable = last entry with no
- - target = latest stable, except a package whose newest release is a prerelease with no stable
above it, which takes the prerelease. That is the
OpenTelemetry.*packages on beta and rc,Microsoft.Agents.AI.Hosting,Microsoft.SemanticKernel.Connectors.HuggingFaceandAspire.Hosting.Keycloak.
Run the lookups with xargs -P 12; ~210 packages otherwise takes minutes.
Check for intra-repo drift too. The same package at different versions across projects is a bug even when neither is behind latest — grep for duplicate ids with differing versions and reconcile, unless the split is a TFM floor.
Only four distinct dev dependencies exist: typescript, sass, esbuild, vitest. Read latest
with npm view <pkg> version and npm view <pkg> dist-tags --json. After editing a package.json,
run npm install in that directory so package-lock.json matches — a lock file left behind is the
most common miss.
The lock files also carry transitive packages that no direct bump moves, and that is where the
repository's Dependabot alerts sit. Run npm audit fix (never --force) in every directory with a
lock file, and report what only a held major would fix.
A lock file has to come out of npm install on Linux unchanged. npm on Windows can write a "name"
into the lock's root package from the folder name when package.json has none, and Linux npm drops
it again. In the Boilerplate every generated project then ends its build with a modified
package-lock.json, and the CI test that wants a clean git tree after bit new fails. Run
npm install once in a Linux container to check, or keep the root package as it was.
Every step is SHA-pinned with a # vX.Y.Z comment. Resolve the tag to a commit and update both:
gh api "repos/<owner>/<repo>/releases?per_page=100" --jq '.[]|select(.prerelease==false)|.tag_name' \
| sed 's/^v//' | sort -V | tail -1
gh api repos/<owner>/<repo>/git/ref/tags/v<tag> --jq '.object.sha,.object.type'Never use releases/latest here. It returns whatever the repo flagged as latest, which is not
the newest release when an action maintains parallel major lines. Azure/webapps-deploy ships v2
and v3 side by side and flags v2.2.19 — a sweep that trusts the flag silently keeps a deploy step
one major behind. Enumerate and sort instead, as above.
If .object.type is tag it is annotated — dereference with
gh api repos/<owner>/<repo>/git/tags/<sha> --jq .object.sha to get the commit. Pinning the tag
object instead of the commit does not work.
Where the pin's comment is a moving major (# v2), rewrite it to the concrete release the SHA
belongs to (# v2.0.0), so the comment records a version rather than a branch.
Runner images (runs-on) are part of this: no ubuntu-latest, pin the concrete image.
Xcode follows the iOS workload. The iOS jobs pin a runner image and setup-xcode's xcode-version, and both
must give the Xcode that .NET for iOS asks for: the RecommendedXcodeVersion in Versions.plist of the
Microsoft.iOS.Sdk.net<tfm>_<x.y> pack whose version the newest workload set of the SDK's band names
(Microsoft.NET.Workloads.<band> on NuGet). A workload set can raise it without any SDK bump, and then every iOS
build fails with "This version of .NET for iOS requires Xcode ...". When the macOS-* image doesn't have that Xcode,
use GitHub's image that does (xcode-27 for Xcode 27).
Tasks are major-versioned (UseDotNet@2, FileTransform@2). The newest major is the highest
<Task>V<n> folder in gh api repos/microsoft/azure-pipelines-tasks/contents/Tasks --jq '.[].name'.
A task can change name across majors while keeping its id: NodeTool@1 resolves to UseNodeV1.
AzureRmWebAppDeployment@5 passes DeploymentTypeLinux: 'zipDeploy' on purpose. For webAppLinux @5
defaults to oneDeploy with CleanDeploymentFlag: true, which deletes every file the package does not
carry, the template's App_Data (SQLite database, local file storage, Hangfire jobs) included.
zipDeploy keeps them, as @4 did. Keep that input on any later major. The GitHub CD's
azure/webapps-deploy steps pass clean: false for the same reason: that action always uses OneDeploy.
Features pin a major only (ghcr.io/devcontainers/features/node:2). Read the current major from
repos/devcontainers/features/contents/src/<feature>/devcontainer-feature.json → .version. Before
bumping a major, check the feature's own option schema still accepts the options we pass, and skim
repos/devcontainers/features/commits?path=src/<feature>/devcontainer-feature.json for what the
major changed.
Feature options take a major too, never latest — "helm": "3", not "helm": "latest". The
install scripts resolve a partial version through find_version_from_git_tags.
The image follows the newest .NET SDK: curl -s https://builds.dotnet.microsoft.com/dotnet/release-metadata/10.0/releases.json → .latest-sdk. Both
devcontainers must agree.
There are no .config/dotnet-tools.json manifests any more: every tool is run with dnx <package>@<version>, so the pins live in the workflow and doc lines that call them (grep -rn "dnx .*@"). vpk must equal the Velopack PackageReference version in the same project — Velopack
requires the CLI and the library to match, and this has drifted before. dotnet-ef should equal the
EF Core package version. Likewise the npx playwright@<version> command in the Boilerplate's two
.runsettings (src/Tests and src/Internal/Boilerplate.Tests.E2E) must equal
Microsoft.Playwright.MSTest.v4: a remote Playwright server of any other version refuses the client, and
every test that runs on it fails at connect.
src/global.json is rollForward: disable and tracks the newest SDK. The two template
global.json files are 10.0.100 + latestFeature deliberately — they must accept any 10.0.x on a
consumer's machine. Do not "update" them.
In Boilerplate.Server.AppHost/Infrastructure/Extensions/IDistributedApplicationBuilderExtensions.cs.
Pin a major (pgvector/pgvector:pg18, mssql/server:2025-latest, redis:8), never latest. Where
an image publishes no major-only tag, pin the full newest tag and note it.
Check the image is still maintained, not just that the tag is newest of its line. redis/redis-stack
was pinned here until its upstream stopped publishing — Redis 8 folded the Stack modules into core, so
Stack was abandoned at 7.4.x while redis kept shipping. A stale-but-valid tag looks identical to a
current one; compare the registry's last_updated against the project's newest tag:
curl -s "https://hub.docker.com/v2/repositories/<repo>/tags?page_size=3&ordering=last_updated" \
| tr '{' '\n' | grep -oE '"name":"[^"]*"|"last_updated":"[^"]*"'Deliver this, not a diff dump.
1. Changed — one table, grouped by surface:
| Package | From | To | Notes |
|---|
2. Breaking or important — for every changed entry, fetch the release notes between the two versions and read them:
- GitHub-hosted:
gh api repos/<owner>/<repo>/releases --jq '.[]|select(...)|.body' - NuGet-only: the package's
releaseNotesfromhttps://api.nuget.org/v3/registration5-gz-semver2/<id>/index.json, else the project URL
Call out, with a one-line consequence and a link: any major bump; anything the notes label breaking, removed, or deprecated; behaviour changes in a deploy or build path; security fixes. A minor or patch whose notes are pure dependency bumps needs one line, not a summary. Say plainly when a change is routine.
3. Held back — what was left and which hold-back rule covers it. Flag any pin whose rationale has expired.
4. Missed / drifted — pins that are behind for no discoverable reason, and the same package at different versions across projects. These are the ones worth a maintainer's attention.
5. Not verified — anything only a real build or CI run can prove. Deploy-path action bumps
(azure/webapps-deploy), devcontainer feature majors, and analyzer bumps are in this category.
npm installin every directory whosepackage.jsonchanged.dotnet buildthe affected solutions; a Roslyn or MSBuild pin change needs a real build.- Never rewrite files through Python or
sed— BOM and CRLF get clobbered. Use targeted edits.
Finish every sweep with an issue, a PR and a full CI run. upstream is bitfoundation/bitplatform,
origin is the maintainer's fork.
-
Issue on upstream, label
dependencies, titleProject dependencies are outdated. The body lists what is behind, in a sentence or two per surface. -
Branch named after the issue number, cut from
upstream/develop, not from whatever branch is checked out. When the working tree sits on another branch, apply the diff in a worktree (git worktree add ../bitplatform-<n> -b <n> upstream/develop, thengit diff | git apply --3way) so that branch and any build running on it stay untouched. -
One commit,
feat(deps): update project dependencies #<n>. Push toorigin. -
PR from
<fork-owner>:<n>intoupstream/develop, titleUpdate project dependencies (#<n>). The body starts withcloses #<n>and carries the report: Changed, Breaking or important, Held back, Missed / drifted, Not verified. -
Run
all.ci.ymlandall.ci.utilities.ymlon the fork against the branch and give both URLs with the PR link:gh workflow run all.ci.yml --repo <fork-owner>/bitplatform --ref <n> gh workflow run all.ci.utilities.yml --repo <fork-owner>/bitplatform --ref <n> gh run list --repo <fork-owner>/bitplatform --workflow all.ci.yml --branch <n> --limit 1 --json url --jq '.[0].url' gh run list --repo <fork-owner>/bitplatform --workflow all.ci.utilities.yml --branch <n> --limit 1 --json url --jq '.[0].url'
A red job goes into the PR as a comment, with the failing step and its first error line.
-
Clean up at once, without asking:
git restorethe pushed files in the original checkout (aftergit diff <pushed-commit> -- <files>comes back empty), delete build artifacts the sweep created, andgit worktree removethe worktree.git statusmust match what it was before the sweep.