Skip to content

Commit 99f657f

Browse files
GitHub workflow queue based callback (#50)
1 parent 0cafb8f commit 99f657f

54 files changed

Lines changed: 3021 additions & 1297 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
Set-StrictMode -Version Latest
2+
3+
$script:RepositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..' '..' '..'))
4+
$script:GetProductEnvironmentNamesScript = Join-Path $script:RepositoryRoot 'tools/infrastructure/get-product-environment-names.ps1'
5+
$script:GetProductConventionsScript = Join-Path $script:RepositoryRoot 'tools/infrastructure/get-product-conventions.ps1'
6+
7+
function Resolve-GitHubAppAuthorizationContext {
8+
[CmdletBinding()]
9+
param(
10+
[Parameter(Mandatory)]
11+
[string] $GatedEnvironmentsText,
12+
13+
[Parameter(Mandatory)]
14+
[string] $TriggeringActor
15+
)
16+
17+
$gatedEnvironments = @(
18+
$GatedEnvironmentsText -split "`r?`n" |
19+
ForEach-Object { $_.Trim() } |
20+
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
21+
)
22+
23+
if ($gatedEnvironments.Count -eq 0) {
24+
throw "Input 'gated-environments' must contain at least one environment."
25+
}
26+
27+
$primaryEnv = ""
28+
$pipelineEnvironments = @()
29+
$installationId = ""
30+
$environments = & $script:GetProductEnvironmentNamesScript
31+
32+
foreach ($environmentName in $environments) {
33+
$conventions = & $script:GetProductConventionsScript -EnvironmentName $environmentName -AsHashtable
34+
$githubConfig = $conventions.SubProducts.Github
35+
$expectedAppSlug = $githubConfig.AppSlug
36+
37+
if ($expectedAppSlug -and ($TriggeringActor -eq "$expectedAppSlug[bot]")) {
38+
$primaryEnv = $githubConfig.AuthorizedEnvironment.Primary
39+
$pipelineEnvironments = @($githubConfig.AuthorizedEnvironment.Pipeline)
40+
$installationId = [string] $githubConfig.InstallationId
41+
break
42+
}
43+
}
44+
45+
if ([string]::IsNullOrWhiteSpace($primaryEnv)) {
46+
throw "Unable to resolve dispatching actor '$TriggeringActor' to a supported GitHub App."
47+
}
48+
49+
if ([string]::IsNullOrWhiteSpace($installationId)) {
50+
throw "GitHub App installation id is not configured for dispatching actor '$TriggeringActor'."
51+
}
52+
53+
$authorizedTargetEnvs = @(
54+
$gatedEnvironments |
55+
Where-Object { $_ -in $pipelineEnvironments }
56+
)
57+
58+
if ($authorizedTargetEnvs.Count -eq 0) {
59+
throw "Dispatching actor '$TriggeringActor' is not authorized for any workflow-gated environment. Gated environments: $($gatedEnvironments -join ', '). Pipeline environments: $($pipelineEnvironments -join ', ')."
60+
}
61+
62+
return @{
63+
Primary = $primaryEnv
64+
Pipeline = @($pipelineEnvironments)
65+
AuthorizedTargetEnvs = @($authorizedTargetEnvs)
66+
InstallationId = $installationId
67+
}
68+
}
69+
70+
function Resolve-WorkflowQueueContext {
71+
[CmdletBinding()]
72+
param(
73+
[Parameter(Mandatory)]
74+
[string] $WorkflowName,
75+
76+
[Parameter(Mandatory)]
77+
[string] $EnvironmentName,
78+
79+
[Parameter()]
80+
[string] $LocalVerificationDirectiveJson = ""
81+
)
82+
83+
if ([string]::IsNullOrWhiteSpace($WorkflowName)) {
84+
throw "WorkflowName is required."
85+
}
86+
87+
$separatorIndex = $WorkflowName.IndexOf('-')
88+
if ($separatorIndex -le 0 -or $separatorIndex -ge ($WorkflowName.Length - 1)) {
89+
throw "Workflow name '$WorkflowName' must match '<dispatcher>-<instanceId>'."
90+
}
91+
92+
$workflowDispatcherName = $WorkflowName.Substring(0, $separatorIndex)
93+
$instanceId = $WorkflowName.Substring($separatorIndex + 1)
94+
95+
if (-not [string]::IsNullOrWhiteSpace($LocalVerificationDirectiveJson)) {
96+
$localVerificationDirective = $LocalVerificationDirectiveJson | ConvertFrom-Json -AsHashtable
97+
$storageConnectionString = [string] $localVerificationDirective.storageConnectionString
98+
99+
if ([string]::IsNullOrWhiteSpace($storageConnectionString)) {
100+
throw "Local verification directive must include 'storageConnectionString'."
101+
}
102+
103+
return @{
104+
WorkflowDispatcherName = $workflowDispatcherName
105+
InstanceId = $instanceId
106+
StorageConnectionString = $storageConnectionString
107+
}
108+
}
109+
110+
$conventions = & $script:GetProductConventionsScript -EnvironmentName $EnvironmentName -AsHashtable
111+
$subProduct = $conventions.SubProducts[$workflowDispatcherName]
112+
if ($null -eq $subProduct) {
113+
throw "Unable to resolve workflow dispatcher '$workflowDispatcherName' in conventions for environment '$EnvironmentName'."
114+
}
115+
116+
$storageAccountName = $subProduct.StorageAccountName
117+
if ([string]::IsNullOrWhiteSpace($storageAccountName)) {
118+
throw "Unable to resolve the storage account for workflow dispatcher '$workflowDispatcherName' in environment '$EnvironmentName'."
119+
}
120+
121+
return @{
122+
WorkflowDispatcherName = $workflowDispatcherName
123+
InstanceId = $instanceId
124+
StorageAccountName = $storageAccountName
125+
}
126+
}
127+
128+
function Get-GitHubWorkflowConclusion {
129+
[CmdletBinding()]
130+
param(
131+
[Parameter(Mandatory)]
132+
[string] $NeedsJson
133+
)
134+
135+
$needs = $NeedsJson | ConvertFrom-Json -AsHashtable
136+
$results = @(
137+
$needs.GetEnumerator() |
138+
Where-Object { $_.Key -ne 'github-app-authz' } |
139+
ForEach-Object { $_.Value.result }
140+
)
141+
142+
if ($results -contains 'failure') {
143+
return 'failure'
144+
}
145+
146+
if ($results -contains 'cancelled') {
147+
return 'cancelled'
148+
}
149+
150+
return 'success'
151+
}
152+
153+
Export-ModuleMember -Function Resolve-GitHubAppAuthorizationContext, Resolve-WorkflowQueueContext, Get-GitHubWorkflowConclusion
Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,34 @@
11
name: github-app-authz-envs
22
description: Retrieves primary and pipeline environments by matching GitHub App slug against triggering actor
33

4+
inputs:
5+
gated-environments:
6+
description: Multi-line list of workflow environments eligible for this workflow
7+
required: true
8+
49
outputs:
510
primary:
611
description: 'The primary environment for this GitHub App'
712
value: ${{ steps.envs.outputs.primary }}
813
pipeline:
914
description: 'JSON array of authorized environments in the deployment pipeline'
1015
value: ${{ steps.envs.outputs.pipeline }}
16+
authorized-target-envs:
17+
description: 'JSON array of workflow-gated environments authorized for the dispatching GitHub App'
18+
value: ${{ steps.envs.outputs.authorized-target-envs }}
19+
installation-id:
20+
description: 'GitHub App installation id for the dispatching actor'
21+
value: ${{ steps.envs.outputs.installation-id }}
1122

1223
runs:
1324
using: composite
1425
steps:
15-
- id: envs
26+
- id: envs
1627
shell: pwsh
28+
env:
29+
GATED_ENVIRONMENTS: ${{ inputs.gated-environments }}
30+
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
1731
run: |
18-
$environments = & ./tools/infrastructure/get-product-environment-names.ps1
19-
$actor = "${{ github.triggering_actor }}"
20-
21-
$primaryEnv = ""
22-
$pipelineJson = "[]"
23-
24-
foreach ($env in $environments) {
25-
$conventions = & ./tools/infrastructure/get-product-conventions.ps1 -EnvironmentName $env | ConvertFrom-Json
26-
$githubConfig = $conventions.SubProducts.Github
27-
$expectedAppSlug = $githubConfig.AppSlug
28-
29-
if ($expectedAppSlug -and ($actor -eq "${expectedAppSlug}[bot]")) {
30-
$primaryEnv = $githubConfig.AuthorizedEnvironment.Primary
31-
$pipelineJson = $githubConfig.AuthorizedEnvironment.Pipeline | ConvertTo-Json -Compress
32-
break
33-
}
34-
}
35-
36-
echo "primary=$primaryEnv" >> $env:GITHUB_OUTPUT
37-
echo "pipeline=$pipelineJson" >> $env:GITHUB_OUTPUT
32+
${{ github.action_path }}/get-authorized-target-envs.ps1 `
33+
-GatedEnvironments $env:GATED_ENVIRONMENTS `
34+
-TriggeringActor $env:TRIGGERING_ACTOR
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[CmdletBinding()]
2+
param(
3+
[Parameter(Mandatory)]
4+
[string] $GatedEnvironments,
5+
6+
[Parameter(Mandatory)]
7+
[string] $TriggeringActor
8+
)
9+
10+
Set-StrictMode -Version Latest
11+
$ErrorActionPreference = 'Stop'
12+
13+
Import-Module (Join-Path $PSScriptRoot '..' '_shared' 'GitHubWorkflowQueueSupport.psm1') -Force
14+
15+
$authorizationContext = Resolve-GitHubAppAuthorizationContext -GatedEnvironmentsText $GatedEnvironments -TriggeringActor $TriggeringActor
16+
17+
if ($env:GITHUB_OUTPUT) {
18+
"primary=$($authorizationContext.Primary)" >> $env:GITHUB_OUTPUT
19+
"pipeline=$(@($authorizationContext.Pipeline) | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT
20+
"authorized-target-envs=$(@($authorizationContext.AuthorizedTargetEnvs) | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT
21+
"installation-id=$($authorizationContext.InstallationId)" >> $env:GITHUB_OUTPUT
22+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[CmdletBinding()]
2+
param(
3+
[Parameter()]
4+
[string] $LocalVerificationDirective = "",
5+
6+
[Parameter(Mandatory)]
7+
[string] $PrimaryEnvironment,
8+
9+
[Parameter(Mandatory)]
10+
[string] $Repository,
11+
12+
[Parameter(Mandatory)]
13+
[int] $RunAttempt,
14+
15+
[Parameter(Mandatory)]
16+
[long] $RunId,
17+
18+
[Parameter(Mandatory)]
19+
[string] $WorkflowName
20+
)
21+
22+
Set-StrictMode -Version Latest
23+
$ErrorActionPreference = 'Stop'
24+
25+
Import-Module (Join-Path $PSScriptRoot '..' '_shared' 'GitHubWorkflowQueueSupport.psm1') -Force
26+
27+
$workflowQueueContext = Resolve-WorkflowQueueContext -WorkflowName $WorkflowName -EnvironmentName $PrimaryEnvironment -LocalVerificationDirectiveJson $LocalVerificationDirective
28+
$payloadJson = [ordered]@{
29+
environment = $PrimaryEnvironment
30+
instanceId = $workflowQueueContext.InstanceId
31+
repository = $Repository
32+
runAttempt = $RunAttempt
33+
runId = $RunId
34+
workflowName = $WorkflowName
35+
} | ConvertTo-Json -Compress
36+
37+
if ($env:GITHUB_OUTPUT) {
38+
"payload-json=$payloadJson" >> $env:GITHUB_OUTPUT
39+
if ($workflowQueueContext.ContainsKey('StorageAccountName')) {
40+
"storage-account=$($workflowQueueContext.StorageAccountName)" >> $env:GITHUB_OUTPUT
41+
}
42+
if ($workflowQueueContext.ContainsKey('StorageConnectionString')) {
43+
"storage-connection-string=$($workflowQueueContext.StorageConnectionString)" >> $env:GITHUB_OUTPUT
44+
}
45+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: publish-github-workflow-event
2+
description: Log into Azure, build a GitHub workflow queue payload, and publish it to the workflow queue
3+
4+
inputs:
5+
github-environment:
6+
description: Primary GitHub environment used for Azure login and storage-account resolution
7+
required: true
8+
github-app-installation-id:
9+
description: GitHub App installation id used to populate queue message user-context claims
10+
required: true
11+
message-type:
12+
description: Queue message type to publish
13+
required: true
14+
local-verification:
15+
description: Optional local-only queue publication override JSON supplied by the local Functions dispatcher
16+
required: false
17+
workflow-name:
18+
description: Dispatcher-prefixed workflow name used for instance correlation and queue routing
19+
required: true
20+
needs-json:
21+
description: JSON serialization of the workflow needs context, required for completed events
22+
required: false
23+
repository:
24+
description: Repository name in owner/repo format
25+
required: true
26+
run-attempt:
27+
description: GitHub workflow run attempt number
28+
required: true
29+
run-id:
30+
description: GitHub workflow run id
31+
required: true
32+
queue-name:
33+
description: Queue name to publish to
34+
required: false
35+
default: default-queue
36+
37+
outputs:
38+
published:
39+
description: Whether the queue message was published successfully
40+
value: ${{ steps.publish.outputs.published }}
41+
message-id:
42+
description: Published MessageBody id
43+
value: ${{ steps.publish.outputs.message-id }}
44+
conclusion:
45+
description: Derived workflow conclusion written into the completed payload when applicable
46+
value: ${{ steps.publish.outputs.conclusion }}
47+
48+
runs:
49+
using: composite
50+
steps:
51+
- name: Azure login
52+
if: ${{ inputs.local-verification == '' }}
53+
uses: ./.github/actions/azure-login
54+
with:
55+
github-environment: ${{ inputs.github-environment }}
56+
57+
- id: publish
58+
shell: pwsh
59+
env:
60+
GITHUB_ENVIRONMENT: ${{ inputs.github-environment }}
61+
GITHUB_APP_INSTALLATION_ID: ${{ inputs.github-app-installation-id }}
62+
MESSAGE_TYPE: ${{ inputs.message-type }}
63+
NEEDS_JSON: ${{ inputs.needs-json }}
64+
QUEUE_NAME: ${{ inputs.queue-name }}
65+
REPOSITORY: ${{ inputs.repository }}
66+
RUN_ATTEMPT: ${{ inputs.run-attempt }}
67+
RUN_ID: ${{ inputs.run-id }}
68+
WORKFLOW_NAME: ${{ inputs.workflow-name }}
69+
LOCAL_VERIFICATION: ${{ inputs.local-verification }}
70+
run: |
71+
${{ github.action_path }}/publish-message.ps1 `
72+
-GitHubEnvironment $env:GITHUB_ENVIRONMENT `
73+
-GitHubAppInstallationId $env:GITHUB_APP_INSTALLATION_ID `
74+
-LocalVerificationDirective $env:LOCAL_VERIFICATION `
75+
-MessageType $env:MESSAGE_TYPE `
76+
-NeedsJson $env:NEEDS_JSON `
77+
-Repository $env:REPOSITORY `
78+
-RunAttempt ([int]$env:RUN_ATTEMPT) `
79+
-RunId ([long]$env:RUN_ID) `
80+
-WorkflowName $env:WORKFLOW_NAME `
81+
-QueueName $env:QUEUE_NAME

0 commit comments

Comments
 (0)