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:
Osher 2026-04-25 19:59:10 +03:00
parent 3a2844b7e9
commit 67c280d263
18 changed files with 519 additions and 97 deletions

View 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
View file

@ -0,0 +1 @@
module.exports = async () => 'hello from script-file'

View file

@ -0,0 +1 @@
module.exports = async ({context}) => ({repo: context.repo.repo, owner: context.repo.owner})

View file

@ -0,0 +1 @@
module.exports = 42

View file

@ -0,0 +1 @@
module.exports = async ({require}) => require('./sibling-module').value

View file

@ -0,0 +1 @@
module.exports = {value: 'loaded-by-require'}

View file

@ -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