Custom BeforeBuild target's generated wwwroot file missing from staticwebassets.endpoints.json depending on build environment
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 281
Description
### 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:
```xml
```
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:
- https://github.com/Popl7/essessay-tailwind-manifest-repro/actions/runs/32241988885
- https://github.com/Popl7/essessay-tailwind-manifest-repro/actions/runs/32241993765
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
1. **Baseline** (`BeforeTargets="BeforeBuild"`, single `dotnet publish`) — fails on GitHub Actions / Render, works locally.
2. **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.
3. **Re-hook at `BeforeTargets="AssignTargetPaths;Publish"`** per #68108 (still one `dotnet publish` invocation) — still fails.
4. **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
- #68108 — recommends `AssignTargetPaths` as the hook point; insufficient in our case.
- #56755, #46156 — both about the manifest going stale/missing relative to actual `wwwroot` content, 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 the `AssignTargetPaths;Publish` hook on GitHub Actions specifically.
Contributor guide
Research direction
Start with the minimal repro's Essessay/Essessay.csproj, Dockerfile, and .github/workflows/check-tailwind-manifest.yml, then rerun the workflow that checks the published Essessay.staticwebassets.endpoints.json. Compare the generated manifest with the wwwroot/css/site.css file across the reported environments. Done means the css/site.css route is present deterministically in the manifest and the workflow check passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, docker, github-actions
- Domain
- build-system
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100