mirror of
https://github.com/actions/github-script.git
synced 2026-06-06 17:14:23 +00:00
feat: add script-file input for lint-friendly external JS scripts
Closes #714 Inline `script` is a YAML string — invisible to linters and IDEs. The common workaround was wrapping a `require` call inside the inline script, which still needs boilerplate and assumes a path convention. New optional `script-file` input that accepts a path to a JS file. The file must `module.exports` an async function receiving the standard IoC dependency bag (`github`, `octokit`, `getOctokit`, `context`, `core`, `exec`, `glob`, `io`, `require`). ```yaml - uses: actions/checkout@v4 - uses: actions/github-script@v9 with: script-file: .github/scripts/my-script.js ``` `script` and `script-file` are mutually exclusive — exactly one must be provided. Relative paths resolve against `$GITHUB_WORKSPACE`; absolute paths are used as-is. - `action.yml` — adds `script-file` input; makes `script` optional - `src/script-file.ts` — path resolution and script loading logic - `src/args.ts` — `AsyncFunctionArguments` extracted from `async-function.ts` so neither execution path depends on the other - `src/main.ts` — mutual-exclusion validation; dispatches to the right execution path - `types/non-webpack-require.ts` — corrects `__non_webpack_require__` type from deprecated `NodeRequire` / wrong `NodeJS.RequireResolve` to `NodeJS.Require` - `__test__/script-file.test.ts` — 10 tests covering path resolution, arg forwarding, error cases - `README.md` — new `## Script file` section with usage, IoC bag table, path resolution rules - `.github/fixtures/script-file/` — fixture JS files for integration tests - `.github/workflows/integration.yml` — 10 new integration test jobs: happy path (relative path, absolute path, all IoC args, json/string encoding, require-in-file) and error cases (both inputs set, neither set, nonexistent file, non-function export, file:// protocol)
This commit is contained in:
parent
3a2844b7e9
commit
67c280d263
18 changed files with 519 additions and 97 deletions
5
.github/fixtures/script-file/all-args.js
vendored
Normal file
5
.github/fixtures/script-file/all-args.js
vendored
Normal file
|
|
@ -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
.github/fixtures/script-file/basic.js
vendored
Normal file
1
.github/fixtures/script-file/basic.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = async () => 'hello from script-file'
|
||||||
1
.github/fixtures/script-file/json-return.js
vendored
Normal file
1
.github/fixtures/script-file/json-return.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = async ({context}) => ({repo: context.repo.repo, owner: context.repo.owner})
|
||||||
1
.github/fixtures/script-file/not-a-function.js
vendored
Normal file
1
.github/fixtures/script-file/not-a-function.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = 42
|
||||||
1
.github/fixtures/script-file/sibling-caller.js
vendored
Normal file
1
.github/fixtures/script-file/sibling-caller.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = async ({require}) => require('./sibling-module').value
|
||||||
1
.github/fixtures/script-file/sibling-module.js
vendored
Normal file
1
.github/fixtures/script-file/sibling-module.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = {value: 'loaded-by-require'}
|
||||||
171
.github/workflows/integration.yml
vendored
171
.github/workflows/integration.yml
vendored
|
|
@ -361,3 +361,174 @@ jobs:
|
||||||
echo $'::error::\u274C' "Expected base-url to equal '$expected', got $actual"
|
echo $'::error::\u274C' "Expected base-url to equal '$expected', got $actual"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
||||||
|
|
|
||||||
102
README.md
102
README.md
|
|
@ -27,8 +27,9 @@ You are welcome to still raise bugs in this repo.
|
||||||
|
|
||||||
### This action
|
### This action
|
||||||
|
|
||||||
To use this action, provide an input named `script` that contains the body of an asynchronous JavaScript function call.
|
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 will be provided:
|
|
||||||
|
The following arguments are available to both forms:
|
||||||
|
|
||||||
- `github` A pre-authenticated
|
- `github` A pre-authenticated
|
||||||
[octokit/rest.js](https://octokit.github.io/rest.js) client with pagination plugins
|
[octokit/rest.js](https://octokit.github.io/rest.js) client with pagination plugins
|
||||||
|
|
@ -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))
|
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
|
## Examples
|
||||||
|
|
||||||
Note that `github-token` is optional in this action, and the input is there
|
Note that `github-token` is optional in this action, and the input is there
|
||||||
|
|
@ -377,9 +428,8 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/github-script@v9
|
- uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script-file: ./path/to/script.js
|
||||||
const script = require('./path/to/script.js')
|
|
||||||
console.log(script({github, context}))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
And then export a function from your module:
|
And then export a function from your module:
|
||||||
|
|
@ -390,39 +440,7 @@ module.exports = ({github, context}) => {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that because you can't `require` things like the GitHub context or
|
The exported function may be async if you like:
|
||||||
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:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
module.exports = async ({github, context, core}) => {
|
module.exports = async ({github, context, core}) => {
|
||||||
|
|
@ -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
|
### Use npm packages
|
||||||
|
|
||||||
Like importing your own files above, you can also use installed modules.
|
Like importing your own files above, you can also use installed modules.
|
||||||
|
|
|
||||||
133
__test__/script-file.test.ts
Normal file
133
__test__/script-file.test.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
import * as path from 'node:path'
|
||||||
|
import * as os from 'node:os'
|
||||||
|
import * as fs from 'node:fs'
|
||||||
|
import {callScriptFile, resolveScriptFilePath} from '../src/script-file'
|
||||||
|
|
||||||
|
const fullArgs = {
|
||||||
|
github: {},
|
||||||
|
octokit: {},
|
||||||
|
getOctokit: () => null,
|
||||||
|
context: {},
|
||||||
|
core: {},
|
||||||
|
exec: {},
|
||||||
|
glob: {},
|
||||||
|
io: {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
require: require,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
__original_require__: require
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('resolveScriptFilePath', () => {
|
||||||
|
test('rejects file:// protocol', () => {
|
||||||
|
expect(() => resolveScriptFilePath('file:///some/path.js')).toThrow(
|
||||||
|
'"script-file" must not use the "file://" protocol'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns absolute path as-is', () => {
|
||||||
|
const abs = '/absolute/path/to/script.js'
|
||||||
|
expect(resolveScriptFilePath(abs)).toEqual(abs)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolves relative path against GITHUB_WORKSPACE when set', () => {
|
||||||
|
const original = process.env['GITHUB_WORKSPACE']
|
||||||
|
process.env['GITHUB_WORKSPACE'] = '/workspace'
|
||||||
|
try {
|
||||||
|
expect(resolveScriptFilePath('scripts/run.js')).toEqual(
|
||||||
|
'/workspace/scripts/run.js'
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env['GITHUB_WORKSPACE']
|
||||||
|
} else {
|
||||||
|
process.env['GITHUB_WORKSPACE'] = original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('callScriptFile', () => {
|
||||||
|
let tmpDir: string
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'github-script-test-'))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, {recursive: true, force: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls the exported function with args', async () => {
|
||||||
|
const scriptPath = path.join(tmpDir, 'script.js')
|
||||||
|
fs.writeFileSync(
|
||||||
|
scriptPath,
|
||||||
|
'module.exports = async ({core}) => core.value'
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await callScriptFile(
|
||||||
|
{...fullArgs, core: {value: 42}} as never,
|
||||||
|
scriptPath,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
require
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(42)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('forwards all injected args to the exported function', async () => {
|
||||||
|
const scriptPath = path.join(tmpDir, 'all-args.js')
|
||||||
|
fs.writeFileSync(
|
||||||
|
scriptPath,
|
||||||
|
`module.exports = async (args) => Object.keys(args).sort()`
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await callScriptFile(
|
||||||
|
fullArgs as never,
|
||||||
|
scriptPath,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
require
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(Object.keys(fullArgs).sort())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws when file does not export a function', async () => {
|
||||||
|
const scriptPath = path.join(tmpDir, 'not-a-fn.js')
|
||||||
|
fs.writeFileSync(scriptPath, 'module.exports = 42')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
callScriptFile(fullArgs as never, scriptPath, require)
|
||||||
|
).rejects.toThrow('"script-file" must export a function, got number')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws when file does not exist', async () => {
|
||||||
|
const scriptPath = path.join(tmpDir, 'nonexistent.js')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
callScriptFile(fullArgs as never, scriptPath, require)
|
||||||
|
).rejects.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('propagates rejection from the exported function', async () => {
|
||||||
|
const scriptPath = path.join(tmpDir, 'throws.js')
|
||||||
|
fs.writeFileSync(
|
||||||
|
scriptPath,
|
||||||
|
"module.exports = async () => { throw new Error('boom') }"
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
callScriptFile(fullArgs as never, scriptPath, require)
|
||||||
|
).rejects.toThrow('boom')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects file:// path before loading', async () => {
|
||||||
|
await expect(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
callScriptFile(fullArgs as never, 'file:///some/path.js', require)
|
||||||
|
).rejects.toThrow('"script-file" must not use the "file://" protocol')
|
||||||
|
})
|
||||||
|
})
|
||||||
11
action.yml
11
action.yml
|
|
@ -6,8 +6,15 @@ branding:
|
||||||
icon: code
|
icon: code
|
||||||
inputs:
|
inputs:
|
||||||
script:
|
script:
|
||||||
description: The script to run
|
description: The script to run (mutually exclusive with script-file)
|
||||||
required: true
|
required: false
|
||||||
|
script-file:
|
||||||
|
description: |
|
||||||
|
A path to a JS file that exports an action handler. (mutually exclusive with script)
|
||||||
|
Path may be absolute, or relative to your repo root.
|
||||||
|
The handler may be async.
|
||||||
|
The handler is called with an IoC bag with {context, core, exec, github, octokit, getOctokit, glob, io, require} - destructure what you need.
|
||||||
|
required: false
|
||||||
github-token:
|
github-token:
|
||||||
description: The GitHub token used to create an authenticated client
|
description: The GitHub token used to create an authenticated client
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
|
|
|
||||||
36
dist/index.js
vendored
36
dist/index.js
vendored
|
|
@ -65029,6 +65029,25 @@ function parseNumberArray(listString) {
|
||||||
|
|
||||||
// EXTERNAL MODULE: external "path"
|
// EXTERNAL MODULE: external "path"
|
||||||
var external_path_ = __nccwpck_require__(1017);
|
var external_path_ = __nccwpck_require__(1017);
|
||||||
|
;// CONCATENATED MODULE: ./src/script-file.ts
|
||||||
|
|
||||||
|
function resolveScriptFilePath(scriptFile) {
|
||||||
|
if (scriptFile.startsWith('file://')) {
|
||||||
|
throw new Error('"script-file" must not use the "file://" protocol');
|
||||||
|
}
|
||||||
|
return external_path_.isAbsolute(scriptFile)
|
||||||
|
? scriptFile
|
||||||
|
: external_path_.resolve(process.env['GITHUB_WORKSPACE'], scriptFile);
|
||||||
|
}
|
||||||
|
async function callScriptFile(args, scriptFile, requireFn) {
|
||||||
|
const resolvedPath = resolveScriptFilePath(scriptFile);
|
||||||
|
const scriptFn = requireFn(resolvedPath);
|
||||||
|
if (typeof scriptFn !== 'function') {
|
||||||
|
throw new Error(`"script-file" must export a function, got ${typeof scriptFn}`);
|
||||||
|
}
|
||||||
|
return scriptFn(args);
|
||||||
|
}
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./src/wrap-require.ts
|
;// CONCATENATED MODULE: ./src/wrap-require.ts
|
||||||
|
|
||||||
const wrapRequire = new Proxy(require, {
|
const wrapRequire = new Proxy(require, {
|
||||||
|
|
@ -65065,6 +65084,7 @@ const wrapRequire = new Proxy(require, {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
process.on('unhandledRejection', handleError);
|
process.on('unhandledRejection', handleError);
|
||||||
main().catch(handleError);
|
main().catch(handleError);
|
||||||
async function main() {
|
async function main() {
|
||||||
|
|
@ -65091,13 +65111,20 @@ async function main() {
|
||||||
opts.baseUrl = baseUrl;
|
opts.baseUrl = baseUrl;
|
||||||
}
|
}
|
||||||
const github = getOctokit(token, opts, retry, requestLog);
|
const github = getOctokit(token, opts, retry, requestLog);
|
||||||
const script = core.getInput('script', { required: true });
|
const scriptInline = core.getInput('script');
|
||||||
|
const scriptFile = core.getInput('script-file');
|
||||||
|
if (scriptInline && scriptFile) {
|
||||||
|
throw new Error('Only one of "script" or "script-file" may be provided, not both');
|
||||||
|
}
|
||||||
|
if (!scriptInline && !scriptFile) {
|
||||||
|
throw new Error('One of "script" or "script-file" must be provided');
|
||||||
|
}
|
||||||
// Wrap getOctokit so secondary clients inherit retry, logging,
|
// Wrap getOctokit so secondary clients inherit retry, logging,
|
||||||
// orchestration ID, and the action's retries input.
|
// orchestration ID, and the action's retries input.
|
||||||
// Deep-copy opts to prevent shared references with the primary client.
|
// Deep-copy opts to prevent shared references with the primary client.
|
||||||
const configuredGetOctokit = createConfiguredGetOctokit(getOctokit, { ...opts, retry: { ...opts.retry }, request: { ...opts.request } }, retry, requestLog);
|
const configuredGetOctokit = createConfiguredGetOctokit(getOctokit, { ...opts, retry: { ...opts.retry }, request: { ...opts.request } }, retry, requestLog);
|
||||||
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors.
|
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors.
|
||||||
const result = await callAsyncFunction({
|
const args = {
|
||||||
require: wrapRequire,
|
require: wrapRequire,
|
||||||
__original_require__: require,
|
__original_require__: require,
|
||||||
github,
|
github,
|
||||||
|
|
@ -65108,7 +65135,10 @@ async function main() {
|
||||||
exec: exec,
|
exec: exec,
|
||||||
glob: glob,
|
glob: glob,
|
||||||
io: io
|
io: io
|
||||||
}, script);
|
};
|
||||||
|
const result = scriptFile
|
||||||
|
? await callScriptFile(args, scriptFile, require)
|
||||||
|
: await callAsyncFunction(args, scriptInline);
|
||||||
let encoding = core.getInput('result-encoding');
|
let encoding = core.getInput('result-encoding');
|
||||||
encoding = encoding ? encoding : 'json';
|
encoding = encoding ? encoding : 'json';
|
||||||
let output;
|
let output;
|
||||||
|
|
|
||||||
18
src/args.ts
Normal file
18
src/args.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as exec from '@actions/exec'
|
||||||
|
import type {context, getOctokit} from '@actions/github'
|
||||||
|
import * as glob from '@actions/glob'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
|
||||||
|
export type AsyncFunctionArguments = {
|
||||||
|
context: typeof context
|
||||||
|
core: typeof core
|
||||||
|
github: ReturnType<typeof getOctokit>
|
||||||
|
octokit: ReturnType<typeof getOctokit>
|
||||||
|
getOctokit: typeof getOctokit
|
||||||
|
exec: typeof exec
|
||||||
|
glob: typeof glob
|
||||||
|
io: typeof io
|
||||||
|
require: NodeJS.Require
|
||||||
|
__original_require__: NodeJS.Require
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,8 @@
|
||||||
import * as core from '@actions/core'
|
import type {AsyncFunctionArguments} from './args'
|
||||||
import * as exec from '@actions/exec'
|
export type {AsyncFunctionArguments}
|
||||||
import type {context, getOctokit} from '@actions/github'
|
|
||||||
import * as glob from '@actions/glob'
|
|
||||||
import * as io from '@actions/io'
|
|
||||||
|
|
||||||
const AsyncFunction = Object.getPrototypeOf(async () => null).constructor
|
const AsyncFunction = Object.getPrototypeOf(async () => null).constructor
|
||||||
|
|
||||||
export declare type AsyncFunctionArguments = {
|
|
||||||
context: typeof context
|
|
||||||
core: typeof core
|
|
||||||
github: ReturnType<typeof getOctokit>
|
|
||||||
octokit: ReturnType<typeof getOctokit>
|
|
||||||
getOctokit: typeof getOctokit
|
|
||||||
exec: typeof exec
|
|
||||||
glob: typeof glob
|
|
||||||
io: typeof io
|
|
||||||
require: NodeRequire
|
|
||||||
__original_require__: NodeRequire
|
|
||||||
}
|
|
||||||
|
|
||||||
export function callAsyncFunction<T>(
|
export function callAsyncFunction<T>(
|
||||||
args: AsyncFunctionArguments,
|
args: AsyncFunctionArguments,
|
||||||
source: string
|
source: string
|
||||||
|
|
|
||||||
24
src/main.ts
24
src/main.ts
|
|
@ -10,6 +10,7 @@ import {RequestRequestOptions} from '@octokit/types'
|
||||||
import {callAsyncFunction} from './async-function'
|
import {callAsyncFunction} from './async-function'
|
||||||
import {createConfiguredGetOctokit} from './create-configured-getoctokit'
|
import {createConfiguredGetOctokit} from './create-configured-getoctokit'
|
||||||
import {RetryOptions, getRetryOptions, parseNumberArray} from './retry-options'
|
import {RetryOptions, getRetryOptions, parseNumberArray} from './retry-options'
|
||||||
|
import {callScriptFile} from './script-file'
|
||||||
import {wrapRequire} from './wrap-require'
|
import {wrapRequire} from './wrap-require'
|
||||||
|
|
||||||
process.on('unhandledRejection', handleError)
|
process.on('unhandledRejection', handleError)
|
||||||
|
|
@ -58,7 +59,17 @@ async function main(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const github = getOctokit(token, opts, retry, requestLog)
|
const github = getOctokit(token, opts, retry, requestLog)
|
||||||
const script = core.getInput('script', {required: true})
|
const scriptInline = core.getInput('script')
|
||||||
|
const scriptFile = core.getInput('script-file')
|
||||||
|
|
||||||
|
if (scriptInline && scriptFile) {
|
||||||
|
throw new Error(
|
||||||
|
'Only one of "script" or "script-file" may be provided, not both'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!scriptInline && !scriptFile) {
|
||||||
|
throw new Error('One of "script" or "script-file" must be provided')
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap getOctokit so secondary clients inherit retry, logging,
|
// Wrap getOctokit so secondary clients inherit retry, logging,
|
||||||
// orchestration ID, and the action's retries input.
|
// orchestration ID, and the action's retries input.
|
||||||
|
|
@ -71,8 +82,7 @@ async function main(): Promise<void> {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors.
|
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors.
|
||||||
const result = await callAsyncFunction(
|
const args = {
|
||||||
{
|
|
||||||
require: wrapRequire,
|
require: wrapRequire,
|
||||||
__original_require__: __non_webpack_require__,
|
__original_require__: __non_webpack_require__,
|
||||||
github,
|
github,
|
||||||
|
|
@ -83,9 +93,11 @@ async function main(): Promise<void> {
|
||||||
exec,
|
exec,
|
||||||
glob,
|
glob,
|
||||||
io
|
io
|
||||||
},
|
}
|
||||||
script
|
|
||||||
)
|
const result = scriptFile
|
||||||
|
? await callScriptFile(args, scriptFile, __non_webpack_require__)
|
||||||
|
: await callAsyncFunction(args, scriptInline)
|
||||||
|
|
||||||
let encoding = core.getInput('result-encoding')
|
let encoding = core.getInput('result-encoding')
|
||||||
encoding = encoding ? encoding : 'json'
|
encoding = encoding ? encoding : 'json'
|
||||||
|
|
|
||||||
29
src/script-file.ts
Normal file
29
src/script-file.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as path from 'path'
|
||||||
|
import type {AsyncFunctionArguments} from './args'
|
||||||
|
|
||||||
|
export function resolveScriptFilePath(scriptFile: string): string {
|
||||||
|
if (scriptFile.startsWith('file://')) {
|
||||||
|
throw new Error('"script-file" must not use the "file://" protocol')
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.isAbsolute(scriptFile)
|
||||||
|
? scriptFile
|
||||||
|
: path.resolve(process.env['GITHUB_WORKSPACE']!, scriptFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function callScriptFile(
|
||||||
|
args: AsyncFunctionArguments,
|
||||||
|
scriptFile: string,
|
||||||
|
requireFn: NodeJS.Require
|
||||||
|
): Promise<unknown> {
|
||||||
|
const resolvedPath = resolveScriptFilePath(scriptFile)
|
||||||
|
const scriptFn = requireFn(resolvedPath)
|
||||||
|
|
||||||
|
if (typeof scriptFn !== 'function') {
|
||||||
|
throw new Error(
|
||||||
|
`"script-file" must export a function, got ${typeof scriptFn}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return scriptFn(args)
|
||||||
|
}
|
||||||
18
types/args.d.ts
vendored
Normal file
18
types/args.d.ts
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/// <reference types="node" />
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as exec from '@actions/exec';
|
||||||
|
import type { context, getOctokit } from '@actions/github';
|
||||||
|
import * as glob from '@actions/glob';
|
||||||
|
import * as io from '@actions/io';
|
||||||
|
export type AsyncFunctionArguments = {
|
||||||
|
context: typeof context;
|
||||||
|
core: typeof core;
|
||||||
|
github: ReturnType<typeof getOctokit>;
|
||||||
|
octokit: ReturnType<typeof getOctokit>;
|
||||||
|
getOctokit: typeof getOctokit;
|
||||||
|
exec: typeof exec;
|
||||||
|
glob: typeof glob;
|
||||||
|
io: typeof io;
|
||||||
|
require: NodeJS.Require;
|
||||||
|
__original_require__: NodeJS.Require;
|
||||||
|
};
|
||||||
20
types/async-function.d.ts
vendored
20
types/async-function.d.ts
vendored
|
|
@ -1,19 +1,3 @@
|
||||||
/// <reference types="node" />
|
import type { AsyncFunctionArguments } from './args';
|
||||||
import * as core from '@actions/core';
|
export type { AsyncFunctionArguments };
|
||||||
import * as exec from '@actions/exec';
|
|
||||||
import type { context, getOctokit } from '@actions/github';
|
|
||||||
import * as glob from '@actions/glob';
|
|
||||||
import * as io from '@actions/io';
|
|
||||||
export declare type AsyncFunctionArguments = {
|
|
||||||
context: typeof context;
|
|
||||||
core: typeof core;
|
|
||||||
github: ReturnType<typeof getOctokit>;
|
|
||||||
octokit: ReturnType<typeof getOctokit>;
|
|
||||||
getOctokit: typeof getOctokit;
|
|
||||||
exec: typeof exec;
|
|
||||||
glob: typeof glob;
|
|
||||||
io: typeof io;
|
|
||||||
require: NodeRequire;
|
|
||||||
__original_require__: NodeRequire;
|
|
||||||
};
|
|
||||||
export declare function callAsyncFunction<T>(args: AsyncFunctionArguments, source: string): Promise<T>;
|
export declare function callAsyncFunction<T>(args: AsyncFunctionArguments, source: string): Promise<T>;
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
declare const __non_webpack_require__: NodeRequire
|
declare const __non_webpack_require__: NodeJS.Require
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue