Skip to content

Add option in Arcade to preserve RepoOrigin and insert it into the publish-to-disk path #15572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
3 changes: 3 additions & 0 deletions src/Microsoft.DotNet.Arcade.Sdk/tools/Publish.proj
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
<AutoGenerateSymbolPackages Condition="'$(AutoGenerateSymbolPackages)' == '' and
('$(DotNetBuildSourceOnly)' != 'true' or ('$(DotNetBuildInnerRepo)' == 'true' and '$(DotNetBuildOrchestrator)' != 'true'))">true</AutoGenerateSymbolPackages>

<PreserveRepoOrigin Condition="'$(PreserveRepoOrigin)' == ''">false</PreserveRepoOrigin>

<!-- This tracks the dependent targets of PublishToAzureDevOpsArtifacts. We can't rename this
property as it is used by other repositories already. -->
<PublishDependsOnTargets>BeforePublish;AutoGenerateSymbolPackages</PublishDependsOnTargets>
Expand Down Expand Up @@ -365,6 +367,7 @@
IsReleaseOnlyPackageVersion="$(IsReleaseOnlyPackageVersion)"
PushToLocalStorage="$(PushToLocalStorage)"
AssetsLocalStorageDir="$(SourceBuiltAssetsDir)"
PreserveRepoOrigin="$(PreserveRepoOrigin)"
ShippingPackagesLocalStorageDir="$(SourceBuiltShippingPackagesDir)"
NonShippingPackagesLocalStorageDir="$(SourceBuiltNonShippingPackagesDir)"
AssetManifestsLocalStorageDir="$(SourceBuiltAssetManifestsDir)"
Expand Down
106 changes: 106 additions & 0 deletions src/Microsoft.DotNet.Build.Tasks.Feed.Tests/BuildModelFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,112 @@ public void RoundTripFromTaskItemsToFileToXml()
}
}

[Fact]
public void PreserveRepoOrigin()
{
var localPackagePath = TestInputs.GetFullPath(Path.Combine("Nupkgs", "test-package-a.1.0.0.nupkg"));

const string bopSymbolsNupkg = "foo/bar/baz/bop.symbols.nupkg";
string bobSymbolsExpectedId = $"assets/symbols/{Path.GetFileName(bopSymbolsNupkg)}";
const string zipArtifact = "foo/bar/baz/bing.zip";
// New PDB artifact
const string pdbArtifact = "foo/bar/baz/bing.pdb";

var artifacts = new ITaskItem[]
{
new TaskItem(bopSymbolsNupkg, new Dictionary<string, string>
{
{ "ManifestArtifactData", "NonShipping=true;Category=SMORKELER" },
{ "RelativeBlobPath", bobSymbolsExpectedId},
{ "RepoOrigin", "runtime" },
{ "ThisIsntArtifactMetadata", "YouGoofed!" },
{ "Kind", "Blob" }
}),
// Include a package and a fake zip too
// Note that the relative blob path is a "first class" attribute,
// not parsed from ManifestArtifactData
new TaskItem(zipArtifact, new Dictionary<string, string>
{
{ "RelativeBlobPath", zipArtifact },
{ "ManifestArtifactData", "ARandomBitOfMAD=" },
{ "Kind", "Blob" }
}),
new TaskItem(localPackagePath, new Dictionary<string, string>()
{
// This isn't recognized or used for a nupkg
{ "RelativeBlobPath", zipArtifact },
{ "RepoOrigin", "runtime" },
{ "ManifestArtifactData", "ShouldWePushDaNorpKeg=YES" },
{ "Kind", "Package" }
}),
// New PDB artifact with RelativePdbPath
new TaskItem(pdbArtifact, new Dictionary<string, string>
{
{ "RelativePdbPath", pdbArtifact },
{ "RepoOrigin", "runtime" },
{ "ManifestArtifactData", "NonShipping=false;Category=PDB" },
{ "Kind", "PDB" }
})
};

var modelFromItems = _buildModelFactory.CreateModel(artifacts: artifacts,
artifactVisibilitiesToInclude: ArtifactVisibility.All,
buildId: _testAzdoBuildId,
manifestBuildData: _defaultManifestBuildData,
repoUri: _testAzdoRepoUri,
repoBranch: _testBuildBranch,
repoCommit: _testBuildCommit,
repoOrigin: _testRepoOrigin,
isStableBuild: true,
publishingVersion: PublishingInfraVersion.Latest,
isReleaseOnlyPackageVersion: false);

modelFromItems.Should().NotBeNull();
_taskLoggingHelper.HasLoggedErrors.Should().BeFalse();

modelFromItems.Artifacts.Blobs.Should().SatisfyRespectively(
blob =>
{
blob.Id.Should().Be(bobSymbolsExpectedId);
blob.NonShipping.Should().BeTrue();
blob.Attributes.Should().Contain("Id", bobSymbolsExpectedId);
blob.Attributes.Should().Contain("Category", "SMORKELER");
blob.Attributes.Should().Contain("NonShipping", "true");
blob.Attributes.Should().Contain("RepoOrigin", "runtime");
},
blob =>
{
blob.Id.Should().Be(zipArtifact);
blob.NonShipping.Should().BeFalse();
blob.Attributes.Should().Contain("Id", zipArtifact);
blob.Attributes.Should().Contain("ARandomBitOfMAD", string.Empty);
blob.Attributes.Should().Contain("RepoOrigin", _testRepoOrigin);
});

modelFromItems.Artifacts.Packages.Should().SatisfyRespectively(
package =>
{
package.Id.Should().Be("test-package-a");
package.Version.Should().Be("1.0.0");
package.NonShipping.Should().BeFalse();
package.Attributes.Should().Contain("Id", "test-package-a");
package.Attributes.Should().Contain("Version", "1.0.0");
package.Attributes.Should().Contain("ShouldWePushDaNorpKeg", "YES");
package.Attributes.Should().Contain("RepoOrigin", "runtime");
});

modelFromItems.Artifacts.Pdbs.Should().SatisfyRespectively(
pdb =>
{
pdb.Id.Should().Be(pdbArtifact);
pdb.NonShipping.Should().BeFalse();
pdb.Attributes.Should().Contain("NonShipping", "false");
pdb.Attributes.Should().Contain("Category", "PDB");
pdb.Attributes.Should().Contain("Id", pdbArtifact);
pdb.Attributes.Should().Contain("RepoOrigin", "runtime");
});
}

