Skip to content

fix: SentryCreateRelease uses the assembly's informational version and no longer requires UseSentryCLI - #5679

Merged
jamescrosswell merged 4 commits into
mainfrom
fix/create-release-3536
Oct 7, 2026
Merged

jamescrosswell merged 4 commits into
mainfrom
fix/create-release-3536

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes two bugs in the MSBuild targets that create a Sentry release at build time (SentryCreateRelease / SentrySetCommits). Both were diagnosed in #3536 (comment).

The release was named after AssemblyVersion instead of the informational version

SentryGetApplicationVersion loaded the built assembly with Assembly.LoadFile and read AssemblyInformationalVersionAttribute through reflection. To do that, reflection has to resolve the type of every assembly-level attribute. If any of them comes from an assembly MSBuild can't load, it throws, and the task quietly fell back to AssemblyVersion (App@1.0.0.0). Two common triggers:

The task now reads the name with AssemblyName.GetAssemblyName and the version from FileVersionInfo.ProductVersion. Neither loads the assembly. Two alternatives didn't work: GetCustomAttributesData() throws the same FileNotFoundException, and System.Reflection.Metadata can't be referenced from an inline task on the .NET SDK.

Releases weren't created unless UseSentryCLI was set explicitly

_GetSentryRelease ran after AfterBuild, but CheckSentryCLI only sets the defaults for UseSentryCLI and SentryCreateRelease after Build. As a result:

  • SentryCreateRelease=true on its own did nothing
  • SentrySetCommits=true, which is meant to imply SentryCreateRelease, did nothing even with UseSentryCLI=true

_GetSentryRelease now runs after PrepareSentryCLI, and is skipped when the CLI isn't usable, since nothing would create the release. The DispatchToInnerBuilds hook is gone: NuGet doesn't import buildTransitive targets into the outer build of a multi-targeted project, so it never ran. And since the CLI has been resolved by that point, the releases propose-version fallback now uses the bundled CLI rather than whatever sentry-cli is on PATH.

Notes

Closes #3536

🤖 Generated with Claude Code

…seSentryCLI

SentryGetApplicationVersion read AssemblyInformationalVersionAttribute via
reflection, which throws when any assembly attribute's type can't be loaded
by MSBuild (e.g. UserSecretsIdAttribute), silently falling back to
AssemblyVersion. Read FileVersionInfo.ProductVersion instead, which doesn't
load the assembly.

_GetSentryRelease ran before CheckSentryCLI had defaulted UseSentryCLI and
SentryCreateRelease, so neither SentryCreateRelease nor SentrySetCommits did
anything unless UseSentryCLI was set explicitly. Run it after PrepareSentryCLI.

Fixes #3536

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.12%. Comparing base (8973c9b) to head (63f0ceb).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5679      +/-   ##
==========================================
- Coverage   75.21%   75.12%   -0.09%     
==========================================
  Files         515      515              
  Lines       18989    18989              
  Branches     3693     3693              
==========================================
- Hits        14282    14265      -17     
- Misses       3852     3868      +16     
- Partials      855      856       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/Sentry/buildTransitive/Sentry.targets Outdated
Comment thread src/Sentry/buildTransitive/Sentry.targets Outdated
Comment thread src/Sentry/buildTransitive/Sentry.targets Outdated
jamescrosswell and others added 2 commits October 8, 2026 03:08
…l version

On Windows, FileVersionInfo.ProductVersion falls back to the file version
when there's no AssemblyInformationalVersionAttribute, whereas the runtime
falls back to the assembly version. When ProductVersion equals the file
version but not the assembly version, check whether the assembly references
the attribute type before using it.

Also skip _GetSentryRelease when the Sentry CLI isn't usable, since nothing
would create the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Windows

The type-name scan also matches assemblies that only mention
AssemblyInformationalVersionAttribute without applying it, e.g. to read their
own version. Skip the check when the SDK generated the attribute, only apply
it on Windows, default to the assembly version like the runtime, and warn
when the type name suggests the attribute might be there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ric-oliv
ric-oliv self-requested a review October 7, 2026 15:02

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4dce6e8. Configure here.

Comment thread src/Sentry/buildTransitive/Sentry.targets
@jamescrosswell

jamescrosswell commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

@ric-oliv the version handling on Windows has become hard to follow across the review threads, so here's a write-up of the whole picture, with a suggestion at the end.

The problem

With SentryCreateRelease on, the build creates a Sentry release named <assembly name>@<version>. That version has to match the one the Sentry SDK reports at runtime, otherwise events end up attached to a different release from the one the build created. At runtime the SDK uses the assembly's informational version if it has one, and its assembly version otherwise.

The three version attributes

Attribute MSBuild property Notes
AssemblyVersion, the assembly version AssemblyVersion Always present; derived from Version (e.g. 1.2.3.0), or 1.0.0.0 if nothing is set
AssemblyFileVersion, the file version FileVersion Usually present; derived from Version by default
AssemblyInformationalVersion, the informational version InformationalVersion A free-form string, e.g. 1.2.3+abc123. SDK projects generate it by default. Versioning tools such as Nerdbank.GitVersioning write their own. Projects that turn generation off, and older non-SDK projects, may not have one.

What the build task reads, and why

Before this PR, the task loaded the built dll into MSBuild and read the informational version attribute through reflection. Reflection has to resolve the type of every assembly-level attribute. If any of them comes from an assembly MSBuild can't load, it throws, and the task silently fell back to the assembly version. UserSecretsIdAttribute and MVC's ApplicationPartAttribute both do this, and that's #3536.

A metadata reader (System.Reflection.Metadata) would avoid loading anything, but inline MSBuild tasks can't reference it under dotnet build.

With this PR, the task reads none of the three attributes directly. It reads FileVersionInfo.ProductVersion, the product version, which needs nothing loaded.

FileVersionInfo is a general .NET API for reading a file's version information. That idea comes from Windows, where any .exe or .dll, native or .NET, can carry a version resource: the fields shown in Explorer under Properties → Details. For a .NET assembly, the compiler builds that resource itself from the attributes:

Field in the Windows version resource Filled from
File version File version attribute, or the assembly version if there isn't one
Product version Informational version attribute, or the file version if there isn't one

So the product version isn't a fourth attribute. It's a value derived from the attributes and stored in a separate, Windows-format part of the dll.

Why the product version differs by platform

The compiler writes the Windows version resource whatever OS it runs on, so every dll has one. FileVersionInfo.ProductVersion reads it differently per OS:

  • On Windows, it reads the product version field from that resource. That gives the compiler's fallback: the informational version, or the file version if there isn't one.
  • On Linux and macOS, .NET's implementation doesn't parse the Windows resource. It reads the attributes from the .NET metadata and computes the product version itself, falling back to the assembly version. That's the same answer the runtime gives.

So on Linux and macOS the task always gets the same version as the runtime. On Windows it gets a different one in exactly one situation: the assembly has no informational version, and its file version differs from its assembly version. The product version then comes back as the file version, but the runtime uses the assembly version.

Why that's ambiguous on Windows

The task can see the product version, file version and assembly version, but not whether an informational version attribute exists. Suppose the product version equals the file version but differs from the assembly version. Two quite different projects produce exactly that:

  • No informational version: the runtime uses the assembly version.
  • An informational version that happens to equal the file version: the runtime uses it, i.e. the file version.

The task has two clues for telling them apart:

  1. MSBuild properties. These show when the .NET SDK generated the attribute (UsingMicrosoftNETSdk, GenerateAssemblyInfo, GenerateAssemblyInformationalVersionAttribute). If the SDK generated it, the attribute definitely exists.
  2. A byte scan of the dll for the name AssemblyInformationalVersionAttribute.
    • If the name is absent, the attribute definitely is too.
    • If the name is present, either the attribute is applied, or the app's code merely uses the type, e.g. GetCustomAttribute<AssemblyInformationalVersionAttribute>() to print its own version. The scan can't tell which.

Project setups

These examples use assembly version 1.0.0.0 and file version 7.7.7.7 wherever the two differ:

Setup Informational version in the dll Release at runtime
SDK defaults Generated, e.g. 1.2.3+abc App@1.2.3+abc
SDK defaults plus UserSecretsId (the #3536 bug) Generated App@1.2.3+abc
Nerdbank.GitVersioning or GitVersion Written by the tool, differs from the file version The tool's version
SDK, with InformationalVersion set equal to FileVersion Generated, equals the file version App@7.7.7.7
No informational version; file = assembly version None App@1.0.0.0
No informational version; file ≠ assembly version None App@1.0.0.0
No informational version; file ≠ assembly version; the app's code reads the attribute None (the type is only mentioned) App@1.0.0.0
Hand-written informational version equal to the file version (classic AssemblyInfo.cs with the assembly version pinned) Hand-written, equals the file version App@7.7.7.7

The designs so far

  • Before this PR (main): load the dll and read the attribute through reflection. If that throws, use the assembly version.
  • First commit (2c4ada1): use the product version, falling back to the assembly version if it's empty.
  • Second commit (ecfd480), all platforms: like the first commit, except when the product version equals the file version but not the assembly version, and the attribute name isn't in the dll, use the assembly version.
  • Current (4dce6e8, your suggestion):
    • Skip the check if the SDK generated the attribute.
    • Otherwise, on Windows only, when the product version equals the file version but not the assembly version, use the assembly version.
    • Warn if the attribute name is in the dll.
  • Proposed: same as current, except when the attribute name is in the dll, use the product version (still with the warning).

This is how the current and proposed designs decide. They only differ at the last step:

flowchart TD
    A[Read product version] --> B{"Windows, attribute not generated by the SDK,<br/>and product = file ≠ assembly version?"}
    B -- no --> C["Use product version<br/>(not ambiguous)"]
    B -- yes --> D{"AssemblyInformationalVersionAttribute<br/>name in the dll?"}
    D -- no --> E["Use assembly version<br/>(attribute is absent)"]
    D -- yes --> F["Warn, then guess<br/>(applied, or only referenced)"]
    F --> G["Current: assembly version<br/>wrong if hand-written"]
    F --> H["Proposed: product version<br/>wrong if only mentioned"]
Loading

Results on Windows

"Right" means the build-time release matches the runtime one.

Setup Before this PR First commit Second commit Current Proposed
SDK defaults right* right right right right
SDK defaults plus UserSecretsId (#3536) wrong, silently right right right right
Nerdbank.GitVersioning or GitVersion right* right right right right
SDK, informational = file version right* right right right right
No informational version; file = assembly right* right right right right
No informational version; file ≠ assembly right* wrong, silently right right right
…and the app's code reads the attribute right* wrong, silently wrong, silently right, but warns wrong, warns
Hand-written informational = file version right* right right wrong, warns right, warns

* Only if nothing in the dll triggers the #3536 failure. If something does (for example UserSecretsId or an MVC library), the old code silently uses the assembly version in every setup.

On Linux and macOS, every design in this PR gets every setup right with no warnings. Only the pre-PR code fails there, in the #3536 setup.

FYI: we've switched to the proposed design

The second commit fixed "no informational version, file ≠ assembly" (your first comment). The current design fixed "…and the app's code reads the attribute" (your second comment), but at the cost of "hand-written informational = file version". Neither option gets both right, and both warn in both setups, so I went with the less bad one: 63f0ceb switches to the proposed design above.

Justification: keeping the assembly version fixed while bumping the file and informational versions together is a common pattern in older projects, whereas code that reads an attribute it never sets just gets null back. Both setups also only come up in Windows builds of non-SDK projects or projects that turn off attribute generation. The integration tests now cover both, so shout if you see it differently.

🤖 Written with Claude Code

…iguous on Windows

When the assembly references AssemblyInformationalVersionAttribute but the
product version equals the file version, the attribute is more likely
hand-written with that value than merely mentioned in code. Use the product
version and keep the warning, rather than falling back to the assembly
version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamescrosswell
jamescrosswell merged commit b415ffd into main Oct 7, 2026
51 checks passed
@jamescrosswell
jamescrosswell deleted the fix/create-release-3536 branch October 7, 2026 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SentryCreateRelease not using AssemblyInformationalVersion from Nerdbank.GitVersioning

2 participants