-
Notifications
You must be signed in to change notification settings - Fork 1
Publishing
Packaging is done by GitHub Actions, in .github/workflows/.
-
ci.yml— build and test on every push and PR to master. Builds with-warnaserror, so the repository's zero-warning bar is enforced there, and a new NuGet advisory fails the build. -
publish.yml— one workflow for all three packages, deliberately.
A NuGet Trusted Publishing policy is bound to a single workflow file and a single repository,
so one file means one policy covers every package, every future version, and any future Me.Toolkit.Maui.X
package. Do not split publishing into per-package workflow files — that would require a policy
per file.
| Trigger | Publishes |
|---|---|
tag v<version>
|
every package |
tag configuration-v<version>
|
Me.Toolkit.Maui.Configuration only |
tag hosting-v<version>
|
Me.Toolkit.Maui.Hosting only |
tag webhostpatch-v<version>
|
Me.Toolkit.Maui.WebHostPatch only |
| manual run | defaults to a dry run that packs and uploads the .nupkg files as artifacts without publishing |
The workflow verifies the tag matches each project's <Version>, runs the tests, and packs with
--no-build, so what is published is exactly what the tests ran against.
A tag push does not pass --skip-duplicate. That flag is for deliberately re-running a release
that is already out, and it cannot distinguish that from a refusal — the first attempt to publish
these packages was rejected three times with 409 The package ID is reserved and reported success,
because that is a 409 like any other. Only a manual run can ask for it.
It builds, ships in the demo, and has tests — but its reading session is unfinished and has never
been exercised against a physical tag. The project sets IsPackable=false, so a plain
dotnet pack on the solution cannot produce a Me.Toolkit.Maui.Nfc.nupkg by accident; publish.yml has no
nfc-v* trigger either. Remove the IsPackable line and add the trigger together when it is ready.
It is still built by the publish job, because the tests read every library's compiled assemblies
off disk through MetadataLoadContext. Skipping its build fails PublicApiSurfaceTests and
MultiTargetingTests, not just its own tests.
This repository was called Mauime, and the packages were to be Mauime.*. They cannot be:
409 The package ID is reserved. You can upload your package with a different
package ID. Reach out to support@nuget.org if you have questions.
An ID beginning with Maui is reserved. No package on nuget.org starts with those four letters —
200 search results and eight targeted probes found none — and every community MAUI package puts the
word second: CommunityToolkit.Maui, Plugin.Maui.Audio, Syncfusion.Maui.Toolkit. The clearest
case is Reactor.Maui, whose project is called MauiReactor.
Blazor and Xamarin were never reserved this way, which is why Blazorme.* and Xamarinme.*
publish without trouble. Reservations are granted per request, not automatically for every Microsoft
technology name, and MAUI's was taken when it shipped in 2022.
Only the published ID changes. Consumers install Me.Toolkit.Maui.Configuration and write
using Me.Toolkit.Maui.Configuration;, which is ordinary for a vendor- or toolkit-prefixed package.
Toolkit.* alone was tried next and refused the same way: "This package ID has been reserved.
Please request access to upload to this reserved namespace from the owner of the reserved prefix."
That Toolkit.Data and Toolkit.CodeBase already exist proves nothing — a reservation blocks only
new IDs, which is exactly why Xamarinme.* survives while Mauime.* does not.
Me.Toolkit.Maui.* was accepted, with 201 Created.
Reservations cannot be queried through any API. The only test is a push, so each candidate prefix costs a release run, and any ordinary English word is a gamble. Everything — repository, folders, projects, assemblies, namespaces, types and package IDs — carries the same name, so there is nothing to keep in step.
Me.* is not yet prefix-reserved. Requesting that reservation would give the verified badge and
stop anyone else taking Me.Something; it is worth doing now that something is published under it.
publish.yml uses runs-on: windows-latest, and unlike ci.yml this is not merely about coverage —
it decides what ends up inside the published package. Directory.Build.props builds
net10.0-ios and net10.0-maccatalyst only off Linux, and net10.0-windows10.0.19041.0 only on
Windows:
| Runner | Slices packed |
|---|---|
ubuntu-latest |
2 of 5 — net10.0, net10.0-android
|
macos-latest |
4 of 5 — no Windows |
windows-latest |
all 5 |
Packing on the wrong runner does not fail. It produces a valid package that silently supports two platforms instead of five, publishes it with a green tick, and nobody finds out until someone's iOS app fails to restore it. Since a published version cannot be replaced, that is a permanent mistake.
So the job ends with “Verify every package carries all five target frameworks”, which opens each
.nupkg and fails the run if any expected slice is missing. The framework names are matched by
prefix, because the platform versions are stamped in — net10.0-android36.0,
net10.0-windows10.0.19041. Treat that step as the real rule and the runs-on line as its
consequence.
The projects declare <Version>26.09.09</Version> and NuGet normalises that to 26.9.9. The
verification step compares against the raw csproj text, so the tag is:
v26.09.09
not v26.9.9. Tagging the normalised form fails the version check — which is the point: it is the
same check that catches tagging a version no project declares.
nuget.org discourages API keys for automated publishing. The job requests a GitHub OIDC token
(id-token: write) and NuGet/login@v1 exchanges it for an API key valid for one hour. No
publishing secret is stored in the repository — do not introduce NUGET_API_KEY.
The login step sits immediately before the push on purpose: the key expires, and each OIDC token buys exactly one key.
This has to exist before the first tag is pushed. Without it the run does everything correctly and fails at the final step. Created on nuget.org under Account → Trusted Publishing:
| Field | Value |
|---|---|
| Package Owner | melihercan |
| CI/CD Provider | GitHub Actions |
| Repository Owner | melihercan |
| Repository | Me.Toolkit.Maui |
| Workflow File |
publish.yml (file name only, no path) |
| Environment | (none) |
| Scopes | Push new packages and package versions |
| Glob | Me.Toolkit.Maui.* |
Renaming the GitHub repository breaks the policy. This repository was renamed from Mauime,
and the next publish failed at the login step with
Token exchange failed (HTTP 401)
No matching trust policy owned by user 'melihercan' was found.
The policy records the repository name, captured when it is created, and matches the OIDC token on that name. A rename leaves it pointing at a name that no longer exists. Nothing warns you: the policy still shows Active with a correct-looking glob and scope, and the failure appears only on the next publish.
Editing cannot fix it. The edit form has no Repository field — only Policy Name, Workflow File, Environment, Scopes and Glob. The repository is fixed at creation, so the policy must be deleted and recreated.
And recreating it with the old name silently reproduces the fault. GitHub redirects the old name
to the new one, so nuget.org resolves Mauime happily, stores Mauime, and fails to match a token
claiming Me.Toolkit.Maui — the summary line then reads Repository: Mauime #1361272941, with the
right repository ID beside the wrong name. That summary line is the only place the problem is
visible; check it after creating the policy.
The failure is at least harmless — the token exchange happens before anything is pushed, so a stale policy cannot publish something wrong, it just cannot publish at all.
Because Me.Toolkit.Maui.* are new IDs with no existing owner, the glob has to permit new packages, not
just new versions of existing ones — that is the "Push new packages and package versions" scope
above. A policy limited to new versions works for every release after the first and fails on the
first.
A newly pushed package is accepted with 201 Created and then goes through nuget.org's validation
pipeline before it becomes downloadable. Until it clears, the version is taken — a second push
gets 409 already exists — but it is absent from the flat container and its package page returns
404 to anyone but the owner.
26.9.9 took around three hours. Long enough that pending and failed are indistinguishable from
outside, and only the owner's account page shows which. Do
not conclude a push failed because the package is not there yet; check the push step's status line
first, and the account page second.
Run the workflow manually from the Actions tab and leave Dry run ticked. It packs, prints the
contents of every .nupkg, runs the five-framework check, and uploads the packages as build
artifacts — without ever requesting an OIDC token. That exercises everything except the policy
itself, and is worth doing once before the first real tag.
Date-based: 26.09.09, normalised by NuGet to 26.9.9, matching the convention used across these
repositories. Bump <Version> in each .csproj together with its <PackageReleaseNotes>.
AssemblyVersion and FileVersion are left to derive from Version rather than being set
separately, so they cannot drift.
Nothing here inherits a version history: these are new package IDs with no consumers, so there is no
lowest-version or downgrade problem to reason about. The Xamarinme.* packages stay on nuget.org
exactly as they are and are not deprecated — nothing referencing them will ever resolve a Me.Toolkit.Maui.*
package, because NuGet has no redirect between IDs.
DemoApp is a MAUI app, not a web app, so there is no equivalent of a GitHub Pages demo to publish.
It is deliberately excluded from both workflows: it is slow to build and nothing in it ships. Its
value is in being run on a device, which is where the 0.0.0.0 defect in NetworkAddress was found
— see the Modernization Log.