This commit is contained in:
Ricky Curtice 2025-06-08 17:26:18 +09:00 committed by GitHub
commit b9725d2b7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 178333 additions and 22 deletions

View file

@ -2,13 +2,21 @@
import {callAsyncFunction} from '../src/async-function' import {callAsyncFunction} from '../src/async-function'
describe('callAsyncFunction', () => { describe(callAsyncFunction.name, () => {
test('calls the function with its arguments', async () => { test('calls the function with its arguments', async () => {
const result = await callAsyncFunction({foo: 'bar'} as any, 'return foo') const result = await callAsyncFunction({foo: 'bar'} as any, 'return foo')
expect(result).toEqual('bar') expect(result).toEqual('bar')
}) })
test('throws on ReferenceError', async () => { test('can await a Promise', async () => {
const result = await callAsyncFunction(
{} as any,
'return await new Promise(resolve => resolve("bar"))'
)
expect(result).toEqual('bar')
})
test(`throws an ${ReferenceError.name}`, async () => {
expect.assertions(1) expect.assertions(1)
try { try {

View file

@ -2,7 +2,7 @@
import {getRetryOptions} from '../src/retry-options' import {getRetryOptions} from '../src/retry-options'
describe('getRequestOptions', () => { describe(getRetryOptions.name, () => {
test('retries disabled if retries == 0', async () => { test('retries disabled if retries == 0', async () => {
const [retryOptions, requestOptions] = getRetryOptions( const [retryOptions, requestOptions] = getRetryOptions(
0, 0,

View file

@ -0,0 +1,194 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {SupportedLanguage, interpretScript} from '../src/interpret-script'
const scripts: Record<SupportedLanguage, string> = {
[SupportedLanguage.cjs]: `
const FS = require('node:fs') // Proof that we are in CommonJS.
var a // Proof that we are NOT in TypeScript.
return foo // Proof that we executed correctly. Also, this is the pre-existing function-style syntax.
`,
[SupportedLanguage.cts]: `
const FS = require('node:fs') // Proof that we are in CommonJS.
let a: string // Proof that we are in TypeScript.
return foo // Proof that we executed correctly.
`,
[SupportedLanguage.mts]: `
import FS from 'node:fs' // Proof that we are in an ES Module.
let a: string // Proof that we are in TypeScript.
return foo // Proof that we executed correctly.
`
}
describe(interpretScript.name, () => {
describe(`language set to ${SupportedLanguage.cjs}`, () => {
test(`throws when given a ${SupportedLanguage.cts} script`, async () => {
return expect(
interpretScript(
SupportedLanguage.cjs,
{foo: 'bar', require} as any,
scripts.cts
)
).rejects
})
test(`throws when given an ${SupportedLanguage.mts} script`, async () => {
return expect(
interpretScript(
SupportedLanguage.cjs,
{foo: 'bar', require} as any,
scripts.mts
)
).rejects
})
test(`interprets a ${SupportedLanguage.cjs} script`, async () => {
return expect(
interpretScript(
SupportedLanguage.cjs,
{foo: 'bar', require} as any,
scripts.cjs
)
).resolves
})
test(`when given a ${SupportedLanguage.cjs} script returns a function that can run it correctly`, async () => {
const result = await interpretScript(
SupportedLanguage.cjs,
{foo: 'bar', require} as any,
scripts.cjs
)
return expect(result()).resolves.toEqual('bar')
})
})
describe(`language set to ${SupportedLanguage.cts}`, () => {
test(`throws when given a ${SupportedLanguage.cjs} script`, async () => {
return expect(
interpretScript(
SupportedLanguage.cts,
{foo: 'bar', require} as any,
scripts.cjs
)
).rejects
})
test(`throws when given an ${SupportedLanguage.mts} script`, async () => {
return expect(
interpretScript(
SupportedLanguage.cts,
{foo: 'bar', require} as any,
scripts.mts
)
).rejects
})
test(`interprets a ${SupportedLanguage.cts} script`, async () => {
return expect(
interpretScript(
SupportedLanguage.cts,
{foo: 'bar', require} as any,
scripts.cts
)
).resolves
})
test(`when given a ${SupportedLanguage.cts} script returns a function that can run it correctly`, async () => {
const result = await interpretScript(
SupportedLanguage.cts,
{foo: 'bar', require} as any,
scripts.cts
)
return expect(result()).resolves.toEqual('bar')
})
test(`a script imports a script from disk`, async () => {
const result = await interpretScript(
SupportedLanguage.cts,
{require} as any,
`
const {test} = require('../test/requireable')
return test()
`
)
return expect(result()).resolves.toEqual('hello')
})
})
describe(`language set to ${SupportedLanguage.mts}`, () => {
test(`throws when given a ${SupportedLanguage.cjs} script`, async () => {
return expect(
interpretScript(SupportedLanguage.mts, {foo: 'bar'} as any, scripts.cjs)
).rejects
})
test(`throws when given a ${SupportedLanguage.cts} script`, async () => {
return expect(
interpretScript(SupportedLanguage.mts, {foo: 'bar'} as any, scripts.cts)
).rejects
})
test(`interprets an ${SupportedLanguage.mts} script`, async () => {
return expect(
interpretScript(SupportedLanguage.mts, {foo: 'bar'} as any, scripts.mts)
).resolves
})
test(`when given an ${SupportedLanguage.mts} script returns a function that can run it correctly`, async () => {
const result = await interpretScript(
SupportedLanguage.mts,
{foo: 'bar'} as any,
scripts.mts
)
return expect(result()).resolves.toEqual('bar')
})
test(`can access console`, async () => {
const result = await interpretScript(
SupportedLanguage.mts,
{} as any,
`console`
)
return expect(result()).resolves
})
test(`can access process`, async () => {
const result = await interpretScript(
SupportedLanguage.mts,
{} as any,
`process`
)
return expect(result()).resolves
})
test(`a script that returns an object`, async () => {
const result = await interpretScript(
SupportedLanguage.mts,
{} as any,
`return {a: 'b'}`
)
return expect(result()).resolves.toEqual({a: 'b'})
})
test(`a script that uses a root level await`, async () => {
const result = await interpretScript(
SupportedLanguage.mts,
{} as any,
`await Promise.resolve()`
)
return expect(result()).resolves
})
test(`a script imports a script from disk`, async () => {
const result = await interpretScript(
SupportedLanguage.mts,
{require} as any,
`
const {test} = await import('../test/importable')
return test()
`
)
return expect(result()).resolves.toEqual('hello')
})
})
})

View file

@ -32,6 +32,9 @@ inputs:
base-url: base-url:
description: An optional GitHub REST API URL to connect to a different GitHub instance. For example, https://my.github-enterprise-server.com/api/v3 description: An optional GitHub REST API URL to connect to a different GitHub instance. For example, https://my.github-enterprise-server.com/api/v3
required: false required: false
language:
description: The language to interpret the script as. Pick from "cjs", "cts", "mts".
default: "cjs"
outputs: outputs:
result: result:
description: The return value of the script, stringified with `JSON.stringify` description: The return value of the script, stringified with `JSON.stringify`

178039
dist/index.js vendored

File diff suppressed because one or more lines are too long

10
package-lock.json generated
View file

@ -17,7 +17,8 @@
"@octokit/core": "^5.0.1", "@octokit/core": "^5.0.1",
"@octokit/plugin-request-log": "^4.0.0", "@octokit/plugin-request-log": "^4.0.0",
"@octokit/plugin-retry": "^6.0.1", "@octokit/plugin-retry": "^6.0.1",
"@types/node": "^20.9.0" "@types/node": "^20.9.0",
"typescript": "^5.2.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.5", "@types/jest": "^29.5.5",
@ -31,8 +32,7 @@
"jest": "^29.7.0", "jest": "^29.7.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1"
"typescript": "^5.2.2"
}, },
"engines": { "engines": {
"node": ">=20.0.0 <21.0.0" "node": ">=20.0.0 <21.0.0"
@ -7077,7 +7077,6 @@
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -12518,8 +12517,7 @@
"typescript": { "typescript": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w=="
"dev": true
}, },
"unbox-primitive": { "unbox-primitive": {
"version": "1.0.2", "version": "1.0.2",

View file

@ -47,7 +47,8 @@
"@octokit/core": "^5.0.1", "@octokit/core": "^5.0.1",
"@octokit/plugin-request-log": "^4.0.0", "@octokit/plugin-request-log": "^4.0.0",
"@octokit/plugin-retry": "^6.0.1", "@octokit/plugin-retry": "^6.0.1",
"@types/node": "^20.9.0" "@types/node": "^20.9.0",
"typescript": "^5.2.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.5", "@types/jest": "^29.5.5",
@ -61,7 +62,6 @@
"jest": "^29.7.0", "jest": "^29.7.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1"
"typescript": "^5.2.2"
} }
} }

View file

@ -23,6 +23,11 @@ export function callAsyncFunction<T>(
args: AsyncFunctionArguments, args: AsyncFunctionArguments,
source: string source: string
): Promise<T> { ): Promise<T> {
const fn = new AsyncFunction(...Object.keys(args), source) const commonJsArgs = {
return fn(...Object.values(args)) ...args,
module: {exports: {}},
exports: {}
}
const fn = new AsyncFunction(...Object.keys(commonJsArgs), source)
return fn(...Object.values(commonJsArgs))
} }

53
src/interpret-script.ts Normal file
View file

@ -0,0 +1,53 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import {Context} from '@actions/github/lib/context'
import {GitHub} from '@actions/github/lib/utils'
import * as glob from '@actions/glob'
import * as io from '@actions/io'
import {ModuleKind, ScriptTarget, transpileModule} from 'typescript'
import {callAsyncFunction} from './async-function'
export enum SupportedLanguage {
cjs = 'cjs',
cts = 'cts',
mts = 'mts'
}
interface CommonContext {
context: Context
core: typeof core
github: InstanceType<typeof GitHub>
exec: typeof exec
glob: typeof glob
io: typeof io
}
interface CjsContext extends CommonContext {
require: NodeRequire
__original_require__: NodeRequire
}
export async function interpretScript<T>(
language: SupportedLanguage,
context: CjsContext,
script: string
): Promise<() => Promise<T>> {
switch (language) {
case SupportedLanguage.cts:
case SupportedLanguage.mts: {
const fileName = `github-script.${language}`
script = transpileModule(script, {
compilerOptions: {
module: ModuleKind.CommonJS, // Take the incoming TypeScript and compile it to CommonJS to run in the CommonJS environment of this action.
target: ScriptTarget.Latest,
strict: true
},
fileName
}).outputText
}
}
return async () => callAsyncFunction(context, script)
}

View file

@ -7,7 +7,7 @@ import * as io from '@actions/io'
import {requestLog} from '@octokit/plugin-request-log' import {requestLog} from '@octokit/plugin-request-log'
import {retry} from '@octokit/plugin-retry' import {retry} from '@octokit/plugin-retry'
import {RequestRequestOptions} from '@octokit/types' import {RequestRequestOptions} from '@octokit/types'
import {callAsyncFunction} from './async-function' import {SupportedLanguage, interpretScript} from './interpret-script'
import {RetryOptions, getRetryOptions, parseNumberArray} from './retry-options' import {RetryOptions, getRetryOptions, parseNumberArray} from './retry-options'
import {wrapRequire} from './wrap-require' import {wrapRequire} from './wrap-require'
@ -21,6 +21,7 @@ type Options = {
previews?: string[] previews?: string[]
retry?: RetryOptions retry?: RetryOptions
request?: RequestRequestOptions request?: RequestRequestOptions
language?: string
} }
async function main(): Promise<void> { async function main(): Promise<void> {
@ -38,6 +39,15 @@ async function main(): Promise<void> {
exemptStatusCodes, exemptStatusCodes,
defaultGitHubOptions defaultGitHubOptions
) )
const languageRaw = core.getInput('language')
const langValues = Object.keys(SupportedLanguage)
if (!langValues.includes(languageRaw)) {
throw new Error(
`"language" must be one of the following: "${langValues.join('", "')}"`
)
}
const language = SupportedLanguage[languageRaw as SupportedLanguage]
const opts: Options = { const opts: Options = {
log: debug ? console : undefined, log: debug ? console : undefined,
@ -56,8 +66,8 @@ 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 script = core.getInput('script', {required: true})
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors. const executable = await interpretScript(
const result = await callAsyncFunction( language,
{ {
require: wrapRequire, require: wrapRequire,
__original_require__: __non_webpack_require__, __original_require__: __non_webpack_require__,
@ -72,6 +82,9 @@ async function main(): Promise<void> {
script script
) )
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors.
const result = await executable()
let encoding = core.getInput('result-encoding') let encoding = core.getInput('result-encoding')
encoding = encoding ? encoding : 'json' encoding = encoding ? encoding : 'json'

3
test/importable.ts Normal file
View file

@ -0,0 +1,3 @@
export function test() {
return 'hello'
}

3
test/requireable.js Normal file
View file

@ -0,0 +1,3 @@
exports.test = function test() {
return 'hello'
}