Skip to content

chore!: migrate to pnpm, upgrade dependencies, hash-pin GitHub Actions - #43

Open
jehannes wants to merge 4 commits into
mainfrom
version-and-workflow-upadate-09102026
Open

jehannes wants to merge 4 commits into
mainfrom
version-and-workflow-upadate-09102026

Conversation

@jehannes

@jehannes jehannes commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Release 0.4.0.

Package manager

  • Migrate npm -> pnpm 11.25.0, pinned via packageManager. package-lock.json is replaced by pnpm-lock.yaml.
  • pnpm-workspace.yaml sets nodeLinker: hoisted. This is required, not a preference: the package publishes bundleDependencies (the runtime deps that aws_lambda_nodejs re-bundles at the consumer's synth time), and pnpm refuses to pack those under its default symlinked layout, failing with BUNDLED_DEPENDENCIES_WITHOUT_HOISTED. hoisted also produces the same flat, real node_modules npm does, so npm pack/publish stays usable as a fallback.
  • Rule on dependency build scripts via allowBuilds: esbuild allowed (it bundles the Lambda handlers), unrs-resolver and @parcel/watcher denied. Note that onlyBuiltDependencies was removed in pnpm 11 and is now silently inert.
  • Move the npm overrides into pnpm-workspace.yaml, replacing the deprecated "$jest" reference with an explicit version.

Dependencies

  • Upgrade the runtime and dev dependency set. Publishing now targets the public registry explicitly via publishConfig.
  • Hold typescript at ~6.0.x and move jsii to ~6.0.12. jsii's major.minor tracks the TypeScript compiler it wraps, so jsii@6.0 IS the TypeScript 6.0 line. This also resolves a pre-existing mismatch, since jsii ^5.9 wants TS ~5.9 while the repo already pinned TS ~6.0.
  • Hold aws-cdk on 2.x. aws-cdk 3.0.0 is an accidental publish that AWS deprecated ("published accidentally, please use 2.x.x instead") but which still holds the "latest" dist-tag, so upgrade tooling will offer it.
  • Keep the aws-cdk-lib and constructs devDependencies at the floor of their peerDependencies ranges. jsii requires this and warns otherwise (JSII6, metadata/missing-dev-dependency); compiling against the oldest supported version is what keeps those ranges honest.

Workflows

  • Hash-pin every action to a full commit SHA, matching the convention already used elsewhere in alliander-opensource.
  • Pin CI to Node 24: jsii supports ^20, ^22 and ^24 only.
  • Run lint, test and build on pull requests, and declare least-privilege permissions.
  • Keep the job named "pre-commit": branch protection on main requires a status check with exactly that name.

Docs

  • Correct the install instructions, which pointed at GitHub Packages although the package is published to npmjs, and fix the WafUsage enum casing in the usage example.
  • Move development documentation to CONTRIBUTING.md. jsii embeds README.md into the .jsii manifest, which is published and rendered on Construct Hub, so contributor-only detail does not belong there.

@jehannes
jehannes force-pushed the version-and-workflow-upadate-09102026 branch 4 times, most recently from 9afc346 to 4a637b1 Compare September 10, 2026 08:47

@MasselinkJ MasselinkJ left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Kiro]: Reviewed by cloning the branch, running pnpm install --frozen-lockfile / lint / test / build locally, and diffing the approach against Alliander/cp-aws-sso-config, Alliander/cp-gh-actions-ami-builder and the shared cp-gh-shared-actions they consume.

First, the parts I checked that hold up, because this PR makes a lot of specific claims and most of them are accurate:

  • All the version pins resolve. aws-cdk@3.0.0 really is deprecated with exactly the quoted message and really does still hold the latest dist-tag.
  • jsii 6.0.12 depends on typescript: ~6.0 and jsii 5.9.53 on ~5.9, so both the version-coupling argument and the pre-existing mismatch are real.
  • The bundled @jsii/check-node@1.140.0 lists 20/22/24 as supported. A real build on Node 26 printed exactly the predicted warning (^24.0.0, ^22.0.0, ^20.0.0 [DEPRECATED]), so pinning CI to 24 is right.
  • nodeLinker: hoisted genuinely is required for bundleDependencies; the BUNDLED_DEPENDENCIES_WITHOUT_HOISTED guard is real, not a preference.
  • The minimumReleaseAge reasoning is correct for pnpm 11.25.0. Full lockfile re-validation landed in 11.1.3 (pnpm#10438, fixed by pnpm#11583), and my install printed ✓ Lockfile passes supply-chain policies (462 entries in 16.4s). So the 3-days-not-7 rationale is verified rather than assumed.
  • pnpm does implement OIDC trusted publishing natively, so NODE_AUTH_TOKEN: '' plus id-token: write is a valid setup even though pnpm 11 dropped the npm CLI delegation. It is absent from the pnpm publish docs, so a --dry-run on the first release would be worth it.
  • The lockfile carries no registry URLs, so "don't commit a registry" really does avoid churn between the Artifactory and public paths.
  • Both README fixes match the source, and dropping npx from build.sh is a real improvement.
  • Install, test and build all pass: 40/40 tests, 100% coverage, jsii 0 errors / 0 warnings.

Four things to look at, three inline below plus this one.

Renovate / Dependabot

There is no renovate.json or .github/dependabot.yml in the repo. Both reference repos ship renovate.json, and this PR makes the gap matter more than it did before:

  • Every action is now hash-pinned, which is the right call, but hash pins do not update themselves. Without update tooling they rot silently and the repo drifts onto old action versions with no signal.
  • Because minimumReleaseAge is now set explicitly, minimumReleaseAgeStrict auto-enables (pnpm 11.1.3+). In CI, which is non-TTY, an immature pick aborts with ERR_PNPM_NO_MATURE_MATCHING_VERSION rather than auto-excluding. So a bot bumping a dependency inside the 3-day window produces a hard CI failure, and whoever picks it up needs to know that minimumReleaseAgeExclude is the intended escape hatch and not to lower the window.
  • minimumReleaseAge also interacts with bot scheduling: Renovate's default is to open PRs as soon as a release appears, which is inside the cooldown by construction. Worth pairing the config with minimumReleaseAge in Renovate itself so the two agree, otherwise most bot PRs land red.

Copying the reference repos' renovate.json and adding a note in CONTRIBUTING.md about the cooldown interaction would close this. Fine as a follow-up rather than a blocker, as long as it does not get lost.

One unrelated note: the commit message is better than the PR description. It has a whole "Registry" section explaining the fallback design and the minimumReleaseAge choice that never made it into the PR body, and that is the context a reviewer needs most.

Comment thread .github/workflows/pr.yml Outdated
# protection rule first if you restructure this.
pre-commit:
runs-on: ubuntu-latest
runs-on: self-hosted

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Kiro]: self-hosted on a public repo with a pull_request trigger.

