feat(deploy): publish to npm, Docker Hub and GitHub releases from one tag - #33
Merged
Conversation
… tag Three install routes out of one tag, and a fix for two that were already broken. The Linux worker tarball could not start. It ships rsagent-worker.mjs with no node_modules beside it, but the bundle marked better-sqlite3 and @azure/identity external, so neither resolved at runtime. Reproduced on a clean node:24 container with main's own bundle: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'better-sqlite3' @azure/identity was never optional despite being declared that way: tedious depends on it outright and requires it at the top of connection.js, so it loads the moment mssql does. Nothing is marked external now, and the bundle is verified to run with an empty directory beside it. The npm package declared six dependencies it had already inlined, so `npm i -g` downloaded the whole tree to sit unused next to a self-contained bundle. It installs one package now, checked with a real global install rather than a dry run. The outbox moves from better-sqlite3 to the runtime's node:sqlite. That is what makes single-file executables possible at all — a SEA cannot load a native addon — and it removes the last thing compiled from C++ on a customer's database server. The API surface is small, confined to outbox.ts, and the existing 15 outbox tests carried over unchanged. Two new tests cover the hand-written BEGIN/COMMIT/ROLLBACK that replaces better-sqlite3's transaction() helper; both fail if the ROLLBACK is removed. node:sqlite is still marked experimental, which is the real cost here. It is mitigated by pinning the runtime the worker ships with, and by the surface being one file. Knock-on: the control-plane image no longer installs python3 and build-essential, which existed only so better-sqlite3 could compile on arm64. Verified the image builds without them and that argon2, the one remaining native module, still hashes. Docker Hub joins GHCR from the same build, so the two registries hold the same digest rather than two builds that could differ. `latest` no longer follows a prerelease — the previous form enabled it for any tag, so a v0.1.0-rc.1 would have become what `docker pull` returns by default. Release assets are now verifiable: SHA256SUMS, a build attestation per artefact, and the Windows Node runtime checked against nodejs.org's published SHASUMS256.txt instead of being trusted because curl exited 0. That binary is installed as a service on a database server. pnpm release:version sets every manifest and WORKER_VERSION in lockstep, and the release job refuses to publish if any of them disagrees with the tag. Versions are lockstep because the worker, control plane and protocol are one system speaking one wire format to itself. If the bundle is wrong, everything except npm fails to start, and npm hides it by installing the missing package anyway. That is why CLAUDE.md now says to run the bundle from an empty directory before calling worker work done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj
Drops NPM_TOKEN entirely. The workflow's id-token: write permission mints a short-lived OIDC token that npm exchanges for publish rights, scoped to this repository and this workflow file, so there is no long-lived credential in the repository to leak, expire or forget to rotate. --provenance is removed rather than kept: trusted publishing generates the attestation itself, and requesting it explicitly is what makes a publish fail wherever the OIDC token is absent. Same reason provenance=true comes out of .npmrc — it would break the one manual publish each package needs before it can have a trusted publisher at all. That bootstrap is npm's chicken-and-egg, not a gap here: a trusted publisher is configured on a package's settings page, and a package that has never been published has no settings page. Each name needs one 0.0.0 publish from a scratch directory, once, ever. Documented with the exact steps so the next published package does not rediscover it. Requires pnpm 10.20 or later, which implements the OIDC exchange itself. This repository pins pnpm@10.23.0 via packageManager, and pnpm publishes its own releases this way. Note pnpm 11.0.8 regressed it (pnpm/pnpm#11513) — worth knowing before taking pnpm 11. The workflow filename is matched exactly by the trusted publisher config, so renaming release.yml breaks publishing until both packages are updated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj
Pushing an image needs a read/write access token. Syncing the README to the Docker Hub listing needs read/write/delete, because that is what the description API demands. Holding the second permanently, in a workflow, so a listing page has a README on it is the wrong trade: anything that compromised the workflow could then delete every published tag in the namespace. The step is marked continue-on-error, so a read/write token is the correct thing to issue and the description is pasted by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj
The namespace is techsemics, not semics. semics/remote-sql-agent does not exist and the push would have failed on the first tagged release, after npm had already published — the point at which a version number is spent and cannot be reused. techsemics/remote-sql-agent exists and is public. The GitHub slug semics-tech/remote-sql-agent is a different string and is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj
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.
Three install routes out of one tag — and a fix for two that were already broken.
The bug this found
The Linux worker tarball cannot start. It ships
rsagent-worker.mjswith nonode_modulesbeside it, but the bundle marksbetter-sqlite3and@azure/identityexternal. Reproduced on a cleannode:24container usingmain's own bundle:@azure/identitywas never optional despite being declared that way —tediousdepends on it outright and requires it at the top ofconnection.js, so it loads the momentmssqldoes. Nothing is marked external now, and the bundle is verified to run in an empty directory.The npm route hid this, because npm installed the packages anyway.
What changed
Outbox →
node:sqlite. A SEA cannot load a native addon, so this is what makes single-file executables possible at all. It also removes the last thing compiled from C++ on a customer's database server. The 15 existing outbox tests carried over unchanged; two new ones cover the hand-writtenBEGIN/COMMIT/ROLLBACKreplacing better-sqlite3'stransaction()— both fail if the ROLLBACK is removed.The cost is real:
node:sqliteis still marked experimental. Mitigated by pinning the runtime the worker ships with, and by the surface being one file.Single-file executables for linux-x64, win-x64 and darwin-arm64, each built by the OS it targets — cross-injection works, but the macOS result needs re-signing with macOS-only tooling, and an unsigned binary installed as a service on a database server is what a security review should stop. Each build runs its own output with
--rsagent-selftestbefore upload, because a failed injection leaves a perfectly healthy copy ofnode.Docker Hub alongside GHCR, same build and same digest.
latestno longer follows a prerelease — the previous form enabled it for any tag, sov0.1.0-rc.1would have become whatdocker pullreturns by default.npm install is one package now, verified with a real global install rather than a dry run.
Verifiable assets:
SHA256SUMS, a build attestation per artefact, and the Windows Node runtime checked against nodejs.org's publishedSHASUMS256.txtrather than trusted because curl exited 0.pnpm release:versionsets every manifest andWORKER_VERSIONin lockstep; the release job refuses to publish if any disagrees with the tag.Knock-on: the control-plane image drops
python3andbuild-essential, which existed only for better-sqlite3 on arm64.Verification
pnpm audit --audit-level highnode:24npm i -gfrom a packed tarballrsagenton PATHNeeds you before anything publishes
@remote-sql-agentnpm org; addNPM_TOKENsemics/remote-sql-agent; addDOCKERHUB_USERNAMEandDOCKERHUB_TOKENv0.1.0-rc.1as a live rehearsal — npm versions cannot be reusedSee docs/releasing.md.
One thing to decide
node:sqlitebeing experimental is the judgement call in here. Everything else is mechanical. If you would rather not put an experimental API on the outbox path, the alternative is dropping the single-file executables and keeping better-sqlite3 — the other two routes work either way.🤖 Generated with Claude Code
https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj