Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions Documentation/AzureDevOps/SendingJobsToHelix.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,30 @@ steps:

### Internal builds

In the dev.azure.com/dnceng/internal project, you can use the `DotNet-HelixApi-Access` variable group to provide this secret to your build and then specify the `HelixApiAccessToken` secret for the `HelixAccessToken` parameter.
Internal builds can authenticate with Entra ID through an Azure service connection or with a legacy Helix access token.

Please note that authorized jobs *cannot* be submitted to queues with `IsInternalOnly` set to false. To determine this value for a particular queue, see the list of available queues [here](https://helix.dot.net/api/2018-03-14/info/queues).

#### Entra ID authentication

Set `HelixUseEntraAuthentication` to `true` and pass an Azure service connection authorized for Helix through `HelixAzureSubscription`. These parameters configure the `send-to-helix.yml` steps template and the SDK tasks that submit jobs.

When Entra authentication is enabled, the template does not forward `HelixAccessToken` to the Helix processes. If a legacy token is still injected by a variable group, explicit Entra opt-in takes precedence and the task ignores the token with a warning.

```yaml
steps:
- template: /eng/common/templates/steps/send-to-helix.yml
displayName: Send to Helix
parameters:
HelixUseEntraAuthentication: true
HelixAzureSubscription: <Azure service connection ID authorized for Helix>
# other parameters here
```

#### Legacy access-token authentication

In the dev.azure.com/dnceng/internal project, you can use the `DotNet-HelixApi-Access` variable group to provide this secret to your build and then specify the `HelixApiAccessToken` secret for the `HelixAccessToken` parameter.

Example:

```yaml
Expand Down Expand Up @@ -155,4 +175,3 @@ As surfaced by the Helix API and backing Kusto (Azure Data Explorer) database, h
- InfraRetry – Work item completed as expected, but on the 2nd-Nth attempt; this can be a requested-by-the-workitem retry, machine being rebooted or deleted during execution, or any number of random Azure components being flaky. Typically ignoreable for test runs.
- PassOnRetry – Special legacy retry functionality which is purposefully obsoleted as it does not play well with Azure DevOps test reporting (reporting the same facts twice causes issues)
- Timeout – Work Item did not complete within its specified timeout and was forcibly killed. Corresponds to exit code -3 (made up value since the process never exited)

44 changes: 40 additions & 4 deletions eng/common/templates-official/steps/send-to-helix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ parameters:
HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/'
HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number
HelixTargetQueues: '' # required -- semicolon delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues
HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group
HelixAccessToken: '' # optional -- legacy access token; not forwarded when HelixUseEntraAuthentication is true
HelixUseEntraAuthentication: false # optional -- use refreshable Entra authentication instead of a PAT or anonymous access
HelixAzureSubscription: '' # required when HelixUseEntraAuthentication is true -- Azure service connection ID authorized for Helix
HelixConfiguration: '' # optional -- additional property attached to a job
HelixPreCommands: '' # optional -- commands to run before Helix work item execution
HelixPostCommands: '' # optional -- commands to run after Helix work item execution
Expand All @@ -22,14 +24,36 @@ parameters:
DotNetCliVersion: '' # optional -- version of the CLI to send to Helix; based on this: https://github.com/ghraw/dotnet/core/main/release-notes/releases-index.json
EnableXUnitReporter: false # optional -- true enables XUnit result reporting to Mission Control
WaitForWorkItemCompletion: true # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is "fire and forget."
IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set
IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if Entra is disabled, HelixAccessToken is empty, and Creator is set
HelixBaseUri: 'https://helix.dot.net/' # optional -- sets the Helix API base URI (allows targeting int)
Creator: '' # optional -- if the build is external, use this to specify who is sending the job
DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO
condition: succeeded() # optional -- condition for step to execute; defaults to succeeded()
continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false

steps:
- ${{ if and(eq(parameters.HelixUseEntraAuthentication, true), eq(parameters.HelixAzureSubscription, '')) }}:
- pwsh: throw "HelixAzureSubscription must be set when HelixUseEntraAuthentication is true."
displayName: Validate Helix Entra authentication
condition: ${{ parameters.condition }}

- ${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
- task: AzureCLI@2
displayName: Initialize Helix Entra authentication
inputs:
azureSubscription: ${{ parameters.HelixAzureSubscription }}
addSpnToEnvironment: true
scriptType: pscore
scriptLocation: inlineScript
inlineScript: |
if ([string]::IsNullOrWhiteSpace($env:servicePrincipalId) -or [string]::IsNullOrWhiteSpace($env:tenantId)) {
throw "The Helix Azure service connection did not provide a service principal or tenant ID."
}

Write-Host "##vso[task.setvariable variable=HelixEntraClientId]$env:servicePrincipalId"
Write-Host "##vso[task.setvariable variable=HelixEntraTenantId]$env:tenantId"
condition: ${{ parameters.condition }}

- powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj /restore /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"'
displayName: ${{ parameters.DisplayNamePrefix }} (Windows)
env:
Expand All @@ -39,7 +63,13 @@ steps:
HelixBuild: ${{ parameters.HelixBuild }}
HelixConfiguration: ${{ parameters.HelixConfiguration }}
HelixTargetQueues: ${{ parameters.HelixTargetQueues }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixUseEntraAuthentication: ${{ parameters.HelixUseEntraAuthentication }}
Comment thread
missymessa marked this conversation as resolved.
${{ if eq(parameters.HelixUseEntraAuthentication, false) }}:
HelixAccessToken: ${{ parameters.HelixAccessToken }}
${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId)
AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId)
AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.HelixAzureSubscription }}
HelixPreCommands: ${{ parameters.HelixPreCommands }}
HelixPostCommands: ${{ parameters.HelixPostCommands }}
WorkItemDirectory: ${{ parameters.WorkItemDirectory }}
Expand Down Expand Up @@ -70,7 +100,13 @@ steps:
HelixBuild: ${{ parameters.HelixBuild }}
HelixConfiguration: ${{ parameters.HelixConfiguration }}
HelixTargetQueues: ${{ parameters.HelixTargetQueues }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixUseEntraAuthentication: ${{ parameters.HelixUseEntraAuthentication }}
${{ if eq(parameters.HelixUseEntraAuthentication, false) }}:
HelixAccessToken: ${{ parameters.HelixAccessToken }}
${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId)
AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId)
AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.HelixAzureSubscription }}
HelixPreCommands: ${{ parameters.HelixPreCommands }}
HelixPostCommands: ${{ parameters.HelixPostCommands }}
WorkItemDirectory: ${{ parameters.WorkItemDirectory }}
Expand Down
44 changes: 40 additions & 4 deletions eng/common/templates/steps/send-to-helix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ parameters:
HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/'
HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number
HelixTargetQueues: '' # required -- semicolon delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues
HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group
HelixAccessToken: '' # optional -- legacy access token; not forwarded when HelixUseEntraAuthentication is true
HelixUseEntraAuthentication: false # optional -- use refreshable Entra authentication instead of a PAT or anonymous access
HelixAzureSubscription: '' # required when HelixUseEntraAuthentication is true -- Azure service connection ID authorized for Helix
HelixConfiguration: '' # optional -- additional property attached to a job
HelixPreCommands: '' # optional -- commands to run before Helix work item execution
HelixPostCommands: '' # optional -- commands to run after Helix work item execution
Expand All @@ -22,14 +24,36 @@ parameters:
DotNetCliVersion: '' # optional -- version of the CLI to send to Helix; based on this: https://github.com/ghraw/dotnet/core/main/release-notes/releases-index.json
EnableXUnitReporter: false # optional -- true enables XUnit result reporting to Mission Control
WaitForWorkItemCompletion: true # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is "fire and forget."
IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set
IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if Entra is disabled, HelixAccessToken is empty, and Creator is set
HelixBaseUri: 'https://helix.dot.net/' # optional -- sets the Helix API base URI (allows targeting int)
Creator: '' # optional -- if the build is external, use this to specify who is sending the job
DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO
condition: succeeded() # optional -- condition for step to execute; defaults to succeeded()
continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false

steps:
- ${{ if and(eq(parameters.HelixUseEntraAuthentication, true), eq(parameters.HelixAzureSubscription, '')) }}:
- pwsh: throw "HelixAzureSubscription must be set when HelixUseEntraAuthentication is true."
displayName: Validate Helix Entra authentication
condition: ${{ parameters.condition }}

- ${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
- task: AzureCLI@2
displayName: Initialize Helix Entra authentication
inputs:
azureSubscription: ${{ parameters.HelixAzureSubscription }}
addSpnToEnvironment: true
scriptType: pscore
scriptLocation: inlineScript
inlineScript: |
if ([string]::IsNullOrWhiteSpace($env:servicePrincipalId) -or [string]::IsNullOrWhiteSpace($env:tenantId)) {
throw "The Helix Azure service connection did not provide a service principal or tenant ID."
}

Write-Host "##vso[task.setvariable variable=HelixEntraClientId]$env:servicePrincipalId"
Write-Host "##vso[task.setvariable variable=HelixEntraTenantId]$env:tenantId"
condition: ${{ parameters.condition }}

- powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj /restore /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"'
displayName: ${{ parameters.DisplayNamePrefix }} (Windows)
env:
Expand All @@ -39,7 +63,13 @@ steps:
HelixBuild: ${{ parameters.HelixBuild }}
HelixConfiguration: ${{ parameters.HelixConfiguration }}
HelixTargetQueues: ${{ parameters.HelixTargetQueues }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixUseEntraAuthentication: ${{ parameters.HelixUseEntraAuthentication }}
Comment thread
missymessa marked this conversation as resolved.
${{ if eq(parameters.HelixUseEntraAuthentication, false) }}:
HelixAccessToken: ${{ parameters.HelixAccessToken }}
${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId)
AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId)
AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.HelixAzureSubscription }}
HelixPreCommands: ${{ parameters.HelixPreCommands }}
HelixPostCommands: ${{ parameters.HelixPostCommands }}
WorkItemDirectory: ${{ parameters.WorkItemDirectory }}
Expand Down Expand Up @@ -70,7 +100,13 @@ steps:
HelixBuild: ${{ parameters.HelixBuild }}
HelixConfiguration: ${{ parameters.HelixConfiguration }}
HelixTargetQueues: ${{ parameters.HelixTargetQueues }}
HelixAccessToken: ${{ parameters.HelixAccessToken }}
HelixUseEntraAuthentication: ${{ parameters.HelixUseEntraAuthentication }}
${{ if eq(parameters.HelixUseEntraAuthentication, false) }}:
HelixAccessToken: ${{ parameters.HelixAccessToken }}
${{ if eq(parameters.HelixUseEntraAuthentication, true) }}:
AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId)
AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId)
AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.HelixAzureSubscription }}
HelixPreCommands: ${{ parameters.HelixPreCommands }}
HelixPostCommands: ${{ parameters.HelixPostCommands }}
WorkItemDirectory: ${{ parameters.WorkItemDirectory }}
Expand Down
50 changes: 50 additions & 0 deletions src/Microsoft.DotNet.Helix/Client/CSharp/ApiFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Azure.Core;

namespace Microsoft.DotNet.Helix.Client
{
Expand All @@ -17,6 +18,15 @@ public static IHelixApi GetAuthenticated(string accessToken)
return new HelixApi(new HelixApiOptions(new HelixApiTokenCredential(accessToken)));
}

/// <summary>
/// Obtains an API client using an Entra credential for authenticated access to internal queues.
/// The client requests the production Helix API scope and refreshes tokens based on their expiry.
/// </summary>
public static IHelixApi GetAuthenticatedWithEntra(TokenCredential credential)
{
return new HelixApi(new HelixApiOptions(ValidateEntraCredential(credential)));
}

/// <summary>
/// Obtains API client for unauthenticated access to external queues.
/// The client will access production Helix instance.
Expand All @@ -43,6 +53,29 @@ public static IHelixApi GetAuthenticated(string baseUri, string accessToken)
return new HelixApi(new HelixApiOptions(new Uri(baseUri), new HelixApiTokenCredential(accessToken)));
}

/// <summary>
/// Obtains an API client using an Entra credential for authenticated access to the provided Helix instance.
/// Production and staging scopes are selected from the base URI.
/// </summary>
public static IHelixApi GetAuthenticatedWithEntra(string baseUri, TokenCredential credential)
{
return new HelixApi(new HelixApiOptions(new Uri(baseUri), ValidateEntraCredential(credential)));
}

/// <summary>
/// Obtains an API client using an Entra credential and explicit scope for a custom Helix instance.
/// </summary>
public static IHelixApi GetAuthenticatedWithEntra(
string baseUri,
TokenCredential credential,
string scope)
{
return new HelixApi(new HelixApiOptions(
new Uri(baseUri),
ValidateEntraCredential(credential),
new[] { scope }));
}

/// <summary>
/// Obtains API client for unauthenticated access to external queues.
/// The client will access Helix instance at the provided URI.
Expand All @@ -55,5 +88,22 @@ public static IHelixApi GetAnonymous(string baseUri)
{
return new HelixApi(new HelixApiOptions(new Uri(baseUri)));
}

private static TokenCredential ValidateEntraCredential(TokenCredential credential)
{
if (credential == null)
{
throw new ArgumentNullException(nameof(credential));
}

if (credential is HelixApiTokenCredential)
{
throw new ArgumentException(
"HelixApiTokenCredential represents a PAT. Use GetAuthenticated(...) for PAT authentication.",
nameof(credential));
}

return credential;
}
}
}
Loading
Loading