Skip to content

publish: ship aube on npm as @endevco/aube#12

Merged
jdx merged 6 commits intomainfrom
claude/inspiring-ardinghelli-a6f0ef
Apr 18, 2026
Merged

publish: ship aube on npm as @endevco/aube#12
jdx merged 6 commits intomainfrom
claude/inspiring-ardinghelli-a6f0ef

Conversation

@jdx
Copy link
Copy Markdown
Contributor

@jdx jdx commented Apr 18, 2026

Summary

  • Add a preinstall-based npm distribution alongside the existing mise/cargo/curl paths. Root package is @endevco/aube; six per-platform subs are @endevco/aube-<os>-<arch> (darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64, win32-arm64).
  • New standalone workflow publish-npm.yml triggers on release: published (and workflow_dispatch for reruns). Decoupled from release-plz.yml so an npm hiccup never blocks crates.io or the GitHub release.
  • Auth via npm Trusted Publishing (OIDC) — no NPM_TOKEN secret.

How it works

At install time, @endevco/aube's preinstall script spawns npm install --no-save @endevco/aube-<os>-<arch>@<version> and hardlinks (falling back to copy) the three binaries (aube, aubr, aubx) from the sub-package's bin/ into the root's ./bin/. Shape mirrors @jdxcode/mise — no runtime JS shim and no optionalDependencies sprawl in package-lock.json. Note this means --ignore-scripts and fully offline caches won't work; those users keep the mise/cargo paths.

The multicall dispatch from #6 works through npm because the preinstall creates three named files, so aubr invoked via npm's bin wrapper sees argv[0] ending in aubr and routes to run.

At release time, npm/scripts/publish.mjs downloads each aube-<tag>-<target>.{tar.gz,zip} from the just-published GitHub release, extracts the binaries, stages a platform-scoped package.json with correct os/cpu/bin, and npm publishes each sub-package. Root publishes last so its preinstall can resolve every sub. Auto-picks the next dist-tag for pre-releases (1.0.0-beta.1next) and latest for stable. DRY_RUN=1, SKIP_ROOT=1, and SKIP_PLATFORMS=1 env flags exist for manual recovery.

Why OIDC

npm's Trusted Publishing (GA mid-2025) exchanges a short-lived GitHub OIDC token for a one-shot npm publish token. No long-lived secret to rotate or leak, and the publish is provenance-signed. Requires npm ≥ 11.5.1 — the workflow upgrades to npm@latest before publishing to avoid drift with the version Node 24 ships.

Pre-merge setup

On npmjs.com/org/endevco → Settings → Trusted Publishers, add an org-level trusted publisher:

  • Repo: endevco/aube
  • Workflow: .github/workflows/publish-npm.yml
  • Environment: (blank)

