Skip to content

Stress line fix, first release 9000.0.1, hardened CI on Node 26 - #64

Merged
phroi merged 32 commits into
masterfrom
stress-line
Oct 25, 2026
Merged

phroi merged 32 commits into
masterfrom
stress-line

Conversation

@phroi

@phroi phroi commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

The only behaviour change here is the bot stress line. Everything else clears the way for the first release of the rewrite: the audit advisories that turned the gate red, the two release jobs that never worked, a CI that now pins and checks everything it depends on, the move to Node 26 and the latest majors. One commit per concern.

What each reader gets:

  • Operators: a bot that restores its CKB line with one matured withdrawal instead of two. The testnet journal shows both branches of that claim.
  • Integrators: @ickb/sdk 9000.0.1 on npm, for Node 26 and later, with its source beside the build and maps that reach it, published by this workflow with provenance. No registry token exists anywhere.
  • Users: the interface deployed to ickb.org from this repository, with the order line saying that a bot fills it.
  • The repository: a gate that goes red when a dependency, an action or Node has a newer release we should be on.

The stress line

The bot kept the cost of one deposit beyond the reserve. It compared spendable CKB plus requests in flight against that line. A cap-sized anchor returns about the deposit capacity, which is the receipt plus fees short of the cost. So from the floor one withdrawal never restored the line: stress stayed true in flight and after maturity, each dip took a second anchor, a quiet bot would settle near two deposits of CKB.

The line now counts the reserve, so the reserve is the slack and one anchor restores it. Coverage still keeps two deposits spendable. The accepted cost: a cap-sized seller right after restoration can be served short by up to the reserve less the receipt, still above the minimum match. The replaced test compared one shannon pending against a balance one shannon short. The new one uses the shape the journal shows and fails against the old code. Two independent reviews converged on this over comparing against the capacity, counting a request as a full line, or dropping the guard. The rationale is in the bot policy.

Testnet evidence, from this branch running on the bot since October 23: that evening a ready anchor during an in-flight request drew no withdrawal plan under the new line, where the old line would have taken it. On October 25, after the queued sells had left the pool, one maturity put the bot back above the line. The next ready anchor sat there for six turns and drew no plan.

The release

The first master push tried to release 9000.0.0 before GitHub Pages and the npm trusted publisher existed. That run cannot be replayed: its Pages artifact expired, the audit at that commit fails today. Both released packages move to 9000.0.1, so the merge of this PR is the first release. 9000.0.0 was never published. The one-time setup checklists became one paragraph naming what the two release jobs depend on outside the tree.

CI

One workflow, ci.yaml, checks and releases. A separate release workflow would either rerun the gate or trust a hand-off from it, so I kept one file and named it for what it does.

What runs:

  • GitHub-owned actions only, pinned by commit. The runner image npm installs pnpm. pnpm installs the Node the lockfile pins (devEngines.runtime, one sha256 per platform) and runs every script on it. setup-node and pnpm/action-setup are gone.
  • The SDK publishes from a pnpm deploy tree holding its 47 declared packages, built with npm run because pnpm run would reinstall the whole workspace first. The interface builds in a fresh job. The gate tooling never runs where an artifact is produced.
  • The publish job runs in the npm environment, which admits master only. The npm trusted publisher matches organization, repository and workflow file, not the branch.
  • No caching, no secrets, pull_request only.

What pull requests check beyond the gate, as pnpm fresh. These depend on the date, so release pushes skip them and a release cannot fail on something that appeared after review:

  • the audit;
  • pnpm outdated: a newer release of a dependency, majors included, once past the release age, now three days; and any newer release of a pinned action, since pnpm applies no age to actions. The one hold, TypeScript 6 until typescript-eslint supports 7, is declared with its reason;
  • scripts/tooling/pins.ts: a security release of the pinned Node major at least three days old. Nothing else audits the runtime. The October one was found by reading the announcement.

Repository settings changed alongside: Pages enabled with the Actions source, GitHub-owned SHA-pinned actions only, read-only default token, approval for every workflow run from an outside contributor, the npm environment restricted to master, the build check required to merge.

