mirror of
https://github.com/actions/github-script.git
synced 2026-06-07 09:34: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
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 * as exec from '@actions/exec'
|
||||
import type {context, getOctokit} from '@actions/github'
|
||||
import * as glob from '@actions/glob'
|
||||
import * as io from '@actions/io'
|
||||
import type {AsyncFunctionArguments} from './args'
|
||||
export type {AsyncFunctionArguments}
|
||||
|
||||
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>(
|
||||
args: AsyncFunctionArguments,
|
||||
source: string
|
||||
|
|
|
|||
44
src/main.ts
44
src/main.ts
|
|
@ -10,6 +10,7 @@ import {RequestRequestOptions} from '@octokit/types'
|
|||
import {callAsyncFunction} from './async-function'
|
||||
import {createConfiguredGetOctokit} from './create-configured-getoctokit'
|
||||
import {RetryOptions, getRetryOptions, parseNumberArray} from './retry-options'
|
||||
import {callScriptFile} from './script-file'
|
||||
import {wrapRequire} from './wrap-require'
|
||||
|
||||
process.on('unhandledRejection', handleError)
|
||||
|
|
@ -58,7 +59,17 @@ async function main(): Promise<void> {
|
|||
}
|
||||
|
||||
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,
|
||||
// orchestration ID, and the action's retries input.
|
||||
|
|
@ -71,21 +82,22 @@ async function main(): Promise<void> {
|
|||
)
|
||||
|
||||
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors.
|
||||
const result = await callAsyncFunction(
|
||||
{
|
||||
require: wrapRequire,
|
||||
__original_require__: __non_webpack_require__,
|
||||
github,
|
||||
octokit: github,
|
||||
getOctokit: configuredGetOctokit,
|
||||
context,
|
||||
core,
|
||||
exec,
|
||||
glob,
|
||||
io
|
||||
},
|
||||
script
|
||||
)
|
||||
const args = {
|
||||
require: wrapRequire,
|
||||
__original_require__: __non_webpack_require__,
|
||||
github,
|
||||
octokit: github,
|
||||
getOctokit: configuredGetOctokit,
|
||||
context,
|
||||
core,
|
||||
exec,
|
||||
glob,
|
||||
io
|
||||
}
|
||||
|
||||
const result = scriptFile
|
||||
? await callScriptFile(args, scriptFile, __non_webpack_require__)
|
||||
: await callAsyncFunction(args, scriptInline)
|
||||
|
||||
let encoding = core.getInput('result-encoding')
|
||||
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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue