Skip to content

feat(bindings): publish the Bundle Pin scripts as the npm package zingolib-bundle-pin - #2941

Merged
juanky201271 merged 7 commits into
devfrom
feat/bundle-pin-package
Oct 13, 2026
Merged

juanky201271 merged 7 commits into
devfrom
feat/bundle-pin-package

Conversation

@zancas

@zancas zancas commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

zingolib#2930 rules that the scripts a consumer runs to hold its Bundle Pin and fetch the Prebuilt Bundles are defined in one place. This pull request adds that place. Nothing is deleted.

What changes

bindings/pin is the npm package zingolib-bundle-pin. It carries the modules kitchen-sync proved in kitchen-sync#1 and kitchen-sync#2 under lib/. Two commands under bin/ replace the entry scripts: pin-binding-layer <commit> moves the pin, and fetch-binding-layer --platform <platform> prints the path of the pinned bundle. Both take the consumer's root from the working directory, with --root and --pin overrides, because a file under node_modules cannot find that root by its own path.

The modules differ from the kitchen-sync copies in four ways. The registry obtains one token per repository and passes it to the manifest and blob requests. parsePin returns a malformed-pin outcome for text that is not JSON. The android bundle's directory holds a marker file that names the bundle's file, as the iOS placement's marker names its digest, so a held bundle is matched by name. The disk helpers for an absent file and for the atomic replacement of a directory live once, in lib/disk.mjs.

The package depends on nothing. Its 26 tests run on the test runner that Node ships, so zingolib gains no Node dependency and no lock file.

bundle-pin.yaml is a new workflow with two jobs. The check job runs the tests, lists the files npm pack would publish, and fails a pull request that changes the package without raising its version. The publish job runs on a push to dev and publishes the package when npm lacks the version package.json names. The published content is therefore always a merged commit, and the provenance names that commit. The publish uses npm trusted publishing, so only that job holds id-token: write, and no npm token is stored. binding-layer-publish.yaml is unchanged.

Set up by hand, once

The npm scope zingolabs is held by an account that has published nothing, so the package is unscoped and needs no organization.

  1. Publish version 0.1.0 by hand from bindings/pin with npm publish --access public. npm configures a trusted publisher on a package that exists, so the first version cannot come from the workflow.
  2. On npmjs.com, in the package's settings, add a trusted publisher that names this repository and the workflow file bundle-pin.yaml. The workflow publishes every later version.
  3. Add the check Bundle Pin package to the required checks of dev.

Open

AGENTS.md's scripting rule names Rust in the workbench crate for committed tooling. The consumers of this package hold no Rust, so the package is Node. The rule needs an exception for bindings/pin, which this pull request does not write.

bindings/published.toml could record the package version beside each entry's digests. That is deferred.

Follow-ups

After the first publish, kitchen-sync installs the package and deletes its scripts/. Step 4 of zingolib#2930 installs it in zingo-mobile.

🤖 Generated with Claude Code

…ngolabs/bundle-pin

zingolib#2930 rules that the scripts a consumer runs to hold its Bundle Pin and fetch the Prebuilt Bundles are defined in one place. That place is bindings/pin, beside the Android and Swift packaging, so the scripts and the bundle layout they read change in one pull request. The package carries the modules kitchen-sync proved, with two commands, pin-binding-layer and fetch-binding-layer, that take the consumer root from the working directory. It depends on nothing, and its tests run on the runner Node ships.

The Binding Layer publish workflow publishes the package by npm trusted publishing when npm lacks the version the package names, and a new check fails a pull request that changes the package without raising that version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@zancas
zancas requested a review from juanky201271 October 12, 2026 23:18
zancas and others added 2 commits October 12, 2026 16:19
The registry-url setting of setup-node writes an npmrc entry that reads an auth token from an environment variable the job never sets. The empty entry can stand in the way of the OIDC exchange that trusted publishing performs, and the default registry is already npmjs.org, so the setting goes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The npm scope zingolabs is held by an account that has published nothing, and whose holder is unknown. An unscoped name needs no organization, and trusted publishing serves it the same way, so the package is zingolib-bundle-pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@zancas zancas changed the title feat(bindings): publish the Bundle Pin scripts as the npm package @zingolabs/bundle-pin feat(bindings): publish the Bundle Pin scripts as the npm package zingolib-bundle-pin Oct 13, 2026
zancas and others added 3 commits October 12, 2026 21:11
… scripts

The npm publish of bindings/pin leaves the /publish job and becomes a
publish job of the "Bundle Pin package" workflow that runs on a push to
dev. The /publish job checked out the pull request's branch head, so the
package shipped unmerged content, and its provenance named the default
branch's head, a commit that did not hold the published files. A publish
from dev names the merged commit, removes the race in which a later push
to the pull request found its own version already on npm, and lets a
failed publish retry on the next push instead of being refused with the
recorded Binding Layer entry. Only the publish job holds id-token: write,
and it installs a pinned npm rather than npm@latest.

