Skip to content

Commit

Permalink
Throw error if run() is not exported (#109)
Browse files Browse the repository at this point in the history
Whenever `local-action` is called, the entrypoint input must point to a
file that exports a `run` function. Otherwise, a not-very-useful error
occurs. This PR outputs a more useful error message when this scenario
occurs in the future.

See #104
  • Loading branch information
ncalteen authored Oct 3, 2024
1 parent b1002d2 commit 6c10228
Show file tree
Hide file tree
Showing 17 changed files with 133 additions and 3 deletions.
2 changes: 2 additions & 0 deletions __fixtures__/javascript/no-export/.env.fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ACTIONS_STEP_DEBUG=false
INPUT_MILLISECONDS=2400
16 changes: 16 additions & 0 deletions __fixtures__/javascript/no-export/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: JavaScript (No Import)
description: This action doesn't import any dependencies

inputs:
myInput:
description: An input
required: true
default: value

outputs:
myOutput:
description: An output

runs:
using: node20
main: dist/index.js
Empty file.
12 changes: 12 additions & 0 deletions __fixtures__/javascript/no-export/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "javascript-action",
"description": "GitHub Actions JavaScript template",
"version": "0.0.0",
"engines": {
"node": ">=20"
},
"dependencies": {
"@actions/core": "^1.10.1"
},
"devDependencies": {}
}
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions __fixtures__/typescript-esm/no-export/.env.fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ACTIONS_STEP_DEBUG=false
INPUT_MILLISECONDS=2400
17 changes: 17 additions & 0 deletions __fixtures__/typescript-esm/no-export/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: The name of your action here
description: Provide a description here
author: Your name or organization here

inputs:
milliseconds:
description: Your input description here
required: true
default: '1000'

outputs:
time:
description: Your output description here

runs:
using: node20
main: dist/index.js
Empty file.
17 changes: 17 additions & 0 deletions __fixtures__/typescript-esm/no-export/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "typescript-action",
"description": "GitHub Actions TypeScript template",
"version": "0.0.0",
"type": "module",
"engines": {
"node": ">=20"
},
"dependencies": {
"@actions/core": "^1.10.1"
},
"devDependencies": {
"@types/node": "^20.14.7",
"ts-node": "^10.9.2",
"typescript": "^5.5.2"
}
}
Empty file.
Empty file.
25 changes: 25 additions & 0 deletions __fixtures__/typescript-esm/no-export/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationMap": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"newLine": "lf",
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"pretty": true,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"strictNullChecks": true,
"target": "ES2022"
},
"exclude": ["node_modules"],
"include": ["src"]
}
25 changes: 25 additions & 0 deletions __tests__/commands/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { EnvMeta, ResetEnvMetadata } from '../../src/stubs/env-stubs.js'
const quibbleEsm = jest.fn().mockImplementation(() => {})
const quibbleDefault = jest.fn().mockImplementation(() => {})

// Stub console.log to reduce noise
console.log = jest.fn().mockImplementation(() => {})

// @ts-expect-error - `quibble` is the default, but we need to mock esm() too
quibbleDefault.esm = quibbleEsm

Expand Down Expand Up @@ -59,6 +62,17 @@ describe('Command: run', () => {
expect(core.setFailed).not.toHaveBeenCalled()
expect(quibbleEsm).toHaveBeenCalled()
})

it('TypeScript ESM Action: Throws if run is not exported', async () => {
EnvMeta.actionFile = `./__fixtures__/typescript-esm/no-export/action.yml`
EnvMeta.actionPath = `./__fixtures__/typescript-esm/no-export`
EnvMeta.dotenvFile = `./__fixtures__/typescript-esm/no-export/.env.fixture`
EnvMeta.entrypoint = `./__fixtures__/typescript-esm/no-export/src/main.ts`

await expect(action()).rejects.toThrow(
`Entrypoint ${EnvMeta.entrypoint} does not export a run() function`
)
})
})

describe('JavaScript', () => {
Expand All @@ -79,6 +93,17 @@ describe('Command: run', () => {

await expect(action()).resolves.toBeUndefined()
})

it('JavaScript Action: Throws if run is not exported', async () => {
EnvMeta.actionFile = `./__fixtures__/javascript/no-export/action.yml`
EnvMeta.actionPath = `./__fixtures__/javascript/no-export`
EnvMeta.dotenvFile = `./__fixtures__/javascript/no-export/.env.fixture`
EnvMeta.entrypoint = `./__fixtures__/javascript/no-export/src/main.js`

await expect(action()).rejects.toThrow(
`Entrypoint ${EnvMeta.entrypoint} does not export a run() function`
)
})
})

describe('JavaScript (ESM)', () => {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@github/local-action",
"description": "Local Debugging for GitHub Actions",
"version": "2.1.2",
"version": "2.1.3",
"type": "module",
"author": "Nick Alteen <ncalteen@github.com>",
"private": false,
Expand Down
14 changes: 14 additions & 0 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ export async function action(): Promise<void> {
// ESM actions need to be imported, not required.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { run } = await import(path.resolve(EnvMeta.entrypoint))

// Check if the required path is a function.
if (typeof run !== 'function')
throw new Error(
`Entrypoint ${EnvMeta.entrypoint} does not export a run() function`
)

// eslint-disable-next-line @typescript-eslint/no-unsafe-call
await run()
} else {
Expand All @@ -149,6 +156,13 @@ export async function action(): Promise<void> {
// CJS actions need to be required, not imported.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require
const { run } = require(path.resolve(EnvMeta.entrypoint))

// Check if the required path is a function.
if (typeof run !== 'function')
throw new Error(
`Entrypoint ${EnvMeta.entrypoint} does not export a run() function`
)

// eslint-disable-next-line @typescript-eslint/no-unsafe-call
await run()
}
Expand Down

0 comments on commit 6c10228

Please sign in to comment.