diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 55170eb0c9..9d2285ddbf 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"microsoft.dnceng.secretmanager": {
- "version": "1.1.0-beta.24413.3",
+ "version": "1.1.0-beta.24420.1",
"commands": [
"secret-manager"
]
@@ -15,7 +15,7 @@
]
},
"microsoft.dnceng.configuration.bootstrap": {
- "version": "1.1.0-beta.24413.3",
+ "version": "1.1.0-beta.24420.1",
"commands": [
"bootstrap-dnceng-configuration"
]
diff --git a/.vault-config/maestrolocal.yaml b/.vault-config/maestrolocal.yaml
index dae4a9b0ef..f87f74e309 100644
--- a/.vault-config/maestrolocal.yaml
+++ b/.vault-config/maestrolocal.yaml
@@ -18,13 +18,3 @@ secrets:
hasPrivateKey: true
hasWebhookSecret: false
hasOAuthSecret: true
-
- dn-bot-dnceng-build-rw-code-rw-release-rw:
- type: azure-devops-access-token
- parameters:
- domainAccountName: dn-bot
- domainAccountSecret:
- location: helixkv
- name: dn-bot-account-redmond
- organizations: dnceng
- scopes: build_execute code_write release_execute
diff --git a/.vault-config/shared/maestro-secrets.yaml b/.vault-config/shared/maestro-secrets.yaml
index 544b4c9f99..38dbe367aa 100644
--- a/.vault-config/shared/maestro-secrets.yaml
+++ b/.vault-config/shared/maestro-secrets.yaml
@@ -4,13 +4,3 @@ github:
hasPrivateKey: true
hasWebhookSecret: true
hasOAuthSecret: true
-
-dn-bot-dnceng-build-rw-code-rw-release-rw:
- type: azure-devops-access-token
- parameters:
- domainAccountName: dn-bot
- domainAccountSecret:
- location: helixkv
- name: dn-bot-account-redmond
- organizations: dnceng
- scopes: build_execute code_write release_execute
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 592c06a4de..d8479a1a97 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
3.1.24
6.0.26
6.0.0
- 8.1.0
+ 8.2.0
true
true
@@ -18,7 +18,7 @@
-
+
@@ -150,6 +150,6 @@
-
+
diff --git a/arcade-services.sln b/arcade-services.sln
index 7c6548ad3a..2d491b556e 100644
--- a/arcade-services.sln
+++ b/arcade-services.sln
@@ -136,6 +136,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProductConstructionService.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProductConstructionService.DependencyFlow", "src\ProductConstructionService\ProductConstructionService.DependencyFlow\ProductConstructionService.DependencyFlow.csproj", "{E312686C-A134-486F-9F62-89CE6CA34702}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProductConstructionService.DependencyFlow.Tests", "test\ProductConstructionService.DependencyFlow.Tests\ProductConstructionService.DependencyFlow.Tests.csproj", "{045BBB97-5A75-443A-8447-2035CBC0549A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProductConstructionService.FeedCleaner", "src\ProductConstructionService\ProductConstructionService.FeedCleaner\ProductConstructionService.FeedCleaner.csproj", "{B5833185-DD07-4C64-A4DA-D8290294F7E7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProductConstructionService.FeedCleaner.Tests", "test\ProductConstructionService.FeedCleaner.Tests\ProductConstructionService.FeedCleaner.Tests.csproj", "{D94319F8-FCA0-495B-8B6E-190B4EEBEF93}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProductConstructionService.ScenarioTests", "test\ProductConstructionService.ScenarioTests\ProductConstructionService.ScenarioTests.csproj", "{12D91D30-EC50-4D2B-8D67-0C19FCD2303F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -651,6 +659,54 @@ Global
{E312686C-A134-486F-9F62-89CE6CA34702}.Release|x64.Build.0 = Release|Any CPU
{E312686C-A134-486F-9F62-89CE6CA34702}.Release|x86.ActiveCfg = Release|Any CPU
{E312686C-A134-486F-9F62-89CE6CA34702}.Release|x86.Build.0 = Release|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Debug|x64.Build.0 = Debug|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Debug|x86.Build.0 = Debug|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Release|x64.ActiveCfg = Release|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Release|x64.Build.0 = Release|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Release|x86.ActiveCfg = Release|Any CPU
+ {045BBB97-5A75-443A-8447-2035CBC0549A}.Release|x86.Build.0 = Release|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Debug|x64.Build.0 = Debug|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Debug|x86.Build.0 = Debug|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Release|x64.ActiveCfg = Release|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Release|x64.Build.0 = Release|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Release|x86.ActiveCfg = Release|Any CPU
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7}.Release|x86.Build.0 = Release|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Debug|x64.Build.0 = Debug|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Debug|x86.Build.0 = Debug|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Release|x64.ActiveCfg = Release|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Release|x64.Build.0 = Release|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Release|x86.ActiveCfg = Release|Any CPU
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93}.Release|x86.Build.0 = Release|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Debug|x64.Build.0 = Debug|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Debug|x86.Build.0 = Debug|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Release|x64.ActiveCfg = Release|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Release|x64.Build.0 = Release|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Release|x86.ActiveCfg = Release|Any CPU
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -702,6 +758,10 @@ Global
{90C7747B-EBEF-4CF5-92A7-7856A3A13CAA} = {243A4561-BF35-405A-AF12-AC57BB27796D}
{29A75658-2DC4-4E85-8A53-97198F00F28D} = {1A456CF0-C09A-4DE6-89CE-1110EED31180}
{E312686C-A134-486F-9F62-89CE6CA34702} = {243A4561-BF35-405A-AF12-AC57BB27796D}
+ {045BBB97-5A75-443A-8447-2035CBC0549A} = {1A456CF0-C09A-4DE6-89CE-1110EED31180}
+ {B5833185-DD07-4C64-A4DA-D8290294F7E7} = {243A4561-BF35-405A-AF12-AC57BB27796D}
+ {D94319F8-FCA0-495B-8B6E-190B4EEBEF93} = {1A456CF0-C09A-4DE6-89CE-1110EED31180}
+ {12D91D30-EC50-4D2B-8D67-0C19FCD2303F} = {1A456CF0-C09A-4DE6-89CE-1110EED31180}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {32B9C883-432E-4FC8-A1BF-090EB033DD5B}
diff --git a/azure-pipelines-product-construction-service.yml b/azure-pipelines-product-construction-service.yml
index a105ccd795..16bad7e91a 100644
--- a/azure-pipelines-product-construction-service.yml
+++ b/azure-pipelines-product-construction-service.yml
@@ -2,6 +2,7 @@
name: $(Date:yyyyMMdd)$(Rev:r)
trigger:
+ batch: true
branches:
include:
- main
@@ -12,6 +13,7 @@ pr:
- main
variables:
+# https://dev.azure.com/dnceng/internal/_library?itemType=VariableGroups&view=VariableGroupView&variableGroupId=189
- group: Publish-Build-Assets
- name: resourceGroupName
value: product-construction-service
@@ -20,15 +22,18 @@ variables:
- name: diffFolder
value: $(Build.ArtifactStagingDirectory)/diff
- ${{ if ne(variables['Build.SourceBranch'], 'refs/heads/production') }}:
- # https://dev.azure.com/dnceng/internal/_library?itemType=VariableGroups&view=VariableGroupView&variableGroupId=189
+ - name: subscriptionId
+ value: e6b5f9f5-0ca4-4351-879b-014d78400ec2
- name: containerappName
value: product-construction-int
- name: containerjobNames
- value: sub-triggerer-twicedaily-int,sub-triggerer-daily-int,sub-triggerer-weekly-int,longest-path-updater-job-int
+ value: sub-triggerer-twicedaily-int,sub-triggerer-daily-int,sub-triggerer-weekly-int,longest-path-updater-job-int,feed-cleaner-int
- name: containerRegistryName
value: productconstructionint
- name: containerappEnvironmentName
value: product-construction-service-env-int
+ - name: containerappWorkspaceName
+ value: product-construction-service-workspace-int
- name: dockerRegistryUrl
value: productconstructionint.azurecr.io
- name: serviceConnectionName
@@ -65,6 +70,17 @@ stages:
- ${{ if notin(variables['Build.Reason'], 'PullRequest') }}:
- ${{ if ne(variables['Build.SourceBranch'], 'refs/heads/production') }}:
+ - task: AzureCLI@2
+ inputs:
+ azureSubscription: $(serviceConnectionName)
+ scriptType: pscore
+ scriptLocation: inlineScript
+ inlineScript: |
+ New-Item -ItemType Directory -Path $(diffFolder)
+ $before = az containerapp show --name $(containerappName) -g $(resourceGroupName) --output json
+ Set-Content -Path $(diffFolder)/before.json -Value $before
+ displayName: Snapshot configuration (before)
+
- task: AzureCLI@2
name: GetAuthInfo
displayName: Get PCS Token
@@ -85,8 +101,10 @@ stages:
scriptLocation: scriptPath
scriptPath: $(Build.SourcesDirectory)/eng/deployment/product-construction-service-deploy.ps1
arguments: >
+ -subscriptionId $(subscriptionId)
-resourceGroupName $(resourceGroupName)
-containerappName $(containerappName)
+ -workspaceName $(containerappWorkspaceName)
-newImageTag $(DockerTag.newDockerImageTag)
-containerRegistryName $(containerRegistryName)
-imageName $(containerName)
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 764d2dcfc8..064556356b 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -91,37 +91,37 @@
-
+
https://github.com/dotnet/arcade
- 51321b7e150a2f426cb9e1334687bdfab68ec323
+ 80264e60280e2815e7d65871081ccac04a32445c
-
+
https://github.com/dotnet/arcade
- 51321b7e150a2f426cb9e1334687bdfab68ec323
+ 80264e60280e2815e7d65871081ccac04a32445c
-
+
https://github.com/dotnet/arcade
- 51321b7e150a2f426cb9e1334687bdfab68ec323
+ 80264e60280e2815e7d65871081ccac04a32445c
-
+
https://github.com/dotnet/arcade
- 51321b7e150a2f426cb9e1334687bdfab68ec323
+ 80264e60280e2815e7d65871081ccac04a32445c
-
+
https://github.com/dotnet/arcade
- 51321b7e150a2f426cb9e1334687bdfab68ec323
+ 80264e60280e2815e7d65871081ccac04a32445c
-
+
https://github.com/dotnet/arcade
- 51321b7e150a2f426cb9e1334687bdfab68ec323
+ 80264e60280e2815e7d65871081ccac04a32445c
-
+
https://github.com/dotnet/dnceng
- a06a815e1a599e209b49b37667074e3504810e67
+ a0d339b12f248ca5b42d3a53a49c6b09ef1db579
-
+
https://github.com/dotnet/dnceng
- a06a815e1a599e209b49b37667074e3504810e67
+ a0d339b12f248ca5b42d3a53a49c6b09ef1db579
diff --git a/eng/Versions.props b/eng/Versions.props
index 33959d5b5f..6b2f39315b 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -9,11 +9,11 @@
true
1.0.0-preview.1
- 8.0.0-beta.24413.2
- 8.0.0-beta.24413.2
- 8.0.0-beta.24413.2
- 8.0.0-beta.24413.2
- 8.0.0-beta.24413.2
+ 8.0.0-beta.24426.2
+ 8.0.0-beta.24426.2
+ 8.0.0-beta.24426.2
+ 8.0.0-beta.24426.2
+ 8.0.0-beta.24426.2
17.4.1
1.1.0-beta.24376.1
1.1.0-beta.24376.1
@@ -37,8 +37,8 @@
1.1.0-beta.24376.1
1.1.0-beta.24376.1
1.1.0-beta.24376.1
- 1.1.0-beta.24413.3
- 1.1.0-beta.24413.3
+ 1.1.0-beta.24420.1
+ 1.1.0-beta.24420.1
diff --git a/eng/common/templates/steps/telemetry-start.yml b/eng/common/templates/steps/telemetry-start.yml
index 32c01ef0b5..6abbcb33a6 100644
--- a/eng/common/templates/steps/telemetry-start.yml
+++ b/eng/common/templates/steps/telemetry-start.yml
@@ -8,7 +8,7 @@ parameters:
steps:
- ${{ if and(eq(parameters.runAsPublic, 'false'), not(eq(variables['System.TeamProject'], 'public'))) }}:
- - task: AzureKeyVault@1
+ - task: AzureKeyVault@2
inputs:
azureSubscription: 'HelixProd_KeyVault'
KeyVaultName: HelixProdKV
diff --git a/eng/deployment/product-construction-service-deploy.ps1 b/eng/deployment/product-construction-service-deploy.ps1
index 50fb761df3..1c4ac33fbc 100644
--- a/eng/deployment/product-construction-service-deploy.ps1
+++ b/eng/deployment/product-construction-service-deploy.ps1
@@ -2,8 +2,10 @@
# The script determines the color of the currently active revision, deactivates the old inactive revision,
# and deploys the new revision, switching all traffic to it if the health probes pass.
param(
+ [Parameter(Mandatory=$true)][string]$subscriptionId,
[Parameter(Mandatory=$true)][string]$resourceGroupName,
[Parameter(Mandatory=$true)][string]$containerappName,
+ [Parameter(Mandatory=$true)][string]$workspaceName,
[Parameter(Mandatory=$true)][string]$newImageTag,
[Parameter(Mandatory=$true)][string]$containerRegistryName,
[Parameter(Mandatory=$true)][string]$imageName,
@@ -52,12 +54,39 @@ function StopAndWait([string]$pcsStatusUrl, [string]$pcsStopUrl, [hashtable]$aut
return
}
+function GetLogsLink {
+ param (
+ [string]$revisionName,
+ [string]$resourceGroup,
+ [string]$workspaceName,
+ [string]$subscriptionId
+ )
+
+ $query = "ContainerAppConsoleLogs_CL `
+| where RevisionName_s == '$revisionName' `
+| project TimeGenerated, Log_s"
+
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($query)
+ $memoryStream = New-Object System.IO.MemoryStream
+ $compressedStream = New-Object System.IO.Compression.GZipStream($memoryStream, [System.IO.Compression.CompressionMode]::Compress, $true)
+
+ $compressedStream.Write($bytes, 0, $bytes.Length)
+ $compressedStream.Close()
+ $memoryStream.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
+ $data = $memoryStream.ToArray()
+ $encodedQuery = [Convert]::ToBase64String($data)
+ $encodedQuery = [System.Web.HttpUtility]::UrlEncode($encodedQuery)
+ return "https://ms.portal.azure.com#@72f988bf-86f1-41af-91ab-2d7cd011db47/blade/Microsoft_OperationsManagementSuite_Workspace/Logs.ReactView/" +
+ "resourceId/%2Fsubscriptions%2F$subscriptionId%2FresourceGroups%2F$resourceGroup%2Fproviders%2FMicrosoft.OperationalInsights%2Fworkspaces%2F" +
+ "$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/q/$encodedQuery/timespan/P1D/limit/1000"
+}
+
az extension add --name containerapp --upgrade
Write-Host "Fetching all revisions to determine the active label"
$containerappTraffic = az containerapp ingress traffic show --name $containerappName --resource-group $resourceGroupName | ConvertFrom-Json
# find the currently active revision
-$activeRevision = $containerappTraffic | Where-Object { $_.weight -eq 100 }
+$activeRevision = $containerappTraffic | Where-Object { $_.weight -eq 100 }
Write-Host "Currently active revision: $($activeRevision.revisionName) with label $($activeRevision.label)"
@@ -125,7 +154,16 @@ try
Write-Host "All traffic has been redirected to label $inactiveLabel"
}
else {
- Write-Warning "New revision is not running. Check revision $newRevisionName logs in the inactive revisions. Deactivating the new revision"
+ Write-Warning "New revision failed to start. Deactivating the new revision.."
+ $link = GetLogsLink `
+ -revisionName "$newRevisionName" `
+ -subscriptionId "$subscriptionId" `
+ -resourceGroup "$resourceGroupName" `
+ -workspaceName "$workspaceName"
+
+ Write-Host " Check revision $newRevisionName logs in the inactive revision: `
+ $link"
+
az containerapp revision deactivate --revision $newRevisionName --name $containerappName --resource-group $resourceGroupName
exit 1
}
diff --git a/eng/service-templates/ProductConstructionService/provision.bicep b/eng/service-templates/ProductConstructionService/provision.bicep
index 95b2017316..c552213a2d 100644
--- a/eng/service-templates/ProductConstructionService/provision.bicep
+++ b/eng/service-templates/ProductConstructionService/provision.bicep
@@ -52,7 +52,7 @@ param pcsIdentityName string = 'ProductConstructionServiceInt'
param deploymentIdentityName string = 'ProductConstructionServiceDeploymentInt'
@description('Bicep requires an image when creating a containerapp. Using a dummy image for that.')
-var containerImageName = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
+param containerImageName = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
@description('Virtual network name')
param virtualNetworkName string = 'product-construction-service-vnet-int'
@@ -78,11 +78,17 @@ param longestBuildPathUpdaterIdentityName string = 'LongestBuildPathUpdaterInt'
@description('Longest Build Path Updater Job Name')
param longestBuildPathUpdaterJobName string = 'longest-path-updater-job-int'
+@description('Feed Cleaner Job name')
+param feedCleanerJobName string = 'feed-cleaner-int'
+
+@description('Feed Cleaner Identity name')
+param feedCleanerIdentityName string = 'FeedCleanerInt'
+
@description('Network security group name')
-var networkSecurityGroupName = 'product-construction-service-nsg-int'
+param networkSecurityGroupName string = 'product-construction-service-nsg-int'
@description('Resource group where PCS IP resources will be created')
-var infrastructureResourceGroupName = 'product-construction-service-ip-int'
+param infrastructureResourceGroupName string = 'product-construction-service-ip-int'
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
name: networkSecurityGroupName
@@ -357,23 +363,28 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-pr
resource deploymentIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
- name: deploymentIdentityName
- location: location
+ name: deploymentIdentityName
+ location: location
}
resource pcsIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
- name: pcsIdentityName
- location: location
+ name: pcsIdentityName
+ location: location
}
resource subscriptionTriggererIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
- name: subscriptionTriggererIdentityName
- location: location
+ name: subscriptionTriggererIdentityName
+ location: location
}
resource longestBuildPathUpdaterIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
- name: longestBuildPathUpdaterIdentityName
- location: location
+ name: longestBuildPathUpdaterIdentityName
+ location: location
+}
+
+resource feedCleanerIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
+ name: feedCleanerIdentityName
+ location: location
}
// allow acr pulls to the identity used for the aca's
@@ -409,6 +420,17 @@ resource longestBuildPathUpdaterIdentityAcrPull 'Microsoft.Authorization/roleAss
}
}
+// allow acr pulls to the identity used for the feed cleaner
+resource feedCleanerIdentityAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: containerRegistry // Use when specifying a scope that is different than the deployment scope
+ name: guid(subscription().id, resourceGroup().id, acrPullRole)
+ properties: {
+ roleDefinitionId: acrPullRole
+ principalType: 'ServicePrincipal'
+ principalId: feedCleanerIdentity.properties.principalId
+ }
+}
+
// azure system role for setting up acr pull access
var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
@@ -418,12 +440,14 @@ var acrPushRole = subscriptionResourceId('Microsoft.Authorization/roleDefinition
var kvSecretUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
// azure system role for setting storage queue access
var storageQueueContrubutorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
-// azure system role for setting controbutor access
+// azure system role for setting contributor access
var contributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
// azure system role Key Vault Reader
var keyVaultReaderRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '21090545-7ca7-4776-b22c-e363652d74d2')
// storage account blob contributor
var blobContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
+// Key Vault Crypto User role
+var kvCryptoUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '12338af0-0e69-4776-bea7-57ae8d297424')
// application insights for service logging
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
@@ -576,7 +600,7 @@ module subscriptionTriggererTwiceDaily 'scheduledContainerJob.bicep' = {
containerRegistryName: containerRegistryName
containerAppsEnvironmentId: containerAppsEnvironment.id
containerImageName: containerImageName
- dllFullPath: '/app/SubscriptionTriggerer/SubscriptionTriggerer.dll'
+ dllFullPath: '/app/SubscriptionTriggerer/ProductConstructionService.SubscriptionTriggerer.dll'
argument: 'twicedaily'
contributorRoleId: contributorRole
deploymentIdentityPrincipalId: deploymentIdentity.properties.principalId
@@ -598,7 +622,7 @@ module subscriptionTriggererDaily 'scheduledContainerJob.bicep' = {
containerRegistryName: containerRegistryName
containerAppsEnvironmentId: containerAppsEnvironment.id
containerImageName: containerImageName
- dllFullPath: '/app/SubscriptionTriggerer/SubscriptionTriggerer.dll'
+ dllFullPath: '/app/SubscriptionTriggerer/ProductConstructionService.SubscriptionTriggerer.dll'
argument: 'daily'
contributorRoleId: contributorRole
deploymentIdentityPrincipalId: deploymentIdentity.properties.principalId
@@ -620,7 +644,7 @@ module subscriptionTriggererWeekly 'scheduledContainerJob.bicep' = {
containerRegistryName: containerRegistryName
containerAppsEnvironmentId: containerAppsEnvironment.id
containerImageName: containerImageName
- dllFullPath: '/app/SubscriptionTriggerer/SubscriptionTriggerer.dll'
+ dllFullPath: '/app/SubscriptionTriggerer/ProductConstructionService.SubscriptionTriggerer.dll'
argument: 'weekly'
contributorRoleId: contributorRole
deploymentIdentityPrincipalId: deploymentIdentity.properties.principalId
@@ -642,7 +666,7 @@ module longestBuildPathUpdater 'scheduledContainerJob.bicep' = {
containerRegistryName: containerRegistryName
containerAppsEnvironmentId: containerAppsEnvironment.id
containerImageName: containerImageName
- dllFullPath: '/app/LongestBuildPathUpdater/LongestBuildPathUpdater.dll'
+ dllFullPath: '/app/LongestBuildPathUpdater/ProductConstructionService.LongestBuildPathUpdater.dll'
argument: ''
contributorRoleId: contributorRole
deploymentIdentityPrincipalId: deploymentIdentity.properties.principalId
@@ -653,6 +677,28 @@ module longestBuildPathUpdater 'scheduledContainerJob.bicep' = {
]
}
+module feedCleaner 'scheduledContainerJob.bicep' = {
+ name: 'feedCleaner'
+ params: {
+ jobName: feedCleanerJobName
+ location: location
+ aspnetcoreEnvironment: aspnetcoreEnvironment
+ applicationInsightsConnectionString: applicationInsights.properties.ConnectionString
+ userAssignedIdentityId: feedCleanerIdentity.id
+ cronSchedule: '0 2 * * *'
+ containerRegistryName: containerRegistryName
+ containerAppsEnvironmentId: containerAppsEnvironment.id
+ containerImageName: containerImageName
+ dllFullPath: '/app/FeedCleaner/ProductConstructionService.FeedCleaner.dll'
+ contributorRoleId: contributorRole
+ deploymentIdentityPrincipalId: deploymentIdentity.properties.principalId
+ }
+ dependsOn: [
+ feedCleanerIdentityAcrPull
+ longestBuildPathUpdater
+ ]
+}
+
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
name: keyVaultName
location: location
@@ -715,6 +761,18 @@ resource secretAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
}
}
+// allow crypto access to the identity used for the aca's
+resource cryptoAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: keyVault // Use when specifying a scope that is different than the deployment scope
+ name: guid(subscription().id, resourceGroup().id, kvCryptoUserRole)
+ properties: {
+ roleDefinitionId: kvCryptoUserRole
+ principalType: 'ServicePrincipal'
+ principalId: pcsIdentity.properties.principalId
+ }
+}
+
+
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
name: storageAccountName
location: location
diff --git a/eng/service-templates/ProductConstructionService/scheduledContainerJob.bicep b/eng/service-templates/ProductConstructionService/scheduledContainerJob.bicep
index 8815949f85..0244ed32ec 100644
--- a/eng/service-templates/ProductConstructionService/scheduledContainerJob.bicep
+++ b/eng/service-templates/ProductConstructionService/scheduledContainerJob.bicep
@@ -8,7 +8,7 @@ param containerRegistryName string
param containerAppsEnvironmentId string
param containerImageName string
param dllFullPath string
-param argument string
+param argument string = ''
param contributorRoleId string
param deploymentIdentityPrincipalId string
diff --git a/eng/templates/jobs/e2e-tests.yml b/eng/templates/jobs/e2e-tests.yml
index a1aec97511..e5e2fea754 100644
--- a/eng/templates/jobs/e2e-tests.yml
+++ b/eng/templates/jobs/e2e-tests.yml
@@ -96,6 +96,11 @@ jobs:
.\darc\darc.exe get-default-channels --source-repo arcade-services --ci --password "$(GetAuthInfo.Token)" --bar-uri "$(GetAuthInfo.BarUri)" --debug
displayName: Test BAR token auth
+ - template: /eng/common/templates-official/steps/get-federated-access-token.yml
+ parameters:
+ federatedServiceConnection: "ArcadeServicesInternal"
+ outputVariableName: "AzdoToken"
+
- task: DotNetCoreCLI@2
displayName: Run E2E tests
inputs:
@@ -115,7 +120,7 @@ jobs:
MAESTRO_BASEURIS: ${{ variables.MaestroTestEndpoints }}
MAESTRO_TOKEN: $(GetAuthInfo.Token)
GITHUB_TOKEN: $(maestro-scenario-test-github-token)
- AZDO_TOKEN: $(dn-bot-dnceng-build-rw-code-rw-release-rw)
+ AZDO_TOKEN: $(AzdoToken)
DARC_PACKAGE_SOURCE: $(Pipeline.Workspace)\PackageArtifacts
DARC_DIR: $(Build.SourcesDirectory)\darc
DARC_IS_CI: true
diff --git a/eng/templates/stages/deploy.yaml b/eng/templates/stages/deploy.yaml
index 1f9fe168d6..311812c1c8 100644
--- a/eng/templates/stages/deploy.yaml
+++ b/eng/templates/stages/deploy.yaml
@@ -3,7 +3,6 @@ parameters:
type: boolean
# --- Secret Variable group requirements ---
-# dn-bot-dnceng-build-rw-code-rw-release-rw
# maestro-scenario-test-github-token
stages:
diff --git a/global.json b/global.json
index 775b5d6bef..6cb5cb8183 100644
--- a/global.json
+++ b/global.json
@@ -15,6 +15,6 @@
}
},
"msbuild-sdks": {
- "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24413.2"
+ "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24426.2"
}
}
diff --git a/src/Maestro/Client/src/MaestroApiFactory.cs b/src/Maestro/Client/src/MaestroApiFactory.cs
index bc60def6d6..a2cf94ffd5 100644
--- a/src/Maestro/Client/src/MaestroApiFactory.cs
+++ b/src/Maestro/Client/src/MaestroApiFactory.cs
@@ -40,7 +40,7 @@ public static IMaestroApi GetAuthenticated(
bool disableInteractiveAuth)
{
return new MaestroApi(new MaestroApiOptions(
- MaestroApiOptions.StagingBuildAssetRegistryBaseUri,
+ MaestroApiOptions.StagingMaestroUri,
accessToken,
managedIdentityId,
disableInteractiveAuth));
diff --git a/src/Maestro/Client/src/MaestroApiOptions.cs b/src/Maestro/Client/src/MaestroApiOptions.cs
index bba86e5697..d6d549c4ba 100644
--- a/src/Maestro/Client/src/MaestroApiOptions.cs
+++ b/src/Maestro/Client/src/MaestroApiOptions.cs
@@ -11,26 +11,31 @@ namespace Microsoft.DotNet.Maestro.Client
{
public partial class MaestroApiOptions
{
- public const string ProductionBuildAssetRegistryBaseUri = "https://maestro.dot.net/";
- public const string OldProductionBuildAssetRegistryBaseUri = "https://maestro-prod.westus2.cloudapp.azure.com/";
-
// https://ms.portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/54c17f3d-7325-4eca-9db7-f090bfc765a8/isMSAApp~/false
private const string MaestroProductionAppId = "54c17f3d-7325-4eca-9db7-f090bfc765a8";
- public const string StagingBuildAssetRegistryBaseUri = "https://maestro.int-dot.net/";
- public const string OldStagingBuildAssetRegistryBaseUri = "https://maestro-int.westus2.cloudapp.azure.com/";
-
// https://ms.portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/baf98f1b-374e-487d-af42-aa33807f11e4/isMSAApp~/false
private const string MaestroStagingAppId = "baf98f1b-374e-487d-af42-aa33807f11e4";
+ public const string ProductionMaestroUri = "https://maestro.dot.net/";
+ public const string OldProductionMaestroUri = "https://maestro-prod.westus2.cloudapp.azure.com/";
+
+ public const string StagingMaestroUri = "https://maestro.int-dot.net/";
+ public const string OldPcsStagingUri = "https://maestro-int.westus2.cloudapp.azure.com/";
+ public const string PcsStagingUri = "https://product-construction-int.delightfuldune-c0f01ab0.westus2.azurecontainerapps.io/";
+ public const string PcsLocalUri = "https://localhost:53180/";
+
private const string APP_USER_SCOPE = "Maestro.User";
private static readonly Dictionary EntraAppIds = new Dictionary
{
- [StagingBuildAssetRegistryBaseUri.TrimEnd('/')] = MaestroStagingAppId,
- [OldStagingBuildAssetRegistryBaseUri.TrimEnd('/')] = MaestroStagingAppId,
- [ProductionBuildAssetRegistryBaseUri.TrimEnd('/')] = MaestroProductionAppId,
- [OldProductionBuildAssetRegistryBaseUri.TrimEnd('/')] = MaestroProductionAppId,
+ [StagingMaestroUri.TrimEnd('/')] = MaestroStagingAppId,
+ [OldPcsStagingUri.TrimEnd('/')] = MaestroStagingAppId,
+ [PcsStagingUri.TrimEnd('/')] = MaestroStagingAppId,
+ [PcsLocalUri.TrimEnd('/')] = MaestroStagingAppId,
+
+ [ProductionMaestroUri.TrimEnd('/')] = MaestroProductionAppId,
+ [OldProductionMaestroUri.TrimEnd('/')] = MaestroProductionAppId,
};
///
@@ -44,7 +49,7 @@ public MaestroApiOptions(string baseUri, string accessToken, string managedIdent
: this(
new Uri(baseUri),
AppCredentialResolver.CreateCredential(
- new AppCredentialResolverOptions(EntraAppIds[(baseUri ?? ProductionBuildAssetRegistryBaseUri).TrimEnd('/')])
+ new AppCredentialResolverOptions(EntraAppIds[(baseUri ?? ProductionMaestroUri).TrimEnd('/')])
{
DisableInteractiveAuth = disableInteractiveAuth,
Token = accessToken,
diff --git a/src/Maestro/DependencyUpdater/DependencyUpdater.cs b/src/Maestro/DependencyUpdater/DependencyUpdater.cs
index 4e4a76b63a..f34e74734b 100644
--- a/src/Maestro/DependencyUpdater/DependencyUpdater.cs
+++ b/src/Maestro/DependencyUpdater/DependencyUpdater.cs
@@ -42,6 +42,8 @@ public sealed class DependencyUpdater : IServiceImplementation, IDependencyUpdat
private readonly ILogger _logger;
private readonly BuildAssetRegistryContext _context;
private readonly IActorProxyFactory _subscriptionActorFactory;
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ private readonly SubscriptionIdGenerator _subscriptionIdGenerator;
public DependencyUpdater(
IReliableStateManager stateManager,
@@ -49,7 +51,8 @@ public DependencyUpdater(
BuildAssetRegistryContext context,
IBasicBarClient barClient,
IActorProxyFactory subscriptionActorFactory,
- OperationManager operations)
+ OperationManager operations,
+ SubscriptionIdGenerator subscriptionIdGenerator)
{
_operations = operations;
_stateManager = stateManager;
@@ -57,6 +60,7 @@ public DependencyUpdater(
_context = context;
_barClient = barClient;
_subscriptionActorFactory = subscriptionActorFactory;
+ _subscriptionIdGenerator = subscriptionIdGenerator;
}
public async Task StartUpdateDependenciesAsync(int buildId, int channelId)
@@ -330,10 +334,16 @@ where sub.Enabled
private async Task UpdateSubscriptionAsync(Guid subscriptionId, int buildId)
{
+ if (!_subscriptionIdGenerator.ShouldTriggerSubscription(subscriptionId))
+ {
+ _logger.LogInformation("Skipping subscription '{subscriptionId}', Maestro won't trigger PCS subscriptions", subscriptionId);
+ return;
+ }
+
using (_operations.BeginOperation(
- "Updating subscription '{subscriptionId}' with build '{buildId}'",
- subscriptionId,
- buildId))
+ "Updating subscription '{subscriptionId}' with build '{buildId}'",
+ subscriptionId,
+ buildId))
{
try
{
diff --git a/src/Maestro/DependencyUpdater/Program.cs b/src/Maestro/DependencyUpdater/Program.cs
index 8c3793a118..70bec120a5 100644
--- a/src/Maestro/DependencyUpdater/Program.cs
+++ b/src/Maestro/DependencyUpdater/Program.cs
@@ -63,5 +63,7 @@ public static void Configure(IServiceCollection services)
services.AddScoped();
services.AddTransient();
services.AddKustoClientProvider("Kusto");
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ services.AddSingleton(sp => new(RunningService.Maestro));
}
}
diff --git a/src/Maestro/FeedCleanerService/FeedCleanerService.cs b/src/Maestro/FeedCleanerService/FeedCleanerService.cs
index 3f540f3002..0b51be8dd7 100644
--- a/src/Maestro/FeedCleanerService/FeedCleanerService.cs
+++ b/src/Maestro/FeedCleanerService/FeedCleanerService.cs
@@ -68,7 +68,16 @@ public async Task CleanManagedFeedsAsync()
foreach (var azdoAccount in Options.AzdoAccounts)
{
- List allFeeds = await _azureDevOpsClient.GetFeedsAsync(azdoAccount);
+ List allFeeds;
+ try
+ {
+ allFeeds = await _azureDevOpsClient.GetFeedsAsync(azdoAccount);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, $"Failed to get feeds for account {azdoAccount}");
+ continue;
+ }
IEnumerable managedFeeds = allFeeds.Where(f => Regex.IsMatch(f.Name, FeedConstants.MaestroManagedFeedNamePattern));
foreach (var feed in managedFeeds)
diff --git a/src/Maestro/Maestro.Data/SubscriptionIdGenerator.cs b/src/Maestro/Maestro.Data/SubscriptionIdGenerator.cs
new file mode 100644
index 0000000000..0741d6dc84
--- /dev/null
+++ b/src/Maestro/Maestro.Data/SubscriptionIdGenerator.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Maestro.Data;
+
+public enum RunningService
+{
+ Maestro,
+ PCS
+}
+
+public class SubscriptionIdGenerator(RunningService runningService)
+{
+ private readonly RunningService _runningService = runningService;
+
+ public const string PcsSubscriptionIdPrefix = "00000000";
+
+ public Guid GenerateSubscriptionId()
+ {
+ if (_runningService == RunningService.PCS)
+ {
+ return Guid.Parse($"{PcsSubscriptionIdPrefix}{Guid.NewGuid().ToString().Substring(PcsSubscriptionIdPrefix.Length)}");
+ }
+
+ var guid = Guid.NewGuid();
+
+ if (guid.ToString().StartsWith(PcsSubscriptionIdPrefix))
+ {
+ return GenerateSubscriptionId();
+ }
+
+ return guid;
+ }
+
+ public bool ShouldTriggerSubscription(Guid subscriptionId)
+ {
+ bool startsWithPcsSubscriptionIdPrefix = subscriptionId.ToString().StartsWith(PcsSubscriptionIdPrefix);
+ bool runningServiceIsPCS = _runningService == RunningService.PCS;
+ return runningServiceIsPCS == startsWithPcsSubscriptionIdPrefix;
+ }
+}
diff --git a/src/Maestro/Maestro.MergePolicies/ValidateCoherencyMergePolicy.cs b/src/Maestro/Maestro.MergePolicies/ValidateCoherencyMergePolicy.cs
index 23f09e7d55..8720c27426 100644
--- a/src/Maestro/Maestro.MergePolicies/ValidateCoherencyMergePolicy.cs
+++ b/src/Maestro/Maestro.MergePolicies/ValidateCoherencyMergePolicy.cs
@@ -21,7 +21,7 @@ public override Task EvaluateAsync(IPullRequest pr,
return Task.FromResult(Succeed("Coherency check successful."));
var description = new StringBuilder("Coherency update failed for the following dependencies:");
- foreach (CoherencyErrorDetails error in pr.CoherencyErrors ?? Enumerable.Empty())
+ foreach (CoherencyErrorDetails error in pr.CoherencyErrors ?? [])
{
description.Append("\n * ").Append(error.Error);
diff --git a/src/Maestro/Maestro.Web/Api/v2018_07_16/Controllers/SubscriptionsController.cs b/src/Maestro/Maestro.Web/Api/v2018_07_16/Controllers/SubscriptionsController.cs
index 65588aa472..65aa678637 100644
--- a/src/Maestro/Maestro.Web/Api/v2018_07_16/Controllers/SubscriptionsController.cs
+++ b/src/Maestro/Maestro.Web/Api/v2018_07_16/Controllers/SubscriptionsController.cs
@@ -32,13 +32,16 @@ public class SubscriptionsController : Controller
{
private readonly BuildAssetRegistryContext _context;
private readonly IBackgroundQueue _queue;
+ protected readonly SubscriptionIdGenerator _subscriptionIdGenerator;
public SubscriptionsController(
BuildAssetRegistryContext context,
- IBackgroundQueue queue)
+ IBackgroundQueue queue,
+ SubscriptionIdGenerator subscriptionIdGenerator)
{
_context = context;
_queue = queue;
+ _subscriptionIdGenerator = subscriptionIdGenerator;
}
///
@@ -119,6 +122,11 @@ public virtual async Task TriggerSubscription(Guid id, [FromQuery
protected async Task TriggerSubscriptionCore(Guid id, int buildId)
{
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ if (!_subscriptionIdGenerator.ShouldTriggerSubscription(id))
+ {
+ return BadRequest("Maestro shouldn't trigger PCS subscriptions");
+ }
Data.Models.Subscription subscription = await _context.Subscriptions.Include(sub => sub.LastAppliedBuild)
.Include(sub => sub.Channel)
.FirstOrDefaultAsync(sub => sub.Id == id);
@@ -467,6 +475,8 @@ public virtual async Task Create([FromBody, Required] Subscriptio
Data.Models.Subscription subscriptionModel = subscription.ToDb();
subscriptionModel.Channel = channel;
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ subscriptionModel.Id = _subscriptionIdGenerator.GenerateSubscriptionId();
Data.Models.Subscription equivalentSubscription = await FindEquivalentSubscription(subscriptionModel);
if (equivalentSubscription != null)
diff --git a/src/Maestro/Maestro.Web/Api/v2019_01_16/Controllers/SubscriptionsController.cs b/src/Maestro/Maestro.Web/Api/v2019_01_16/Controllers/SubscriptionsController.cs
index 6aa9325f99..0a09d828b4 100644
--- a/src/Maestro/Maestro.Web/Api/v2019_01_16/Controllers/SubscriptionsController.cs
+++ b/src/Maestro/Maestro.Web/Api/v2019_01_16/Controllers/SubscriptionsController.cs
@@ -27,8 +27,9 @@ public class SubscriptionsController : v2018_07_16.Controllers.SubscriptionsCont
public SubscriptionsController(
BuildAssetRegistryContext context,
- IBackgroundQueue queue)
- : base(context, queue)
+ IBackgroundQueue queue,
+ SubscriptionIdGenerator subscriptionIdGenerator)
+ : base(context, queue, subscriptionIdGenerator)
{
_context = context;
}
@@ -274,7 +275,9 @@ public override async Task Create([FromBody, Required] Maestro.Ap
Data.Models.Subscription subscriptionModel = subscription.ToDb();
subscriptionModel.Channel = channel;
-
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ subscriptionModel.Id = _subscriptionIdGenerator.GenerateSubscriptionId();
+
// Check that we're not about add an existing subscription that is identical
Data.Models.Subscription equivalentSubscription = await FindEquivalentSubscription(subscriptionModel);
if (equivalentSubscription != null)
diff --git a/src/Maestro/Maestro.Web/Api/v2020_02_20/Controllers/SubscriptionsController.cs b/src/Maestro/Maestro.Web/Api/v2020_02_20/Controllers/SubscriptionsController.cs
index ff98c98d27..da09451bb8 100644
--- a/src/Maestro/Maestro.Web/Api/v2020_02_20/Controllers/SubscriptionsController.cs
+++ b/src/Maestro/Maestro.Web/Api/v2020_02_20/Controllers/SubscriptionsController.cs
@@ -33,8 +33,9 @@ public class SubscriptionsController : v2019_01_16.Controllers.SubscriptionsCont
public SubscriptionsController(
BuildAssetRegistryContext context,
IBackgroundQueue queue,
- IGitHubClientFactory gitHubClientFactory)
- : base(context, queue)
+ IGitHubClientFactory gitHubClientFactory,
+ SubscriptionIdGenerator subscriptionIdGenerator)
+ : base(context, queue, subscriptionIdGenerator)
{
_context = context;
_gitHubClientFactory = gitHubClientFactory;
@@ -444,6 +445,8 @@ public async Task Create([FromBody, Required] SubscriptionData su
Data.Models.Subscription subscriptionModel = subscription.ToDb();
subscriptionModel.Channel = channel;
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ subscriptionModel.Id = _subscriptionIdGenerator.GenerateSubscriptionId();
// Check that we're not about add an existing subscription that is identical
Data.Models.Subscription equivalentSubscription = await FindEquivalentSubscription(subscriptionModel);
diff --git a/src/Maestro/Maestro.Web/Pages/DependencyFlow/Incoming.cshtml.cs b/src/Maestro/Maestro.Web/Pages/DependencyFlow/Incoming.cshtml.cs
index a70a7049c3..699b13f777 100644
--- a/src/Maestro/Maestro.Web/Pages/DependencyFlow/Incoming.cshtml.cs
+++ b/src/Maestro/Maestro.Web/Pages/DependencyFlow/Incoming.cshtml.cs
@@ -84,11 +84,11 @@ public async Task OnGet(int channelId, string owner, string repo)
var incoming = new List();
foreach (var dep in Build.DependentBuildIds)
{
- var lastConsumedBuildOfDependency = graph[dep.BuildId];
+ var lastConsumedBuildOfDependency = graph[dep.DependentBuildId];
if (lastConsumedBuildOfDependency == null)
{
- _logger.LogWarning("Failed to find build with id '{BuildId}' in the graph", dep.BuildId);
+ _logger.LogWarning("Failed to find build with id '{DependentBuildId}' in the graph", dep.DependentBuildId);
continue;
}
diff --git a/src/Maestro/Maestro.Web/Startup.cs b/src/Maestro/Maestro.Web/Startup.cs
index 12babe7b03..45cd2ca194 100644
--- a/src/Maestro/Maestro.Web/Startup.cs
+++ b/src/Maestro/Maestro.Web/Startup.cs
@@ -270,6 +270,9 @@ public override void ConfigureServices(IServiceCollection services)
// in such a way that will work with sizing.
services.AddSingleton();
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ services.AddSingleton(sp => new(RunningService.Maestro));
+
services.AddTransient(sp =>
new ProcessManager(
sp.GetRequiredService>(),
diff --git a/src/Maestro/SubscriptionActorService/Program.cs b/src/Maestro/SubscriptionActorService/Program.cs
index 9a92d6a496..6c2c1ec5ff 100644
--- a/src/Maestro/SubscriptionActorService/Program.cs
+++ b/src/Maestro/SubscriptionActorService/Program.cs
@@ -85,7 +85,7 @@ public static void Configure(IServiceCollection services)
return PcsApiFactory.GetAnonymous(uri);
}
- return PcsApiFactory.GetAuthenticated(uri, managedIdentityId: "system");
+ return PcsApiFactory.GetAuthenticated(uri, managedIdentityId: "system", disableInteractiveAuth: true);
});
services.AddMergePolicies();
diff --git a/src/Maestro/SubscriptionActorService/PullRequestActor.cs b/src/Maestro/SubscriptionActorService/PullRequestActor.cs
index 39b0d71ac4..179bd1b06e 100644
--- a/src/Maestro/SubscriptionActorService/PullRequestActor.cs
+++ b/src/Maestro/SubscriptionActorService/PullRequestActor.cs
@@ -1040,7 +1040,7 @@ private async Task GetRepositoryBranchUpdate()
RepositoryBranchUpdate? update = await _context.RepositoryBranchUpdates.FindAsync(repo, branch);
if (update == null)
{
- RepositoryBranch repoBranch = await GetRepositoryBranch(repo, branch);
+ var repoBranch = await GetRepositoryBranch(repo, branch);
_context.RepositoryBranchUpdates.Add(
update = new RepositoryBranchUpdate { RepositoryBranch = repoBranch });
}
@@ -1052,13 +1052,13 @@ private async Task GetRepositoryBranchUpdate()
return update;
}
- private async Task GetRepositoryBranch(string repo, string branch)
+ private async Task GetRepositoryBranch(string repo, string branch)
{
- RepositoryBranch? repoBranch = await _context.RepositoryBranches.FindAsync(repo, branch);
+ var repoBranch = await _context.RepositoryBranches.FindAsync(repo, branch);
if (repoBranch == null)
{
_context.RepositoryBranches.Add(
- repoBranch = new RepositoryBranch
+ repoBranch = new Maestro.Data.Models.RepositoryBranch
{
RepositoryName = repo,
BranchName = branch
@@ -1160,10 +1160,8 @@ private async Task> ProcessCodeFlowUpdatesAsync(
try
{
- await _pcsClient.CodeFlow.FlowAsync(new CodeFlowRequest
+ await _pcsClient.CodeFlow.FlowBuildAsync(new CodeFlowRequest(update.SubscriptionId, update.BuildId)
{
- BuildId = update.BuildId,
- SubscriptionId = update.SubscriptionId,
PrBranch = codeFlowStatus.PrBranch,
PrUrl = pr.Url,
});
@@ -1203,10 +1201,8 @@ private async Task> RequestCodeFlowBranchAsync(UpdateAssetsPa
try
{
- await _pcsClient.CodeFlow.FlowAsync(new CodeFlowRequest
+ await _pcsClient.CodeFlow.FlowBuildAsync(new CodeFlowRequest(update.SubscriptionId, update.BuildId)
{
- BuildId = update.BuildId,
- SubscriptionId = update.SubscriptionId,
PrBranch = codeFlowUpdate.PrBranch,
});
}
diff --git a/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs b/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs
index b80f68c855..03c67343a7 100644
--- a/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs
+++ b/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs
@@ -23,7 +23,7 @@ internal class LocalSettings
public string AzureDevOpsToken { get; set; }
- public string BuildAssetRegistryBaseUri { get; set; } = MaestroApiOptions.ProductionBuildAssetRegistryBaseUri;
+ public string BuildAssetRegistryBaseUri { get; set; } = MaestroApiOptions.ProductionMaestroUri;
///
/// Saves the settings in the settings files
@@ -82,7 +82,7 @@ static string PreferOptionToSetting(string option, string localSetting)
localSettings.BuildAssetRegistryToken = PreferOptionToSetting(options.BuildAssetRegistryToken, localSettings.BuildAssetRegistryToken);
localSettings.BuildAssetRegistryBaseUri = options.BuildAssetRegistryBaseUri
?? localSettings.BuildAssetRegistryBaseUri
- ?? MaestroApiOptions.ProductionBuildAssetRegistryBaseUri;
+ ?? MaestroApiOptions.ProductionMaestroUri;
return localSettings;
}
diff --git a/src/Microsoft.DotNet.Darc/DarcLib/AzureDevOpsClient.cs b/src/Microsoft.DotNet.Darc/DarcLib/AzureDevOpsClient.cs
index 0f928f5712..b894650c60 100644
--- a/src/Microsoft.DotNet.Darc/DarcLib/AzureDevOpsClient.cs
+++ b/src/Microsoft.DotNet.Darc/DarcLib/AzureDevOpsClient.cs
@@ -218,7 +218,8 @@ public async Task DoesBranchExistAsync(string repoUri, string branch)
projectName,
$"_apis/git/repositories/{repoName}/refs?filter=heads/{branch}",
_logger,
- versionOverride: "7.0");
+ versionOverride: "7.0",
+ logFailure: false);
var refs = ((JArray)content["value"]).ToObject>();
return refs.Any(refs => refs.Name == $"refs/heads/{branch}");
diff --git a/src/Microsoft.DotNet.Darc/DarcLib/GitHubClient.cs b/src/Microsoft.DotNet.Darc/DarcLib/GitHubClient.cs
index 36c3118e2f..8e6f2c30fc 100644
--- a/src/Microsoft.DotNet.Darc/DarcLib/GitHubClient.cs
+++ b/src/Microsoft.DotNet.Darc/DarcLib/GitHubClient.cs
@@ -1,14 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using Newtonsoft.Json.Serialization;
-using Octokit;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Net;
@@ -17,12 +12,18 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
-using Microsoft.DotNet.Services.Utility;
-using System.Collections.Immutable;
-using Maestro.MergePolicyEvaluation;
using Maestro.Common;
+using Maestro.MergePolicyEvaluation;
using Microsoft.DotNet.DarcLib.Helpers;
+using Microsoft.DotNet.Services.Utility;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Newtonsoft.Json.Serialization;
+using Octokit;
+#nullable enable
namespace Microsoft.DotNet.DarcLib;
public class GitHubClient : RemoteRepoBase, IRemoteGitRepo
@@ -39,16 +40,16 @@ public class GitHubClient : RemoteRepoBase, IRemoteGitRepo
private static readonly Regex PullRequestUriPattern =
new(@"^/repos/(?[^/]+)/(?[^/]+)/pulls/(?\d+)$");
- private readonly Lazy _lazyClient;
private readonly IRemoteTokenProvider _tokenProvider;
private readonly ILogger _logger;
private readonly JsonSerializerSettings _serializerSettings;
private readonly string _userAgent = $"DarcLib-{DarcLibVersion}";
+ private IGitHubClient? _lazyClient = null;
static GitHubClient()
{
string version = Assembly.GetExecutingAssembly()
- .GetCustomAttribute()
+ .GetCustomAttribute()!
.InformationalVersion;
_product = new ProductHeaderValue("DarcLib", version);
}
@@ -57,7 +58,7 @@ public GitHubClient(
IRemoteTokenProvider remoteTokenProvider,
IProcessManager processManager,
ILogger logger,
- IMemoryCache cache)
+ IMemoryCache? cache)
: this(remoteTokenProvider, processManager, logger, null, cache)
{
}
@@ -66,8 +67,8 @@ public GitHubClient(
IRemoteTokenProvider remoteTokenProvider,
IProcessManager processManager,
ILogger logger,
- string temporaryRepositoryPath,
- IMemoryCache cache)
+ string? temporaryRepositoryPath,
+ IMemoryCache? cache)
: base(remoteTokenProvider, processManager, temporaryRepositoryPath, cache, logger)
{
_tokenProvider = remoteTokenProvider;
@@ -77,11 +78,8 @@ public GitHubClient(
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
};
- _lazyClient = new Lazy(CreateGitHubClient);
}
- public virtual IGitHubClient Client => _lazyClient.Value;
-
public bool AllowRetries { get; set; } = true;
///
@@ -123,14 +121,14 @@ private async Task GetFileContentsAsync(string owner, string repo, strin
responseContent = JObject.Parse(await response.Content.ReadAsStringAsync());
}
- var content = responseContent["content"].ToString();
+ var content = responseContent["content"]!.ToString();
_logger.LogInformation(
$"Getting the contents of file '{filePath}' from repo '{owner}/{repo}' in branch '{branch}' succeeded!");
return this.GetDecodedContent(content);
}
- catch (HttpRequestException reqEx) when (reqEx.Message.Contains(((int) HttpStatusCode.NotFound).ToString()))
+ catch (HttpRequestException reqEx) when (reqEx.Message.Contains(((int)HttpStatusCode.NotFound).ToString()))
{
throw new DependencyFileNotFoundException(filePath, $"{owner}/{repo}", branch, reqEx);
}
@@ -142,14 +140,12 @@ private async Task GetFileContentsAsync(string owner, string repo, strin
/// Repo to create a branch in
/// New branch name
/// Base of new branch
- ///
public async Task CreateBranchAsync(string repoUri, string newBranch, string baseBranch)
{
- _logger.LogInformation(
- $"Verifying if '{newBranch}' branch exists in repo '{repoUri}'. If not, we'll create it...");
+ _logger.LogInformation("Verifying if '{branch}' branch exists in repo '{repoUri}'. If not, we'll create it...", newBranch, repoUri);
(string owner, string repo) = ParseRepoUri(repoUri);
- string latestSha = await GetLastCommitShaAsync(owner, repo, baseBranch);
+ string? latestSha = await GetLastCommitShaAsync(owner, repo, baseBranch);
string body;
var gitRef = $"refs/heads/{newBranch}";
@@ -164,7 +160,8 @@ public async Task CreateBranchAsync(string repoUri, string newBranch, string bas
$"https://github.com/{owner}/{repo}",
$"repos/{owner}/{repo}/branches/{newBranch}",
_logger,
- retryCount: 0)) { }
+ retryCount: 0,
+ logFailure: false)) { }
githubRef.Force = true;
body = JsonConvert.SerializeObject(githubRef, _serializerSettings);
@@ -175,11 +172,11 @@ public async Task CreateBranchAsync(string repoUri, string newBranch, string bas
_logger,
body)) { }
- _logger.LogInformation($"Branch '{newBranch}' exists, updated");
+ _logger.LogInformation("Branch '{branch}' exists, updated", newBranch);
}
- catch (HttpRequestException exc) when (exc.Message.Contains(((int) HttpStatusCode.NotFound).ToString()))
+ catch (HttpRequestException exc) when (exc.Message.Contains(((int)HttpStatusCode.NotFound).ToString()))
{
- _logger.LogInformation($"'{newBranch}' branch doesn't exist. Creating it...");
+ _logger.LogInformation("'{branch}' branch doesn't exist. Creating it...", newBranch);
body = JsonConvert.SerializeObject(githubRef, _serializerSettings);
using (await ExecuteRemoteGitCommandAsync(
@@ -189,14 +186,14 @@ public async Task CreateBranchAsync(string repoUri, string newBranch, string bas
_logger,
body)) { }
- _logger.LogInformation($"Branch '{newBranch}' created in repo '{repoUri}'!");
-
+ _logger.LogInformation("Branch '{branch}' created in repo '{repoUri}'", newBranch, repoUri);
return;
}
catch (HttpRequestException exc)
{
_logger.LogError(
- $"Checking if '{newBranch}' branch existed in repo '{repoUri}' failed with '{exc.Message}'");
+ "Checking if '{branch}' branch existed in repo '{repoUri}' failed with '{error}'",
+ newBranch, repoUri, exc.Message);
throw;
}
}
@@ -225,7 +222,7 @@ public async Task DoesBranchExistAsync(string repoUri, string branch)
(string owner, string repo) = ParseRepoUri(repoUri);
try
{
- await Client.Repository.Branch.Get(owner, repo, branch);
+ await GetClient(repoUri).Repository.Branch.Get(owner, repo, branch);
return true;
}
catch (NotFoundException)
@@ -243,7 +240,7 @@ public async Task DoesBranchExistAsync(string repoUri, string branch)
///
private async Task DeleteBranchAsync(string owner, string repo, string branch)
{
- await Client.Git.Reference.Delete(owner, repo, $"heads/{branch}");
+ await GetClient(owner, repo).Git.Reference.Delete(owner, repo, $"heads/{branch}");
}
///
@@ -259,8 +256,8 @@ public async Task> SearchPullRequestsAsync(
string repoUri,
string pullRequestBranch,
PrStatus status,
- string keyword = null,
- string author = null)
+ string? keyword = null,
+ string? author = null)
{
(string owner, string repo) = ParseRepoUri(repoUri);
var query = new StringBuilder();
@@ -288,9 +285,9 @@ public async Task> SearchPullRequestsAsync(
responseContent = JObject.Parse(await response.Content.ReadAsStringAsync());
}
- var items = JArray.Parse(responseContent["items"].ToString());
+ var items = JArray.Parse(responseContent["items"]!.ToString());
- IEnumerable prs = items.Select(r => r["number"].ToObject());
+ IEnumerable prs = items.Select(r => r["number"]!.ToObject());
return prs;
}
@@ -314,7 +311,7 @@ public async Task GetPullRequestStatusAsync(string pullRequestUrl)
responseContent = JObject.Parse(await response.Content.ReadAsStringAsync());
}
- if (Enum.TryParse(responseContent["state"].ToString(), true, out PrStatus status))
+ if (Enum.TryParse(responseContent["state"]!.ToString(), true, out PrStatus status))
{
if (status == PrStatus.Open)
{
@@ -323,7 +320,7 @@ public async Task GetPullRequestStatusAsync(string pullRequestUrl)
if (status == PrStatus.Closed)
{
- if (bool.TryParse(responseContent["merged"].ToString(), out bool merged))
+ if (bool.TryParse(responseContent["merged"]!.ToString(), out bool merged))
{
if (merged)
{
@@ -346,7 +343,7 @@ public async Task GetPullRequestStatusAsync(string pullRequestUrl)
public async Task GetPullRequestAsync(string pullRequestUrl)
{
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
- Octokit.PullRequest pr = await Client.PullRequest.Get(owner, repo, id);
+ Octokit.PullRequest pr = await GetClient(owner, repo).PullRequest.Get(owner, repo, id);
return new PullRequest
{
Title = pr.Title,
@@ -370,7 +367,7 @@ public async Task CreatePullRequestAsync(string repoUri, PullRequest pul
{
Body = pullRequest.Description
};
- Octokit.PullRequest createdPullRequest = await Client.PullRequest.Create(owner, repo, pr);
+ Octokit.PullRequest createdPullRequest = await GetClient(repoUri).PullRequest.Create(owner, repo, pr);
return createdPullRequest.Url;
}
@@ -385,7 +382,7 @@ public async Task UpdatePullRequestAsync(string pullRequestUri, PullRequest pull
{
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUri);
- await Client.PullRequest.Update(
+ await GetClient(owner, repo).PullRequest.Update(
owner,
repo,
id,
@@ -405,7 +402,7 @@ public async Task> GetPullRequestCommitsAsync(string pullRequestUr
{
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
- IReadOnlyList pullRequestCommits = await Client.PullRequest.Commits(owner, repo, id);
+ IReadOnlyList pullRequestCommits = await GetClient(owner, repo).PullRequest.Commits(owner, repo, id);
IList commits = new List(pullRequestCommits.Count);
foreach (var commit in pullRequestCommits)
@@ -427,8 +424,8 @@ public async Task> GetPullRequestCommitsAsync(string pullRequestUr
public async Task MergeDependencyPullRequestAsync(string pullRequestUrl, MergePullRequestParameters parameters, string mergeCommitMessage)
{
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
- Octokit.PullRequest pr = await Client.PullRequest.Get(owner, repo, id);
-
+ Octokit.PullRequest pr = await GetClient(owner, repo).PullRequest.Get(owner, repo, id);
+
var mergePullRequest = new MergePullRequest
{
CommitMessage = mergeCommitMessage,
@@ -436,11 +433,11 @@ public async Task MergeDependencyPullRequestAsync(string pullRequestUrl, MergePu
MergeMethod = parameters.SquashMerge ? PullRequestMergeMethod.Squash : PullRequestMergeMethod.Merge
};
- await Client.PullRequest.Merge(owner, repo, id, mergePullRequest);
+ await GetClient(owner, repo).PullRequest.Merge(owner, repo, id, mergePullRequest);
if (parameters.DeleteSourceBranch)
{
- await Client.Git.Reference.Delete(owner, repo, $"heads/{pr.Head.Ref}");
+ await GetClient(owner, repo).Git.Reference.Delete(owner, repo, $"heads/{pr.Head.Ref}");
}
}
@@ -453,14 +450,14 @@ public async Task MergeDependencyPullRequestAsync(string pullRequestUrl, MergePu
public async Task CreateOrUpdatePullRequestCommentAsync(string pullRequestUrl, string message)
{
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
- IssueComment lastComment = (await Client.Issue.Comment.GetAllForIssue(owner, repo, id))[^1];
+ IssueComment lastComment = (await GetClient(owner, repo).Issue.Comment.GetAllForIssue(owner, repo, id))[^1];
if (lastComment != null && lastComment.Body.EndsWith(CommentMarker))
{
- await Client.Issue.Comment.Update(owner, repo, lastComment.Id, message + CommentMarker);
+ await GetClient(owner, repo).Issue.Comment.Update(owner, repo, lastComment.Id, message + CommentMarker);
}
else
{
- await Client.Issue.Comment.Create(owner, repo, id, message + CommentMarker);
+ await GetClient(owner, repo).Issue.Comment.Create(owner, repo, id, message + CommentMarker);
}
}
@@ -477,13 +474,14 @@ private static string CheckRunId(MergePolicyEvaluationResult result, string sha)
public async Task CreateOrUpdatePullRequestMergeStatusInfoAsync(string pullRequestUrl, IReadOnlyList evaluations)
{
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
+ var client = GetClient(owner, repo);
// Get the sha of the latest commit for the current PR
- string prSha = (await Client.PullRequest.Get(owner, repo, id))?.Head?.Sha
+ string prSha = (await client.PullRequest.Get(owner, repo, id))?.Head?.Sha
?? throw new InvalidOperationException("We cannot find the sha of the pull request");
// Get a list of all the merge policies checks runs for the current PR
- List existingChecksRuns =
- (await Client.Check.Run.GetAllForReference(owner, repo, prSha))
+ List existingChecksRuns =
+ (await client.Check.Run.GetAllForReference(owner, repo, prSha))
.CheckRuns.Where(e => e.ExternalId.StartsWith(MergePolicyConstants.MaestroMergePolicyCheckRunPrefix)).ToList();
var toBeAdded = evaluations.Where(e => existingChecksRuns.All(c => c.ExternalId != CheckRunId(e, prSha)));
@@ -492,17 +490,17 @@ public async Task CreateOrUpdatePullRequestMergeStatusInfoAsync(string pullReque
foreach (var newCheckRunValidation in toBeAdded)
{
- await Client.Check.Run.Create(owner, repo, CheckRunForAdd(newCheckRunValidation, prSha));
+ await client.Check.Run.Create(owner, repo, CheckRunForAdd(newCheckRunValidation, prSha));
}
foreach (var updatedCheckRun in toBeUpdated)
- {
+ {
MergePolicyEvaluationResult eval = evaluations.Last(e => updatedCheckRun.ExternalId == CheckRunId(e, prSha));
CheckRunUpdate newCheckRunUpdateValidation = CheckRunForUpdate(eval);
- await Client.Check.Run.Update(owner, repo, updatedCheckRun.Id, newCheckRunUpdateValidation);
+ await client.Check.Run.Update(owner, repo, updatedCheckRun.Id, newCheckRunUpdateValidation);
}
foreach (var deletedCheckRun in toBeDeleted)
{
- await Client.Check.Run.Update(owner, repo, deletedCheckRun.Id, CheckRunForDelete(deletedCheckRun));
+ await client.Check.Run.Update(owner, repo, deletedCheckRun.Id, CheckRunForDelete(deletedCheckRun));
}
}
@@ -636,14 +634,14 @@ public async Task> GetFilesAtCommitAsync(string repoUri, string co
TreeResponse recursiveTree = await GetRecursiveTreeAsync(owner, repo, pathTree.Sha);
- GitFile[] files = await Task.WhenAll(
+ GitFile?[] files = await Task.WhenAll(
recursiveTree.Tree.Where(treeItem => treeItem.Type == TreeType.Blob)
.Select(
async treeItem =>
{
return await GetGitTreeItem(path, treeItem, owner, repo);
}));
- return [.. files];
+ return [.. files.Where(f => f != null)];
}
///
@@ -654,7 +652,7 @@ public async Task> GetFilesAtCommitAsync(string repoUri, string co
/// Organization
/// Repository
/// Git file with tree item contents.
- public async Task GetGitTreeItem(string path, TreeItem treeItem, string owner, string repo)
+ public async Task GetGitTreeItem(string path, TreeItem treeItem, string owner, string repo)
{
// If we have a cache available here, attempt to get the value in the cache
// before making the request. Generally, we are requesting the same files for each
@@ -707,10 +705,10 @@ private async Task GetGitItemImpl(string path, TreeItem treeItem, strin
{
try
{
- blob = await Client.Git.Blob.Get(owner, repo, treeItem.Sha);
+ blob = await GetClient(owner, repo).Git.Blob.Get(owner, repo, treeItem.Sha);
break;
}
- catch (Exception e) when ((e is ForbiddenException || e is AbuseException ) && attempts < maxAttempts)
+ catch (Exception e) when ((e is ForbiddenException || e is AbuseException) && attempts < maxAttempts)
{
// AbuseException exposes a retry-after field which lets us know how long we should wait. ForbiddenException does not, so use 60 seconds
var retryAfterSeconds = 60;
@@ -726,7 +724,7 @@ private async Task GetGitItemImpl(string path, TreeItem treeItem, strin
}
return blob;
-
+
},
ex => _logger.LogError(ex, $"Failed to get blob at sha {treeItem.Sha}"),
ex => ex is ApiException apiex && apiex.StatusCode >= HttpStatusCode.InternalServerError);
@@ -759,8 +757,8 @@ private async Task ExecuteRemoteGitCommandAsync(
string repoUri,
string requestUri,
ILogger logger,
- string body = null,
- string versionOverride = null,
+ string? body = null,
+ string? versionOverride = null,
int retryCount = 15,
bool logFailure = true)
{
@@ -818,7 +816,7 @@ private HttpClient CreateHttpClient(string repoUri)
/// Path to file
/// Branch
/// Sha of file or null if the file does not exist.
- public async Task CheckIfFileExistsAsync(string repoUri, string filePath, string branch)
+ public async Task CheckIfFileExistsAsync(string repoUri, string filePath, string branch)
{
string commit;
(string owner, string repo) = ParseRepoUri(repoUri);
@@ -831,15 +829,16 @@ public async Task CheckIfFileExistsAsync(string repoUri, string filePath
HttpMethod.Get,
$"https://github.com/{owner}/{repo}",
$"repos/{owner}/{repo}/contents/{filePath}?ref={branch}",
- _logger))
+ _logger,
+ logFailure: false))
{
content = JObject.Parse(await response.Content.ReadAsStringAsync());
}
- commit = content["sha"].ToString();
+ commit = content["sha"]!.ToString();
return commit;
}
- catch (HttpRequestException exc) when (exc.Message.Contains(((int) HttpStatusCode.NotFound).ToString()))
+ catch (HttpRequestException exc) when (exc.Message.Contains(((int)HttpStatusCode.NotFound).ToString()))
{
return null;
}
@@ -851,7 +850,7 @@ public async Task CheckIfFileExistsAsync(string repoUri, string filePath
/// Repository uri
/// Branch to retrieve the latest sha for
/// Latest sha. Nulls if no commits were found.
- public Task GetLastCommitShaAsync(string repoUri, string branch)
+ public Task GetLastCommitShaAsync(string repoUri, string branch)
{
(string owner, string repo) = ParseRepoUri(repoUri);
return GetLastCommitShaAsync(owner, repo, branch);
@@ -863,7 +862,7 @@ public Task GetLastCommitShaAsync(string repoUri, string branch)
/// Repository URI
/// Sha of the commit
/// Return the commit matching the specified sha. Null if no commit were found.
- public Task GetCommitAsync(string repoUri, string sha)
+ public Task GetCommitAsync(string repoUri, string sha)
{
(string owner, string repo) = ParseRepoUri(repoUri);
return GetCommitAsync(owner, repo, sha);
@@ -876,10 +875,10 @@ public Task GetCommitAsync(string repoUri, string sha)
/// Repository name
/// Sha of the commit
/// Return the commit matching the specified sha. Null if no commit were found.
- private async Task GetCommitAsync(string owner, string repo, string sha)
+ private async Task GetCommitAsync(string owner, string repo, string sha)
{
- Repository repository = await Client.Repository.Get(owner, repo);
- Octokit.GitHubCommit commit = await Client.Repository.Commit.Get(repository.Id, sha);
+ Repository repository = await GetClient(owner, repo).Repository.Get(owner, repo);
+ Octokit.GitHubCommit commit = await GetClient(owner, repo).Repository.Commit.Get(repository.Id, sha);
if (commit == null)
{
return null;
@@ -894,7 +893,7 @@ private async Task GetCommitAsync(string owner, string repo, string sha)
/// Repository name
/// Branch to retrieve the latest sha for
/// Latest sha. Null if no commits were found.
- private async Task GetLastCommitShaAsync(string owner, string repo, string branch)
+ private async Task GetLastCommitShaAsync(string owner, string repo, string branch)
{
try
{
@@ -908,7 +907,7 @@ private async Task GetLastCommitShaAsync(string owner, string repo, stri
content = JObject.Parse(await response.Content.ReadAsStringAsync());
}
- return content["sha"].ToString();
+ return content["sha"]!.ToString();
}
catch (HttpRequestException exc) when (exc.Message.Contains(((int)HttpStatusCode.NotFound).ToString())
|| exc.Message.Contains(((int)HttpStatusCode.UnprocessableEntity).ToString()))
@@ -926,7 +925,7 @@ public async Task> GetPullRequestChecksAsync(string pullRequestUrl)
{
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
- var commits = await Client.Repository.PullRequest.Commits(owner, repo, id);
+ var commits = await GetClient(owner, repo).Repository.PullRequest.Commits(owner, repo, id);
var lastCommitSha = commits[commits.Count - 1].Sha;
return (await GetChecksFromStatusApiAsync(owner, repo, lastCommitSha))
@@ -946,7 +945,7 @@ public async Task> GetLatestPullRequestReviewsAsync(string pullReq
{
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
- var reviews = await Client.Repository.PullRequest.Review.GetAll(owner, repo, id);
+ var reviews = await GetClient(owner, repo).Repository.PullRequest.Review.GetAll(owner, repo, id);
// Filter out comments because they could come after Approved/ChangedRequested, and they don't change the decision.
reviews = reviews.Where(r => r.State != PullRequestReviewState.Commented).ToImmutableList();
@@ -955,8 +954,8 @@ public async Task> GetLatestPullRequestReviewsAsync(string pullReq
var newestActionableReviews = reviews.GroupBy(r => r.User.Login)
.ToDictionary(g => g.Key,
g => (from r in reviews
- where r.User.Login == g.Key
- select r)
+ where r.User.Login == g.Key
+ select r)
.OrderByDescending(r => r.SubmittedAt)
.First())
.Values;
@@ -982,8 +981,8 @@ private static ReviewState TranslateReviewState(PullRequestReviewState state)
private async Task> GetChecksFromStatusApiAsync(string owner, string repo, string @ref)
{
- var status = await Client.Repository.Status.GetCombined(owner, repo, @ref);
-
+ var status = await GetClient(owner, repo).Repository.Status.GetCombined(owner, repo, @ref);
+
return status.Statuses.Select(
s =>
{
@@ -1004,7 +1003,7 @@ private async Task> GetChecksFromStatusApiAsync(string owner, strin
private async Task> GetChecksFromChecksApiAsync(string owner, string repo, string @ref)
{
- var checkRuns = await Client.Check.Run.GetAllForReference(owner, repo, @ref);
+ var checkRuns = await GetClient(owner, repo).Check.Run.GetAllForReference(owner, repo, @ref);
return checkRuns.CheckRuns.Select(
run =>
{
@@ -1027,9 +1026,21 @@ private async Task> GetChecksFromChecksApiAsync(string owner, strin
.ToList();
}
- private Octokit.GitHubClient CreateGitHubClient()
+ public virtual IGitHubClient GetClient(string repoUri)
{
- var token = _tokenProvider.GetTokenForRepository(GitHubApiUri);
+ _lazyClient ??= CreateGitHubClient(repoUri);
+ return _lazyClient;
+ }
+
+ public virtual IGitHubClient GetClient(string owner, string repo)
+ {
+ _lazyClient ??= CreateGitHubClient($"https://github.com/{owner}/{repo}");
+ return _lazyClient;
+ }
+
+ private Octokit.GitHubClient CreateGitHubClient(string repoUri)
+ {
+ var token = _tokenProvider.GetTokenForRepository(repoUri);
if (string.IsNullOrEmpty(token))
{
throw new DarcException(
@@ -1045,7 +1056,7 @@ private Octokit.GitHubClient CreateGitHubClient()
private async Task GetRecursiveTreeAsync(string owner, string repo, string treeSha)
{
- TreeResponse tree = await Client.Git.Tree.GetRecursive(owner, repo, treeSha);
+ TreeResponse tree = await GetClient(owner, repo).Git.Tree.GetRecursive(owner, repo, treeSha);
if (tree.Truncated)
{
throw new NotSupportedException(
@@ -1059,13 +1070,13 @@ private async Task GetTreeForPathAsync(string owner, string repo,
{
var pathSegments = new Queue(path.Split('/', '\\'));
var currentPath = new List();
- Octokit.Commit commit = await Client.Git.Commit.Get(owner, repo, commitSha);
+ Octokit.Commit commit = await GetClient(owner, repo).Git.Commit.Get(owner, repo, commitSha);
string treeSha = commit.Tree.Sha;
while (true)
{
- TreeResponse tree = await Client.Git.Tree.Get(owner, repo, treeSha);
+ TreeResponse tree = await GetClient(owner, repo).Git.Tree.Get(owner, repo, treeSha);
if (tree.Truncated)
{
throw new NotSupportedException(
@@ -1079,14 +1090,11 @@ private async Task GetTreeForPathAsync(string owner, string repo,
string subfolder = pathSegments.Dequeue();
currentPath.Add(subfolder);
- TreeItem subfolderItem = tree.Tree.Where(ti => ti.Type == TreeType.Tree)
- .FirstOrDefault(ti => ti.Path == subfolder);
-
- if (subfolderItem == null)
- {
- throw new DirectoryNotFoundException(
+ TreeItem? subfolderItem = tree.Tree
+ .Where(ti => ti.Type == TreeType.Tree)
+ .FirstOrDefault(ti => ti.Path == subfolder)
+ ?? throw new DirectoryNotFoundException(
$"The path '{string.Join("/", currentPath)}' could not be found.");
- }
treeSha = subfolderItem.Sha;
}
@@ -1109,7 +1117,7 @@ private async Task GetCommitMapForPathAsync(
$"Getting the contents of file/files in '{path}' of repo '{repoUri}' at commit '{assetsProducedInCommit}'");
(string owner, string repo) = ParseRepoUri(repoUri);
- List contents;
+ List? contents;
using (HttpResponseMessage response = await ExecuteRemoteGitCommandAsync(
HttpMethod.Get,
@@ -1120,7 +1128,7 @@ private async Task GetCommitMapForPathAsync(
contents = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync());
}
- foreach (GitHubContent content in contents)
+ foreach (GitHubContent content in contents!)
{
if (content.Type == GitHubContentType.File)
{
@@ -1196,7 +1204,7 @@ await CommitFilesAsync(
branch,
commitMessage,
_logger,
- await _tokenProvider.GetTokenForRepositoryAsync(GitHubApiUri),
+ await _tokenProvider.GetTokenForRepositoryAsync(repoUri),
Constants.DarcBotName,
Constants.DarcBotEmail);
}
@@ -1230,12 +1238,12 @@ public async Task GitDiffAsync(string repoUri, string baseVersion, stri
{
BaseVersion = baseVersion,
TargetVersion = targetVersion,
- Ahead = content["ahead_by"].Value(),
- Behind = content["behind_by"].Value(),
+ Ahead = content["ahead_by"]!.Value(),
+ Behind = content["behind_by"]!.Value(),
Valid = true
};
}
- catch (HttpRequestException reqEx) when (reqEx.Message.Contains(((int) HttpStatusCode.NotFound).ToString()))
+ catch (HttpRequestException reqEx) when (reqEx.Message.Contains(((int)HttpStatusCode.NotFound).ToString()))
{
return GitDiff.UnknownDiff();
}
diff --git a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs
index bb7130e0b8..2bcde8f6d1 100644
--- a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs
+++ b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs
@@ -96,8 +96,11 @@ public async Task ExecuteAsync(int retryCount = 3)
errorDetails = $"Error details: {errorDetails}";
}
- _logger.LogError($"A '{(int)response.StatusCode} - {response.StatusCode}' status was returned for a HTTP request. " +
- $"We'll set the retries amount to 0. {errorDetails}");
+ _logger.LogError(
+ "A '{httpCode} - {status}' status was returned for a HTTP request. We'll set the retries amount to 0. {error}",
+ (int)response.StatusCode,
+ response.StatusCode,
+ errorDetails);
}
retriesRemaining = 0;
@@ -125,14 +128,22 @@ public async Task ExecuteAsync(int retryCount = 3)
if (_logFailure)
{
_logger.LogError("There was an error executing method '{method}' against URI '{requestUri}' " +
- "after {maxRetries} attempts. Exception: {exception}", _method, _requestUri, retryCount, ex);
+ "after {maxRetries} attempts. Exception: {exception}",
+ _method,
+ _requestUri,
+ retryCount,
+ ex);
}
throw;
}
else if (_logFailure)
{
_logger.LogWarning("There was an error executing method '{method}' against URI '{requestUri}'. " +
- "{retriesRemaining} attempts remaining. Exception: {ex.ToString()}", _method, _requestUri, retriesRemaining, ex);
+ "{retriesRemaining} attempts remaining. Exception: {ex.ToString()}",
+ _method,
+ _requestUri,
+ retriesRemaining,
+ ex);
}
}
--retriesRemaining;
diff --git a/src/Microsoft.DotNet.Darc/DarcLib/IRemoteGitRepo.cs b/src/Microsoft.DotNet.Darc/DarcLib/IRemoteGitRepo.cs
index b14a7b6e8c..62df62bcf2 100644
--- a/src/Microsoft.DotNet.Darc/DarcLib/IRemoteGitRepo.cs
+++ b/src/Microsoft.DotNet.Darc/DarcLib/IRemoteGitRepo.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
+#nullable enable
namespace Microsoft.DotNet.DarcLib;
public interface IRemoteGitRepo : IGitRepoCloner, IGitRepo
@@ -50,8 +51,8 @@ Task> SearchPullRequestsAsync(
string repoUri,
string pullRequestBranch,
PrStatus status,
- string keyword = null,
- string author = null);
+ string? keyword = null,
+ string? author = null);
///
/// Get the status of a pull request
@@ -114,7 +115,7 @@ Task> SearchPullRequestsAsync(
/// Repository uri
/// Branch to retrieve the latest sha for
/// Latest sha. Null if no commits were found.
- Task GetLastCommitShaAsync(string repoUri, string branch);
+ Task GetLastCommitShaAsync(string repoUri, string branch);
///
/// Get a commit in a repo
@@ -122,7 +123,7 @@ Task> SearchPullRequestsAsync(
/// Repository URI
/// Sha of the commit
/// Return the commit matching the specified sha. Null if no commit were found.
- Task GetCommitAsync(string repoUri, string sha);
+ Task GetCommitAsync(string repoUri, string sha);
///
/// Gets a list of file under a given path in a given revision.
@@ -167,6 +168,7 @@ Task> SearchPullRequestsAsync(
Task DoesBranchExistAsync(string repoUri, string branch);
}
+#nullable disable
public class PullRequest
{
public string Title { get; set; }
diff --git a/src/Microsoft.DotNet.Darc/DarcLib/VirtualMonoRepo/VmrCloneManager.cs b/src/Microsoft.DotNet.Darc/DarcLib/VirtualMonoRepo/VmrCloneManager.cs
index 3c5a7a6e84..c5e1cf00d8 100644
--- a/src/Microsoft.DotNet.Darc/DarcLib/VirtualMonoRepo/VmrCloneManager.cs
+++ b/src/Microsoft.DotNet.Darc/DarcLib/VirtualMonoRepo/VmrCloneManager.cs
@@ -75,8 +75,6 @@ public async Task PrepareVmrAsync(
public async Task PrepareVmrAsync(string checkoutRef, CancellationToken cancellationToken)
{
- _vmrInfo.VmrUri = _vmrInfo.VmrUri;
-
ILocalGitRepo vmr = await PrepareVmrAsync(
[_vmrInfo.VmrUri],
[checkoutRef],
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2018_07_16/Controllers/SubscriptionsController.cs b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2018_07_16/Controllers/SubscriptionsController.cs
index a03f079da2..e89f6e89fa 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2018_07_16/Controllers/SubscriptionsController.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2018_07_16/Controllers/SubscriptionsController.cs
@@ -27,15 +27,19 @@ public class SubscriptionsController : ControllerBase
private readonly BuildAssetRegistryContext _context;
private readonly IWorkItemProducerFactory _workItemProducerFactory;
private readonly ILogger _logger;
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ protected readonly SubscriptionIdGenerator _subscriptionIdGenerator;
public SubscriptionsController(
BuildAssetRegistryContext context,
IWorkItemProducerFactory workItemProducerFactory,
- ILogger logger)
+ ILogger logger,
+ SubscriptionIdGenerator subscriptionIdGenerator)
{
_context = context;
_workItemProducerFactory = workItemProducerFactory;
_logger = logger;
+ _subscriptionIdGenerator = subscriptionIdGenerator;
}
///
@@ -112,6 +116,11 @@ public virtual async Task TriggerSubscription(Guid id, [FromQuery
protected async Task TriggerSubscriptionCore(Guid id, int buildId)
{
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ if (!_subscriptionIdGenerator.ShouldTriggerSubscription(id))
+ {
+ return BadRequest($"PCS can only trigger subscriptions which ids start with ${SubscriptionIdGenerator.PcsSubscriptionIdPrefix}");
+ }
Maestro.Data.Models.Subscription? subscription = await _context.Subscriptions.Include(sub => sub.LastAppliedBuild)
.Include(sub => sub.Channel)
.FirstOrDefaultAsync(sub => sub.Id == id);
@@ -178,7 +187,7 @@ where sub.Enabled
if (subscriptionToUpdate != null)
{
- await _workItemProducerFactory.CreateProducer().ProduceWorkItemAsync(new()
+ await _workItemProducerFactory.CreateProducer().ProduceWorkItemAsync(new()
{
SubscriptionId = subscriptionToUpdate.Id,
BuildId = buildId
@@ -200,7 +209,7 @@ public virtual async Task TriggerDailyUpdateAsync()
.ToListAsync())
.Where(s => (int)s.PolicyObject.UpdateFrequency == (int)UpdateFrequency.EveryDay);
- var workitemProducer = _workItemProducerFactory.CreateProducer();
+ var workitemProducer = _workItemProducerFactory.CreateProducer();
foreach (var subscription in enabledSubscriptionsWithTargetFrequency)
{
@@ -427,6 +436,7 @@ public virtual async Task Create([FromBody, Required] Subscriptio
Maestro.Data.Models.Subscription subscriptionModel = subscription.ToDb();
subscriptionModel.Channel = channel;
+ subscriptionModel.Id = _subscriptionIdGenerator.GenerateSubscriptionId();
Maestro.Data.Models.Subscription? equivalentSubscription = await FindEquivalentSubscription(subscriptionModel);
if (equivalentSubscription != null)
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2019_01_16/Controllers/SubscriptionsController.cs b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2019_01_16/Controllers/SubscriptionsController.cs
index 06d31fe134..e3390d19d0 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2019_01_16/Controllers/SubscriptionsController.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2019_01_16/Controllers/SubscriptionsController.cs
@@ -25,8 +25,9 @@ public class SubscriptionsController : v2018_07_16.Controllers.SubscriptionsCont
public SubscriptionsController(
BuildAssetRegistryContext context,
IWorkItemProducerFactory workItemProducerFactory,
- ILogger logger)
- : base(context, workItemProducerFactory, logger)
+ ILogger logger,
+ SubscriptionIdGenerator subscriptionIdGenerator)
+ : base(context, workItemProducerFactory, logger, subscriptionIdGenerator)
{
_context = context;
}
@@ -272,6 +273,7 @@ public override async Task Create([FromBody, Required] Maestro.Ap
Maestro.Data.Models.Subscription subscriptionModel = subscription.ToDb();
subscriptionModel.Channel = channel;
+ subscriptionModel.Id = _subscriptionIdGenerator.GenerateSubscriptionId();
// Check that we're not about add an existing subscription that is identical
Maestro.Data.Models.Subscription? equivalentSubscription = await FindEquivalentSubscription(subscriptionModel);
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Controllers/BuildsController.cs b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Controllers/BuildsController.cs
index fffec48dd3..9343dca97c 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Controllers/BuildsController.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Controllers/BuildsController.cs
@@ -338,77 +338,4 @@ await _workItemProducerFactory.CreateProducer()
},
new Build(buildModel));
}
-
- // TODO PORT THIS TO A WORKITEM PROCESSOR:
- /*
- private class BuildCoherencyInfoWorkItem : IBackgroundWorkItem
- {
- private readonly BuildAssetRegistryContext _context;
- private readonly IRemoteFactory _remoteFactory;
- private readonly IBasicBarClient _barClient;
- private readonly ILogger _logger;
-
- public BuildCoherencyInfoWorkItem(
- BuildAssetRegistryContext context,
- IRemoteFactory remoteFactory,
- IBasicBarClient barClient,
- ILogger logger)
- {
- _context = context;
- _remoteFactory = remoteFactory;
- _barClient = barClient;
- _logger = logger;
- }
-
- public async Task ProcessAsync(JToken argumentToken)
- {
- // This method is called asynchronously whenever a new build is inserted in BAR.
- // It's goal is to compute the incoherent dependencies that the build have and
- // persist the list of them in BAR.
-
- var buildId = argumentToken.Value();
- var graphBuildOptions = new DependencyGraphBuildOptions()
- {
- IncludeToolset = false,
- LookupBuilds = false,
- NodeDiff = NodeDiff.None
- };
-
- try
- {
- Maestro.Data.Models.Build build = await _context.Builds.FindAsync(buildId);
-
- DependencyGraph graph = await DependencyGraph.BuildRemoteDependencyGraphAsync(
- _remoteFactory,
- _barClient,
- build.GitHubRepository ?? build.AzureDevOpsRepository,
- build.Commit,
- graphBuildOptions,
- _logger);
-
- var incoherencies = new List();
-
- foreach (var incoherence in graph.IncoherentDependencies)
- {
- incoherencies.Add(new Maestro.Data.Models.BuildIncoherence
- {
- Name = incoherence.Name,
- Version = incoherence.Version,
- Repository = incoherence.RepoUri,
- Commit = incoherence.Commit
- });
- }
-
- _context.Entry(build).Reload();
- build.Incoherencies = incoherencies;
-
- _context.Builds.Update(build);
- await _context.SaveChangesAsync();
- }
- catch (Exception e)
- {
- _logger.LogWarning(e, $"Problems computing the dependency incoherencies for BAR build {buildId}");
- }
- }
- }*/
}
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Controllers/SubscriptionsController.cs b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Controllers/SubscriptionsController.cs
index fc4efabac8..c8efa2c0ac 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Controllers/SubscriptionsController.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Api/v2020_02_20/Controllers/SubscriptionsController.cs
@@ -30,8 +30,9 @@ public SubscriptionsController(
BuildAssetRegistryContext context,
IGitHubClientFactory gitHubClientFactory,
IWorkItemProducerFactory workItemProducerFactory,
- ILogger logger)
- : base(context, workItemProducerFactory, logger)
+ ILogger logger,
+ SubscriptionIdGenerator subscriptionIdGenerator)
+ : base(context, workItemProducerFactory, logger, subscriptionIdGenerator)
{
_context = context;
_gitHubClientFactory = gitHubClientFactory;
@@ -441,6 +442,7 @@ public async Task Create([FromBody, Required] SubscriptionData su
Maestro.Data.Models.Subscription subscriptionModel = subscription.ToDb();
subscriptionModel.Channel = channel;
+ subscriptionModel.Id = _subscriptionIdGenerator.GenerateSubscriptionId();
// Check that we're not about add an existing subscription that is identical
Maestro.Data.Models.Subscription? equivalentSubscription = await FindEquivalentSubscription(subscriptionModel);
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Configuration/DataProtection.cs b/src/ProductConstructionService/ProductConstructionService.Api/Configuration/DataProtection.cs
index ba9bc9c6ae..27a23c4df8 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Configuration/DataProtection.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Configuration/DataProtection.cs
@@ -13,7 +13,7 @@ internal static class DataProtection
private static readonly TimeSpan DataProtectionKeyLifetime = new(days: 240, hours: 0, minutes: 0, seconds: 0);
- public static void AddDataProtection(this WebApplicationBuilder builder)
+ public static void AddDataProtection(this WebApplicationBuilder builder, DefaultAzureCredential credential)
{
var keyBlobUri = builder.Configuration[DataProtectionKeyBlobUri];
var dataProtectionKeyUri = builder.Configuration[DataProtectionKeyUri];
@@ -26,7 +26,6 @@ public static void AddDataProtection(this WebApplicationBuilder builder)
return;
}
- var credential = new DefaultAzureCredential();
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri(keyBlobUri), credential)
.ProtectKeysWithAzureKeyVault(new Uri(dataProtectionKeyUri), credential)
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Configuration/GitHubClientFactoryConfiguration.cs b/src/ProductConstructionService/ProductConstructionService.Api/Configuration/GitHubClientFactoryConfiguration.cs
index b0f40f1ef1..1ee182b11b 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Configuration/GitHubClientFactoryConfiguration.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Configuration/GitHubClientFactoryConfiguration.cs
@@ -8,9 +8,10 @@ namespace ProductConstructionService.Api.Configuration;
public static class GitHubClientFactoryConfiguration
{
- private const string GitHubConfiguration = "GitHub";
-
- public static void AddGitHubClientFactory(this WebApplicationBuilder builder)
+ public static void AddGitHubClientFactory(
+ this WebApplicationBuilder builder,
+ string? appId,
+ string? appSecret)
{
builder.Services.Configure(o =>
{
@@ -22,6 +23,10 @@ public static void AddGitHubClientFactory(this WebApplicationBuilder builder)
});
builder.Services.AddSingleton();
- builder.Services.Configure(builder.Configuration.GetSection(GitHubConfiguration));
+ builder.Services.Configure(o =>
+ {
+ o.GitHubAppId = !string.IsNullOrEmpty(appId) ? int.Parse(appId) : 0;
+ o.PrivateKey = appSecret;
+ });
}
}
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Configuration/KeyVaultSecretsWithPrefix.cs b/src/ProductConstructionService/ProductConstructionService.Api/Configuration/KeyVaultSecretsWithPrefix.cs
new file mode 100644
index 0000000000..27754d0d14
--- /dev/null
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Configuration/KeyVaultSecretsWithPrefix.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Azure.Extensions.AspNetCore.Configuration.Secrets;
+using Azure.Security.KeyVault.Secrets;
+
+namespace ProductConstructionService.Api.Configuration;
+
+internal class KeyVaultSecretsWithPrefix(string prefix) : KeyVaultSecretManager
+{
+ private readonly string _prefix = prefix;
+
+ public override string GetKey(KeyVaultSecret secret)
+ => _prefix + secret.Name.Replace("--", ConfigurationPath.KeyDelimiter);
+}
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Dockerfile b/src/ProductConstructionService/ProductConstructionService.Api/Dockerfile
index 0f51b8d432..1e138d62a8 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Dockerfile
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Dockerfile
@@ -27,14 +27,16 @@ WORKDIR /src/ProductConstructionService
COPY ["src/ProductConstructionService/ProductConstructionService.Api/ProductConstructionService.Api.csproj", "./ProductConstructionService.Api/"]
COPY ["src/ProductConstructionService/ProductConstructionService.Common/ProductConstructionService.Common.csproj", "./ProductConstructionService.Common/"]
COPY ["src/ProductConstructionService/ProductConstructionService.DependencyFlow/ProductConstructionService.DependencyFlow.csproj", "./ProductConstructionService.DependencyFlow/"]
+COPY ["src/ProductConstructionService/ProductConstructionService.FeedCleaner/ProductConstructionService.FeedCleaner.csproj", "./ProductConstructionService.FeedCleaner/"]
COPY ["src/ProductConstructionService/ProductConstructionService.LongestBuildPathUpdater/ProductConstructionService.LongestBuildPathUpdater.csproj", "./ProductConstructionService.LongestBuildPathUpdater/"]
COPY ["src/ProductConstructionService/ProductConstructionService.ServiceDefaults/ProductConstructionService.ServiceDefaults.csproj", "./ProductConstructionService.ServiceDefaults/"]
COPY ["src/ProductConstructionService/ProductConstructionService.SubscriptionTriggerer/ProductConstructionService.SubscriptionTriggerer.csproj", "./ProductConstructionService.SubscriptionTriggerer/"]
COPY ["src/ProductConstructionService/ProductConstructionService.WorkItems/ProductConstructionService.WorkItems.csproj", "./ProductConstructionService.WorkItems/"]
RUN dotnet restore "./ProductConstructionService.Api/ProductConstructionService.Api.csproj"
-RUN dotnet restore "./ProductConstructionService.SubscriptionTriggerer/ProductConstructionService.SubscriptionTriggerer.csproj"
+RUN dotnet restore "./ProductConstructionService.FeedCleaner/ProductConstructionService.FeedCleaner.csproj"
RUN dotnet restore "./ProductConstructionService.LongestBuildPathUpdater/ProductConstructionService.LongestBuildPathUpdater.csproj"
+RUN dotnet restore "./ProductConstructionService.SubscriptionTriggerer/ProductConstructionService.SubscriptionTriggerer.csproj"
WORKDIR /src/Maestro
COPY ["src/Maestro/Client/src", "./Client/src"]
@@ -57,14 +59,18 @@ WORKDIR ./ProductConstructionService.Api
RUN dotnet build -c $BUILD_CONFIGURATION -o /app/build --no-restore
RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish/ProductConstructionService /p:UseAppHost=false
-WORKDIR ../ProductConstructionService.SubscriptionTriggerer
+WORKDIR ../ProductConstructionService.FeedCleaner
RUN dotnet build -c $BUILD_CONFIGURATION -o /app/build --no-restore
-RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish/SubscriptionTriggerer /p:UseAppHost=false
+RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish/FeedCleaner /p:UseAppHost=false
WORKDIR ../ProductConstructionService.LongestBuildPathUpdater
RUN dotnet build -c $BUILD_CONFIGURATION -o /app/build --no-restore
RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish/LongestBuildPathUpdater /p:UseAppHost=false
+WORKDIR ../ProductConstructionService.SubscriptionTriggerer
+RUN dotnet build -c $BUILD_CONFIGURATION -o /app/build --no-restore
+RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish/SubscriptionTriggerer /p:UseAppHost=false
+
# Build Angular app
FROM mcr.microsoft.com/devcontainers/typescript-node:dev-20 AS angular
WORKDIR /maestro-angular
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/InitializationBackgroundService.cs b/src/ProductConstructionService/ProductConstructionService.Api/InitializationBackgroundService.cs
index 5d523085b6..94fe1ad578 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/InitializationBackgroundService.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/InitializationBackgroundService.cs
@@ -27,6 +27,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
// If Vmr cloning is taking more than 20 min, something is wrong
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, new CancellationTokenSource(TimeSpan.FromMinutes(20)).Token);
+ IVmrInfo vmrInfo = scope.ServiceProvider.GetRequiredService();
+ vmrInfo.VmrUri = options.VmrUri;
IVmrCloneManager vmrCloneManager = scope.ServiceProvider.GetRequiredService();
await vmrCloneManager.PrepareVmrAsync("main", linkedTokenSource.Token);
linkedTokenSource.Token.ThrowIfCancellationRequested();
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Pages/Error.cshtml.cs b/src/ProductConstructionService/ProductConstructionService.Api/Pages/Error.cshtml.cs
index bee6ee6a84..2f0e73c027 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Pages/Error.cshtml.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Pages/Error.cshtml.cs
@@ -1,10 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ProductConstructionService.Api.Pages;
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/PcsStartup.cs b/src/ProductConstructionService/ProductConstructionService.Api/PcsStartup.cs
index a0303c56f4..0542b150c8 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/PcsStartup.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/PcsStartup.cs
@@ -36,8 +36,8 @@
using ProductConstructionService.Api.VirtualMonoRepo;
using ProductConstructionService.Common;
using ProductConstructionService.DependencyFlow.WorkItems;
-using ProductConstructionService.DependencyFlow.WorkItemProcessors;
using ProductConstructionService.WorkItems;
+using ProductConstructionService.DependencyFlow;
namespace ProductConstructionService.Api;
@@ -47,13 +47,19 @@ internal static class PcsStartup
private static class ConfigurationKeys
{
+ // All secrets loaded from KeyVault will have this prefix
+ public const string KeyVaultSecretPrefix = "KeyVaultSecrets:";
+
+ // Secrets coming from the KeyVault
+ public const string GitHubClientId = $"{KeyVaultSecretPrefix}github-app-id";
+ public const string GitHubClientSecret = $"{KeyVaultSecretPrefix}github-app-private-key";
+ public const string GitHubToken = $"{KeyVaultSecretPrefix}BotAccount-dotnet-bot-repo-PAT";
+
+ // Configuration from appsettings.json
public const string AzureDevOpsConfiguration = "AzureDevOps";
public const string DatabaseConnectionString = "BuildAssetRegistrySqlConnectionString";
public const string DependencyFlowSLAs = "DependencyFlowSLAs";
public const string EntraAuthenticationKey = "EntraAuthentication";
- public const string GitHubClientId = "github-oauth-id";
- public const string GitHubClientSecret = "github-oauth-secret";
- public const string GitHubToken = "BotAccount-dotnet-bot-repo-PAT";
public const string KeyVaultName = "KeyVaultName";
public const string ManagedIdentityId = "ManagedIdentityClientId";
}
@@ -75,7 +81,8 @@ static PcsStartup()
{
var context = (BuildAssetRegistryContext)entry.Context;
ILogger logger = context.GetService>();
- var workItemProducer = context.GetService().CreateProducer();
+ var workItemProducer = context.GetService().CreateProducer();
+ var subscriptionIdGenerator = context.GetService();
BuildChannel entity = entry.Entity;
Build? build = context.Builds
@@ -85,36 +92,37 @@ static PcsStartup()
if (build == null)
{
- logger.LogError($"Could not find build with id {entity.BuildId} in BAR. Skipping dependency update.");
+ logger.LogError("Could not find build with id {buildId} in BAR. Skipping dependency update.", entity.BuildId);
}
else
{
bool hasAssetsWithPublishedLocations = build.Assets
.Any(a => a.Locations.Any(al => al.Type != LocationType.None && !al.Location.EndsWith("/artifacts")));
- if (hasAssetsWithPublishedLocations)
+ if (!hasAssetsWithPublishedLocations)
{
- List subscriptionsToUpdate = context.Subscriptions
- .Where(sub =>
- sub.Enabled &&
- sub.ChannelId == entity.ChannelId &&
- (sub.SourceRepository == entity.Build.GitHubRepository || sub.SourceDirectory == entity.Build.AzureDevOpsRepository) &&
- JsonExtensions.JsonValue(sub.PolicyString, "lax $.UpdateFrequency") == ((int)UpdateFrequency.EveryBuild).ToString())
- .ToList();
-
- // TODO: https://github.com/dotnet/arcade-services/issues/3811 Add a feature switch to trigger specific subscriptions
- /*foreach (Subscription subscription in subscriptionsToUpdate)
- {
- workItemProducer.ProduceWorkItemAsync(new()
- {
- BuildId = entity.BuildId,
- SubscriptionId = subscription.Id
- }).GetAwaiter().GetResult();
- }*/
+ logger.LogInformation("Skipping Dependency update for Build {buildId} because it contains no assets in valid locations", entity.BuildId);
+ return;
}
- else
+
+ List subscriptionsToUpdate = context.Subscriptions
+ .Where(sub =>
+ sub.Enabled &&
+ sub.ChannelId == entity.ChannelId &&
+ (sub.SourceRepository == entity.Build.GitHubRepository || sub.SourceDirectory == entity.Build.AzureDevOpsRepository) &&
+ JsonExtensions.JsonValue(sub.PolicyString, "lax $.UpdateFrequency") == ((int)UpdateFrequency.EveryBuild).ToString())
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880)
+ .ToList()
+ .Where(sub => subscriptionIdGenerator.ShouldTriggerSubscription(sub.Id))
+ .ToList();
+
+ foreach (Subscription subscription in subscriptionsToUpdate)
{
- logger.LogInformation($"Skipping Dependency update for Build {entity.BuildId} because it contains no assets in valid locations");
+ workItemProducer.ProduceWorkItemAsync(new()
+ {
+ BuildId = entity.BuildId,
+ SubscriptionId = subscription.Id
+ }).GetAwaiter().GetResult();
}
}
};
@@ -143,28 +151,36 @@ internal static async Task ConfigurePcs(
string? gitHubToken = builder.Configuration[ConfigurationKeys.GitHubToken];
builder.Services.Configure(ConfigurationKeys.AzureDevOpsConfiguration, (o, s) => s.Bind(o));
- builder.AddDataProtection();
- builder.AddTelemetry();
-
DefaultAzureCredential azureCredential = new(new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = managedIdentityId,
});
+ builder.AddDataProtection(azureCredential);
+ builder.AddTelemetry();
+
if (addKeyVault)
{
Uri keyVaultUri = new($"https://{builder.Configuration.GetRequiredValue(ConfigurationKeys.KeyVaultName)}.vault.azure.net/");
- builder.Configuration.AddAzureKeyVault(keyVaultUri, azureCredential);
+ builder.Configuration.AddAzureKeyVault(
+ keyVaultUri,
+ azureCredential,
+ new KeyVaultSecretsWithPrefix(ConfigurationKeys.KeyVaultSecretPrefix));
}
+ // TODO (https://github.com/dotnet/arcade-services/issues/3880) - Remove subscriptionIdGenerator
+ builder.Services.AddSingleton(sp => new(RunningService.PCS));
+
builder.AddBuildAssetRegistry();
builder.AddWorkItemQueues(azureCredential, waitForInitialization: initializeService);
+ builder.AddDependencyFlowProcessors();
builder.AddVmrRegistrations(gitHubToken);
builder.AddMaestroApiClient(managedIdentityId);
- builder.AddGitHubClientFactory();
+ builder.AddGitHubClientFactory(
+ builder.Configuration[ConfigurationKeys.GitHubClientId],
+ builder.Configuration[ConfigurationKeys.GitHubClientSecret]);
builder.Services.AddGitHubTokenProvider();
builder.Services.AddScoped();
- builder.Services.AddWorkItemProcessor();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.Configure(_ => { });
@@ -265,6 +281,7 @@ internal static async Task ConfigurePcs(
public static void ConfigureApi(this IApplicationBuilder app, bool isDevelopment)
{
+ app.UseApiRedirection();
app.UseExceptionHandler(a =>
a.Run(async ctx =>
{
@@ -275,7 +292,6 @@ public static void ConfigureApi(this IApplicationBuilder app, bool isDevelopment
ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await ctx.Response.WriteAsync(output, Encoding.UTF8);
}));
- app.UseApiRedirection();
app.UseEndpoints(e =>
{
var controllers = e.MapControllers();
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Program.cs b/src/ProductConstructionService/ProductConstructionService.Api/Program.cs
index 70f377e1ca..19f9bf70c6 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/Program.cs
+++ b/src/ProductConstructionService/ProductConstructionService.Api/Program.cs
@@ -67,7 +67,6 @@ await app.Services.UseLocalWorkItemQueues(
app.UseStaticFiles();
}
-app.UseStatusCodePagesWithReExecute("/Error", "?code={0}");
app.UseCookiePolicy();
app.UseAuthentication();
app.UseRouting();
@@ -79,6 +78,7 @@ await app.Services.UseLocalWorkItemQueues(
a => PcsStartup.ConfigureApi(a, isDevelopment));
// Add security headers
+app.UseStatusCodePagesWithReExecute("/Error", "?code={0}");
app.ConfigureSecurityHeaders();
// Map pages and non-API controllers
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/appsettings.Development.json b/src/ProductConstructionService/ProductConstructionService.Api/appsettings.Development.json
index 52397e3ac4..bc78ea7503 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/appsettings.Development.json
+++ b/src/ProductConstructionService/ProductConstructionService.Api/appsettings.Development.json
@@ -1,11 +1,6 @@
{
"Logging": {
"LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning",
- "Azure.Core": "Warning",
- "Microsoft.EntityFrameworkCore.Database.Command": "Warning",
- "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information",
"Microsoft.DotNet.DarcLib": "Trace"
}
},
@@ -36,6 +31,6 @@
"Scope": [ "api://baf98f1b-374e-487d-af42-aa33807f11e4/Maestro.User" ]
},
"ApiRedirect": {
- "Uri": "https://maestro.dot.net/"
+ // "Uri": "https://maestro.int-dot.net/"
}
}
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/appsettings.Staging.json b/src/ProductConstructionService/ProductConstructionService.Api/appsettings.Staging.json
index 33f5f2c9dc..8dc3c18e2b 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/appsettings.Staging.json
+++ b/src/ProductConstructionService/ProductConstructionService.Api/appsettings.Staging.json
@@ -2,7 +2,7 @@
"KeyVaultName": "ProductConstructionInt",
"ConnectionStrings": {
"queues": "https://productconstructionint.queue.core.windows.net",
- "redis": "prodconstaging.redis.cache.windows.net:6380"
+ "redis": "prodconstaging.redis.cache.windows.net:6380,ssl=true"
},
"ManagedIdentityClientId": "1d43ba8a-c2a6-4fad-b064-6d8c16fc0745",
"VmrUri": "https://github.com/maestro-auth-test/dnceng-vmr",
@@ -28,9 +28,9 @@
"default": {
"ManagedIdentityId": "1d43ba8a-c2a6-4fad-b064-6d8c16fc0745"
}
- },
- "ApiRedirect": {
- "Uri": "https://maestro.dot.net/",
- "ManagedIdentityClientId": "1d43ba8a-c2a6-4fad-b064-6d8c16fc0745"
}
+ // "ApiRedirect": {
+ // "Uri": "https://maestro.dot.net/",
+ // "ManagedIdentityClientId": "1d43ba8a-c2a6-4fad-b064-6d8c16fc0745"
+ // }
}
diff --git a/src/ProductConstructionService/ProductConstructionService.Api/appsettings.json b/src/ProductConstructionService/ProductConstructionService.Api/appsettings.json
index 8e9d94ffd3..c0803ea3ac 100644
--- a/src/ProductConstructionService/ProductConstructionService.Api/appsettings.json
+++ b/src/ProductConstructionService/ProductConstructionService.Api/appsettings.json
@@ -1,14 +1,14 @@
{
- "GitHub": {
- "GitHubAppId": "[vault(github-app-id)]",
- "PrivateKey": "[vault(github-app-private-key)]"
- },
"Logging": {
"LogLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning",
+
"Azure.Core": "Warning",
- "Azure.Identity": "Warning"
+ "Azure.Identity": "Warning",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Warning",
+ "Microsoft.IdentityModel.LoggingExtensions.IdentityLoggerAdapter": "Warning"
}
},
"AllowedHosts": "*",
diff --git a/src/ProductConstructionService/ProductConstructionService.AppHost/Program.cs b/src/ProductConstructionService/ProductConstructionService.AppHost/Program.cs
index 96b33497cf..26202e1933 100644
--- a/src/ProductConstructionService/ProductConstructionService.AppHost/Program.cs
+++ b/src/ProductConstructionService/ProductConstructionService.AppHost/Program.cs
@@ -3,7 +3,7 @@
var builder = DistributedApplication.CreateBuilder(args);
-var redisCache = builder.AddRedis("redis");
+var redisCache = builder.AddRedis("redis", port: 55689);
var queues = builder.AddAzureStorage("storage")
.RunAsEmulator(emulator => emulator.WithImageTag("3.31.0")) // Workaround for https://github.com/dotnet/aspire/issues/5078
.AddQueues("queues");
diff --git a/src/ProductConstructionService/ProductConstructionService.Client/Generated/Assets.cs b/src/ProductConstructionService/ProductConstructionService.Client/Generated/Assets.cs
new file mode 100644
index 0000000000..27b2e13252
--- /dev/null
+++ b/src/ProductConstructionService/ProductConstructionService.Client/Generated/Assets.cs
@@ -0,0 +1,598 @@
+// 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.Immutable;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Core;
+
+
+
+namespace ProductConstructionService.Client
+{
+ public partial interface IAssets
+ {
+ Task BulkAddLocationsAsync(
+ IImmutableList body,
+ CancellationToken cancellationToken = default
+ );
+
+ AsyncPageable ListAssetsAsync(
+ int? buildId = default,
+ bool? loadLocations = default,
+ string name = default,
+ bool? nonShipping = default,
+ string version = default,
+ CancellationToken cancellationToken = default
+ );
+
+ Task> ListAssetsPageAsync(
+ int? buildId = default,
+ bool? loadLocations = default,
+ string name = default,
+ bool? nonShipping = default,
+ int? page = default,
+ int? perPage = default,
+ string version = default,
+ CancellationToken cancellationToken = default
+ );
+
+ Task GetDarcVersionAsync(
+ CancellationToken cancellationToken = default
+ );
+
+ Task GetAssetAsync(
+ int id,
+ CancellationToken cancellationToken = default
+ );
+
+ Task AddAssetLocationToAssetAsync(
+ int assetId,
+ Models.LocationType assetLocationType,
+ string location,
+ CancellationToken cancellationToken = default
+ );
+
+ Task RemoveAssetLocationFromAssetAsync(
+ int assetId,
+ int assetLocationId,
+ CancellationToken cancellationToken = default
+ );
+
+ }
+
+ internal partial class Assets : IServiceOperations, IAssets
+ {
+ public Assets(ProductConstructionServiceApi client)
+ {
+ Client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ public ProductConstructionServiceApi Client { get; }
+
+ partial void HandleFailedRequest(RestApiException ex);
+
+ partial void HandleFailedBulkAddLocationsRequest(RestApiException ex);
+
+ public async Task BulkAddLocationsAsync(
+ IImmutableList body,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ if (body == default(IImmutableList))
+ {
+ throw new ArgumentNullException(nameof(body));
+ }
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/assets/bulk-add-locations",
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Post;
+
+ if (body != default(IImmutableList))
+ {
+ _req.Content = RequestContent.Create(Encoding.UTF8.GetBytes(Client.Serialize(body)));
+ _req.Headers.Add("Content-Type", "application/json; charset=utf-8");
+ }
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnBulkAddLocationsFailed(_req, _res).ConfigureAwait(false);
+ }
+
+
+ return;
+ }
+ }
+ }
+
+ internal async Task OnBulkAddLocationsFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedBulkAddLocationsRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedListAssetsRequest(RestApiException ex);
+
+ public AsyncPageable ListAssetsAsync(
+ int? buildId = default,
+ bool? loadLocations = default,
+ string name = default,
+ bool? nonShipping = default,
+ string version = default,
+ CancellationToken cancellationToken = default
+ )
+ {
+ async IAsyncEnumerable> GetPages(string _continueToken, int? _pageSizeHint)
+ {
+ int? page = 1;
+ int? perPage = _pageSizeHint;
+
+ if (!string.IsNullOrEmpty(_continueToken))
+ {
+ page = int.Parse(_continueToken);
+ }
+
+ while (true)
+ {
+ Page _page = null;
+
+ try {
+ _page = await ListAssetsPageAsync(
+ buildId,
+ loadLocations,
+ name,
+ nonShipping,
+ page,
+ perPage,
+ version,
+ cancellationToken
+ ).ConfigureAwait(false);
+ if (_page.Values.Count < 1)
+ {
+ yield break;
+ }
+ }
+ catch (RestApiException e) when (e.Response.Status == 404)
+ {
+ yield break;
+ }
+
+ yield return _page;
+ page++;
+ }
+ }
+ return AsyncPageable.Create(GetPages);
+ }
+
+ public async Task> ListAssetsPageAsync(
+ int? buildId = default,
+ bool? loadLocations = default,
+ string name = default,
+ bool? nonShipping = default,
+ int? page = default,
+ int? perPage = default,
+ string version = default,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/assets",
+ false);
+
+ if (!string.IsNullOrEmpty(name))
+ {
+ _url.AppendQuery("name", Client.Serialize(name));
+ }
+ if (!string.IsNullOrEmpty(version))
+ {
+ _url.AppendQuery("version", Client.Serialize(version));
+ }
+ if (buildId != default)
+ {
+ _url.AppendQuery("buildId", Client.Serialize(buildId));
+ }
+ if (nonShipping != default)
+ {
+ _url.AppendQuery("nonShipping", Client.Serialize(nonShipping));
+ }
+ if (loadLocations != default)
+ {
+ _url.AppendQuery("loadLocations", Client.Serialize(loadLocations));
+ }
+ if (page != default)
+ {
+ _url.AppendQuery("page", Client.Serialize(page));
+ }
+ if (perPage != default)
+ {
+ _url.AppendQuery("perPage", Client.Serialize(perPage));
+ }
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnListAssetsFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnListAssetsFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize>(_content);
+ return Page.FromValues(_body, (page + 1).ToString(), _res);
+ }
+ }
+ }
+ }
+
+ internal async Task OnListAssetsFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedListAssetsRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedGetDarcVersionRequest(RestApiException ex);
+
+ public async Task GetDarcVersionAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/assets/darc-version",
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnGetDarcVersionFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnGetDarcVersionFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnGetDarcVersionFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedGetDarcVersionRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedGetAssetRequest(RestApiException ex);
+
+ public async Task GetAssetAsync(
+ int id,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/assets/{id}".Replace("{id}", Uri.EscapeDataString(Client.Serialize(id))),
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnGetAssetFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnGetAssetFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnGetAssetFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedGetAssetRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedAddAssetLocationToAssetRequest(RestApiException ex);
+
+ public async Task AddAssetLocationToAssetAsync(
+ int assetId,
+ Models.LocationType assetLocationType,
+ string location,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ if (assetLocationType == default)
+ {
+ throw new ArgumentNullException(nameof(assetLocationType));
+ }
+
+ if (string.IsNullOrEmpty(location))
+ {
+ throw new ArgumentNullException(nameof(location));
+ }
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/assets/{assetId}/locations".Replace("{assetId}", Uri.EscapeDataString(Client.Serialize(assetId))),
+ false);
+
+ if (!string.IsNullOrEmpty(location))
+ {
+ _url.AppendQuery("location", Client.Serialize(location));
+ }
+ if (assetLocationType != default)
+ {
+ _url.AppendQuery("assetLocationType", Client.Serialize(assetLocationType));
+ }
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Post;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnAddAssetLocationToAssetFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnAddAssetLocationToAssetFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnAddAssetLocationToAssetFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedAddAssetLocationToAssetRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedRemoveAssetLocationFromAssetRequest(RestApiException ex);
+
+ public async Task RemoveAssetLocationFromAssetAsync(
+ int assetId,
+ int assetLocationId,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/assets/{assetId}/locations/{assetLocationId}".Replace("{assetId}", Uri.EscapeDataString(Client.Serialize(assetId))).Replace("{assetLocationId}", Uri.EscapeDataString(Client.Serialize(assetLocationId))),
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Delete;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnRemoveAssetLocationFromAssetFailed(_req, _res).ConfigureAwait(false);
+ }
+
+
+ return;
+ }
+ }
+ }
+
+ internal async Task OnRemoveAssetLocationFromAssetFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedRemoveAssetLocationFromAssetRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+ }
+}
diff --git a/src/ProductConstructionService/ProductConstructionService.Client/Generated/BuildTime.cs b/src/ProductConstructionService/ProductConstructionService.Client/Generated/BuildTime.cs
new file mode 100644
index 0000000000..fc8c050538
--- /dev/null
+++ b/src/ProductConstructionService/ProductConstructionService.Client/Generated/BuildTime.cs
@@ -0,0 +1,111 @@
+// 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Core;
+
+
+
+namespace ProductConstructionService.Client
+{
+ public partial interface IBuildTime
+ {
+ Task GetBuildTimesAsync(
+ int days,
+ int id,
+ CancellationToken cancellationToken = default
+ );
+
+ }
+
+ internal partial class BuildTime : IServiceOperations, IBuildTime
+ {
+ public BuildTime(ProductConstructionServiceApi client)
+ {
+ Client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ public ProductConstructionServiceApi Client { get; }
+
+ partial void HandleFailedRequest(RestApiException ex);
+
+ partial void HandleFailedGetBuildTimesRequest(RestApiException ex);
+
+ public async Task GetBuildTimesAsync(
+ int days,
+ int id,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/buildtime/{id}".Replace("{id}", Uri.EscapeDataString(Client.Serialize(id))),
+ false);
+
+ if (days != default)
+ {
+ _url.AppendQuery("days", Client.Serialize(days));
+ }
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnGetBuildTimesFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnGetBuildTimesFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnGetBuildTimesFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedGetBuildTimesRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+ }
+}
diff --git a/src/ProductConstructionService/ProductConstructionService.Client/Generated/Builds.cs b/src/ProductConstructionService/ProductConstructionService.Client/Generated/Builds.cs
new file mode 100644
index 0000000000..3718298afd
--- /dev/null
+++ b/src/ProductConstructionService/ProductConstructionService.Client/Generated/Builds.cs
@@ -0,0 +1,772 @@
+// 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.Immutable;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure;
+using Azure.Core;
+
+
+
+namespace ProductConstructionService.Client
+{
+ public partial interface IBuilds
+ {
+ AsyncPageable ListBuildsAsync(
+ string azdoAccount = default,
+ int? azdoBuildId = default,
+ string azdoProject = default,
+ string buildNumber = default,
+ string commit = default,
+ int? channelId = default,
+ bool? loadCollections = default,
+ DateTimeOffset? notAfter = default,
+ DateTimeOffset? notBefore = default,
+ string repository = default,
+ CancellationToken cancellationToken = default
+ );
+
+ Task> ListBuildsPageAsync(
+ string azdoAccount = default,
+ int? azdoBuildId = default,
+ string azdoProject = default,
+ string buildNumber = default,
+ string commit = default,
+ int? channelId = default,
+ bool? loadCollections = default,
+ DateTimeOffset? notAfter = default,
+ DateTimeOffset? notBefore = default,
+ int? page = default,
+ int? perPage = default,
+ string repository = default,
+ CancellationToken cancellationToken = default
+ );
+
+ Task CreateAsync(
+ Models.BuildData body,
+ CancellationToken cancellationToken = default
+ );
+
+ Task GetBuildAsync(
+ int id,
+ CancellationToken cancellationToken = default
+ );
+
+ Task GetBuildGraphAsync(
+ int id,
+ CancellationToken cancellationToken = default
+ );
+
+ Task GetLatestAsync(
+ string buildNumber = default,
+ string commit = default,
+ int? channelId = default,
+ bool? loadCollections = default,
+ DateTimeOffset? notAfter = default,
+ DateTimeOffset? notBefore = default,
+ string repository = default,
+ CancellationToken cancellationToken = default
+ );
+
+ Task GetCommitAsync(
+ int buildId,
+ CancellationToken cancellationToken = default
+ );
+
+ Task UpdateAsync(
+ Models.BuildUpdate body,
+ int buildId,
+ CancellationToken cancellationToken = default
+ );
+
+ }
+
+ internal partial class Builds : IServiceOperations, IBuilds
+ {
+ public Builds(ProductConstructionServiceApi client)
+ {
+ Client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ public ProductConstructionServiceApi Client { get; }
+
+ partial void HandleFailedRequest(RestApiException ex);
+
+ partial void HandleFailedListBuildsRequest(RestApiException ex);
+
+ public AsyncPageable ListBuildsAsync(
+ string azdoAccount = default,
+ int? azdoBuildId = default,
+ string azdoProject = default,
+ string buildNumber = default,
+ string commit = default,
+ int? channelId = default,
+ bool? loadCollections = default,
+ DateTimeOffset? notAfter = default,
+ DateTimeOffset? notBefore = default,
+ string repository = default,
+ CancellationToken cancellationToken = default
+ )
+ {
+ async IAsyncEnumerable> GetPages(string _continueToken, int? _pageSizeHint)
+ {
+ int? page = 1;
+ int? perPage = _pageSizeHint;
+
+ if (!string.IsNullOrEmpty(_continueToken))
+ {
+ page = int.Parse(_continueToken);
+ }
+
+ while (true)
+ {
+ Page _page = null;
+
+ try {
+ _page = await ListBuildsPageAsync(
+ azdoAccount,
+ azdoBuildId,
+ azdoProject,
+ buildNumber,
+ commit,
+ channelId,
+ loadCollections,
+ notAfter,
+ notBefore,
+ page,
+ perPage,
+ repository,
+ cancellationToken
+ ).ConfigureAwait(false);
+ if (_page.Values.Count < 1)
+ {
+ yield break;
+ }
+ }
+ catch (RestApiException e) when (e.Response.Status == 404)
+ {
+ yield break;
+ }
+
+ yield return _page;
+ page++;
+ }
+ }
+ return AsyncPageable.Create(GetPages);
+ }
+
+ public async Task> ListBuildsPageAsync(
+ string azdoAccount = default,
+ int? azdoBuildId = default,
+ string azdoProject = default,
+ string buildNumber = default,
+ string commit = default,
+ int? channelId = default,
+ bool? loadCollections = default,
+ DateTimeOffset? notAfter = default,
+ DateTimeOffset? notBefore = default,
+ int? page = default,
+ int? perPage = default,
+ string repository = default,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/builds",
+ false);
+
+ if (!string.IsNullOrEmpty(repository))
+ {
+ _url.AppendQuery("repository", Client.Serialize(repository));
+ }
+ if (!string.IsNullOrEmpty(commit))
+ {
+ _url.AppendQuery("commit", Client.Serialize(commit));
+ }
+ if (!string.IsNullOrEmpty(buildNumber))
+ {
+ _url.AppendQuery("buildNumber", Client.Serialize(buildNumber));
+ }
+ if (azdoBuildId != default)
+ {
+ _url.AppendQuery("azdoBuildId", Client.Serialize(azdoBuildId));
+ }
+ if (!string.IsNullOrEmpty(azdoAccount))
+ {
+ _url.AppendQuery("azdoAccount", Client.Serialize(azdoAccount));
+ }
+ if (!string.IsNullOrEmpty(azdoProject))
+ {
+ _url.AppendQuery("azdoProject", Client.Serialize(azdoProject));
+ }
+ if (channelId != default)
+ {
+ _url.AppendQuery("channelId", Client.Serialize(channelId));
+ }
+ if (notBefore != default)
+ {
+ _url.AppendQuery("notBefore", Client.Serialize(notBefore));
+ }
+ if (notAfter != default)
+ {
+ _url.AppendQuery("notAfter", Client.Serialize(notAfter));
+ }
+ if (loadCollections != default)
+ {
+ _url.AppendQuery("loadCollections", Client.Serialize(loadCollections));
+ }
+ if (page != default)
+ {
+ _url.AppendQuery("page", Client.Serialize(page));
+ }
+ if (perPage != default)
+ {
+ _url.AppendQuery("perPage", Client.Serialize(perPage));
+ }
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnListBuildsFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnListBuildsFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize>(_content);
+ return Page.FromValues(_body, (page + 1).ToString(), _res);
+ }
+ }
+ }
+ }
+
+ internal async Task OnListBuildsFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedListBuildsRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedCreateRequest(RestApiException ex);
+
+ public async Task CreateAsync(
+ Models.BuildData body,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ if (body == default(Models.BuildData))
+ {
+ throw new ArgumentNullException(nameof(body));
+ }
+
+ if (!body.IsValid)
+ {
+ throw new ArgumentException("The parameter is not valid", nameof(body));
+ }
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/builds",
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Post;
+
+ if (body != default(Models.BuildData))
+ {
+ _req.Content = RequestContent.Create(Encoding.UTF8.GetBytes(Client.Serialize(body)));
+ _req.Headers.Add("Content-Type", "application/json; charset=utf-8");
+ }
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnCreateFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnCreateFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnCreateFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedCreateRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedGetBuildRequest(RestApiException ex);
+
+ public async Task GetBuildAsync(
+ int id,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/builds/{id}".Replace("{id}", Uri.EscapeDataString(Client.Serialize(id))),
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnGetBuildFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnGetBuildFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnGetBuildFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedGetBuildRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedGetBuildGraphRequest(RestApiException ex);
+
+ public async Task GetBuildGraphAsync(
+ int id,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/builds/{id}/graph".Replace("{id}", Uri.EscapeDataString(Client.Serialize(id))),
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnGetBuildGraphFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnGetBuildGraphFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnGetBuildGraphFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedGetBuildGraphRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedGetLatestRequest(RestApiException ex);
+
+ public async Task GetLatestAsync(
+ string buildNumber = default,
+ string commit = default,
+ int? channelId = default,
+ bool? loadCollections = default,
+ DateTimeOffset? notAfter = default,
+ DateTimeOffset? notBefore = default,
+ string repository = default,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/builds/latest",
+ false);
+
+ if (!string.IsNullOrEmpty(repository))
+ {
+ _url.AppendQuery("repository", Client.Serialize(repository));
+ }
+ if (!string.IsNullOrEmpty(commit))
+ {
+ _url.AppendQuery("commit", Client.Serialize(commit));
+ }
+ if (!string.IsNullOrEmpty(buildNumber))
+ {
+ _url.AppendQuery("buildNumber", Client.Serialize(buildNumber));
+ }
+ if (channelId != default)
+ {
+ _url.AppendQuery("channelId", Client.Serialize(channelId));
+ }
+ if (notBefore != default)
+ {
+ _url.AppendQuery("notBefore", Client.Serialize(notBefore));
+ }
+ if (notAfter != default)
+ {
+ _url.AppendQuery("notAfter", Client.Serialize(notAfter));
+ }
+ if (loadCollections != default)
+ {
+ _url.AppendQuery("loadCollections", Client.Serialize(loadCollections));
+ }
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnGetLatestFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnGetLatestFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnGetLatestFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedGetLatestRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedGetCommitRequest(RestApiException ex);
+
+ public async Task GetCommitAsync(
+ int buildId,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/builds/{buildId}/commit".Replace("{buildId}", Uri.EscapeDataString(Client.Serialize(buildId))),
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Get;
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnGetCommitFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnGetCommitFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnGetCommitFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize(content)
+ );
+ HandleFailedGetCommitRequest(ex);
+ HandleFailedRequest(ex);
+ Client.OnFailedRequest(ex);
+ throw ex;
+ }
+
+ partial void HandleFailedUpdateRequest(RestApiException ex);
+
+ public async Task UpdateAsync(
+ Models.BuildUpdate body,
+ int buildId,
+ CancellationToken cancellationToken = default
+ )
+ {
+
+ if (body == default(Models.BuildUpdate))
+ {
+ throw new ArgumentNullException(nameof(body));
+ }
+
+ const string apiVersion = "2020-02-20";
+
+ var _baseUri = Client.Options.BaseUri;
+ var _url = new RequestUriBuilder();
+ _url.Reset(_baseUri);
+ _url.AppendPath(
+ "/api/builds/{buildId}".Replace("{buildId}", Uri.EscapeDataString(Client.Serialize(buildId))),
+ false);
+
+ _url.AppendQuery("api-version", Client.Serialize(apiVersion));
+
+
+ using (var _req = Client.Pipeline.CreateRequest())
+ {
+ _req.Uri = _url;
+ _req.Method = RequestMethod.Patch;
+
+ if (body != default(Models.BuildUpdate))
+ {
+ _req.Content = RequestContent.Create(Encoding.UTF8.GetBytes(Client.Serialize(body)));
+ _req.Headers.Add("Content-Type", "application/json; charset=utf-8");
+ }
+
+ using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
+ {
+ if (_res.Status < 200 || _res.Status >= 300)
+ {
+ await OnUpdateFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ if (_res.ContentStream == null)
+ {
+ await OnUpdateFailed(_req, _res).ConfigureAwait(false);
+ }
+
+ using (var _reader = new StreamReader(_res.ContentStream))
+ {
+ var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
+ var _body = Client.Deserialize(_content);
+ return _body;
+ }
+ }
+ }
+ }
+
+ internal async Task OnUpdateFailed(Request req, Response res)
+ {
+ string content = null;
+ if (res.ContentStream != null)
+ {
+ using (var reader = new StreamReader(res.ContentStream))
+ {
+ content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+ }
+
+ var ex = new RestApiException(
+ req,
+ res,
+ content,
+ Client.Deserialize