From fe0f8cb871300271808c4ac3b969b69514f0ba66 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 21 Jul 2026 14:28:04 +0200 Subject: [PATCH 1/2] trigger-workflow-dispatch: tolerate earlier run timestamps A workflow run can be timestamped slightly before the dispatch response. Since GitGitGadget uses that response's `Date` header as the polling lower bound, the run can be excluded. Allow five seconds of leeway so the dispatched run remains eligible for polling. Signed-off-by: Johannes Schindelin --- GitGitGadget/trigger-workflow-dispatch.js | 4 +++- __tests__/trigger-workflow-dispatch.test.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/GitGitGadget/trigger-workflow-dispatch.js b/GitGitGadget/trigger-workflow-dispatch.js index 7a85011..c0206f5 100644 --- a/GitGitGadget/trigger-workflow-dispatch.js +++ b/GitGitGadget/trigger-workflow-dispatch.js @@ -52,7 +52,9 @@ const triggerWorkflowDispatch = async (context, token, owner, repo, workflow_id, { ref, inputs } ) - const runs = await waitForWorkflowRun(context, token, owner, repo, workflow_id, new Date(date).toISOString()) + // Avoid missing the run if its timestamp is slightly earlier than the response. + const after = new Date(Date.parse(date) - 5000).toISOString() + const runs = await waitForWorkflowRun(context, token, owner, repo, workflow_id, after) return runs[0] } diff --git a/__tests__/trigger-workflow-dispatch.test.js b/__tests__/trigger-workflow-dispatch.test.js index 1515d24..b03971d 100644 --- a/__tests__/trigger-workflow-dispatch.test.js +++ b/__tests__/trigger-workflow-dispatch.test.js @@ -7,7 +7,7 @@ const mockHTTPSRequest = jest.fn(async (_context, _hostname, method, requestPath } } if (method === 'GET' && requestPath === '/user') return { login: 'the actor' } - if (method === 'GET' && requestPath === '/repos/hello/world/actions/runs?actor=the actor&event=workflow_dispatch&created=2023-01-23T01:23:45.000Z..*') { + if (method === 'GET' && requestPath === '/repos/hello/world/actions/runs?actor=the actor&event=workflow_dispatch&created=2023-01-23T01:23:40.000Z..*') { return { workflow_runs: [ { path: 'not this one.yml' }, From 5ca8cce1d3b84b9bed544b6fb74318c0ea438b6f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 21 Jul 2026 15:08:12 +0200 Subject: [PATCH 2/2] trigger-workflow-dispatch: avoid polling when run details are returned GitGitGadget must identify the workflow run created by each dispatch. The old response omitted that identity, forcing a time-based poll that is sensitive to clock skew. GitHub's workflow dispatch API can now return the run ID when `return_run_details` is set; see https://github.blog/changelog/2026-02-19-workflow-dispatch-api-now-returns-run-ids/. Use those details directly when available. Retain polling for responses without run details, using the existing clock-skew leeway. Signed-off-by: Johannes Schindelin --- GitGitGadget/trigger-workflow-dispatch.js | 7 +++-- __tests__/trigger-workflow-dispatch.test.js | 32 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/GitGitGadget/trigger-workflow-dispatch.js b/GitGitGadget/trigger-workflow-dispatch.js index c0206f5..6a023c5 100644 --- a/GitGitGadget/trigger-workflow-dispatch.js +++ b/GitGitGadget/trigger-workflow-dispatch.js @@ -44,14 +44,17 @@ const triggerWorkflowDispatch = async (context, token, owner, repo, workflow_id, token = await getInstallationAccessToken(context, installationID) } - const { headers: { date } } = await gitHubAPIRequest( + const response = await gitHubAPIRequest( context, token, 'POST', `/repos/${owner}/${repo}/actions/workflows/${workflow_id}/dispatches`, - { ref, inputs } + { ref, inputs, return_run_details: true } ) + if (response.workflow_run_id) return response + + const date = response.headers?.date || new Date().toISOString() // Avoid missing the run if its timestamp is slightly earlier than the response. const after = new Date(Date.parse(date) - 5000).toISOString() const runs = await waitForWorkflowRun(context, token, owner, repo, workflow_id, after) diff --git a/__tests__/trigger-workflow-dispatch.test.js b/__tests__/trigger-workflow-dispatch.test.js index b03971d..66ce0e4 100644 --- a/__tests__/trigger-workflow-dispatch.test.js +++ b/__tests__/trigger-workflow-dispatch.test.js @@ -1,4 +1,11 @@ const mockHTTPSRequest = jest.fn(async (_context, _hostname, method, requestPath) => { + if (method === 'POST' && requestPath === '/repos/hello/world/actions/workflows/run-details.yml/dispatches') { + return { + workflow_run_id: 12345, + run_url: 'https://api.github.com/repos/hello/world/actions/runs/12345', + html_url: 'https://github.com/hello/world/actions/runs/12345' + } + } if (method === 'POST' && requestPath === '/repos/hello/world/actions/workflows/the-workflow.yml/dispatches') { return { headers: { @@ -45,13 +52,36 @@ const { privateKey } = generateKeyPairSync('rsa', { }) process.env['GITHUB_APP_PRIVATE_KEY'] = privateKey -test('trigger a workflow_dispatch event and wait for workflow run', async () => { +beforeEach(() => mockHTTPSRequest.mockClear()) + +test('use workflow run details returned by the dispatch', async () => { + const context = {} + const run = await triggerWorkflowDispatch(context, 'my-token', 'hello', 'world', 'run-details.yml', 'HEAD', { abc: 123 }) + expect(run).toEqual({ + workflow_run_id: 12345, + run_url: 'https://api.github.com/repos/hello/world/actions/runs/12345', + html_url: 'https://github.com/hello/world/actions/runs/12345' + }) + expect(mockHTTPSRequest).toHaveBeenCalledTimes(1) + expect(mockHTTPSRequest.mock.calls[0][4]).toEqual({ + ref: 'HEAD', + inputs: { abc: 123 }, + return_run_details: true + }) +}) + +test('fall back to waiting for the workflow run', async () => { const context = {} const run = await triggerWorkflowDispatch(context, 'my-token', 'hello', 'world', 'the-workflow.yml', 'HEAD', { abc: 123 }) expect(run).toEqual({ path: '.github/workflows/the-workflow.yml', breadcrumb: true }) + expect(mockHTTPSRequest.mock.calls[0][4]).toEqual({ + ref: 'HEAD', + inputs: { abc: 123 }, + return_run_details: true + }) }) test('list workflow runs', async () => {