Repository navigation
feat: Native AOT tool packages and release pipeline (PR 3 of 3) - #193
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Classic Info-Zip unzip (shipped by ubuntu-latest) can only auto-create
one missing directory level under -d. artifacts/fallback/{cli,mcp}
needs two (fallback and cli/mcp), so both unzip calls in the
"Smoke test the fallback packages" step failed with "checkdir: cannot
create extraction directory", leaving all 28 smoke tests failing with
FileNotFoundException instead of passing. Create artifacts/fallback
first so unzip only has to create the one remaining level.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add pointer-package-ids.sh, which prints the IDs each pointer package lists in its DotnetToolSettings.xml. publish now fails before any push if a pointer lists a package that wasn't built, and it waits on NuGet.org for exactly those IDs instead of a hand-written list. Also harden the release workflows from the final review: - publish: read-only top-level permissions, write permissions only on the publish job, job timeouts, tag validation for pushed tags, and re-run guidance in the workflow_dispatch comment. - pack: fallback smoke TRX upload on failure, wider pull request paths, and a concurrency group keyed on the release tag. - set-server-json-version.sh prints its error when no version matches. - wait-for-nuget.sh caps each request at 30 seconds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ARCHITECTURE.md names the four places a RID lives, says publish derives its wait list from the pointer packages, and updates the CI line and the release flow (server.json stamps, NuGet.org validation wait). Both tool project comments point at the other RID lists. CONTRIBUTING.md warns that a local native pack ships a stale publish folder. The PR 3 plan's workflow, action, and script blocks now match the committed files, and it records deviation 14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
pack.yml uses actions/checkout with ref: ${{ inputs.ref }} even on PR-triggered runs where inputs.ref is unset, risking the workflow running against the wrong revision.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR (3/3) completes the Native AOT packaging/release work by turning ProjGraph.Cli and ProjGraph.Mcp into RID-specific Native AOT .NET tool packages (plus a framework-dependent any fallback), adding a single “pack → install → smoke” path across CI and releases, and wiring a safer publish order (push sub-packages first, wait for NuGet indexing, then push pointer packages).
Changes:
- Add smoke-suite guards for “native really is native” and capture MCP server stderr into failing test output.
- Switch CLI and MCP tool projects to
PublishAot=true+ToolPackageRuntimeIdentifiers=...and update integration tests/docs accordingly. - Introduce
pack.yml+ packaging scripts and restructurepublish.ymlto build/pack/push in a deterministic, release-safe order.
File summaries
| File | Description |
|---|---|
tests/ProjGraph.Tests.Smoke.Aot/NativeBuildGuardTests.cs |
New guard tests that fail if “native” points at a JIT apphost or .dll. |
tests/ProjGraph.Tests.Smoke.Aot/McpParityTests.cs |
Writes captured MCP stderr to xUnit output to aid diagnosis on failures. |
tests/ProjGraph.Tests.Smoke.Aot/Helpers/McpServerPair.cs |
Captures native/reference server stderr lines via MCP stdio transport options. |
tests/ProjGraph.Tests.Integration.Mcp/Helpers/McpServerProcess.cs |
Updates integration helper to launch the framework-dependent MCP apphost from bin/<cfg>/<tfm>/. |
src/ProjGraph.Mcp/README.md |
Documents SDK requirement and which platforms get native vs fallback builds. |
src/ProjGraph.Mcp/ProjGraph.Mcp.csproj |
Enables Native AOT tool packaging and disables symbols/publish symbols for pointer/tool packaging correctness/size. |
src/ProjGraph.Cli/README.md |
Documents SDK requirement and native vs fallback platform behavior. |
src/ProjGraph.Cli/ProjGraph.Cli.csproj |
Enables Native AOT tool packaging (RID list, symbols/symbol-copy behavior). |
src/ProjGraph.Cli/Program.cs |
Narrows IL3050 suppression to the CommandApp construction helper. |
docs/superpowers/plans/2026-09-14-native-aot-pr3-packaging-release.md |
Adds the implementation plan detailing the workflow/scripts and release flow. |
docfx/guides/getting-started.md |
Adds Native AOT platform notes and fallback behavior to getting started docs. |
CONTRIBUTING.md |
Updates smoke-test documentation to the package-based smoke script/action path. |
CLAUDE.md |
Updates tool packaging and release documentation and smoke test project description. |
ARCHITECTURE.md |
Documents new pack/publish workflow structure and tool packaging invariants/sync points. |
.github/workflows/publish.yml |
Restructures to prepare → pack → publish, pushes sub-packages first, waits for NuGet indexing, then pushes pointers, then releases/registry publish. |
.github/workflows/pack.yml |
New reusable workflow to build/smoke native RID packages and pack portable/pointer/fallback packages. |
.github/workflows/ci.yml |
Switches aot-smoke to use the composite action (pack → install → smoke). |
.github/scripts/wait-for-nuget.sh |
New script to poll NuGet flat container until expected versions are listed. |
.github/scripts/verify-packages.sh |
New script to assert exact package set presence (guard against missing/extra packages). |
.github/scripts/set-server-json-version.sh |
New script to stamp server.json version before packing/publishing. |
.github/scripts/pointer-package-ids.sh |
New script to derive the NuGet wait/verify list from pointer packages’ DotnetToolSettings.xml. |
.github/scripts/native-tool-smoke.sh |
New script implementing the unified per-RID pack/install/smoke flow. |
.github/actions/native-tool-smoke/action.yml |
New composite action to run the smoke script on runners (and Alpine container for musl). |
Review details
Suppressed comments (1)
.github/workflows/pack.yml:101
- Same issue as above: in pull_request runs
inputs.refis unset, so this checkout may not be pinned to the PR commit. Add a fallback (e.g.github.sha) so pack-portable runs against the correct code when the workflow is PR-triggered.
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
- Files reviewed: 22/23 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| - uses: actions/checkout@v7 | ||
| with: | ||
| ref: ${{ inputs.ref }} | ||
|
|
There was a problem hiding this comment.
Made explicit in 71d1496: both checkouts now use ref: ${{ inputs.ref || github.sha }}, with a comment saying which ref each trigger gets.
The previous behaviour was already correct. When ref is empty, actions/checkout uses its default, and for pull_request that's the same merge commit as github.sha. The first run of this PR confirms it: every pack-native leg ran ./.github/actions/native-tool-smoke, which exists only on this branch. The change only puts that intent in the file.
dotnet tool install on macOS arm64 downloads microsoft.netcore.app.host.osx-x64 for RID-specific packages (the SDK reads tools/any as a pre-net6 framework), so the smoke NuGet config now lists NuGet.org after the local packages. On Windows the tool path holds .cmd launchers, so the smoke suite runs the executables installed in the tool store. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Last of three PRs from the Native AOT design (
docs/superpowers/specs/2026-09-14-native-aot-design.md).ProjGraph.CliandProjGraph.Mcpnow ship as RID-specific Native AOT .NET tool packages, with a framework-dependentanyfallback. Every native package is smoke-tested on its own platform before a release pushes anything. Installation is unchanged for users (dotnet tool install -g ProjGraph.Cli,dnx ProjGraph.Mcp@x.y.z): the SDK picks the right package.PublishAot=trueandToolPackageRuntimeIdentifiers=win-x64;linux-x64;linux-arm64;linux-musl-x64;linux-musl-arm64;osx-arm64;any..github/scripts/native-tool-smoke.shbuilds, runsdotnet pack -r <rid>, installs both tools withdotnet tool installfrom the local packages, and runsTests.Smoke.Aotagainst the JIT build of the same commit..github/actions/native-tool-smokeruns it on the runner, or insidemcr.microsoft.com/dotnet/sdk:10.0-alpineforlinux-musl-*.aot-smokejob uses it for linux-x64. The job id is unchanged.pack.yml(new, reusable).pack-nativeruns one job per RID:windows-latest,ubuntu-latest,ubuntu-24.04-arm, andmacos-latest, with the musl legs in Alpine.pack-portablepacks the 6 libraries, the 2 pointer packages and the 2anypackages, then smoke-tests theanypackages.verify-packages.sh.publish.yml:prepare→pack→publish.publishchecks all 22 packages, then pushes everything except the pointers.DotnetToolSettings.xml, and the step fails before any push if a pointer lists a package that wasn't built.-p:Version;Directory.Build.propsis no longer edited.server.jsonversion is stamped before packing.NativeBuildGuardTestsfail if a "native" build is really a JIT apphost or a.dll.CreateApphelper.CLAUDE.md,CONTRIBUTING.mdandARCHITECTURE.mdcover the tool packages, every RID sync point, and the new release flow.The branch also carries the implementation plan (
docs/superpowers/plans/2026-09-14-native-aot-pr3-packaging-release.md).Results (local, linux-x64, SDK 10.0.401)
ProjGraph.Cli.linux-x64~14.7 MB,ProjGraph.Mcp.linux-x64~17.1 MB,ProjGraph.Cli.any~8.2 MB, pointer packages ~32 KB. Without excluding native symbols, the CLI package would be 32.3 MB.docker runcommand.pack-portablecommands passed 28/28 against theanypackages.dotnet tool installresolvesprojgraph.cli.linux-x64, and the command is a symlink to the native binary with the exec bit set.linux-musl-x64.dotnet formatis clean. 1,178 tests pass andTests.Smoke.Aotreports 30 skipped.ci.yml,pack.ymlandpublish.yml, and shellcheck is clean on all 5 scripts.pack-nativelegs passed the 30-test smoke suite on the installed packages (win-x64, linux-x64, linux-arm64, linux-musl-x64, linux-musl-arm64, osx-arm64), along withpack-portable(28/28),aot-smoke, andbuild×3.Deviations from the spec
linux-musl-x64andlinux-musl-arm64. Without them,dotnet tool installanddnxon Alpine resolve the glibclinux-x64package instead ofany, because the RID graph maps musl to glibc. The tool then fails withprojgraph: not found; I reproduced this insdk:10.0-alpine. The release grows from 18 to 22 packages.pack.ymlis a reusable workflow that is also PR-triggered on packaging paths. Otherwise the first Windows, macOS, Arm64 or musl native build would happen on a release tag.dotnet tool installinstead of extracting executables from the.nupkg. This tests the pointer's RID resolution and the exec bit. The PR 2 note about runningchmod +xon extracted binaries no longer applies. The smoke NuGet config lists the local packages plus NuGet.org, and on Windows the suite runs the.exefiles in the tool store (see below).aot-smokeuses the same pack → install → smoke path instead ofdotnet publish.CopyOutputSymbolsToPublishDirectory=falsewhenPublishAot=true). They made the CLI package 32.3 MB instead of 14.7 MB.IncludeSymbols=falseon both tool projects. A pointer package fails to pack a symbols package (NU5017). The six library.snupkgfiles still ship.pointersartifact, so push order is decided by directory, not by file name.pack-portablesmoke-tests theanypackages too, with the native-build guard filtered out.ProjGraph.Mcp.exe. The MCP SDK wraps stdio commands incmd.exe /c, which is fragile with a quoteddotnet.exehost path.Tests.Integration.Mcp'sMcpServerProcessnow launches the framework-dependent apphost frombin/<cfg>/net10.0/. Without that change, four MCP integration tests fail.runtimeconfig.json, becausePublishAotis set in the csproj. This affects the smoke reference anddotnet run. Theanypackages don't get them and are tested separately.server.jsonversion is stamped before packing, not only inpublish, viaset-server-json-version.sh. EveryProjGraph.Mcp*package embeds.mcp/server.json, so the spec's sed-in-publish would have shipped0.4.0inside all 8 MCP packages.pointer-package-ids.sh, andpublishfails before any push if a listed package wasn't built. A RID added only toToolPackageRuntimeIdentifierstherefore stops the release instead of shipping a pointer whose package for that RID doesn't exist.Found during execution
unzip -d artifacts/fallback/clican't create two missing folder levels, which would have failed every release's fallback smoke. The folder is now created first.server.jsonversion in the packaged MCPserver.json: deviation 13.ARCHITECTURE.mdand both csproj files.dotnet tool installalways downloadsmicrosoft.netcore.app.host.osx-x64for RID-specific packages, because the SDK readstools/anyas a pre-net6 framework. The smoke config had no NuGet.org source, so that download failed. NuGet.org is now listed after the local packages; no ProjGraph package with the CI version exists there.projgraph.cmdlaunchers, not.exefiles. The Windows smoke run now uses the executables in the tool store, which also makesNativeBuildGuardTestsmeaningful on Windows.dotnetcommands inside heredocs while I prototyped. Every file was written through the editor, and the tree is checked fordtk/rtk.Known follow-ups (not blocking)
server.json:set-server-json-version.shexits 0 ifserver.jsonis missing. That can't happen in CI today, and a one-line existence check fixes it.Maintainer checklist (after merge; these publish public packages or change settings)
NUGET_API_KEYglob covers the 14 newProjGraph.*package IDs. If it lists explicit IDs, the first native push fails.aot-smokerequired ondevelop(still pending from PR 2; the ruleset has no required status checks yet).v1.2.0-rc.1on the merge commit. Thepublishlog must show:holds the 20 expected packagesandholds the 2 expected packages;Every package the pointers list is present;is listed on NuGet.orgfor all 14 IDs;dotnet tool install -g ProjGraph.Cli --version <rc>, thenprojgraph --help.(printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"rc-check","version":"1.0"}}}'; sleep 10) | dnx ProjGraph.Mcp@<rc> --yesmust print aserverInfo.versionstarting with<rc>.ls -l ~/.dotnet/tools/projgraphmust link into…projgraph.cli.<rid>….docker run --rm mcr.microsoft.com/dotnet/sdk:10.0-alpine sh -c 'dotnet tool install -g ProjGraph.Cli --version <rc> && ~/.dotnet/tools/projgraph --help'.osx-x64or Windows on Armwin-arm64), the install must resolveprojgraph.cli.anyand run.curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.HandyS11/projgraph"must list the RC version.publishfailures: prefer "Re-run failed jobs", which reuses the run's packages. A new dispatch should run from the tag ref.Exit criteria (spec, PR 3)
pack-nativeis green on all six RIDs andpack-portableis green on this PR, together withbuild×3 andaot-smoke.dnxanddotnet tool installwork on each native RID and on a fallback platform.🤖 Generated with Claude Code