Skip to content

fix: pack content at clean paths on every platform, and guard it in CI - #57

Merged
xavierjohn merged 2 commits into
mainfrom
fix/packagepath-forward-slashes
Aug 19, 2026
Merged

fix: pack content at clean paths on every platform, and guard it in CI#57
xavierjohn merged 2 commits into
mainfrom
fix/packagepath-forward-slashes

Conversation

@xavierjohn

Copy link
Copy Markdown
Owner

Companion to xavierjohn/Trellis#713, which fixes the same defect in the framework.

PackagePath separators are platform-dependent in a way that hides the defect on Windows. A trailing backslash in PackagePath="trellis\" is recognized as a directory marker on Windows, so the doc packs to trellis/<name>.md. On Linux the backslash is not a separator: it normalizes to trellis/ and NuGet then appends its own, so the doc packs to the malformed trellis//<name>.md.

That malformed path still satisfies the trellis/*.md glob the copy logic uses, so documentation is still delivered and every functional test stays green. Nothing asserted the exact entry path, so the defect reached published packages — Trellis.Microservices.Abstractions 0.1.0-alpha.72 on nuget.org carries trellis// on both of its references.

The fix

Converts every PackagePath to forward slashes, including the root markers PackagePath="\" and PackagePath="\README.md". Those forms are not actually broken today — verified from published bytes that README.md and icon.png land at the package root — but a single rule ("no backslashes in PackagePath") is easier to hold than "no trailing backslash, except the root one", and it matches the sibling Trellis.ResourceNaming repo.

The guard

New build/test-package-layout.ps1, with two checks because neither alone is sufficient:

  1. Declarations — no PackagePath contains a backslash. Platform-independent, so it holds the line on a developer's Windows machine where check 2 cannot fail.
  2. Packed entries — every entry is a clean relative path, and every reference sits at trellis/<name>.md. The real behaviour, but it can only fail on Linux.

The declaration check parses the XML rather than scanning text. A line-based regex missed two of three planted violations — PackagePath='trellis\' and the <PackagePath>trellis\</PackagePath> metadata element — and would also have flagged the comments that quote the malformed value on purpose. XDocument handles all of these and supplies line numbers via IXmlLineInfo.

Not every package ships a reference, so an empty set for one package is legitimate and is not an error. The vacuous case — nothing anywhere ships a reference, leaving every per-package assertion trivially true — is caught once after the loop.

CI placement

The "Verify package layout" step is deliberately unconditional, even though the preceding Pack step is gated on !github.base_ref:

  • On pull requests, Pack is skipped, so the packed check reports SKIP while the declaration check still runs.
  • On pushes to main, artifacts/ exists and both checks run against the real Linux-packed bytes — the only place trellis// can actually appear.

Validation

Scenario Result
Against the defective published packages exit 1, correctly lists the trellis// entries
Against newly packed fixed packages exit 0
With artifacts/ absent exit 0, reports SKIP
Planted single-quoted / metadata-element violations caught at correct line numbers
Prose inside <!-- --> quoting the bad pattern correctly ignored

Packed output re-verified: README.md and icon.png still at package root; build/, buildTransitive/, trellis/ all correct. 352/352 tests pass.

Note

This changes packed output, so the fix reaches nuget.org only on the next publish. Already-published packages keep trellis//, which is harmless since delivery works either way.

PackagePath separators are platform-dependent in a way that hides the
defect on Windows. A trailing backslash in PackagePath="trellis\" is
recognized as a directory marker on Windows, so the doc packs to
trellis/<name>.md. On Linux the backslash is not a separator: it
normalizes to "trellis/" and NuGet then appends its own, so the doc packs
to the malformed "trellis//<name>.md".

That malformed path still satisfies the trellis/*.md glob the copy logic
uses, so documentation is still delivered and every functional test stays
green. Nothing asserted the exact entry path, so the defect reached
published packages: Trellis.Microservices.Abstractions 0.1.0-alpha.72 on
nuget.org carries trellis// on both of its references.

Converts every PackagePath in the repository to forward slashes. This
also covers the root markers PackagePath="\" and PackagePath="\README.md"
used for icon.png and the NuGet readme. Those forms are not actually
broken today, but a single rule - no backslashes in PackagePath - is
easier to hold than "no trailing backslash, except the root one", and it
matches the sibling Trellis.ResourceNaming repository.

Adds build/test-package-layout.ps1 with two checks, because neither alone
is sufficient:

  1. Declarations - no PackagePath contains a backslash. This is
     platform-independent, so it holds the line on a developer's Windows
     machine where check 2 cannot fail.
  2. Packed entries - every entry is a clean relative path and every
     reference sits at trellis/<name>.md. This is the real behaviour, but
     it can only fail on Linux.

The declaration check parses the XML rather than scanning text. A
line-based regex missed two of three planted violations - the
single-quoted attribute form and the <PackagePath> metadata element - and
would also have flagged the comments that quote the malformed value on
purpose to explain the defect. XDocument handles all of these and
supplies line numbers through IXmlLineInfo.

Not every package ships a reference, so an empty set for one package is
legitimate and is not an error. The vacuous case, where nothing anywhere
ships a reference and every per-package assertion is trivially true, is
caught once after the loop.

The CI step is deliberately unconditional even though the preceding Pack
step is gated on `!github.base_ref`. On pull requests Pack is skipped, so
the packed check reports SKIP while the declaration check still runs; on
pushes to main both run against the real Linux-packed bytes.

Verified: the script exits 1 against the defective published packages and
lists their trellis// entries, exits 0 against newly packed fixed
packages, and exits 0 with SKIP when artifacts/ is absent. Packed output
still places README and icon at the package root. 352/352 tests pass.
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Test Results

352 tests  ±0   352 ✅ ±0   1m 2s ⏱️ +18s
  4 suites ±0     0 💤 ±0 
  4 files   ±0     0 ❌ ±0 

Results for commit 508a4a2. ± Comparison against base commit d0aa164.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a cross-platform NuGet packing defect where backslashes in PackagePath can produce malformed trellis//<name>.md entries on Linux, and adds a CI guard to prevent regressions across platforms.

Changes:

  • Normalize all PackagePath values to forward slashes in project/props/targets files to ensure consistent packed paths across Windows and Linux.
  • Add build/test-package-layout.ps1 to validate (1) PackagePath declarations and (2) the actual packed .nupkg entry paths/placement.
  • Run the package-layout verification step in CI unconditionally (declaration check always; packed-bytes check runs when artifacts/ exists).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Trellis.Yarp/src/Trellis.Yarp.csproj Switch NuGet README PackagePath from \ to / for platform-stable root packing.
Trellis.Microservices.AspNetCore/src/Trellis.Microservices.AspNetCore.csproj Switch NuGet README PackagePath from \ to / for platform-stable root packing.
Trellis.Microservices.Abstractions/src/Trellis.Microservices.Abstractions.csproj Normalize README PackagePath to forward slashes.
Directory.Build.targets Normalize packed doc/targets PackagePath values to forward slashes; document the Linux trellis// failure mode.
Directory.Build.props Normalize packed icon PackagePath to /.
build/test-package-layout.ps1 New script validating PackagePath declarations and packed entry layout.
.github/workflows/build.yml Add unconditional “Verify package layout” step to enforce the guard in CI.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread build/test-package-layout.ps1 Outdated
@@ -0,0 +1,131 @@
#!/usr/bin/env pwsh
The file is UTF-8 with BOM per repository convention, so the kernel's
binfmt_script handler never sees '#!' as the first two bytes and the
shebang cannot work on Linux or macOS. It implied the script was directly
executable when it is not.

CI invokes it as 'pwsh build/test-package-layout.ps1', so nothing
depended on it. Matches lint-api-reference.ps1 in this repository, which
is BOM-encoded with no shebang. The comment-based help immediately below
still documents the script.

Raised in review on PR #57.
@xavierjohn

Copy link
Copy Markdown
Owner Author

Good catch, and confirmed — Linux's binfmt_script handler requires #! as the literal first two bytes, so a UTF-8 BOM makes the shebang unreachable. Fixed in 508a4a2 by dropping the shebang rather than the BOM, since .github/copilot-instructions.md mandates UTF-8 with BOM for all repository files. That also matches docs/lint-api-reference.ps1 in this repo, which is BOM-encoded with no shebang. The comment-based help immediately below still documents the script, and CI invokes it as pwsh build/test-package-layout.ps1, so nothing depended on it.

Auditing every .ps1 across the three related repos found the same combination elsewhere:

File BOM Shebang
Trellis.Microservices build/test-package-layout.ps1 yes yes -> fixed here
Trellis build/test-apireference-payload.ps1 yes yes -> fixed in xavierjohn/Trellis#713
Trellis docs/audit-doc-freshness.ps1 yes yes -> pre-existing, left alone
Trellis.ResourceNaming build/test-apireference-packaging.ps1 no yes -> already valid

The framework one was in scope because that PR already edits the file. audit-doc-freshness.ps1 is untouched by either PR, so I have left it as a separate cleanup rather than widening an unrelated change.

@xavierjohn
xavierjohn merged commit e768fad into main Aug 19, 2026
2 checks passed
@xavierjohn
xavierjohn deleted the fix/packagepath-forward-slashes branch August 19, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants