From 76f36a6418058b3c861a17b69e6523c5dd6f1592 Mon Sep 17 00:00:00 2001 From: Missy Messa Date: Tue, 7 Apr 2026 15:12:03 -0700 Subject: [PATCH] Replace external PublishSymbols task with SymbolUploadHelper-based task (WI 10148/10149) Replaces the Microsoft.SymbolUploader.Build.Task dependency in PublishToSymbolServers.proj with a new PublishSymbolsUsingSymbolUploadHelper MSBuild task that uses the proven internal SymbolUploadHelper infrastructure already used by Maestro promotion publishing. Key changes: - New task PublishSymbolsUsingSymbolUploadHelper in Build.Tasks.Feed that wraps SymbolUploadHelper - Supports both PAT auth (backward compat) and DefaultIdentityTokenCredential (Entra/MI) - Updated PublishToSymbolServers.proj to use the new task and reference Build.Tasks.Feed - SymbolServerTargets now use org names (microsoft, microsoftpublicsymbols) instead of URLs Migration path: When PATs are still configured, PAT auth is used automatically. When PATs are removed, DefaultIdentityTokenCredential provides Entra auth. --- .../SdkTasks/PublishToSymbolServers.proj | 34 +-- .../PublishSymbolsUsingSymbolUploadHelper.cs | 246 ++++++++++++++++++ 2 files changed, 263 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.DotNet.Build.Tasks.Feed/src/PublishSymbolsUsingSymbolUploadHelper.cs diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishToSymbolServers.proj b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishToSymbolServers.proj index a3e199b3ae5..6009efe2196 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishToSymbolServers.proj +++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishToSymbolServers.proj @@ -4,14 +4,15 @@ - + + $(DotNetSymbolServerTokenMsdl) - - + + $(DotNetSymbolServerTokenSymWeb) - - + - + diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/PublishSymbolsUsingSymbolUploadHelper.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/PublishSymbolsUsingSymbolUploadHelper.cs new file mode 100644 index 00000000000..6f3853633ac --- /dev/null +++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/PublishSymbolsUsingSymbolUploadHelper.cs @@ -0,0 +1,246 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Frozen; +using System.Linq; +using System.Threading.Tasks; +using Azure.Core; +using Microsoft.Build.Framework; +using Microsoft.DotNet.ArcadeAzureIntegration; +using Microsoft.DotNet.Internal.SymbolHelper; +using MsBuildUtils = Microsoft.Build.Utilities; + +namespace Microsoft.DotNet.Build.Tasks.Feed; + +/// +/// MSBuild task that publishes symbols to a symbol server using the internal SymbolUploadHelper +/// infrastructure. Supports both PAT-based auth (legacy) and Entra/managed identity auth. +/// +public class PublishSymbolsUsingSymbolUploadHelper : MsBuildUtils.Task +{ + /// + /// The Azure DevOps organization to publish symbols to (e.g., "microsoft" or "microsoftpublicsymbols"). + /// + [Required] + public string AzdoOrg { get; set; } + + /// + /// Optional personal access token. If provided, PAT-based auth is used. + /// If empty or not set, DefaultIdentityTokenCredential (Entra/MI) is used. + /// + public string PersonalAccessToken { get; set; } + + /// + /// Optional managed identity client ID for DefaultIdentityTokenCredential. + /// + public string ManagedIdentityClientId { get; set; } + + /// + /// Symbol packages (*.symbols.nupkg) to publish. + /// + public ITaskItem[] PackagesToPublish { get; set; } + + /// + /// Individual files (PDBs, DLLs, etc.) to publish. + /// + public ITaskItem[] FilesToPublish { get; set; } + + /// + /// Files to exclude from symbol packages during publishing. + /// + public ITaskItem[] PackageExcludeFiles { get; set; } + + /// + /// Number of days before the symbol request expires. Default is 3650. + /// + public int ExpirationInDays { get; set; } = 3650; + + /// + /// Whether to enable verbose logging from the symbol client. + /// + public bool VerboseLogging { get; set; } + + /// + /// Whether to perform a dry run without actually publishing. + /// + public bool DryRun { get; set; } + + /// + /// Whether to convert portable PDBs to Windows PDBs. + /// + public bool ConvertPortablePdbsToWindowsPdbs { get; set; } + + /// + /// Whether to treat PDB conversion issues as informational messages. + /// + public bool TreatPdbConversionIssuesAsInfo { get; set; } + + /// + /// Comma-separated list of PDB conversion diagnostic IDs to treat as warnings. + /// + public string PdbConversionTreatAsWarning { get; set; } + + /// + /// Whether to publish special CLR files (DAC, DBI, SOS) under diagnostic indexes. + /// + public bool PublishSpecialClrFiles { get; set; } + + /// + /// Directory containing loose PDB/DLL files to publish. + /// If set, files from this directory are added via AddDirectory instead of individual file items. + /// + public string PDBArtifactsDirectory { get; set; } + + public override bool Execute() + { + return ExecuteAsync().GetAwaiter().GetResult(); + } + + private async Task ExecuteAsync() + { + try + { + TokenCredential credential = CreateCredential(); + TaskTracer tracer = new(Log, verbose: VerboseLogging); + + IEnumerable pdbWarnings = ParsePdbConversionWarnings(); + FrozenSet exclusions = PackageExcludeFiles?.Select(i => i.ItemSpec).ToFrozenSet() + ?? FrozenSet.Empty; + + SymbolPublisherOptions options = new( + AzdoOrg, + credential, + packageFileExcludeList: exclusions, + convertPortablePdbs: ConvertPortablePdbsToWindowsPdbs, + treatPdbConversionIssuesAsInfo: TreatPdbConversionIssuesAsInfo, + pdbConversionTreatAsWarning: pdbWarnings, + dotnetInternalPublishSpecialClrFiles: PublishSpecialClrFiles, + verboseClient: VerboseLogging, + isDryRun: DryRun); + + SymbolUploadHelper helper = DryRun + ? SymbolUploadHelperFactory.GetSymbolHelperFromLocalTool(tracer, options, ".") + : await SymbolUploadHelperFactory.GetSymbolHelperWithDownloadAsync(tracer, options); + + string requestName = $"arcade-sdk/{AzdoOrg}/{Guid.NewGuid()}"; + Log.LogMessage(MessageImportance.High, "Creating symbol request '{0}' for org '{1}'", requestName, AzdoOrg); + + int result = await helper.CreateRequest(requestName); + if (result != 0) + { + Log.LogError("Failed to create symbol request '{0}'. Exit code: {1}", requestName, result); + return false; + } + + bool succeeded = false; + try + { + // Add loose files directory if specified + if (!string.IsNullOrEmpty(PDBArtifactsDirectory)) + { + Log.LogMessage(MessageImportance.High, "Adding directory '{0}' to symbol request", PDBArtifactsDirectory); + result = await helper.AddDirectory(requestName, PDBArtifactsDirectory); + if (result != 0) + { + Log.LogError("Failed to add directory to symbol request. Exit code: {0}", result); + return false; + } + } + + // Add individual file items if specified (and no directory was given) + if (FilesToPublish?.Length > 0 && string.IsNullOrEmpty(PDBArtifactsDirectory)) + { + // SymbolUploadHelper works with directories, so we need to group files + // by their parent directory and add each directory. + var directories = FilesToPublish + .Select(f => System.IO.Path.GetDirectoryName(f.ItemSpec)) + .Where(d => !string.IsNullOrEmpty(d)) + .Distinct(StringComparer.OrdinalIgnoreCase); + + foreach (string dir in directories) + { + Log.LogMessage(MessageImportance.Normal, "Adding file directory '{0}' to symbol request", dir); + result = await helper.AddDirectory(requestName, dir); + if (result != 0) + { + Log.LogError("Failed to add directory '{0}' to symbol request. Exit code: {1}", dir, result); + return false; + } + } + } + + // Add symbol packages + if (PackagesToPublish?.Length > 0) + { + IEnumerable packagePaths = PackagesToPublish.Select(p => p.ItemSpec); + Log.LogMessage(MessageImportance.High, "Adding {0} symbol package(s) to request", PackagesToPublish.Length); + + result = await helper.AddPackagesToRequest(requestName, packagePaths); + if (result != 0) + { + Log.LogError("Failed to add packages to symbol request. Exit code: {0}", result); + return false; + } + } + + Log.LogMessage(MessageImportance.High, "Finalizing symbol request with expiration of {0} days", ExpirationInDays); + result = await helper.FinalizeRequest(requestName, (uint)ExpirationInDays); + if (result != 0) + { + Log.LogError("Failed to finalize symbol request. Exit code: {0}", result); + return false; + } + + succeeded = true; + } + finally + { + if (!succeeded) + { + Log.LogMessage(MessageImportance.High, "Symbol publishing failed. Deleting request '{0}'.", requestName); + await helper.DeleteRequest(requestName); + } + } + + Log.LogMessage(MessageImportance.High, "Successfully published symbols to '{0}'", AzdoOrg); + return true; + } + catch (Exception ex) + { + Log.LogErrorFromException(ex, showStackTrace: true); + return false; + } + } + + private TokenCredential CreateCredential() + { + if (!string.IsNullOrEmpty(PersonalAccessToken)) + { + Log.LogMessage(MessageImportance.Normal, "Using PAT-based authentication for symbol publishing"); + return new PATCredential(PersonalAccessToken); + } + + Log.LogMessage(MessageImportance.Normal, "Using Entra/managed identity authentication for symbol publishing"); + var options = new DefaultIdentityTokenCredentialOptions(); + if (!string.IsNullOrEmpty(ManagedIdentityClientId)) + { + options.ManagedIdentityClientId = ManagedIdentityClientId; + } + return new DefaultIdentityTokenCredential(options); + } + + private IEnumerable ParsePdbConversionWarnings() + { + if (string.IsNullOrEmpty(PdbConversionTreatAsWarning)) + { + return []; + } + + return PdbConversionTreatAsWarning + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(s => int.TryParse(s, out _)) + .Select(int.Parse); + } +}