Repository navigation
chore: move to pnpm 12 and publish with npm trusted publishing - #137
Conversation
…h audit findings - @cornerstonejs/core 4.22.3 -> 5.11.4 (adds metadata/utils peers), dicom-codec 1.1.6, codec-openjph 2.4.11, codec-openjpeg 1.3.6 - dcmjs 0.50.1 -> 0.52.0 - Fix high/critical bun audit findings: aws-cdk-lib 2.272.0 (cli 2.1144.0), ws 8.22.0, adm-zip 0.6.1, overrides for axios, fast-uri, js-yaml, pacote, smol-toml, tar; re-resolve other transitive deps within their ranges - cs3d: compile with module node20 so it can import the ESM-only core 5 types - jest: transform gl-matrix, which core 5 installs nested - bunfig: pin linker = "hoisted" (bun.lock configVersion 1 defaults to isolated) and apply minimumReleaseAge to install:update-lockfile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 5.11.5 Bun does not accept a scope wildcard in minimumReleaseAgeExcludes, so the excludes list each @cornerstonejs package in the dependency tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fixable audit advisories - Override http-cache-semantics to 4.3.0 to clear the lerna advisory. - Ignore four dev-only DoS advisories in the CI audit step: braces has no patched release, and nx pins brace-expansion 5.0.8 exactly. - Bump constructs to 10.8.1 to meet the aws-cdk-lib peer range ^10.5.0. - Note in both bunfig files that the Cornerstone3D exclude lists must match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed publishing - Replace bun and lerna with pnpm 12.9.1 for install, lockfile, scripts and audit. Bun stays the runtime for the CLIs and the Docker service image. - pnpm-workspace.yaml holds the overrides, the release-age rule (with a @cornerstonejs/* exclude), allowBuilds and auditConfig.ignoreGhsas. Range overrides fix brace-expansion, so only braces (no patched release) is ignored. - CI uses pnpm/action-setup and Node 24.15.0, as Cornerstone3D does. - Add publish.yml and scripts/release/*, adapted from the Cornerstone3D release flow: version from the last commit message, atomic push of the version commit and tag, then npm publish --provenance with OIDC. - Fix repository url/directory in each package.json so provenance matches. - Align healthlakestore to 1.7.6 with the other packages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take #136 (squash of the branch this one builds on). Keep the pnpm side of the bun-to-pnpm conflicts: drop bun.lock and the bunfig files, keep the pnpm audit step. Carry over the review fixes: the scp bin scripts become 100755, and the braces ignore comment in pnpm-workspace.yaml says the advisory is unreachable, not dev-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ruleset can allow it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mark @radicalimaging/healthlakestore private so the release scripts skip it, and restore its version to 1.6.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rleisti
left a comment
There was a problem hiding this comment.
Review
Nice work overall — the "push before publish" ordering, the atomic push, the E404-vs-other-failure distinction in isPublished, and pinning npm for OIDC are all good calls. A few things need attention before this goes live, mostly around the failure/recovery paths, since npm can't take a version back.
🚫 Blockers
1. The failure-recovery path publishes the wrong code (or nothing).
Two issues combine here:
- Re-run is a silent no-op. When
publish-package.mjsfails partway, it tells the operator to "Re-run this workflow." But a re-run checks out the originalGITHUB_SHA, while master now points at the pushedchore(release): publishcommit. The tip check (publish.yml:61) setsproceed=false, every step is skipped, and the run goes green with nothing published. - Recover mode builds the wrong commit. If the gap is instead healed by the next merge (the other path the header comment describes), recover mode builds and tests the new merge's code and publishes it under the old version (
publish.yml:106). Result: npm1.7.7holds commit B's code, tagv1.7.7points at commit A, the provenance attestation names commit B, and B's changes never get a version of their own.
Suggested fix: in recover mode, check out the commit the version tag points at (git checkout "v$VERSION") before building and publishing. Then either let the tip check pass when FETCH_HEAD is the release commit whose parent is HEAD, or change the error message to say "start a manual run (workflow_dispatch) on master" instead of "re-run".
⚠️ Should fix
2. Consider gating the npm publish with a GitHub environment (publish.yml:33).
id-token: write covers the whole job, so dependency code that runs during build and test could use the trusted-publishing token. Splitting into a minimal publish job narrows that a little. But neither change helps against a malicious merged PR, which can edit the workflow itself. A stronger control: put the publish in an npm-publish environment with required reviewers and a master-only branch rule, and name that environment in each package's trusted publisher config on npmjs.com. Then npm only accepts tokens from approved runs, and that gate lives in repo settings, not in code a PR can change.
3. The bump type comes only from the tip commit's message (scripts/release/version.mjs:21).
This replaces lerna's conventional-commits analysis of everything since the last tag. Failure cases:
- A
featlands, then afixlands before the first run reaches the tip → the tip run releases both as a patch. - Merge commits ("Merge pull request #…") or non-conventional titles always give a patch.
feat!:/BREAKING CHANGE:gives a minor, never a major.
Suggest scanning git log v<current>..HEAD and taking the highest bump, or at least handling ! / BREAKING CHANGE.
4. The tag output is put straight into the shell (publish.yml:154, and the same in the "Create the GitHub release" step).
TAG='${{ steps.release.outputs.tag }}' puts a value from package.json straight into the script, in a job that holds the deploy key and the OIDC token. In recover mode, a crafted version string in a merged PR could break out of the quotes. Low likelihood (it needs a merged PR), but the fix costs nothing:
env:
TAG: ${{ steps.release.outputs.tag }}5. git add -A packages can commit build or test output (scripts/release/publish-version.mjs:75).
This runs right after build and test, so any file they write under packages/<pkg>/ that .gitignore doesn't cover ends up in the release commit. That commit is pushed to protected master with the deploy key, skipping review. Stage only what the script changed: the package.json files and pnpm-lock.yaml.
6. The header comment is wrong about what prevents a release loop (publish.yml:17).
It says GITHUB_TOKEN pushes the version commit, so the workflow can't re-trigger itself. The push actually uses RELEASE_DEPLOY_KEY, and deploy-key pushes do trigger workflows. The only loop guard is [skip ci] in the commit message (with the chore(release): publish subject check as a fallback). Please fix the comment so nobody removes [skip ci] thinking it isn't needed.
💬 Nits
7. The override check lets some ranges through (scripts/check-pinned-versions.sh:83).
is_pinned only rejects ^, ~, *, >= and <=, so overrides like >1.0.0, <2, 1.x, 1 - 2 or 1.0.0 || 2.0.0 pass as "exact", even though pnpm-workspace.yaml says the script enforces exact values. An allowlist regex for exact semver would be stricter than a denylist.
8. Non->= internal ranges are silently pinned to exact (scripts/release/publish-version.mjs:49).
Every internal dependency uses >= today, so this is latent. But a future workspace:* (which check-pinned-versions.sh allows) or ^1.7.6 would quietly become "1.7.7". Consider throwing on any unexpected prefix instead.
9. Registry lookups run one at a time, twice per release (scripts/release/workspace-packages.mjs:78).
findUnpublished and the loop in publish-package.mjs each run one npm view per package in sequence. Promise.all in findUnpublished is an easy win.
- recover mode builds the tagged commit, and a re-run continues from the version commit of the run instead of skipping every step - the bump type comes from every commit since the last tag, and `type!:` or `BREAKING CHANGE:` gives a major - the tag reaches the shell through `env` - the release commit stages only the manifests and pnpm-lock.yaml - the header comment names `[skip ci]` as the guard against a release loop - overrides must be exact semver (allowlist), unexpected internal ranges stop the release, and the registry checks run in parallel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thank you for the review. Commit 754851e changes points 1, 3 to 9. Point 2 does not change. The reason is below. 1. Failure recovery (blocker)
3. Bump type
If the tag of the current version does not exist, the script stops with an error. 4. Tag in the shellThe push step and the release step now get the tag through 5.
|
rleisti
left a comment
There was a problem hiding this comment.
Re-review of 754851e
Thanks — items 1, 3, 4, 5, 6, 7, 8 and 9 are resolved. I checked the override fix (>1.0.0 is now rejected and the current overrides pass) and ran the new bump logic on sample messages; both behave as intended.
🚫 Blocker (new)
The first release after merge will fail: there is no v1.7.6 tag.
version.mjs now throws when v<current version> doesn't exist, and the newest v* tag on the remote is v1.5.0 (lerna made per-package tags). The failure is harmless (it happens before any push or publish), but nothing will release until someone creates the tag. Please either push v1.7.6 on the commit that was published as 1.7.6 before merging (the first master commit carrying 1.7.6 is 7b39944 / #124; can you confirm that's the published one?), or fall back to the last commit that changed packages/create-dicomweb/package.json's version when the tag is missing.
⚠️ Should fix
Item 2 (still open): gate the publish with a GitHub environment.
id-token: write still covers the whole job. Rather than only splitting jobs, put the publish in an npm-publish environment with required reviewers and a master-only branch rule, and name that environment in each package's trusted publisher config on npmjs.com. npm then only accepts tokens from approved runs, and the gate lives in repo settings, where a PR can't change it.
Recover mode from a later merge attests the wrong commit.
When merge B heals a missing version, the job builds tag A, but the provenance attestation names B (publish.yml, "Check out the tagged commit"). The warning is good, but the published attestation is still wrong. Suggest allowing workflow_dispatch on refs/tags/v* for recover mode only, so GITHUB_SHA is the tagged commit. The next-merge run could then stop with an error telling the operator to start a manual run on the tag instead of publishing.
💬 Nit
publish-version.mjs now correctly rejects workspace:* / ^ on internal dependencies, but check-pinned-versions.sh still accepts workspace:*. So that mistake passes CI and only fails at release time, after merge. Consider running the same check in check-pinned-versions.sh.
- version.mjs falls back to the last commit that set the current version when no tag names it, so the first release does not need a v1.7.6 tag - recover mode publishes only from a run that holds the tagged commit: a re-run, or a manual run on a v* tag that master holds. A run for a later merge fails and names the tag, so the attestation names the right commit - check-pinned-versions.sh accepts only >=<version> or an exact version for a workspace package, as publish-version.mjs does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thank you for the re-review. Commit 6609c66 changes the blocker, the attestation point, and the nit. Item 2 does not change. Blocker: no
|
- prepare installs, checks the supply chain (with the audit), builds, tests, makes the version commit and packs the tarballs, with no credential. - push checks the version commit and pushes it with the deploy key of the `release` environment. A failed push now says why it failed. - publish publishes the tarballs in dependency order from the `npm-publish` environment, the only job with an OIDC token. The first failure stops it. - Actions are pinned by commit, and the publish workflow restores no cache. - release-mode counts only the packages of the last release, so a new or re-published package no longer blocks every release. - version.mjs needs the tag of the current version. - Tests for the bump rules, the publish order and the version commit check. Addresses #139. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- github-release runs after a recover run, where push is skipped. - publish uses !cancelled(), so a cancel stops it. - The release artifact can be uploaded again, and stays for 30 days. - verify-version-commit.mjs takes the tag, needs a version above the current one, and accepts only the lockfile specifiers of the packages of the release. - publish-package.mjs compares the install fields of the package.json in each tarball with the tag, and shows the npm output again. - release-mode.mjs no longer needs the version at the tag, because lerna left most packages of v1.7.6 at 1.7.4 there. - One list of dependency types for the release scripts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- publish-package.mjs rejects a tarball entry outside package/, a link, a duplicate entry, and a binding.gyp that the tag does not hold. It also compares imports, directories, gypfile and publishConfig. - Only a new version runs the audit. A recover run publishes a tag that no commit can change, so a later advisory must not block it. ci-supply-chain.sh gets SKIP_AUDIT for this. - The lockfile check skips the diff header only before the first hunk. - "Re-run all jobs" of a run that pushed its version commit fails, and names "Re-run failed jobs", when master moved since. - current-version.mjs reads VERSION_SOURCE for the workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review of 59805c3 + 21e9c8eThe job split is a solid redesign, and all the earlier items are addressed. I ran the release scripts in a scratch clone (no push, no publish): the release tests pass,
|
- publish-package.mjs rejects a tarball entry that is not in its normal spelling (package/./package.json, package//package.json), because npm keeps the last copy of a path. findEntryProblems has a test. - version.mjs stops before the push when the next tag exists, or when npm holds the next version, so a run cannot end green with nothing published. - The push job computes the next version again from the commits, so the build job cannot choose the version. release-type.mjs holds the shared computation, with Node built-ins only. - The lockfile check needs the same `>=` prefix on both specifier lines. - release-mode.mjs checks npm again for up to 6 minutes before `recover`, because the registry CDN can answer from a copy up to 300 s old. - After a failed push, the job reads master and the tag on GitHub. A push that GitHub took continues, and an existing tag gets its own error. - The comments name the tagged-commit step as the guard, not provenance. - readCurrentVersion replaces three reads of VERSION_SOURCE, and version.mjs no longer uses execa or semver. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review of 3460776 + 6349ecaAll set from my side. The audit now runs only for a new version, so a later advisory can't block a recover run. The other hardening looks good too: the push job recalculates the version itself, I re-ran the flow in a scratch clone (no push, no publish): the release tests pass, Remaining before merge: please confirm each package's trusted publisher on npmjs.com names both Optional nit, still open: if canvas's prebuilt binary download times out, the release fails at the |
- publish-package.mjs checks every tarball before the first publish, so a failed check cannot leave npm with part of a version. - The tarball check rejects an npm-shrinkwrap.json that the tag does not hold, any node_modules/ entry, and entries that differ only in case. - The publish job checks the tag output of the prepare job again: the tag must name the commit of the run, or its version commit. - The recover errors tell the operator to re-run the master runs that failed while npm lacked the version. - RegExp.escape replaces the local escape helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take master's tree (the squash merge of #137 and the v1.7.7 release) and keep only the Docker changes of this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
This PR replaces bun and lerna with pnpm 12 as the package manager. It also adds a GitHub Actions workflow that publishes the packages with npm trusted publishing (OIDC). The setup follows Cornerstone3D and OHIF.
This PR depended on #136. #136 merged, and the base branch of this PR is now
master.User requirements
pnpm install,pnpm run build,pnpm run test.mkdicomweb,dicomwebserver,dicomwebscp,createdicomweb,deploydicomweb) and theoven/bunDocker service image do not change.masterpublishes 9@radicalimaging/*packages to npm at one new version.@radicalimaging/healthlakestoreis private, and the release does not publish it.type!:or aBREAKING CHANGE:footer increases the major version.featincreases the minor version.masterneeds one approved review. A release needs no other approval.publish.ymlon the version tag.>=ranges, so a consumer still accepts later releases.master, or a manual run on av*tag, gets the npm credentials and the deploy key. A workflow on another branch gets neither.pnpm audit --audit-level=highfinds an advisory thatauditConfig.ignoreGhsasdoes not hold. Before, only CI ran the audit, and only when the lockfile changed. Arecoverrun does not run the audit, because no commit can change the tag that the run publishes.pnpm audit --audit-level=highgives the same result in CI and on a local machine.Implementation requirements
packageManagerispnpm@12.9.1, the newest release that is older than the 2-dayminimumReleaseAge.lerna,lerna.json,bun.lock,bunfig.toml, andbunfig.update-lockfile.toml.pnpm -r.pnpm runandnode -ein place ofbun runandbun -e. Thebunruntime commands,bun linkfor example, do not change.pnpm-workspace.yamlnodeLinker: hoistedkeeps thenode_moduleslayout that the jest configs and the Docker build expect.linkWorkspacePackages: truelinks the packages, because they use>=ranges and notworkspace:.minimumReleaseAge: 2880, withminimumReleaseAgeExclude: ['@cornerstonejs/*']. One glob replaces the two duplicate lists in the bunfig files.package.json. Range overrides forbrace-expansionfix the version that nx pins exactly, so the audit no longer needs those 3 ignores.auditConfig.ignoreGhsasholds onlyGHSA-vfj7-8cjw-p6xm(braces), because no patchedbracesrelease exists.allowBuilds:canvasandesbuildrun their install scripts.core-js-puredoes not.scripts/ci-supply-chain.shuses pnpm. It runs the audit whenpnpm-lock.yamlorpnpm-workspace.yamlchanges.FORCE_AUDIT=1replacesFORCE_BUN_AUDIT=1.SKIP_AUDIT=1skips the audit. The publish workflow usesSKIP_AUDIT=1, and runs the audit in its own step inreleasemode.scripts/check-pinned-versions.shalso checks each override value inpnpm-workspace.yaml. The check uses an allowlist: each value must be an exact semver version. An override key can still select a range.scripts/check-pinned-versions.shaccepts only>=<its version>or an exact version for a dependency on a workspace package, aspublish-version.mjsdoes..github/workflows/ci.ymlusespnpm/action-setup@v6.1.0,actions/setup-node@v6.4.0, and Node24.15.0, as Cornerstone3D does.testandbuilddo not change..github/workflows/publish.ymlandscripts/release/*come from the Cornerstone3D release flow, withmasterin place ofmain.publish.ymlhas 4 jobs, and each credential goes only to the job that needs it. The workflow-levelpermissionsis{}.prepare(contents: read, no secret): install, supply-chain checks withFORCE_AUDIT=1, build, test, version commit, andnpm pack. Only this job runs the scripts of the dependencies.push(releaseenvironment): runsverify-version-commit.mjs, then pushes the version commit and the tag withRELEASE_DEPLOY_KEY.publish(npm-publishenvironment,id-token: write): checks out the tag, and publishes the tarballs withnpm publish <tarball> --provenanceand npm11.19.0. The job installs no dependency of the repository.github-release(contents: write): creates the GitHub release.prepareto the other jobs.verify-version-commit.mjsuses only Node built-ins. The script accepts a version commit only if all of these are true:v<new version>.release-type.mjs, so thepreparejob cannot choose the version.specifier:lines in the lockfile keeps the same>=prefix.versionto the new version, and the ranges of the internal dependencies to[>=]<new version>.pnpm-lock.yaml, the commit changes only thespecifier:lines of the internal dependencies.publish-package.mjspublishes only the tarballs that match the publishable packages of the tag.npm publish. A failed check therefore leaves npm with no part of the version.package/, in its normal spelling, and must occur once, in any case. npm drops the first directory of each entry and keeps the last copy of a path. So a second top-level directory, or a second spelling such aspackage/./package.json, can replacepackage/package.json.binding.gypor annpm-shrinkwrap.jsonmust also be in the tag. npm runsnode-gypfor the first file at install, and installs the dependency tree of the second file.node_modules/, because no package of this repository bundles its dependencies.publishjob checks thetagoutput of thepreparejob again. The tag must nameGITHUB_SHA, or the version commit on top ofGITHUB_SHA.package.jsoninside each tarball with the tag:name,version, the entry points,imports,bin,directories,files,scripts,gypfile,publishConfig, and the dependencies.current-version.mjsreadsVERSION_SOURCEfor the workflow.mastermoved after the push. Before, the run did nothing and passed.npm publishwrites its output to the job log, so the log shows the provenance link.releaseartifact stays for 30 days. After that time, a re-run of thepublishjob fails, and a manual run on the tag is necessary.version.mjsreads the commits inv<current>..HEAD. Change after the second review: if the tag of the current version does not exist, the script stops with an error. Thegit log -Sfallback is gone, because the tagv1.7.6now exists.version.mjsstops before the push when the next tag exists already, or when npm holds a package at the next version already. Before, the publish skipped every package, and the run passed with nothing published.v1.7.6points to83dde1c, the commit that published 1.7.6. That commit is not onmaster, sov1.7.6..HEADstarts at the merge baseb8bbbd9, andversion.mjsgives a warning.publish-version.mjsstages onlypnpm-lock.yamland thepackage.jsonfiles, so build output and test output do not go into the release commit.publish-version.mjsstops when an internal dependency range is not>=<version>or an exact version.pushjob pushes the version commit and the tag in one atomic push, before thepublishjob starts. When git reports a failure, the job readsmasterand the tag on GitHub:mastermoved: the error names a race.RELEASE_DEPLOY_KEYstops the job with its own error.[skip ci]in the version commit message stops a release loop. The check of thechore(release): publishsubject is a fallback.env: TAG, and not through an expression in the script.recovermode publishes the version thatHEADalready carries when npm does not hold that version. The mode takes no new version.release-mode.mjschecks only the packages of the last release: the packages that carry<current>now, and that are publishable at the tagv<current>. The version at the tag does not count, because lerna left 8 packages at 1.7.4 at the tagv1.7.6.release-mode.mjschecks npm again, up to 6 times at 60 s intervals, before it givesrecover. The registry CDN can answer from a copy that is up to 300 s old, so a run right after a publish can see the new version as missing.recoverrun publishes only whenHEADis the tagged commit, so npm gets the code of the tag and the provenance attestation names that commit.masterholds that commit.workflow_dispatch) on av*tag runs inrecovermode.mastermust hold the tagged commit, and the tag name must be the same as the version in that commit.masteris still the release gate, and the environments have no required reviewers. The environments limit the branches and the tags that get the credentials.package.jsonnow hasrepository.urlgit+https://github.com/RadicalImaging/Static-DICOMWeb.gitand the correctdirectory. npm provenance checks this URL.s3-deploypointed toRadical/static-dicomweb.cs3d,healthlakestore, andstatic-wado-webserverhad the wrongdirectory.healthlakestore: the package is now"private": true, so the release scripts skip the package, and npm refuses a publish of the package. The version stays 1.6.5. Every other package carries 1.7.6, and npm holds 1.7.6 for each of them, so the first publish run is a normalreleaserun.pnpm install --frozen-lockfile. The installer stage and the runtime stage do not change.Setup before the first release
An admin completed all steps on 2026-10-08.
@radicalimaging/*packages nameRadicalImaging/Static-DICOMWebandpublish.ymlas the trusted publisher.masterreplaces the classic protection. The ruleset requires 1 approving review, and it blocks force pushes and branch deletion. Deploy keys and repository admins can bypass the ruleset.RELEASE_DEPLOY_KEYof thereleaseenvironment holds the private key. The repository has noRELEASE_DEPLOY_KEYsecret. The admin replaced the first key, which was a repository secret. A repository ruleset cannot name the GitHub Actions app as a bypass actor, soGITHUB_TOKENcannot push tomaster.npm-publishallows the branchmasterand the tagsv*.releaseallows the branchmasteronly.release-tagsruleset stops the creation, the update, and the deletion ofrefs/tags/v*. Deploy keys and repository admins can bypass the ruleset. Without this ruleset, a user with write access can push av*tag on a branch and get past thenpm-publishenvironment.npm-publish. The entries without an environment are gone, so npm accepts an OIDC token ofpublish.ymlonly from a job in that environment.npm access set mfa=publish). Trusted publishing still works with this setting.Test
pnpm install --frozen-lockfilepasses.FORCE_AUDIT=1 bash scripts/ci-supply-chain.shexits with code 0.bash scripts/check-pinned-versions.shexits with code 0. The override check rejects>1.0.0,<2,1.x,1 - 2, and1.0.0 || 2.0.0.pnpm run buildpasses.pnpm run testpasses in all packages, with the same test counts as under bun.docker build .builds the x64 image, andmkdicomweb --helpruns in the image.version.mjsgives1.7.7from 18 commits, with the tagv1.7.6.pnpm run test:releasepasses 4 tests. The tests cover the bump rules, the publish order,verify-version-commit.mjs, and the check of the tarball entries.verify-version-commit.mjsrefused a version commit for v9.9.9, because the commits give 1.7.7.version.mjsstopped when the tagv1.7.7existed, and gave 1.7.7 without that tag.publish-package.mjsrefused astatic-wado-webservertarball with an addednpm-shrinkwrap.json. That package is the last in the publish order, and the script published nothing.actionlint1.7.12 finds no problem inpublish.ymlandci.yml.publish-version.mjsmade a real version commit for 1.7.7, andverify-version-commit.mjsaccepted the commit. The commit went through a git bundle into a depth-1 clone of the parent, as in thepushjob, and the check passed there too. A version commit with an addedpostinstallscript failed the check.pack-packages.mjspacked the 9 packages.publish-package.mjswith--dry-runpublished them in dependency order, withcs3dfirst andstatic-wado-webserverlast. An extra tarball inmanifest.jsonstopped the script. A tarball with an addedpostinstallscript in itspackage.jsonstopped the script.verify-version-commit.mjsrefused the real version commit under the tagv9.9.9. The script also refused a lockfile line+++ evil: injectedin a hunk, and a version that is not above the current version.publish-package.mjsrefused a tarball with a second top-level directoryzz/that holds apackage.jsonwith apostinstallscript.release-mode.mjsat 1.7.6 givesrelease. Withhealthlakestoremade public, the result is stillrelease.jq,check-pinned-versions.shrejectsworkspace:*,^1.7.6, and>=1.7.5on an internal dependency, and accepts1.7.6.arm-Dockerfile, and I did not runpublish.yml. The first merge tomasterafter this PR is the first test of the push with the deploy key and of the recovery paths.🤖 Generated with Claude Code