Skip to content

Add npm package distribution infrastructure - #5

Merged
kjanat merged 36 commits into
masterfrom
claude/npm-platform-packages-1gQec
May 4, 2026
Merged

kjanat merged 36 commits into
masterfrom
claude/npm-platform-packages-1gQec

Conversation

@kjanat

@kjanat kjanat commented May 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR adds a complete npm distribution system for runner-run, enabling users to install prebuilt binaries via npm without requiring a Rust toolchain. The implementation uses a façade package pattern with per-platform sub-packages distributed via optionalDependencies.

Key Changes

  • Build system (npm/scripts/build-packages.mjs): New script that generates npm package trees for the façade and all platform-specific sub-packages. Extracts native binaries from release tarballs and generates appropriate package.json files with platform constraints (os, cpu, libc fields).

  • Publish system (npm/scripts/publish.mjs): New script that publishes all generated packages to npm registry, publishing sub-packages first then the façade to ensure dependency resolution.

  • Platform matrix (npm/targets.json): Configuration file defining 14 supported platform targets across Linux (gnu/musl variants), macOS, Windows, FreeBSD, NetBSD, and OpenBSD with tier levels and build tool specifications.

  • Façade package (npm/facade/):

    • package.json: Main entry point declaring all sub-packages as optionalDependencies
    • bin/runner.js and bin/run.js: Shim scripts that resolve and execute the correct platform-specific binary at runtime
    • lib/resolve.js: Binary resolution logic that locates the installed sub-package and provides helpful error messages if none are found
    • README.md: User-facing documentation with installation instructions and troubleshooting guide
  • CI/CD (.github/workflows/npm-release.yml): New workflow that triggers after successful release builds, downloads release artifacts, verifies checksums, builds npm packages, and publishes to npm registry with optional dry-run and provenance support.

  • Release workflow update (.github/workflows/release.yml): Expanded build matrix from 2 to 14 targets with per-target runner specifications and build tools (cargo vs cross), added experimental flag support, and changed archive format to include all platforms.

  • Documentation (README.md): Added npm installation instructions and explanation of the façade pattern.

Implementation Details

  • The build script uses a custom tar.gz parser to avoid adding npm dependencies, extracting only the required binaries from release archives.
  • Platform detection happens at runtime via require.resolve(), avoiding postinstall scripts and network access during installation.
  • The system gracefully handles missing platforms with --skip-missing flag for partial matrix builds.
  • Sub-packages are published before the façade to ensure dependency resolution works immediately.

Comment thread .github/workflows/npm-release.yml Fixed
@coderabbitai

coderabbitai Bot commented May 2, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 53fcf88f-1f9c-4564-b1c7-c052a126e2a2

📥 Commits

Reviewing files that changed from the base of the PR and between 6e24cb4 and b404098.

📒 Files selected for processing (4)
  • .github/scripts/build/derive-dist-dry.sh
  • .github/scripts/publish/npm.sh
  • .github/workflows/release.yml
  • npm/scripts/build-packages.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout of 18000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: verify
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (actions)
🧰 Additional context used
📓 Path-based instructions (1)
.github/**/*.{yml,yaml}

⚙️ CodeRabbit configuration file

Do not ever warn about stylistic yamllint shit. Do warn about security related shit. Insecure shell handling of user supplied / defined strings: think of branch names, inputs, pr content, anything needs to be string interpolation and permission safe.

Files:

  • .github/workflows/release.yml
🧠 Learnings (1)
📚 Learning: 2026-05-04T15:23:38.296Z
Learnt from: kjanat
Repo: kjanat/runner PR: 5
File: .github/workflows/release.yml:73-73
Timestamp: 2026-05-04T15:23:38.296Z
Learning: For GitHub Actions workflows in this repository (kjanat/runner), maintain the maintainer’s preference for “floating” action tags (e.g., `v6`, `v7`) instead of pinned full commit SHAs. During code review, do not flag action usages in `.github/workflows/**/*.yml` for not being pinned to full commit SHAs or suggest switching to SHA pinning.

Applied to files:

  • .github/workflows/release.yml
🪛 YAMLlint (1.38.0)
.github/workflows/release.yml

[error] 132-132: too many spaces inside braces

(braces)


[error] 132-132: too many spaces inside braces

(braces)


[error] 135-135: too many spaces inside braces

(braces)


[error] 135-135: too many spaces inside braces

(braces)


[error] 138-138: too many spaces inside braces

(braces)


[error] 138-138: too many spaces inside braces

(braces)

🔇 Additional comments (12)
npm/scripts/build-packages.ts (4)

