Skip to content

migrate to ESM and upgrade @actions/* to latest majors - #21

Merged
BrandonLWhite merged 7 commits into
mainfrom
chore/esm-migration-actions-core-v3
Aug 13, 2026
Merged

migrate to ESM and upgrade @actions/* to latest majors#21
BrandonLWhite merged 7 commits into
mainfrom
chore/esm-migration-actions-core-v3

Conversation

@BrandonLWhite

@BrandonLWhite BrandonLWhite commented Aug 13, 2026

Copy link
Copy Markdown
Member

Summary:

Upgrades @actions/core to 3.0.1, which is ESM-only, and migrates the
repository from CommonJS to native ESM to make that possible.
@actions/cache and @actions/exec move to 6.2.0 and 3.0.0 in the same
change because they are ESM-only at those majors too, and because
@actions/cache@5 pins @actions/core@^2 — bumping core alone would
resolve a second, nested CommonJS copy of core into the bundle. This lifts
the constraint that AGENTS.md and .agents/INSTRUCTIONS.md previously
carried as the repository's central hard rule.

The action's workflow interface is untouched: action.yml inputs are
byte-identical, runs.using stays node24, and observable behavior is
unchanged.

Actions:

  • Bumped @actions/core 2.0.3 to 3.0.1, @actions/cache 5.2.0 to 6.2.0,
    and @actions/exec 2.0.0 to 3.0.0, and set "type": "module".
  • Converted src/ and __tests__/ to import/export, node: builtin
    specifiers, and explicit .js extensions on relative imports.
  • Replaced the suite's jest.spyOn(module, 'export') mocking, which frozen
    ESM namespaces make impossible, with jest.unstable_mockModule against
    new fixtures in __fixtures__/. Two fixtures are deliberately partial
    rather than pure stubs, because mocking a module replaces it for every
    consumer: core.js re-exports the real @actions/core since
    @actions/cache imports named exports from it (setSecret among them),
    and fs-promises.js keeps readFile real so the TOML fixtures still
    load while mocking only symlink and stat.
  • Dropped the jest.spyOn(main, 'run') self-spy, which ESM makes
    impossible and which only asserted that run reached its end.
  • Replaced @vercel/ncc with rollup, matching the upstream
    actions/javascript-action template. Three deviations from that template:
    @rollup/plugin-json is required here because @actions/cache imports
    its own package.json, and sourcemaps are off because the map is ~16.5MB
    and dist/ is committed.
  • Set commonjs({ ignoreTryCatch: false }), which is mandatory rather than
    stylistic. Several bundled dependencies probe for optional modules with
    require() inside a try/catch (minimatch resolves path that way; undici
    probes node:http2 and node:crypto). The plugin leaves those alone by
    default, and in an ESM bundle require is undefined, so the throw is
    swallowed and each dependency silently takes a degraded fallback. minimatch
    fell back to sep: '/', which stopped Windows paths from matching and broke
    @actions/cache's saveCache on windows-latest only. This was a
    regression from ncc, not from the @actions/cache major — resolvePaths is
    byte-identical between cache 5.2.0 and 6.2.0 and minimatch is 3.1.5 in both
    trees. Added __tests__/dist.test.js to assert the committed bundle has no
    executable bare require(, and verified it fails without the fix.
  • Kept dist/licenses.txt via rollup-plugin-license, attributing 43
    bundled packages. The bundle inlines the whole production tree, including
    the Azure Storage SDK that @actions/cache@6 adds.
  • Extracted Jest config into jest.config.js with transform disabled,
    and removed four dead Babel devDependencies that had no config anywhere
    in the repository.
  • Rewrote the obsolete "do not bump @actions/* past their CommonJS
    majors" rule in AGENTS.md and .agents/INSTRUCTIONS.md into ESM
    guidance, and recorded the design under docs/superpowers/specs/.
  • Verified the cache save path deliberately, because a green CI run does not
    exercise it: both OS jobs normally restore from an existing key and skip
    saveCache, so the Windows bug was invisible. Forcing a miss by salting the
    cache key on a throwaway branch reproduced the failure, and after the fix
    windows-latest wrote all three caches (13.19 MB poetry, 6.99 MB tox, 531 KB
    poethepoet). Both pitfalls are now documented in .agents/INSTRUCTIONS.md.
  • Verified beyond a green build: 10 tests pass with coverage unchanged at
    100% statements/lines/functions and 92.85% branches, a clean npm ci
    reports 0 vulnerabilities, the committed dist/ matches a fresh build,
    and the bundle was executed directly to confirm it loads as ESM —
    printing Nothing to install. and exiting 0 for the empty-pyproject
    fixture, and emitting ::error:: with exit 1 for a missing file.

BREAKING CHANGE: The repository's source and its bundled dist/index.js are
now ESM rather than CommonJS. This does not change the action's workflow
interface — action.yml inputs, runs.using: node24, and runtime behavior
are all unchanged, so no consumer workflow needs updating. It does break
forks, patches, or tooling that expect require()/module.exports in
src/ or dist/index.js.

BrandonLWhite and others added 4 commits August 13, 2026 16:06
Records the design for upgrading @actions/core to 3.x and migrating the
codebase from CommonJS to ESM, which the upgrade forces.

Covers the atomic three-package bump (core/cache/exec), the src/ and
__tests__/ conversion, replacing @vercel/ncc with rollup, and the
documentation constraints this inverts in AGENTS.md and
.agents/INSTRUCTIONS.md.

Also gitignores docs/superpowers/plans/, matching the convention that
plans are ephemeral working docs while specs are preserved in git.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@actions/core 3.x, @actions/cache 6.x, and @actions/exec 3.x are ESM-only,
so require() of them throws at runtime. Migrate src/ and __tests__/ to
native ESM and bump all three together — @actions/cache@5 pins
@actions/core@^2, so bumping core alone resolves a second nested CommonJS
copy of core into the bundle.

ESM module namespace objects are frozen, so the test suite's
jest.spyOn(module, 'export') mocking no longer works. Tests now substitute
modules with jest.unstable_mockModule against fixtures in __fixtures__/.

Two fixtures are deliberately partial rather than pure stubs. Mocking a
module replaces it for every consumer, not just for the code under test:
__fixtures__/core.js re-exports the real @actions/core and overrides only
the asserted functions, because @actions/cache imports named exports from
it (setSecret among them) and a narrower mock makes main.test.js fail to
load. __fixtures__/fs-promises.js does the same to keep readFile real so
the TOML fixtures still load, mocking only symlink and stat.

Drops the jest.spyOn(main, 'run') self-spy from main.test.js, which ESM
makes impossible and which only asserted that run reached its end.
Coverage is unchanged: 9 tests, 100% statements/lines/functions, 92.85%
branches.

Verified the bundle loads and runs as ESM, not just that it builds:
  env 'INPUT_INSTALL-CONFIG-FILE=__tests__/data/pyproject.empty.toml' \
    node dist/index.js
prints "Nothing to install." and exits 0; a missing file emits ::error::
and exits 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aligns the bundler with the upstream actions/javascript-action template
this repository derives from, and drops a largely dormant dependency. The
bundle shrinks from ~3.4MB to ~2.9MB.

Two deliberate deviations from the upstream config:

- @rollup/plugin-json is required. It is absent upstream, where the only
  dependency is @actions/core. Here @actions/cache imports its own
  package.json and the build fails outright without it.
- sourcemap is disabled. The generated map is ~16.5MB, and because dist/
  is committed that would land in git history on every dependency bump
  and bury the dist/ diff check-dist.yml reports.

rollup-plugin-license replaces ncc's --license flag, attributing 43
bundled packages (all MIT/ISC/Apache-2.0/BSD-3-Clause, matching the
allowed list in .licensed.yml). The bundle inlines the whole production
dependency tree, including the Azure Storage SDK @actions/cache@6 adds,
so those notices have to ship with it.

Removes dist/package.json, which only existed because ncc emits a
{"type":"module"} marker alongside an ESM bundle. dist/ is back to
exactly index.js and licenses.txt.

Verified the rollup bundle at runtime, not just at build time: the empty
pyproject fixture prints "Nothing to install." and exits 0, a missing file
emits ::error:: and exits 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moves Jest configuration out of package.json into jest.config.js and sets
transform: {}, which is correct for native ESM and means babel-jest is
never invoked.

Removes @babel/core, @babel/eslint-parser, @babel/preset-env, and
babel-preset-jest. There is no babel configuration anywhere in this
repository, so these have been inert since the original template
scaffold — applying the repository's own standing rule against carrying
unused dependencies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@BrandonLWhite BrandonLWhite added the ai-assisted This PR contains AI-generated commits label Aug 13, 2026
Fixes a silent bundling bug that broke @actions/cache's saveCache on
windows-latest only.

Several bundled dependencies probe for optional modules with require()
inside a try/catch: minimatch resolves `path` that way, and undici probes
node:http2 and node:crypto. @rollup/plugin-commonjs leaves requires inside
try/catch untouched by default, and in an ESM bundle `require` is
undefined, so the ReferenceError was swallowed by the empty catch and each
dependency silently took its fallback path. No error, no failed build.

For minimatch the fallback is `{ sep: '/' }`. minimatch only rewrites
backslashes to forward slashes when sep !== '/', so on Windows the file
paths @actions/glob feeds it were never normalized and stopped matching the
pattern. glob returned zero matches, and @actions/cache reported "Path
Validation Error: Path(s) specified in the action for caching do(es) not
exist" for C:\Program Files (x86)\pipx\venvs\poetry. Linux was unaffected
because sep is '/' there anyway.

This was a bundler regression from ncc, not from the @actions/cache major:
resolvePaths is byte-identical between cache 5.2.0 and 6.2.0, and minimatch
is 3.1.5 in both trees. ncc/webpack rewrites try/catch requires into its own
module registry, so the bundle it produced worked.

The bug was only reachable on a cache miss, which is why CI hid it — both
OS jobs normally restore from an existing key and skip saveCache. Confirmed
by salting the cache key on a throwaway branch to force a miss: windows
failed and ubuntu saved all three packages.

Adds __tests__/dist.test.js, which asserts the committed bundle contains no
executable bare require(). Verified it fails without this fix, catching all
six real offenders. Two documented pitfalls added to .agents/INSTRUCTIONS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BrandonLWhite and others added 2 commits August 13, 2026 16:59
super-linter's NATURAL_LANGUAGE check flags "builtins" and "sourcemap" via
its terminology rules. Uses "built-ins", "source map", and "source maps" in
prose; occurrences inside code blocks are left alone since they are literal
identifiers (preferBuiltins, sourcemap: false).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design listed two deviations from the upstream rollup config; there are
three. ignoreTryCatch: false is mandatory, not stylistic, and was found
during implementation rather than design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@BrandonLWhite
BrandonLWhite force-pushed the chore/esm-migration-actions-core-v3 branch from f599921 to 80e3808 Compare August 13, 2026 22:04
@BrandonLWhite
BrandonLWhite marked this pull request as ready for review August 13, 2026 22:27
@BrandonLWhite BrandonLWhite changed the title build!: migrate to ESM and upgrade @actions/* to latest majors migrate to ESM and upgrade @actions/* to latest majors Aug 13, 2026
@BrandonLWhite
BrandonLWhite merged commit 1cca596 into main Aug 13, 2026
21 checks passed
@BrandonLWhite
BrandonLWhite deleted the chore/esm-migration-actions-core-v3 branch August 13, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted This PR contains AI-generated commits

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant