Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<CssParserReleaseVersionSuffix>20230414.1</CssParserReleaseVersionSuffix>
<HumanizerReleaseVersion>2.14.1</HumanizerReleaseVersion>
<MSBuildLocatorReleaseVersion>1.10.2</MSBuildLocatorReleaseVersion>
<NewtonsoftJsonReleaseVersion>13.0.3</NewtonsoftJsonReleaseVersion>
<SolutionPersistenceVersion>1.0.52</SolutionPersistenceVersion>
<SpectreConsoleReleaseVersion>0.54.0</SpectreConsoleReleaseVersion>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<!-- The FileVersion revision from the Microsoft-shipped package. This must be updated
when the submodule is updated. See https://github.com/dotnet/source-build/issues/5509 -->
<FileVersionRevision>50722</FileVersionRevision>
<!-- A NuGet package produced by this component, used by tests to validate FileVersionRevision. -->
<!-- Used by the update-external-metadata script to determine the package to download. -->
<FileVersionValidationPackage>Microsoft.IdentityModel.Tokens</FileVersionValidationPackage>
</PropertyGroup>

Expand Down
6 changes: 6 additions & 0 deletions src/externalPackages/projects/newtonsoft-json.proj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
<DotNetToolArgs>$(DotNetToolArgs) /p:PublicSign=true</DotNetToolArgs>
<DotNetToolArgs>$(DotNetToolArgs) /p:TreatWarningsAsErrors=false</DotNetToolArgs>
<DotNetToolArgs>$(DotNetToolArgs) /p:AdditionalConstants=SIGNED</DotNetToolArgs>
<!-- The upstream build computes a date-based FileVersion revision via PowerShell (GetVersion in build.ps1).
Set it here to match the official NuGet package. See https://github.com/dotnet/source-build/issues/5511 -->
<FileVersionRevision>27908</FileVersionRevision>
<!-- Used by the update-external-metadata script to determine the package to download. -->
<FileVersionValidationPackage>Newtonsoft.Json</FileVersionValidationPackage>
<DotNetToolArgs>$(DotNetToolArgs) /p:FileVersion=$(NewtonsoftJsonReleaseVersion).$(FileVersionRevision)</DotNetToolArgs>

<BuildCommand>$(DotNetTool) pack $(NewtonsoftJsonProjectPath) /bl:$(ArtifactsLogRepoDir)build.binlog $(DotNetToolArgs)</BuildCommand>

Expand Down
127 changes: 108 additions & 19 deletions tests/SbrpTests/ExternalPackageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using NuGet.Packaging;
using SbrpUtilities;
using Xunit;
using Xunit.Abstractions;
Expand Down Expand Up @@ -87,17 +88,18 @@ public void SourceRevisionIdMatchesSubmoduleCommit()
/// <summary>
/// Validates that the FileVersionRevision property in external package .proj files
/// matches the actual FileVersion revision from the Microsoft-shipped NuGet package.
/// The test downloads the package specified by FileVersionValidationPackage using the
/// NuGet protocol API (honoring NuGet.config sources), extracts a DLL, reads its
/// FileVersion, and compares the revision component.
/// The test discovers the package ID from the component's build output directory
/// (artifacts/obj/{component}/), downloads the same package from NuGet, extracts a DLL,
/// reads its FileVersion, and compares the revision component.
/// See https://github.com/dotnet/source-build/issues/5509
/// </summary>
[Fact]
[SkippableFact]
public async Task FileVersionRevisionMatchesPublishedPackage()
{
string repoRoot = PathUtilities.GetRepoRoot().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string projectsDir = Path.Combine(repoRoot, "src", "externalPackages", "projects");
string versionsPropsPath = Path.Combine(repoRoot, "eng", "Versions.props");
string artifactsObjDir = Path.Combine(repoRoot, "artifacts", "obj");

string[] projFiles = Directory.GetFiles(projectsDir, "*.proj");
Assert.True(projFiles.Length > 0, $"No .proj files found in {projectsDir}");
Expand All @@ -107,7 +109,7 @@ public async Task FileVersionRevisionMatchesPublishedPackage()

foreach (string projFile in projFiles)
{
string packageName = Path.GetFileNameWithoutExtension(projFile);
string componentName = Path.GetFileNameWithoutExtension(projFile);
XDocument doc = XDocument.Load(projFile);
string? fileVersionRevision = doc.Descendants("FileVersionRevision").FirstOrDefault()?.Value;

Expand All @@ -116,47 +118,134 @@ public async Task FileVersionRevisionMatchesPublishedPackage()
continue;
}

string? validationPackage = doc.Descendants("FileVersionValidationPackage").FirstOrDefault()?.Value;
if (string.IsNullOrEmpty(validationPackage))
// Find matching release version in eng/Versions.props
string? releaseVersion = CommonUtilities.FindReleaseVersion(versionsPropsPath, componentName);

if (string.IsNullOrEmpty(releaseVersion))
{
errors.Add($"{packageName}.proj: Has FileVersionRevision but no FileVersionValidationPackage property.");
errors.Add($"{componentName}.proj: No matching release version property found in eng/Versions.props.");
checkedCount++;
continue;
}

// Find matching release version in eng/Versions.props
string? releaseVersion = CommonUtilities.FindReleaseVersion(versionsPropsPath, packageName);
// Find a .nupkg produced by this specific component
string componentObjDir = Path.Combine(artifactsObjDir, componentName);
if (!Directory.Exists(componentObjDir))
{
Output.WriteLine($"Skipping {componentName}: build output directory not found ({componentObjDir}).");
continue;
}

if (string.IsNullOrEmpty(releaseVersion))
string[] componentNupkgs = Directory.GetFiles(componentObjDir, "*.nupkg", SearchOption.AllDirectories);

// Find the first package that contains a DLL so we can read its FileVersion
string? packageId = null;
foreach (string nupkg in componentNupkgs)
{
errors.Add($"{packageName}.proj: No matching release version property found in eng/Versions.props.");
checkedCount++;
using PackageArchiveReader reader = new(nupkg);
bool hasDll = reader.GetLibItems()
.SelectMany(group => group.Items)
.Any(item => item.EndsWith(".dll", StringComparison.OrdinalIgnoreCase));
if (hasDll)
{
packageId = reader.NuspecReader.GetId();
break;
}
}

if (packageId is null)
{
Output.WriteLine($"Skipping {componentName}: no .nupkg with a DLL found in build output.");
continue;
}

// Download the package and read the FileVersion revision
// Download the published package and read the FileVersion revision
var (revision, fileVersion) = await CommonUtilities.GetFileVersionRevisionAsync(
repoRoot, validationPackage, releaseVersion);
repoRoot, packageId, releaseVersion);

if (revision is null)
{
errors.Add($"{packageName}.proj: Unable to download {validationPackage} {releaseVersion} to validate FileVersionRevision.");
errors.Add($"{componentName}.proj: Unable to download {packageId} {releaseVersion} to validate FileVersionRevision.");
checkedCount++;
continue;
}

if (!int.TryParse(fileVersionRevision, out int expectedRevision) || expectedRevision != revision.Value)
{
errors.Add($"{packageName}.proj: FileVersionRevision '{fileVersionRevision}' does not match " +
$"actual revision '{revision}' from {validationPackage} {releaseVersion} " +
errors.Add($"{componentName}.proj: FileVersionRevision '{fileVersionRevision}' does not match " +
$"actual revision '{revision}' from {packageId} {releaseVersion} " +
$"(FileVersion: {fileVersion}).");
}

checkedCount++;
}

Assert.True(checkedCount > 0, "No external packages with FileVersionRevision were found to validate.");
Skip.If(checkedCount == 0, "No components with FileVersionRevision had build output to validate.");
Assert.True(errors.Count == 0,
$"FileVersionRevision validation failed:{Environment.NewLine}{string.Join(Environment.NewLine, errors)}");
}

/// <summary>
/// Validates that the release version configured in eng/Versions.props for each external
/// component matches the version of at least one NuGet package produced by that component.
/// The test scans each component's build output directory (artifacts/obj/{component}/) for
/// .nupkg files and verifies at least one has the expected version.
/// This catches cases where a submodule is updated but the release version is not.
/// </summary>
[SkippableFact]
public void ReleaseVersionMatchesPackageOutput()
{
string repoRoot = PathUtilities.GetRepoRoot().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string projectsDir = Path.Combine(repoRoot, "src", "externalPackages", "projects");
string versionsPropsPath = Path.Combine(repoRoot, "eng", "Versions.props");
string artifactsObjDir = Path.Combine(repoRoot, "artifacts", "obj");

string[] projFiles = Directory.GetFiles(projectsDir, "*.proj");
Assert.True(projFiles.Length > 0, $"No .proj files found in {projectsDir}");

List<string> errors = new();
int checkedCount = 0;

foreach (string projFile in projFiles)
{
string componentName = Path.GetFileNameWithoutExtension(projFile);
string? releaseVersion = CommonUtilities.FindReleaseVersion(versionsPropsPath, componentName);

if (string.IsNullOrEmpty(releaseVersion))
{
continue;
}

// Scan the component's own build output directory for .nupkg files
string componentObjDir = Path.Combine(artifactsObjDir, componentName);
if (!Directory.Exists(componentObjDir))
{
Output.WriteLine($"Skipping {componentName}: build output directory not found ({componentObjDir}).");
continue;
}

string[] componentNupkgs = Directory.GetFiles(componentObjDir, "*.nupkg", SearchOption.AllDirectories);
if (componentNupkgs.Length == 0)
{
Output.WriteLine($"Skipping {componentName}: no .nupkg files found in build output.");
continue;
}

string versionSuffix = $".{releaseVersion}.nupkg";
bool foundMatch = componentNupkgs.Any(f =>
Path.GetFileName(f).EndsWith(versionSuffix, StringComparison.OrdinalIgnoreCase));

if (!foundMatch)
{
string foundPackages = string.Join(", ", componentNupkgs.Select(Path.GetFileName));
errors.Add($"{componentName}: Expected package version {releaseVersion} but found: {foundPackages}");
}

checkedCount++;
}

Skip.If(checkedCount == 0, "No components with release versions had build output to validate.");
Assert.True(errors.Count == 0,
$"Release version validation failed:{Environment.NewLine}{string.Join(Environment.NewLine, errors)}");
}
}