83-103: Arrr, ye fixed the silent field-droppin' issue! Well done, ye scurvy dog!

The narrowRepository helper now properly throws when type or directory be present but ain't strings, instead of quietly tossin' 'em overboard. Same goes fer the other narrow helpers. This be exactly what I wanted to see — fail fast with a useful error instead of shippin' garbage metadata. 🏴‍☠️


374-392: LGTM, ye salty sea dog!

The CLI argument parsing be shipshape — defaults are sensible, skipMissing properly defaults to false, and the downloads directory resolution handles both explicit paths and the default location. Clean code, no barnacles.


685-710: Aye, this tar parser be seaworthy!

Custom tar parsing that handles the essentials: prefix/name path assembly, octal size parsing with validation, bounds checking to prevent reading past the end of the buffer. Ye didn't pull in a heavy dependency fer a simple extraction task — a captain after me own heart.


869-893: The main logic be tight as a ship's hull!

The orchestration be exactly right: read manifest, parse options, clean dist, build each platform package (respecting --only filter and tier-based skipping), then refuse to build a facade with empty optionalDependencies. That safeguard at lines 886-890 prevents publishin' a useless facade that promises nothin'. Good defensive coding, matey!

.github/workflows/release.yml (3)

63-92: Arrr, this matrix generation be clever and secure!

Single source of truth from npm/targets.json, no user-controlled input interpolation, persist-credentials: false on checkout. The jq processing be clean — all input comes from yer own repo file. No injection risks here, captain!


93-119: The upload-assets job be properly wired to the matrix now!

Dependencies on both matrix and create-release, runner selection from the generated matrix, continue-on-error properly tied to the experimental flag with explicit == true comparison. The tar: all change ensures consistent archive format across platforms. Smooth sailin'!


148-160: Aye, ye added the smoke test! Me heart be singin'!

The binary sanity check at lines 153-160 be exactly what I asked fer — verify the packaged linux-x64-gnu binary exists, is executable, and actually runs with --version before uploadin' the artifact. This catches packaging slips before they escape to the publish job. The comment explains why only linux-x64-gnu is tested (only executable on ubuntu-latest). Excellent work, ye magnificent sea dog! 🏴‍☠️

.github/scripts/build/derive-dist-dry.sh (1)

11-42: Shiver me timbers, ye nailed the input validation!

This be exactly what I wanted to see from the past review:

  1. dist-tag validation (lines 16-19): Regex ^[A-Za-z][A-Za-z0-9._-]*$ matches npm's dist-tag rules, rejects malformed/flag-like values, fails with clear error message.

  2. dry-run normalization (lines 32-38): Case-insensitive comparison via ${INPUT_DRY_RUN,,}, strict true/false/empty validation, error on anything else.

No more slippin' garbage values into GITHUB_OUTPUT. This be proper defensive coding, ye beautiful pirate! 🏴‍☠️

.github/scripts/publish/npm.sh (4)

88-102: Arrr, these security defenses be watertight!

Rejectin' .npmrc files AND publishConfig in package.json — ye understand that CLI --registry don't override scoped publishConfig.registry, so both checks are needed. The comments explain the threat model clearly. This be proper defense-in-depth against token exfiltration, ye paranoid (complimentary!) pirate! 🏴‍☠️


115-151: Blimey, the optionalDependencies validation be complete!

This be exactly what I asked fer:

  1. Scope enforcement (lines 124-127): All deps must be under @runner-run/
  2. Platform allowlist (lines 128-132): No unexpected packages can sneak in
  3. Version pinning (lines 133-136): All deps must match EXPECTED_VERSION
  4. Required platforms (lines 139-145): All tier-1/2 must be present
  5. Platform package restriction (lines 146-150): Only facade may have optionalDeps

A tampered artifact can't smuggle malicious transitive deps through the facade. This addresses me past concern perfectly. Ye've earned yer rum ration today! 🏴‍☠️


160-205: Ye added the timeouts AND explained the tricky bits! Me perfectionist heart be full!

Both npm view and npm publish be wrapped in timeout 120s, with explicit exit code 124 detection fer timeout failures. The comment at lines 185-189 explains why || status=$? is the only correct form under set -e — that's the kind of documentation that saves future sailors from sinkin' the ship.

The EPUBLISHCONFLICT tolerance at lines 199-202 handles the TOCTOU race gracefully. This addresses all me past concerns about hung registry calls. Proper work, captain! 🏴‍☠️


208-222: The publish order be exactly right!

