diff --git a/CHANGELOG.md b/CHANGELOG.md index 1323043..4544363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v4 + +This version adds support for +[`pre`](https://docs.github.com/en/actions/reference/metadata-syntax-for-github-actions#runspre) +and +[`post`](https://docs.github.com/en/actions/reference/metadata-syntax-for-github-actions#runspost) +scripts for actions. These should follow the same structure as the `run` action +code (see the +[`README.md`](https://github.com/github/local-action#action-structure) for more +details). + ## v3 This version adds **experimental** support for [pnpm](https://pnpm.io/) and diff --git a/README.md b/README.md index 3177d8b..2b7e06c 100644 --- a/README.md +++ b/README.md @@ -182,31 +182,35 @@ For additional information about transpiled action code, see | `-h`, `--help` | Display help information | | `-V`, `--version` | Display version information | -### `local-action run ` - -| Argument | Description | -| ------------------ | ------------------------------------------------------ | -| `path` | Path to the local action directory | -| | Example: `/path/to/action.yml` | -| `logic entrypoint` | Action logic entrypoint (relative to action directory) | -| | Example: `src/main.ts` | -| `dotenv file` | Path to the local `.env` file for action inputs | -| | Example: `/path/to/.env` | -| | See the example [`.env.example`](.env.example) | +### `local-action run [--pre
] [--post ]`
+
+| Argument                   | Description                                                         |
+| -------------------------- | ------------------------------------------------------------------- |
+| `path`                     | Path to the local action directory                                  |
+|                            | Example: `/path/to/action.yml`                                      |
+| `logic entrypoint`         | Action logic entrypoint (relative to action directory)              |
+|                            | Example: `src/main.ts`                                              |
+| `dotenv file`              | Path to the local `.env` file for action inputs                     |
+|                            | Example: `/path/to/.env`                                            |
+|                            | See the example [`.env.example`](.env.example)                      |
+| `--pre 
`   | (Optional) `pre` command entrypoint (relative to action directory)  |
+|                            | Example: `pre/main.ts`                                              |
+| `--post ` | (Optional) `post` command entrypoint (relative to action directory) |
+|                            | Example: `post/main.ts`                                             |
 
 Examples:
 
 ```bash
-local-action run /path/to/typescript-action src/main.ts .env
+local-action run /path/to/typescript-action src/main.ts .env --pre pre/main.ts --post post/main.ts
 
 # The `run` action is invoked by default as well
-local-action /path/to/typescript-action src/main.ts .env
+local-action /path/to/typescript-action src/main.ts .env --pre pre/main.ts --post post/main.ts
 ```
 
 #### Output
 
 ```console
-$ local-action run /path/to/typescript-action src/main.ts .env
+$ local-action run /path/to/typescript-action src/main.ts .env --pre pre/main.ts --post post/main.ts
      _        _   _               ____       _
     / \   ___| |_(_) ___  _ __   |  _ \  ___| |__  _   _  __ _  __ _  ___ _ __
    / _ \ / __| __| |/ _ \| '_ \  | | | |/ _ \ '_ \| | | |/ _` |/ _` |/ _ \ '__|