Four independent reviews, two critics over three rounds and two fresh sessions after them, went over the CI and release changes; every finding is fixed in the commits.

Also in here

  • Audit: follow-redirects and vite 8.3.4, zod 4.6.6, fast-uri 3.1.9, the last admitted by the release age, now three days.
  • Node 26.12.1, the October security release of the line that becomes LTS on October 28, pinned once in devEngines; the SDK promises >=26, the private packages run on the pin. The tracked unit examples run the checkout's node_modules/node/bin/node, so a pin change reaches a host with pull and install.
  • The latest majors everywhere the gate allows: eslint-plugin-unicorn 78, @types/node 26, and the rest of the dependencies at their newest admitted versions.
  • Documentation: the host must stay up, how to watch it from another host, coverage repair needs two deposits spendable, the seller estimate under sustained selling, the funding recommendation keeps the reserve on top of 3.2 deposits.
  • Dropped: the CNAME file (Actions-sourced Pages ignores it), the duplicate publish flags, a broken source link in the interface, an esbuild override and four audit ignores that matched nothing. The bundle uses relative asset URLs, so it works at any mount point.
  • Dependencies updated to the newest versions the release age admits, from a lockfile regenerated from scratch, which the freshness check now requires.

@phroi
phroi force-pushed the stress-line branch 2 times, most recently from b647269 to 4860578 Compare October 25, 2026 16:40
@phroi phroi changed the title fix(bot): the stress line counts the reserve, so one matured anchor restores it Stress line fix, first release 9000.0.1, hardened CI Oct 25, 2026
@phroi
phroi marked this pull request as ready for review October 25, 2026 17:37
@phroi phroi changed the title Stress line fix, first release 9000.0.1, hardened CI Stress line fix, first release 9000.0.1, hardened CI on Node 26 Oct 25, 2026
phroi added 12 commits October 25, 2026 22:36
pnpm audit fails the gate on follow-redirects below 1.16.1 (two high,
reached through CCC spore and axios) and on vite 8.3.0 to 8.3.2 (six
advisories in vite and launch-editor). An override pins the transitive
follow-redirects range to 1.16.1, following the existing security pins;
vite moves to 8.3.4, the newest release past the seven-day age rule.
The 2026-10-21 Node.js security release fixes two high TLS hostname
checks, a Brotli decoder out-of-bounds write and undici, c-ares and
CA-bundle issues in 22.x, 24.x and 26.x; 20.x got no release. The bot
is a long-running fetch-over-TLS client, so the floor moves to the
first fixed 22.x in every engines field, the CI pin and the READMEs.
…estores it

The line was one deposit's cost, receipt included, beyond the reserve, and
stress compared spendable CKB plus requests in flight against it. A cap
anchor in flight carries its capacity plus the owner marker that came out of
the liquid cells, and matured it returns about the capacity less fees: 208 CKB
plus fees under the cost. From the floor the line was never restored, in
flight or after maturity (journal 2026-10-19 08:47 to 09:49: pending
119,537 CKB against a cost of about 119,647, spendable near zero, stress
true throughout), so each dip took a second anchor and a quiet bot would
settle near two deposits of CKB, the line already rejected.

Stress now compares total CKB, reserve included, against the same cost, so
the reserve is the slack: in flight and matured, a cap anchor clears the line
by about 790 CKB. Coverage still keeps two deposits spendable, above the
line. A cap-sized seller right after restoration may be served short by up
to the reserve less the receipt, above the minimum match; the tail follows.

The replaced test used a pending amount of one shannon against a balance one
shannon short, which hid the gap; the new one uses the journal's shape.
Reviewed independently by Fable 5.1 and Opus 5.5 at d4a5b0a, both arriving
at this change over comparing against the capacity (zero margin), counting
a request as a full line (short after maturity) or dropping the guard.
The bot's request window is twenty to sixty minutes before a deposit's
claim, so a deposit maturing during an outage waits a full cycle; nothing
said so. Two days of a laptop host at 23% uptime showed it. The alerting
example forwarded every bot.turn.failed while the prose said consecutive;
each single failure in the journal recovered on the next turn. The watcher
also ran on the same host and died with it, which was the actual failure,
so the example is now an off-host staleness check. `systemctl --user
--failed` never lists a Restart=always unit, so that line is gone.
The ring section said the coverage rule repairs a thin segment once per
cycle without its precondition. Two days of testnet sell pressure showed
the tip walking empty segments for a whole window with no repair possible,
since matured CKB went to sellers and spendable never reached two deposits.
…ling

The supply model counts every pool deposit at its claim date. Wallets take
surplus deposits days before the bot can see them, downtime rolls deposits
a cycle without warning, and the bot's iCKB float caps a claim cluster.
Recorded as accepted limits; a model change needs mainnet evidence.
…says what that means

"Converts at a variable time" hid the dependency: an order is filled only
when a bot holds the other side, at the one price the interface asks, with
no priority to buy and no promise of eventual service, and an unfilled sell
returns at the age cutoff. The intent line now names the bot; the README
owns the rest, next to the fee the form already cites.
The apps/ layout predates the workspace; the link 404ed since the rewrite.
… point

Vite's default base assumed the domain root, so the deploy that lands before
ickb.org moves would serve a page whose assets 404 at ickb.github.io/stack/.
The app has no client-side routes, so relative URLs cost nothing.
… deposits

0049726 moved the stress line onto total CKB and reworded the funding
bullet to put the reserve inside the 3.2 deposits. That was wrong: a
coverage deposit still needs two deposits of CKB spendable beyond the
reserve (spendable >= 2 * depositCost), so a bot funded with the reserve
inside never seeds the ring. Restore master's wording and say why.
Transitive through @ckb-ccc/core > @joyid/ckb > @joyid/common > abitype.
The audit step also reports four fast-uri advisories fixed in 3.1.9, under
@microsoft/api-extractor > @microsoft/tsdoc-config > ajv; that version was
published 2026-10-19 and is inside the seven-day minimumReleaseAge until
2026-10-27, so the gate stays red on audit until then.
pnpm skips the minimumReleaseAge check when registry metadata lacks a
time field (minimumReleaseAgeIgnoreMissingTime, added in 11.0.0 with
default true, unchanged in the pinned 12.4.2; see
pnpm.io/settings/dependency-resolution). The seven-day rule is meant to
hold for every package, so such a package now fails instead. Lockfile
unchanged; only future resolutions are affected.
phroi added 11 commits October 25, 2026 23:05
…ctober advisories

Seven days was chosen on September 14 so that a compromised release has
time to be caught before it can be installed. The wait is also how long a
fix stays unadopted, and three days is what a pulled or regressed release
needs to show (user decision 2026-10-25). The first fix it admits: fast-uri
3.1.9, published 2026-10-19, closes GHSA-w74x-v6f9-j3xg, GHSA-wjr9-j795-jxcr
(moderate), GHSA-xhq7-xmqj-769h and GHSA-333x-w4r6-6crp (low), which the
audit reported.
The first master push (9582ab4, run 38780788044) tried to release 9000.0.0
before GitHub Pages was enabled and the npm trusted publisher existed, so both
jobs failed with 404 and nothing was published. That run cannot be replayed:
its Pages artifact expired and a full rerun would fail today's audit. The
release rule only fires on a version change against the previous commit, so
this bump makes the merge of this PR the first release; 9000.0.0 is skipped
rather than consumed, as the README now records.
…nores

The legacy site kept public/CNAME because a branch-sourced Pages site reads
the domain from the published branch. This repository publishes through
actions/deploy-pages, and GitHub's custom-domain guide states that for a
custom workflow "any existing CNAME file is ignored and is not required": the
domain is the repository's Pages setting alone.
pnpm 12.4.2 (pnpm/pnpm@9502f3c4, crates/publish/src/publish_options.rs)
falls back to publishConfig.access when --access is absent and never reads
publishConfig.provenance, so the flag and the field each duplicated or
duplicated nothing. The access setting stays in publishConfig; the explicit
--provenance flag stays because without it pnpm decides from the OIDC token
and proceeds silently without provenance when the visibility lookup fails.
…g dependencies