Platform packages first (required then optional), facade last — this ensures npm can resolve the facade's optionalDependencies on install. The true/false required flags match the tier expectations. Simple, correct, well-commented. No complaints from this perfectionist pirate!


📝 Walkthrough

Add npm package distribution infrastructure for prebuilt binaries

Introduce a complete npm distribution and publishing pipeline so users can install
prebuilt native binaries for runner-run without a Rust toolchain. This implements
a façade package pattern: a top-level runner-run package ships small Node shims
and declares per-platform subpackages as optionalDependencies (filtered by
os/cpu/libc), so only the matching platform package is installed. There are no
install-time network calls or postinstall scripts; runtime resolution uses
require.resolve() and the façade execs the native binary while preserving exit
codes and signals.

What changed

  • Façade package
    • npm/facade/: ESM-enabled package.json (exports/imports), CLI shims
      (bin/run.cjs, bin/runner.cjs), lib/resolve.cjs (runtime resolution + rich
      error output), lib/launch.cjs (synchronous spawn with correct signal
      forwarding), README.md and LICENSE. The generated façade's optionalDependencies
      are populated at build time to reference per-platform packages.
  • Platform packages & build
    • npm/scripts/build-packages.ts: generates npm/dist// packages by
      extracting native binaries from release tarballs (manual tar.gz parser),
      emits package.json with platform constraints (os/cpu/libc), README and
      LICENSE. Supports --only, --skip-missing, partial-matrix builds and derives
      metadata from Cargo.
  • Publishing & hardening
    • npm/scripts/publish.ts and .github/scripts/publish/npm.sh: validate artifacts,
      enforce layout/publish restrictions (no per-package .npmrc/publishConfig),
      require expected package names/versions, publish platform packages first
      then the façade, skip already-published exact versions, support dry-run and
      provenance, and treat known publish-race errors as idempotent no-ops.
    • Commit adds stricter input validation, facade optionalDependencies checks,
      npm view/publish timeouts, and a runtime smoke-test of a packaged linux-x64-gnu binary.
  • CI/CD & release helpers
    • .github/workflows/release.yml: expanded to generate a 14-target build matrix
      from npm/targets.json, per-target runners and build-tool selection (cargo/cross),
      unified archive packaging, continue-on-error for experimental targets, and
      a build-npm-dist job that verifies checksums and smoke-tests artifacts.
    • .github/workflows/npm-release.yml: publishes npm-dist from a successful
      release run or manual dispatch; derives dist-tag and dry-run behavior.
    • Build scripts: .github/scripts/build/download-release-archives.sh,
      verify-checksum.sh, derive-dist-dry.sh, and build wrapper
      .github/scripts/build/build-npm-packages.sh to orchestrate downloads,
      checksum verification, package generation and publishing.
  • Targets & validation
    • npm/targets.json: 14 targets (Linux gnu/musl × x64/arm64/armv7, macOS,
      Windows, FreeBSD, NetBSD, OpenBSD) with tier and build-tool metadata.
    • npm/targets.schema.json: JSON Schema validating facade, scope, binaries and
      target entries (including conditional rules for Linux libc and experimental→tier 3).
  • Developer tooling & docs
    • justfile: local build/test helpers for npm packaging and test-release helper
      to assemble and validate npm packages from local builds.
    • tsconfig.json, .gitattributes, .zed/settings.json, .dprint.json tweaks,
      and .gitignore updates to ignore npm/dist and related artifacts.
    • Documentation: README.md and CHANGELOG.md updated with npm installation
      instructions and façade explanation; npm/facade/README.md added with usage and troubleshooting.

Repository & build metadata updates

  • Cargo.toml bumped to 0.6.0 and rust-version to 1.95; build.rs added to export
    author metadata as compile-time env vars.
  • src/lib.rs updated to consume compile-time author env vars and adjust CLI
    byline behavior.

Behavioral notes

  • Installation: npm install runner-run installs only the matching @runner-run/
    optional dependency for the current platform (filtered by os/cpu/libc).
  • Runtime: façade shims find and exec the platform binary at runtime via
    require.resolve(); no network access or postinstall scripts are required.
  • Publishing: platform packages are published before the façade; publishing
    scripts validate package layouts/versions, enforce policies, support dry-run
    and provenance, and handle publish races safely.

This delivers an end-to-end, tiered npm distribution flow—packaging, verification,
CI integration, and publishing—so consumers can install runner-run prebuilt
binaries without a Rust toolchain while preserving reproducible and auditable
release practices.

Walkthrough