Org-level config covers all new @endevco/* packages, so the first release auto-covers root + 6 subs.

Limitations / follow-ups

  • aube (unscoped) is taken on npm by a year-old placeholder (estjs/aube, 1 version, never updated). If you want the unscoped name, file an npm dispute — unrelated to this PR.
  • No Alpine / musl package yet. Linux users on glibc distros get linux-<arch>; muslc users will hit the glibc binary and fail. Adding linux-x64-musl / linux-arm64-musl needs corresponding Rust release targets first.
  • Retrying after partial publish failure: same-version republishes return 403. Recovery is running the workflow manually with SKIP_PLATFORMS=1 (or vice versa) to publish only what hasn't shipped.

Test plan

  • Built aube, aubr, aubx locally; staged a fake @endevco/aube-darwin-arm64 sub-package under node_modules/; ran the link logic and confirmed each bin dispatches to the right subcommand via argv[0] basename (aubr --helprun's help, aubx --helpdlx's help).
  • npm pack --dry-run on the root package — tarball contains only installArchSpecificPackage.js + package.json (README copied in by publish script at release time).
  • npm pack --dry-run on a hand-staged platform package — contains bin/{aube,aubr,aubx}, package.json with correct os/cpu, README.md.
  • End-to-end first publish under 1.0.0-beta.X with next dist-tag, then npm install -g @endevco/aube@next from a clean machine. Runs for real on the next release.

🤖 Generated with Claude Code


Note

Medium Risk
Adds a new release-triggered GitHub Actions workflow and preinstall-driven npm packaging/publishing logic, which can impact release automation and end-user installation behavior if misconfigured.

Overview
Adds npm distribution for aube by introducing a root @endevco/aube package that installs a platform-specific @endevco/aube-<os>-<arch> subpackage at preinstall time and links/copies the native aube/aubr/aubx binaries into ./bin.

Introduces a new publish-npm GitHub Actions workflow that runs on release: published (or manual tag input) and uses npm Trusted Publishing (OIDC) to download release artifacts, stage per-platform npm packages, and publish them before publishing the root package. Documentation is updated to mention npm install -g @endevco/aube as an install option.

Reviewed by Cursor Bugbot for commit cf71251. Bugbot is set up for automated code reviews on this repo. Configure here.

jdx and others added 3 commits April 18, 2026 15:34
Add a preinstall-based npm distribution alongside the existing
mise/cargo/curl paths. The root `@endevco/aube` package is tiny (just
a preinstall script); at install time it fetches the matching
`@endevco/aube-<os>-<arch>` sub-package and hardlinks the three
binaries (aube, aubr, aubx) into ./bin. Mirrors the shape of
`@jdxcode/mise` — no runtime JS shim, no optionalDependencies sprawl.

A new workflow `publish-npm.yml` fires on `release: published` (and
`workflow_dispatch` for reruns) and is decoupled from `release-plz.yml`
so an npm hiccup never blocks crates.io or the GitHub release. Uses
OIDC / Trusted Publishing — no NPM_TOKEN secret to rotate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR adds an npm distribution path for aube as @endevco/aube, using a preinstall-script approach (mirroring @jdxcode/mise) to download and hardlink platform-specific binaries at install time. Six scoped sub-packages (@endevco/aube-<os>-<arch>) are published by a new standalone publish-npm.yml workflow that uses npm Trusted Publishing (OIDC) — decoupled from the existing release-plz.yml so npm failures don't block crates.io or the GitHub release.

  • Missing --provenance flag: npmPublish() does not pass --provenance to npm publish, so packages are not actually provenance-signed despite the PR description claiming they are.
  • Unnecessary custom PAT: The workflow uses secrets.AUBE_GH_TOKEN where secrets.GITHUB_TOKEN (always available, contents: read already declared) would suffice — creating an avoidable manual secret.
  • rm -rf cross-platform issue: npm/package.json's prepack script uses rm -rf bin, which fails on Windows.
  • spawnSync null-status edge case: The run() helper in publish.mjs does not distinguish a non-zero exit from a process that failed to spawn, producing a confusing exited null message.

Confidence Score: 3/5

Safe to merge for the install mechanism itself, but two concrete issues — missing --provenance and the unnecessary custom PAT — should be fixed before the first real release.

The preinstall architecture is sound and mirrors a proven pattern. However, the --provenance omission directly contradicts a stated design goal, and the custom PAT creates an avoidable operational dependency that will silently break if not configured.

npm/scripts/publish.mjs (missing --provenance) and .github/workflows/publish-npm.yml (custom PAT vs built-in token) need attention before the first npm release.

Important Files Changed

Filename Overview
.github/workflows/publish-npm.yml New workflow publishing to npm via OIDC Trusted Publishing; uses a custom PAT secret (AUBE_GH_TOKEN) instead of the auto-provisioned GITHUB_TOKEN, which is unnecessary and adds operational overhead.
npm/installArchSpecificPackage.js Preinstall script that downloads and hardlinks platform-specific sub-package binaries; mirrors the @jdxcode/mise pattern correctly, with proper fallback from hardlink to copy and Windows exe-suffix handling.
npm/scripts/publish.mjs Release-time publish script that downloads archives, stages platform packages, and publishes; missing --provenance flag contradicts the PR description's provenance-signing claim, and spawnSync null-status edge case is unhandled.
npm/package.json Root npm package definition with preinstall hook and correct files allowlist; prepack uses Unix-only rm -rf which breaks on Windows.
npm/.gitignore Correctly gitignores generated artifacts (/bin, /node_modules, /.stage, /*.tgz, /README.md) created at install or publish time.
docs/installation.md Adds npm installation instructions including global install, npx usage, and a clear note about --ignore-scripts / offline limitations.
README.md README updated to reference the new npm distribution path alongside the existing mise and cargo install options.

Sequence Diagram

sequenceDiagram
    participant GH as GitHub Release Event
    participant WF as publish-npm.yml
    participant GHR as GitHub Releases
    participant NR as npm Registry

    GH->>WF: release:published (or workflow_dispatch)
    WF->>WF: Resolve tag
    WF->>WF: setup-node@v4 (Node 24)
    WF->>WF: npm install -g npm@latest

    loop For each of 6 platform targets
        WF->>GHR: gh release download aube-{tag}-{triple}.tar.gz/.zip
        GHR-->>WF: archive
        WF->>WF: extract + chmod 0o755
        WF->>WF: Stage @endevco/aube-{os}-{cpu}/package.json + bin/
        WF->>NR: npm publish @endevco/aube-{os}-{cpu} (OIDC)
    end

    WF->>WF: Rewrite npm/package.json version
    WF->>WF: Copy README.md into npm/
    WF->>NR: npm publish @endevco/aube (OIDC)

    Note over NR: User runs: npm install -g @endevco/aube
    NR-->>NR: Triggers preinstall script
    NR->>NR: npm install @endevco/aube-{os}-{arch}@{ver}
    NR->>NR: hardlink (or copy) bin/{aube,aubr,aubx} into ./bin/
Loading

Fix All in Claude Code

Reviews (2): Last reviewed commit: "docs(installation): trim redundant npm-p..." | Re-trigger Greptile

Comment thread npm/scripts/publish.mjs
Comment on lines +135 to +139
function npmPublish(stageDir, npmTag, dryRun) {
const args = ['publish', '--access', 'public', '--tag', npmTag];
if (dryRun) args.push('--dry-run');
run('npm', args, { cwd: stageDir });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing --provenance flag for provenance-signed publish

The PR description states "the publish is provenance-signed," but npm publish is invoked without --provenance. With npm's Trusted Publishing (OIDC), OIDC auth is used for the token exchange, but the provenance attestation is only attached when --provenance is explicitly passed. Without it, the packages are published without a provenance record even though the OIDC environment is present.

Suggested change
function npmPublish(stageDir, npmTag, dryRun) {
const args = ['publish', '--access', 'public', '--tag', npmTag];
if (dryRun) args.push('--dry-run');
run('npm', args, { cwd: stageDir });
}
function npmPublish(stageDir, npmTag, dryRun) {
const args = ['publish', '--access', 'public', '--tag', npmTag, '--provenance'];
if (dryRun) args.push('--dry-run');
run('npm', args, { cwd: stageDir });
}

Fix in Claude Code

Comment on lines +69 to +72
env:
GITHUB_TOKEN: ${{ secrets.AUBE_GH_TOKEN }}
TAG: ${{ steps.tag.outputs.tag }}
run: node npm/scripts/publish.mjs
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Custom PAT secret instead of built-in GITHUB_TOKEN

secrets.AUBE_GH_TOKEN is used to authenticate gh release download, but the job already has contents: read from the built-in GITHUB_TOKEN. The built-in token is always available and requires no manual configuration. Using a custom PAT means:

  1. A new repo secret (AUBE_GH_TOKEN) must be created and rotated manually.
  2. If the secret is absent or expired, the workflow fails with a silent empty-string substitution rather than a clear error.

Consider using secrets.GITHUB_TOKEN (the auto-provisioned token) instead:

Suggested change
env:
GITHUB_TOKEN: ${{ secrets.AUBE_GH_TOKEN }}
TAG: ${{ steps.tag.outputs.tag }}
run: node npm/scripts/publish.mjs
- name: Publish @endevco/aube and platform packages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.tag.outputs.tag }}
run: node npm/scripts/publish.mjs

Fix in Claude Code

jdx and others added 2 commits April 18, 2026 15:40
Public release assets redirect from /releases/download/<tag>/<asset>
to a signed CDN URL — fetch follows the redirect. No GitHub API hit,
no rate limit exposure, no GITHUB_TOKEN in the workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trusted Publishing only covers the token handshake; the provenance
statement requires `npm publish --provenance` explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 180d263. Configure here.

Comment thread npm/installArchSpecificPackage.js
When the child is killed by a signal, close() fires with code=null.
process.exit(null) coerces to 0, so npm would record the preinstall
as successful even though no binary was fetched. Exit 1 in that case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx merged commit 98df790 into main Apr 18, 2026
16 checks passed
@jdx jdx deleted the claude/inspiring-ardinghelli-a6f0ef branch April 18, 2026 21:08
jdx added a commit that referenced this pull request Apr 18, 2026
aube can now be installed via npm (`npm install -g @endevco/aube`), and
Linux binaries have been rebuilt with broader glibc compatibility and a
pure-Rust TLS stack — no more OpenSSL system dependency.

## Highlights

- **Install from npm** — `npm install -g @endevco/aube` ships native
binaries for all six supported platforms (macOS, Linux, Windows ×
arm64/x64). The multicall shims `aubr` and `aubx` work out of the box.
- **Better Linux portability** — Linux targets are now built with
`cross`, producing binaries that run on older glibc versions. The switch
from OpenSSL to `rustls` removes the system OpenSSL dependency entirely.

## Added

- **npm distribution** — aube is now published on npm as
`@endevco/aube`. At install time, a `preinstall` script fetches the
correct `@endevco/aube-<os>-<arch>` sub-package and hardlinks the three
binaries (`aube`, `aubr`, `aubx`) into place. No runtime JS shim — npm's
bin wrapper calls the native binary directly. Pre-releases use the
`next` dist-tag; stable releases use `latest`.
([#12](#12) by @jdx)

  ```sh
  npm install -g @endevco/aube
  # or try it without installing
  npx @endevco/aube --version
  ```

> **Note:** Because install relies on `preinstall`, the
`--ignore-scripts` flag and fully offline caches are not supported. Use
mise or `cargo install` in those environments.

## Changed

- **TLS backend switched to rustls** — HTTP requests now use the
pure-Rust `rustls` TLS implementation instead of the system's OpenSSL
via `native-tls`. This eliminates the need for OpenSSL headers at build
time and removes the OpenSSL runtime dependency on Linux.
([#15](#15) by @jdx)
- **Linux builds use `cross`** — Linux release binaries (x86_64 and
aarch64) are now compiled inside `cross`'s Docker images, which target
an older glibc baseline for broader distribution compatibility.
([#15](#15) by @jdx)

## Fixed

- **Per-registry client certificate auth** — The mTLS client certificate
path now concatenates cert and key into a single PEM buffer and calls
`Identity::from_pem`, which works correctly under rustls. The previous
`Identity::from_pkcs8_pem` was a `native-tls`-only API.
([#15](#15) by @jdx)

**Full Changelog**:
6587e37...16ded6f

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> This is primarily a release/versioning PR (changelogs, version bumps,
and dependency lockfile updates) with no functional code changes shown
in the diff, so runtime risk is low.
> 
> **Overview**
> Prepares the `v1.0.0-beta.2` release by adding a top-level
`CHANGELOG.md` and per-crate changelogs, and bumping the workspace +
internal crate versions from `1.0.0-beta.1` to `1.0.0-beta.2`.
> 
> Updates `Cargo.lock` for the release (dependency version/lock refresh)
and syncs the reported CLI/docs version (`aube.usage.kdl`,
`docs/cli/index.md`, `docs/cli/commands.json`) to `1.0.0-beta.2`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
0e53762. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: release-plz[bot] <release-plz+bot@users.noreply.github.com>
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