The maintainer's first-release steps (register the npm trusted publisher,
enable Pages, move the domain, archive the legacy repository) are being
carried out and would go stale once done. What a future maintainer needs is
what the two release jobs depend on outside the tree: the trusted publisher's
organization, repository and workflow name, and the Pages source and domain.
check.yaml also published the SDK and deployed the site. One file stays,
because a separate release workflow would either rerun the gate or trust a
workflow_run hand-off from it. The npm trusted publisher pins the workflow
filename, so the rename lands before that registration.
…setup

At its pinned commit the action runs a vendored pnpm to install pnpm@<version>
with --no-lockfile; npm, present once setup-node runs, does the same from the
packageManager field with the registry's integrity check. The workflow now
uses GitHub-owned actions only, which lets the repository's Actions policy
allow nothing else.
…ace from its own job

The publish job installed the whole workspace (736 packages) to run tsgo, and
the deploy job shipped the bundle the gate built in a job where eslint,
vitest and the rest had already executed. The publish job now resolves only
the SDK's declared dependencies from the lockfile with `pnpm deploy` (47
packages), moves that node_modules into place because deploy refuses the
package directory as a target, and runs the build script with npm: `pnpm run`
would first reinstall the whole workspace, since verifyDepsBeforeRun defaults
to install in pnpm 11+. The SDK declares tsgo itself, through the catalog so
the gate's compiler and the release's compiler stay the same version. The
interface job installs the workspace in a fresh job instead: its bundle pulls
the SDK from source, and the minimal tree cannot resolve that without a
second deploy and a typecheck-free build. The gate's tooling is never run in
either job. Verified in a scratch clone: no root node_modules in the SDK job
and a dist byte-identical to the full-workspace build.

The same README paragraph and the workflow comment said each push to master
is a squash commit; the ruleset allows only the merge method, so the release
rule compares with the merge commit's first parent, and they now say so. The
README also gains the recovery rule for a failed release.
npm's trusted publisher matches organization, repository and workflow file,
not the branch, so without an environment a workflow edit on any branch could
mint an accepted token. The `npm` environment exists in the repository with a
deployment branch policy of master and no reviewers, so it adds no step; the
trusted publisher is to be registered with that environment name. The README
also records that the package disallows tokens (Opus audit of b647269, D2/D3).
Three majors behind on both. The majors moved the actions to the node24
runtime and, for checkout v6, persist credentials in a separate file; this
workflow sets persist-credentials: false on every checkout, so neither
changes its behaviour.
devEngines.runtime pins Node once, in the root package.json; pnpm 12
resolves it like a dependency, records each platform's tarball URL and
sha256 in the lockfile, installs it under node_modules on `pnpm install`,
and runs every script on it. The workflow no longer needs setup-node or a
version of its own: the runner image's npm only installs pnpm. 26 becomes
the LTS line on October 28; 26.12.1 is the October security release, four
days old, so it passes the release age. The published SDK promises the
major, `engines.node >=26`, the same kind of claim as a dependency range;
the private packages declare no floor, since they run on the pin; the
@types/node line follows. The tracked unit examples run the checkout's
node_modules/node/bin/node, so a pin change reaches a host with pull and
install (user decision 2026-10-25: an exact pin rather than an `lts` alias,
since the host gets the locked version either way). Two notes: npm 11
enforces devEngines in the repository root, so inside the checkout npm runs
as `pnpm exec npm ...`; the SDK publish job builds in a deploy tree without
devEngines, so its two build commands run on the image's Node, which cannot
affect the artifact (tsgo is native, the rewrite script edits specifiers).
phroi added 8 commits October 25, 2026 23:05
…ts only