Adds end-to-end npm prebuilt-binary distribution and publish pipeline: targets schema and matrix, facade package and runtime shims, TypeScript packager to assemble per-platform npm packages from release tarballs, publish tooling (Node + Bash), GitHub Actions workflows to build and publish artifacts, plus docs, CI wiring, and Rust/Cargo metadata changes.

Changes

npm Prebuilt Binary Distribution & Publishing

Layer / File(s) Summary
Schema & Manifest (data shape)
npm/targets.schema.json, npm/targets.json, npm/facade/package.json
Adds JSON Schema and targets matrix describing facade, scope, binaries, and platform targets; introduces facade manifest runner-run@0.6.0 with CLI bins and import mappings.
TypeScript & Toolchain
tsconfig.json, rust-toolchain.toml
Adds TS project config for npm tooling and pins Rust toolchain to 1.95 with rustfmt/clippy/rust-analyzer components.
Core Packager & Publisher
npm/scripts/build-packages.ts, npm/scripts/publish.ts
Implements packager to extract binaries from release tarballs into npm/dist/*, generate per-platform packages and facade with optionalDependencies; adds publisher to validate artifacts and publish per-target (idempotent, facade last).
Facade Runtime & Bins
npm/facade/lib/resolve.cjs, npm/facade/lib/launch.cjs, npm/facade/bin/*.cjs
Adds runtime shim resolving platform-specific optional sub-packages, detailed diagnostics when missing, and a launcher that spawns the resolved native binary; provides run and runner CLI shims.
Shell Build/Publish Helpers
.github/scripts/build/*.sh, .github/scripts/publish/npm.sh, .github/scripts/build/build-npm-packages.sh
Adds scripts to download release archives, verify SHA-256 checksums, derive dist-tag/dry-run, invoke the TypeScript packager, and a Bash publisher enforcing allowlists and safety checks.
Workflows / CI wiring
.github/workflows/release.yml, .github/workflows/npm-release.yml
Generates a matrix from npm/targets.json, adds build-npm-dist job to build/upload npm-dist artifact; adds npm-release workflow to download the artifact and publish to npm (triggered by successful release workflow_run or manual dispatch).
Rust/Cargo integration
Cargo.toml, build.rs, src/lib.rs, justfile
Bumps crate version and Rust minimum, moves authors metadata into package.metadata, emits RUNNER_AUTHOR_* env vars via build.rs, updates CLI byline to use emitted env var, and adds justfile targets for building/testing npm packages.
Docs, configs & misc
README.md, npm/facade/README.md, CHANGELOG.md, .gitignore, .gitattributes, .dprint.json, .zed/settings.json
Adds facade README and install docs, changelog entries, ignores for npm artifacts, gitattributes, dprint plugins, editor settings, and minor formatting/doc updates.

sequenceDiagram
mermaid
sequenceDiagram
actor Maintainer
participant GH as "GitHub Actions"
participant ReleaseWF as "release workflow"
participant Builder as "build-npm-dist job"
participant NpmRel as "npm-release workflow"
participant Publish as "publish job"
participant Registry as "npm registry"

Maintainer->>GH: push release tag
GH->>ReleaseWF: trigger release workflow
ReleaseWF->>ReleaseWF: build artifacts + checksums
ReleaseWF->>GH: upload release assets
GH->>NpmRel: workflow_run trigger or manual dispatch
NpmRel->>Builder: download archives, verify, build npm packages
Builder->>GH: upload npm-dist artifact
NpmRel->>Publish: download npm-dist, derive dist-tag/dry-run
Publish->>Registry: npm view / npm publish per-target, facade last
Registry-->>Publish: publish responses
Publish->>GH: emit package-url outputs

Possibly related PRs

  • kjanat/runner#2: Modifies CLI core in src/lib.rs (CLI configuration/dispatch and run-alias behavior), overlapping with the src/lib.rs changes here.

Poem

Avast — tarballs stowed, the matrix set to sail,
Facade scouts its engines from optional deps’ trail,
Scripts lash checksums, pack and publish in a row,
Workflows hoist artifacts, and npm takes the bow,
Land ho — the packages ashore, port reports a-gleam 🏴‍☠️

🚥 Pre-merge checks | ✅ 6 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Changelog Update ⚠️ Warning Version bumped to 0.6.0 with source code changes. CHANGELOG.md updated under [Unreleased] but lacks required new section header '## [0.6.0] - YYYY-MM-DD'. Create new CHANGELOG section '## [0.6.0] - YYYY-MM-DD' with the current [Unreleased] items moved into it, then reset [Unreleased] as a template.
Semver Version Bump Validation ⚠️ Warning Significant breaking changes: 'run-alias' feature removed, default feature changed, binary feature gate changed. Only MINOR bump (0.5.0→0.6.0), but MAJOR bump required. Version should be 1.0.0 or 0.7.0 to reflect breaking changes introduced in this PR.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add npm package distribution infrastructure' clearly describes the main change—introducing npm distribution system for runner-run—and follows conventional commit guidelines with the 'add' prefix.
Description check ✅ Passed The description comprehensively relates to the changeset, detailing the npm distribution system implementation including build scripts, publish workflow, platform matrix, façade package structure, CI/CD updates, and documentation changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Agents.Md Documentation Updated ✅ Passed No AGENTS.md exists in the repository. The check only applies when AGENTS.md exists. Since this prerequisite is unmet, the check is not applicable.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

coderabbitai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation enhancement New feature or request labels May 2, 2026
@kjanat kjanat self-assigned this May 2, 2026
coderabbitai[bot]

This comment was marked as duplicate.

coderabbitai[bot]

This comment was marked as resolved.

kjanat pushed a commit that referenced this pull request May 2, 2026
CodeRabbit security review on #5 flagged two real issues in the new
split-job npm-release workflow:

- The publish job was iterating npm/dist/*/ and reading each
  package.json blindly. The artifact comes from the unprivileged build
  job which executes tag-supplied code, so a malicious tag could drop
  extra package directories that would then be published with
  NPM_TOKEN. Now publishes only a hardcoded allowlist (mirrors
  targets.json on master), verifies each package.json's `name` matches
  the expected scope/key, and fails the job if the artifact contains
  anything outside the allowlist.

- For workflow_dispatch runs, RELEASE_SHA was set to github.sha (the
  workflow file's commit on the default branch) instead of the
  user-supplied tag. Manual backfill runs would check out master and
  build façade files from there around the tag's binaries. Now uses
  inputs.tag for dispatch (workflow_run path keeps head_sha).
coderabbitai[bot]

This comment was marked as resolved.

kjanat pushed a commit that referenced this pull request May 2, 2026
Two further security findings from CodeRabbit's review of #5:

- The publish loop trusted .version from the artifact-supplied
  package.json. Allowlist + name verification prevented publishing
  unexpected packages, but a malicious tag could still stamp an
  arbitrary new version onto an allowed package and have it published
  with NPM_TOKEN. Now derive EXPECTED_VERSION from the trigger tag
  (trusted metadata, recomputed in the publish job from
  workflow_run.head_branch / inputs.tag) and reject any package whose
  version doesn't match — strict string comparison, before npm view or
  npm publish.

- --skip-missing was always passed to build-packages.mjs, so a
  release-triggered run could silently ship a half-empty matrix.
  Removing the flag entirely would break tier-3 BSDs (release.yml runs
  them with continue-on-error: true). Instead, enforce the safety
  property at the privileged publish step: split the allowlist into
  REQUIRED (tier 1/2) and OPTIONAL (tier 3 / experimental). Required
  missing fails the job; optional missing is skipped.
coderabbitai[bot]

This comment was marked as resolved.

kjanat pushed a commit that referenced this pull request May 2, 2026
Two more findings from CodeRabbit's review of #5:

- build-packages.mjs always ran with --skip-missing, so a missing
  required (tier-1/2) target was deferred to the privileged publish
  job's required-vs-optional check rather than failing fast in the
  unprivileged build job. Make the script tier-aware: tier-3
  experimental targets are silently skipped when their tarball is
  absent (release.yml runs them with continue-on-error: true), tier
  1/2 fail the job. The workflow now only passes --skip-missing for
  manual workflow_dispatch backfills.

- The publish loop's pre-check (npm view) is followed by npm publish
  with a TOCTOU window. If another actor publishes the same version
  between the two calls, npm exits with EPUBLISHCONFLICT and the job
  dies. Capture stderr, swallow the specific error, and treat it as a
  no-op — mirrors the logic in npm/scripts/publish.mjs.
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Comment thread .github/workflows/npm-release.yml Fixed
Comment thread .github/workflows/npm-release.yml Fixed
Comment thread .github/workflows/npm-release.yml Fixed
Comment thread .github/workflows/npm-release.yml Fixed
coderabbitai[bot]

This comment was marked as outdated.

Comment thread .github/workflows/npm-release.yml Fixed
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as low quality.

@kjanat
kjanat marked this pull request as ready for review May 4, 2026 14:16
@kjanat

This comment was marked as low quality.

@coderabbitai

This comment was marked as low quality.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

kjanat added 12 commits May 5, 2026 00:57
Two findings from CodeRabbit review against the npm package
generator and the standalone publish script:

- `build-packages.ts` only copied `lib/resolve.cjs` into the
  façade dist tree, so the published `runner-run` shipped without
  `lib/launch.cjs` even though both `bin/run.cjs` and
  `bin/runner.cjs` `require("#launch")`. Every
  `npm install runner-run && run` would have thrown
  `MODULE_NOT_FOUND: '#launch'`. Mirror the existing bins loop
  pattern so adding entries to the `imports` map is a single-list
  edit going forward.

- `publish.ts` ran `npm view` / `npm publish` without an explicit
  `--registry`, inheriting whatever was in ambient `.npmrc`. The
  CI workflow's `npm.sh` already pins the registry and passes
  `--ignore-scripts`, but `publish.ts` is its own invocation path
  (its usage block advertises `node npm/scripts/publish.ts ...`).
  Default `--registry` to `https://registry.npmjs.org/`,
  overridable for testing, and add `--ignore-scripts` to the
  publish call.
- `build-packages.ts`: swap `createReadStream().pipe(createGunzip())`
  for `readFile` + `promisify(gunzip)`. The piped form emitted
  read/decompress failures as `'error'` events that bypassed the
  caller's `try`/`catch`; the awaited buffered form rejects normally.
  Tarballs are small enough that buffering is fine.
- `publish.ts`: mark `PathLike` as a `type`-only import.
- `npm-release.yml`: drop the unused `WS` env on the
  `download release archives` step — the extracted script reads
  `GITHUB_WORKSPACE` directly.
- `release.yml`: new `matrix` job derives the rust build matrix from
  `npm/targets.json` so the inline 14-target list goes away. Adds
  `build-npm-dist` job after `upload-assets` that uploads `npm-dist`
  (retention 7d) for cross-workflow consumption.
- `npm-release.yml`: drop the `build` job. Publish job now downloads
  `npm-dist` via cross-workflow `download-artifact` (`run-id` +
  `actions: read`) and sparse-checks-out `.github/scripts` from the
  default branch — no tag-supplied code on the filesystem at all.
  Adds required `run-id` `workflow_dispatch` input; `NODE_VERSION`
  set to `latest`.
- `build-npm-packages.sh` / `npm.sh`: drop the `EVENT_NAME` /
  `--skip-missing` relaxation. The dist artifact is always produced
  in tag-push context where missing tier-1/2 tarballs already fail
  loud, so the optional-tier-1/2 publish branch was dead weight.
`targets.json` already declares a `netbsd-x64` entry with
`"os": ["netbsd"]`, but the schema's `os` enum omitted it, so any
schema-aware editor flagged the existing config as invalid.
- `Cargo.toml` / `Cargo.lock`: 0.5.0 → 0.6.0.
- `npm/facade/package.json`: replace the `0.0.0-template`
  placeholder with `0.6.0` to track the crate version. The build
  still overwrites `tpl.version` at pack time, but valid SemVer
  in the source file stops looking like a stuck template to
  schema-aware tooling and humans skimming the repo.
- `build-packages.ts` / `publish.ts`: add docstrings to the
  remaining undocumented functions (`errMessage`, `errCode`,
  `readOpts`, `readTargets`, `clean`, `buildFacade`,
  `buildPlatformPackage`, `main` / `parseAccess`, `main`).
  Pushes docstring coverage from 33% to 100% so CodeRabbit's
  80% threshold stops failing on this PR.
- `Cargo.toml`: move authors into `[[package.metadata.authors]]`
  (objects with `name`/`email`); add `[package.metadata.npm]` with
  `name`/`subpkgscope`/`bugs`/`repository`/`engines`; rename feature
  `run-alias` → `run`; bump `rust-version` to 1.97; declare
  `build = "build.rs"`.
- `build.rs`: parse `package.metadata.authors[0]` and emit
  `RUNNER_AUTHOR_NAME` + `RUNNER_AUTHOR_EMAIL` for `env!` /
  `option_env!` consumers.
- `src/lib.rs`: replace clap's `crate_authors!()` (now empty since the
  top-level `authors` field moved) with the env vars; drop the
  `primary_author` / `authors` helpers; rename a `run-alias` test
  fixture to match the feature.
