feat!: migrate to the node24 runtime#6
Merged
netmenya merged 10 commits intopuzl-cloud:mainfrom Apr 24, 2026
Merged
Conversation
Reflect version `4.0.0` of action and pinned version of `@actions/cache`.
b63ffc8 to
139d0c0
Compare
BREAKING CHANGE: The action now requires the Node.js 24 runtime. Self-hosted runners need GitHub Actions Runner v2.327.1 or later. Migrate the project from CJS/node20 to ESM/node24 to support the node24-native @actions/* 3.x toolkit packages (which are ESM-only). This matches the pattern used by GitHub's official first-party actions (upload-artifact v7, download-artifact v8). Runtime: - Update runs.using from node20 to node24 in all three action.yml files - Bump version from 4.0.0 to 5.0.0 ESM migration: - Add "type": "module" to package.json - Update tsconfig module to NodeNext with NodeNext module resolution - Add .js extensions to all relative imports (required by NodeNext) - Rename jest.config.js to jest.config.cjs (CJS config in ESM project) TypeScript toolchain: - Update target/lib from ES6 to ES2024 (per Node Target Mapping) - Add skipLibCheck for type definition compatibility - Bump typescript from ^5.4.5 to ^5.9.3 (latest 5.x, capped below 6.0 by ts-jest 29.1.2 peer dependency) - Update tsconfig comments with valid values for TS 5.6+ Dependencies: - Bump @actions/core from ^1.10.0 to ^3.0.0 (node24-native, ESM-only) - Bump @actions/glob from 0.1.2 to ^0.6.1 (node24-native, ESM-only) - Bump @types/node from ^20.12.11 to ^24.12.2 - Bump actions/publish-action from v0.3.0 to v0.4.0 (node24-native) - Remove unused dependencies not imported anywhere in source: @actions/cache, @actions/exec, @actions/io, filenamify (this action uses its own puzl.cloud cache implementation) Test infrastructure: - Add tsconfig.test.json with CJS module output so jest.mock() continues to work (Jest's ESM mode breaks jest.mock hoisting) - Add jest-resolver.cjs to resolve ESM-only package exports by stripping the exports field and falling back to the main field - Add babel-jest with @babel/plugin-transform-modules-commonjs and @babel/plugin-transform-export-namespace-from to transform ESM-only @actions/* 3.x packages for Jest's CJS test runner - Add moduleNameMapper to strip .js extensions from relative imports so ts-jest in CJS mode finds the .ts source files - Remove vestigial @actions/cache mock from actionUtils test Linting: - Add eslint-import-resolver-typescript so eslint-plugin-import understands NodeNext .js extension imports - Fix pre-existing prettier formatting issues Dev container: - Update image from 1-22-bookworm to 4-24-bookworm Build: ncc auto-detects "type": "module" and bundles in ESM mode, resolving @actions/* 3.x exports via the "import" condition.
Update all version references in code examples: - puzl-cloud/github-actions-cache@v4 -> @v5 - puzl-cloud/github-actions-cache/restore@v4 -> /restore@v5 - puzl-cloud/github-actions-cache/save@v4 -> /save@v5 - puzl-cloud/github-actions-cache@v3 -> @v5 (stale reference) - actions/checkout@v4 -> @v6 in example workflows
- actions/checkout v4 -> v6 (first version with native node24) - actions/setup-node v4 -> v6 - actions/upload-artifact v4 -> v7 - Node.js setup version 20.x -> 24.x in static-tests and check-dist
Rebuild all four ncc bundles (restore, save, save-only, restore-only) with the updated ESM toolchain and node24-native @actions/* 3.x dependencies. ncc now outputs ESM (import/export) instead of CJS (require/module.exports), matching the "type": "module" declaration. Each dist/ directory also gains a package.json generated by ncc to declare the bundle's module type.
139d0c0 to
c58e4c4
Compare
Member
|
@LayZeeDK Thanks for the PR! Could you please run |
…er's npm Running npm install inside the dev container introduced in the previous commits (mcr.microsoft.com/devcontainers/typescript-node: 4-24-bookworm, which ships Node 24 and npm 11.9) regenerates the lockfile with 12 redundant "peer": true markers omitted. npm 11.9's resolver does not write "peer": true on entries that are already listed in devDependencies, where older npm versions (including the 11.6 shipped by some host environments) still do. Both lockfile variants are semantically equivalent -- same resolved versions, same integrity hashes, same resolution graph -- but they differ in presence of those flags. Commit the regenerated form now, so the next time a contributor runs npm install inside the dev container they do not end up with a dirty working tree. This is purely a cosmetic stabilization: the lockfile may still flap back if npm install is later run under an older npm (e.g. on a host or CI runner that has not caught up to 11.9), but it will stop flapping on every dev container rebuild.
Running `licensed cache` brings the .licenses/ records in sync with the current package-lock.json after the Node 24 / ESM migration (d1028cf): - 65 dependency records removed for packages no longer in the tree. The old action pulled @actions/cache, which transitively depended on @azure/storage-blob and its full Azure SDK cascade (@azure/core-*, @azure/ms-rest-js, @azure/abort-controller), plus @opentelemetry/api, @bufbuild/*, @protobuf-ts/*, node-fetch, and related transitive deps. The new action uses @actions/core and @actions/glob only, so all of the above dropped out. - 2 records refreshed for @actions/core and @actions/glob, which were bumped to new major versions in the migration. These were the two records licensed status reported as "cached dependency record out of date" on this branch until the cache was regenerated.
Switch from the binary tarball download to ruby/setup-ruby + gem install. The upstream project (licensee/licensed) only ever published linux-x64 and darwin-x64 tarballs for 2.12.2, and stopped publishing binary tarballs entirely after 4.5.0 (August 2024) -- the tarball approach is a dead-end for non-amd64 runners and for any future version bump. Ruby is pinned to 3.3 via ruby/setup-ruby@v1, matching the dev container (which pins the same version via the Ruby dev container feature). This alignment prevents .licenses/ YAML drift between CI regenerations and contributor regenerations that would otherwise occur when the two environments resolve different Ruby minors from their respective "default" sources. 3.3 specifically is the newest Ruby where csv is still a default gem. Ruby 3.4 moved csv to bundled gems, which breaks licensed 2.12.2's implicit require 'csv' in sources/gradle.rb unless csv is installed alongside. Staying on 3.3 keeps the install a single gem install. build-essential and cmake are installed via apt because neither the runner baseline (confirmed via `act` against catthehacker/ubuntu: act-latest) nor ruby/setup-ruby provides them. They are required to compile the rugged native extension (libgit2 bindings) pulled in transitively via the licensee gem. pkg-config and libssl-dev are already present on any reasonable Ubuntu runner and are not reinstalled. gem install runs without sudo because ruby/setup-ruby installs Ruby to a user-local hostedtoolcache path and prepends its bin directory to PATH; sudo gem install would reset PATH and fail to find gem. Version pinned to 2.12.2 to keep the existing .licenses/ metadata in sync with the tool version.
On some Docker Desktop setups (observed on Windows arm64 with a Dev Drive workspace), the host-to-container bind mount arrives with an unexpected owner (e.g. UID 1001, GID 116) and mode 700 on the mount root. The container's node user (UID 1000) then lacks traverse permission on the workspace directory, so even though files inside are world-readable, they cannot be reached. This manifests as npm install failing with a cryptic error: npm error code ERR_INVALID_ARG_TYPE npm error The "path" argument must be of type string or an instance of Buffer or URL. Received null The null comes from npm walking up from cwd looking for package.json and hitting a directory it cannot stat; it then calls path.resolve on an unresolved value. Running sudo chown -R node:node . at the very start of postCreateCommand re-owns the mount so the node user can read, write, and traverse the workspace. The fix is a no-op on hosts where the UID already matches (typical Linux hosts with host UID 1000), and safe on all bind-mount backends tested (grpc-fuse on Windows Docker Desktop, overlay on native Linux). The recursive flag is kept for portability: on grpc-fuse (Windows, some macOS configurations) chown at the mount root propagates to all entries automatically, but on native Linux bind mounts only the target inode is affected, so contained files would keep their original UIDs without -R.
Adds the same licensed version CI uses (2.12.2) to the dev container, so contributors can reproduce the licensed status check locally and run licensed cache to regenerate .licenses/ metadata before pushing. A binary-tarball approach was considered but abandoned: licensed 2.12.2 only ships linux-x64 and darwin-x64 release assets (no linux-arm64), and the upstream project stopped publishing binary tarballs entirely after 4.5.0, so the tarball path is a dead-end for arm64 hosts and for future version bumps. The gem-based install works on both linux/amd64 and linux/arm64 containers. Two dev container features supply the tooling neither the base image (mcr.microsoft.com/devcontainers/typescript-node:4-24-bookworm) nor the apt step provides: - ghcr.io/devcontainers/features/ruby:1 pinned to 3.3. The version is explicit because 3.3 is the newest Ruby where csv is still a default gem; Ruby 3.4 moved csv to bundled gems, which breaks licensed 2.12.2's implicit require 'csv' in sources/gradle.rb. Staying on 3.3 keeps the install a single gem install, no csv workaround needed. This version also matches the Ruby pinned on the CI side via ruby/setup-ruby@v1, so .licenses/ regenerations produce identical YAML on both ends. - ghcr.io/devcontainers-extra/features/cmake:1. cmake is required to compile libgit2 for the rugged gem (pulled in transitively via the licensee gem), and the base image does not ship it. The community cmake feature installs it declaratively alongside the Ruby feature, keeping the apt step minimal. The apt step now only installs pigz, which is needed at runtime by the action itself (src/constants.ts defines TAR_COMMAND = "tar -I pigz") and by the integration tests. gem install runs without sudo because the Ruby feature installs into a user-accessible RVM path; sudo would reset PATH and the gem shim would not be found. --no-document skips rdoc/ri generation to keep postCreateCommand fast. Version pinned to 2.12.2 to stay in sync with the existing .licenses/ metadata and the CI workflow.
netmenya
approved these changes
Apr 23, 2026
sergeimonakhov
approved these changes
Apr 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5.
Description
Migrate the action from the deprecated
node20runtime tonode24, and from CJS to ESM. Also refresh the license compliance tooling (theLicensedCI workflow and the dev container's license tooling) so both stay green against the new dependency tree.This is a major version bump (
v4->v5) because the runtime change is breaking: self-hosted runners need GitHub Actions Runner v2.327.1 or later.What changed
Runtime & version
runs.usingupdated fromnode20tonode24in all threeaction.ymlfiles4.0.0to5.0.0ESM migration (matching the pattern used by GitHub's official actions:
upload-artifactv7,download-artifactv8)"type": "module"topackage.jsontsconfig.jsonmodule toNodeNextwithNodeNextmodule resolution.jsextensions to all relative imports (required byNodeNext)jest.config.jstojest.config.cjsDependencies
@actions/core^1.10.0->^3.0.0(node24-native, ESM-only)@actions/glob0.1.2->^0.6.1(node24-native, ESM-only)@types/node^20.12.11->^24.12.2typescript^5.4.5->^5.9.3(latest 5.x, capped below 6.0 byts-jestpeer dependency)actions/publish-actionv0.3.0->v0.4.0(node24-native)@actions/cache,@actions/exec,@actions/io,filenamify(not imported anywhere in source -- this action uses its own puzl.cloud cache implementation)package-lock.jsonunder the dev container's npm 11.9 to drop 12 redundant"peer": truemarkers; stabilizes the lockfile so contributor rebuilds don't leave a dirty working treeTypeScript toolchain
ES6toES2024(per Node Target Mapping)skipLibCheckfor type definition compatibilityTest infrastructure
tsconfig.test.jsonwith CJS module output sojest.mock()continues to workjest-resolver.cjsto resolve ESM-only package exports viamainfield fallbackbabel-jestwith@babel/plugin-transform-modules-commonjsand@babel/plugin-transform-export-namespace-fromto transform ESM@actions/*packages for Jesteslint-import-resolver-typescriptsoeslint-plugin-importunderstands.jsextension importsCI workflows
actions/checkoutv4 -> v6actions/setup-nodev4 -> v6actions/upload-artifactv4 -> v720.x->24.xDev container
npm installto fail withERR_INVALID_ARG_TYPE / Received nullon some Docker Desktop setups (observed on Windows arm64 with Dev Drive, where the bind mount arrives owned by UID 1001 with mode 700).sudo chown -R node:node .at the start ofpostCreateCommandre-owns the mount so thenodeuser can traverse the workspace; it is a no-op on hosts where the UID already matchesghcr.io/devcontainers/features/ruby:1and cmake viaghcr.io/devcontainers-extra/features/cmake:1so contributors can runlicensed status/licensed cachelocally against the same tool version CI uses. cmake is required to compile libgit2 for theruggednative extension pulled in transitively via thelicenseegemLicense compliance tooling
.licenses/cache for the post-Node 24 dependency tree: 65 dependency records removed (the old@actions/cache/@azure/storage-blob/@azure/core-*/@opentelemetry/@bufbuild/@protobuf-ts/node-fetchcascade that dropped out during the migration), and 2 records refreshed for the@actions/coreand@actions/globmajor bumps.github/workflows/licensed.ymlswitched from a binary tarball download toruby/setup-ruby@v1+gem install licensed:2.12.2. Upstreamlicensee/licensedstopped publishing binary release tarballs after 4.5.0 (August 2024), and 2.12.2 itself only shippedlinux-x64anddarwin-x64binaries -- the tarball path is a dead-end for non-amd64 runners and for any future version bumpruby/setup-ruby@v1) and the dev container (via the Ruby feature). 3.3 is the newest Ruby wherecsvis still a default gem; on Ruby 3.4+csvwas moved to bundled gems, which would breaklicensed 2.12.2's implicitrequire 'csv'inlib/licensed/sources/gradle.rbwithout an explicitgem install csv. Pinning both ends to the same Ruby version also prevents subtle.licenses/YAML drift between contributor-generated caches and CI-generated cacheslicenseditself still pinned to 2.12.2 to keep the existing.licenses/metadata in sync with the tool version; a future commit could upgrade tolicensed5.x (which declarescsvas a runtime dep explicitly), but that is a separate concernMotivation and Context
The
node20runtime is deprecated. On June 2nd, 2026, GitHub Actions runners will default tonode24. See Deprecation of Node 20 on GitHub Actions runners.The
@actions/*3.x toolkit packages are ESM-only, requiring the project to migrate from CJS to ESM. This is the same approach used by GitHub's own first-party actions.How Has This Been Tested?
Dev container (
mcr.microsoft.com/devcontainers/typescript-node:4-24-bookworm)npm run format-check-- all files pass Prettiernpm run lint-- all files pass ESLint (witheslint-import-resolver-typescriptfor.jsimports)npm run build-- TypeScript 5.9.3 compiles and ncc bundles all four entry points in ESM modenpm run test-- 79/79 tests pass (all 8 suites), including integration testsnpm run build && git diff dist/-- dist matches committed bundleslicensed status-- 3 dependencies checked, 0 errors found (after cache regeneration)Local workflow testing with
nektos/actv0.2.84 (platform:catthehacker/ubuntu:act-latest)check-distjob: passed -- build output matches committed diststatic-testsjob: 73/79 tests pass (6 integration test failures due to missingpigzon act image -- these pass in the dev container)Licensedjob: passed (with--container-architecture linux/amd64so the emulated runner sees an x86-64 shell) -- 3 dependencies checked, 0 errors found. Also verified at the intermediate commit where only the cache regen had landed but the CI still used the original tarball install, to confirm the 4-commit license-tooling series is bisect-safeFORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true-- identical results, confirming the env var is a no-op since the action already declaresnode24E2E cache round-trip with
nektos/acton Windows 11 arm64 (Docker Desktop, arm64-native, no QEMU emulation)catthehacker/ubuntu:act-latestwithpigzandtreeinstalled, plus a host bind mount (-v <host>/.act-cache/puzl:/.puzl) to simulate the persistent/.puzlvolume thatpuzl-ubuntu-latestprovides in production. Jobs invoked with-P puzl-ubuntu-latest=act-puzl:latest --pull=falsetest-save: passed -- all 6 cache entries written as pigz-compressed tarballs under/.puzl/cache/test-Linux-1/and persisted to the host bind mount. Covers the threepathshapes from the workflow: working-directory path (test-cache), outside-workdir path (~/test-cache), and./test/**/distglob match. Save completed withCache saved with key: test-Linux-1test-restore: passed -- all 6 entries restored from the bind-mounted cache across a separateactinvocation (i.e. a separate container lifecycle, matching the cross-job semantics of the real runner). Everyverify-cache-files.shstep confirmed aFile content: Linux 1round-trip for all three path shapes, andcache-hit=truewas emittedtest-restoredeclaresneeds: test-save, act re-rantest-saveagainst the already-populated cache, and the second save short-circuited withCache hit occurred on the primary key test-Linux-1, not saving cache.-- matching production behaviourTypes of changes
Checklist:
CONTRIBUTINGdocument exists in this repository.