feat(scaffolder): add returnWorkflowRunDetails to github:actions:dispatch action

Signed-off-by: abdellahhanane <abdellahhanane44@gmail.com>
This commit is contained in:
abdellahhanane
2026-03-04 22:37:10 -05:00
parent c7fb3299ee
commit 2a278818ff
5 changed files with 193 additions and 18 deletions
@@ -24,10 +24,13 @@ export function createGithubActionsDispatchAction(options: {
workflowId: string;
branchOrTagName: string;
workflowInputs?: Record<string, string> | 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;
@@ -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',
);
});
});
@@ -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,
},
},
],
}),
},
];
@@ -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();
});
});
@@ -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<never, 204> 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;
}
},
});
}