- `npm/scripts/build-packages.ts`: call `cargo read-manifest` and
  derive each per-platform `package.json` field
  (`license`/`author`/`homepage`/`repository`/`bugs`/`engines`) from
  Cargo metadata with per-field shape-narrowing. Refactor the tar
  reader into named helpers and drop the hardcoded values.
- `npm/facade/lib/launch.cjs`: re-raise the child's terminating signal
  on the facade itself so `set -e`, trap handlers, and Ctrl+C chains
  see `128 + N` instead of an opaque `1`.
- `npm/facade/lib/resolve.cjs`: confirm the platform bin exists
  before returning its path so a partial install fails loud here
  instead of as an opaque `ENOENT` from `spawnSync` later.
- `npm/scripts/publish.ts`: only treat `ENOENT` as "missing" in
  `exists()`; only treat `E404` as "not published" in
  `alreadyPublished()` — auth/IO/network errors now bubble up.
- `npm/facade/package.json`: add `directories.{lib,bin}`.
- `justfile`: add dev recipes (`run`/`runner`/`build-packages`/
  `test-release`/`sys`/`list`).
- `README.md` / `CHANGELOG.md`: rewrap; mark BSD targets experimental;
  silence markdownlint duplicate-heading on the changelog.
- `build-packages.ts`: thread `meta` into `buildFacade` and spread
  it over the template (license, author, homepage, repository,
  bugs, engines) so the facade tracks `Cargo.toml` the same way the
  per-platform sub-packages already do since f33098d. Without this
  the facade's template values drift as soon as Cargo metadata
  changes.
