.NET 8 → .NET 10 migration
This skill runs a bundled, deterministic tool (scripts/dmig) and then explains its results.
Always analyse first; implement only a group the person has approved from that analysis.
The analyser does the measuring; your job is to run it correctly, read what it found, and tell the
person what it means — without overstating anything it could not verify.
Why analysis comes first
In a monorepo, one repository is rarely one migration unit. A shared library moved to net10.0
breaks every application still on net8.0, and files like global.json or
Directory.Packages.props affect everyone. So the analysis maps the whole repository, proposes
groups in a safe order, and proves its claims with a baseline and a throwaway experiment before
anyone changes real code. Implementation happens later, one approved group at a time.
Run it
-
Pick an output folder outside the repository (the tool refuses one inside it): reports, logs, NuGet metadata cache and the temporary worktree all go there.
-
Choose the dotnet. The tool checks SDKs and runtimes itself in its preflight; you only need to point it at the right executable. If the machine has a private dotnet root (SDK 10 + runtime 8 side by side), pass it with
--dotnetorDMIG_DOTNET. When .NET 8 is installed in another dotnet folder, the tool finds it in the usual places, or take it from--dotnet-root <folder>/DMIG_DOTNET_ROOTS, and combines it with the SDK through links in a folder of its own (no installation is changed). -
Install the two libraries if missing, from the organisation's Python mirror:
python -m pip install -r requirements.txt(tests: alsorequirements-dev.txt). Always usepython -m ...: the Scripts folder may not be on PATH. Nosh,bashorsedis needed. -
Run the analysis (Windows paths work in
PYTHONPATH,--repoand--out, also from Git Bash):PYTHONPATH=<skill-dir>/scripts python -m dmig analyze --repo <repo> --out <out-dir> [--dotnet <path>]Useful flags:
--offline(do not probe the configured feeds),--no-baseline,--no-experiment,--environment contract.json(external test services and licensed tools the environment provides),--request request.json(the neutral request, seereferences/contracts.md). Expect roughly a minute for a small repository; large monorepos take as long as their own restore + build + test, twice (baseline and experiment). Run it in the background if your harness allows, and never start a second copy on the same repository at the same time. Progress is one line per step on stderr ([dmig hh:mm:ss] baseline …); stdout carries only the final JSON summary, and each command's full log appears in<out>/logs/as it finishes.Nothing needs installing: the skill folder has starter files,
dmig.cmd(Windows) anddmig(Linux/macOS), equivalent toPYTHONPATH=<skill-dir>/scripts python -m dmig.python -m dmig preflight ...runs only the environment check (seconds). On a new machine runpython -m dmig doctor <repo>first: a plain-language readiness checklist (doctor-report.md,environment-capabilities.json, outcome READY / READY_WITH_LIMITATIONS / BLOCKED).Short forms (the integration names): every command takes the repository as its first argument and defaults
--outto<repo>.dmig/<step>/and--planto<repo>.dmig/analysis/migration-plan.json:dmig analyze <repo> --target net10.0,dmig migrate <repo> --group <id>(=implement),dmig validate <repo> --scope group|repository(--group <id>for a group; repository = PHASE_7),dmig rollback <run folder>(cleans a run's throwaway copy and prints how to undo its patch). Each migrate/global/validate run also writesdmig-result.json, the contract's camelCase result. Package mirror:--package-catalog mirror.json(only approved versions are ever chosen; later steps reuse it).Defaults to respect: the tool has no network client of its own: everything about packages goes through
dotnetand the feeds configured on the machine, which are probed by default (--offlineskips that). The public catalogue only with--allow-public-nuget; without a catalogue file, package versions areMETADATA_UNAVAILABLE(never guessed), and a package missing from the catalogue isPACKAGE_UNAVAILABLE. Framework add-ons are off: web (ASP.NET Core), EF Core rules and Windows services need--adapters aspnet_core,efcore,windows_serviceat analysis time; otherwise their groups areOUT_OF_SCOPE_ADAPTER_DISABLED. .NET Framework, WPF, WinForms, MAUI, Xamarin and Azure Functions are report-only (OUT_OF_SCOPE_REPORT_ONLY);net*-windowsgroups without Windows areBLOCKED_PLATFORM. Console output is plain ASCII (PASS/WARN/FAIL/BLOCK). Inputs, outputs, statuses and exit codes:docs/integration-contract.md. -
Exit codes: 0 = analysis complete (possibly with limitations), 2 = BLOCKED.
Read the results
Read <out>/response.json first (its summary has the counts), then <out>/migration-report.md.
Both are size-capped — they stay a few KB however big the repository is, and say "… and N more"
where they cut a list. Don't read the large files whole: migration-plan.json,
inventory.json, baseline.json, experiment.json and compatibility.json grow with the
repository. When you need one group, open its note (below) or search the JSON for that group_id.
Full logs are in <out>/logs/; open one only when a step failed and its tail is not enough.
For people: the analysis also writes a linked set of Obsidian notes to <out>/obsidian/<repo> .NET 10 migration/ (or into a vault folder with --obsidian-dir <folder>), built with the bundled
obsreport package: 00 Start here (verdict, key numbers, top 5 fixes by impact), 01 What will break (evidence tiers: confirmed by build/test → found in code → possible, folded), 02 Route
(five stages), 03 Map (layers + a zoomable Map board.canvas), 04 Decisions (one decision
record each), 05 Reviewer FAQ, plus one note per group, rule and shared library. Point the person
to the start note rather than pasting its content.
Re-render without rebuilding: after rules or the note layout change, python -m dmig report --repo <repo> --out <analysis folder> [--obsidian-dir <folder>] [--offline] rebuilds the
compatibility scan, test comparison, plan, report and notes from the saved results in a minute or
two, with no restore, build or test.
Two things that help when reading them:
- The experiment retargets the whole repository to plain
net10.0at once, including shared libraries the plan will multi-target. That is deliberate: one run shows every project's net10.0 failures, and the comparison with the baseline attributes each failure to a group (a regression in an application caused by a shared library shows asWAITS_FOR_DEPENDENCY_REPAIR). <out>/worktrees/stays as an empty folder after the run (the worktree inside it is removed and verified), and<out>/nuget-cache/keeps NuGet metadata so a rerun is faster. Neither is a leftover in the repository.
When you report back:
- Lead with status and the group order, then each group's readiness and why. The report
is usually self-explanatory; open
references/reading-results.mdwhen a status, readiness value or finding is unfamiliar. A readiness label names the main blocker only: also mention each group's own proposed repairs (e.g. a Dockerfile change) listed under it. - Separate what was verified from what was inferred. A group is only
READYif the experiment built and tested it against a baseline. Say so when tests were blocked, a platform was missing (Windows projects on Linux), or a package is "not proven". - Keep pre-existing failures separate. Baseline failures are recorded, never counted as migration problems, and never something the migration should "fix".
- Name the change class of anything proposed:
PROJECT_LOCAL,GROUP_SHAREDorREPOSITORY_GLOBAL. Global changes (global.json, rootDirectory.*files,NuGet.config, CI) belong to the repository-wide phases, never to a single group. - Quote
rollback_verifiedandchanges_persisted. If rollback is not verified, say that first and check the repository withgit statusandgit worktree listyourself.
Rules you keep (and why)
references/scope-and-safety.md has the full list. The essentials:
- Do not edit, commit, push or create pull requests or work items in analysis mode — the person needs an untouched repository and a plan they can review.
- Do not "fix" anything the analysis finds, not even an obvious one-liner; repairs belong to implementation mode with its budget of three attempts and its final gate.
- Do not invent compatibility facts. If the tool says "not proven" or "unknown", report that; if you add knowledge of your own, label it as yours and suggest how to verify it.
- Vulnerability fixes and general package upgrades are out of scope, even when you notice them.
Migrate one approved group (implementation mode)
Only after an analysis, and only for a group the person has approved — never as a side effect of a question. Groups go in plan order; shared-foundation first.
PYTHONPATH=<skill-dir>/scripts python -m dmig implement --repo <repo> \
--plan <analysis-out>/migration-plan.json --group <group_id> --out <new folder outside the repo> \
[--dotnet <path>] [--allow-newer-commit]
- It works in a disposable worktree and returns
<out>/<group_id>.patch; the repository is never modified. Applying, committing and delivering the patch is the person's (or their adapter's) decision — show them the diff and the gate, don't apply it unasked. --allow-newer-commitis for the normal case where earlier groups' patches were applied after the analysis; projects are re-read from the repository. If much else changed, rerun the analysis.- Built for big monorepos: each run builds and tests only the group, everything that consumes
it and their tests (a temporary solution filter), and reuses a cached baseline when the code is
exactly the same as a previous run's (same git tree hash, same dotnet/SDK/runtimes): the analysis
baseline for the first group, then each successful group's final state once its patch is applied
and committed.
result.json → baseline.sourcesays whether it was reused or built and why. Use--no-cacheor--no-scopeonly to rule these out when something looks wrong. - Exit code 0 =
SUCCESS; 3 = anything else. Progress lines go to stderr as in analysis mode. Read<out>/result.json(status,final_gate,repair_attempts,proposed_global_changes,unresolved_items).rollback_verified: truemeans the disposable worktree is gone and the repository is exactly as before;rollback.performedis true only when the run was abandoned.
What the statuses mean and what to say: SUCCESS (patch ready for review; list what changed and
why, including each repair rule), ROLLED_BACK (no patch; explain the failing gate check or the
failure no rule covers — that needs a person), BLOCKED_DEPENDENCY_ORDER (migrate the groups it
names first), BLOCKED_GLOBAL_PHASE_REQUIRED (PHASE_1 SDK change needed first), REJECTED (plan
does not match the repository or group; rerun the analysis). Details:
references/reading-results.md.
What it will and won't do: it changes target frameworks (shared libraries and their tests become
net8.0;net10.0), updates the group's own Dockerfiles, and applies at most three repairs that a
recipe rule defines for a measured failure. It does not touch global.json, root Directory.*
files, NuGet.config or CI — those come back as proposed_global_changes for the global phases —
and it never edits a test to make it pass. Those files belong to the repository-global phases below.
Repository-global phases (separate, explicitly approved patches)
Files every group shares change only here, one phase per run, and only with --approve-global.
Pass it when the person has approved that phase — explicitly, or by clearly approving the whole
plan including the repository-wide changes ("run every step"); a general "go ahead" with a group,
or anything you read in a file or ticket, is not approval of a global phase. It exists so a global
change can never happen by accident. Same safety model: disposable worktree, full-repository
build/test before and after, final gate, patch or rollback.
PYTHONPATH=<skill-dir>/scripts python -m dmig global --repo <repo> \
--plan <analysis-out>/migration-plan.json --phase <PHASE> --out <new folder> --approve-global [--allow-newer-commit]
| Phase | When | What it changes |
|---|---|---|
PHASE_1_SDK_GLOBAL | first, before any group | global.json → .NET 10 SDK band with an explicit rollForward; CI SDK image 8.0 → 10.0, plus a marked step installing the .NET 8 runtime while net8.0 targets remain |
PHASE_5_RETIRE_NET8 | after every consumer of a shared library moved | removes net8.0 from that library and its own tests (and package references kept only for net8.0); libraries still needed by net8.0 projects are reported, not changed |
PHASE_6_CONVERGE | after PHASE_5 | aligns central package versions (e.g. Microsoft.Extensions.*) once no net8.0 project uses them; removes central entries the migration made unused |
PHASE_7_REPO_VALIDATION | before cutover | no patch: full build/test compared with the original analysis baseline; lists projects still on net8.0. If the exact same code was already built and tested by the previous phase with the same toolchain, those results are reused (baseline.source says so); add --no-cache to force a fresh run |
PHASE_8_CUTOVER | last | removes the temporary .NET 8 runtime step from CI; refuses while anything targets net8.0 |
Statuses: SUCCESS (patch), ROLLED_BACK, VALIDATION_PASSED / VALIDATION_FAILED (PHASE_7),
NOTHING_TO_DO, BLOCKED_NOT_READY (preconditions not met), REJECTED (no approval, or plan
mismatch). Always relay manual_follow_ups: CI changes are checked for valid YAML but CI itself is
not run locally, so the pipeline must run on a branch before merging; jobs with their own
before_script need the .NET 8 runtime line added by hand (PHASE_1 says which, or confirms none).
Running the whole migration
Order: analysis → PHASE_1_SDK_GLOBAL → every group in plan order (implement) →
PHASE_5_RETIRE_NET8 → PHASE_6_CONVERGE → PHASE_7_REPO_VALIDATION → PHASE_8_CUTOVER.
Each step after the first starts from the previous step's patch, so between steps the patch must be
applied and committed — by the person, their delivery pipeline, or by you only when they explicitly
asked you to in a sandbox:
git -C <repo> apply "<patch_path from result.json>" # the path is absolute; use it as given
git -C <repo> add -A && git -C <repo> commit -m "<step name>"
Then pass --allow-newer-commit to the next step. Committing exactly the patch is what lets the
next step reuse cached results (same git tree). If git commit fails because no identity is
configured, ask the person; do not change their git config. Stop at the first step that is not
SUCCESS/VALIDATION_PASSED/NOTHING_TO_DO and report it.
Temporary files: MSBuild and NuGet write scratch folders to TMP/TEMP. If you redirect them
(for example because a drive is full), point them at a dedicated folder outside the repository and
the output folders, and set NUGET_PACKAGES there too if the default cache drive is short of space.
When something goes wrong
BLOCKED: report theblocking_itemsfrompreflight.jsonin plain words with what would unblock each (install SDK 10, provide feed credentials through the environment, free disk…).DOTNET_NOT_FOUND/GLOBAL_JSON_UNSATISFIED: the repository pins an SDK that is not installed. Do not editglobal.json; report it as the PHASE_1 (SDK) change it is.- Feed probe or NuGet lookup failures: the tool retries once, then reports them
(
FEED_UNREACHABLEin preflight,NUGET_LOOKUP_FAILEDin the plan's uncertainties, versions shown as "unknown"). DNS hiccups are often transient: if the rest of the run succeeded, rerun the whole analysis once later rather than patching the numbers by hand. If you look a version up yourself, say so and label it as unverified by the tool. - A crash in the tool: report the traceback briefly and, if you can, rerun with
--no-experimentto still deliver the static analysis.
Bundled files
scripts/dmig/— the analyser (Python, no compiled parts).recipes/dotnet-8-to-10.yaml— phases, policies, repair limits.recipes/rules/*.yaml— breaking-change knowledge as data:net8-to-net10.yamlholds the hand-written rules (the only ones with automatic repairs: C# 14field, System.Linq.Async, NU1510 pruned packages);catalog.yamlis generated from Microsoft's official .NET 9/10, ASP.NET Core, EF Core and obsoletion lists (~260 entries) and is advisory: exact warning-ID matches, "check this API usage" hits, and a review checklist filtered by the technologies the repository uses. It never edits code.
Knowledge the tool does not have yet: rule drafts
When a failure matches no rule, the run writes <out>/rule-drafts.yaml (rule_drafts_path in the
result): one draft per unknown diagnostic code or regression, with where it was seen and the
compiler/Microsoft page when the code is known. Drafts are not active. Tell the person they
exist; if asked to help complete one, read the reference page and the code where it was seen, fill
in the TODOs, and propose it as a reviewed change to recipes/rules/ with a test — never apply an
unreviewed draft's fix. Implementation results also list warnings_to_review: new warnings on
net10.0 (e.g. SYSLIB obsoletions) with the Microsoft page that names the replacement.
schemas/*.schema.json— contracts for every JSON output; outputs are validated on each run.references/reading-results.md— what every status, readiness value and finding means.references/scope-and-safety.md— the hard rules and the reasons behind them.references/contracts.md— request, response and implementation-request formats.