mirror of
https://github.com/actions/github-script.git
synced 2026-06-06 17:14:23 +00:00
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)
534 lines
19 KiB
YAML
534 lines
19 KiB
YAML
name: Integration
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
test-return:
|
|
name: 'Integration test: return'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- id: output-set
|
|
uses: ./
|
|
with:
|
|
script: return core.getInput('input-value')
|
|
result-encoding: string
|
|
input-value: output
|
|
- run: |
|
|
echo "- Validating output is produced"
|
|
expected="output"
|
|
if [[ "${{steps.output-set.outputs.result}}" != "$expected" ]]; then
|
|
echo $'::error::\u274C' "Expected '$expected', got ${{steps.output-set.outputs.result}}"
|
|
exit 1
|
|
fi
|
|
echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
test-relative-require:
|
|
name: 'Integration test: relative-path require'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- id: relative-require
|
|
uses: ./
|
|
with:
|
|
script: return require('./package.json').name
|
|
result-encoding: string
|
|
- run: |
|
|
echo "- Validating relative require output"
|
|
expected="@actions/github-script"
|
|
if [[ "${{steps.relative-require.outputs.result}}" != "$expected" ]]; then
|
|
echo $'::error::\u274C' "Expected '$expected', got ${{steps.relative-require.outputs.result}}"
|
|
exit 1
|
|
fi
|
|
echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
test-npm-require:
|
|
name: 'Integration test: npm package require'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: ./.github/actions/install-dependencies
|
|
- id: npm-require
|
|
uses: ./
|
|
with:
|
|
script: return require('@actions/core/package.json').name
|
|
result-encoding: string
|
|
- run: |
|
|
echo "- Validating npm require output"
|
|
expected="@actions/core"
|
|
if [[ "${{steps.npm-require.outputs.result}}" != "$expected" ]]; then
|
|
echo $'::error::\u274C' "Expected '$expected', got ${{steps.npm-require.outputs.result}}"
|
|
exit 1
|
|
fi
|
|
echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
test-previews:
|
|
name: 'Integration test: GraphQL previews option'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: ./.github/actions/install-dependencies
|
|
- id: previews-default
|
|
name: Default previews not set
|
|
uses: ./
|
|
with:
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({url: "/graphql"}).headers.accept
|
|
result-encoding: string
|
|
- id: previews-set-single
|
|
name: Previews set to a single value
|
|
uses: ./
|
|
with:
|
|
previews: foo
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({url: "/graphql"}).headers.accept
|
|
result-encoding: string
|
|
- id: previews-set-multiple
|
|
name: Previews set to comma-separated list
|
|
uses: ./
|
|
with:
|
|
previews: foo,bar,baz
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({url: "/graphql"}).headers.accept
|
|
result-encoding: string
|
|
- run: |
|
|
echo "- Validating previews default"
|
|
expected="application/vnd.github.v3+json"
|
|
if [[ "${{steps.previews-default.outputs.result}}" != $expected ]]; then
|
|
echo $'::error::\u274C' "Expected '$expected', got ${{steps.previews-default.outputs.result}}"
|
|
exit 1
|
|
fi
|
|
echo "- Validating previews set to a single value"
|
|
expected="application/vnd.github.foo-preview+json"
|
|
if [[ "${{steps.previews-set-single.outputs.result}}" != $expected ]]; then
|
|
echo $'::error::\u274C' "Expected '$expected', got ${{steps.previews-set-single.outputs.result}}"
|
|
exit 1
|
|
fi
|
|
echo "- Validating previews set to multiple values"
|
|
expected="application/vnd.github.foo-preview+json,application/vnd.github.bar-preview+json,application/vnd.github.baz-preview+json"
|
|
if [[ "${{steps.previews-set-multiple.outputs.result}}" != $expected ]]; then
|
|
echo $'::error::\u274C' "Expected '$expected', got ${{steps.previews-set-multiple.outputs.result}}"
|
|
exit 1
|
|
fi
|
|
echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
test-user-agent:
|
|
name: 'Integration test: user-agent option'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: ./.github/actions/install-dependencies
|
|
- id: user-agent-default
|
|
name: Default user-agent not set
|
|
uses: ./
|
|
with:
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({}).headers['user-agent']
|
|
result-encoding: string
|
|
- id: user-agent-set
|
|
name: User-agent set
|
|
uses: ./
|
|
with:
|
|
user-agent: foobar
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({}).headers['user-agent']
|
|
result-encoding: string
|
|
- id: user-agent-empty
|
|
name: User-agent set to an empty string
|
|
uses: ./
|
|
with:
|
|
user-agent: ''
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({}).headers['user-agent']
|
|
result-encoding: string
|
|
- run: |
|
|
# User-agent format: <prefix> [actions_orchestration_id/<id>] octokit-core.js/<version> ...
|
|
# When ACTIONS_ORCHESTRATION_ID is set, the orchestration ID is inserted after the prefix
|
|
echo "- Validating user-agent default"
|
|
ua="${{steps.user-agent-default.outputs.result}}"
|
|
if [[ "$ua" != "actions/github-script"* ]] || [[ "$ua" != *"octokit-core.js/"* ]]; then
|
|
echo $'::error::\u274C' "Expected user-agent to start with 'actions/github-script' and contain 'octokit-core.js/', got $ua"
|
|
exit 1
|
|
fi
|
|
echo "- Validating user-agent set to a value"
|
|
ua="${{steps.user-agent-set.outputs.result}}"
|
|
if [[ "$ua" != "foobar"* ]] || [[ "$ua" != *"octokit-core.js/"* ]]; then
|
|
echo $'::error::\u274C' "Expected user-agent to start with 'foobar' and contain 'octokit-core.js/', got $ua"
|
|
exit 1
|
|
fi
|
|
echo "- Validating user-agent set to an empty string"
|
|
ua="${{steps.user-agent-empty.outputs.result}}"
|
|
if [[ "$ua" != "actions/github-script"* ]] || [[ "$ua" != *"octokit-core.js/"* ]]; then
|
|
echo $'::error::\u274C' "Expected user-agent to start with 'actions/github-script' and contain 'octokit-core.js/', got $ua"
|
|
exit 1
|
|
fi
|
|
echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
test-get-octokit:
|
|
name: 'Integration test: getOctokit with token'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: ./.github/actions/install-dependencies
|
|
- id: secondary-client
|
|
name: Create a second client with getOctokit
|
|
uses: ./
|
|
env:
|
|
APP_TOKEN: ${{ github.token }}
|
|
with:
|
|
script: |
|
|
const appOctokit = getOctokit(process.env.APP_TOKEN)
|
|
const {data} = await appOctokit.rest.repos.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo
|
|
})
|
|
|
|
return `${appOctokit !== github}:${data.full_name}`
|
|
result-encoding: string
|
|
- run: |
|
|
echo "- Validating secondary client output"
|
|
expected="true:${{ github.repository }}"
|
|
if [[ "${{steps.secondary-client.outputs.result}}" != "$expected" ]]; then
|
|
echo $'::error::\u274C' "Expected '$expected', got ${{steps.secondary-client.outputs.result}}"
|
|
exit 1
|
|
fi
|
|
echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
test-debug:
|
|
strategy:
|
|
matrix:
|
|
environment: ['', 'debug-integration-test']
|
|
environment:
|
|
name: ${{ matrix.environment }}
|
|
deployment: false
|
|
name: "Integration test: debug option (runner.debug mode ${{ matrix.environment && 'enabled' || 'disabled' }})"
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: ./.github/actions/install-dependencies
|
|
- id: debug-default
|
|
name: Default debug not set
|
|
uses: ./
|
|
with:
|
|
script: |
|
|
const log = github.log
|
|
return {
|
|
runnerDebugMode: core.isDebug(),
|
|
debug: log.debug === console.debug,
|
|
info: log.info === console.info
|
|
}
|
|
- id: debug-true
|
|
name: Debug set to true
|
|
uses: ./
|
|
with:
|
|
debug: true
|
|
script: |
|
|
const log = github.log
|
|
return {
|
|
runnerDebugMode: core.isDebug(),
|
|
debug: log.debug === console.debug,
|
|
info: log.info === console.info
|
|
}
|
|
- id: debug-false
|
|
name: Debug set to false
|
|
uses: ./
|
|
with:
|
|
debug: false
|
|
script: |
|
|
const log = github.log
|
|
return {
|
|
runnerDebugMode: core.isDebug(),
|
|
debug: log.debug === console.debug,
|
|
info: log.info === console.info
|
|
}
|
|
- id: evaluate-tests
|
|
name: Evaluate test outputs
|
|
env:
|
|
RDMODE: ${{ runner.debug == '1' }}
|
|
run: |
|
|
# tests table, pipe separated: label | output | expected
|
|
# leading and trailing spaces on any field are trimmed
|
|
tests=$(cat << 'TESTS'
|
|
Validating debug default |\
|
|
${{ steps.debug-default.outputs.result }} |\
|
|
{"runnerDebugMode":${{ env.RDMODE }},"debug":${{ env.RDMODE }},"info":${{ env.RDMODE }}}
|
|
Validating debug set to true |\
|
|
${{ steps.debug-true.outputs.result }} |\
|
|
{"runnerDebugMode":${{ env.RDMODE }},"debug":true,"info":true}
|
|
Validating debug set to false |\
|
|
${{ steps.debug-false.outputs.result }} |\
|
|
{"runnerDebugMode":${{ env.RDMODE }},"debug":false,"info":false}
|
|
TESTS
|
|
)
|
|
|
|
strim() { shopt -s extglob; lt="${1##+( )}"; echo "${lt%%+( )}"; }
|
|
while IFS='|' read label output expected; do
|
|
label="$(strim "$label")"; output="$(strim "$output")"; expected="$(strim "$expected")"
|
|
echo -n "- $label:"
|
|
if [[ "$output" != "$expected" ]]; then
|
|
echo $'\n::error::\u274C' "Expected '$expected', got '$output'"
|
|
exit 1
|
|
fi
|
|
echo $' \u2705'
|
|
done <<< "$tests"
|
|
|
|
echo $'\u2705 Test passed' | tee -a $GITHUB_STEP_SUMMARY
|
|
|
|
test-base-url:
|
|
name: 'Integration test: base-url option'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: ./.github/actions/install-dependencies
|
|
|
|
- id: base-url-default
|
|
name: API URL with base-url not set
|
|
uses: ./
|
|
with:
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({}).url
|
|
result-encoding: string
|
|
|
|
- id: base-url-default-graphql
|
|
name: GraphQL URL with base-url not set
|
|
uses: ./
|
|
with:
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({url: "/graphql"}).url
|
|
result-encoding: string
|
|
|
|
- id: base-url-set
|
|
name: API URL with base-url set
|
|
uses: ./
|
|
with:
|
|
base-url: https://my.github-enterprise-server.com/api/v3
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({}).url
|
|
result-encoding: string
|
|
|
|
- id: base-url-set-graphql
|
|
name: GraphQL URL with base-url set
|
|
uses: ./
|
|
with:
|
|
base-url: https://my.github-enterprise-server.com/api/v3
|
|
script: |
|
|
const endpoint = github.request.endpoint
|
|
return endpoint({url: "/graphql"}).url
|
|
result-encoding: string
|
|
|
|
- run: |
|
|
echo "- Validating API URL default"
|
|
expected="https://api.github.com/"
|
|
actual="${{steps.base-url-default.outputs.result}}"
|
|
if [[ "$expected" != "$actual" ]]; then
|
|
echo $'::error::\u274C' "Expected base-url to equal '$expected', got $actual"
|
|
exit 1
|
|
fi
|
|
echo "- Validating GraphQL URL default"
|
|
expected="https://api.github.com/graphql"
|
|
actual="${{steps.base-url-default-graphql.outputs.result}}"
|
|
if [[ "$expected" != "$actual" ]]; then
|
|
echo $'::error::\u274C' "Expected base-url to equal '$expected', got $actual"
|
|
exit 1
|
|
fi
|
|
echo "- Validating base-url set to a value"
|
|
expected="https://my.github-enterprise-server.com/api/v3/"
|
|
actual="${{steps.base-url-set.outputs.result}}"
|
|
if [[ "$expected" != "$actual" ]]; then
|
|
echo $'::error::\u274C' "Expected base-url to equal '$expected', got $actual"
|
|
exit 1
|
|
fi
|
|
echo "- Validating GraphQL URL with base-url set to a value"
|
|
expected="https://my.github-enterprise-server.com/api/v3/graphql"
|
|
actual="${{steps.base-url-set-graphql.outputs.result}}"
|
|
if [[ "$expected" != "$actual" ]]; then
|
|
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
|