Nothing audited the actions or asked for newer dependencies, and the audit
sat inside `pnpm check`, whose result therefore moved with the advisory
database while the tree stood still: the base commit passed the gate on
October 15 and fails it today. A release push running that gate could fail
on an advisory or a release that appeared after review, with no rerun able
to recover and the next merge releasing nothing. `pnpm check` is now a
function of the tree. `pnpm fresh` holds the date-dependent checks and CI
runs it on pull_request events only, where the required `build` check still
enforces it: the audit, and `pnpm -r outdated --include-github-actions`,
which reports every newer release of a dependency past the three-day age,
majors included, and any newer release of a pinned action (pnpm applies no
age to actions; the Actions policy limits them to GitHub-owned ones). The
project tracks the latest majors; the one hold, TypeScript 6 while
typescript-eslint declares `typescript <6.1.0`, is an update.ignoreDeps entry
with its reason beside it. Verified at pnpm 12.4.2: outdated honours
minimumReleaseAge and exits 1 when outdated; a bare SHA pin is matched to
its tag through git, with no API or token.
pnpm outdated does not report devEngines, and the resolver applies no release
age to it (a ^22.23.0 range resolved a five-day-old release under a
seven-day rule), so the runtime had no freshness check: the October security
release was found by reading the announcement. scripts/tooling/pins.ts, run
by `pnpm fresh`, reads the pin from package.json, requires the SDK's engines
floor to be the pin's major, and fails when the Node project lists a newer
release of that major flagged as a security release that is at least three
days old (user decision 2026-10-25). Non-security releases are not flagged.
Two majors behind. The new no-top-level-side-effects rule is off for config
modules, which exist to build a value at top level; a test helper's fallback
expression becomes a default parameter.
The exact 19.3.0 went stale when the app moved to 19.3.1; a caret range keeps
the connector on the same single React without an edit per patch release.
The emitted maps pointed at the source through a fixed `sourceRoot` of
`../src`, which the dist-only tarball did not contain and which, resolved
against a nested map's own location, named `dist/src/...` for 46 of the 60
references. `src` now ships beside `dist`, so an integrator's debugger shows
the code and anyone can read what they installed, and `sourceRoot` is gone,
so each map carries its own relative path. The build's exclude list and the
matching `files` negations guarded against test files under `src`, where
none live (tests are under `test/`); both are deleted (Fable and Opus
reviews of e3d220b).
The lockfile is regenerated from scratch under the three-day age and the
direct ranges move to the newest admitted releases; the freshness check
requires this state.
…as run

Rerunning an older failed deploy or publish would put a superseded bundle
or version back in front (Opus audit of 78f4aef, B).
…age-size ignores

None matches an installed package: no esbuild or @esbuild/* and no
image-size appears in the lockfile, so the override only rewrote vite's
optional peer range and the two advisory ignores silenced nothing (Opus
review of 748389f). The braces ignore under the same React Native chain
stays while that package is present.
The regenerated lockfile resolves patched versions of both, so the audit
without these entries reports only the elliptic advisory that stays ignored
(Fable review of 748389f).
@phroi
phroi merged commit 8daadbc into master Oct 25, 2026
3 checks passed
@phroi
phroi deleted the stress-line branch October 25, 2026 22:26
@phroi

phroi commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

Merged!! The diff is one bot fix plus a lot of plumbing, so here is the record.

What the diff doesn't show: three days of work and a history rewritten twice to read as one story. Read in order, I see four phases:

  1. 23 Oct, the stress line: the bot kept one deposit beyond the reserve and never got it back with one withdrawal. The fix counts the reserve, and the branch ran on the testnet bot from that afternoon, with both sides of the claim showing in the journal by the 25th.
  2. 23 to 24 Oct, what the reviews and the testnet surfaced: uptime as the operating requirement, coverage repair, the seller estimate, the funding recommendation, the order intent line. Plus the audit pins and the Node security release of 21 Oct.
  3. 25 Oct, the first release: Pages, the npm trusted publisher, the workflow rebuilt around GitHub-owned actions with the SDK published from a deploy tree, the repository settings tightened, four independent reviews over several rounds.
  4. 25 Oct, Node 26 and the freshness checks: the runtime pinned once and installed by pnpm, the gate split into what depends on the tree and what depends on the date, the latest majors, a lockfile regenerated from scratch.

Now live: @ickb/sdk 9000.0.1 on npm with provenance, ickb.org served from here, the 1000.x versions deprecated, the two legacy repositories archived.

Phroi %378

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