From b1002d26db514850dd9b516234c469d8ffadeedd Mon Sep 17 00:00:00 2001 From: Nick Alteen Date: Thu, 5 Sep 2024 17:34:28 -0400 Subject: [PATCH] Walk directory for needed paths (#103) Ref #102 Depending on the structure of the action repository, the `node_modules` and `package.json` needed by `local-action` may not exist in the expected locations. This PR updates the behavior of the action to instead walk the directory structure and attempt to locate the paths more dynamically, allowing support for more customized action repositories. --- .gitignore | 3 + README.md | 4 +- .../no-import/node_modules/.gitkeep | 0 .../success/node_modules/.gitkeep | 0 .../no-import/node_modules/.gitkeep | 0 .../javascript/success/node_modules/.gitkeep | 0 .../no-import/node_modules/.gitkeep | 0 .../success/node_modules/.gitkeep | 0 .../no-import/node_modules/.gitkeep | 0 .../typescript/success/node_modules/.gitkeep | 0 __tests__/commands/run.test.ts | 34 +++-------- package-lock.json | 11 +--- package.json | 3 +- src/commands/run.ts | 60 ++++++++++++++++--- src/stubs/summary-stubs.ts | 3 +- src/utils/package.ts | 32 +++++++--- 16 files changed, 92 insertions(+), 58 deletions(-) create mode 100644 __fixtures__/javascript-esm/no-import/node_modules/.gitkeep create mode 100644 __fixtures__/javascript-esm/success/node_modules/.gitkeep create mode 100644 __fixtures__/javascript/no-import/node_modules/.gitkeep create mode 100644 __fixtures__/javascript/success/node_modules/.gitkeep create mode 100644 __fixtures__/typescript-esm/no-import/node_modules/.gitkeep create mode 100644 __fixtures__/typescript-esm/success/node_modules/.gitkeep create mode 100644 __fixtures__/typescript/no-import/node_modules/.gitkeep create mode 100644 __fixtures__/typescript/success/node_modules/.gitkeep diff --git a/.gitignore b/.gitignore index bcff3aa..854c8d0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ node_modules npm-debug.log* reports Thumbs.db + +# Hack to keep the fake node_modules dirs for tests +!__fixtures__/**/node_modules diff --git a/README.md b/README.md index 4566f48..545e0fc 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ the following when preparing for release: [`@vercel/ncc`](https://www.npmjs.com/package/@vercel/ncc) **This tool supports non-transpiled action code only.** This is because it uses -[`proxyquire`](https://github.com/thlorenz/proxyquire) to override GitHub -Actions Toolkit dependencies (e.g +[`quibble`](https://github.com/testdouble/quibble) to override GitHub Actions +Toolkit dependencies (e.g [`@actions/core`](https://www.npmjs.com/package/@actions/core)). In transpiled code, this simply doesn't work. diff --git a/__fixtures__/javascript-esm/no-import/node_modules/.gitkeep b/__fixtures__/javascript-esm/no-import/node_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/__fixtures__/javascript-esm/success/node_modules/.gitkeep b/__fixtures__/javascript-esm/success/node_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/__fixtures__/javascript/no-import/node_modules/.gitkeep b/__fixtures__/javascript/no-import/node_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/__fixtures__/javascript/success/node_modules/.gitkeep b/__fixtures__/javascript/success/node_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/__fixtures__/typescript-esm/no-import/node_modules/.gitkeep b/__fixtures__/typescript-esm/no-import/node_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/__fixtures__/typescript-esm/success/node_modules/.gitkeep b/__fixtures__/typescript-esm/success/node_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/__fixtures__/typescript/no-import/node_modules/.gitkeep b/__fixtures__/typescript/no-import/node_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/__fixtures__/typescript/success/node_modules/.gitkeep b/__fixtures__/typescript/success/node_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/__tests__/commands/run.test.ts b/__tests__/commands/run.test.ts index b1d2893..ef0e85e 100644 --- a/__tests__/commands/run.test.ts +++ b/__tests__/commands/run.test.ts @@ -20,7 +20,7 @@ jest.unstable_mockModule('../../src/utils/output.js', () => { const { action } = await import('../../src/commands/run.js') // Prevent output during tests -jest.spyOn(console, 'log').mockImplementation(() => {}) +// jest.spyOn(console, 'log').mockImplementation(() => {}) jest.spyOn(console, 'table').mockImplementation(() => {}) describe('Command: run', () => { @@ -35,28 +35,8 @@ describe('Command: run', () => { jest.resetAllMocks() }) - describe('TypeScript', () => { - it('Action: success', async () => { - EnvMeta.actionFile = `./__fixtures__/typescript/success/action.yml` - EnvMeta.actionPath = `./__fixtures__/typescript/success` - EnvMeta.dotenvFile = `./__fixtures__/typescript/success/.env.fixture` - EnvMeta.entrypoint = `./__fixtures__/typescript/success/src/main.ts` - - await expect(action()).resolves.toBeUndefined() - }) - - it('Action: no-import', async () => { - EnvMeta.actionFile = `./__fixtures__/typescript/no-import/action.yml` - EnvMeta.actionPath = `./__fixtures__/typescript/no-import` - EnvMeta.dotenvFile = `./__fixtures__/typescript/no-import/.env.fixture` - EnvMeta.entrypoint = `./__fixtures__/typescript/no-import/src/main.ts` - - await expect(action()).resolves.toBeUndefined() - }) - }) - describe('TypeScript ESM', () => { - it('Action: success', async () => { + it('TypeScript ESM Action: success', async () => { EnvMeta.actionFile = `./__fixtures__/typescript-esm/success/action.yml` EnvMeta.actionPath = `./__fixtures__/typescript-esm/success` EnvMeta.dotenvFile = `./__fixtures__/typescript-esm/success/.env.fixture` @@ -68,7 +48,7 @@ describe('Command: run', () => { expect(quibbleEsm).toHaveBeenCalled() }) - it('Action: no-import', async () => { + it('TypeScript ESM Action: no-import', async () => { EnvMeta.actionFile = `./__fixtures__/typescript-esm/no-import/action.yml` EnvMeta.actionPath = `./__fixtures__/typescript-esm/no-import` EnvMeta.dotenvFile = `./__fixtures__/typescript-esm/no-import/.env.fixture` @@ -82,7 +62,7 @@ describe('Command: run', () => { }) describe('JavaScript', () => { - it('Action: success', async () => { + it('JavaScript Action: success', async () => { EnvMeta.actionFile = `./__fixtures__/javascript/success/action.yml` EnvMeta.actionPath = `./__fixtures__/javascript/success` EnvMeta.dotenvFile = `./__fixtures__/javascript/success/.env.fixture` @@ -91,7 +71,7 @@ describe('Command: run', () => { await expect(action()).resolves.toBeUndefined() }) - it('Action: no-import', async () => { + it('JavaScript Action: no-import', async () => { EnvMeta.actionFile = `./__fixtures__/javascript/no-import/action.yml` EnvMeta.actionPath = `./__fixtures__/javascript/no-import` EnvMeta.dotenvFile = `./__fixtures__/javascript/no-import/.env.fixture` @@ -102,7 +82,7 @@ describe('Command: run', () => { }) describe('JavaScript (ESM)', () => { - it('Action: success', async () => { + it('JavaScript ESM Action: success', async () => { EnvMeta.actionFile = `./__fixtures__/javascript/success/action.yml` EnvMeta.actionPath = `./__fixtures__/javascript/success` EnvMeta.dotenvFile = `./__fixtures__/javascript/success/.env.fixture` @@ -113,7 +93,7 @@ describe('Command: run', () => { expect(quibbleDefault).toHaveBeenCalled() }) - it('Action: no-import', async () => { + it('JavaScript ESM Action: no-import', async () => { EnvMeta.actionFile = `./__fixtures__/javascript/no-import/action.yml` EnvMeta.actionPath = `./__fixtures__/javascript/no-import` EnvMeta.dotenvFile = `./__fixtures__/javascript/no-import/.env.fixture` diff --git a/package-lock.json b/package-lock.json index 7a4f91d..dd3bca7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@github/local-action", - "version": "2.1.1", + "version": "2.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@github/local-action", - "version": "2.1.1", + "version": "2.1.2", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", @@ -30,7 +30,6 @@ "@types/figlet": "^1.5.8", "@types/jest": "^29.5.12", "@types/node": "^22.0.2", - "@types/proxyquire": "^1.3.31", "@typescript-eslint/eslint-plugin": "^8.4.0", "@typescript-eslint/parser": "^8.4.0", "eslint": "^8.57.0", @@ -2158,12 +2157,6 @@ "undici-types": "~6.19.2" } }, - "node_modules/@types/proxyquire": { - "version": "1.3.31", - "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.31.tgz", - "integrity": "sha512-uALowNG2TSM1HNPMMOR0AJwv4aPYPhqB0xlEhkeRTMuto5hjoSPZkvgu1nbPUkz3gEPAHv4sy4DmKsurZiEfRQ==", - "dev": true - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", diff --git a/package.json b/package.json index 7db0327..f51e499 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@github/local-action", "description": "Local Debugging for GitHub Actions", - "version": "2.1.1", + "version": "2.1.2", "type": "module", "author": "Nick Alteen ", "private": false, @@ -60,7 +60,6 @@ "@types/figlet": "^1.5.8", "@types/jest": "^29.5.12", "@types/node": "^22.0.2", - "@types/proxyquire": "^1.3.31", "@typescript-eslint/eslint-plugin": "^8.4.0", "@typescript-eslint/parser": "^8.4.0", "eslint": "^8.57.0", diff --git a/src/commands/run.ts b/src/commands/run.ts index 01b99f4..e66104f 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -1,4 +1,5 @@ import { config } from 'dotenv' +import { createRequire } from 'module' import quibble from 'quibble' import { CORE_STUBS, CoreMeta } from '../stubs/core-stubs.js' import { EnvMeta } from '../stubs/env-stubs.js' @@ -6,6 +7,8 @@ import type { Action } from '../types.js' import { printTitle } from '../utils/output.js' import { isESM } from '../utils/package.js' +const require = createRequire(import.meta.url) + export async function action(): Promise { const { Chalk } = await import('chalk') const chalk = new Chalk() @@ -90,24 +93,63 @@ export async function action(): Promise { printTitle(CoreMeta.colors.green, 'Running Action') - // Stub the `@actions/toolkit` libraries and run the action. Quibble requires - // a different approach depending on if this is an ESM action. + // Get the node_modules path, starting with the entrypoint. + /* istanbul ignore next */ + const dirs = + path.dirname(EnvMeta.entrypoint).split('/') || // Unix + path.dirname(EnvMeta.entrypoint).split('\\') || // Windows + [] + while (dirs.length > 0) { + // Check if the current directory has a node_modules directory. + if (fs.existsSync(path.join(...dirs, 'node_modules'))) break + + // Move up the directory tree. + dirs.pop() + } + /* istanbul ignore if */ + if (dirs.length === 0) + throw new Error('Could not find node_modules directory') + + // Stub the `@actions/toolkit` libraries and run the action. Quibble and + // local-action require a different approach depending on if the called action + // is written in ESM. if (isESM()) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call await quibble.esm( - `${path.resolve(EnvMeta.actionPath)}/node_modules/@actions/core/lib/core.js`, + path.resolve( + ...dirs, + 'node_modules', + '@actions', + 'core', + 'lib', + 'core.js' + ), CORE_STUBS ) + + // 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)) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + await run() } else { // eslint-disable-next-line @typescript-eslint/no-unsafe-call quibble( - `${path.resolve(EnvMeta.actionPath)}/node_modules/@actions/core`, + path.resolve( + ...dirs, + 'node_modules', + '@actions', + 'core', + 'lib', + 'core.js' + ), CORE_STUBS ) - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { run } = await import(path.resolve(EnvMeta.entrypoint)) - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - await run() + // 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)) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + await run() + } } diff --git a/src/stubs/summary-stubs.ts b/src/stubs/summary-stubs.ts index 621ee40..662ba18 100644 --- a/src/stubs/summary-stubs.ts +++ b/src/stubs/summary-stubs.ts @@ -59,8 +59,7 @@ export class Summary { CoreMeta.stepSummaryPath, fs.constants.R_OK | fs.constants.W_OK ) - } catch (error) { - console.error(error) + } catch { throw new Error( `Unable to access summary file: '${CoreMeta.stepSummaryPath}'. Check if the file has correct read/write permissions.` ) diff --git a/src/utils/package.ts b/src/utils/package.ts index b2d10a0..3b40776 100644 --- a/src/utils/package.ts +++ b/src/utils/package.ts @@ -1,4 +1,5 @@ import fs from 'fs' +import { dirname, join } from 'path' import { EnvMeta } from '../stubs/env-stubs.js' /** @@ -7,12 +8,29 @@ import { EnvMeta } from '../stubs/env-stubs.js' * @returns True if the project is an ESM module, false otherwise. */ export function isESM(): boolean { - const packageJson = JSON.parse( - fs.readFileSync(`${EnvMeta.actionPath}/package.json`, { - encoding: 'utf8', - flag: 'r' - }) - ) as { type: string } + // Starting at this directory, walk up the directory tree until we find a + // package.json file. + /* istanbul ignore next */ + const dirs = + dirname(EnvMeta.entrypoint).split('/') || // Unix + dirname(EnvMeta.entrypoint).split('\\') || // Windows + [] + while (dirs.length > 0) { + // Check if the current directory has a packge.json. + if (fs.existsSync(join(...dirs, 'package.json'))) { + const packageJson = JSON.parse( + fs.readFileSync(join(...dirs, 'package.json'), 'utf8') + ) as { [key: string]: any } - return packageJson.type === 'module' + return packageJson.type === 'module' + } + + // Move up the directory tree. + dirs.pop() + } + + // If we reach the root directory and still haven't found a package.json + // file, assume that the project is not an ESM module. + /* istanbul ignore next */ + return false }