The two repos this was modelled on are both private (I confirmed via the API); aws-jwt-sts is public. That is the case GitHub explicitly advises against: forks of a public repo can run code on your runner just by opening a pull request. It is also a regression, since this job ran on ubuntu-latest before this PR.

What makes it concrete rather than theoretical is that a fork controls the very files doing the hardening. pnpm-workspace.yaml is checked out from the PR head, so a fork can set allowBuilds: {anything: true} or dangerouslyAllowAllBuilds, and pnpm install --frozen-lockfile then runs their build scripts on Alliander infrastructure. The supply-chain hardening in this PR protects maintainers installing locally; on the fork path the attacker supplies the policy, so it protects nothing there.

It compounds with build-and-publish.yml, which also runs self-hosted and holds id-token: write for npm trusted publishing. Fork PR code and the job that can publish @alliander-opensource/aws-jwt-sts share a runner pool, so anything a PR leaves behind in $HOME, the tool cache, or on PATH is reachable by the release job.

Suggest ubuntu-latest here at minimum, and preferably for the release job too. Once the Artifactory branch is settled (see my other comment) nothing in either job needs Alliander-internal network access, so there is no reason to be on self-hosted. If self-hosted has to stay, it needs ephemeral runners plus "Require approval for all outside collaborators" on fork PRs. I could not check that setting myself, the Actions permissions API returns 403 for my token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dit komt vanuit max-glaser maar bleek niet te kloppen

@jehannes jehannes Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[kiro]: Agreed, and changed in c24117f - both pr.yml and build-and-publish.yml are back on ubuntu-latest.

One thing to add, because it makes this more than a hardening argument: it was also non-functional. GET /repos/.../actions/runners returns total_count: 0, and the pre-commit job on this branch sat queued for five hours with nothing picking it up. Since branch protection requires a check named exactly pre-commit, that check could never report and the PR could not have merged. Every earlier run on the branch is cancelled or queued, none completed.

So the fork-PR exposure you described was the right call, and the practical effect was simply that CI never ran.

Your point about the attacker supplying the policy is the load-bearing one: pnpm-workspace.yaml is checked out from the PR head, so a fork controls allowBuilds, which is what makes pnpm install --frozen-lockfile on our infrastructure a concrete concern rather than a theoretical one. The release job carries the separate reason in a comment at the top of its job block, since it holds id-token: write and must not share a pool with fork-supplied code.

Two caveats, both of which now live only in this thread:

  • GitHub-hosted runners still need enabling at the organisation level for this repository. Until they are, the job stays queued, which looks identical to a hung build.
  • I could not verify the "Require approval for all outside collaborators" setting either, since the Actions permissions API returns 403 for my token too. Moot while both jobs are hosted, but worth confirming if self-hosted ever comes back.

Correction to an earlier version of this reply. It said both of those were recorded as comments in pr.yml. They were, in c24117f, but 4541611 trimmed the workflow comments back and they are gone. The reasoning survives here and in CONTRIBUTING.md; the org-level runner caveat does not appear in pr.yml any more, so if that check is ever silently not running, this thread is the only pointer.

Comment thread .github/workflows/pr.yml Outdated
# authenticated before it runs.
- name: select npm registry
env:
ARTIFACTORY_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Kiro]: Long-lived secret where the org pattern uses a short-lived OIDC token.

Credit first: this select npm registry step is a faithful reproduction of the "Configure npm" step in cp-gh-shared-actions/actions/setup-artifactory — same ~/.npmrc layout, same ${NODE_AUTH_TOKEN} indirection so the token is not written into the file, same umask 077, and the same ordering constraint ahead of pnpm/action-setup because its self-installer fetches @pnpm/exe first. That reasoning is all correct.

What did not carry over is the credential model. The shared action mints a short-lived token through Alliander/get-artifactory-credentials over OIDC, and describes itself as "the single place that obtains Artifactory credentials". This step substitutes a long-lived static secret, on a public repository, and exports it via $GITHUB_ENV — which puts it in the environment of every later step: pnpm install, lint, test, build, including esbuild's postinstall that allowBuilds deliberately permits.

The stated reason for inlining is accurate: GitHub does not let a public repo consume actions from private or internal repos, and both cp-gh-shared-actions and get-artifactory-credentials are private. But "cannot call the shared action" does not have to mean "replace OIDC with a static secret".

Worth asking whether the Artifactory path belongs in this repo's CI at all. Fork PRs never receive the secret, so the public-registry fallback is already the load-bearing path for external contributors, and it demonstrably works. Dropping the Artifactory branch would remove the secret, roughly 40 lines of shell across both workflows, and all the ~/.npmrc handling. If it stays, scope the credential with a step-level env: on the install step instead of $GITHUB_ENV.

Two smaller notes on this block:

  • The comment a few lines down says the token is "never written to disk". True of .npmrc, but $GITHUB_ENV is a file on disk.
  • The fallback branch writes registry=https://registry.npmjs.org/ into the shared runner's ~/.npmrc and never cleans it up, and the publish step does the same. The org's shared action only ever writes the mirror there, and pairs with cleanup-artifactory on if: always(). A later job on the same runner inheriting that file would silently resolve from the public registry, bypassing the mirror's ~7 day cooldown. Pointing npm_config_userconfig at $RUNNER_TEMP avoids this. (The file holds the ${NODE_AUTH_TOKEN} placeholder rather than the token itself, so this is config pollution, not credential leakage.)

@jehannes jehannes Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[kiro]: Taken, and gone further than scoping the env: - the whole Artifactory branch is removed from both workflows in c24117f.

What decided it: the credential does not exist. GET /repos/.../actions/secrets returns none, GET /repos/.../actions/organization-secrets returns total_count: 0, and there are no environments. So secrets.ARTIFACTORY_TOKEN was always empty and only the else branch ever executed. The supply-chain hardening was guarding a path nothing took, and the static-secret exposure you describe was latent rather than active - but it would have become active the moment someone added the secret, which is the worst version of that.

That also settles your "worth asking whether the Artifactory path belongs in this repo's CI at all": the public-registry fallback was not a fallback, it was the only path. Removing the branch drops roughly 40 lines of shell, both ~/.npmrc writes and the $GITHUB_ENV export.

Your two smaller notes were both correct and are both resolved by the removal:

  • The "never written to disk" comment was wrong, $GITHUB_ENV is a file. Comment deleted rather than reworded.
  • The uncleaned ~/.npmrc is gone from both workflows. On a hosted runner it would have been moot anyway since the filesystem is ephemeral, but the write no longer exists.

The design intent is not lost. CONTRIBUTING.md now records the three constraints for wiring the mirror into CI if the credential does appear: short-lived and obtained per run over OIDC in the way get-artifactory-credentials does it rather than a static secret; scoped with a step-level env: on the install step rather than $GITHUB_ENV; and the public path stays the one fork pull requests take, so external contributions keep working. Your reading of why the shared action cannot be consumed here is accurate and is recorded alongside it.

minimumReleaseAge is now documented as the primary control rather than a backstop, since every CI install takes the public path.

Still open from your review: the pnpm publish --dry-run on the first release. Worth doing, since pnpm's OIDC trusted publishing is absent from its docs as you noted, so the first tag is the wrong place to discover a problem.

Comment thread package.json Outdated
"test": "jest --coverage",
"cdk": "cdk",
"lint": "npx oxlint ."
"lint": "oxlint ."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Kiro]: The new lint gate cannot fail.

This PR promotes pnpm run lint to a gating step in both workflows, which is a good change, but as written it can never fail. Two independent reasons, both reproduced on this branch:

  1. The config is never loaded. The file is oxlintrc.json, with no leading dot; oxlint only auto-discovers .oxlintrc.json. I planted a debugger statement in src/index.ts and toggled only the filename: with .oxlintrc.json (carrying no-debugger: off) the warning was suppressed, with oxlintrc.json it fired. So all 87 rules in that file are inert and CI lints against oxlint's defaults, not this repo's config.
  2. Warnings do not fail. 86 of the 87 rules are warn, and oxlint exits 0 on warnings. Measured: exit=0 plain, exit=1 with --deny-warnings.

cp-aws-sso-config gets both right: .oxlintrc.json, plus --deny-warnings in its lint:check script and in its oxlint pre-commit hook.

Suggested fix — rename oxlintrc.json → .oxlintrc.json and deny warnings:

"lint": "oxlint --deny-warnings ."

I checked that this is safe to apply here: with the config actually loaded and --deny-warnings on, the current tree still passes clean (exit 0, no findings), so it will not turn the PR red or need a follow-up cleanup commit.

Pre-existing on main, so not introduced here, but this PR is what turns it into a gate that reviewers and branch protection will trust, which is why it seems worth fixing in this change rather than later.

@jehannes jehannes Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[kiro]: Confirmed independently and both fixes applied in c24117f - oxlintrc.json to .oxlintrc.json, and "lint": "oxlint --deny-warnings .".

I reproduced it with a different discriminator than your debugger test, to check the conclusion rather than the method. unicorn/no-invalid-fetch-options is the single rule the config sets to off, so it distinguishes "config loaded" from "config ignored" without touching rule severity:

oxlint probe.ts                     -> unicorn(no-invalid-fetch-options) fires
oxlint -c oxlintrc.json probe.ts    -> suppressed
oxlint probe.ts (after rename)      -> suppressed

That is the stronger form of your finding: not just that the file was not discovered, but that the repository was linting against oxlint's defaults with all 87 rules inert, including the one deliberate off. Exit codes match what you measured: 0 with warnings present, 1 under --deny-warnings.

Also confirmed your claim that this is safe to apply as-is. With the config loaded and warnings denied, the current tree exits 0 with no findings, so it needs no cleanup commit.

One extra check, since the same class of bug bit twice: .mjs is not in the config's ignorePatterns, so I verified oxlint actually covers the new scripts/check-peer-floors.mjs rather than assuming a clean pass meant coverage. A .mjs probe with a debugger statement fails under --deny-warnings, so it is genuinely linted.

Agreed on the framing that this was pre-existing on main but that this PR is what turns it into a gate people rely on, which is why it belongs here.

Comment thread CONTRIBUTING.md Outdated

This repository uses [pnpm](https://pnpm.io). The version is pinned in the `packageManager` field of `package.json`, and pnpm installs that version for you on first use, so there is nothing to install beyond pnpm itself.

Use Node.js 20, 22 or 24. Do not use a newer major: jsii compiles this construct and only supports those release lines, so a newer Node produces an "untested version" warning and unsupported behaviour.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Kiro]: This recommends a Node line that jsii already warns on today, and drops entirely in 50 days.

I traced @jsii/check-node@1.140.0, which is what jsii 6.0.12 bundles and what emits the "untested version" warning this sentence describes. Its release table:

new NodeRelease(20, { endOfLife: new Date('2026-04-30') }),
new NodeRelease(22, { endOfLife: new Date('2027-04-30') }),
new NodeRelease(24, { endOfLife: new Date('2028-04-30') }),

Node 20's EOL was 2026-04-30, already past. jsii's own support window defaults to EOL + 6 months (DEFAULT_EXTENDED_SUPPORT_MONTHS = 6) and the state is deprecated = endOfLife && !endOfJsiiSupport. So a contributor who follows this line today gets a yellow warning on every build:

Node 20 has reached end-of-life on 2026-04-30 and will no longer be supported in new releases after 2026-10-30. Please upgrade to a supported node version as soon as possible.

On 2026-10-30, 50 days out, Node 20 crosses jsii EOS and flips to the red bgRed.white.bold branch: "Node 20 is end-of-life and not supported anymore by this software since 2026-10-30."

This is visible in the build output that the PR's own Node reasoning is drawn from. My pnpm run build printed the supported set as:

- ^24.0.0 (Planned end-of-life: 2028-04-30)
- ^22.0.0 (Planned end-of-life: 2027-04-30)
- ^20.0.0 (Planned end-of-life: 2026-04-30) [DEPRECATED]

22 and 24 carry no marker; 20 does. So the reasoning behind pinning CI to 24 is right, it just did not carry through to the contributor-facing guidance. Simplest fix is to drop 20:

Use Node.js 22 or 24.

While in here, the repo currently spreads across three Node versions with nothing reconciling them: this line says 20/22/24, both workflows pin node-version: 24, and @types/node is ^26.4.1 — types describing a runtime newer than the one CI uses and newer than anything jsii supports. There is no engines field, so none of it is enforced.

If you want it enforced rather than documented, devEngines.runtime is the right field. It is development-only by design, so unlike engines it does not constrain consumers of the published construct:

"devEngines": {
  "runtime": { "name": "node", "version": "^24.0.0", "onFail": "download" }
}

pnpm resolves the range, records the exact version and checksum in pnpm-lock.yaml, and runs scripts on it, which would collapse the three-way spread to one pinned source. One caveat: with onFail: "download" pnpm provisions Node itself, which makes actions/setup-node's node-version: 24 redundant, so it is worth picking a single owner for that rather than having both.

@jehannes jehannes Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[kiro]: Correct, and fixed in c24117f - the line now reads Node 22 or 24, with a short note that 20 is accepted but deprecated and why.

I verified the release table you quoted from the installed @jsii/check-node@1.140.0: Node 20 at endOfLife: 2026-04-30, DEFAULT_EXTENDED_SUPPORT_MONTHS = 6, deprecated = endOfLife && !endOfJsiiSupport. A local jsii run prints the same three lines you saw, with [DEPRECATED] on 20 only. So the guidance was recommending a line that warns today and stops being supported on 2026-10-30.

On the version spread, updated after cd4915e

Your observation that the repo spread across three Node versions with nothing reconciling them is now resolved rather than documented around, but it took two changes and the second one invalidates the reasoning in the first version of this reply. Recording both, since the conclusion moved.

First pass (c24117f): @types/node went from ^26.4.1 to ^24.13.3. My argument was that these typings bound what the handlers may reference, that the handlers deployed to Runtime.NODEJS_22_X, and therefore that the bound must not run far ahead of the runtime. On that criterion the strictly correct answer was ^22, and ^24 was defensible only because I measured that 22 and 24 declare an identical module set while 26 adds three modules that do not exist on a Node 22 Lambda.

Second pass (cd4915e): the Lambda runtime itself moved to NODEJS_24_X, which removes the compromise. Typings, CI and runtime are now all 24, so ^24 is exactly right rather than one major loose. nodejs22.x reaches Lambda deprecation on 2027-04-30 against 2028-04-30 for nodejs24.x, and nodejs24.x has been GA since 2025-11-25, so the runtime bump stands on its own merits and not just on tidiness.

The measurement that still matters is why ^26 stays out, and it is unchanged by the runtime move:

  • @types/node@26 ships quic.d.ts, ffi.d.ts, vfs.d.ts
  • @types/node@24 and @types/node@22 ship an identical 48-module set, none of those three

Compiled both ways rather than inferring, using import * as quic from 'node:quic': accepted under 26, TS2591 under 22. So ^26 would let a handler import a module absent from the Node 24 Lambda and still typecheck. .github/dependabot.yml ignores @types/node majors for that reason, and CONTRIBUTING.md now says the typings and the runtime move together.

