Skip to content

Custom BeforeBuild target's generated wwwroot file missing from staticwebassets.endpoints.json depending on build environment #68641

Description

@Popl7

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="&quot;$(TailwindExe)&quot; --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

  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 How to add static assets for Blazor in a MSBuild Target #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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions