Skip to content

feat(cli): release pipeline — cross-compiled binaries plus an npm wrapper - #346

Merged
blockchain-maxis merged 7 commits into
blockchain-maxis:mainfrom
Mamavee001:feat/cli-release-pipeline
Sep 2, 2026
Merged

feat(cli): release pipeline — cross-compiled binaries plus an npm wrapper#346
blockchain-maxis merged 7 commits into
blockchain-maxis:mainfrom
Mamavee001:feat/cli-release-pipeline

Conversation

@Mamavee001

Copy link
Copy Markdown
Contributor

closes #293

Labeled maintainer-owned — I attempted it anyway since it was assigned alongside the other three, but the actual "go live" step (flipping CLI_RELEASE_ENABLED and adding NPM_TOKEN) needs npm publish credentials only a maintainer has, and I did not fabricate or attempt to work around that. Everything up to that switch is real, built, and verified below.

Stacked on #335, #336, #342, #345 (scaffold, config, link/--json, unit tests) — cherry-picked (authorship preserved) since this needs the cli/ module to exist. Top commit (a362b76) is the one to review.

Summary

npx executes JavaScript from the npm registry and cannot run a Go binary directly. Keeping the designed entrypoint — npx @signet/cli link, no install step — needs the wrapper pattern esbuild, swc, and turbo use:

@signet/cli                    zero-dependency JS shim that execs the binary
  optionalDependencies:
    @signet/cli-linux-x64      one prebuilt Go binary each
    @signet/cli-linux-arm64
    @signet/cli-darwin-arm64
    @signet/cli-windows-x64

Changes

  • cli/npm/cli/bin/signet.js (new): the shim. Resolves process.platform/process.arch to the one optionalDependency npm actually installed, and execFileSyncs its binary, forwarding stdio and the real exit code. Zero dependencies.
  • cli/npm/cli-<platform> (new, ×4): one package per release target — just a package.json with os/cpu constraints and an empty bin/ (binaries are never committed; the release workflow stages them in).
  • scripts/release/stage-platform-package.mjs (new): copies a built binary into a platform package's bin/, always as exactly signet/signet.exe regardless of the build step's own (disambiguated) output filename — that fixed name is the only thing the shim ever looks for — and pins the package's version.
  • scripts/release/pin-shim-version.mjs (new): pins @signet/cli's own version and every optionalDependency's version to the release version exactly, so the shim can never resolve a floating range that drifted from what was actually tested.
  • .github/workflows/release-cli.yml (new): on a cli-v*.*.* tag, cross-compiles all four targets from a single ubuntu-latest runner (Go cross-compiles cleanly with just GOOS/GOARCH env vars and CGO_ENABLED=0 — no build matrix or artifact-shuffling between jobs needed), checksums them, stages+pins the npm packages, and creates a GitHub Release with the binaries and checksums attached — all of that runs on every tag push. Actual npm publish is gated behind vars.CLI_RELEASE_ENABLED == 'true' plus a secrets.NPM_TOKEN in the production environment, mirroring deploy.yml's existing DEPLOY_ENABLED pattern.

Verification — real, not just read

