Skip to content

Bug: nullable-gate-cannot-fail-incremental-build #512

Description

@drmoisan
  • Work Mode: full-bug

Summary

The repository-mandated type-check gate msbuild TaskMaster.sln /t:Build /p:Nullable=enable /p:TreatWarningsAsErrors=true cannot fail in practice. MSBuild's incremental up-to-date check does not invalidate on a command-line /p: change alone, so when the outputs are already current from a prior /t:Build, CoreCompile is skipped entirely and the gate returns exit 0 without ever re-running nullable analysis. The gate has been reporting success while never executing.

Environment

  • OS/version: Windows 11
  • Runtime: .NET Framework 4.8.1
  • Command/flags used: msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true
  • Data source or fixture: none

Steps to Reproduce

  1. Run the analyzer build: msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true. It succeeds.
  2. Immediately run the mandated type-check gate: msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true. It returns exit 0.
  3. Now force recompilation of the same code with the same properties: msbuild TaskMaster\TaskMaster.csproj /t:Rebuild /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true.
  4. Observe it returns exit 1 with a large number of CS86xx nullable errors.

Expected Behavior

The type-check gate actually performs nullable flow analysis on every invocation and fails when nullable violations exist, so that "the nullable gate passed" is a meaningful statement.

Actual Behavior

Step 2 returns exit 0 because MSBuild considers the outputs up to date and skips CoreCompile. Step 3, which forces recompilation of identical source under identical properties, returns exit 1 with 195 errors, 64 of them CS86xx in TaskMaster.csproj. A separate measurement during the same session counted 220 CS86xx errors concentrated in AppOlObjects.cs, AppAutoFileObjects.cs, AppToDoObjects.cs, AppOlObjects.FolderTreeService.cs, AppStagingFilenames.cs, ApplicationGlobals.cs, and AppItemEngines.cs.

The same source, the same properties, and two different verdicts depending only on whether the output timestamps happened to be current.

Logs / Screenshots

  • Attached minimal logs or snippet
  • Snippet:
# /t:Build with /p:Nullable=enable /p:TreatWarningsAsErrors=true
EXIT=0

# /t:Rebuild, same properties, same source
EXIT=1
195 errors (64 CS86xx)

Precedent: .github/workflows/ci.yml already carries a comment documenting exactly this MSBuild behavior and uses /t:Rebuild for its own TreatWarningsAsErrors step for that reason. The repository's own CI has the workaround; the policy command documented in CLAUDE.md and .claude/rules/csharp.md does not.

Impact / Severity

  • Blocker
  • High
  • Medium
  • Low

This is a quality gate that reports success without running. Every agent and developer who has run the documented toolchain has recorded a passing nullable check that did not execute, which means the recorded evidence across prior features overstates what was verified. It also masks a substantial pre-existing nullable debt in TaskMaster.csproj.

Severity is High rather than Blocker because the debt it masks is pre-existing rather than newly introduced, and CI's /t:Rebuild step does exercise TreatWarningsAsErrors (though CI does not pass /p:Nullable=enable, so CI does not surface the CS86xx set either).

Source

From: docs/features/potential/2026-08-08-nullable-gate-cannot-fail-incremental-build.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions