Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ jobs:
if-no-files-found: error
retention-days: 7

- name: Upload all PDB symbols archive
uses: actions/upload-artifact@v7
with:
name: ${{ steps.build-package.outputs.artifact-name }}-all-symbols
path: tested/eng/Builder/bin/nupkg/*.symbols.zip
if-no-files-found: error
retention-days: 7

publish-package:
needs: [build-solution, build-package]
runs-on: windows-latest
Expand Down
9 changes: 9 additions & 0 deletions eng/Builder.Tests/GitHubActionsBuildTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ public void PullRequestEvent_CreatesTraceablePackageIdentity()
"nupkg",
"WpfLab.WpfRuntime.0.0.0-test.20260311123456.sha333333.snupkg")),
identity.SymbolPackagePath);
Assert.Equal(
Path.GetFullPath(Path.Join(
repositoryPath,
"eng",
"Builder",
"bin",
"nupkg",
"WpfLab.WpfRuntime.0.0.0-test.20260311123456.sha333333.symbols.zip")),
identity.AllSymbolsArchivePath);
Assert.Equal(
$"WpfLab.WpfRuntime-nupkg-pr-11781-sha-{TestedSha}-run-42-attempt-3-version-0.0.0-test.20260311123456.sha333333",
identity.ArtifactName);
Expand Down
3 changes: 2 additions & 1 deletion eng/Builder.Tests/GitHubArtifactCommentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,15 @@ public void ArtifactFilter_RequiresExactIdentityAndSortsById()
CreateArtifact(2, $"WpfLab.WpfRuntime-nupkg-pr-11781-sha-{TestedSha}-run-42-attempt-2-version-0.0.0-test.20260311123456.sha222222"),
CreateArtifact(1, $"WpfLab.WpfRuntime-nupkg-pr-11781-sha-{TestedSha}-run-42-attempt-2-version-0.0.0-test.20260311123456.sha222222"),
CreateArtifact(6, $"WpfLab.WpfRuntime-nupkg-pr-11781-sha-{TestedSha}-run-42-attempt-2-version-0.0.0-test.20260311123456.sha222222-symbols"),
CreateArtifact(7, $"WpfLab.WpfRuntime-nupkg-pr-11781-sha-{TestedSha}-run-42-attempt-2-version-0.0.0-test.20260311123456.sha222222-all-symbols"),
CreateArtifact(3, $"WpfLab.WpfRuntime-nupkg-pr-11781-sha-{HeadSha}-run-99-attempt-2-version-0.0.0-test.20260311123456.sha111111"),
CreateArtifact(4, $"WpfLab.WpfRuntime-nupkg-pr-11781-sha-{HeadSha}-run-42-attempt-2-version-0.0.0-test.20260311123456.sha111111", expired: true),
CreateArtifact(5, $"WpfLab.WpfRuntime-nupkg-pr-11781-sha-{HeadSha}-run-42-attempt-2-version-0.0.0-test.20260311123456.sha111111", size: 0),
};

var filtered = GitHubArtifactCommentFormatter.FilterArtifacts(artifacts, 11781, 42, 2);

Assert.Equal([1L, 2L, 6L], filtered.Select(artifact => artifact.Id));
Assert.Equal([1L, 2L, 6L, 7L], filtered.Select(artifact => artifact.Id));
Assert.All(filtered, artifact => Assert.Equal(TestedSha, artifact.TestedSha.ToString()));
Assert.All(filtered, artifact => Assert.Equal("0.0.0-test.20260311123456.sha222222", artifact.PackageVersion));
}
Expand Down
100 changes: 93 additions & 7 deletions eng/Builder.Tests/NuGetPackageServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
using System.IO.Compression;
using System.Xml.Linq;
using WpfReorganize.Builder;

namespace WpfReorganize.Builder.Tests;

public sealed class NuGetPackageServiceTests
{
[Fact]
public void GenerateNuspecIncludesRepositoryReadme()
{
var stagingDirectory = CreateStagingDirectory();
foreach (var rid in new[] { "win-x64", "win-x86" })
{
Directory.CreateDirectory(Path.Join(stagingDirectory, "runtimes", rid, "native"));
}

var readmePath = Path.Join(Path.GetTempPath(), $"builder-readme-{Guid.NewGuid():N}.md");
File.WriteAllText(readmePath, "# Package README");

var nuspecPath = NuGetPackageService.GenerateNuspec(stagingDirectory, "1.2.3", [], readmePath);
var document = XDocument.Load(nuspecPath);
XNamespace ns = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd";
var readme = document.Descendants(ns + "readme").Single().Value;
var readmeTarget = document.Descendants(ns + "file")
.Single(element => element.Attribute("src")?.Value == "README.md")
.Attribute("target")?.Value;
var packagedContent = File.ReadAllText(Path.Join(stagingDirectory, "README.md"));

Assert.Equal(("README.md", "README.md", "# Package README"), (readme, readmeTarget, packagedContent));
}

[Fact]
public void GenerateSymbolNuspecIncludesMultiplePdbFilesWithRuntimePaths()
{
var stagingDirectory = CreateStagingDirectory();
WritePdb(stagingDirectory, "win-x64", "PresentationCore.pdb");
WritePdb(stagingDirectory, "win-x64", "PresentationFramework.pdb");
WritePdb(stagingDirectory, "win-x86", "PresentationCore.pdb");
WritePortablePdb(stagingDirectory, "win-x64", "PresentationCore.pdb");
WritePortablePdb(stagingDirectory, "win-x64", "PresentationFramework.pdb");
WritePortablePdb(stagingDirectory, "win-x86", "PresentationCore.pdb");

var nuspecPath = NuGetPackageService.GenerateSymbolNuspec(stagingDirectory, "1.2.3");
var document = XDocument.Load(nuspecPath);
XNamespace ns = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd";
var targets = document.Descendants(ns + "file")
.Select(element => element.Attribute("target")?.Value)
.Select(element => element.Attribute("target")?.Value
?? throw new InvalidDataException("Symbol file target is missing"))
.ToArray();

Assert.Equal(
Expand All @@ -30,14 +56,67 @@ public void GenerateSymbolNuspecIncludesMultiplePdbFilesWithRuntimePaths()
}

[Fact]
public void GenerateSymbolNuspecThrowsWhenNoPdbFilesExist()
public void GenerateSymbolNuspecDeclaresSymbolsPackageType()
{
var stagingDirectory = CreateStagingDirectory();
WritePortablePdb(stagingDirectory, "win-x64", "PresentationCore.pdb");

var nuspecPath = NuGetPackageService.GenerateSymbolNuspec(stagingDirectory, "1.2.3");
var document = XDocument.Load(nuspecPath);
XNamespace ns = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd";
var packageType = document.Descendants(ns + "packageType").Single();

Assert.Equal("SymbolsPackage", packageType.Attribute("name")?.Value);
}

[Fact]
public void GenerateSymbolNuspecExcludesNonPortablePdbFiles()
{
var stagingDirectory = CreateStagingDirectory();
WritePortablePdb(stagingDirectory, "win-x64", "Portable.pdb");
WriteWindowsPdb(stagingDirectory, "win-x64", "Windows.pdb");

var nuspecPath = NuGetPackageService.GenerateSymbolNuspec(stagingDirectory, "1.2.3");
var document = XDocument.Load(nuspecPath);
XNamespace ns = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd";
var targets = document.Descendants(ns + "file")
.Select(element => element.Attribute("target")?.Value
?? throw new InvalidDataException("Symbol file target is missing"))
.ToArray();

Assert.Equal([@"runtimes\win-x64\lib\net8.0\Portable.pdb"], targets);
}

[Fact]
public void GenerateSymbolNuspecThrowsWhenNoPortablePdbFilesExist()
{
var stagingDirectory = CreateStagingDirectory();
WriteWindowsPdb(stagingDirectory, "win-x64", "Windows.pdb");

Assert.Throws<InvalidOperationException>(() =>
NuGetPackageService.GenerateSymbolNuspec(stagingDirectory, "1.2.3"));
}

[Fact]
public void CreateAllSymbolsArchiveIncludesPortableAndNonPortablePdbFiles()
{
var stagingDirectory = CreateStagingDirectory();
WritePortablePdb(stagingDirectory, "win-x64", "PresentationCore.pdb");
WriteWindowsPdb(stagingDirectory, "win-x86", "PresentationCore.pdb");
var outputDirectory = Path.Join(Path.GetTempPath(), $"builder-symbol-output-{Guid.NewGuid():N}");

var archivePath = NuGetPackageService.CreateAllSymbolsArchive(stagingDirectory, "1.2.3", outputDirectory);
using var archive = ZipFile.OpenRead(archivePath);
var entries = archive.Entries.Select(entry => entry.FullName).ToArray();

Assert.Equal(
[
"runtimes/win-x64/lib/net8.0/PresentationCore.pdb",
"runtimes/win-x86/lib/net8.0/PresentationCore.pdb",
],
entries);
}

private static string CreateStagingDirectory()
{
var stagingDirectory = Path.Join(Path.GetTempPath(), $"builder-symbol-tests-{Guid.NewGuid():N}");
Expand All @@ -49,6 +128,13 @@ private static string CreateStagingDirectory()
return stagingDirectory;
}

private static void WritePdb(string stagingDirectory, string rid, string fileName) =>
File.WriteAllText(Path.Join(stagingDirectory, "runtimes", rid, "lib", "net8.0", fileName), "pdb");
private static void WritePortablePdb(string stagingDirectory, string rid, string fileName) =>
File.WriteAllBytes(
Path.Join(stagingDirectory, "runtimes", rid, "lib", "net8.0", fileName),
"BSJBportable"u8.ToArray());

private static void WriteWindowsPdb(string stagingDirectory, string rid, string fileName) =>
File.WriteAllBytes(
Path.Join(stagingDirectory, "runtimes", rid, "lib", "net8.0", fileName),
"Microsoft C/C++ MSF 7.00"u8.ToArray());
}
4 changes: 3 additions & 1 deletion eng/Builder.Tests/WorkflowContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ public void BuildWorkflow_UsesTrustedReadOnlyPullRequestTargetContract()
Assert.Equal(2, CountOccurrences(normalized, " path: tested\n fetch-depth: 0"));
Assert.Equal(2, CountOccurrences(workflow, "Builder.dll ci-build"));
Assert.DoesNotContain("github.event.pull_request.head.sha", workflow, StringComparison.Ordinal);
Assert.Equal(2, CountOccurrences(workflow, "tested/eng/Builder/bin/nupkg/*."));
Assert.Equal(3, CountOccurrences(workflow, "tested/eng/Builder/bin/nupkg/*."));
Assert.Contains("path: tested/eng/Builder/bin/nupkg/*.nupkg", workflow, StringComparison.Ordinal);
Assert.Contains("path: tested/eng/Builder/bin/nupkg/*.snupkg", workflow, StringComparison.Ordinal);
Assert.Contains("path: tested/eng/Builder/bin/nupkg/*.symbols.zip", workflow, StringComparison.Ordinal);
Assert.Contains("name: ${{ steps.build-package.outputs.artifact-name }}-symbols", workflow, StringComparison.Ordinal);
Assert.Contains("name: ${{ steps.build-package.outputs.artifact-name }}-all-symbols", workflow, StringComparison.Ordinal);
Assert.DoesNotContain("steps.build-package.outputs.symbol-package-path", workflow, StringComparison.Ordinal);
Assert.Contains("name: Push generated package to NuGet registries", workflow, StringComparison.Ordinal);
Assert.DoesNotContain("if: github.event_name != 'pull_request_target'", workflow, StringComparison.Ordinal);
Expand Down
5 changes: 4 additions & 1 deletion eng/Builder/BuildService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,12 @@ public static int Run(BuilderContext context, string version)
return 1;
}
var runtimePackageDependencies = NuGetPackageService.ReadRuntimePackageDependencies(context.RepoRoot);
var nuspecPath = NuGetPackageService.GenerateNuspec(context.StagingDir, version, runtimePackageDependencies);
var readmePath = Path.Join(context.RepoRoot, "README.md");
var nuspecPath = NuGetPackageService.GenerateNuspec(context.StagingDir, version, runtimePackageDependencies, readmePath);
var symbolNuspecPath = NuGetPackageService.GenerateSymbolNuspec(context.StagingDir, version);
var nupkgPath = NuGetPackageService.PackNuGet(nuspecPath, context.NupkgOutputDir);
var snupkgPath = NuGetPackageService.PackSymbolNuGet(symbolNuspecPath, context.NupkgOutputDir);
var allSymbolsArchivePath = NuGetPackageService.CreateAllSymbolsArchive(context.ArtifactsDir, version, context.NupkgOutputDir);

// ---- Step 7: Compare against official package ----
Log.Step("Comparing against official Microsoft.WindowsDesktop.App.Ref...");
Expand All @@ -210,6 +212,7 @@ public static int Run(BuilderContext context, string version)
Log.Info($"Build complete! Elapsed: {elapsed.TotalSeconds:F1}s");
Log.Info($"NuGet package: {nupkgPath}");
Log.Info($"NuGet symbol package: {snupkgPath}");
Log.Info($"All-symbols archive: {allSymbolsArchivePath}");
return failedProjects.Count > 0 ? 2 : 0;

}
Expand Down
1 change: 1 addition & 0 deletions eng/Builder/GitHubActionsBuildCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public async Task<int> RunAsync()
["version"] = identity.PackageVersion,
["package-path"] = identity.PackagePath,
["symbol-package-path"] = identity.SymbolPackagePath,
["all-symbols-archive-path"] = identity.AllSymbolsArchivePath,
["artifact-name"] = identity.ArtifactName,
});
}
Expand Down
5 changes: 5 additions & 0 deletions eng/Builder/GitHubActionsBuildIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal sealed record GitHubActionsBuildIdentity(
string PackageVersion,
string PackagePath,
string SymbolPackagePath,
string AllSymbolsArchivePath,
string ArtifactName)
{
public static GitHubActionsBuildIdentity Create(
Expand Down Expand Up @@ -41,6 +42,9 @@ public static GitHubActionsBuildIdentity Create(
"nupkg",
$"{PackageMetadata.Id}.{packageVersion}.nupkg"));
var symbolPackagePath = Path.ChangeExtension(packagePath, ".snupkg");
var allSymbolsArchivePath = Path.Join(
Path.GetDirectoryName(packagePath)!,
$"{Path.GetFileNameWithoutExtension(packagePath)}.symbols.zip");
var artifactName =
$"{PackageMetadata.Id}-nupkg-{artifactIdentity}-sha-{parsedTestedSha}-run-{runId}-attempt-{runAttempt}-version-{packageVersion}";

Expand All @@ -49,6 +53,7 @@ public static GitHubActionsBuildIdentity Create(
packageVersion,
packagePath,
symbolPackagePath,
allSymbolsArchivePath,
artifactName);
}

Expand Down
2 changes: 2 additions & 0 deletions eng/Builder/GitHubActionsBuildService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ await RunRequiredAsync(
identity.PackageVersion).ConfigureAwait(false);
RequireNonEmptyFile(identity.PackagePath);
RequireNonEmptyFile(identity.SymbolPackagePath);
RequireNonEmptyFile(identity.AllSymbolsArchivePath);
await RunRequiredAsync(
"test generated package",
_dotnetPath,
Expand All @@ -209,6 +210,7 @@ await RunRequiredAsync(
identity.PackagePath).ConfigureAwait(false);
RequireNonEmptyFile(identity.PackagePath);
RequireNonEmptyFile(identity.SymbolPackagePath);
RequireNonEmptyFile(identity.AllSymbolsArchivePath);
}

private Task RebuildSolutionAsync(
Expand Down
2 changes: 1 addition & 1 deletion eng/Builder/GitHubArtifactCommentFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public static IReadOnlyList<GitHubArtifactCommentItem> FilterArtifacts(
{
ArgumentNullException.ThrowIfNull(artifacts);
var pattern = new Regex(
$"^{Regex.Escape(PackageMetadata.Id)}-nupkg-pr-{pullRequestNumber}-sha-([0-9a-fA-F]{{40}})-run-{runId}-attempt-{runAttempt}-version-([0-9A-Za-z.+-]+?)(?:-symbols)?$",
$"^{Regex.Escape(PackageMetadata.Id)}-nupkg-pr-{pullRequestNumber}-sha-([0-9a-fA-F]{{40}})-run-{runId}-attempt-{runAttempt}-version-([0-9A-Za-z.+-]+?)(?:-symbols|-all-symbols)?$",
RegexOptions.CultureInvariant);
return artifacts
.Where(artifact => artifact is not null
Expand Down
Loading
Loading