I can't trigger the actual workflow (no tag push, no npm credentials), so I ran the equivalent steps locally for real:

  • Cross-compiled all four targets (linux/amd64, linux/arm64, darwin/arm64, windows/amd64) with the exact build command the workflow uses.
  • Staged each into its cli/npm/cli-<platform> package and pinned versions with the two release scripts.
  • npm pack'd the shim and the Windows platform package, installed both tarballs into a scratch project, and ran the installed signet bin exactly as npx @signet/cli would: --version, --help, link --json, and exit-code passthrough (2 on an invalid public key) all worked correctly through the shim.
  • Found and fixed a real bug this way: stage-platform-package.mjs originally named the destination file after the build step's own disambiguated output name (e.g. cli-windows-x64-signet.exe) instead of the fixed signet.exe the shim requires — every platform package would have been silently unresolvable. Fixed before this commit.
  • Validated release-cli.yml's YAML syntax (python3 -c 'import yaml; yaml.safe_load(...)') and checked its structure against deploy.yml's existing opt-in-gate convention.
  • node scripts/check-docs.mjs — passes (CLI_RELEASE_ENABLED/NPM_TOKEN added to the allow-list as GitHub-side config, not app env, matching the existing DEPLOY_ENABLED precedent).
  • pnpm workspace unaffected — cli/npm/* isn't part of the pnpm workspace glob.

What's left for a maintainer

  • Set the CLI_RELEASE_ENABLED repository variable to "true".
  • Add an NPM_TOKEN secret (npm automation token with publish access to the @signet org) to the production environment.
  • Push a cli-v0.1.0-shaped tag (or run the workflow manually) to cut the first real release.

ibochivincent-lang and others added 5 commits August 30, 2026 16:51
Every CLI issue is blocked on this module existing. Add cli/ as a
standalone Go module (go 1.25, no cgo) alongside the pnpm workspace,
building a signet binary via cobra: cmd/signet/main.go is the
entrypoint, internal/cmd holds the command tree, and internal/link,
internal/keys, internal/spec are placeholder packages for the
wallet-linking, key-management, and API-spec work that follows.

signet --version carries a version and commit string injected via
ldflags. signet --help and a bare `signet` invocation both print real
usage text — cobra only renders the Usage: section when a command is
Runnable() or has subcommands, so the root command needed an explicit
RunE (falling through to cmd.Help()) to show it before any subcommand
exists. golangci-lint is configured via cli/.golangci.yml, and a new
CI job builds, vets, tests, and lints the module.

Verified go build/go vet/go test all pass, and the binary
cross-compiles with CGO_ENABLED=0 for linux/amd64 and darwin/arm64.
…entity

Self-hosters and testnet users need to point the CLI at a deployment
other than the default, and repeat runs shouldn't re-ask which identity
to use. Every invocation needing --url and --source made self-hosted
deployments second-class.

Add internal/config: Load/Save a JSON file under os.UserConfigDir()/
signet (baseUrl, source), and Resolve combines flag > env (SIGNET_URL,
baseUrl only) > config file > built-in default per field. Wire --url/
--source as persistent flags on the root command, resolved in
PersistentPreRunE and attached to the command context via
config.WithResolved/FromContext for subcommands to read once they
exist. An explicit --source is saved back to the config file
(RememberSource) so the next run without it picks the same identity
back up. No config file is required for the default deployment.

Document the precedence and file location in cli/README.md, and add
SIGNET_URL to check-docs.mjs's ENV_ALLOW list — it's the cli/ module's
own shell env, not part of the pnpm workspace's .env.
CI pipelines that link a deploy wallet need to parse the result, and
without structured output automation scrapes human-formatted text
that's free to change between releases.

Add internal/link.Link(handle, publicKey, network), validating the
handle (mirroring HANDLE_PATTERN from packages/types) and the public
key's charset/length, returning a Result{Handle, PublicKey, Network,
Status}. This doesn't yet perform a real on-chain claim or call a
deployment's HTTP API — internal/keys and internal/spec, which it
would need for that, are themselves still scaffolded — but the output
contract is real and stable now.

Wire it up as `signet link <handle> --public-key ... [--network]
[--json]`. Without --json, prints a human summary to stdout. With
--json, encodes exactly one JSON object to stdout via encoding/json
and nothing else; on a validation error, stdout stays empty in both
modes and the error reaches the user via stderr (main.go's existing
error handling), never stdout.

Covered by cmd/link_test.go (asserting --json's stdout decodes as the
expected JSON and is exactly one line, non-json output is not JSON,
and an invalid input leaves stdout empty) and link/link_test.go (the
validation logic in isolation).
… hygiene

The CLI orchestrates the developer's deploy identity and ships as a
prebuilt binary with no preview deploy or browser console — a
regression surfaces on a user's machine, not in a PR check.

- internal/keys.ResolvePublicKey(binary, source) resolves a named local
  identity to its public key by shelling out to `stellar keys address
  <source>`, rather than the CLI owning key storage or signing itself.
  Tested against a real faked `stellar` binary (internal/keys/testdata
  /fakestellar, compiled on first use — the standard Go helper-binary
  pattern), not just a swapped-out Go function, so the actual exec.Command
  wiring is exercised.
- internal/exitcode + internal/cmd.ExitCode(err) map an error to a
  process exit code (0 ok, 1 generic, 2 invalid input) via an ExitCoder
  interface — link.ValidationError is the first implementer. exitcode
  lives in its own leaf package so internal/link's error type and
  internal/cmd's classifier can both depend on it without a cycle
  (cmd already imports link for the `link` command).
- secrets_test.go asserts no secret-shaped value (a Stellar S... key)
  ever reaches stdout, stderr, or an error string, across --json and
  non-json runs — which caught a real issue while writing it: the
  invalid-public-key error echoed the raw value back, which would leak
  a real secret key a user passed to the wrong flag by mistake. Fixed
  in link.go to report the shape problem without repeating the value.
- go test -race now runs in CI (needs cgo — a C toolchain, which
  ubuntu-latest has — unrelated to the production binary's
  CGO_ENABLED=0 requirement, since the test binary is never shipped).
  Not runnable in this environment for lack of a local C compiler.
- Config precedence (flag > env > config > default) and --json stdout
  purity were already covered by blockchain-maxis#262/blockchain-maxis#264's tests; unchanged here.

Scoping note: this does not cover "loopback single-use and timeout
behaviour" or a "state mismatch" — no loopback HTTP server exists in
the CLI's architecture today (neither blockchain-maxis#269 nor blockchain-maxis#263 needed one; the
CLI-link challenge exchange is a plain two-request HTTP round trip, not
a browser callback flow), so there is nothing yet to write that test
against. Documented here rather than fabricated.
npx executes JavaScript from the npm registry and cannot run a Go
binary directly, so the designed entrypoint (npx @signet/cli link, no
install step) needed the wrapper pattern esbuild/swc/turbo use:

  @signet/cli                    zero-dependency JS shim that execs the binary
    optionalDependencies:
      @signet/cli-linux-x64      one prebuilt Go binary each
      @signet/cli-linux-arm64
      @signet/cli-darwin-arm64
      @signet/cli-windows-x64

- cli/npm/cli: the shim (bin/signet.js) — resolves the one platform
  package npm installed for the caller and execFileSync's it,
  forwarding stdio and the real exit code. No dependencies.
- cli/npm/cli-<platform>: one package per target, each just a
  package.json (os/cpu constraints) and an empty bin/ populated by the
  release workflow — binaries are never committed.
- scripts/release/stage-platform-package.mjs: copies a built binary
  into a platform package's bin/ (always named exactly signet/signet.exe,
  regardless of the build step's own output filename — the shim only
  ever looks for that name) and pins the package's version.
- scripts/release/pin-shim-version.mjs: pins @signet/cli's own version
  and every optionalDependency's version to the release version
  exactly, so the shim never resolves a floating range that could
  drift from what was actually tested.
- .github/workflows/release-cli.yml: on a `cli-v*.*.*` tag, cross-
  compiles all four targets from one runner (Go cross-compiles cleanly
  with just GOOS/GOARCH, no build matrix needed), checksums them,
  stages+pins the npm packages, and creates a GitHub Release with the
  binaries and checksums attached — all of which runs on every tag
  push. Actual `npm publish` is opt-in, mirroring deploy.yml's
  DEPLOY_ENABLED gate: requires the CLI_RELEASE_ENABLED repository
  variable and an NPM_TOKEN secret, both maintainer-configured.

Verified for real, not just read: built all four targets, staged each
into its npm package, npm pack'd both the shim and the Windows
platform package, installed the tarballs into a scratch project, and
ran the installed `signet` bin end-to-end — --version, --help, `link
--json`, and exit-code passthrough (2 on invalid input) all worked
through the shim exactly as they do calling the Go binary directly.
Fixed a real bug found this way: the staging script originally named
the destination file after the build step's own (disambiguated)
output name instead of the fixed `signet`/`signet.exe` the shim
requires, which would have made every platform package unresolvable.

The workflow YAML itself can't be exercised without a real tag push
and npm credentials — validated its syntax and structure against
deploy.yml's existing conventions instead. Labeled maintainer-owned in
the issue; actually enabling publishing (the repo variable + secret)
is left to the maintainer.
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@Mamavee001 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

@Mamavee001 is attempting to deploy a commit to the blockchainmaxis-8449's projects Team on Vercel.

A member of the Team first needs to authorize it.

@netlify

netlify Bot commented Aug 30, 2026

Copy link
Copy Markdown

Deploy Preview for stellar-signet ready!

Name Link
🔨 Latest commit edde027
🔍 Latest deploy log https://app.netlify.com/projects/stellar-signet/deploys/6a98121f31f24c000878bc0e
😎 Deploy Preview https://deploy-preview-346--stellar-signet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Conflict resolution against the squashed blockchain-maxis#335/blockchain-maxis#336/blockchain-maxis#342:
- .github/workflows/ci.yml, README.md: main's.
- cli/cmd/signet/main.go, cli/internal/link/link.go, cli/internal/cmd/link.go:
  this branch's, which carry blockchain-maxis#345's exit-code mapping that this PR is stacked
  on. Taking main's first silently dropped it — caught because the shim then
  forwarded exit 1 instead of 2 in the end-to-end check below.
- cli/README.md: this branch's, with the stale 'Linked …' example updated to
  main's honest 'Validated … not yet submitted' wording.
- scripts/check-docs.mjs: keep the CLI_RELEASE_ENABLED / NPM_TOKEN allowlist
  entries (GitHub repo variable and Actions secret, not app env).

Reapplied the handle-error secret redaction from blockchain-maxis#345, which this branch
predates.

Verified the release path end to end rather than by reading it — built a
linux/amd64 binary with the workflow's own ldflags, staged it through
scripts/release/stage-platform-package.mjs, pinned with pin-shim-version.mjs,
laid the two packages out as npm would under node_modules/@signet, and ran the
shim:

  --version              -> signet version 9.9.9-test (commit deadbeef)
  link --json            -> one JSON object, nothing else
  invalid handle, $? -> 2   (the shim forwards the binary's real exit code)

Then reverted the staged binary and the 9.9.9-test version pins.

Publishing stays inert on merge: release-cli.yml triggers only on a cli-v*
tag (none exist) and the npm publish step is additionally gated on
vars.CLI_RELEASE_ENABLED, which is not set.

go build/vet/test -race, gofmt, golangci-lint v2.13.2 (0 issues) and
check-docs all clean.
secrets_test.go: take main's, which carries the strengthened assertion on the
error string (the branch's copy predates it).
@blockchain-maxis
blockchain-maxis merged commit 75be0b4 into blockchain-maxis:main Sep 2, 2026
10 of 11 checks passed
blockchain-maxis added a commit to Otfrugger/signet that referenced this pull request Sep 2, 2026
Everything except cli/internal/browser/ and its docs was a cherry-pick of
blockchain-maxis#335/blockchain-maxis#336/blockchain-maxis#342/blockchain-maxis#345/blockchain-maxis#356/blockchain-maxis#357/blockchain-maxis#358, all now on main — took main's for all of
those. scripts/check-docs.mjs keeps all three allowlist entries rather than
either side's pair (WAYLAND_DISPLAY from this branch, CLI_RELEASE_ENABLED and
NPM_TOKEN from blockchain-maxis#346).

Verified the fallback behaves as issue blockchain-maxis#257 requires, by driving OpenOrPrint
with DISPLAY and WAYLAND_DISPLAY unset:

  headless, browser allowed -> prints 'Open this URL to continue: <url>', returns nil
  --no-browser              -> same

so the opener's failure is absorbed rather than propagated, and both paths
reach the developer at the same URL.

Left unwired deliberately, unlike blockchain-maxis#357's CheckStellarCLI: there is no call site
to attach it to yet — `signet link` performs no approval flow (that lands with
the loopback server and the pairing endpoints), so wiring it now would mean
inventing the flow rather than connecting to one. Both the package doc and
cli/README say so plainly.

go build/vet, gofmt, golangci-lint v2.13.2 (0 issues), go test -race ./... and
check-docs all clean.
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.

Release pipeline: cross-compiled binaries plus an npm wrapper so npx @signet/cli link works

4 participants