Skip to content

feat(npm): per-platform binary distribution with OIDC release pipeline - #10

Open
younna-ai-opencode wants to merge 10 commits into
masterfrom
comment-checker-npm
Open

feat(npm): per-platform binary distribution with OIDC release pipeline#10
younna-ai-opencode wants to merge 10 commits into
masterfrom
comment-checker-npm

Conversation

@younna-ai-opencode

Copy link
Copy Markdown
Collaborator

comment-checker is now installable as an npm package. The launcher resolves the platform-specific binary package at runtime, and all five platform packages plus the root launcher are published by a tag-triggered GitHub Actions pipeline with npm OIDC provenance — no static tokens anywhere.

What changed

Distribution (npm)

  • Launcher at npm/packages/comment-checker/ (bin: comment-checker) built with tsdown, Node >= 18, Effect v4rc vendored deps pinned.
  • Five platform packages (linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64) generated from the single canonical table scripts/release/targets.json.
  • The committed launcher manifest deliberately carries no optionalDependencies — pnpm cannot record unresolvable optional deps (pnpm#3960), which breaks --frozen-lockfile on fresh clones. sync-root-version.ts injects the five pins at publish time. The published root therefore pins them exactly to the tag version.

Release pipeline (.github/workflows/release.yml)

  • Tag-triggered (v*); VERSION derives from the git tag only (KTD5).
  • One matrix lane per platform: build → check-matrix gate → binary-exists gate → smoke (exit 0/2) → stage tar.gz + sha256 sidecar → upload → pnpm publish --provenance (OIDC, no static token). Then publish-npm-main gates every published platform package against the table (os/cpu/libc, shape-normalized deep equality), cross-checks published tarball sha256 vs recorded sidecar (retried for registry RYW), injects pins via sync-root-version, publishes the root, and verifies the published pins.
  • check-matrix.ts enforces table ↔ workflow agreement including target/suffix pairing (a swapped suffix fails), plus launcher-manifest pin agreement.
  • All third-party actions SHA-pinned; permissions: {} at workflow level with per-job grants.
  • ci.yml gains an npm job gating lint, build, typecheck, both suites, and check-matrix on push/PR (previously the entirely new JS surface was ungated).

Review-driven hardening (ce-code-review 20260817-043848-ac825c53)

  • F1 registry gate jq shape-normalized on both sides (libc array vs bare string, absent vs null); fixture-verified for all five lanes plus a mismatch case.
  • F2 linux-arm64 lane now builds on ubuntu-24.04-arm (native) instead of cross-executing an aarch64 binary on an x86 runner.
  • F3 sync-root-version --allow-env (workflow step + deno task) and a fidelity test that replays declared flags.
  • F4 portable sha256_of (sha256sum, else shasum) in stage and cross-check.
  • F5 bounded npm pack retry (5x/5s) for the registry read-after-write.
  • F6 platform packages no longer declare a bin (esbuild precedent — only the launcher owns the shim).
  • F7 parser rejects flag-shaped/missing values; shebang + exec bit pinned in the Node suite.
  • F8 check-matrix --workflow-path + missing/extra/swapped failure fixtures.

Verification

  • deno task lint, deno task test (23/23), deno task check-matrix green.
  • Node suite 16/16 (node --test); pnpm lint, pnpm -r build, pnpm -r typecheck, pnpm install --frozen-lockfile green.
  • Both workflows parse as YAML; matrix lanes validated (5 rows, runner per arch).
  • Rust gates run in ci.yml — no local cargo toolchain on this machine.
  • First real release is human-owned (trusted-publisher records, tag cut): docs/publishing/first-release-checklist.md.

Session-settled decisions carried from planning (docs/plans/2026-08-17-001, in-tree): five platform suffixes + canonical targets.json (KTD1/2), libc glibc (KTD4), git tag as the single version source (KTD5), committed manifest without optional pins (pnpm#3960). Durable residual findings are filed as issues #3#9 (Related).

Related

Note (proposal only — AGENTS.md is a Locked file)

Proposed one-line Directory-map addition in AGENTS.md (not applied by this PR):

|`npm/`|npm distribution layer (launcher + per-platform binary packages)|

New concepts

Per-platform binary packages + optionalDependencies (the esbuild pattern). A compiled CLI cannot ship one tarball for every OS/arch. Instead, one tarball is published per platform (-linux-x64, -darwin-arm64, …), each declaring os/cpu/libc; the root package lists all five as optionalDependencies, so npm installs only the host-compatible one. Used by esbuild, Biome, Turbo. Preferred when the binary is self-contained; different architecture when the package ships in-process bindings (napi-rs) or needs on-consumer compilation.


Compound Engineering

ryan added 10 commits August 17, 2026 03:36
- targets.json: five platform targets (KD1/KTD2)
- generate-platform-manifest.ts: emit per-platform package.json (--dry-run)
- sync-root-version.ts: inject version + five optionalDeps pins at publish
- check-matrix.ts: identity rules, product set, cross-file workflow agreement
- launcher manifest carries no optionalDeps pre-publish (pnpm cannot lock
  unresolvable optional deps; pins injected at release time)
- matrix job per target: build, gate (check-matrix + smoke), sha256 sidecar,
  tar.gz upload, platform publish with trusted publishing
- publish-npm-main: tag gates (semver, reachability), registry re-verify
  (version/os/cpu/libc vs table), binary sha cross-check, root publish
- upload-gh-release-assets: tarballs attached via pinned action
- workflow-level permissions: {}; per-job id-token/contents grants only
- every third-party action pinned by full commit SHA
- src/platform.ts: pure optionalDepName/binaryFileName (no Effect imports)
- index.ts imports from platform; dist emits index.mjs + platform.mjs
- launcher.test.mjs: shim spawn + arg passthrough, exit-code propagation,
  BinaryNotFound naming host package, targets.json round-trip (15 tests)
- README Publishing section: tag flow, trusted-publisher bindings, verify steps
- docs/publishing/first-release-checklist.md: one-time org setup, tag watch,
  post-publish verification, deprecate/bump escape hatch, action-pin duty
- scripts/release/args.ts: shared flag parser (was triplicated; identical
  missing-value/unknown-arg semantics, no new deps)
- release.yml: derive SUFFIXES + BINARY_NAME from targets.json (KTD2);
  merge registry gates into one npm view per package; one root verify view;
  use shasum -a 256 (macOS runners lack sha256sum)
- launcher: drop unreachable fs.exists pre-check (single BinaryNotFound arm,
  removes TOCTOU window + message duplication)
- launcher test: hostDep derived from optionalDepName
- README: stop leaking plan-internal unit id
Review 20260817-043848-ac825c53 found 3 P0 pipeline blockers plus
silent-pass gates; all fixed:

- P0 matrix: linux-arm64 builds on ubuntu-24.04-arm (native aarch64);
  cross-linker now only for x64 runners
- P0 registry gate: jq deep-equality now shape-normalizes both sides
  (libc array vs bare string, absent vs null) and no longer feeds an
  --argjson object to fromjson; Dar'in/windows rows verified
- P0 sync-root-version needs --allow-env (env VERSION): workflow step
  and deno.jsonc task both declare it
- P1 portable sha256_of helper (sha256sum first, shasum fallback) in
  stage and cross-check steps
- P1 npm pack retry (5x/5s) after publish read-after-write race
- P1 platform packages no longer declare a bin (esbuild precedent);
  test asserts pkg.bin === undefined
- P1 ci.yml npm job gates lint, build, typecheck, node + deno suites,
  and check-matrix on push/PR
- P2 check-matrix --workflow-path + tests for missing/extra/swapped
  matrix rows
- P2 parseFlags rejects flag-shaped/missing values; eq-form covered;
  shebang + exec bit pinned on dist/index.mjs
- P3 comment fix (platforms.json -> targets.json)

Gates: deno task lint/test (23/23), check-matrix ok, pnpm lint/build/
typecheck, node suite 16/16, frozen install ok.
Solution doc (docs/solutions/architecture-patterns/rust-cli-npm-distribution.md)
grounds the launcher + five per-platform package pattern, the pnpm#3960
constraint that keeps optionalDependencies out of the committed manifest,
the inject-at-publish sync, and the tag-triggered OIDC release pipeline
with its observed silent-gate failure modes. CONCEPTS.md seeds the npm
distribution vocabulary (launcher, platform package, release lane).
All doc citations validated by validate-doc-claims.py (0 flags).
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