Two corrections to my own earlier framing, since you may have read it before the edit:

  • I described the binding constraint as NODEJS_22_X. That was true when written and is no longer.
  • I said @types/node's major line is a coarse bound, which is still worth knowing: @types/node@22 already declares process.threadCpuUsage and process.execve, neither of which is in Node 22.0. The pin reliably bounds whole modules, not every individual member.

Worth being explicit that nothing was failing at ^26.4.1 - the branch built clean, as your own run found. That change closed a latent hole rather than fixing a break.

On devEngines: still not adopted, and the runtime bump does not change that. Your own caveat is the reason. With onFail: "download" pnpm provisions Node and actions/setup-node's pin becomes redundant, so it trades a documented spread for two competing owners of the same decision. The spread is now genuinely gone, so there is less for it to fix.

jehannes added a commit that referenced this pull request Sep 10, 2026
Follow-up to the review on #43. Each item below was verified rather than
reasoned about, and the measurement is included where it changed the outcome.

Runners
- pr.yml and build-and-publish.yml move from self-hosted back to ubuntu-latest.
  This repository is public with a pull_request trigger, so a fork can run its
  own code in the PR job -- including the files that configure the install
  (pnpm-workspace.yaml, and therefore allowBuilds). The release job holds
  id-token: write for trusted publishing and must not share a runner pool with
  that. Beyond the security argument this was also non-functional: no
  self-hosted runner is registered for this repository, so the pre-commit job
  sat queued for five hours and the required status check could never report.
  GitHub-hosted runners still need enabling at the organisation level; the
  comment in pr.yml records that so the symptom is not mistaken for a hang.

Registry
- Drop the Artifactory branch from both workflows. No ARTIFACTORY_TOKEN is
  reachable from this repository -- no repository secret, no organisation secret
  visible to it, no environments -- so only the public-registry fallback ever
  executed. Removing it also removes a long-lived credential exported through
  $GITHUB_ENV into every later step, two uncleaned ~/.npmrc writes, and a
  comment claiming the token was never written to disk. CONTRIBUTING.md records
  the constraints for reintroducing it over OIDC if the credential appears.
- minimumReleaseAge is now the primary supply-chain control rather than a
  backstop, since every CI install takes the public path. Comments in
  pnpm-workspace.yaml and CONTRIBUTING.md updated accordingly.

Lint
- Rename oxlintrc.json to .oxlintrc.json and add --deny-warnings. The config was
  never loaded: oxlint only auto-discovers the dotted name. Verified with a probe
  on unicorn/no-invalid-fetch-options, the one rule the file sets to "off" -- it
  fired without -c and was suppressed with it, so all 87 rules were inert and CI
  linted against oxlint's defaults. Warnings also exited 0. The current tree
  passes clean with the config loaded and warnings denied.

Peer floors
- Add scripts/check-peer-floors.mjs, run from build.sh ahead of tsc. jsii
  compares devDependencies[name] against semver.minVersion(peerRange).raw in
  lib/project-info.js, but reports a mismatch as JSII6, a warning that exits 0,
  and its severity cannot be raised because it is emitted before diagnostic
  overrides load. Measured: --fail-on-warnings also exits 0 with the warning
  present. The invariant was therefore unenforceable in CI and a bump would have
  merged green. The check fails on inconsistency, not on upgrades: raising the
  devDependency and the peer range together passes.

Dependency updates
- Add .github/dependabot.yml. Without it only security updates ran, which
  matters more now that every action is hash-pinned, because a SHA pin never
  resolves to anything newer and the workflows would rot silently. cooldown is
  4 days, one above minimumReleaseAge, because Dependabot resolves without
  honouring it and can pull in a younger transitive dependency.
- The ignore list is scoped throughout. A bare dependency-name ignore expands to
  >= 0 and applies on the security path too, so aws-cdk-lib and constructs are
  not listed at all -- the check above gates them instead. aws-cdk ignores only
  the accidental 3.0.0 publish, typescript and @types/node only majors, and jsii
  is not ignored, since a jsii release is the signal that a paired TypeScript
  bump exists and both land in the same grouped pull request.

Dependencies
- @types/node moves from ^26.4.1 to ^24.13.3, matching the Node CI builds on.
  These typings bound what the handlers may reference and they deploy to
  NODEJS_22_X: @types/node 26 declares node:quic, node:ffi and node:vfs, none of
  which exist there. Confirmed by compiling an import of node:quic -- accepted
  under 26, TS2591 under 22. @types/node 24 declares the same module set as 22,
  so it does not reintroduce that gap. Nothing was failing at 26; this closes a
  latent hole rather than fixing a break.

Docs
- CONTRIBUTING.md: drop Node 20 from the supported set. It reached end-of-life
  on 2026-04-30 and @jsii/check-node already marks it [DEPRECATED], with jsii
  support ending 2026-10-30. Also fix a circular reference to a README section
  that this branch had already moved into CONTRIBUTING.md.

Signed-off-by: jehannes <jkzuidema@outlook.com>
@jehannes

jehannes commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

[kiro]: All four points addressed, with replies in each thread. Summary here plus the Renovate/Dependabot item, and two findings that changed the shape of the answer.

Commits: c24117f for the CI and lint work, cd4915e for the Lambda runtime. (4541611 in between trims workflow comments.) An earlier version of this comment cited 8d2788b, which the rebase orphaned.

Dependabot

Added .github/dependabot.yml covering github-actions and npm. Your argument was the deciding one: SHA pins never resolve to anything newer, so hash-pinning without update tooling trades one failure mode for another. Note the repo did already have Dependabot security updates running (#42 is one). What was missing is version updates, which is exactly the half that maintains action pins.

cooldown is set to 4 days, not 3, because of the interaction you identified. One day of margin over minimumReleaseAge, since Dependabot resolves without honouring it and can pull in a transitive younger than the direct dependency it is bumping. That case still needs the minimumReleaseAgeExclude escape hatch, which is now documented in CONTRIBUTING.md alongside the minimumReleaseAgeStrict / non-TTY behaviour you described.

Where your suggestion led somewhere unexpected

Writing the ignore list surfaced two things worth recording.

A bare dependency-name ignore also suppresses security updates. It expands to >= 0 and applies on the security path, not just the version path. My first draft ignored aws-cdk-lib and constructs outright to protect their floor pins, which would have silenced security PRs for them. Given the push warned about 16 open vulnerabilities on main, that is not academic.

The floor invariant was unenforceable, which is why ignoring looked necessary. jsii does check it. lib/project-info.js compares devDependencies[name] !== semver.minVersion(peerRange).raw, so it demands exact equality and any bump violates it, including a patch. But it reports JSII6, a warning that exits 0, and its severity cannot be raised because it is emitted before diagnostic overrides load. Measured with aws-cdk-lib set to 2.269.0:

jsii                        -> Warnings: 1, exit 0
jsii --fail-on-warnings     -> Warnings: 1, exit 0

So a Dependabot bump on either would have merged green while quietly widening the range the package claims to support. That is worth flagging beyond this PR: the JSII6 note in the original commit message described the rule, but the rule had never actually been enforced here.

Rather than hide those two from Dependabot, c24117f adds scripts/check-peer-floors.mjs, about 50 lines with no new dependencies, which reimplements the comparison and exits non-zero. build.sh runs it ahead of tsc, so it gates both workflows and local builds. It fails on inconsistency, not on upgrades: raising the devDependency and the peer range together passes, verified. It walks all of peerDependencies rather than the two names, and reports ranges it cannot parse as failures rather than skipping them.

That lets the ignore list drop to three fully scoped entries, so none of them touch the security path:

entry scope why
aws-cdk versions: ["3.0.0"] only the accidental deprecated publish; a genuine 3.x still raises a PR
typescript majors only TS 7 needs jsii 7 to exist
@types/node majors only bounded by NODEJS_24_X; see the CONTRIBUTING.md thread

jsii is not ignored. A jsii release is precisely the signal that a paired TypeScript bump is available, and since both are devDependencies they arrive in the same grouped PR and can be reviewed as a pair. An earlier draft ignored jsii minors, which was backwards: it silenced the trigger.

A second vacuous assertion, found by the runtime bump

Your Node reasoning prompted moving the Lambda runtime to NODEJS_24_X in cd4915e, which collapses the version spread completely. Doing it turned up a test problem in the same family as the oxlint one, so it seems worth surfacing here rather than only in the diff.

The synthesized template holds three AWS::Lambda::Function resources, not two: our keyrotate and sign handlers, plus aws-cdk-lib's own CustomS3AutoDeleteObjectsCustomResourceProviderHandler for the OIDC bucket, whose runtime aws-cdk-lib controls and which is currently also nodejs24.x. hasResourceProperties passes when any resource matches, so the bare Runtime: 'nodejs24.x' assertion is satisfied by CDK's handler alone. Verified: with the assertion at nodejs24.x and our handlers left on NODEJS_22_X, the test still passed.

The assertion now matches on an environment variable unique to each handler, PENDING_KEY and DEFAULT_AUDIENCE, alongside Runtime, and covers both functions instead of one. Confirmed load-bearing by mutation: reverting src/index.ts fails it with Expected nodejs24.x but received nodejs22.x.

The uncomfortable part is that it only ever pinned our runtime by luck, for as long as CDK's internal handler happened to sit on a different version. It would have gone quiet on its own the next time aws-cdk-lib bumped that handler to match.

Still open

  • pnpm publish --dry-run before the first real release. Your point about pnpm's OIDC trusted publishing being undocumented stands, and a tag is the wrong place to find out.
  • GitHub-hosted runners need enabling at the organisation level before pre-commit can report.
  • Renovate itself: not added. Dependabot covers the rot and the cooldown alignment, so a second bot would be redundant. Happy to switch if consistency with the reference repos matters more.

On the PR body

Agreed, and it is now further out of date. The "Registry" section describes a CI Artifactory path that no longer exists, the Node reasoning has changed twice, and the Lambda runtime moved. Over to @jehannes for the description; the commit messages on c24117f and cd4915e carry the current reasoning.

Thanks for the review. The oxlint one in particular was a gate everyone would have trusted without it doing anything.

Release 0.4.0.

Package manager
- Migrate npm -> pnpm 11.25.0, pinned via packageManager. package-lock.json
  is replaced by pnpm-lock.yaml.
- pnpm-workspace.yaml sets nodeLinker: hoisted. This is required, not a
  preference: the package publishes bundleDependencies (the runtime deps that
  aws_lambda_nodejs re-bundles at the consumer's synth time), and pnpm refuses
  to pack those under its default symlinked layout, failing with
  BUNDLED_DEPENDENCIES_WITHOUT_HOISTED. hoisted also produces the same flat,
  real node_modules npm does, so npm pack/publish stays usable as a fallback.
- Rule on dependency build scripts via allowBuilds: esbuild allowed (it bundles
  the Lambda handlers), unrs-resolver and @parcel/watcher denied. Note that
  onlyBuiltDependencies was removed in pnpm 11 and is now silently inert.
- Move the npm overrides into pnpm-workspace.yaml, replacing the deprecated
  "$jest" reference with an explicit version.

Registry
- Do not commit a registry at all. Alliander developers pick up the Artifactory
  npm mirror from their user-level ~/.npmrc, external contributors get
  registry.npmjs.org, and CI selects one explicitly, falling back to the public
  registry when no ARTIFACTORY_TOKEN secret is present so that pull requests
  from forks still build. Committing either one would break the other case, and
  no other repository in alliander-opensource pins an internal registry.
- The org's shared setup-artifactory composite action cannot be used here:
  cp-gh-shared-actions is private and get-artifactory-credentials is internal,
  and GitHub does not permit a public repository to consume actions from
  private or internal repositories. The step is inlined instead.
- Add minimumReleaseAge: 4320 (3 days) as a supply-chain backstop for the
  public-registry path, which gets no mirror-side cooldown. Deliberately
  narrower than Artifactory's ~7 days, because pnpm re-validates the entire
  committed lockfile against this policy on every install.

Dependencies
- Upgrade the runtime and dev dependency set. Publishing now targets the public
  registry explicitly via publishConfig.
- Hold typescript at ~6.0.x and move jsii to ~6.0.12. jsii's major.minor tracks
  the TypeScript compiler it wraps, so jsii@6.0 IS the TypeScript 6.0 line.
  This also resolves a pre-existing mismatch, since jsii ^5.9 wants TS ~5.9
  while the repo already pinned TS ~6.0.
- Hold aws-cdk on 2.x. aws-cdk 3.0.0 is an accidental publish that AWS
  deprecated ("published accidentally, please use 2.x.x instead") but which
  still holds the "latest" dist-tag, so upgrade tooling will offer it.
- Keep the aws-cdk-lib and constructs devDependencies at the floor of their
  peerDependencies ranges. jsii requires this and warns otherwise
  (JSII6, metadata/missing-dev-dependency); compiling against the oldest
  supported version is what keeps those ranges honest.

Workflows
- Hash-pin every action to a full commit SHA, matching the convention already
  used elsewhere in alliander-opensource.
- Pin CI to Node 24: jsii supports ^20, ^22 and ^24 only.
- Run lint, test and build on pull requests, and declare least-privilege
  permissions.
- Keep the job named "pre-commit": branch protection on main requires a status
  check with exactly that name.

Docs
- Correct the install instructions, which pointed at GitHub Packages although
  the package is published to npmjs, and fix the WafUsage enum casing in the
  usage example.
- Move development documentation to CONTRIBUTING.md. jsii embeds README.md into
  the .jsii manifest, which is published and rendered on Construct Hub, so
  contributor-only detail does not belong there.

BREAKING CHANGE: the aws-cdk-lib peerDependency changes from the exact pin
"2.254.0" to "^2.268.0". The range is more permissive above 2.268.0, but the
minimum supported version rises, so consumers on aws-cdk-lib 2.254.0 through
2.267.x must upgrade aws-cdk-lib to take this release.

Signed-off-by: jehannes <jkzuidema@outlook.com>
Follow-up to the review on #43. Each item below was verified rather than
reasoned about, and the measurement is included where it changed the outcome.

Runners
- pr.yml and build-and-publish.yml move from self-hosted back to ubuntu-latest.
  This repository is public with a pull_request trigger, so a fork can run its
  own code in the PR job -- including the files that configure the install
  (pnpm-workspace.yaml, and therefore allowBuilds). The release job holds
  id-token: write for trusted publishing and must not share a runner pool with
  that. Beyond the security argument this was also non-functional: no
  self-hosted runner is registered for this repository, so the pre-commit job
  sat queued for five hours and the required status check could never report.
  GitHub-hosted runners still need enabling at the organisation level; the
  comment in pr.yml records that so the symptom is not mistaken for a hang.

Registry
- Drop the Artifactory branch from both workflows. No ARTIFACTORY_TOKEN is
  reachable from this repository -- no repository secret, no organisation secret
  visible to it, no environments -- so only the public-registry fallback ever
  executed. Removing it also removes a long-lived credential exported through
  $GITHUB_ENV into every later step, two uncleaned ~/.npmrc writes, and a
  comment claiming the token was never written to disk. CONTRIBUTING.md records
  the constraints for reintroducing it over OIDC if the credential appears.
- minimumReleaseAge is now the primary supply-chain control rather than a
  backstop, since every CI install takes the public path. Comments in
  pnpm-workspace.yaml and CONTRIBUTING.md updated accordingly.

Lint
- Rename oxlintrc.json to .oxlintrc.json and add --deny-warnings. The config was
  never loaded: oxlint only auto-discovers the dotted name. Verified with a probe
  on unicorn/no-invalid-fetch-options, the one rule the file sets to "off" -- it
  fired without -c and was suppressed with it, so all 87 rules were inert and CI
  linted against oxlint's defaults. Warnings also exited 0. The current tree
  passes clean with the config loaded and warnings denied.

Peer floors
- Add scripts/check-peer-floors.mjs, run from build.sh ahead of tsc. jsii
  compares devDependencies[name] against semver.minVersion(peerRange).raw in
  lib/project-info.js, but reports a mismatch as JSII6, a warning that exits 0,
  and its severity cannot be raised because it is emitted before diagnostic
  overrides load. Measured: --fail-on-warnings also exits 0 with the warning
  present. The invariant was therefore unenforceable in CI and a bump would have
  merged green. The check fails on inconsistency, not on upgrades: raising the
  devDependency and the peer range together passes.

Dependency updates
- Add .github/dependabot.yml. Without it only security updates ran, which
  matters more now that every action is hash-pinned, because a SHA pin never
  resolves to anything newer and the workflows would rot silently. cooldown is
  4 days, one above minimumReleaseAge, because Dependabot resolves without
  honouring it and can pull in a younger transitive dependency.
- The ignore list is scoped throughout. A bare dependency-name ignore expands to
  >= 0 and applies on the security path too, so aws-cdk-lib and constructs are
  not listed at all -- the check above gates them instead. aws-cdk ignores only
  the accidental 3.0.0 publish, typescript and @types/node only majors, and jsii
  is not ignored, since a jsii release is the signal that a paired TypeScript
  bump exists and both land in the same grouped pull request.

Dependencies
- @types/node moves from ^26.4.1 to ^24.13.3, matching the Node CI builds on.
  These typings bound what the handlers may reference and they deploy to
  NODEJS_22_X: @types/node 26 declares node:quic, node:ffi and node:vfs, none of
  which exist there. Confirmed by compiling an import of node:quic -- accepted
  under 26, TS2591 under 22. @types/node 24 declares the same module set as 22,
  so it does not reintroduce that gap. Nothing was failing at 26; this closes a
  latent hole rather than fixing a break.

Docs
- CONTRIBUTING.md: drop Node 20 from the supported set. It reached end-of-life
  on 2026-04-30 and @jsii/check-node already marks it [DEPRECATED], with jsii
  support ending 2026-10-30. Also fix a circular reference to a README section
  that this branch had already moved into CONTRIBUTING.md.

Signed-off-by: jehannes <jkzuidema@outlook.com>
Signed-off-by: jehannes <jkzuidema@outlook.com>
@jehannes
jehannes force-pushed the version-and-workflow-upadate-09102026 branch from 781d33f to 4541611 Compare September 10, 2026 14:30
Move both NodejsFunction runtimes from NODEJS_22_X to NODEJS_24_X.

Why now
- nodejs22.x reaches Lambda deprecation on 2027-04-30 (create blocked
  2027-07-01); nodejs24.x runs to 2028-04-30. nodejs24.x has been GA since
  2025-11-25, so this is not a fresh runtime.
- It removes the last version mismatch in the repository. @types/node is ^24
  and CI pins Node 24, so the typings were sitting a major ahead of the runtime
  they bound -- the same class of gap that made ^26 wrong, only narrower. Of the
  two internally coherent pairings, types 24 + runtime 24 and types 22 +
  runtime 22, the tree had neither. Now typings, build and runtime are one
  version.

Cost
- No dependency change: Runtime.NODEJS_24_X already exists in the pinned
  aws-cdk-lib 2.268.0.
- No runtime dependency constrains it: @aws-sdk/client-kms and client-s3
  declare node >=20, base64url >=6, powertools/logger and jsrsasign declare
  nothing.
- No bundling or esbuild target override in src/index.ts, so the bundle target
  follows the runtime automatically.

Consumer impact
- This is an in-place CloudFormation update of the Runtime property. The
  functions are not replaced and their ARNs are preserved.
- The handlers are authored and bundled by this package, so no consumer-supplied
  code runs on the changed runtime.
- The jsii API surface is unchanged; .jsii is byte-identical.

Test correctness
- The existing assertion in src/test/index.test.ts became vacuous under this
  change and had to be repaired, not just retargeted. The synthesized template
  holds THREE AWS::Lambda::Function resources: our keyrotate and sign handlers,
  plus aws-cdk-lib's own S3 auto-delete-objects custom resource handler, whose
  runtime aws-cdk-lib controls and which is currently also nodejs24.x.
  hasResourceProperties passes when ANY resource matches, so a bare
  `Runtime: 'nodejs24.x'` match is satisfied by that third resource on its own.
  Verified: with the assertion at nodejs24.x and our handlers left on
  NODEJS_22_X, the test still passed.
- The assertion now matches on an environment variable unique to each handler
  (PENDING_KEY for keyrotate, DEFAULT_AUDIENCE for sign) alongside Runtime, and
  covers both functions rather than one. Confirmed load-bearing by mutation:
  reverting src/index.ts to NODEJS_22_X fails it with "Expected nodejs24.x but
  received nodejs22.x", and the CDK-internal handler no longer satisfies it.
- This also means the runtime was only ever pinned by test for as long as
  aws-cdk-lib's internal handler happened to differ from ours.

Docs
- The @types/node note in CONTRIBUTING.md and the matching comment in
  .github/dependabot.yml cited NODEJS_22_X as the bound; both now cite
  NODEJS_24_X and say the two move together.

Signed-off-by: jehannes <jkzuidema@outlook.com>
@sonarqubecloud

Copy link
Copy Markdown

Comment thread CONTRIBUTING.md
//alliander.jfrog.io/artifactory/api/npm/alliander-npm-all/:_authToken <token>
```

Keep both the registry and the token in that user-level file. This repository intentionally commits no `.npmrc` at all; if you add one, never put credentials in it, and note that since pnpm 10 only registry and auth settings are read from `.npmrc` — every other pnpm setting belongs in `pnpm-workspace.yaml`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to add .npmrc (and perhaps also package-lock.json) to .gitignore?

Comment thread .github/workflows/pr.yml
run: 'pip3 install --only-binary :all: pre-commit==4.6.2'

- name: run pre-commit
run: pre-commit run --all-files -c .pre-commit-config-ci.yaml

@tapaskchowdhury tapaskchowdhury Sep 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be a good idea to also hash pin .pre-commit-config-ci.yaml?

Comment thread package.json
@@ -1,14 +1,15 @@
{
"name": "@alliander-opensource/aws-jwt-sts",
"license": "MIT",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it is MIT license where as everywhere else it is Apache. Perhaps bring that also inline with the rest unless there is a good reason for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants