Skip to content

Hopefully fix those dreaded 😕 reactions by GitForWindowsHelper#197

Merged
dscho merged 12 commits intomainfrom
accommodate-for-clock-skew
Mar 31, 2026
Merged

Hopefully fix those dreaded 😕 reactions by GitForWindowsHelper#197
dscho merged 12 commits intomainfrom
accommodate-for-clock-skew

Conversation

@dscho
Copy link
Copy Markdown
Member

@dscho dscho commented Feb 17, 2026

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 😕 reactions in git-for-windows/git#6097 (comment):

image

The response for that webhook is simply empty, because it timed out before responding, unfortunately:

image

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 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...

Comment thread GitForWindowsHelper/cascading-runs.js Outdated
const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === 'gitforwindowshelper'
? 'gitforwindowshelper[bot]' : req.body.sender.login
const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === activeBot
? activeBot : req.body.sender.login
Copy link
Copy Markdown
Member

@rimrul rimrul Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're replacing both the name with the [bot] suffix and the name without it with activeBot (here and elsewhere). Does that work as intended?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, this changed at some stage. Let me see whether it's still having the [bot] suffix in the non-embargoed org (when it lacks it in the embargoed one). It does not! No idea when that changed (we know that it did have that slug when we introduced that check, that was the entire point of it), but it did: I just created a check_run for testing and this is what was delivered as webhook payload:

webhook payload
{
  "action": "completed",
  "check_run": {
    "id": 64006248551,
    "name": "test-for-slug",
    "node_id": "CR_kwDOAWJAkM8AAAAO5xHYZw",
    "head_sha": "55fbc0b7141a236a3428b57f8f1cd4d1037eec89",
    "external_id": "",
    "url": "https://api.github.com/repos/git-for-windows/git/check-runs/64006248551",
    "html_url": "https://github.com/git-for-windows/git/runs/64006248551",
    "details_url": "https://github.com/git-for-windows/gfw-helper-github-app",
    "status": "completed",
    "conclusion": "success",
    "started_at": "2026-02-18T13:19:33Z",
    "completed_at": "2026-02-18T13:19:33Z",
    "output": {
      "title": "Let's test the app.slug",
      "summary": "In https://github.com/git-for-windows/gfw-helper-github-app/pull/197#discussion_r2822100475, we got interested in the question whether the app.slug is still recorded with the `[bot]` suffix or not. This test will clarify that.",
      "text": null,
      "annotations_count": 0,
      "annotations_url": "https://api.github.com/repos/git-for-windows/git/check-runs/64006248551/annotations"
    },
    "check_suite": {
      "id": 57640820771,
      "node_id": "CS_kwDOAWJAkM8AAAANa6kgIw",
      "head_branch": "shears/seen",
      "head_sha": "55fbc0b7141a236a3428b57f8f1cd4d1037eec89",
      "status": "completed",
      "conclusion": "success",
      "url": "https://api.github.com/repos/git-for-windows/git/check-suites/57640820771",
      "before": "7b5116b39542d5f52eb7687936bfa5817455c67d",
      "after": "55fbc0b7141a236a3428b57f8f1cd4d1037eec89",
      "pull_requests": [

      ],
      "app": {
        "id": 246505,
        "client_id": "Iv1.db762010367b9bea",
        "slug": "gitforwindowshelper",
        "node_id": "A_kwHOAEXAL84AA8Lp",
        "owner": {
          "login": "git-for-windows",
          "id": 4571183,
          "node_id": "MDEyOk9yZ2FuaXphdGlvbjQ1NzExODM=",
          "avatar_url": "https://avatars.githubusercontent.com/u/4571183?v=4",
          "gravatar_id": "",
          "url": "https://api.github.com/users/git-for-windows",
          "html_url": "https://github.com/git-for-windows",
          "followers_url": "https://api.github.com/users/git-for-windows/followers",
          "following_url": "https://api.github.com/users/git-for-windows/following{/other_user}",
          "gists_url": "https://api.github.com/users/git-for-windows/gists{/gist_id}",
          "starred_url": "https://api.github.com/users/git-for-windows/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/git-for-windows/subscriptions",
          "organizations_url": "https://api.github.com/users/git-for-windows/orgs",
          "repos_url": "https://api.github.com/users/git-for-windows/repos",
          "events_url": "https://api.github.com/users/git-for-windows/events{/privacy}",
          "received_events_url": "https://api.github.com/users/git-for-windows/received_events",
          "type": "Organization",
          "user_view_type": "public",
          "site_admin": false
        },
        "name": "GitForWindowsHelper",
        "description": "This is a GitHub App whose purpose is to help with Git for Windows' day-to-day tasks. It supports slash commands (such as `/hi`) in the form of issue comments in git-for-windows/git.",
        "external_url": "https://github.com/git-for-windows/gfw-helper-github-app",
        "html_url": "https://github.com/apps/gitforwindowshelper",
        "created_at": "2022-10-11T21:14:51Z",
        "updated_at": "2025-02-07T18:45:11Z",
        "permissions": {
          "actions": "write",
          "administration": "write",
          "checks": "write",
          "contents": "write",
          "deployments": "read",
          "discussions": "write",
          "emails": "read",
          "environments": "read",
          "issues": "write",
          "metadata": "read",
          "pull_requests": "write",
          "workflows": "write"
        },
        "events": [
          "check_run",
          "check_suite",
          "issue_comment",
          "pull_request",
          "push",
          "workflow_dispatch",
          "workflow_job",
          "workflow_run"
        ]
      },
      "created_at": "2026-02-17T10:10:49Z",
      "updated_at": "2026-02-18T13:19:33Z"
    },
    "app": {
      "id": 246505,
      "client_id": "Iv1.db762010367b9bea",
      "slug": "gitforwindowshelper",
      "node_id": "A_kwHOAEXAL84AA8Lp",
      "owner": {
        "login": "git-for-windows",
        "id": 4571183,
        "node_id": "MDEyOk9yZ2FuaXphdGlvbjQ1NzExODM=",
        "avatar_url": "https://avatars.githubusercontent.com/u/4571183?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/git-for-windows",
        "html_url": "https://github.com/git-for-windows",
        "followers_url": "https://api.github.com/users/git-for-windows/followers",
        "following_url": "https://api.github.com/users/git-for-windows/following{/other_user}",
        "gists_url": "https://api.github.com/users/git-for-windows/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/git-for-windows/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/git-for-windows/subscriptions",
        "organizations_url": "https://api.github.com/users/git-for-windows/orgs",
        "repos_url": "https://api.github.com/users/git-for-windows/repos",
        "events_url": "https://api.github.com/users/git-for-windows/events{/privacy}",
        "received_events_url": "https://api.github.com/users/git-for-windows/received_events",
        "type": "Organization",
        "user_view_type": "public",
        "site_admin": false
      },
      "name": "GitForWindowsHelper",
      "description": "This is a GitHub App whose purpose is to help with Git for Windows' day-to-day tasks. It supports slash commands (such as `/hi`) in the form of issue comments in git-for-windows/git.",
      "external_url": "https://github.com/git-for-windows/gfw-helper-github-app",
      "html_url": "https://github.com/apps/gitforwindowshelper",
      "created_at": "2022-10-11T21:14:51Z",
      "updated_at": "2025-02-07T18:45:11Z",
      "permissions": {
        "actions": "write",
        "administration": "write",
        "checks": "write",
        "contents": "write",
        "deployments": "read",
        "discussions": "write",
        "emails": "read",
        "environments": "read",
        "issues": "write",
        "metadata": "read",
        "pull_requests": "write",
        "workflows": "write"
      },
      "events": [
        "check_run",
        "check_suite",
        "issue_comment",
        "pull_request",
        "push",
        "workflow_dispatch",
        "workflow_job",
        "workflow_run"
      ]
    },
    "pull_requests": [

    ]
  },
  "repository": {
    "id": 23216272,
    "node_id": "MDEwOlJlcG9zaXRvcnkyMzIxNjI3Mg==",
    "name": "git",
    "full_name": "git-for-windows/git",
    "private": false,
    "owner": {
      "login": "git-for-windows",
      "id": 4571183,
      "node_id": "MDEyOk9yZ2FuaXphdGlvbjQ1NzExODM=",
      "avatar_url": "https://avatars.githubusercontent.com/u/4571183?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/git-for-windows",
      "html_url": "https://github.com/git-for-windows",
      "followers_url": "https://api.github.com/users/git-for-windows/followers",
      "following_url": "https://api.github.com/users/git-for-windows/following{/other_user}",
      "gists_url": "https://api.github.com/users/git-for-windows/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/git-for-windows/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/git-for-windows/subscriptions",
      "organizations_url": "https://api.github.com/users/git-for-windows/orgs",
      "repos_url": "https://api.github.com/users/git-for-windows/repos",
      "events_url": "https://api.github.com/users/git-for-windows/events{/privacy}",
      "received_events_url": "https://api.github.com/users/git-for-windows/received_events",
      "type": "Organization",
      "user_view_type": "public",
      "site_admin": false
    },
    "html_url": "https://github.com/git-for-windows/git",
    "description": "A fork of Git containing Windows-specific patches.",
    "fork": true,
    "url": "https://api.github.com/repos/git-for-windows/git",
    "forks_url": "https://api.github.com/repos/git-for-windows/git/forks",
    "keys_url": "https://api.github.com/repos/git-for-windows/git/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/git-for-windows/git/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/git-for-windows/git/teams",
    "hooks_url": "https://api.github.com/repos/git-for-windows/git/hooks",
    "issue_events_url": "https://api.github.com/repos/git-for-windows/git/issues/events{/number}",
    "events_url": "https://api.github.com/repos/git-for-windows/git/events",
    "assignees_url": "https://api.github.com/repos/git-for-windows/git/assignees{/user}",
    "branches_url": "https://api.github.com/repos/git-for-windows/git/branches{/branch}",
    "tags_url": "https://api.github.com/repos/git-for-windows/git/tags",
    "blobs_url": "https://api.github.com/repos/git-for-windows/git/git/blobs{/sha}",
    "git_tags_url": "https://api.github.com/repos/git-for-windows/git/git/tags{/sha}",
    "git_refs_url": "https://api.github.com/repos/git-for-windows/git/git/refs{/sha}",
    "trees_url": "https://api.github.com/repos/git-for-windows/git/git/trees{/sha}",
    "statuses_url": "https://api.github.com/repos/git-for-windows/git/statuses/{sha}",
    "languages_url": "https://api.github.com/repos/git-for-windows/git/languages",
    "stargazers_url": "https://api.github.com/repos/git-for-windows/git/stargazers",
    "contributors_url": "https://api.github.com/repos/git-for-windows/git/contributors",
    "subscribers_url": "https://api.github.com/repos/git-for-windows/git/subscribers",
    "subscription_url": "https://api.github.com/repos/git-for-windows/git/subscription",
    "commits_url": "https://api.github.com/repos/git-for-windows/git/commits{/sha}",
    "git_commits_url": "https://api.github.com/repos/git-for-windows/git/git/commits{/sha}",
    "comments_url": "https://api.github.com/repos/git-for-windows/git/comments{/number}",
    "issue_comment_url": "https://api.github.com/repos/git-for-windows/git/issues/comments{/number}",
    "contents_url": "https://api.github.com/repos/git-for-windows/git/contents/{+path}",
    "compare_url": "https://api.github.com/repos/git-for-windows/git/compare/{base}...{head}",
    "merges_url": "https://api.github.com/repos/git-for-windows/git/merges",
    "archive_url": "https://api.github.com/repos/git-for-windows/git/{archive_format}{/ref}",
    "downloads_url": "https://api.github.com/repos/git-for-windows/git/downloads",
    "issues_url": "https://api.github.com/repos/git-for-windows/git/issues{/number}",
    "pulls_url": "https://api.github.com/repos/git-for-windows/git/pulls{/number}",
    "milestones_url": "https://api.github.com/repos/git-for-windows/git/milestones{/number}",
    "notifications_url": "https://api.github.com/repos/git-for-windows/git/notifications{?since,all,participating}",
    "labels_url": "https://api.github.com/repos/git-for-windows/git/labels{/name}",
    "releases_url": "https://api.github.com/repos/git-for-windows/git/releases{/id}",
    "deployments_url": "https://api.github.com/repos/git-for-windows/git/deployments",
    "created_at": "2014-08-22T07:07:36Z",
    "updated_at": "2026-02-16T06:39:23Z",
    "pushed_at": "2026-02-17T23:18:53Z",
    "git_url": "git://github.com/git-for-windows/git.git",
    "ssh_url": "git@github.com:git-for-windows/git.git",
    "clone_url": "https://github.com/git-for-windows/git.git",
    "svn_url": "https://github.com/git-for-windows/git",
    "homepage": "http://gitforwindows.org/",
    "size": 364052,
    "stargazers_count": 9072,
    "watchers_count": 9072,
    "language": "C",
    "has_issues": true,
    "has_projects": true,
    "has_downloads": true,
    "has_wiki": true,
    "has_pages": false,
    "has_discussions": true,
    "forks_count": 2795,
    "mirror_url": null,
    "archived": false,
    "disabled": false,
    "open_issues_count": 224,
    "license": {
      "key": "other",
      "name": "Other",
      "spdx_id": "NOASSERTION",
      "url": null,
      "node_id": "MDc6TGljZW5zZTA="
    },
    "allow_forking": true,
    "is_template": false,
    "web_commit_signoff_required": true,
    "has_pull_requests": true,
    "pull_request_creation_policy": "all",
    "topics": [
      "hacktoberfest"
    ],
    "visibility": "public",
    "forks": 2795,
    "open_issues": 224,
    "watchers": 9072,
    "default_branch": "main",
    "custom_properties": {

    }
  },
  "organization": {
    "login": "git-for-windows",
    "id": 4571183,
    "node_id": "MDEyOk9yZ2FuaXphdGlvbjQ1NzExODM=",
    "url": "https://api.github.com/orgs/git-for-windows",
    "repos_url": "https://api.github.com/orgs/git-for-windows/repos",
    "events_url": "https://api.github.com/orgs/git-for-windows/events",
    "hooks_url": "https://api.github.com/orgs/git-for-windows/hooks",
    "issues_url": "https://api.github.com/orgs/git-for-windows/issues",
    "members_url": "https://api.github.com/orgs/git-for-windows/members{/member}",
    "public_members_url": "https://api.github.com/orgs/git-for-windows/public_members{/member}",
    "avatar_url": "https://avatars.githubusercontent.com/u/4571183?v=4",
    "description": ""
  },
  "sender": {
    "login": "gitforwindowshelper[bot]",
    "id": 115591365,
    "node_id": "BOT_kgDOBuPIxQ",
    "avatar_url": "https://avatars.githubusercontent.com/in/246505?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D",
    "html_url": "https://github.com/apps/gitforwindowshelper",
    "followers_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/followers",
    "following_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/following{/other_user}",
    "gists_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/subscriptions",
    "organizations_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/orgs",
    "repos_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/repos",
    "events_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/events{/privacy}",
    "received_events_url": "https://api.github.com/users/gitforwindowshelper%5Bbot%5D/received_events",
    "type": "Bot",
    "user_view_type": "public",
    "site_admin": false
  },
  "installation": {
    "id": 31544731,
    "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMzE1NDQ3MzE="
  }
}

So it would seem that the sender.login has the suffix, but the app.slug does not... Hmm. I guess I need to think about the appropriate change to accommodate for that...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. My change was wrong, we must compare including the [bot] suffix, and if we're using app.slug to backfill a login, we must append said suffix.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made that change.

Range-diff
  • 1: 6eaea71 ! 1: 7657e4d Optionally override the active GitHub App via the environment

    @@ GitForWindowsHelper/cascading-runs.js: const getToken = (() => {
      
      const isAllowed = async (context, owner, repo, login) => {
     -    if (login === 'gitforwindowshelper[bot]') return true
    -+    if (login === activeBot) return true
    ++    if (login === `${activeBot}[bot]`) return true
          const getCollaboratorPermissions = require('./get-collaborator-permissions')
          const token = await getToken(context, owner, repo)
          const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
    @@ GitForWindowsHelper/cascading-runs.js: const cascadingRuns = async (context, req
     -    const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === 'gitforwindowshelper'
     -        ? 'gitforwindowshelper[bot]' : req.body.sender.login
     +    const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === activeBot
    -+        ? activeBot : req.body.sender.login
    ++        ? `${activeBot}[bot]` : req.body.sender.login
      
          if (action === 'completed') {
              if (name === 'tag-git') {
    @@ GitForWindowsHelper/finalize-g4w-release.js: module.exports = async (context, re
      
          const isAllowed = async (login) => {
     -        if (login === 'gitforwindowshelper[bot]') return true
    -+        if (login === activeBot) return true
    ++        if (login === `${activeBot}[bot]`) return true
              const getCollaboratorPermissions = require('./get-collaborator-permissions')
              const token = await getToken()
              const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
    @@ GitForWindowsHelper/org.js
     +    activeBot: process.env.ACTIVE_BOT || 'gitforwindowshelper',
      }
     
    - ## GitForWindowsHelper/self-hosted-arm64-runners.js ##
    -@@
    --const { activeOrg } = require('./org')
    -+const { activeOrg, activeBot } = require('./org')
    - 
    - module.exports = async (context, req) => {
    -     const action = req.body.action
    -@@ GitForWindowsHelper/self-hosted-arm64-runners.js: module.exports = async (context, req) => {
    -     })()
    - 
    -     const isAllowed = async (login) => {
    --        if (login === 'gitforwindowshelper[bot]') return true
    -+        if (login === activeBot) return true
    -         const getCollaboratorPermissions = require('./get-collaborator-permissions')
    -         const token = await getToken()
    -         const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
    -
      ## __tests__/index.test.js ##
     @@ __tests__/index.test.js: test('a completed `tag-git` run triggers `git-artifacts` runs', async () => {
                      slug: 'gitforwindowshelper',

@@ -1,4 +1,4 @@
const { activeOrg } = require('./org')
const { activeOrg, activeBot } = require('./org')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a little confused why this file still exists, but we haven't merged #136 yet.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just merged it. Now we'll see whether the deployment fails like I observed ;-)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, it worked. Strange. But still, we should move away from the function profile-based deployment to the OpenID Connect one I champion in this here PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rebased the branch, dropping those changes in now-deleted code.

dscho added 12 commits March 31, 2026 09:05
Not all slash commands run from PR comments...

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
This allows for funny business in the URL. While at it, also URL-encode
option values like `[bot]`.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Seems as if the date we get in the response when creating a workflow
dispatch _might_ be at, or after, the date the corresponding workflow
run is created. Most likely explanation: clock skew. Let's allow for
that.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
To support embargoed builds in a private fork, we need to be able to do
things outside of the regular `git-for-windows` org.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
To support embargoed builds in a private fork, we need to be able to do
things outside of the regular `git-for-windows` org. This also requires
a GitHub App to be installed on that org, and to allow for developing
that App separately from the public one (so that urgent fixes can be
made in private, without notifying anyone of ongoing security work), it
must be a GitHub App other than the one that is installed in the
`git-for-windows` org.

Also fix the check_run tests to use `sender.login: 'ghost'`, which is
what GitHub actually sends for check runs created via the API by an app
nowadays (yes, this changed at some stage, and we had to adapt).

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
This allows overriding the default Azure Function name used for
deploying via setting a repository secret (in Settings>Secrets and
variables).

The name of the Azure Function is treated as a secret because it can be
used to derive the URL of the Azure Function and to attempt to DOS it.

At this point, it is not _actually_ necessary to provide the correct
Azure Function app name here, as we are using a so-called "publish
profile" that overrides the app name.

However, we are about to switch to Role-Based Access Control, where it
very much matters whether the correct name is specified or not.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Apparently the `publish-profile` deployments are no longer working as
expected for recently-created Azure Functions. That is, the existing
`gfw-helper-github-app` Function still works, obviously, but when I
registered a new Function as described in the `README.md` and tried to
deploy it the same way as `gfw-helper-github-app`, it failed thusly:

  ▶ Run Azure/functions-action@v1
  Successfully parsed SCM credential from publish-profile format.
  Using SCM credential for authentication, GitHub Action will not perform resource validation.
  (node:1549) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
  (Use `node --trace-deprecation ...` to show where the warning was created)
  Error: Execution Exception (state: ValidateAzureResource) (step: Invocation)
  Error:   When request Azure resource at ValidateAzureResource, Get Function App Settings : Failed to acquire app settings from https://<scmsite>/api/settings with publish-profile
  Error:     Failed to fetch Kudu App Settings.
  Unauthorized (CODE: 401)
  Error:       Error: Failed to fetch Kudu App Settings.
  Unauthorized (CODE: 401)
      at Kudu.<anonymous> (/home/runner/work/_actions/Azure/functions-action/v1/lib/appservice-rest/Kudu/azure-app-kudu-service.js:69:23)
      at Generator.next (<anonymous>)
      at fulfilled (/home/runner/work/_actions/Azure/functions-action/v1/lib/appservice-rest/Kudu/azure-app-kudu-service.js:5:58)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
  Error: Deployment Failed!

My guess is that finally the reality of publish profiles being highly
insecure has caught up with new Azure Function registrations, and it is
now required to use the much more secure method of using Role-Based
Access Control. At least in my tests, this works, so let's use it.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
These files are not needed for the Azure Function to do its job, and
only add unnecessary churn to the deployments, so let's skip them.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Apparently after the `publish-profile` deployments stopped working as expected,
even RBAC now fails. At least when I tried last, it failed thusly:

  ▶Run azure/login@v1
  Running Azure CLI Login.
  /usr/bin/az cloud set -n azurecloud
  Done setting cloud: "azurecloud"
  Note: Azure/login action also supports OIDC login mechanism. Refer
    https://github.com/azure/login#configure-a-service-principal-with-a-federated-credential-to-use-oidc-based-authentication
    for more details.
  Attempting Azure CLI login by using service principal with secret...
  Error: AADSTS7000215: Invalid client secret provided. Ensure the secret being
    sent in the request is the client secret value, not the client secret ID, for
    a secret added to app '***'.
  Trace ID: 8b5b5391-5b96-472c-a0e7-11ae90b1c000
  Correlation ID: bd635df4-82ca-441c-af9e-154d18e92773
  Timestamp: 2026-02-12 16:28:25Z

  Error: The error may be caused by passing a service principal certificate with
    --password. Please note that --password no longer accepts a service principal
    certificate. To pass a service principal certificate, use --certificate instead.

  Error: Login failed with Error: The process '/usr/bin/az' failed with exit
    code 1. Double check if the 'auth-type' is correct. Refer to
	https://github.com/Azure/login#readme for more information.

My guess is that even RBAC is considered too insecure. Let's use OpenID Connect,
as it is tied to the GitHub workflow and is therefore as secure as it gets. Even
if the name of the Managed Identity, the tenant and the subscription IDs are
known, an attacker cannot authenticate as that managed identity.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
As announced in the GitHub changelog at
https://github.blog/changelog/2026-02-19-workflow-dispatch-api-now-returns-run-ids/
the REST API's workflow dispatch endpoint now supports a boolean
`return_run_details` parameter that, when set to `true`, causes
the response to be a `200 OK` with a JSON body containing
`workflow_run_id`, `run_url`, and `html_url` instead of the
previous `204 No Content`.

This eliminates the need to poll for the workflow run after
triggering it, which was the root cause of the clock skew problems
that this branch set out to fix (the polling required a time-based
search window, and the response timestamp could come after the
workflow run's `created_at`).

The polling fallback is retained for robustness in case the API
regresses or the parameter is not honored. The test mock returns
the new `200` response for all dispatches except
`upload-snapshot.yml`, which still returns the `204`-style
response, so both code paths are exercised.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@dscho dscho force-pushed the accommodate-for-clock-skew branch from 664e51c to 04ff263 Compare March 31, 2026 08:10
@dscho
Copy link
Copy Markdown
Member Author

dscho commented Mar 31, 2026

Now, this is utterly satisfying: While I was working on rebasing this branch, my AI assistant found https://github.blog/changelog/2026-02-19-workflow-dispatch-api-now-returns-run-ids/. So that ugly polling is no longer necessary, at long last!

I did keep it, just in case, because with GitHub it's like with a box of chocolates, you never know what you're gonna get. But I did reduce the clock skew allowance from 15s to 5s.

Range-diff
  • -: ------- > 1: e96a604 get-webhook-event-payload: also handle issue comments

  • 5: aaedd6c = 2: f271bd7 test-pr-comment-delivery: log curl invocations by default

  • 7: 21744da = 3: e8f7d09 LOG_HTTPS_REQUESTS: quote URL

  • 9: 2bae65a ! 4: 0250bfe trigger-workflow-dispatch: accommodate for clock skew

    @@ GitForWindowsHelper/trigger-workflow-dispatch.js: const triggerWorkflowDispatch
          )
      
     -    const runs = await waitForWorkflowRun(context, owner, repo, workflow_id, new Date(date).toISOString(), token)
    -+    // to avoid any potential clock skew, we set the "after" time to 15 seconds before the current time
    -+    const after = new Date(Date.parse(date) - 15000).toISOString()
    ++    // to avoid any potential clock skew, we set the "after" time to 5 seconds before the current time
    ++    const after = new Date(Date.parse(date) - 5000).toISOString()
     +    const runs = await waitForWorkflowRun(context, owner, repo, workflow_id, after, token)
          return runs[0]
      }
  • 1: 9853f98 ! 5: 239bbf8 Optionally override the active GitHub org via the environment

    @@ GitForWindowsHelper/index.js
      
      module.exports = async function (context, req) {
          const withStatus = (status, headers, body) => {
    -@@ GitForWindowsHelper/index.js: module.exports = async function (context, req) {
    -     try {
    -         const selfHostedARM64Runners = require('./self-hosted-arm64-runners')
    -         if (req.headers['x-github-event'] === 'workflow_job'
    --            && ['git-for-windows/git-for-windows-automation', 'git-for-windows/git-sdk-arm64'].includes(req.body.repository.full_name)
    -+            && [`${activeOrg}/git-for-windows-automation`, `${activeOrg}/git-sdk-arm64`].includes(req.body.repository.full_name)
    -             && ['queued', 'completed'].includes(req.body.action)
    -             && req.body.workflow_job.labels.length === 2
    -             && req.body.workflow_job.labels[0] === 'Windows'
     @@ GitForWindowsHelper/index.js: module.exports = async function (context, req) {
              if (req.headers['x-github-event'] === 'workflow_run'
                  && req.body.workflow_run?.event === 'workflow_dispatch'
    @@ GitForWindowsHelper/org.js (new)
     +    activeOrg: process.env.ACTIVE_ORG || 'git-for-windows'
     +}
     
    - ## GitForWindowsHelper/self-hosted-arm64-runners.js ##
    -@@
    -+const { activeOrg } = require('./org')
    -+
    - module.exports = async (context, req) => {
    -     const action = req.body.action
    -     const owner = req.body.repository.owner.login
    -@@ GitForWindowsHelper/self-hosted-arm64-runners.js: module.exports = async (context, req) => {
    -         const answer = await triggerWorkflowDispatch(
    -             context,
    -             token,
    --            'git-for-windows',
    -+            activeOrg,
    -             'git-for-windows-automation',
    -             'create-azure-self-hosted-runners.yml',
    -             'main', {
    -@@ GitForWindowsHelper/self-hosted-arm64-runners.js: module.exports = async (context, req) => {
    -         const answer = await triggerWorkflowDispatch(
    -             context,
    -             token,
    --            'git-for-windows',
    -+            activeOrg,
    -             'git-for-windows-automation',
    -             'delete-self-hosted-runner.yml',
    -             'main', {
    -
      ## GitForWindowsHelper/slash-commands.js ##
     @@
     +const { activeOrg } = require('./org')
  • 2: 6eaea71 ! 6: 7657e4d Optionally override the active GitHub App via the environment

    @@ GitForWindowsHelper/cascading-runs.js: const getToken = (() => {
      
      const isAllowed = async (context, owner, repo, login) => {
     -    if (login === 'gitforwindowshelper[bot]') return true
    -+    if (login === activeBot) return true
    ++    if (login === `${activeBot}[bot]`) return true
          const getCollaboratorPermissions = require('./get-collaborator-permissions')
          const token = await getToken(context, owner, repo)
          const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
    @@ GitForWindowsHelper/cascading-runs.js: const cascadingRuns = async (context, req
     -    const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === 'gitforwindowshelper'
     -        ? 'gitforwindowshelper[bot]' : req.body.sender.login
     +    const sender = req.body.sender.login === 'ghost' && checkRun?.app?.slug === activeBot
    -+        ? activeBot : req.body.sender.login
    ++        ? `${activeBot}[bot]` : req.body.sender.login
      
          if (action === 'completed') {
              if (name === 'tag-git') {
    @@ GitForWindowsHelper/finalize-g4w-release.js: module.exports = async (context, re
      
          const isAllowed = async (login) => {
     -        if (login === 'gitforwindowshelper[bot]') return true
    -+        if (login === activeBot) return true
    ++        if (login === `${activeBot}[bot]`) return true
              const getCollaboratorPermissions = require('./get-collaborator-permissions')
              const token = await getToken()
              const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
    @@ GitForWindowsHelper/org.js
     +    activeBot: process.env.ACTIVE_BOT || 'gitforwindowshelper',
      }
     
    - ## GitForWindowsHelper/self-hosted-arm64-runners.js ##
    -@@
    --const { activeOrg } = require('./org')
    -+const { activeOrg, activeBot } = require('./org')
    - 
    - module.exports = async (context, req) => {
    -     const action = req.body.action
    -@@ GitForWindowsHelper/self-hosted-arm64-runners.js: module.exports = async (context, req) => {
    -     })()
    - 
    -     const isAllowed = async (login) => {
    --        if (login === 'gitforwindowshelper[bot]') return true
    -+        if (login === activeBot) return true
    -         const getCollaboratorPermissions = require('./get-collaborator-permissions')
    -         const token = await getToken()
    -         const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
    -
      ## __tests__/index.test.js ##
     @@ __tests__/index.test.js: test('a completed `tag-git` run triggers `git-artifacts` runs', async () => {
                      slug: 'gitforwindowshelper',
  • 3: 0b2fbb0 = 7: ef60df8 deploy: allow overriding the Azure Function name via a repo variable

  • 4: 8868b74 = 8: ecbdf2b deploy: use RBAC instead of publish profile

  • 6: 2690c61 = 9: 0d66474 deploy: exclude more files from being deployed

  • 8: e6343cd = 10: adbe36e deploy: use OpenID Connect instead of RBAC

  • 10: 664e51c = 11: fb14c06 Merge branch 'deploy-in-other-org'

  • -: ------- > 12: 04ff263 trigger-workflow-dispatch: use return_run_details when available

Copy link
Copy Markdown
Member

@mjcheetham mjcheetham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one question/suggestion, but not a blocker.

Comment thread GitForWindowsHelper/trigger-workflow-dispatch.js
@dscho dscho merged commit edf386f into main Mar 31, 2026
1 check passed
@dscho dscho deleted the accommodate-for-clock-skew branch March 31, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants