Skip to content

Commit

Permalink
Windows path fix (#62)
Browse files Browse the repository at this point in the history
This PR fixes the error noted in #53 where Windows-style paths were not
being escaped correctly. It also cleans up the entrypoint script and
some documentation/formatting.
  • Loading branch information
ncalteen authored May 2, 2024
2 parents 474cb00 + fd8d9d6 commit 3378900
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 47 deletions.
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extends:
rules:
{
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'camelcase': 'off',
'eslint-comments/no-use': 'off',
'i18n-text/no-en': 'off',
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ against various repositories with different configurations.
1. Test your updated version

```bash
local-action run <path> <entrypoint> <env file>
local-action run <path> <entrypoint> <dotenv file>

# Or...
npm exec local-action run <path> <entrypoint> <env file>
npm exec local-action run <path> <entrypoint> <dotenv file>
```

Once you're finished testing, make sure to unlink!
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ For additional information about transpiled action code, see
git clone https://github.com/github/local-action.git
```

1. Install dependencies

```bash
npm ci
```

1. Install via `npm`

```bash
Expand Down
2 changes: 1 addition & 1 deletion __tests__/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe('Commmand', () => {
process_stderrSpy.mockRestore()
})

it('Throws if the env file does not exist', async () => {
it('Throws if the dotenv file does not exist', async () => {
process_stderrSpy = jest
.spyOn(process.stderr, 'write')
.mockImplementation()
Expand Down
98 changes: 62 additions & 36 deletions bin/local-action
Original file line number Diff line number Diff line change
@@ -1,46 +1,72 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')

// Back up the environment
const envBackup = { ...process.env }
const pathBackup = process.env.PATH

/**
* This script is used to run the local action. It sets the NODE_OPTIONS
* environment variable to require the bootstrap file, which sets up the
* environment variable to require the bootstrap script, which sets up the
* TypeScript environment for the action.
*/

// Set the first argument (path to action directory) as an environment variable.
// This is used in the bootstrap file to check for a `tsconfig.json`.
const actionPath = process.argv[2] ? path.resolve(process.argv[2]) : ''
process.env.TARGET_ACTION_PATH = actionPath

// Get the other arguments, if present. Validation and error handling is done
// within the package itself.
const entrypoint = process.argv[3] ?? ''
const dotenvFile = process.argv[4] ? path.resolve(process.argv[4]) : ''

// Get the absolute path to the `@github/local-action` package.
const packagePath = path.resolve(__dirname, '..')
const packageIndex = path.join(packagePath, 'src', 'index.ts')

// Set the NODE_OPTIONS environment variable to require the bootstrap file
const options = `--require "${path.join(packagePath, 'src', 'bootstrap.js')}"`
process.env.NODE_OPTIONS = process.env.NODE_OPTIONS
? `${process.env.NODE_OPTIONS} ${options}`
: options

// Run the action
const command = `npx tsx "${packageIndex}" "${actionPath}" "${entrypoint}" "${dotenvFile}"`

try {
execSync(command, { cwd: packagePath, stdio: 'inherit' })
} catch (error) {
process.exit(error.status)
} finally {
// Restore the environment
process.env = { ...envBackup }
process.env.PATH = pathBackup
function entrypoint() {
// Save the current environment and path.
const envBackup = { ...process.env }
const pathBackup = process.env.PATH

// Delete the TARGET_ACTION_PATH environment variable.
delete process.env.TARGET_ACTION_PATH

try {
// Get the absolute path to the `@github/local-action` package.
const packagePath = path.resolve(__dirname, '..')

// Get the absolute path to the bootstrap script. On Windows systems, this
// need to be double-escaped so the path resolves correctly.
const bootstrapPath =
process.platform === 'win32'
? path.join(packagePath, 'src', 'bootstrap.js').replaceAll('\\', '\\\\')
: path.join(packagePath, 'src', 'bootstrap.js')

// Require the bootstrap script in NODE_OPTIONS.
process.env.NODE_OPTIONS = process.env.NODE_OPTIONS
? `${process.env.NODE_OPTIONS} --require ${bootstrapPath}`
: `--require ${bootstrapPath}`

// Start building the command to run local-action.
let command = `npx tsx "${path.join(packagePath, 'src', 'index.ts')}"`

// Process the input arguments.
if (process.argv.length === 2) {
// No arguments...display the help message.
command += ' --help'
} else {
// Iterate over the arguments and build the command.
for (const arg of process.argv.slice(2)) {
// If the argument is a directory and TARGET_ACTION_PATH is not set, set
// it to the absolute path of the directory. The first directory is the
// target action path.
if (
!process.env.TARGET_ACTION_PATH &&
fs.existsSync(path.resolve(arg)) &&
fs.lstatSync(path.resolve(arg)).isDirectory()
)
process.env.TARGET_ACTION_PATH = path.resolve(arg)

// Append the argument to the command.
command += ` ${arg}`
}
}

// Run the command.
execSync(command, { cwd: packagePath, stdio: 'inherit' })
} catch (error) {
process.exit(error.status)
} finally {
// Restore the environment.
process.env = { ...envBackup }
process.env.PATH = pathBackup
}
}

entrypoint()
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": "1.4.2",
"version": "1.5.0",
"author": "Nick Alteen <[email protected]>",
"private": false,
"homepage": "https://github.com/github/local-action",
Expand Down
1 change: 0 additions & 1 deletion src/bootstrap.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-var-requires */

/**
Expand Down
8 changes: 4 additions & 4 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@ export async function makeProgram(): Promise<Command> {
if (!fs.statSync(actionPath).isDirectory())
throw new InvalidArgumentError('Action path must be a directory')
} catch (err: any) {
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
if ('code' in err && err.code === 'ENOENT')
throw new InvalidArgumentError('Action path does not exist')
else throw new InvalidArgumentError(err.message as string)
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}

// Save the action path to environment metadata
Expand Down Expand Up @@ -91,7 +89,9 @@ export async function makeProgram(): Promise<Command> {
program
.name('local-action')
.description('Test a GitHub Action locally')
.version('1.0.0')
.version(
`Version: ${JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8')).version as string}`
)

program
.command('run', { isDefault: true })
Expand All @@ -102,7 +102,7 @@ export async function makeProgram(): Promise<Command> {
'Action entrypoint (relative to the action directory)',
checkEntrypoint
)
.argument('<env file>', 'Path to the local .env file', checkDotenvFile)
.argument('<dotenv file>', 'Path to the local .env file', checkDotenvFile)
.action(async () => {
await runAction()
})
Expand Down

0 comments on commit 3378900

Please sign in to comment.