Skip to content

Commit edf386f

Browse files
authored
Hopefully fix those dreaded 😕 reactions by GitForWindowsHelper (#197)
Ever since GitHub changed their REST API so that the date range in their `search` endpoint didn't work as expected by GitForWindowsHelper's logic anymore, I've been struggling to find a work around or fix for this. I thought that I had at at long last figured out the correct syntax in 77cd499 (after none of 1ff8966, a4e3ff8, 2627695 and 2e91f07 did the job) but today we experienced another of those dreaded :confused: reactions in git-for-windows/git#6097 (comment): <img width="288" height="100" alt="image" src="https://github.com/user-attachments/assets/da852ab2-1441-4894-b542-cbaef240ff2a" /> The response for that webhook is simply empty, because it timed out before responding, unfortunately: <img width="1046" height="217" alt="image" src="https://github.com/user-attachments/assets/853056fb-5b47-4dc5-bfb5-ba1e028b3da6" /> I actually had started looking into this a little over a week ago (using the embargoed org to perform my experiments because I did not want to mess around with the `main` branch of `git-for-windows-automation` in production). And it _looks_ as if the `date:` header returned in that unhelpful 204 that is the (otherwise empty) response when [creating a workflow dispatch event](https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#create-a-workflow-dispatch-event) can be either identical to, or even come _after_, the date reported as the `created_at` attribute of the corresponding workflow run. To be sure, the root cause behind all of these troubles is that for all those years, there was a really annoying gap in GitHub's REST API. It should be so easy to obtain the corresponding workflow run when triggering one via a `workflow_dispatch`: Literally everybody who triggers a workflow run programmatically needs to obtain a reference to the workflow run that was triggered by it. That's necessary. This is required. However, there is no reliable way. And what we did was just to work around that. By polling for workflow runs that were created _after_ we asked for one to be triggered. And even those workarounds were broken, so the entire saga is _quite_ frustrating. The good news: After years of frustration, it seems that some new wind was blown into GitHub Actions, and they fixed that! See https://github.blog/changelog/2026-02-19-workflow-dispatch-api-now-returns-run-ids/ for full details. Unfortunately, this is of course not enough. Apparently yet another external change prevented me from using the Role-Based Access Control (RBAC) method to deploy the Azure Function to the embargoed org, which was _already_ a work-around from back from April 2024, when I worked on creating artifacts for an embargoed Git for Windows release. Of course, I did expect this to deploy without problems, after I verified in my experiments that the clock skew patch works around the new issues in a local run-through. So I basically merged those changes from the embargoed builds to be able to deploy. But the deployment failed, RBAC no longer works, and I documented that in the follow-up commit: Luckily, for completely unrelated reasons, elsewhere (in GitGitGadget), I had already developed patches to deploy an Azure Function via OpenID Connect. So I ported those changes to the Git for Windows helper app. Unfortunately this still was not enough. I had to also allow for overriding not only the `activeOrg` but also the `activeBot`, because in the non-embargoed builds (i.e. in `git-for-windows/git-for-windows-automation`'s `main` branch), we now verify that the sender is the expected bot. The end result is unfortunately a complex PR that tries to do something simple, but for various reasons needs to be a lot more complex just so that we can actually deploy the result...
2 parents 390251e + 04ff263 commit edf386f

File tree

14 files changed

+112
-45
lines changed

14 files changed

+112
-45
lines changed

‎.funcignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
/.*
2+
/*.config.js
23
/*.md
34
/*.gif
5+
/*.svg
46
/host.json
57
/local.settings.json
8+
/package*.json
9+
/test-*.js

‎.github/workflows/deploy.yml‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,24 @@ on:
88
- '.github/workflows/deploy.yml'
99
- 'GitForWindowsHelper/**'
1010

11+
permissions:
12+
contents: read
13+
id-token: write
14+
1115
jobs:
1216
deploy:
1317
if: github.event.repository.fork == false
1418
environment: deploy-to-azure
1519
runs-on: ubuntu-latest
1620
steps:
1721
- uses: actions/checkout@v6
22+
- name: 'Login via Azure CLI'
23+
uses: azure/login@v2
24+
with:
25+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
26+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
27+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
1828
- uses: Azure/functions-action@v1
1929
with:
20-
app-name: GitForWindowsHelper
21-
publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
30+
app-name: ${{ secrets.AZURE_FUNCTION_NAME || 'GitForWindowsHelper' }}
2231
respect-funcignore: true

‎GitForWindowsHelper/cascading-runs.js‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const { activeBot, activeOrg } = require('./org')
2+
13
const getToken = (() => {
24
const tokens = {}
35

@@ -12,7 +14,7 @@ const getToken = (() => {
1214
})()
1315

1416
const isAllowed = async (context, owner, repo, login) => {
15-
if (login === 'gitforwindowshelper[bot]') return true
17+
if (login === `${activeBot}[bot]`) return true
1618
const getCollaboratorPermissions = require('./get-collaborator-permissions')
1719
const token = await getToken(context, owner, repo)
1820
const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
@@ -33,7 +35,7 @@ const triggerGitArtifactsRuns = async (context, checkRunOwner, checkRunRepo, tag
3335
const owner = match[1]
3436
const repo = match[2]
3537
const workflowRunId = Number(match[3])
36-
if (owner !== 'git-for-windows' || repo !== 'git-for-windows-automation') {
38+
if (owner !== activeOrg || repo !== 'git-for-windows-automation') {
3739
throw new Error(`Unexpected repository ${owner}/${repo} for tag-git run ${tagGitCheckRun.id}: ${tagGitCheckRun.url}`)
3840
}
3941

@@ -115,12 +117,12 @@ const cascadingRuns = async (context, req) => {
115117
const checkRunRepo = req.body.repository.name
116118
const checkRun = req.body.check_run
117119
const name = checkRun.name
118-
const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === 'gitforwindowshelper'
119-
? 'gitforwindowshelper[bot]' : req.body.sender.login
120+
const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === activeBot
121+
? `${activeBot}[bot]` : req.body.sender.login
120122

121123
if (action === 'completed') {
122124
if (name === 'tag-git') {
123-
if (checkRunOwner !== 'git-for-windows' || checkRunRepo !== 'git') {
125+
if (checkRunOwner !== activeOrg || checkRunRepo !== 'git') {
124126
throw new Error(`Refusing to handle cascading run in ${checkRunOwner}/${checkRunRepo}`)
125127
}
126128

‎GitForWindowsHelper/component-updates.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const { activeOrg } = require('./org')
2+
13
const guessComponentUpdateDetails = (title, body) => {
24
let [ , package_name, version ] =
35
title.match(/^\[New (\S+) version\] (?:[^0-9]+\s+)?(\S+(?:\s+patch\s+\d+)?)(?! new items)/) ||
@@ -115,14 +117,14 @@ const guessReleaseNotes = async (context, issue) => {
115117
const match = issue.body.match(/See (https:\/\/\S+) for details/)
116118
if (match) return match[1]
117119

118-
const issueMatch = issue.body.match(/https:\/\/github\.com\/git-for-windows\/git\/issues\/(\d+)/)
120+
const issueMatch = issue.body.match(new RegExp(`https://github.com/${activeOrg}/git/issues/(\\d+)`))
119121
if (issueMatch) {
120122
const githubApiRequest = require('./github-api-request')
121123
const issue = await githubApiRequest(
122124
context,
123125
null,
124126
'GET',
125-
`/repos/git-for-windows/git/issues/${issueMatch[1]}`
127+
`/repos/${activeOrg}/git/issues/${issueMatch[1]}`
126128
)
127129
return matchURLInIssue(issue)
128130
}

‎GitForWindowsHelper/finalize-g4w-release.js‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
const { activeOrg, activeBot } = require('./org')
2+
13
module.exports = async (context, req) => {
24
if (req.body.action !== 'completed') return "Nothing to do here: workflow run did not complete yet"
35
if (req.body.workflow_run.conclusion !== 'success') return "Nothing to do here: workflow run did not succeed"
46

57
const releaseWorkflowRunID = req.body.workflow_run.id
6-
const owner = 'git-for-windows'
8+
const owner = activeOrg
79
const repo = 'git'
810
const sender = req.body.sender.login
911

@@ -21,7 +23,7 @@ module.exports = async (context, req) => {
2123
})()
2224

2325
const isAllowed = async (login) => {
24-
if (login === 'gitforwindowshelper[bot]') return true
26+
if (login === `${activeBot}[bot]`) return true
2527
const getCollaboratorPermissions = require('./get-collaborator-permissions')
2628
const token = await getToken()
2729
const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
@@ -31,11 +33,11 @@ module.exports = async (context, req) => {
3133
if (!await isAllowed(sender)) throw new Error(`${sender} is not allowed to do that`)
3234

3335
const { searchIssues } = require('./search')
34-
const items = await searchIssues(context, 'org:git-for-windows is:pr is:open in:comments "The release-git workflow run was started"')
36+
const items = await searchIssues(context, `org:${activeOrg} is:pr is:open in:comments "The release-git workflow run was started"`)
3537

3638
const githubApiRequest = require('./github-api-request')
3739

38-
const needle = `The \`release-git\` workflow run [was started](https://github.com/git-for-windows/git-for-windows-automation/actions/runs/${releaseWorkflowRunID})`
40+
const needle = `The \`release-git\` workflow run [was started](https://github.com/${activeOrg}/git-for-windows-automation/actions/runs/${releaseWorkflowRunID})`
3941
const candidates = []
4042
for (const item of items) {
4143
if (!['OWNER', 'MEMBER'].includes(item.author_association)) continue

‎GitForWindowsHelper/https-request.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const httpsRequest = async (context, hostname, method, requestPath, body, header
2525
options.method === 'GET' ? '' : `-X ${options.method}`,
2626
...Object.entries(options.headers).map(([key, value]) => `-H ${quote(`${key}: ${value}`)}`),
2727
body ? `-d ${quote(body)}` : '',
28-
`https://${options.hostname}${options.path}`,
28+
`'https://${options.hostname}${encodeURI(options.path)}'`,
2929
].filter(e => e).join(' ')
3030
;(context.error || console.error)(commandLine)
3131
}

‎GitForWindowsHelper/index.js‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const validateGitHubWebHook = require('./validate-github-webhook')
2+
const { activeBot, activeOrg } = require('./org')
23

34
module.exports = async function (context, req) {
45
const withStatus = (status, headers, body) => {
@@ -41,7 +42,7 @@ module.exports = async function (context, req) {
4142
if (req.headers['x-github-event'] === 'workflow_run'
4243
&& req.body.workflow_run?.event === 'workflow_dispatch'
4344
&& req.body.workflow_run?.head_branch === 'main'
44-
&& req.body.repository.full_name === 'git-for-windows/git-for-windows-automation'
45+
&& req.body.repository.full_name === `${activeOrg}/git-for-windows-automation`
4546
&& req.body.action === 'completed'
4647
&& req.body.workflow_run.path === '.github/workflows/release-git.yml'
4748
&& req.body.workflow_run.conclusion === 'success') return ok(await finalizeGitForWindowsRelease(context, req))
@@ -53,8 +54,8 @@ module.exports = async function (context, req) {
5354
try {
5455
const { cascadingRuns, handlePush } = require('./cascading-runs.js')
5556
if (req.headers['x-github-event'] === 'check_run'
56-
&& req.body.check_run?.app?.slug === 'gitforwindowshelper'
57-
&& req.body.repository.full_name === 'git-for-windows/git'
57+
&& req.body.check_run?.app?.slug === activeBot
58+
&& req.body.repository.full_name === `${activeOrg}/git`
5859
&& req.body.action === 'completed') return ok(await cascadingRuns(context, req))
5960

6061
if (req.headers['x-github-event'] === 'push'

‎GitForWindowsHelper/org.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
activeOrg: process.env.ACTIVE_ORG || 'git-for-windows',
3+
activeBot: process.env.ACTIVE_BOT || 'gitforwindowshelper',
4+
}

‎GitForWindowsHelper/slash-commands.js‎

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const { activeOrg } = require('./org')
2+
13
module.exports = async (context, req) => {
24
const command = req.body.comment.body
35
const owner = req.body.repository.owner.login
@@ -50,7 +52,7 @@ module.exports = async (context, req) => {
5052

5153
try {
5254
if (command === '/open pr') {
53-
if (owner !== 'git-for-windows' || !['git', 'msys2-runtime'].includes(repo)) return `Ignoring ${command} in unexpected repo: ${commentURL}`
55+
if (owner !== activeOrg || !['git', 'msys2-runtime'].includes(repo)) return `Ignoring ${command} in unexpected repo: ${commentURL}`
5456

5557
await checkPermissions()
5658

@@ -68,7 +70,7 @@ module.exports = async (context, req) => {
6870
const openPR = async (package_name, packageType) => {
6971
const { searchIssues } = require('./search')
7072
const prTitle = `${package_name}: update to ${version}`
71-
const items = await searchIssues(context, `org:git-for-windows is:pull-request "${prTitle}" in:title`)
73+
const items = await searchIssues(context, `org:${activeOrg} is:pull-request "${prTitle}" in:title`)
7274
const alreadyOpenedPR = items.filter(e => e.title === prTitle)
7375

7476
const { appendToIssueComment } = require('./issues');
@@ -91,7 +93,7 @@ module.exports = async (context, req) => {
9193
const answer = await triggerWorkflowDispatch(
9294
context,
9395
await getToken(),
94-
'git-for-windows',
96+
activeOrg,
9597
'git-for-windows-automation',
9698
'open-pr.yml',
9799
'main', {
@@ -112,7 +114,7 @@ module.exports = async (context, req) => {
112114
}
113115

114116
if (command === '/updpkgsums') {
115-
if (owner !== 'git-for-windows'
117+
if (owner !== activeOrg
116118
|| !req.body.issue.pull_request
117119
|| !['build-extra', 'MINGW-packages', 'MSYS2-packages'].includes(repo)) {
118120
return `Ignoring ${command} in unexpected repo: ${commentURL}`
@@ -125,7 +127,7 @@ module.exports = async (context, req) => {
125127
const answer = await triggerWorkflowDispatch(
126128
context,
127129
await getToken(),
128-
'git-for-windows',
130+
activeOrg,
129131
'git-for-windows-automation',
130132
'updpkgsums.yml',
131133
'main', {
@@ -142,7 +144,7 @@ module.exports = async (context, req) => {
142144

143145
const deployMatch = command.match(/^\/deploy(\s+(\S+)\s*)?$/)
144146
if (deployMatch) {
145-
if (owner !== 'git-for-windows'
147+
if (owner !== activeOrg
146148
|| !req.body.issue.pull_request
147149
|| !['build-extra', 'MINGW-packages', 'MSYS2-packages'].includes(repo)) {
148150
return `Ignoring ${command} in unexpected repo: ${commentURL}`
@@ -169,7 +171,7 @@ module.exports = async (context, req) => {
169171
await triggerWorkflowDispatch(
170172
context,
171173
await getToken(),
172-
'git-for-windows',
174+
activeOrg,
173175
'git-for-windows-automation',
174176
'build-and-deploy.yml',
175177
'main', {
@@ -223,7 +225,7 @@ module.exports = async (context, req) => {
223225
e.id = await queueCheckRun(
224226
context,
225227
await getToken(),
226-
'git-for-windows',
228+
activeOrg,
227229
repo,
228230
ref,
229231
deployLabel,
@@ -254,7 +256,7 @@ module.exports = async (context, req) => {
254256
await updateCheckRun(
255257
context,
256258
await getToken(),
257-
'git-for-windows',
259+
activeOrg,
258260
repo,
259261
e.id, {
260262
details_url: e.answer.html_url
@@ -266,7 +268,7 @@ module.exports = async (context, req) => {
266268
}
267269

268270
if (command === '/git-artifacts') {
269-
if (owner !== 'git-for-windows'
271+
if (owner !== activeOrg
270272
|| repo !== 'git'
271273
|| !req.body.issue.pull_request
272274
) {
@@ -327,7 +329,7 @@ module.exports = async (context, req) => {
327329
const answer = await triggerWorkflowDispatch(
328330
context,
329331
await getToken(),
330-
'git-for-windows',
332+
activeOrg,
331333
'git-for-windows-automation',
332334
'tag-git.yml',
333335
'main', {
@@ -389,7 +391,7 @@ module.exports = async (context, req) => {
389391
const releaseCheckRunId = await queueCheckRun(
390392
context,
391393
await getToken(),
392-
'git-for-windows',
394+
activeOrg,
393395
repo,
394396
commitSHA,
395397
'github-release',
@@ -425,7 +427,7 @@ module.exports = async (context, req) => {
425427
const owner = match[1]
426428
const repo = match[2]
427429
workFlowRunIDs[architecture] = match[3]
428-
if (owner !== 'git-for-windows' || repo !== 'git-for-windows-automation') {
430+
if (owner !== activeOrg || repo !== 'git-for-windows-automation') {
429431
throw new Error(`Unexpected repository ${owner}/${repo} for git-artifacts run ${latest.id}: ${latest.url}`)
430432
}
431433

@@ -457,7 +459,7 @@ module.exports = async (context, req) => {
457459
const answer = await triggerWorkflowDispatch(
458460
context,
459461
await getToken(),
460-
'git-for-windows',
462+
activeOrg,
461463
'git-for-windows-automation',
462464
'release-git.yml',
463465
'main', {
@@ -492,7 +494,7 @@ module.exports = async (context, req) => {
492494

493495
const relNotesMatch = command.match(/^\/add (relnote|release ?note)(\s+(blurb|feature|bug)\s+([^]*))?$/i)
494496
if (relNotesMatch) {
495-
if (owner !== 'git-for-windows'
497+
if (owner !== activeOrg
496498
|| !['git', 'build-extra', 'MINGW-packages', 'MSYS2-packages'].includes(repo)) {
497499
return `Ignoring ${command} in unexpected repo: ${commentURL}`
498500
}
@@ -519,7 +521,7 @@ module.exports = async (context, req) => {
519521
const answer = await triggerWorkflowDispatch(
520522
context,
521523
await getToken(),
522-
'git-for-windows',
524+
activeOrg,
523525
'build-extra',
524526
'add-release-note.yml',
525527
'main', {

‎GitForWindowsHelper/trigger-workflow-dispatch.js‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,21 @@ const triggerWorkflowDispatch = async (context, token, owner, repo, workflow_id,
3939
if ('true' === process.env.DO_NOT_TRIGGER_ANYTHING) {
4040
throw new Error(`Would have triggered workflow ${workflow_id} on ${owner}/${repo} with ref ${ref} and inputs ${JSON.stringify(inputs)}`)
4141
}
42-
const { headers: { date } } = await githubApiRequest(
42+
const response = await githubApiRequest(
4343
context,
4444
token,
4545
'POST',
4646
`/repos/${owner}/${repo}/actions/workflows/${workflow_id}/dispatches`,
47-
{ ref, inputs }
47+
{ ref, inputs, return_run_details: true }
4848
)
4949

50-
const runs = await waitForWorkflowRun(context, owner, repo, workflow_id, new Date(date).toISOString(), token)
50+
// If the API returned run details (200), use them directly
51+
if (response.workflow_run_id) return response
52+
53+
// Fall back to polling if we got a 204 (no run details)
54+
const date = response.headers?.date || new Date().toISOString()
55+
const after = new Date(Date.parse(date) - 5000).toISOString()
56+
const runs = await waitForWorkflowRun(context, owner, repo, workflow_id, after, token)
5157
return runs[0]
5258
}
5359

0 commit comments

Comments
 (0)