Summary
A custom MSBuild target that generates a file into wwwroot and is hooked at BeforeTargets="BeforeBuild" produces the file correctly on disk during dotnet publish, but the file's route is sometimes entirely missing from the generated {Project}.staticwebassets.endpoints.json, so MapStaticAssets() never registers an endpoint for it — every request 404s, even though the file exists in the published output at the correct size/content.
This isn't just "wrong extensibility point" (see #68108, which recommends BeforeTargets="AssignTargetPaths" for this exact scenario). In our case, moving the hook there (and to AssignTargetPaths;Publish), and separately splitting dotnet build + dotnet publish --no-build into two invocations, both still reproduced the bug. The only thing that reliably avoided it was generating the file in a fully separate process before invoking dotnet build/publish at all.
More surprisingly, the bug is highly environment-sensitive in a way that doesn't correlate with CPU/memory:
- Building and running the exact same commit locally (Docker Desktop on macOS, both native arm64 and via QEMU amd64 emulation, and via the classic non-BuildKit builder under
--memory/--cpu-quota cgroup limits from 8 GB down to ~768 MB / 1 CPU before the build gets OOM-killed outright) always produces a correct manifest.
- Deploying the same commit to Render.com (their documented free-tier "Starter" build machine: 2 CPU / 8 GB RAM) always produces a manifest missing the route. Confirmed via a temporary diagnostic endpoint that the file was present on disk at the correct size; only the manifest entry was missing.
- Running the exact same Dockerfile in a minimal repro repo on GitHub Actions
ubuntu-latest (4 CPU / 15 GB RAM, plain Docker Engine, no unusual sandboxing) also always reproduces it.
So it's not about being resource-starved — GitHub Actions' runner is more generously specced than several local configurations that worked fine. Whatever the actual trigger is, it's consistent within an environment and differs between environments in a way we couldn't pin down (our best untested guess: something about running inside a genuine Linux VM on a Linux hypervisor vs. Docker Desktop's macOS-hosted virtualization — but we didn't verify this).
Minimal repro
https://github.com/Popl7/essessay-tailwind-manifest-repro
Essessay/Essessay.csproj defines:
<Target Name="BuildTailwindCss" BeforeTargets="BeforeBuild" Inputs="@(TailwindInput)" Outputs="$(TailwindStamp);$(TailwindCss)">
<Exec Command=""$(TailwindExe)" --input Styles/app.css --output wwwroot/css/site.css $(TailwindMinify)" WorkingDirectory="$(MSBuildProjectDirectory)" />
<Touch Files="$(TailwindStamp)" AlwaysCreate="true" />
</Target>
generating wwwroot/css/site.css, referenced from a Razor view and served via app.MapStaticAssets() (Program.cs), .NET 10, Microsoft.NET.Sdk.Web.
Essessay/Dockerfile builds it with a single RUN dotnet publish Essessay/Essessay.csproj -c Release -o /app --no-restore.
.github/workflows/check-tailwind-manifest.yml builds the image with --no-cache on ubuntu-latest and greps the published Essessay.staticwebassets.endpoints.json for a css/site.css route, failing the job if it's missing.
Two runs, both failed the same way:
Relevant excerpt from the job log — the file exists, correctly sized, but only the unrelated Identity Razor Class Library's own css/site.css shows up:
--- disk ---
-rw-r--r-- 1 root root 38287 Aug 19 10:18 site.css
--- manifest routes for site.css ---
"Route":"Identity/css/site.css"
"Route":"Identity/css/site.css"
"Route":"Identity/css/site.css"
"Route":"Identity/css/site.css.br"
"Route":"Identity/css/site.css.gz"
FAIL: css/site.css route is MISSING from the manifest (bug reproduced)
You can re-run the workflow yourself via workflow_dispatch on that repo, or fork it — it's a full, working ASP.NET Core MVC app, --no-cache build, no other dependencies.
Expected
wwwroot/css/site.css, generated during BeforeBuild, gets a route in the published {Project}.staticwebassets.endpoints.json, deterministically, regardless of the machine/environment the build runs on.
Actual
The route is silently missing depending on the build environment (see above), despite the file itself being generated correctly every time.
What we tried
- Baseline (
BeforeTargets="BeforeBuild", single dotnet publish) — fails on GitHub Actions / Render, works locally.
- Split
dotnet build (with the target) + dotnet publish --no-build — still fails, even with the target's Exec running in a separate MSBuild invocation from the one that finalizes the publish manifest.
- Re-hook at
BeforeTargets="AssignTargetPaths;Publish" per How to add static assets for Blazor in a MSBuild Target #68108 (still one dotnet publish invocation) — still fails.
- Workaround that holds: run the target as its own, fully separate process before
dotnet build/publish starts at all (dotnet msbuild Essessay.csproj -t:BuildTailwindCss, then dotnet publish ... -p:SkipTailwindBuild=true to skip it the second time). This is the only approach that has served correctly across multiple Render deploys and would presumably also fix the GitHub Actions repro (untested there, since the repro repo intentionally keeps the original buggy Dockerfile).
Related, but not (as far as we could tell) duplicates
Happy to add more diagnostics to the repro repo if it helps narrow this down — e.g. dotnet --info, MSBuild binlog, or testing the AssignTargetPaths;Publish hook on GitHub Actions specifically.
Summary
A custom MSBuild target that generates a file into
wwwrootand is hooked atBeforeTargets="BeforeBuild"produces the file correctly on disk duringdotnet publish, but the file's route is sometimes entirely missing from the generated{Project}.staticwebassets.endpoints.json, soMapStaticAssets()never registers an endpoint for it — every request 404s, even though the file exists in the published output at the correct size/content.This isn't just "wrong extensibility point" (see #68108, which recommends
BeforeTargets="AssignTargetPaths"for this exact scenario). In our case, moving the hook there (and toAssignTargetPaths;Publish), and separately splittingdotnet build+dotnet publish --no-buildinto two invocations, both still reproduced the bug. The only thing that reliably avoided it was generating the file in a fully separate process before invokingdotnet build/publishat all.More surprisingly, the bug is highly environment-sensitive in a way that doesn't correlate with CPU/memory:
--memory/--cpu-quotacgroup limits from 8 GB down to ~768 MB / 1 CPU before the build gets OOM-killed outright) always produces a correct manifest.ubuntu-latest(4 CPU / 15 GB RAM, plain Docker Engine, no unusual sandboxing) also always reproduces it.So it's not about being resource-starved — GitHub Actions' runner is more generously specced than several local configurations that worked fine. Whatever the actual trigger is, it's consistent within an environment and differs between environments in a way we couldn't pin down (our best untested guess: something about running inside a genuine Linux VM on a Linux hypervisor vs. Docker Desktop's macOS-hosted virtualization — but we didn't verify this).
Minimal repro
https://github.com/Popl7/essessay-tailwind-manifest-repro
Essessay/Essessay.csprojdefines:wwwroot/css/site.css, referenced from a Razor view and served viaapp.MapStaticAssets()(Program.cs), .NET 10,Microsoft.NET.Sdk.Web.Essessay/Dockerfilebuilds it with a singleRUN dotnet publish Essessay/Essessay.csproj -c Release -o /app --no-restore..github/workflows/check-tailwind-manifest.ymlbuilds the image with--no-cacheonubuntu-latestand greps the publishedEssessay.staticwebassets.endpoints.jsonfor acss/site.cssroute, failing the job if it's missing.Two runs, both failed the same way:
Relevant excerpt from the job log — the file exists, correctly sized, but only the unrelated Identity Razor Class Library's own
css/site.cssshows up:You can re-run the workflow yourself via
workflow_dispatchon that repo, or fork it — it's a full, working ASP.NET Core MVC app,--no-cachebuild, no other dependencies.Expected
wwwroot/css/site.css, generated duringBeforeBuild, gets a route in the published{Project}.staticwebassets.endpoints.json, deterministically, regardless of the machine/environment the build runs on.Actual
The route is silently missing depending on the build environment (see above), despite the file itself being generated correctly every time.
What we tried
BeforeTargets="BeforeBuild", singledotnet publish) — fails on GitHub Actions / Render, works locally.dotnet build(with the target) +dotnet publish --no-build— still fails, even with the target'sExecrunning in a separate MSBuild invocation from the one that finalizes the publish manifest.BeforeTargets="AssignTargetPaths;Publish"per How to add static assets for Blazor in a MSBuild Target #68108 (still onedotnet publishinvocation) — still fails.dotnet build/publishstarts at all (dotnet msbuild Essessay.csproj -t:BuildTailwindCss, thendotnet publish ... -p:SkipTailwindBuild=trueto skip it the second time). This is the only approach that has served correctly across multiple Render deploys and would presumably also fix the GitHub Actions repro (untested there, since the repro repo intentionally keeps the original buggy Dockerfile).Related, but not (as far as we could tell) duplicates
AssignTargetPathsas the hook point; insufficient in our case.wwwrootcontent, but in different scenarios (files removed rather than added, and a hard failure rather than a silently incomplete manifest).Happy to add more diagnostics to the repro repo if it helps narrow this down — e.g.
dotnet --info, MSBuild binlog, or testing theAssignTargetPaths;Publishhook on GitHub Actions specifically.