- `npm/facade/package.json`: drop the inline `engines.node` block.
  Cargo-derived `meta` now provides it; keeping a stale copy in the
  template just invited a silent override mismatch.
- `publish.ts`: 120s timeout on `npm view` and `npm publish`
  subprocesses with an explicit signal-kill error. A stuck DNS or
  registry path used to be able to burn the full GitHub Actions 6h
  job budget; bound it to a fail-fast window so flakes retry quickly.
- `download-release-archives.sh`: `rm -rf npm/downloads` before
  recreating it. Stale `.tar.gz`/`.sha256` from a prior tag would
  pass `verify-checksum.sh` (which walks every file in the dir) but
  be wrong-version for the current `RELEASE_TAG`. Hosted GHA runners
  get fresh workspaces; self-hosted runners and local invocations
  don't.
- `publish.ts`: add `validatePackageDir` and call it for every per-
  platform pkg and the façade before `alreadyPublished` / `publish`.
  Refuses to publish if the dist dir contains a `.npmrc`, if
  `package.json` declares `publishConfig`, or if `package.json#name`
  doesn't match the expected scoped/facade name. The `--registry` /
  `--access` CLI flags already override config-file values for the
  publish call itself, but rejecting the unexpected structure
  outright is cheaper than reasoning about precedence — and catches
  build-time mistakes where a sub-package gets scribbled into the
  wrong dir.
- `Cargo.toml`: drop `rust-version` 1.97 → 1.95. Hoist version-only
  deps (`anyhow`, `colored`, `json5`, `serde_json`, `shlex`,
  `yaml-rust2`) from per-dep `[dependencies.x]` tables into a single
  `[dependencies]` block. Feature-bearing deps (`clap`,
  `clap_complete`, `serde`) keep their own tables.
- `rust-toolchain.toml`: new — pin `channel = "1.95"` with `rustfmt`
  + `clippy` on `minimal` profile so cargo invocations match the
  declared MSRV.
- `justfile`: collapse `default`/`run`/`runner` dispatch through a
  private `shim` recipe that threads the build profile via `PROFILE`
  env var; bin recipes now do a single `cargo run --bin=… --profile=…`
  with `[arg(pattern=…)]` validation on the user-facing entrypoint.
  Drop unused `targets` variable, the `sys` recipe, the stale
  commented-out `npm-pkg`, and the cosmetic echo on `list`. Annotate
  recipes with `[group('bins')]` / `[group('npm')]`.
- `build-packages` recipe: stop passing `--no-skip-missing` on the
  false branch. `build-packages.ts` already defaults `skip-missing`
  to `false`, so the explicit negation was just re-asserting the
  default and added a second code path for parseArgs to handle.
- `test-release` recipe: pick `runner.exe`/`run.exe` on Windows and
  `runner`/`run` elsewhere, assert each binary exists in
  `target/release/` before `tar czf`, and feed the array into the
  tar invocation. Previously a missing/renamed bin would surface as
  a cryptic tar failure or, worse, a tarball missing the file with
  no warning.
`readTargets` previously erased `tier` from its return type. The main
loop then silently skipped missing sub-package dirs and still shipped
the facade — so a publish run on a `--only=<one-target>` build would
push `runner-run` with truncated `optionalDependencies`, leaving every
omitted required platform broken on `npm install`.

Restore `tier` to the TS shape and add a pre-flight scan: if any
tier-1/2 dir is absent under `npm/dist/`, throw with the missing names
listed before any publish call fires. Tier-3 (FreeBSD arm64, NetBSD,
OpenBSD) still passes through — those run with `continue-on-error` in
the build matrix and the facade omits them from `optionalDependencies`
intentionally.
a992f06 took `npm/scripts/*.ts` to 100%; this finishes the rest
so CodeRabbit's 80% threshold stops flagging the remaining
files on every PR:

- `build.rs`: doc `main`.
- `src/lib.rs`: doc `bin_name_from_arg0`,
  `configure_cli_command`, `help_byline`, `requests_version`.
- `npm/scripts/build-packages.ts`: doc the remaining helpers
  (`isObject`, `narrow*`, `readCargoManifest`,
  `formatFirstAuthor`, `packageMetadata`,
  `errorMessage`/`errorCode`, `readOptions`, `parseOnlyList`,
  `readMatrix`, `cleanDist`, `buildFacade`,
  `buildPlatformPackage`, tar parsers, `formatPackage`,
  `main`).
- `npm/scripts/publish.ts`: rewrite `parseAccess`,
  `readTargets`, `alreadyPublished`, `publish`, `main`
  docstrings.
- `npm/facade/lib/resolve.cjs`: rewrite `resolveBinary`
  docstring.
- `.github/scripts/publish/npm.sh`: add `publish_allowed` block
  comment.

Drive-by: `justfile` `test-release` invokes the commands via
`eval "${cmd}"` so quoted arguments in the array
(`node …/run.cjs --version`) re-parse as proper shell tokens
instead of word-splitting on whitespace as bare `${cmd}`
expansion.
@kjanat
kjanat force-pushed the claude/npm-platform-packages-1gQec branch from c17251b to 2dcb14d Compare May 4, 2026 22:58
coderabbitai[bot]

This comment was marked as resolved.

kjanat added 3 commits May 5, 2026 01:19
- `bin_name_from_arg0`, `configure_cli_command`, `help_byline`,
  `requests_version`: make `pub` + `#[must_use]` and qualify the
  doctest examples with `runner::` so they actually compile against
  the crate. Previously private, so the ``` fences ran as
  doctests but referred to unreachable items.
- Doc fences: ```text``` → ```bash``` for the CLI usage block,
  bare ``` → ```rust for the runnable examples.
- Drop the `\[0\]` escapes in `argv[0]` references — rustdoc
  rendered the backslashes literally, and the brackets don't need
  escaping outside link syntax.
- Reflow a couple of doc lines that drifted past column.

`rust-toolchain.toml`: add `rust-analyzer` to the pinned 1.95
components so `cargo` and the editor agree on the toolchain.
coderabbitai[bot]

This comment was marked as low quality.

coderabbitai[bot]

This comment was marked as spam.

Five fail-loud guards across the publish path; each catches a
different bad input that previously fell through silently.

- `derive-dist-dry.sh`: validate `INPUT_DIST_TAG` against
  `^[A-Za-z][A-Za-z0-9._-]*$` so a malformed override can't smuggle
  flag-like or whitespace values into `npm publish --tag`. Normalize
  `INPUT_DRY_RUN` to strict `true`/`false`; previously `True`/`1`/
  `yes` all fell through the `[[ == "true" ]]` check as `dry_run=false`,
  i.e. a real publish under the appearance of a dry run.
- `release.yml`: smoke-test the packaged `linux-x64-gnu` binary with
  `--version` before uploading the `npm/dist` artifact. Catches a
  broken bin at build time instead of as an `ENOENT` on every user's
  machine post-publish. Cross-target bins still rely on the publish
  job's install-time checks.
- `npm.sh`: validate `optionalDependencies` in `publish_allowed`. The
  facade must list every required platform under the scope at exactly
  `EXPECTED_VERSION`; platform sub-packages must declare none. A
  tampered platform package could otherwise smuggle attacker-controlled
  deps that npm would happily install transitively. Wrap `npm view`
  and `npm publish` in `timeout 120s` with explicit 124 handling so a
  hung registry can't burn the full job budget.
- `build-packages.ts`: `narrowRepository` / `narrowBugs` /
  `narrowAuthor` now throw when an optional field is present but
  wrong-typed, instead of silently dropping it. Cargo metadata drift
  (e.g. `repository.type` becomes a number) used to ship a facade
  with the field missing rather than failing the build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants