Skip to content
13 changes: 11 additions & 2 deletions .github/workflows/quest-bulk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,23 @@ jobs:
client-id: ${{ secrets.CLIENT_ID }}
tenant-id: ${{ secrets.TENANT_ID }}
audience: ${{ secrets.OSMP_API_AUDIENCE }}


- name: Azure DevOps OpenID Connect
id: azure-devops-oidc-auth
uses: dotnet/docs-tools/.github/actions/oidc-auth-flow@main
with:
client-id: ${{ secrets.QUEST_CLIENT_ID }}
tenant-id: ${{ secrets.TENANT_ID }}
audience: ${{ secrets.QUEST_AUDIENCE }}

- name: bulk-sequester
id: bulk-sequester
uses: dotnet/docs-tools/actions/sequester@main
uses: dotnet/docs-tools/actions/sequester@going-secretless
env:
ImportOptions__ApiKeys__GitHubToken: ${{ secrets.GITHUB_TOKEN }}
ImportOptions__ApiKeys__QuestKey: ${{ secrets.QUEST_KEY }}
ImportOptions__ApiKeys__AzureAccessToken: ${{ steps.azure-oidc-auth.outputs.access-token }}
ImportOptions__ApiKeys__QuestAccessToken: ${{ steps.azure-devops-oidc-auth.outputs.access-token }}
ImportOptions__ApiKeys__SequesterPrivateKey: ${{ secrets.SEQUESTER_PRIVATEKEY }}
ImportOptions__ApiKeys__SequesterAppID: ${{ secrets.SEQUESTER_APPID }}
with:
Expand Down
21 changes: 20 additions & 1 deletion actions/sequester/ImportIssues/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,30 @@ private static async Task<QuestGitHubService> CreateService(ImportOptions option
{
Console.WriteLine("Warning: Imported work items won't be assigned based on GitHub assignee.");
}
string? token = options.ApiKeys.QuestAccessToken
?? options.ApiKeys.QuestKey;
bool useBearerToken = options.ApiKeys.QuestAccessToken is not null;

Console.WriteLine($"Using Azure DevOps token: {token.Length}, {token.Substring(0,6)}");

if (string.IsNullOrWhiteSpace(token))
{
throw new InvalidOperationException("Azure DevOps token is missing.");
}

if (useBearerToken)
{
Console.WriteLine("Using secretless for Azure DevOps.");
}
else
{
Console.WriteLine("Using PAT for Azure DevOps.");
}
return new QuestGitHubService(
gitHubClient,
ospoClient,
options.ApiKeys.QuestKey,
token,
useBearerToken,
options.AzureDevOps.Org,
options.AzureDevOps.Project,
options.AzureDevOps.AreaPath,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Polly;
using System.Net.Http;
using Polly;
using Polly.Contrib.WaitAndRetry;
using Polly.Retry;

Expand Down Expand Up @@ -35,14 +36,16 @@ public sealed class QuestClient : IDisposable
/// <param name="token">The personal access token</param>
/// <param name="org">The Azure DevOps organization</param>
/// <param name="project">The Azure DevOps project</param>
public QuestClient(string token, string org, string project)
/// <param name="useBearerToken">True to use a just in time bearer token, false assumes PAT</param>
public QuestClient(string token, string org, string project, bool useBearerToken)
{
QuestOrg = org;
QuestProject = project;
_client = new HttpClient();
_client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
_client.DefaultRequestHeaders.Authorization =
_client.DefaultRequestHeaders.Authorization = useBearerToken ?
new AuthenticationHeaderValue("Bearer", token) :
new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.ASCII.GetBytes($":{token}")));

Expand Down Expand Up @@ -153,6 +156,10 @@ static async Task<JsonElement> HandleResponseAsync(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
// Temporary debugging code:

string packet = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response: {packet}");
JsonDocument jsonDocument = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
return jsonDocument.RootElement;
}
Expand Down
12 changes: 12 additions & 0 deletions actions/sequester/Quest2GitHub/Options/ApiKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ public sealed record class ApiKeys
/// </remarks>
public string? AzureAccessToken { get; init; }

/// <summary>
/// The client ID for identifying this app with AzureDevOps.
/// </summary>
/// <remarks>
/// Assign this from an environment variable with the following key, <c>ImportOptions__ApiKeys__AzureAccessToken</c>:
/// <code>
/// env:
/// ImportOptions__ApiKeys__QuestAccessToken: ${{ secrets.QUEST_ACCESS_TOKEN }}
/// </code>
/// </remarks>
public string? QuestAccessToken { get; init; }

/// <summary>
/// The Azure DevOps API key.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ internal sealed class EnvironmentVariableReader
internal static ApiKeys GetApiKeys()
{
var githubToken = CoalesceEnvVar(("ImportOptions__ApiKeys__GitHubToken", "GitHubKey"));
var questKey = CoalesceEnvVar(("ImportOptions__ApiKeys__QuestKey", "QuestKey"));
// This is optional so that developers can run the app locally without setting up the devOps token.
// In GitHub Actions, this is preferred.
var questToken = CoalesceEnvVar(("ImportOptions__ApiKeys__QuestAccessToken", "QuestAccessToken"), false);

// These keys are used when the app is run as an org enabled action. They are optional.
// If missing, the action runs using repo-only rights.
Expand All @@ -14,11 +16,15 @@ internal static ApiKeys GetApiKeys()

var azureAccessToken = CoalesceEnvVar(("ImportOptions__ApiKeys__AzureAccessToken", "AZURE_ACCESS_TOKEN"), false);

// This key is the PAT for Quest access. It's now a legacy key. Secretless should be better.
var questKey = CoalesceEnvVar(("ImportOptions__ApiKeys__QuestKey", "QuestKey"), false);

if (!int.TryParse(appIDString, out int appID)) appID = 0;

return new ApiKeys()
{
GitHubToken = githubToken,
QuestAccessToken = questToken,
AzureAccessToken = azureAccessToken,
QuestKey = questKey,
SequesterPrivateKey = oauthPrivateKey,
Expand Down
3 changes: 2 additions & 1 deletion actions/sequester/Quest2GitHub/QuestGitHubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class QuestGitHubService(
IGitHubClient ghClient,
OspoClient? ospoClient,
string azdoKey,
bool useBearerToken,
string questOrg,
string questProject,
string areaPath,
Expand All @@ -40,7 +41,7 @@ public class QuestGitHubService(
IEnumerable<LabelToTagMap> tagMap) : IDisposable
{
private const string LinkedWorkItemComment = "Associated WorkItem - ";
private readonly QuestClient _azdoClient = new(azdoKey, questOrg, questProject);
private readonly QuestClient _azdoClient = new(azdoKey, questOrg, questProject, useBearerToken);
private readonly OspoClient? _ospoClient = ospoClient;
private readonly string _questLinkString = $"https://dev.azure.com/{questOrg}/{questProject}/_workitems/edit/";

Expand Down
Loading