Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ jobs:
owner: codex-
workflow: dispatch.yml
workflow_inputs: '{"cake":"delicious"}'
workflow_timeout_seconds: 30
workflow_job_steps_retry_seconds: 10
- name: Evaluate that the Run ID output has been set
run: |
if [ "${{ steps.return_dispatch.outputs.run_id }}" == "" ]; then
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ name: action-test
on:
workflow_dispatch:
inputs:
distinct_id:
required: true
cake: # Test for input passthrough.
required: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: echo distinct ID ${{ github.event.inputs.distinct_id }}
run: echo ${{ github.event.inputs.distinct_id }}
- name: echo input passthrough
run: echo ${{ github.event.inputs.cake }}
110 changes: 16 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
# GitHub Action: return-dispatch

[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/codex-/return-dispatch/test.yml?style=flat-square)](https://github.com/Codex-/return-dispatch/actions/workflows/test.yml) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![codecov](https://img.shields.io/codecov/c/github/Codex-/return-dispatch?style=flat-square)](https://codecov.io/gh/Codex-/return-dispatch) [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-return–dispatch-blue.svg?colorA=24292e&colorB=0366d6&style=flat-square&longCache=true&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAM6wAADOsB5dZE0gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAERSURBVCiRhZG/SsMxFEZPfsVJ61jbxaF0cRQRcRJ9hlYn30IHN/+9iquDCOIsblIrOjqKgy5aKoJQj4O3EEtbPwhJbr6Te28CmdSKeqzeqr0YbfVIrTBKakvtOl5dtTkK+v4HfA9PEyBFCY9AGVgCBLaBp1jPAyfAJ/AAdIEG0dNAiyP7+K1qIfMdonZic6+WJoBJvQlvuwDqcXadUuqPA1NKAlexbRTAIMvMOCjTbMwl1LtI/6KWJ5Q6rT6Ht1MA58AX8Apcqqt5r2qhrgAXQC3CZ6i1+KMd9TRu3MvA3aH/fFPnBodb6oe6HM8+lYHrGdRXW8M9bMZtPXUji69lmf5Cmamq7quNLFZXD9Rq7v0Bpc1o/tp0fisAAAAASUVORK5CYII=)](https://github.com/marketplace/actions/return-dispatch)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/codex-/return-dispatch/test.yml?style=flat-square)](https://github.com/Codex-/return-dispatch/actions/workflows/test.yml) [![codecov](https://img.shields.io/codecov/c/github/Codex-/return-dispatch?style=flat-square)](https://codecov.io/gh/Codex-/return-dispatch) [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-return–dispatch-blue.svg?colorA=24292e&colorB=0366d6&style=flat-square&longCache=true&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAM6wAADOsB5dZE0gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAERSURBVCiRhZG/SsMxFEZPfsVJ61jbxaF0cRQRcRJ9hlYn30IHN/+9iquDCOIsblIrOjqKgy5aKoJQj4O3EEtbPwhJbr6Te28CmdSKeqzeqr0YbfVIrTBKakvtOl5dtTkK+v4HfA9PEyBFCY9AGVgCBLaBp1jPAyfAJ/AAdIEG0dNAiyP7+K1qIfMdonZic6+WJoBJvQlvuwDqcXadUuqPA1NKAlexbRTAIMvMOCjTbMwl1LtI/6KWJ5Q6rT6Ht1MA58AX8Apcqqt5r2qhrgAXQC3CZ6i1+KMd9TRu3MvA3aH/fFPnBodb6oe6HM8+lYHrGdRXW8M9bMZtPXUji69lmf5Cmamq7quNLFZXD9Rq7v0Bpc1o/tp0fisAAAAASUVORK5CYII=)](https://github.com/marketplace/actions/return-dispatch)

Dispatch an action to a foreign repository and output the newly created run ID.

This Action exists as a workaround for the issue where dispatching an action to foreign repository does not return any kind of identifier.

The returned run ID can be used to await the completion of the remote run using [`await-remote-run`](https://github.com/Codex-/await-remote-run).

## Usage

Ensure you have configured your remote action correctly, see below for an example.

### Dispatching Repository Action
The dispatched workflow needs no special setup. It only has to accept the `workflow_dispatch`
event and declare any inputs you pass via `workflow_inputs`.

```yaml
steps:
Expand All @@ -26,12 +23,6 @@ steps:
owner: repository-owner
workflow: automation-test.yml
workflow_inputs: '{ "some_input": "value" }' # Optional
workflow_timeout_seconds: 120 # Default: 300
workflow_job_steps_retry_seconds:
# Lineal backoff retry attempts are made where the attempt count is
# the magnitude and the scaling value is `workflow_job_steps_retry_seconds`
10 # Default: 5
distinct_id: someDistinctId # Optional

- name: Use the output run ID and URL
run: |
Expand All @@ -47,101 +38,32 @@ steps:
run_id: ${{ steps.return_dispatch.outputs.run_id }}
```

### Receiving Repository Action

In the earliest possible stage for the Action, add the input into the name.

As every step needs a `uses` or `run`, simply `echo` the ID or similar to satisfy this requirement.

```yaml
name: action-test
on:
workflow_dispatch:
inputs:
distinct_id:

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: echo distinct ID ${{ inputs.distinct_id }}
run: echo "${DISTINCT_ID}"
env:
DISTINCT_ID: ${{ inputs.distinct_id }}
```

## Token

To be able to use dispatch we need to use a token which has `repo` permissions. `GITHUB_TOKEN` currently does not allow adding permissions for `repo` level permissions currently so a Personal Access Token (PAT) must be used.
`GITHUB_TOKEN` can only access the repository containing the workflow, so dispatching
to another repository requires a Personal Access Token (PAT). Dispatching within the
same repository works with `GITHUB_TOKEN`, provided it is granted `actions: write`.

### Permissions Required

The permissions required for this action to function correctly are:
One of the following, depending on the token type:

- `repo` scope
- You may get away with simply having `repo:public_repo`
- `repo` is definitely needed if the repository is private.
- `actions:read`
- `actions:write`
- Fine-grained PAT, GitHub App, or `GITHUB_TOKEN`: `Actions` repository permission, **write**
- Classic PAT or OAuth token: `repo` scope

### APIs Used

For the sake of transparency please note that this action uses the following API calls:

- [Create a workflow dispatch event](https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event)
- POST `/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches`
- Permissions:
- `repo`
- `actions:write`
- [List repository workflows](https://docs.github.com/en/rest/actions/workflows#list-repository-workflows)
- GET `/repos/{owner}/{repo}/actions/workflows`
- Permissions:
- `repo`
- `actions:read`
- [List workflow runs](https://docs.github.com/en/rest/actions/workflow-runs#list-workflow-runs-for-a-repository)
- GET `/repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs`
- Permissions:
- `repo`
- [List jobs for a workflow run](https://docs.github.com/en/rest/actions/workflow-jobs#list-jobs-for-a-workflow-run)
- GET `/repos/{owner}/{repo}/actions/runs/{run_id}/jobs`
- Permissions:
- `repo`
- `actions:read`
- Sent with `return_run_details: true`, so the response carries the new run's ID and URL

For more information please see [api.ts](./src/api.ts).
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Where does this help?

If you have an action in a repository that dispatches an action on a foreign repository currently with Github API there is no way to know what the foreign run you've just dispatched is. Identifying this can be cumbersome and tricky.

The consequence of not being provided with something to identify the run is that you cannot easily wait for this run or poll the run for it's completion status (success, failure, etc).

## Flow

```ascii
┌─────────────────┐
│ │
│ Dispatch Action │
│ │
│ with unique ID │
│ │
└───────┬─────────┘
│
│
▼ ┌───────────────┐
┌────────────────┐ │ │
│ │ │ Request steps │
│ Request top 10 ├────────────────►│ │
│ │ │ for each run │
│ workflow runs │ │ │
│ │◄────────────────┤ and search │
└───────┬────────┘ Retry │ │
│ └───────┬───────┘
│ │
Timeout │ │
│ │
▼ ▼
┌──────┐ ┌───────────────┐
│ Fail │ │ Output run ID │
└──────┘ └───────────────┘
```
If you have an action in a repository that dispatches an action on a foreign repository,
you need to know which foreign run you've just dispatched before you can wait for it or
poll it for a completion status (success, failure, etc).

This Action gets you that run ID and URL as step outputs, so you can hand them straight
to something like [`await-remote-run`](https://github.com/Codex-/await-remote-run).
10 changes: 0 additions & 10 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,6 @@ inputs:
required: true
workflow_inputs:
description: A flat JSON object, only supports strings, numbers, and booleans (as per workflow inputs API).
workflow_timeout_seconds:
description: Time until giving up waiting for the start of the workflow run.
default: 300
workflow_job_steps_retry_seconds:
description: |
The interval (in seconds) to wait between retries. A linear backoff strategy is used, where the wait time
increases by this value with each attempt (e.g., 1st retry = this value, 2nd retry = 2x this value, etc.).
default: 5
distinct_id:
description: Specify a static string to use instead of a random distinct ID.

runs:
using: node24
Expand Down
35 changes: 0 additions & 35 deletions src/__snapshots__/return-dispatch.spec.ts.snap

This file was deleted.

41 changes: 0 additions & 41 deletions src/action.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { randomUUID } from "node:crypto";

import * as core from "@actions/core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { type ActionConfig, getConfig } from "./action.ts";

vi.mock("node:crypto", () => ({
randomUUID: vi.fn(),
}));
vi.mock("@actions/core");

describe("Action", () => {
Expand All @@ -27,9 +22,6 @@ describe("Action", () => {
owner: "owner",
workflow: "workflow_name",
workflow_inputs: JSON.stringify(workflowInputs),
workflow_timeout_seconds: "60",
workflow_job_steps_retry_seconds: "3",
distinct_id: "distinct_id",
};

vi.spyOn(core, "getInput").mockImplementation((input: string): string => {
Expand All @@ -47,12 +39,6 @@ describe("Action", () => {
return mockEnvConfig.workflow;
case "workflow_inputs":
return mockEnvConfig.workflow_inputs;
case "workflow_timeout_seconds":
return mockEnvConfig.workflow_timeout_seconds;
case "workflow_job_steps_retry_seconds":
return mockEnvConfig.workflow_job_steps_retry_seconds;
case "distinct_id":
return mockEnvConfig.distinct_id;
default:
throw new Error("invalid input requested");
}
Expand All @@ -74,9 +60,6 @@ describe("Action", () => {
expect(config.owner).toStrictEqual("owner");
expect(config.workflow).toStrictEqual("workflow_name");
expect(config.workflowInputs).toStrictEqual(workflowInputs);
expect(config.workflowTimeoutSeconds).toStrictEqual(60);
expect(config.workflowJobStepsRetrySeconds).toStrictEqual(3);
expect(config.distinctId).toStrictEqual("distinct_id");
});

it("should have a number for a workflow when given a workflow ID", () => {
Expand All @@ -86,20 +69,6 @@ describe("Action", () => {
expect(config.workflow).toStrictEqual(123456);
});

it("should provide a default workflow timeout if none is supplied", () => {
mockEnvConfig.workflow_timeout_seconds = "";
const config: ActionConfig = getConfig();

expect(config.workflowTimeoutSeconds).toStrictEqual(300);
});

it("should provide a default workflow job step retry if none is supplied", () => {
mockEnvConfig.workflow_job_steps_retry_seconds = "";
const config: ActionConfig = getConfig();

expect(config.workflowJobStepsRetrySeconds).toStrictEqual(5);
});

it("should handle no inputs being provided", () => {
mockEnvConfig.workflow_inputs = "";
const config: ActionConfig = getConfig();
Expand Down Expand Up @@ -136,15 +105,5 @@ describe("Action", () => {
callAndAssert('{"vegetable":null}', '"vegetable" value is null');
callAndAssert('{"fruit":[]}', '"fruit" value is Array');
});

it("should handle no distinct_id being provided", () => {
const v4Mock = vi.mocked(randomUUID);
v4Mock.mockImplementationOnce(() => "test-mocked-uuid-is-used");
mockEnvConfig.distinct_id = "";
const config: ActionConfig = getConfig();

expect(config.distinctId).toStrictEqual("test-mocked-uuid-is-used");
expect(v4Mock).toHaveBeenCalledOnce();
});
});
});
38 changes: 0 additions & 38 deletions src/action.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { randomUUID } from "node:crypto";

import * as core from "@actions/core";

const WORKFLOW_TIMEOUT_SECONDS = 5 * 60;
const WORKFLOW_JOB_STEPS_RETRY_SECONDS = 5;

/**
* action.yaml definition.
*/
Expand Down Expand Up @@ -38,21 +33,6 @@ export interface ActionConfig {
* A flat JSON object, only supports strings (as per workflow inputs API).
*/
workflowInputs?: ActionWorkflowInputs;

/**
* Time until giving up on identifying the Run ID.
*/
workflowTimeoutSeconds: number;

/**
* Time in retries for identifying the Run ID.
*/
workflowJobStepsRetrySeconds: number;

/**
* Specify a static ID to use instead of a distinct ID.
*/
distinctId: string;
}

type ActionWorkflowInputs = Record<string, string | number | boolean>;
Expand All @@ -72,14 +52,6 @@ export function getConfig(): ActionConfig {
core.getInput("workflow", { required: true }),
),
workflowInputs: getWorkflowInputs(core.getInput("workflow_inputs")),
workflowTimeoutSeconds:
getNumberFromValue(core.getInput("workflow_timeout_seconds")) ??
WORKFLOW_TIMEOUT_SECONDS,
workflowJobStepsRetrySeconds:
getNumberFromValue(core.getInput("workflow_job_steps_retry_seconds")) ??
WORKFLOW_JOB_STEPS_RETRY_SECONDS,
distinctId:
getOptionalWorkflowValue(core.getInput("distinct_id")) ?? randomUUID(),
};
}

Expand Down Expand Up @@ -141,13 +113,3 @@ function tryGetWorkflowAsNumber(workflowInput: string): string | number {
return workflowInput;
}
}

/**
* We want empty strings to simply be undefined.
*
* While simple, make it very clear that the usage of `||`
* is intentional here.
*/
function getOptionalWorkflowValue(workflowInput: string): string | undefined {
return workflowInput || undefined;
}
Loading
Loading