Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .github/fixtures/script-file/all-args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = async ({github, octokit, getOctokit, context, core, exec, glob, io, require}) => {
return [github, octokit, getOctokit, context, core, exec, glob, io, require]
.map(arg => typeof arg)
.every(t => t === 'function' || t === 'object')
}
1 change: 1 addition & 0 deletions .github/fixtures/script-file/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = async () => 'hello from script-file'
1 change: 1 addition & 0 deletions .github/fixtures/script-file/json-return.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = async ({context}) => ({repo: context.repo.repo, owner: context.repo.owner})
1 change: 1 addition & 0 deletions .github/fixtures/script-file/not-a-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 42
1 change: 1 addition & 0 deletions .github/fixtures/script-file/sibling-caller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = async ({require}) => require('./sibling-module').value
1 change: 1 addition & 0 deletions .github/fixtures/script-file/sibling-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {value: 'loaded-by-require'}
171 changes: 171 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,174 @@ jobs:
echo $'::error::\u274C' "Expected base-url to equal '$expected', got $actual"
exit 1
fi

test-script-file-basic:
name: 'Integration test: script-file - relative path, string return'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
uses: ./
with:
script-file: .github/fixtures/script-file/basic.js
result-encoding: string
- run: |
expected="hello from script-file"
if [[ "${{ steps.act.outputs.result }}" != "$expected" ]]; then
echo $'::error::❌' "Expected '$expected', got ${{ steps.act.outputs.result }}"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-absolute-path:
name: 'Integration test: script-file - absolute path'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
uses: ./
with:
script-file: ${{ github.workspace }}/.github/fixtures/script-file/basic.js
result-encoding: string
- run: |
expected="hello from script-file"
if [[ "${{ steps.act.outputs.result }}" != "$expected" ]]; then
echo $'::error::❌' "Expected '$expected', got ${{ steps.act.outputs.result }}"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-all-ioc-args:
name: 'Integration test: script-file - all IoC args available'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
uses: ./
with:
script-file: .github/fixtures/script-file/all-args.js
- run: |
if [[ "${{ steps.act.outputs.result }}" != "true" ]]; then
echo $'::error::❌' "Expected all IoC args to be present, got ${{ steps.act.outputs.result }}"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-result-encoding-json:
name: 'Integration test: script-file - result-encoding json'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
uses: ./
with:
script-file: .github/fixtures/script-file/json-return.js
- run: |
expected='{"repo":"${{ github.event.repository.name }}","owner":"${{ github.repository_owner }}"}'
if [[ "${{ steps.act.outputs.result }}" != "$expected" ]]; then
echo $'::error::❌' "Expected '$expected', got ${{ steps.act.outputs.result }}"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-require-in-file:
name: 'Integration test: script-file - require inside script file'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
uses: ./
with:
script-file: .github/fixtures/script-file/sibling-caller.js
result-encoding: string
- run: |
expected="loaded-by-require"
if [[ "${{ steps.act.outputs.result }}" != "$expected" ]]; then
echo $'::error::❌' "Expected '$expected', got ${{ steps.act.outputs.result }}"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-conflict-both:
name: 'Integration test: script-file - fails when both script and script-file are set'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
continue-on-error: true
uses: ./
with:
script: return 1
script-file: .github/fixtures/script-file/basic.js
- run: |
if [[ "${{ steps.act.outcome }}" != "failure" ]]; then
echo $'::error::❌' "Expected step to fail when both inputs are set"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-conflict-neither:
name: 'Integration test: script-file - fails when neither script nor script-file is set'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
continue-on-error: true
uses: ./
- run: |
if [[ "${{ steps.act.outcome }}" != "failure" ]]; then
echo $'::error::❌' "Expected step to fail when no input is set"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-nonexistent-file:
name: 'Integration test: script-file - fails on nonexistent file'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
continue-on-error: true
uses: ./
with:
script-file: .github/fixtures/script-file/does-not-exist.js
- run: |
if [[ "${{ steps.act.outcome }}" != "failure" ]]; then
echo $'::error::❌' "Expected step to fail for nonexistent file"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-non-function-export:
name: 'Integration test: script-file - fails when file does not export a function'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
continue-on-error: true
uses: ./
with:
script-file: .github/fixtures/script-file/not-a-function.js
- run: |
if [[ "${{ steps.act.outcome }}" != "failure" ]]; then
echo $'::error::❌' "Expected step to fail for non-function export"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY

test-script-file-file-protocol-rejected:
name: 'Integration test: script-file - fails for file:// protocol'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: act
continue-on-error: true
uses: ./
with:
script-file: file://${{ github.workspace }}/.github/fixtures/script-file/basic.js
- run: |
if [[ "${{ steps.act.outcome }}" != "failure" ]]; then
echo $'::error::❌' "Expected step to fail for file:// protocol"
exit 1
fi
echo $'✅ Test passed' | tee -a $GITHUB_STEP_SUMMARY
104 changes: 65 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ You are welcome to still raise bugs in this repo.

### This action

To use this action, provide an input named `script` that contains the body of an asynchronous JavaScript function call.
The following arguments will be provided:
To use this action, provide either a `script` input (the body of an async function, inline in your workflow YAML) or a `script-file` input (a path to a JS file that `module.exports` an async function). Exactly one of the two must be provided.

The following arguments are available to both forms:

- `github` A pre-authenticated
[octokit/rest.js](https://octokit.github.io/rest.js) client with pagination plugins
Expand Down Expand Up @@ -201,6 +202,56 @@ By default, the following status codes will not be retried: `400, 401, 403, 404,

These retries are implemented using the [octokit/plugin-retry.js](https://github.com/octokit/plugin-retry.js) plugin. The retries use [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) to space out retries. ([source](https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/error-request.ts#L13))

## Script file

Coding long JS logic in yaml is not linted as JS/TS.
Instead of providing the `script` inline, you can use `script-file` to point to a JS file in your repository. The file must proide `module.exports` as an function (that may be async) — making it a proper module that linters and IDEs can fully analyse.

The action handler is called with a single [IoC](https://en.wikipedia.org/wiki/Inversion_of_control) dependency bag (defined in [`src/args.ts`](src/args.ts)). Its members are the same as those available to the inline `script`:

| Name | Description |
| --- | --- |
| `github` | Pre-authenticated [octokit/rest.js](https://octokit.github.io/rest.js) client |
| `octokit` | Alias for `github` |
| `getOctokit` | Factory for additional authenticated Octokit clients (see [Creating additional clients](#creating-additional-clients-with-getoctokit)) |
| `context` | [Workflow run context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts) |
| `core` | [@actions/core](https://github.com/actions/toolkit/tree/main/packages/core) |
| `exec` | [@actions/exec](https://github.com/actions/toolkit/tree/main/packages/exec) |
| `glob` | [@actions/glob](https://github.com/actions/toolkit/tree/main/packages/glob) |
| `io` | [@actions/io](https://github.com/actions/toolkit/tree/main/packages/io) |
| `require` | Wrapped `require` that resolves relative paths and local `node_modules` |

**Path resolution:** relative paths are resolved against `$GITHUB_WORKSPACE`; absolute paths are used as-is. The `file://` protocol is not supported.

`script` and `script-file` are mutually exclusive — exactly one must be provided.

```yaml
- uses: actions/checkout@v4
- uses: actions/github-script@v9
with:
script-file: .github/scripts/my-script.js
```

The action handler:

JS: `.github/scripts/my-script.js`

```js
module.exports = async ({github, context, core /* destructure what you need */}) => {
// your logic here
}
```

or TS: `.github/scripts/my-script.ts`

```ts
import type {AsyncFunctionArguments} from '@actions/github-script'

module.exports = async ({github, context, core /* destructure what you need */}: AsyncFunctionArguments) => {
// your logic here
}
```

## Examples

Note that `github-token` is optional in this action, and the input is there
Expand Down Expand Up @@ -377,52 +428,19 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/github-script@v9
with:
script: |
const script = require('./path/to/script.js')
console.log(script({github, context}))
script-file: ./path/to/script.js

```

And then export a function from your module:

```javascript
module.exports = ({github, context}) => {
module.exports = ({github, context }) => {
return context.payload.client_payload.value
}
```

Note that because you can't `require` things like the GitHub context or
Actions Toolkit libraries, you'll want to pass them as arguments to your
external function.

Additionally, you'll want to use the [checkout
action](https://github.com/actions/checkout) to make sure your script file is
available.

### Run a separate file with an async function

You can also use async functions in this manner, as long as you `await` it in
the inline script.

In your workflow:

```yaml
on: push

jobs:
echo-input:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/github-script@v9
env:
SHA: '${{env.parentSHA}}'
with:
script: |
const script = require('./path/to/script.js')
await script({github, context, core})
```

And then export an async function from your module:
The exported function may be async if you like:

```javascript
module.exports = async ({github, context, core}) => {
Expand All @@ -436,6 +454,14 @@ module.exports = async ({github, context, core}) => {
}
```

Note that because you can't `require` things like the GitHub context or
Actions Toolkit libraries, you'll want to accept them as arguments to your
external function: Your action is called with an [IoC](https://en.wikipedia.org/wiki/Inversion_of_control) dependency bag - destructure from it whatever you need. Check the docs above in the **Script file** section.

Additionally, you'll want to use the [checkout
action](https://github.com/actions/checkout) to make sure your script file is
available.

### Use npm packages

Like importing your own files above, you can also use installed modules.
Expand Down
Loading