The scripts gain four fixes. The digest directory counts as held by its
one visible entry, so a stray dotfile no longer forces a download whose
rename then fails on the existing directory, and a directory that holds
no bundle is replaced. parsePin returns a malformed-pin outcome for text
that is not JSON and for null. fetch-binding-layer without --platform
reports a message that names the platforms. The registry obtains one
token per repository and passes it to the manifest and blob requests.

Every failure message names pin-binding-layer, the published command,
in place of the kitchen-sync alias yarn pin, and the header comments
leave the lib and bin files. The tests cover each fixed behavior, and
the README describes the publish.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juanky201271

Copy link
Copy Markdown
Contributor

Tests pass 25/25 on Node 22, npm pack lists only bin, lib and package.json, the annotation keys and repository names match binding_publish.rs, and the lib diff against kitchen-sync is exactly what the last commit describes.

  • The PR body is stale on the setup: step 2 names binding-layer-publish.yaml as the trusted publisher workflow, but the publish now lives in bundle-pin.yaml. Whoever follows the body configures the wrong workflow and every publish fails. Same for "publishes the package after the bundles" and "unchanged".
  • Duplication: the ENOENT-to-fallback catch is written four times (readPin, held, text, exists), the atomic replace (rm partial, mkdir, write, rm home, rename) twice in fetchBundle and placeSwiftPackage, and the default pin path in both bins. The gate only scans Rust, so CI will not catch it.
  • held() treats any directory with one visible file as the bundle, whatever its name, while two stray files get replaced. Low risk since downloads are atomic, but inconsistent.

…e by name

The ENOENT fallback that readPin, held, text, and exists each wrote now
lives once in lib/disk.mjs as text and exists. The atomic replacement
of a directory, which fetchBundle and placeSwiftPackage each spelled
out, is replaceDirectory there: it fills a partial directory, discards
the partial directory when the fill reports anything but success, and
otherwise removes the old directory and renames the partial one into
its place. Both bins take the default pin path from pinPath.

The android bundle's directory gains a marker file, bundle, that names
the bundle's file, as the iOS placement's marker names its digest. held
reads the marker and checks that the named file exists, so a directory
that holds one stray file no longer counts as the bundle. The marker is
written after the download verifies, inside the partial directory, so
its presence proves a complete bundle. A directory written before this
commit holds no marker and is downloaded once more.

The tests cover the marker, the replacement of an unmarked directory,
and the removal of the partial directory after a corrupt download or a
package without a manifest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@zancas

zancas commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

Commits d5a8b08 and 0fd6850 answer the ten findings of the recall review and the three points above.

  1. Publish source. The npm publish leaves the /publish job. It is now the publish job of bundle-pin.yaml, which runs on a push to dev. The published content is always a merged commit, and the provenance names that commit.
  2. No retry after a failed publish. The publish no longer shares a job with binding-publish. A failed publish leaves npm without the version, and the next push to dev publishes it.
  3. held and a stray dotfile. held no longer counts entries. See the marker below.
  4. parsePin and invalid JSON. parsePin returns malformedPin with the field JSON for text that does not parse, and with the field commit for null.
  5. yarn pin in the messages. Every message names pin-binding-layer.
  6. Version lockout. The publish from dev removes the race. A pull request can no longer publish its own interim version.
  7. Header comments. The seven // headers are gone from lib and bin.
  8. id-token: write at the workflow level. Only the publish job holds the grant. binding-layer-publish.yaml is unchanged from dev.
  9. --platform omitted. pinnedBundle returns unnamedPlatform before it reads the pin. The message names both platforms.
  10. Two tokens per fetch. access runs once per repository, and resolveBundle and download take the grant. resolveTag is the one-shot that pin-binding-layer uses. The publish job installs npm 12.2.0 rather than npm@latest.

On the three points above:

  • The PR body is corrected. It names bundle-pin.yaml, the two jobs, and the four differences from the kitchen-sync copies.
  • The duplication is folded. lib/disk.mjs holds text and exists, which share one ENOENT fallback, and replaceDirectory, which both fetchBundle and placeSwiftPackage call. pinPath gives both bins the default pin path.
  • held matches by name. The android bundle's directory holds a marker file, bundle, that names the bundle's file. The marker is written inside the partial directory after the download verifies, so its presence proves a complete bundle. The directory's own name is the digest, so the marker names the file rather than repeating the digest. A directory written before this commit has no marker and is downloaded once more.

The suite is 26 tests. A live pin-binding-layer and fetch-binding-layer of ba7336710 for android ran against ghcr.io, and a second fetch came from disk through the marker.

@juanky201271 juanky201271 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

goooood!

@juanky201271
juanky201271 merged commit 70362bd into dev Oct 13, 2026
40 checks passed
@juanky201271
juanky201271 deleted the feat/bundle-pin-package branch October 13, 2026 18:17
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.

2 participants