diff --git a/__fixtures__/javascript-esm/success/post/index.js b/__fixtures__/javascript-esm/success/post/index.js
new file mode 100644
index 0000000..2320541
--- /dev/null
+++ b/__fixtures__/javascript-esm/success/post/index.js
@@ -0,0 +1,3 @@
+const { run } = require('./main')
+
+run()
diff --git a/__fixtures__/javascript-esm/success/post/main.js b/__fixtures__/javascript-esm/success/post/main.js
new file mode 100644
index 0000000..c8b0755
--- /dev/null
+++ b/__fixtures__/javascript-esm/success/post/main.js
@@ -0,0 +1,13 @@
+const { getInput, info, setOutput } = require('@actions/core')
+
+async function run() {
+  const myInput = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('JavaScript Action Succeeded!')
+}
+
+module.exports = {
+  run
+}
diff --git a/__fixtures__/javascript-esm/success/pre/index.js b/__fixtures__/javascript-esm/success/pre/index.js
new file mode 100644
index 0000000..2320541
--- /dev/null
+++ b/__fixtures__/javascript-esm/success/pre/index.js
@@ -0,0 +1,3 @@
+const { run } = require('./main')
+
+run()
diff --git a/__fixtures__/javascript-esm/success/pre/main.js b/__fixtures__/javascript-esm/success/pre/main.js
new file mode 100644
index 0000000..c8b0755
--- /dev/null
+++ b/__fixtures__/javascript-esm/success/pre/main.js
@@ -0,0 +1,13 @@
+const { getInput, info, setOutput } = require('@actions/core')
+
+async function run() {
+  const myInput = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('JavaScript Action Succeeded!')
+}
+
+module.exports = {
+  run
+}
diff --git a/__fixtures__/javascript/success/post/index.js b/__fixtures__/javascript/success/post/index.js
new file mode 100644
index 0000000..2320541
--- /dev/null
+++ b/__fixtures__/javascript/success/post/index.js
@@ -0,0 +1,3 @@
+const { run } = require('./main')
+
+run()
diff --git a/__fixtures__/javascript/success/post/main.js b/__fixtures__/javascript/success/post/main.js
new file mode 100644
index 0000000..c8b0755
--- /dev/null
+++ b/__fixtures__/javascript/success/post/main.js
@@ -0,0 +1,13 @@
+const { getInput, info, setOutput } = require('@actions/core')
+
+async function run() {
+  const myInput = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('JavaScript Action Succeeded!')
+}
+
+module.exports = {
+  run
+}
diff --git a/__fixtures__/javascript/success/pre/index.js b/__fixtures__/javascript/success/pre/index.js
new file mode 100644
index 0000000..2320541
--- /dev/null
+++ b/__fixtures__/javascript/success/pre/index.js
@@ -0,0 +1,3 @@
+const { run } = require('./main')
+
+run()
diff --git a/__fixtures__/javascript/success/pre/main.js b/__fixtures__/javascript/success/pre/main.js
new file mode 100644
index 0000000..c8b0755
--- /dev/null
+++ b/__fixtures__/javascript/success/pre/main.js
@@ -0,0 +1,13 @@
+const { getInput, info, setOutput } = require('@actions/core')
+
+async function run() {
+  const myInput = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('JavaScript Action Succeeded!')
+}
+
+module.exports = {
+  run
+}
diff --git a/__fixtures__/path.ts b/__fixtures__/path.ts
new file mode 100644
index 0000000..0a236c2
--- /dev/null
+++ b/__fixtures__/path.ts
@@ -0,0 +1,7 @@
+import { jest } from '@jest/globals'
+
+export const resolve = jest.fn()
+
+export default {
+  resolve
+}
diff --git a/__fixtures__/typescript-esm/success/post/index.ts b/__fixtures__/typescript-esm/success/post/index.ts
new file mode 100644
index 0000000..2804fe8
--- /dev/null
+++ b/__fixtures__/typescript-esm/success/post/index.ts
@@ -0,0 +1,3 @@
+import { run } from './main.js'
+
+run()
diff --git a/__fixtures__/typescript-esm/success/post/main.ts b/__fixtures__/typescript-esm/success/post/main.ts
new file mode 100644
index 0000000..51cab60
--- /dev/null
+++ b/__fixtures__/typescript-esm/success/post/main.ts
@@ -0,0 +1,9 @@
+import { getInput, info, setOutput } from '@actions/core'
+
+export async function run(): Promise {
+  const myInput: string = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('TypeScript ESM Action Succeeded!')
+}
diff --git a/__fixtures__/typescript-esm/success/pre/index.ts b/__fixtures__/typescript-esm/success/pre/index.ts
new file mode 100644
index 0000000..2804fe8
--- /dev/null
+++ b/__fixtures__/typescript-esm/success/pre/index.ts
@@ -0,0 +1,3 @@
+import { run } from './main.js'
+
+run()
diff --git a/__fixtures__/typescript-esm/success/pre/main.ts b/__fixtures__/typescript-esm/success/pre/main.ts
new file mode 100644
index 0000000..51cab60
--- /dev/null
+++ b/__fixtures__/typescript-esm/success/pre/main.ts
@@ -0,0 +1,9 @@
+import { getInput, info, setOutput } from '@actions/core'
+
+export async function run(): Promise {
+  const myInput: string = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('TypeScript ESM Action Succeeded!')
+}
diff --git a/__fixtures__/typescript/success-yaml/post/index.ts b/__fixtures__/typescript/success-yaml/post/index.ts
new file mode 100644
index 0000000..f81171b
--- /dev/null
+++ b/__fixtures__/typescript/success-yaml/post/index.ts
@@ -0,0 +1,3 @@
+import { run } from './main'
+
+run()
diff --git a/__fixtures__/typescript/success-yaml/post/main.ts b/__fixtures__/typescript/success-yaml/post/main.ts
new file mode 100644
index 0000000..2f71fd4
--- /dev/null
+++ b/__fixtures__/typescript/success-yaml/post/main.ts
@@ -0,0 +1,9 @@
+import { getInput, info, setOutput } from '@actions/core'
+
+export async function run(): Promise {
+  const myInput: string = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('TypeScript Action Succeeded!')
+}
diff --git a/__fixtures__/typescript/success-yaml/pre/index.ts b/__fixtures__/typescript/success-yaml/pre/index.ts
new file mode 100644
index 0000000..f81171b
--- /dev/null
+++ b/__fixtures__/typescript/success-yaml/pre/index.ts
@@ -0,0 +1,3 @@
+import { run } from './main'
+
+run()
diff --git a/__fixtures__/typescript/success-yaml/pre/main.ts b/__fixtures__/typescript/success-yaml/pre/main.ts
new file mode 100644
index 0000000..2f71fd4
--- /dev/null
+++ b/__fixtures__/typescript/success-yaml/pre/main.ts
@@ -0,0 +1,9 @@
+import { getInput, info, setOutput } from '@actions/core'
+
+export async function run(): Promise {
+  const myInput: string = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('TypeScript Action Succeeded!')
+}
diff --git a/__fixtures__/typescript/success/post/index.ts b/__fixtures__/typescript/success/post/index.ts
new file mode 100644
index 0000000..f81171b
--- /dev/null
+++ b/__fixtures__/typescript/success/post/index.ts
@@ -0,0 +1,3 @@
+import { run } from './main'
+
+run()
diff --git a/__fixtures__/typescript/success/post/main.ts b/__fixtures__/typescript/success/post/main.ts
new file mode 100644
index 0000000..2f71fd4
--- /dev/null
+++ b/__fixtures__/typescript/success/post/main.ts
@@ -0,0 +1,9 @@
+import { getInput, info, setOutput } from '@actions/core'
+
+export async function run(): Promise {
+  const myInput: string = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('TypeScript Action Succeeded!')
+}
diff --git a/__fixtures__/typescript/success/pre/index.ts b/__fixtures__/typescript/success/pre/index.ts
new file mode 100644
index 0000000..f81171b
--- /dev/null
+++ b/__fixtures__/typescript/success/pre/index.ts
@@ -0,0 +1,3 @@
+import { run } from './main'
+
+run()
diff --git a/__fixtures__/typescript/success/pre/main.ts b/__fixtures__/typescript/success/pre/main.ts
new file mode 100644
index 0000000..2f71fd4
--- /dev/null
+++ b/__fixtures__/typescript/success/pre/main.ts
@@ -0,0 +1,9 @@
+import { getInput, info, setOutput } from '@actions/core'
+
+export async function run(): Promise {
+  const myInput: string = getInput('myInput')
+
+  setOutput('myOutput', myInput)
+
+  info('TypeScript Action Succeeded!')
+}
diff --git a/__tests__/command.test.ts b/__tests__/command.test.ts
index 6dfa29f..1cc1147 100644
--- a/__tests__/command.test.ts
+++ b/__tests__/command.test.ts
@@ -1,7 +1,8 @@
 import { jest } from '@jest/globals'
 import { Command } from 'commander'
+import path from 'path'
 import { ResetCoreMetadata } from '../src/stubs/core/core.js'
-import { ResetEnvMetadata } from '../src/stubs/env.js'
+import { EnvMeta, ResetEnvMetadata } from '../src/stubs/env.js'
 
 const action = jest.fn()
 
@@ -15,15 +16,6 @@ const process_exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
   throw new Error(`process.exit()`)
 })
 
-const process_stderrSpy = jest
-  .spyOn(process.stderr, 'write')
-  .mockImplementation(() => true)
-
-let program: Command
-
-// Prevent output during tests
-jest.spyOn(console, 'log').mockImplementation(() => {})
-
 const { makeProgram } = await import('../src/command.js')
 
 describe('Commmand', () => {
@@ -31,9 +23,6 @@ describe('Commmand', () => {
     // Reset metadata
     ResetEnvMetadata()
     ResetCoreMetadata()
-
-    // Create a new program before each test
-    program = await makeProgram()
   })
 
   afterEach(() => {
@@ -41,146 +30,195 @@ describe('Commmand', () => {
   })
 
   describe('makeProgram()', () => {
-    it('Returns a Program', () => {
+    it('Returns a Program', async () => {
+      const program = await makeProgram()
+
       expect(program).not.toBe(null)
       expect(program).toBeInstanceOf(Command)
     })
 
-    it('Has a run command', () => {
+    it('Has a run command', async () => {
+      const program = await makeProgram()
+
       expect(program.commands.find(c => c.name() === 'run')).toBeInstanceOf(
         Command
       )
     })
 
     it('Runs if all arguments are provided (action.yml)', async () => {
-      await (
-        await makeProgram()
-      ).parseAsync(
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('./__fixtures__/typescript/success')
+
+      program.parse(
         [
           './__fixtures__/typescript/success',
           'src/main.ts',
-          './__fixtures__/typescript/success/.env.fixture'
+          './__fixtures__/typescript/success/.env.fixture',
+          '--pre',
+          'pre/main.ts',
+          '--post',
+          'post/main.ts'
         ],
         {
           from: 'user'
         }
       )
 
-      expect(process_exitSpy).not.toHaveBeenCalled()
       expect(action).toHaveBeenCalled()
     })
 
     it('Runs if all arguments are provided (action.yaml)', async () => {
-      await (
-        await makeProgram()
-      ).parseAsync(
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve(
+        './__fixtures__/typescript/success-yaml'
+      )
+
+      program.parse(
         [
           './__fixtures__/typescript/success-yaml',
           'src/main.ts',
-          './__fixtures__/typescript/success-yaml/.env.fixture'
+          './__fixtures__/typescript/success-yaml/.env.fixture',
+          '--pre',
+          'pre/main.ts',
+          '--post',
+          'post/main.ts'
         ],
         {
           from: 'user'
         }
       )
 
-      expect(process_exitSpy).not.toHaveBeenCalled()
       expect(action).toHaveBeenCalled()
     })
 
     it('Exits if no path argument is provided', async () => {
-      await (await makeProgram()).parseAsync([], { from: 'user' })
+      const program = await makeProgram()
 
-      expect(process_exitSpy).toHaveBeenCalled()
+      program.parse([], { from: 'user' })
 
-      process_stderrSpy.mockRestore()
+      expect(process_exitSpy).toHaveBeenCalled()
     })
 
     it('Exits if no entrypoint argument is provided', async () => {
-      await (
-        await makeProgram()
-      ).parseAsync(['./__fixtures__/typescript/success', ''], { from: 'user' })
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('./__fixtures__/typescript/success')
 
-      expect(process_exitSpy).toHaveBeenCalled()
+      program.parse(['./__fixtures__/typescript/success', ''], { from: 'user' })
 
-      process_stderrSpy.mockRestore()
+      expect(process_exitSpy).toHaveBeenCalled()
     })
 
     it('Exits if no env-file argument is provided', async () => {
-      await (
-        await makeProgram()
-      ).parseAsync(['./__fixtures__/typescript/success', 'src/main.ts'], {
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('./__fixtures__/typescript/success')
+
+      program.parse(['./__fixtures__/typescript/success', 'src/main.ts'], {
         from: 'user'
       })
 
       expect(process_exitSpy).toHaveBeenCalled()
-
-      process_stderrSpy.mockRestore()
     })
 
     it('Exits if the action path is not a directory', async () => {
-      await expect(
-        (await makeProgram()).parseAsync(
-          ['./package.json', 'src/main.ts', '.env'],
-          {
-            from: 'user'
-          }
-        )
-      ).rejects.toThrow('Action path must be a directory')
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('./__fixtures__/typescript/success')
 
-      process_stderrSpy.mockRestore()
+      expect(() => {
+        program.parse(['./package.json', 'src/main.ts', '.env'], {
+          from: 'user'
+        })
+      }).toThrow('Action path must be a directory')
     })
 
     it('Exits if the action path does not exist', async () => {
-      await expect(
-        (await makeProgram()).parseAsync(
-          ['/test/path/does/not/exist', 'src/main.ts', '.env'],
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('/test/path/does/not/exist')
+
+      expect(() => {
+        program.parse(['/test/path/does/not/exist', 'src/main.ts', '.env'], {
+          from: 'user'
+        })
+      }).toThrow('Action path does not exist')
+    })
+
+    it('Exits if the action path does not contain an action.yml or action.yaml', async () => {
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('./__fixtures__')
+
+      expect(() => {
+        program.parse(['./__fixtures__', 'src/main.ts', '.env'], {
+          from: 'user'
+        })
+      }).toThrow('Path must contain an action.yml / action.yaml file')
+    })
+
+    it('Exits if the entrypoint does not exist', async () => {
+      const program = await makeProgram()
+
+      expect(() => {
+        program.parse(
+          ['./__fixtures__/typescript/success', 'src/fake.ts', '.env'],
           {
             from: 'user'
           }
         )
-      ).rejects.toThrow('Action path does not exist')
-
-      process_stderrSpy.mockRestore()
+      }).toThrow('Entrypoint does not exist')
     })
 
-    it('Exits if the action path does not contain an action.yml or action.yaml', async () => {
-      await expect(
-        (await makeProgram()).parseAsync(
-          ['./__fixtures__', 'src/main.ts', '.env'],
+    it('Exits if the pre entrypoint does not exist', async () => {
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('./__fixtures__/typescript/success')
+
+      expect(() => {
+        program.parse(
+          [
+            './__fixtures__/typescript/success',
+            'src/main.ts',
+            './__fixtures__/typescript/success/.env.fixture',
+            '--pre',
+            'pre/fake.ts'
+          ],
           {
             from: 'user'
           }
         )
-      ).rejects.toThrow('Path must contain an action.yml / action.yaml file')
-
-      process_stderrSpy.mockRestore()
+      }).toThrow('PRE entrypoint does not exist')
     })
 
-    it('Exits if the entrypoint does not exist', async () => {
-      await expect(
-        (await makeProgram()).parseAsync(
-          ['./__fixtures__/typescript/success', 'src/fake.ts', '.env'],
+    it('Exits if the post entrypoint does not exist', async () => {
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('./__fixtures__/typescript/success')
+
+      expect(() => {
+        program.parse(
+          [
+            './__fixtures__/typescript/success',
+            'src/main.ts',
+            './__fixtures__/typescript/success/.env.fixture',
+            '--pre',
+            'pre/main.ts',
+            '--post',
+            'post/fake.ts'
+          ],
           {
             from: 'user'
           }
         )
-      ).rejects.toThrow('Entrypoint does not exist')
-
-      process_stderrSpy.mockRestore()
+      }).toThrow('POST entrypoint does not exist')
     })
 
     it('Throws if the dotenv file does not exist', async () => {
-      await expect(
-        (await makeProgram()).parseAsync(
+      const program = await makeProgram()
+      EnvMeta.actionPath = path.resolve('./__fixtures__/typescript/success')
+
+      expect(() => {
+        program.parse(
           ['./__fixtures__/typescript/success', 'src/main.ts', '.notreal.env'],
           {
             from: 'user'
           }
         )
-      ).rejects.toThrow('Environment file does not exist')
-
-      process_stderrSpy.mockRestore()
+      }).toThrow('Environment file does not exist')
     })
   })
 })
diff --git a/__tests__/stubs/env.test.ts b/__tests__/stubs/env.test.ts
index ab70bab..3b7ce3d 100644
--- a/__tests__/stubs/env.test.ts
+++ b/__tests__/stubs/env.test.ts
@@ -13,7 +13,9 @@ const empty: EnvMetadata = {
   env: {},
   inputs: {},
   outputs: {},
-  path: ''
+  path: '',
+  preEntrypoint: undefined,
+  postEntrypoint: undefined
 }
 
 // Prevent output during tests
diff --git a/__tests__/utils/config.test.ts b/__tests__/utils/config.test.ts
new file mode 100644
index 0000000..0d091c2
--- /dev/null
+++ b/__tests__/utils/config.test.ts
@@ -0,0 +1,78 @@
+import { jest } from '@jest/globals'
+import * as path from '../../__fixtures__/path.js'
+import { ResetCoreMetadata } from '../../src/stubs/core/core.js'
+import { EnvMeta, ResetEnvMetadata } from '../../src/stubs/env.js'
+
+jest.unstable_mockModule('path', () => path)
+
+const { getOSEntrypoints } = await import('../../src/utils/config.js')
+
+describe('Config', () => {
+  beforeEach(() => {
+    // Reset metadata
+    ResetEnvMetadata()
+    ResetCoreMetadata()
+
+    path.resolve.mockImplementation(value => value)
+
+    EnvMeta.entrypoint = '/action/entrypoint.ts'
+  })
+
+  afterEach(() => {
+    jest.resetAllMocks()
+  })
+
+  describe('getOSEntrypoints()', () => {
+    it('Gets the ESM entrypoints', () => {
+      EnvMeta.preEntrypoint = '/action/pre.ts'
+      EnvMeta.postEntrypoint = '/action/post.ts'
+
+      const result = getOSEntrypoints('esm')
+
+      expect(result).toEqual({
+        main: '/action/entrypoint.ts',
+        pre: '/action/pre.ts',
+        post: '/action/post.ts'
+      })
+    })
+
+    it('Gets the ESM entrypoints (undefined)', () => {
+      EnvMeta.preEntrypoint = undefined
+      EnvMeta.postEntrypoint = undefined
+
+      const result = getOSEntrypoints('esm')
+
+      expect(result).toEqual({
+        main: '/action/entrypoint.ts',
+        pre: undefined,
+        post: undefined
+      })
+    })
+
+    it('Gets the CJS entrypoints', () => {
+      EnvMeta.preEntrypoint = '/action/pre.ts'
+      EnvMeta.postEntrypoint = '/action/post.ts'
+
+      const result = getOSEntrypoints('cjs')
+
+      expect(result).toEqual({
+        main: '/action/entrypoint.ts',
+        pre: '/action/pre.ts',
+        post: '/action/post.ts'
+      })
+    })
+
+    it('Gets the CJS entrypoints (undefined)', () => {
+      EnvMeta.preEntrypoint = undefined
+      EnvMeta.postEntrypoint = undefined
+
+      const result = getOSEntrypoints('cjs')
+
+      expect(result).toEqual({
+        main: '/action/entrypoint.ts',
+        pre: undefined,
+        post: undefined
+      })
+    })
+  })
+})
diff --git a/bin/local-action.js b/bin/local-action.js
index 6bf5c40..1562555 100755
--- a/bin/local-action.js
+++ b/bin/local-action.js
@@ -44,24 +44,12 @@ function entrypoint() {
     // be prepended to this later.
     let command = `tsx "${path.join(packagePath, 'src', 'index.ts')}"`
 
-    // If there are no input arguments, or the only argument is the help flag,
-    // display the help message.
-    if (
-      process.argv.length === 2 ||
-      ['--help', '-h'].includes(process.argv[2])
-    ) {
-      command += ` --help`
-
-      // Run the command.
-      execSync(command, { stdio: 'inherit' })
-      return
-    }
+    const args = process.argv.slice(2)
 
-    // 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.
+    // Iterate over the arguments and build the command. 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 should be the target action path.
+    for (const arg of args)
       if (
         !process.env.TARGET_ACTION_PATH &&
         fs.existsSync(path.resolve(arg)) &&
@@ -69,8 +57,12 @@ function entrypoint() {
       )
         process.env.TARGET_ACTION_PATH = path.resolve(arg)
 
-      // Append the argument to the command.
-      command += ` ${arg}`
+    // If the TARGET_ACTION_PATH environment variable is not set, display the
+    // help message.
+    if (!process.env.TARGET_ACTION_PATH) {
+      command += `-- --help`
+      execSync(command, { stdio: 'inherit' })
+      return
     }
 
     // Starting in the TARGET_ACTION_PATH, locate the package.json file and
@@ -90,8 +82,8 @@ function entrypoint() {
           'utf8'
         )
 
-        // If the package.json file has a packageManager field, set the
-        // command to use that package manager.
+        // If the package.json file has a packageManager field, set the command
+        // to use that package manager.
         if (json.packageManager?.startsWith('pnpm')) {
           process.env.NODE_PACKAGE_MANAGER = 'pnpm'
           command = 'pnpm dlx ' + command
@@ -123,14 +115,18 @@ function entrypoint() {
       process.exit(1)
     }
 
-    // Run the command.
-    execSync(command, { stdio: 'inherit' })
+    // Run the command. The original arguments are passed after the `--` to
+    // prevent them from being eaten by the package manager.
+    execSync(`${command} -- ${args.join(' ')}`, { stdio: 'inherit' })
   } catch (error) {
-    process.exit(error.status)
-  } finally {
+    console.log()
+    console.log(error)
+
     // Restore the environment.
     process.env = { ...envBackup }
     process.env.PATH = pathBackup
+
+    process.exit(error.status)
   }
 }
 
diff --git a/package-lock.json b/package-lock.json
index 986f5ca..19d474c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@github/local-action",
-  "version": "3.2.1",
+  "version": "4.0.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@github/local-action",
-      "version": "3.2.1",
+      "version": "4.0.0",
       "license": "MIT",
       "dependencies": {
         "@actions/artifact": "^2.3.2",
@@ -4683,6 +4683,7 @@
       "version": "14.0.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz",
       "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==",
+      "license": "MIT",
       "engines": {
         "node": ">=20"
       }
diff --git a/package.json b/package.json
index 4ccc356..448f778 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@github/local-action",
   "description": "Local Debugging for GitHub Actions",
-  "version": "3.2.1",
+  "version": "4.0.0",
   "type": "module",
   "author": "Nick Alteen ",
   "private": false,
diff --git a/src/command.ts b/src/command.ts
index aefa083..fe4285a 100644
--- a/src/command.ts
+++ b/src/command.ts
@@ -5,20 +5,22 @@ import { action } from './commands/run.js'
 import { EnvMeta } from './stubs/env.js'
 
 /**
- * Creates the program for the CLI
+ * Creates the CLI Program
  *
- * @returns A promise that resolves to the program Command object
+ * @returns Command Program Object
  */
 export async function makeProgram(): Promise {
   const program: Command = new Command()
   const fs = await import('fs')
   const path = await import('path')
+  const __dirname = dirname(fileURLToPath(import.meta.url))
 
   /**
-   * Checks if the provided action path is valid
+   * Checks if Action Path is Valid
    *
-   * @param value The action path
-   * @returns The resolved action path
+   * @param value Action Path
+   * @returns Resolved Action Path
+   * @throws InvalidArgumentError
    */
   function checkActionPath(value: string): string {
     const actionPath: string = path.resolve(value)
@@ -51,17 +53,18 @@ export async function makeProgram(): Promise {
   }
 
   /**
-   * Checks if the provided entrypoint is valid
+   * Checks if `main` Entrypoint is Valid
    *
-   * @param value The entrypoint
-   * @returns The resolved entrypoint path
+   * @param value Entrypoint
+   * @returns Resolved Entrypoint Path
+   * @throws InvalidArgumentError
    */
   function checkEntrypoint(value: string): string {
     const entrypoint: string = path.resolve(EnvMeta.actionPath, value)
 
     // Confirm the entrypoint exists
     if (!fs.existsSync(entrypoint))
-      throw new InvalidArgumentError('Entrypoint does not exist')
+      throw new InvalidArgumentError(`Entrypoint does not exist: ${value}`)
 
     // Save the action entrypoint to environment metadata
     EnvMeta.entrypoint = entrypoint
@@ -70,10 +73,53 @@ export async function makeProgram(): Promise {
   }
 
   /**
-   * Checks if the provided dotenv file is valid
+   * Checks if `pre` Entrypoint is Valid
    *
-   * @param value The dotenv file path
-   * @returns The resolved dotenv file path
+   * @param value Entrypoint
+   * @returns Resolved Entrypoint Path
+   * @throws InvalidArgumentError
+   */
+  function checkPreEntrypoint(value: string): string {
+    const entrypoint: string = path.resolve(EnvMeta.actionPath, value)
+
+    // Confirm the entrypoint exists
+    if (!fs.existsSync(entrypoint))
+      throw new InvalidArgumentError(
+        `PRE entrypoint does not exist: ${entrypoint}`
+      )
+
+    // Save the action entrypoint to environment metadata
+    EnvMeta.preEntrypoint = entrypoint
+
+    return entrypoint
+  }
+
+  /**
+   * Checks if `post` Entrypoint is Valid
+   *
+   * @param value Entrypoint
+   * @returns Resolved Entrypoint Path
+   * @throws InvalidArgumentError
+   */
+  function checkPostEntrypoint(value: string): string {
+    const entrypoint: string = path.resolve(EnvMeta.actionPath, value)
+
+    // Confirm the entrypoint exists
+    if (!fs.existsSync(entrypoint))
+      throw new InvalidArgumentError(`POST entrypoint does not exist: ${value}`)
+
+    // Save the action entrypoint to environment metadata
+    EnvMeta.postEntrypoint = entrypoint
+
+    return entrypoint
+  }
+
+  /**
+   * Checks if `dotenv` File is Valid
+   *
+   * @param value Dotenv File Path
+   * @returns Resolved Dotenv File Path
+   * @throws InvalidArgumentError
    */
   function checkDotenvFile(value: string): string {
     const dotenvFile: string = path.resolve(value)
@@ -88,8 +134,6 @@ export async function makeProgram(): Promise {
     return dotenvFile
   }
 
-  const __dirname = dirname(fileURLToPath(import.meta.url))
-
   program
     .name('local-action')
     .description('Test a GitHub Action locally')
@@ -99,16 +143,28 @@ export async function makeProgram(): Promise {
 
   program
     .command('run', { isDefault: true })
-    .description('Run a local action')
-    .argument('', 'Path to the local action directory', checkActionPath)
+    .description('Run a GitHub Action locally')
+    .argument('', 'Path to the action directory', checkActionPath)
     .argument(
       '',
       'Action entrypoint (relative to the action directory)',
       checkEntrypoint
     )
     .argument('', 'Path to the local .env file', checkDotenvFile)
-    .action(async () => {
-      await action()
+    .option(
+      '--pre 
',
+      'Action pre entrypoint (relative to the action directory)',
+      checkPreEntrypoint,
+      undefined
+    )
+    .option(
+      '--post ',
+      'Action post entrypoint (relative to the action directory)',
+      checkPostEntrypoint,
+      undefined
+    )
+    .action(async (actionPath, entrypoint, dotenvFile, options) => {
+      await action(actionPath, entrypoint, dotenvFile, options)
     })
 
   return program
diff --git a/src/commands/run.ts b/src/commands/run.ts
index 92831b7..cec6c3a 100644
--- a/src/commands/run.ts
+++ b/src/commands/run.ts
@@ -1,6 +1,7 @@
 import { config } from 'dotenv'
 import { createRequire } from 'module'
 import { execSync } from 'node:child_process'
+import path from 'path'
 import * as quibble from 'quibble'
 import { ARTIFACT_STUBS } from '../stubs/artifact/artifact.js'
 import { CORE_STUBS, CoreMeta } from '../stubs/core/core.js'
@@ -8,17 +9,25 @@ import { EnvMeta } from '../stubs/env.js'
 import { Context } from '../stubs/github/context.js'
 import { getOctokit } from '../stubs/github/github.js'
 import type { Action } from '../types.js'
+import { getOSEntrypoints } from '../utils/config.ts'
 import { printTitle } from '../utils/output.js'
 import { isESM } from '../utils/package.js'
 
 const require = createRequire(import.meta.url)
 let needsReplug: boolean = false
 
-export async function action(): Promise {
+export async function action(
+  actionPath: string,
+  entrypoint: string,
+  dotenvFile: string,
+  options: {
+    pre: string | undefined
+    post: string | undefined
+  }
+): Promise {
   const { Chalk } = await import('chalk')
   const chalk = new Chalk()
   const fs = await import('fs')
-  const path = await import('path')
   const YAML = await import('yaml')
 
   CoreMeta.colors = {
@@ -35,28 +44,43 @@ export async function action(): Promise {
   const actionType = isESM() ? 'esm' : 'cjs'
 
   // Output the configuration
-  printTitle(CoreMeta.colors.cyan, 'Configuration')
-  console.log()
-  console.table([
+  const startConfig = [
     {
       Field: 'Action Path',
-      Value: EnvMeta.actionPath
+      Value: actionPath
     },
     {
       Field: 'Entrypoint',
-      Value: EnvMeta.entrypoint
-    },
-    {
-      Field: 'Environment File',
-      Value: EnvMeta.dotenvFile
+      Value: entrypoint
     }
-  ])
+  ]
+
+  if (options.pre)
+    startConfig.push({
+      Field: 'Pre Entrypoint',
+      Value: options.pre
+    })
+
+  if (options.post)
+    startConfig.push({
+      Field: 'Post Entrypoint',
+      Value: options.post
+    })
+
+  startConfig.push({
+    Field: 'Environment File',
+    Value: dotenvFile
+  })
+
+  printTitle(CoreMeta.colors.cyan, 'Configuration')
+  console.log()
+  console.table(startConfig)
   console.log()
 
   // Load the environment file
   // @todo Load this into EnvMeta directly? What about secrets...
   config({
-    path: path.resolve(process.cwd(), EnvMeta.dotenvFile),
+    path: path.resolve(process.cwd(), dotenvFile),
     override: true
   })
 
@@ -117,7 +141,7 @@ export async function action(): Promise {
   }
 
   // Starting at the target action's entrypoint, find the package.json file.
-  const dirs = path.dirname(EnvMeta.entrypoint).split(path.sep)
+  const dirs = path.dirname(entrypoint).split(path.sep)
   let packageJsonPath
   let found = false
 
@@ -155,7 +179,7 @@ export async function action(): Promise {
     process.env.NODE_PACKAGE_MANAGER === 'npm' ||
     (process.env.NODE_PACKAGE_MANAGER === 'pnpm' && actionType === 'cjs') ||
     (process.env.NODE_PACKAGE_MANAGER === 'yarn' &&
-      fs.existsSync(path.join(EnvMeta.actionPath, 'node_modules')))
+      fs.existsSync(path.join(actionPath, 'node_modules')))
   ) {
     /**
      * Get the path in the npm cache for each package.
@@ -274,21 +298,7 @@ export async function action(): Promise {
     }
   }
 
-  console.log('')
-  printTitle(CoreMeta.colors.green, 'Running Action')
-  console.log('')
-
-  // The entrypoint is OS-specific. On Windows, it has to start with a leading
-  // slash, then the drive letter, followed by the rest of the path. In both
-  // cases, the path separators are converted to forward slashes.
-  const osEntrypoint =
-    process.platform !== 'win32'
-      ? path.resolve(EnvMeta.entrypoint)
-      : // On Windows, the entrypoint requires a leading slash if the action is
-        // ESM. Otherwise, it can be omitted.
-        actionType === 'esm'
-        ? '/' + path.resolve(EnvMeta.entrypoint)
-        : path.resolve(EnvMeta.entrypoint.replaceAll(path.sep, '/'))
+  const osEntrypoints = getOSEntrypoints(actionType)
 
   // Stub the `@actions/toolkit` libraries and run the action. Quibble and
   // local-action require a different approach depending on if the called action
@@ -296,7 +306,7 @@ export async function action(): Promise {
   // package is installed.
   if (actionType === 'esm') {
     if (stubs['@actions/github'].base)
-      await quibble.esm(
+      await quibble.default.esm(
         path.resolve(
           stubs['@actions/github'].base,
           ...stubs['@actions/github'].lib
@@ -305,7 +315,7 @@ export async function action(): Promise {
       )
 
     if (stubs['@actions/core'].base)
-      await quibble.esm(
+      await quibble.default.esm(
         path.resolve(
           stubs['@actions/core'].base,
           ...stubs['@actions/core'].lib
@@ -314,7 +324,7 @@ export async function action(): Promise {
       )
 
     if (stubs['@actions/artifact'].base)
-      await quibble.esm(
+      await quibble.default.esm(
         path.resolve(
           stubs['@actions/artifact'].base,
           ...stubs['@actions/artifact'].lib
@@ -322,17 +332,53 @@ export async function action(): Promise {
         stubs['@actions/artifact'].stubs
       )
 
-    // ESM actions need to be imported, not required.
-    const { run } = await import(osEntrypoint)
+    try {
+      if (osEntrypoints.pre) {
+        console.log('')
+        printTitle(CoreMeta.colors.red, 'Running Pre Step')
+        console.log('')
+
+        const { run: pre } = await import(osEntrypoints.pre)
 
-    // Check if the required path is a function.
-    if (typeof run !== 'function')
-      throw new Error(
-        `Entrypoint ${EnvMeta.entrypoint} does not export a run() function`
-      )
+        // Check if the required path is a function.
+        if (typeof pre !== 'function')
+          throw new Error(
+            `Entrypoint ${options.pre} does not export a run() function`
+          )
+
+        await pre()
+      }
+
+      console.log('')
+      printTitle(CoreMeta.colors.green, 'Running Action')
+      console.log('')
+
+      // ESM actions need to be imported, not required.
+      const { run } = await import(osEntrypoints.main)
+
+      // Check if the required path is a function.
+      if (typeof run !== 'function')
+        throw new Error(
+          `Entrypoint ${entrypoint} does not export a run() function`
+        )
 
-    try {
       await run()
+
+      if (osEntrypoints.post) {
+        console.log('')
+        printTitle(CoreMeta.colors.red, 'Running Post Step')
+        console.log('')
+
+        const { run: post } = await import(osEntrypoints.post)
+
+        // Check if the required path is a function.
+        if (typeof post !== 'function')
+          throw new Error(
+            `Entrypoint ${options.post} does not export a run() function`
+          )
+
+        await post()
+      }
     } finally {
       if (process.env.NODE_PACKAGE_MANAGER === 'yarn' && needsReplug)
         replug(fs, packageJsonPath, Object.keys(stubs))
@@ -365,17 +411,55 @@ export async function action(): Promise {
         stubs['@actions/artifact'].stubs
       )
 
-    // CJS actions need to be required, not imported.
-    const { run } = require(osEntrypoint)
+    try {
+      if (osEntrypoints.pre) {
+        console.log('')
+        printTitle(CoreMeta.colors.red, 'Running Pre Step')
+        console.log('')
+
+        // CJS actions need to be required, not imported.
+        const { run: pre } = require(osEntrypoints.pre)
 
-    // Check if the required path is a function.
-    if (typeof run !== 'function')
-      throw new Error(
-        `Entrypoint ${EnvMeta.entrypoint} does not export a run() function`
-      )
+        // Check if the required path is a function.
+        if (typeof pre !== 'function')
+          throw new Error(
+            `Entrypoint ${options.pre} does not export a run() function`
+          )
+
+        await pre()
+      }
+
+      console.log('')
+      printTitle(CoreMeta.colors.green, 'Running Action')
+      console.log('')
+
+      // CJS actions need to be required, not imported.
+      const { run } = require(osEntrypoints.main)
+
+      // Check if the required path is a function.
+      if (typeof run !== 'function')
+        throw new Error(
+          `Entrypoint ${EnvMeta.entrypoint} does not export a run() function`
+        )
 
-    try {
       await run()
+
+      if (osEntrypoints.post) {
+        console.log('')
+        printTitle(CoreMeta.colors.red, 'Running Post Step')
+        console.log('')
+
+        // CJS actions need to be required, not imported.
+        const { run: post } = require(osEntrypoints.post)
+
+        // Check if the required path is a function.
+        if (typeof post !== 'function')
+          throw new Error(
+            `Entrypoint ${options.post} does not export a run() function`
+          )
+
+        await post()
+      }
     } finally {
       if (process.env.NODE_PACKAGE_MANAGER === 'yarn' && needsReplug)
         replug(fs, packageJsonPath, Object.keys(stubs))
diff --git a/src/index.ts b/src/index.ts
index 02250c4..2c3a392 100755
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,9 +3,7 @@ import type { Command } from 'commander'
 import { makeProgram } from './command.js'
 
 /**
- * Runs the CLI program
- *
- * @returns A promise that resolves when the program is finished
+ * Runs the Program
  */
 export async function run(): Promise {
   const chalk = (await import('chalk')).default
diff --git a/src/stubs/env.ts b/src/stubs/env.ts
index ccba562..4c471cd 100644
--- a/src/stubs/env.ts
+++ b/src/stubs/env.ts
@@ -12,11 +12,16 @@ export const EnvMeta: EnvMetadata = {
   env: {},
   inputs: {},
   outputs: {},
-  path: ''
+  path: '',
+  postEntrypoint: undefined,
+  preEntrypoint: undefined
 }
 
 /**
- * Resets the environment metadata
+ * Reset the Environment Metadata
+ *
+ * Simple convenience function to reset the environment metadata back to initial
+ * state.
  */
 export function ResetEnvMetadata(): void {
   EnvMeta.actionFile = ''
@@ -28,4 +33,6 @@ export function ResetEnvMetadata(): void {
   EnvMeta.inputs = {}
   EnvMeta.outputs = {}
   EnvMeta.path = ''
+  EnvMeta.postEntrypoint = undefined
+  EnvMeta.preEntrypoint = undefined
 }
diff --git a/src/types.ts b/src/types.ts
index 7d3e831..8004c70 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -2,62 +2,51 @@ import type { Artifact } from './stubs/artifact/internal/shared/interfaces.js'
 
 /** Environment Metadata */
 export type EnvMetadata = {
-  /** Path to the `action.yml` file */
+  /** Path to `action.yml` */
   actionFile: string
-
-  /** Path to the action directory */
+  /** Path to Action Directory */
   actionPath: string
-
   /** Map of Action Artifacts */
   artifacts: Artifact[]
-
-  /** Path to the `.env` file */
+  /** Path to `.env` */
   dotenvFile: string
-
-  /** Environment variables */
+  /** Environment Variables */
   env: {
     [key: string]: string | undefined
     TZ?: string | undefined
   }
-
-  /** System path */
+  /** System Path */
   path: string
-
-  /** Inputs defined in `action.yml` */
+  /** Inputs in `action.yml` */
   inputs: { [key: string]: Input }
-
-  /** Outputs defined in `action.yml` */
+  /** Outputs in `action.yml` */
   outputs: { [key: string]: Output }
-
-  /** Pre-transpilation entrypoint for the action (e.g. `src/main.ts`) */
+  /** Pre-Transpilation Action `main` Entrypoint (e.g. `src/main.ts`) */
   entrypoint: string
+  /** Pre-Transpilation Action `pre` Entrypoint (e.g. `pre/main.ts`) */
+  preEntrypoint: string | undefined
+  /** Pre-Transpilation Action `post` Entrypoint (e.g. `post/main.ts`) */
+  postEntrypoint: string | undefined
 }
 
 /** Metadata for `@actions/core` */
 export type CoreMetadata = {
-  /** Command echo setting */
+  /** Command Echo Setting */
   echo: boolean
-
-  /** Exit code (0 = success, 1 = failure) */
+  /** Exit Code (0 = success, 1 = failure) */
   exitCode: 0 | 1
-
-  /** Exit message (empty if success) */
+  /** Exit Message (success = empty) */
   exitMessage: string
-
-  /** Outputs set during action invocation */
+  /** Action Outputs */
   outputs: { [key: string]: string }
-
-  /** Secrets registered during action invocation */
+  /** Registered Secrets */
   secrets: string[]
-
-  /** Actions step debug setting */
+  /** Actions Step Debug Enabled */
   stepDebug: boolean
-
-  /** Current action state */
+  /** Action State */
   state: { [key: string]: string }
-
   /**
-   * Colors used to send output to the console
+   * Output Colors
    *
    * This is not part of `@actions/core` but is included here for convenience
    * when calling related functions.
@@ -65,9 +54,8 @@ export type CoreMetadata = {
   colors: {
     [key: string]: (message: string) => void
   }
-
   /**
-   * The path to the step summary output file.
+   * Step Summary Output File Path
    *
    * This is not part of `@actions/core` but is included here for convenience
    * when calling related functions.
@@ -75,65 +63,52 @@ export type CoreMetadata = {
   stepSummaryPath: string
 }
 
-/** Properties of an `action.yml` */
+/** Action Properties */
 export type Action = {
-  /** Name of the action */
+  /** Name */
   name: string
-
-  /** Author of the action */
+  /** Author */
   author?: string
-
-  /** Description of the action */
+  /** Description */
   description: string
-
-  /** Inputs defined in the action */
+  /** Inputs */
   inputs: Record
-
-  /** Outputs defined in the action */
+  /** Outputs */
   outputs: Record
-
-  /** How the action is invoked */
+  /** Entrypoint Configuration */
   runs: Runs
 }
 
-/** Input properties of an `action.yml` */
+/** Input Properties */
 export type Input = {
-  /** Description of the input */
+  /** Description */
   description: string
-
-  /** Whether the input is required */
+  /** Required */
   required?: boolean
-
-  /** Default value of the input */
+  /** Default ValuE */
   default?: string
-
-  /** Deprecation message for the input */
+  /** Deprecation Message */
   deprecationMessage?: string
 }
 
-/** Output properties of an `action.yml` */
+/** Output Properties */
 export type Output = {
   /** Description of the output */
   description: string
 }
 
-/** How the action is invoked */
+/** Entrypoint Properties */
 export type Runs = {
-  /** Type of event */
+  /** Type of Entrypoint */
   using: string
-
-  /** The entrypoint */
+  /** Entrypoint Script */
   main: string
-
-  /** Script to run at the start of a job (before `main`) */
+  /** Pre Script */
   pre?: string
-
-  /** Conditions for the `pre` script to run */
+  /** Pre Script Conditions */
   'pre-if'?: () => boolean
-
-  /** Script to run at the end of a job (after `main`) */
+  /** Post Script */
   post?: string
-
-  /** Conditions for the `post` script to run */
+  /** Post Script Conditions */
   'post-if'?: () => boolean
 }
diff --git a/src/utils/config.ts b/src/utils/config.ts
new file mode 100644
index 0000000..e39fc49
--- /dev/null
+++ b/src/utils/config.ts
@@ -0,0 +1,47 @@
+import path from 'path'
+import { EnvMeta } from '../stubs/env.js'
+
+/**
+ * Get OS Action Entrypoints
+ *
+ * The entrypoint is OS-specific. On Windows, it has to start with a leading
+ * slash, then the drive letter, followed by the rest of the path. In both
+ * cases, the path separators are converted to forward slashes.
+ *
+ * On Windows, the entrypoint requires a leading slash if the action is ESM.
+ * Otherwise, it can be omitted.
+ *
+ * @param actionType Action Type (esm | cjs)
+ * @returns Object with main, pre, and post entrypoints
+ */
+export function getOSEntrypoints(actionType: 'esm' | 'cjs'): {
+  main: string
+  pre: string | undefined
+  post: string | undefined
+} {
+  return {
+    main:
+      process.platform !== 'win32'
+        ? path.resolve(EnvMeta.entrypoint)
+        : /* istanbul ignore next */
+          actionType === 'esm'
+          ? '/' + path.resolve(EnvMeta.entrypoint)
+          : path.resolve(EnvMeta.entrypoint.replaceAll(path.sep, '/')),
+    pre: EnvMeta.preEntrypoint
+      ? process.platform !== 'win32'
+        ? path.resolve(EnvMeta.preEntrypoint)
+        : /* istanbul ignore next */
+          actionType === 'esm'
+          ? '/' + path.resolve(EnvMeta.preEntrypoint)
+          : path.resolve(EnvMeta.preEntrypoint.replaceAll(path.sep, '/'))
+      : undefined,
+    post: EnvMeta.postEntrypoint
+      ? process.platform !== 'win32'
+        ? path.resolve(EnvMeta.postEntrypoint)
+        : /* istanbul ignore next */
+          actionType === 'esm'
+          ? '/' + path.resolve(EnvMeta.postEntrypoint)
+          : path.resolve(EnvMeta.postEntrypoint.replaceAll(path.sep, '/'))
+      : undefined
+  }
+}
diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json
index 494e1e9..8bcad4c 100644
--- a/tsconfig.eslint.json
+++ b/tsconfig.eslint.json
@@ -10,6 +10,7 @@
     "__fixtures__/@octokit/",
     "__fixtures__/crypto.ts",
     "__fixtures__/fs.ts",
+    "__fixtures__/path.ts",
     "__fixtures__/tsconfig-paths.ts",
     "__tests__",
     "src",