Skip to content

Commit 3d311da

Browse files
committed
Add github action logs
1 parent a6a0cd3 commit 3d311da

39 files changed

+2410
-164
lines changed

tools/pipeline-witness/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
appsettings.local.json

tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9+
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.6.0" />
910
<PackageReference Include="Azure.Storage.Blobs" Version="12.13.0" />
1011
<PackageReference Include="Azure.Storage.Queues" Version="12.11.0" />
1112
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
@@ -15,6 +16,7 @@
1516
<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />
1617
<PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="19.225.1" />
1718
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
19+
<PackageReference Include="Octokit" Version="12.0.0" />
1820
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
1921
</ItemGroup>
2022

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System;
2+
using Azure.Security.KeyVault.Secrets;
3+
4+
namespace Azure.Sdk.Tools.PipelineWitness.Configuration;
5+
6+
public interface ISecretClientProvider
7+
{
8+
SecretClient GetSecretClient(Uri vaultUri);
9+
}

tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Configuration/PipelineWitnessSettings.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,37 @@ public class PipelineWitnessSettings
2929
/// </summary>
3030
public string BuildCompleteQueueName { get; set; }
3131

32+
/// <summary>
33+
/// Gets or sets the number of concurrent build complete queue workers to register
34+
/// </summary>
35+
public int BuildCompleteWorkerCount { get; set; } = 1;
36+
37+
/// <summary>
38+
/// Gets or sets whether the build definition worker is enabled
39+
/// </summary>
40+
public bool BuildDefinitionWorkerEnabled { get; set; } = true;
41+
42+
/// <summary>
43+
/// Gets or sets the name of the GitHub actions queue
44+
/// </summary>
45+
public string GitHubActionRunsQueueName { get; set; }
46+
47+
/// <summary>
48+
/// Gets or sets the name of the GitHub action queue workers to register
49+
/// </summary>
50+
public int GitHubActionRunsWorkerCount { get; set; } = 1;
51+
52+
/// <summary>
53+
/// Gets or sets secret used to verify GitHub webhook payloads
54+
/// </summary>
55+
public string GitHubWebhookSecret { get; set; }
56+
57+
/// <summary>
58+
/// Gets or sets the access token to use for GitHub API requests. This
59+
/// must be a personal access token with `repo` scope.
60+
/// </summary>
61+
public string GitHubAccessToken { get; set; }
62+
3263
/// <summary>
3364
/// Gets or sets the amount of time a message should be invisible in the queue while being processed
3465
/// </summary>
@@ -64,11 +95,6 @@ public class PipelineWitnessSettings
6495
/// </summary>
6596
public TimeSpan BuildDefinitionLoopPeriod { get; set; } = TimeSpan.FromMinutes(5);
6697

67-
/// <summary>
68-
/// Gets or sets the number of concurrent build complete queue workers to register
69-
/// </summary>
70-
public int BuildCompleteWorkerCount { get; set; } = 1;
71-
7298
/// <summary>
7399
/// Gets or sets the artifact name used by the pipeline owners extraction build
74100
/// </summary>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System;
2+
using System.Linq;
3+
using System.Text.RegularExpressions;
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.Extensions.Options;
6+
7+
namespace Azure.Sdk.Tools.PipelineWitness.Configuration;
8+
9+
public class PostConfigureKeyVaultSettings<T> : IPostConfigureOptions<T> where T : class
10+
{
11+
private static readonly Regex secretRegex = new Regex(@"(?<vault>https://.*?\.vault\.azure\.net)/secrets/(?<secret>.*)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
12+
private readonly ILogger logger;
13+
private readonly ISecretClientProvider secretClientProvider;
14+
15+
public PostConfigureKeyVaultSettings(ILogger<PostConfigureKeyVaultSettings<T>> logger, ISecretClientProvider secretClientProvider)
16+
{
17+
this.logger = logger;
18+
this.secretClientProvider = secretClientProvider;
19+
}
20+
21+
public void PostConfigure(string name, T options)
22+
{
23+
var stringProperties = typeof(T)
24+
.GetProperties()
25+
.Where(x => x.PropertyType == typeof(string));
26+
27+
foreach (var property in stringProperties)
28+
{
29+
var value = (string)property.GetValue(options);
30+
31+
if (value != null)
32+
{
33+
var match = secretRegex.Match(value);
34+
35+
if (match.Success)
36+
{
37+
var vaultUrl = match.Groups["vault"].Value;
38+
var secretName = match.Groups["secret"].Value;
39+
40+
try
41+
{
42+
var secretClient = this.secretClientProvider.GetSecretClient(new Uri(vaultUrl));
43+
this.logger.LogInformation("Replacing setting property {PropertyName} with value from secret {SecretUrl}", property.Name, value);
44+
45+
var response = secretClient.GetSecret(secretName);
46+
var secret = response.Value;
47+
48+
property.SetValue(options, secret.Value);
49+
}
50+
catch (Exception exception)
51+
{
52+
this.logger.LogError(exception, "Unable to read secret {SecretName} from vault {VaultUrl}", secretName, vaultUrl);
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using Azure.Core;
3+
using Azure.Security.KeyVault.Secrets;
4+
5+
namespace Azure.Sdk.Tools.PipelineWitness.Configuration
6+
{
7+
public class SecretClientProvider : ISecretClientProvider
8+
{
9+
private readonly TokenCredential tokenCredential;
10+
11+
public SecretClientProvider(TokenCredential tokenCredential)
12+
{
13+
this.tokenCredential = tokenCredential;
14+
}
15+
16+
public SecretClient GetSecretClient(Uri vaultUri)
17+
{
18+
return new SecretClient(vaultUri, this.tokenCredential);
19+
}
20+
}
21+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
using System.Text.Json;
7+
using System.Threading.Tasks;
8+
using Azure.Sdk.Tools.PipelineWitness.Configuration;
9+
using Azure.Sdk.Tools.PipelineWitness.GitHubActions;
10+
using Azure.Storage.Queues;
11+
using Microsoft.AspNetCore.Mvc;
12+
using Microsoft.Extensions.Logging;
13+
using Microsoft.Extensions.Options;
14+
15+
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
16+
17+
namespace Azure.Sdk.Tools.PipelineWitness.Controllers
18+
{
19+
[Route("api/githubevents")]
20+
[ApiController]
21+
public class GitHubEventsController : ControllerBase
22+
{
23+
private readonly QueueClient queueClient;
24+
private readonly ILogger<GitHubEventsController> logger;
25+
private readonly PipelineWitnessSettings settings;
26+
27+
public GitHubEventsController(ILogger<GitHubEventsController> logger, QueueServiceClient queueServiceClient, IOptions<PipelineWitnessSettings> options)
28+
{
29+
this.logger = logger;
30+
this.settings = options.Value;
31+
this.queueClient = queueServiceClient.GetQueueClient(this.settings.GitHubActionRunsQueueName);
32+
}
33+
34+
// POST api/githubevents
35+
[HttpPost]
36+
public async Task<IActionResult> PostAsync()
37+
{
38+
var eventName = Request.Headers["X-GitHub-Event"].FirstOrDefault();
39+
switch (eventName)
40+
{
41+
case "ping":
42+
return Ok();
43+
case "workflow_run":
44+
this.logger.LogInformation("Received GitHub event workflow_run");
45+
return await ProcessWorkflowRunEventAsync();
46+
default:
47+
this.logger.LogWarning("Received GitHub event {EventName} which is not supported", eventName);
48+
return BadRequest();
49+
}
50+
}
51+
52+
private static bool VerifySignature(string text, string key, string signature)
53+
{
54+
Encoding encoding = Encoding.UTF8;
55+
56+
byte[] textBytes = encoding.GetBytes(text);
57+
byte[] keyBytes = encoding.GetBytes(key);
58+
59+
using HMACSHA256 hasher = new(keyBytes);
60+
byte[] hashBytes = hasher.ComputeHash(textBytes);
61+
62+
var hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
63+
var expectedSignature = $"sha256={hash}";
64+
return signature == expectedSignature;
65+
}
66+
67+
private async Task<IActionResult> ProcessWorkflowRunEventAsync()
68+
{
69+
using var reader = new StreamReader(Request.Body);
70+
var body = await reader.ReadToEndAsync();
71+
var signature = Request.Headers["X-Hub-Signature-256"].FirstOrDefault();
72+
73+
if (!VerifySignature(body, this.settings.GitHubWebhookSecret, signature))
74+
{
75+
this.logger.LogWarning("Received GitHub event with invalid signature");
76+
return Unauthorized();
77+
}
78+
79+
var eventMessage = JsonDocument.Parse(body).RootElement;
80+
81+
if (eventMessage.GetProperty("action").GetString() == "completed")
82+
{
83+
var queueMessage = new GitHubRunCompleteMessage
84+
{
85+
Owner = eventMessage.GetProperty("repository").GetProperty("owner").GetProperty("login").GetString(),
86+
Repository = eventMessage.GetProperty("repository").GetProperty("name").GetString(),
87+
RunId = eventMessage.GetProperty("workflow_run").GetProperty("id").GetInt64(),
88+
};
89+
90+
await this.queueClient.SendMessageAsync(JsonSerializer.Serialize(queueMessage));
91+
}
92+
93+
return Ok();
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)