/// <summary>
/// Validates that errors are logged and null is returned when Kind metadata is missing from an artifact.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,17 @@ public BuildModel CreateModel(

var blobArtifacts = itemsToPushNoExcludes
.Where(i => i.GetMetadata(ArtifactKindMetadata).Equals(nameof(ArtifactKind.Blob), StringComparison.OrdinalIgnoreCase))
.Select(i => _blobArtifactModelFactory.CreateBlobArtifactModel(i, repoOrigin))
.Select(i => _blobArtifactModelFactory.CreateBlobArtifactModel(i, i.GetMetadata("RepoOrigin") is string origin and not "" ? origin : repoOrigin))
.Where(b => artifactVisibilitiesToInclude.HasFlag(b.Visibility));

var packageArtifacts = itemsToPushNoExcludes
.Where(i => i.GetMetadata(ArtifactKindMetadata).Equals(nameof(ArtifactKind.Package), StringComparison.OrdinalIgnoreCase))
.Select(i => _packageArtifactModelFactory.CreatePackageArtifactModel(i, repoOrigin))
.Select(i => _packageArtifactModelFactory.CreatePackageArtifactModel(i, i.GetMetadata("RepoOrigin") is string origin and not "" ? origin : repoOrigin))
.Where(b => artifactVisibilitiesToInclude.HasFlag(b.Visibility));

var pdbArtifacts = itemsToPushNoExcludes
.Where(i => i.GetMetadata(ArtifactKindMetadata).Equals(nameof(ArtifactKind.Pdb), StringComparison.OrdinalIgnoreCase))
.Select(i => _pdbArtifactModelFactory.CreatePdbArtifactModel(i, repoOrigin))
.Select(i => _pdbArtifactModelFactory.CreatePdbArtifactModel(i, i.GetMetadata("RepoOrigin") is string origin and not "" ? origin : repoOrigin))
.Where(b => artifactVisibilitiesToInclude.HasFlag(b.Visibility));

return CreateModel(
Expand Down
26 changes: 18 additions & 8 deletions src/Microsoft.DotNet.Build.Tasks.Feed/src/PushToBuildStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ public class PushToBuildStorage : MSBuildTaskBase

public bool PushToLocalStorage { get; set; }

/// <summary>
/// The final path for any packages published to <see cref="ShippingPackagesLocalStorageDir"/>
/// or <see cref="NonShippingPackagesLocalStorageDir"/> should have the artifact's RepoOrigin
/// appended as a subfolder to the published path.
/// </summary>
public bool PreserveRepoOrigin { get; set; }

public ITaskItem[] ArtifactVisibilitiesToPublish { get; set; }

/// <summary>
Expand Down Expand Up @@ -263,17 +270,20 @@ private void PushToLocalStorageOrAzDO(ArtifactModel artifactModel)
break;

case PackageArtifactModel _:
if (!artifactModel.NonShipping)
{
Directory.CreateDirectory(ShippingPackagesLocalStorageDir);
CopyFileAsHardLinkIfPossible(path, Path.Combine(ShippingPackagesLocalStorageDir, filename), true);
}
else
{
string packageDestinationPath = artifactModel.NonShipping
? NonShippingPackagesLocalStorageDir
: ShippingPackagesLocalStorageDir;

if (PreserveRepoOrigin)
{
Directory.CreateDirectory(NonShippingPackagesLocalStorageDir);
CopyFileAsHardLinkIfPossible(path, Path.Combine(NonShippingPackagesLocalStorageDir, filename), true);
packageDestinationPath = Path.Combine(packageDestinationPath, artifactModel.RepoOrigin);
Copy link
Preview

Copilot AI Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending artifactModel.RepoOrigin directly to file paths could lead to unintended directory traversal if it contains path separator characters. Consider sanitizing or validating the RepoOrigin value before using it in Path.Combine.

Suggested change
packageDestinationPath = Path.Combine(packageDestinationPath, artifactModel.RepoOrigin);
packageDestinationPath = Path.Combine(packageDestinationPath, SanitizeRepoOrigin(artifactModel.RepoOrigin));

Copilot uses AI. Check for mistakes.

}

Directory.CreateDirectory(packageDestinationPath);
CopyFileAsHardLinkIfPossible(path, Path.Combine(packageDestinationPath, filename), true);
break;
}

case BlobArtifactModel _:
string relativeBlobPath = artifactModel.Id;
Expand Down
Loading