diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishArtifactsInManifest.proj b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishArtifactsInManifest.proj
index 73f4f779fef..36aa5db71be 100644
--- a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishArtifactsInManifest.proj
+++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishArtifactsInManifest.proj
@@ -75,16 +75,11 @@
netcoreapp3.1
Publish
-
- 2
+
+ 3
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishSignedAssets.proj b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishSignedAssets.proj
index ce7e413169a..2f4d18b3c10 100644
--- a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishSignedAssets.proj
+++ b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/PublishSignedAssets.proj
@@ -11,12 +11,6 @@
- AzdoTargetFeedPAT : Required token to publish assets to feeds
-->
-
- netcoreapp3.1
- Publish
- dnceng
-
-
+
+ netcoreapp3.1
+ Publish
+ dnceng
+ internal
+ public
+
+
+
+
+ AzureDevOpsOrg="$(AzureDevOpsOrg)"
+ AzureDevOpsProject="$(AzureDevOpsProject)">
+ AzureDevOpsOrg="$(AzureDevOpsOrg)"
+ AzureDevOpsProject="$(AzureDevOpsProject)">
diff --git a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/SetupTargetFeeds.proj b/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/SetupTargetFeeds.proj
deleted file mode 100644
index 25aefb367f7..00000000000
--- a/src/Microsoft.DotNet.Arcade.Sdk/tools/SdkTasks/SetupTargetFeeds.proj
+++ /dev/null
@@ -1,333 +0,0 @@
-
-
-
-
- Publish
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json
- https://dotnetfeed.blob.core.windows.net/arcade-validation/index.json
- https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore/index.json
- https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore-tooling/index.json
- https://dotnetfeed.blob.core.windows.net/aspnet-entityframeworkcore/index.json
- https://dotnetfeed.blob.core.windows.net/aspnet-extensions/index.json
- https://dotnetfeed.blob.core.windows.net/dotnet-coreclr/index.json
- https://dotnetfeed.blob.core.windows.net/dotnet-sdk/index.json
- https://dotnetfeed.blob.core.windows.net/dotnet-tools-internal/index.json
- https://dotnetfeed.blob.core.windows.net/dotnet-toolset/index.json
- https://dotnetfeed.blob.core.windows.net/dotnet-windowsdesktop/index.json
- https://dotnetfeed.blob.core.windows.net/nuget-nugetclient/index.json
- https://dotnetfeed.blob.core.windows.net/aspnet-entityframework6/index.json
- https://dotnetfeed.blob.core.windows.net/aspnet-blazor/index.json
- https://dotnetfeed.blob.core.windows.net/dotnet-iot/index.json
- https://dotnetfeed.blob.core.windows.net/dotnet-experimental/index.json
- https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsArtifactFeed.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsArtifactFeed.cs
new file mode 100644
index 00000000000..200b8adcbe3
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsArtifactFeed.cs
@@ -0,0 +1,67 @@
+// 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;
+
+namespace Microsoft.DotNet.Build.Tasks.Feed
+{
+ ///
+ /// Represents the body of a request sent when creating a new feed.
+ ///
+ /// >
+ /// When creating a new feed, we want to set up permissions based on the org and project.
+ /// Right now, only dnceng's public and internal projects are supported.
+ /// New feeds automatically get the feed administrators and project collection administrators as owners,
+ /// but we want to automatically add some additional permissions so that the build services can push to them,
+ /// and organization users can read from them.
+ ///
+ /// Note that there are two ways of providing read access to the feed:
+ /// 1. Providing explicit access in the permissions list to the "Project Collection Valid Users"
+ /// 2. Updating the Local feed view to allow 'collection' users access.
+ ///
+ /// The second is probably preferrable from a an AzDO pattern and usage standpoint. BUT the AzDO API has a drawback where
+ /// the create feed operation cannot create the local view with the appropriate access. Instead, it must be updated after the
+ /// feed is created. This would be fine except that updating a feed's permissions requires administrative permissions, while
+ /// creating a feed only requires contributor permissions. This would require passing around a PAT with management permissions,
+ /// instead of just r/w permissions, which is not ideal.
+ ///
+ public class AzureDevOpsArtifactFeed
+ {
+ public AzureDevOpsArtifactFeed(string name, string organization, string project)
+ {
+ Name = name;
+ switch (organization)
+ {
+ case "dnceng":
+ switch (project)
+ {
+ case "public":
+ case "internal":
+ Permissions = new List
+ {
+ // Project Collection Build Service
+ new AzureDevOpsFeedPermission("Microsoft.TeamFoundation.ServiceIdentity;116cce53-b859-4624-9a95-934af41eccef:Build:7ea9116e-9fac-403d-b258-b31fcf1bb293", "contributor"),
+ // internal Build Service
+ new AzureDevOpsFeedPermission("Microsoft.TeamFoundation.ServiceIdentity;116cce53-b859-4624-9a95-934af41eccef:Build:b55de4ed-4b5a-4215-a8e4-0a0a5f71e7d8", "contributor"),
+ // Project administrators
+ new AzureDevOpsFeedPermission("Microsoft.TeamFoundation.Identity;S-1-9-1551374245-1349140002-2196814402-2899064621-3782482097-0-0-0-0-1", "administrator"),
+ // Project Collection value users (see class comment for info)
+ new AzureDevOpsFeedPermission("Microsoft.TeamFoundation.Identity;S-1-9-1551374245-3991166389-1514870082-2833517066-1601300440-0-0-0-0-3", "reader"),
+ };
+ break;
+ default:
+ throw new NotImplementedException($"Project '{project}' within organization '{organization}' contains no feed permissions information.");
+ }
+ break;
+ default:
+ throw new NotImplementedException($"Organization '{organization}' contains no feed permissions information.");
+
+ }
+ }
+
+ public string Name { get; set; }
+
+ public List Permissions { get; private set; }
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsFeedPermission.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsFeedPermission.cs
new file mode 100644
index 00000000000..f081ee5eb8e
--- /dev/null
+++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/AzureDevOpsFeedPermission.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.Build.Tasks.Feed
+{
+ public class AzureDevOpsFeedPermission
+ {
+ public AzureDevOpsFeedPermission(string identityDescriptor, string role)
+ {
+ IdentityDescriptor = identityDescriptor;
+ Role = role;
+ }
+
+ public string IdentityDescriptor { get; set; }
+
+ public string Role { get; set; }
+ }
+}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs
index 4b6cfd71bfa..49032c55350 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs
@@ -23,8 +23,24 @@ public class CreateAzureDevOpsFeed : MSBuild.Task
[Output]
public string TargetFeedName { get; set; }
+ ///
+ /// Organization that the feed should be created in
+ ///
+ [Required]
+ public string AzureDevOpsOrg { get; set; }
+
+ ///
+ /// Project that that feed should be created in. The public/internal visibility
+ /// of this project will determine whether the feed is public.
+ ///
+ [Required]
+ public string AzureDevOpsProject { get; set; }
+
+ ///
+ /// Personal access token used to authorize to the API and create the feed
+ ///
[Required]
- public bool IsInternal { get; set; }
+ public string AzureDevOpsPersonalAccessToken { get; set; }
public string RepositoryName { get; set; }
@@ -40,12 +56,9 @@ public class CreateAzureDevOpsFeed : MSBuild.Task
///
public string ContentIdentifier { get; set; }
- [Required]
- public string AzureDevOpsPersonalAccessToken { get; set; }
-
- public string AzureDevOpsFeedsApiVersion { get; set; } = "5.0-preview.1";
+ public string AzureDevOpsFeedsApiVersion { get; set; } = "5.1-preview.1";
- public string AzureDevOpsOrg { get; set; } = "dnceng";
+ public string LocalViewVisibility { get; set; } = "collection";
///
/// Number of characters from the commit SHA prefix that should be included in the feed name.
@@ -86,16 +99,19 @@ private async Task ExecuteAsync()
// or contain any of these: @ ~ ; { } ' + = , < > | / \ ? : & $ * " # [ ] %
string feedCompatibleRepositoryName = RepositoryName?.Replace('/', '-');
- string accessType = IsInternal ? "internal" : "public";
- string publicSegment = IsInternal ? string.Empty : "public/";
- string accessId = IsInternal ? "int" : "pub";
+ // For clarity, and compatibility with existing infrastructure, we include the feed visibility tag.
+ // This serves two purposes:
+ // 1. In nuget.config files (and elsewhere), the name at a glance can identify its visibility
+ // 2. Existing automation has knowledge of "darc-int" and "darc-pub" for purposes of injecting authentication for internal builds
+ // and managing the isolated feeds within the NuGet.config files.
+ string accessTag = GetFeedVisibilityTag(AzureDevOpsOrg, AzureDevOpsProject);
string extraContentInfo = !string.IsNullOrEmpty(ContentIdentifier) ? $"-{ContentIdentifier}" : "";
- string baseFeedName = FeedName ?? $"darc-{accessId}{extraContentInfo}-{feedCompatibleRepositoryName}-{CommitSha.Substring(0, ShaUsableLength)}";
+ string baseFeedName = FeedName ?? $"darc-{accessTag}{extraContentInfo}-{feedCompatibleRepositoryName}-{CommitSha.Substring(0, ShaUsableLength)}";
string versionedFeedName = baseFeedName;
bool needsUniqueName = false;
int subVersion = 0;
- Log.LogMessage(MessageImportance.High, $"Creating the new {accessType} Azure DevOps artifacts feed '{baseFeedName}'...");
+ Log.LogMessage(MessageImportance.High, $"Creating the new Azure DevOps artifacts feed '{baseFeedName}'...");
if (baseFeedName.Length > MaxLengthForAzDoFeedNames)
{
@@ -118,20 +134,24 @@ private async Task ExecuteAsync()
"Basic",
Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", AzureDevOpsPersonalAccessToken))));
- AzureDevOpsArtifactFeed newFeed = new AzureDevOpsArtifactFeed(versionedFeedName, AzureDevOpsOrg);
+ AzureDevOpsArtifactFeed newFeed = new AzureDevOpsArtifactFeed(versionedFeedName, AzureDevOpsOrg, AzureDevOpsProject);
- string body = JsonConvert.SerializeObject(newFeed, _serializerSettings);
+ string createBody = JsonConvert.SerializeObject(newFeed, _serializerSettings);
- HttpRequestMessage postMessage = new HttpRequestMessage(HttpMethod.Post, $"{publicSegment}_apis/packaging/feeds");
- postMessage.Content = new StringContent(body, Encoding.UTF8, "application/json");
- HttpResponseMessage response = await client.SendAsync(postMessage);
+ using HttpRequestMessage createFeedMessage = new HttpRequestMessage(HttpMethod.Post, $"{AzureDevOpsProject}/_apis/packaging/feeds");
+ createFeedMessage.Content = new StringContent(createBody, Encoding.UTF8, "application/json");
+ using HttpResponseMessage createFeedResponse = await client.SendAsync(createFeedMessage);
- if (response.StatusCode == HttpStatusCode.Created)
+ if (createFeedResponse.StatusCode == HttpStatusCode.Created)
{
needsUniqueName = false;
baseFeedName = versionedFeedName;
+
+ /// This is where we would potentially update the Local feed view with permissions to the organization's
+ /// valid users. But, see for more info on why this is not
+ /// done this way.
}
- else if (response.StatusCode == HttpStatusCode.Conflict)
+ else if (createFeedResponse.StatusCode == HttpStatusCode.Conflict)
{
versionedFeedName = $"{baseFeedName}-{++subVersion}";
needsUniqueName = true;
@@ -144,12 +164,12 @@ private async Task ExecuteAsync()
}
else
{
- throw new Exception($"Feed '{baseFeedName}' was not created. Request failed with status code {response.StatusCode}. Exception: {await response.Content.ReadAsStringAsync()}");
+ throw new Exception($"Feed '{baseFeedName}' was not created. Request failed with status code {createFeedResponse.StatusCode}. Exception: {await createFeedResponse.Content.ReadAsStringAsync()}");
}
}
} while (needsUniqueName);
- TargetFeedURL = $"https://pkgs.dev.azure.com/{AzureDevOpsOrg}/{publicSegment}_packaging/{baseFeedName}/nuget/v3/index.json";
+ TargetFeedURL = $"https://pkgs.dev.azure.com/{AzureDevOpsOrg}/{AzureDevOpsProject}/_packaging/{baseFeedName}/nuget/v3/index.json";
TargetFeedName = baseFeedName;
Log.LogMessage(MessageImportance.High, $"Feed '{TargetFeedURL}' created successfully!");
@@ -161,41 +181,31 @@ private async Task ExecuteAsync()
return !Log.HasLoggedErrors;
}
- }
-
- public class Permission
- {
- public Permission(string identityDescriptor, int role)
- {
- IdentityDescriptor = identityDescriptor;
- Role = role;
- }
- public string IdentityDescriptor { get; set; }
-
- public int Role { get; set; }
- }
-
- public class AzureDevOpsArtifactFeed
- {
- public AzureDevOpsArtifactFeed(string name, string organization)
+ ///
+ /// Returns a tag for feed visibility that will be added to the feed name
+ ///
+ /// Organization containing the feed
+ /// Project within containing the feed
+ /// Feed tag
+ ///
+ private string GetFeedVisibilityTag(string organization, string project)
{
- Name = name;
- if (organization == "dnceng")
+ switch (organization)
{
- Permissions = new List
- {
- // Mimic the permissions added to a feed when created in the browser
- new Permission("Microsoft.TeamFoundation.ServiceIdentity;116cce53-b859-4624-9a95-934af41eccef:Build:b55de4ed-4b5a-4215-a8e4-0a0a5f71e7d8", 3), // Project Collection Build Service
- new Permission("Microsoft.TeamFoundation.ServiceIdentity;116cce53-b859-4624-9a95-934af41eccef:Build:7ea9116e-9fac-403d-b258-b31fcf1bb293", 3), // internal Build Service
- new Permission("Microsoft.TeamFoundation.Identity;S-1-9-1551374245-1349140002-2196814402-2899064621-3782482097-0-0-0-0-1", 4), // Feed administrators
- new Permission("Microsoft.TeamFoundation.Identity;S-1-9-1551374245-1846651262-2896117056-2992157471-3474698899-1-2052915359-1158038602-2757432096-2854636005", 4) // Feed administrators and contributors
- };
+ case "dnceng":
+ switch (project)
+ {
+ case "internal":
+ return "int";
+ case "public":
+ return "pub";
+ default:
+ throw new NotImplementedException($"Project '{project}' within organization '{organization}' has no visibility mapping.");
+ }
+ default:
+ throw new NotImplementedException($"Organization '{organization}' has no visibility mapping.");
}
}
-
- public string Name { get; set; }
-
- public List Permissions { get; private set; }
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/model/SetupTargetFeedConfigV3.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/model/SetupTargetFeedConfigV3.cs
index 2296192100b..be97cf2f605 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/model/SetupTargetFeedConfigV3.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/model/SetupTargetFeedConfigV3.cs
@@ -30,6 +30,8 @@ public class SetupTargetFeedConfigV3 : SetupTargetFeedConfigBase
public TaskLoggingHelper Log { get; }
+ public string AzureDevOpsOrg => "dnceng";
+
public SetupTargetFeedConfigV3(
TargetChannelConfig targetChannelConfig,
bool isInternalBuild,
@@ -195,7 +197,8 @@ private void CreateStableSymbolsFeedIfNeeded()
var symbolsFeedTask = new CreateAzureDevOpsFeed()
{
BuildEngine = BuildEngine,
- IsInternal = IsInternalBuild,
+ AzureDevOpsOrg = AzureDevOpsOrg,
+ AzureDevOpsProject = IsInternalBuild ? "internal" : "public",
AzureDevOpsPersonalAccessToken = AzureDevOpsFeedsKey,
RepositoryName = RepositoryName,
CommitSha = CommitSha,
@@ -222,7 +225,8 @@ private void CreateStablePackagesFeedIfNeeded()
var packagesFeedTask = new CreateAzureDevOpsFeed()
{
BuildEngine = BuildEngine,
- IsInternal = IsInternalBuild,
+ AzureDevOpsOrg = AzureDevOpsOrg,
+ AzureDevOpsProject = IsInternalBuild ? "internal" : "public",
AzureDevOpsPersonalAccessToken = AzureDevOpsFeedsKey,
RepositoryName = RepositoryName,
CommitSha = CommitSha