Skip to content

feat(deploy): publish to npm, Docker Hub and GitHub releases from one tag - #33

Merged
semics-tech merged 4 commits into
mainfrom
feat/release-three-routes
Jul 30, 2026
Merged

feat(deploy): publish to npm, Docker Hub and GitHub releases from one tag#33
semics-tech merged 4 commits into
mainfrom
feat/release-three-routes

Conversation

@semics-tech

Copy link
Copy Markdown
Owner

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.mjs with no node_modules beside it, but the bundle marks better-sqlite3 and @azure/identity external. Reproduced on a clean node:24 container using main's own bundle:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'better-sqlite3' imported from /w/head-real.mjs

@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 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-written BEGIN/COMMIT/ROLLBACK replacing better-sqlite3's transaction() — both fail if the ROLLBACK is removed.

The cost is real: node:sqlite is 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-selftest before upload, because a failed injection leaves a perfectly healthy copy of node.

Docker Hub alongside GHCR, same build and same digest. latest no longer follows a prerelease — the previous form enabled it for any tag, so v0.1.0-rc.1 would have become what docker pull returns 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 published SHASUMS256.txt rather than trusted because curl exited 0.

pnpm release:version sets every manifest and WORKER_VERSION in lockstep; the release job refuses to publish if any disagrees with the tag.

Knock-on: the control-plane image drops python3 and build-essential, which existed only for better-sqlite3 on arm64.

Verification

Check Result
lint / typecheck (8 projects) clean
unit 322 passed
browser (Playwright) 25 passed
integration (real SQL Server 2022) 18 passed
pnpm audit --audit-level high exit 0
control-plane image, arm64, no toolchain builds; argon2 still hashes; no gcc/python3/make in runtime
Linux tarball from the image runs on clean node:24
npm i -g from a packed tarball 1 package, rsagent on PATH
SEA binary end to end loads config, creates the RSA credential key, opens a WAL outbox with all four tables

Needs you before anything publishes

  • Create the @remote-sql-agent npm org; add NPM_TOKEN
  • Create Docker Hub semics/remote-sql-agent; add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN
  • Then cut v0.1.0-rc.1 as a live rehearsal — npm versions cannot be reused

See docs/releasing.md.

One thing to decide

node:sqlite being 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

semics-tech and others added 4 commits July 30, 2026 14:46
… 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
@semics-tech
semics-tech merged commit 45789f7 into main Jul 30, 2026
9 checks passed
@semics-tech
semics-tech deleted the feat/release-three-routes branch July 30, 2026 15:28
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.

1 participant