mirror of
https://github.com/actions/github-script.git
synced 2026-02-08 03:57:27 +00:00
feat: Add support for TypeScript scripts
Both of the .cts and .mts flavors. Because this action is written in CommonJS both have to compile to CommonJS in order to execute. As it is TypeScript there's already an expectation of some slowness, so I went with the approach of running the script via the node VM module. While a cleaner approach, it has the caveat that root level await in the script doesn't work. That should become available if https://github.com/actions/github-script/issues/457 is completed.
This commit is contained in:
parent
ccf1a8e117
commit
d3f9a3b3fb
9 changed files with 178341 additions and 15 deletions
196
__test__/interpret-script.test.ts
Normal file
196
__test__/interpret-script.test.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
/* 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.
|
||||||
|
exports = 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.
|
||||||
|
export default 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')
|
||||||
|
exports = 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,
|
||||||
|
`export default {a: 'b'}`
|
||||||
|
)
|
||||||
|
return expect(result()).resolves.toEqual({a: 'b'})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.skip(`a script that uses a root level await`, async () => {
|
||||||
|
// Will not work until we can actually run in ESM. Current code is transpiling the mts to cjs, so we don't get root level awaits yet.
|
||||||
|
const result = await interpretScript(
|
||||||
|
SupportedLanguage.mts,
|
||||||
|
{} as any,
|
||||||
|
`await Promise.resolve()`
|
||||||
|
)
|
||||||
|
return expect(result()).resolves
|
||||||
|
})
|
||||||
|
|
||||||
|
test.skip(`a script imports a script from disk`, async () => {
|
||||||
|
// Will not work until we can actually run in ESM. Current code is transpiling the mts to cjs, so we don't get root level awaits yet.
|
||||||
|
const result = await interpretScript(
|
||||||
|
SupportedLanguage.mts,
|
||||||
|
{require} as any,
|
||||||
|
`
|
||||||
|
const {test} = await import('../test/importable')
|
||||||
|
export default test()
|
||||||
|
`
|
||||||
|
)
|
||||||
|
return expect(result()).resolves.toEqual('hello')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
178044
dist/index.js
vendored
178044
dist/index.js
vendored
File diff suppressed because one or more lines are too long
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
src/interpret-script.ts
Normal file
70
src/interpret-script.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import * as VM from 'node:vm'
|
||||||
|
|
||||||
|
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.cjs:
|
||||||
|
return async () => callAsyncFunction(context, script)
|
||||||
|
case SupportedLanguage.cts:
|
||||||
|
case SupportedLanguage.mts: {
|
||||||
|
const fileName = `github-script.${language}`
|
||||||
|
|
||||||
|
const compilerResult = 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
|
||||||
|
})
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
const runContext: CjsContext & Record<string, unknown> = {
|
||||||
|
module: {exports: {}},
|
||||||
|
exports: {},
|
||||||
|
process,
|
||||||
|
...context
|
||||||
|
}
|
||||||
|
const runResult = VM.runInNewContext(
|
||||||
|
compilerResult.outputText,
|
||||||
|
runContext
|
||||||
|
)
|
||||||
|
|
||||||
|
return runResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/main.ts
19
src/main.ts
|
|
@ -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__,
|
||||||
|
|
@ -71,6 +81,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
3
test/importable.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function test() {
|
||||||
|
return 'hello'
|
||||||
|
}
|
||||||
3
test/requireable.js
Normal file
3
test/requireable.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
exports.test = function test() {
|
||||||
|
return 'hello'
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue