diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index b6b2d97ac8..3538529135 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -24,10 +24,13 @@ export function createGithubActionsDispatchAction(options: { workflowId: string; branchOrTagName: string; workflowInputs?: Record | undefined; + returnWorkflowRunDetails?: boolean | undefined; token?: string | undefined; }, { - [x: string]: any; + workflowRunId?: number | undefined; + workflowRunUrl?: string | undefined; + workflowRunHtmlUrl?: string | undefined; }, 'v2' >; @@ -250,8 +253,8 @@ export function createGithubRepoCreateAction(options: { access: string; } | { - team: string; access: string; + team: string; } )[] | undefined; @@ -441,8 +444,8 @@ export function createPublishGithubAction(options: { access: string; } | { - team: string; access: string; + team: string; } )[] | undefined; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts index 5583b745d5..b917efbf28 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.test.ts @@ -93,6 +93,7 @@ describe('github:actions:dispatch', () => { ).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', + return_run_details: false, workflow_id: workflowId, ref: branchOrTagName, }); @@ -119,9 +120,49 @@ describe('github:actions:dispatch', () => { ).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', + return_run_details: false, workflow_id: workflowId, ref: branchOrTagName, inputs: workflowInputs, }); }); + + it('should call createWorkflowDispatch with return_run_details when using the returnWorkflowRunDetails example', async () => { + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: { + workflow_run_id: 123, + run_url: 'https://api.github.com/repos/owner/repo/actions/runs/123', + html_url: 'https://github.com/owner/repo/actions/runs/123', + }, + }); + + const exampleInput = yaml.parse(examples[3].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: { + ...exampleInput, + repoUrl: 'github.com?repo=repo&owner=owner', + }, + }); + await action.handler(ctx); + + expect( + mockOctokit.rest.actions.createWorkflowDispatch, + ).toHaveBeenCalledWith( + expect.objectContaining({ + owner: 'owner', + repo: 'repo', + return_run_details: true, + }), + ); + + expect(ctx.output).toHaveBeenCalledWith('workflowRunId', 123); + expect(ctx.output).toHaveBeenCalledWith( + 'workflowRunUrl', + 'https://api.github.com/repos/owner/repo/actions/runs/123', + ); + expect(ctx.output).toHaveBeenCalledWith( + 'workflowRunHtmlUrl', + 'https://github.com/owner/repo/actions/runs/123', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.ts index 33ad86dd89..2b8baafdbe 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.examples.ts @@ -71,4 +71,21 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'GitHub Action Workflow returning run details', + example: yaml.stringify({ + steps: [ + { + action: 'github:actions:dispatch', + name: 'Dispatch GitHub Action Workflow and get run ID', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'WORKFLOW_ID', + branchOrTagName: 'main', + returnWorkflowRunDetails: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts index 94a51e5b4e..67f0cf3e82 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.test.ts @@ -101,6 +101,7 @@ describe('github:actions:dispatch', () => { repo: 'repo', workflow_id: workflowId, ref: branchOrTagName, + return_run_details: false, }); }); @@ -128,6 +129,67 @@ describe('github:actions:dispatch', () => { workflow_id: workflowId, ref: branchOrTagName, inputs: workflowInputs, + return_run_details: false, }); }); + + it('should call createWorkflowDispatch with return_run_details when returnWorkflowRunDetails is true', async () => { + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: { + workflow_run_id: 123, + run_url: 'https://api.github.com/repos/owner/repo/actions/runs/123', + html_url: 'https://github.com/owner/repo/actions/runs/123', + }, + }); + + const ctx = Object.assign({}, mockContext, { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'dispatch_workflow', + branchOrTagName: 'main', + returnWorkflowRunDetails: true, + }, + }); + await action.handler(ctx); + + expect( + mockOctokit.rest.actions.createWorkflowDispatch, + ).toHaveBeenCalledWith( + expect.objectContaining({ + owner: 'owner', + repo: 'repo', + workflow_id: 'dispatch_workflow', + ref: 'main', + return_run_details: true, + }), + ); + + expect(ctx.output).toHaveBeenCalledWith('workflowRunId', 123); + expect(ctx.output).toHaveBeenCalledWith( + 'workflowRunUrl', + 'https://api.github.com/repos/owner/repo/actions/runs/123', + ); + expect(ctx.output).toHaveBeenCalledWith( + 'workflowRunHtmlUrl', + 'https://github.com/owner/repo/actions/runs/123', + ); + }); + + it('should not set outputs when returnWorkflowRunDetails is false', async () => { + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: undefined, + }); + + const ctx = Object.assign({}, mockContext, { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'dispatch_workflow', + branchOrTagName: 'main', + returnWorkflowRunDetails: false, + }, + }); + await action.handler(ctx); + + expect(ctx.output).not.toHaveBeenCalled(); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index f35cbcc04c..cc2f5f13e8 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { InputError } from '@backstage/errors'; +import { assertError, InputError } from '@backstage/errors'; import { GithubCredentialsProvider, ScmIntegrations, @@ -62,7 +62,14 @@ export function createGithubActionsDispatchAction(options: { z .record(z.string(), { description: - 'Inputs keys and values to send to GitHub Action configured on the workflow file. The maximum number of properties is 10.', + 'Inputs keys and values to send to GitHub Action configured on the workflow file. The maximum number of properties is 25.', + }) + .optional(), + returnWorkflowRunDetails: z => + z + .boolean({ + description: + 'If true, returns the workflow run ID and URLs as action outputs.', }) .optional(), token: z => @@ -73,6 +80,14 @@ export function createGithubActionsDispatchAction(options: { }) .optional(), }, + output: { + workflowRunId: z => + z.number({ description: 'The triggered workflow run ID' }).optional(), + workflowRunUrl: z => + z.string({ description: 'API URL of the workflow run' }).optional(), + workflowRunHtmlUrl: z => + z.string({ description: 'HTML URL of the workflow run' }).optional(), + }, }, async handler(ctx) { const { @@ -80,6 +95,7 @@ export function createGithubActionsDispatchAction(options: { workflowId, branchOrTagName, workflowInputs, + returnWorkflowRunDetails, token: providedToken, } = ctx.input; @@ -106,20 +122,56 @@ export function createGithubActionsDispatchAction(options: { log: ctx.logger, }); - await ctx.checkpoint({ - key: `create.workflow.dispatch.${owner}.${repo}.${workflowId}`, - fn: async () => { - await client.rest.actions.createWorkflowDispatch({ - owner, - repo, - workflow_id: workflowId, - ref: branchOrTagName, - inputs: workflowInputs, - }); + try { + const runDetails = await ctx.checkpoint({ + key: `create.workflow.dispatch.${owner}.${repo}.${workflowId}`, + fn: async () => { + const response = await client.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: workflowId, + ref: branchOrTagName, + inputs: workflowInputs, + return_run_details: returnWorkflowRunDetails ?? false, + }); - ctx.logger.info(`Workflow ${workflowId} dispatched successfully`); - }, - }); + ctx.logger.info(`Workflow ${workflowId} dispatched successfully`); + + if (returnWorkflowRunDetails && response.data) { + // GitHub's API returns 200 with run details when return_run_details is true. + // @octokit/openapi-types still types this as OctokitResponse because + // it hasn't picked up the updated GitHub OpenAPI spec yet. + // See: https://github.blog/changelog/2026-02-19-workflow-dispatch-api-now-returns-run-ids/ + // This cast can be removed once @octokit/openapi-types includes the updated spec. + // Note: only supported on GitHub.com and GitHub Enterprise Cloud, not GitHub Enterprise Server + const data = response.data as unknown as { + workflow_run_id: number; + run_url: string; + html_url: string; + }; + + return { + workflow_run_id: data.workflow_run_id, + run_url: data.run_url, + workflowRunHtmlUrl: data.html_url, + }; + } + return null; + }, + }); + + if (runDetails) { + ctx.output('workflowRunId', runDetails.workflow_run_id); + ctx.output('workflowRunUrl', runDetails.run_url); + ctx.output('workflowRunHtmlUrl', runDetails.workflowRunHtmlUrl); + } + } catch (e) { + assertError(e); + ctx.logger.warn( + `Failed: dispatching workflow '${workflowId}' on repo: '${repo}', ${e.message}`, + ); + throw e; + } }, }); }