diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml new file mode 100644 index 0000000..677b8db --- /dev/null +++ b/.github/workflows/release-binaries.yml @@ -0,0 +1,74 @@ +name: Release binaries + +# Triggers when a GitHub Release is published (changesets/action does this on +# every npm publish). Builds standalone single-file binaries for every Stripe +# Link CLI target and attaches them, plus a manifest.json with sha256s, to +# the same release. Downstream consumers (Houston, OpenClaw, etc.) pin against +# the manifest URL so they can bundle link-cli without an npm runtime. +# +# workflow_dispatch path is provided for manual rebuilds against a past tag. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + release_tag: + description: Existing release tag to attach binaries to (e.g. @stripe/link-cli@0.4.2) + required: true + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Resolve release tag + id: tag + run: | + TAG="${{ github.event.release.tag_name || github.event.inputs.release_tag }}" + # Strip the @stripe/link-cli@ prefix changesets uses, leaving "0.4.2" + VERSION="${TAG##*@}" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.tag.outputs.tag }} + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.10' + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - run: pnpm turbo run build + + - name: Build binaries + run: | + for target in darwin-arm64 darwin-x64 linux-x64 windows-x64; do + bun run scripts/build-binary.ts "$target" + done + + - name: Generate manifest + run: bun run scripts/generate-manifest.ts "${{ steps.tag.outputs.version }}" + + - name: Attach binaries to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload "${{ steps.tag.outputs.tag }}" \ + dist-bin/link-cli-darwin-arm64 \ + dist-bin/link-cli-darwin-x64 \ + dist-bin/link-cli-linux-x64 \ + dist-bin/link-cli-windows-x64.exe \ + dist-bin/manifest.json \ + --clobber diff --git a/.gitignore b/.gitignore index 550c075..9fcb8a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +dist-bin/ *.log *.tgz .DS_Store diff --git a/README.md b/README.md index aa9e053..d4e4d49 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,18 @@ Or run directly with `npx`: npx @stripe/link-cli ``` +### Standalone binaries + +Each release also ships single-file standalone binaries for darwin-arm64, darwin-x64, linux-x64, and windows-x64. These do not require Node or npm at runtime and are intended for embedding inside other applications (desktop apps, CI runners, agent platforms). + +Download from the [latest release](https://github.com/stripe/link-cli/releases/latest), or pin a specific build via the per-release `manifest.json`: + +``` +https://github.com/stripe/link-cli/releases/download//manifest.json +``` + +The manifest reports the `version`, file names, sha256 checksums, and download URLs for every target. + ### Use with agents Install the skill: diff --git a/scripts/build-binary.ts b/scripts/build-binary.ts new file mode 100644 index 0000000..d33900d --- /dev/null +++ b/scripts/build-binary.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env bun +/** + * Build a single-file standalone link-cli binary for the requested target. + * + * Usage: + * bun run scripts/build-binary.ts + * + * Targets: darwin-arm64 | darwin-x64 | linux-x64 | windows-x64 + * + * Output is written to dist-bin/link-cli-[.exe]. + * + * The bundle entrypoint is the same packages/cli/dist/cli.js that tsup emits, + * so this script must run AFTER `pnpm turbo run build`. + * + * Two of ink's transitive dependencies are stubbed at bundle time: + * - react-devtools-core: only used when DEV=true; ink uses a dynamic import, + * but tsup bundles the static reference inside ink/build/devtools.js, which + * then breaks compile if the optional dep is not installed. + * - update-notifier: marked external in tsup config; replaced with a noop + * so the standalone binary does not need the on-disk package present. + */ +import { mkdirSync } from 'node:fs'; +import type { BunPlugin } from 'bun'; + +const TARGETS = { + 'darwin-arm64': 'bun-darwin-arm64', + 'darwin-x64': 'bun-darwin-x64', + 'linux-x64': 'bun-linux-x64', + 'windows-x64': 'bun-windows-x64', +} as const; + +type Target = keyof typeof TARGETS; + +const stubPlugin: BunPlugin = { + name: 'stub-optional-deps', + setup(build) { + build.onResolve( + { filter: /^(react-devtools-core|update-notifier)$/ }, + (args) => ({ path: args.path, namespace: 'stub' }), + ); + build.onLoad({ filter: /.*/, namespace: 'stub' }, (args) => { + if (args.path === 'react-devtools-core') { + return { + contents: 'export default { connectToDevTools() {} };', + loader: 'js', + }; + } + return { + contents: + 'const noop = () => ({ notify: () => {} }); export default noop;', + loader: 'js', + }; + }); + }, +}; + +const target = (process.argv[2] ?? 'darwin-arm64') as Target; +const flag = TARGETS[target]; +if (!flag) { + console.error(`Unknown target: ${target}`); + console.error(`Valid targets: ${Object.keys(TARGETS).join(', ')}`); + process.exit(1); +} + +mkdirSync('dist-bin', { recursive: true }); + +const ext = target.startsWith('windows') ? '.exe' : ''; +const outfile = `./dist-bin/link-cli-${target}${ext}`; + +console.log(`Building ${flag} -> ${outfile}`); + +await Bun.build({ + entrypoints: ['./packages/cli/dist/cli.js'], + // @ts-expect-error compile is a Bun.build option but not in the public types yet + compile: { target: flag, outfile }, + plugins: [stubPlugin], + minify: true, +}); diff --git a/scripts/generate-manifest.ts b/scripts/generate-manifest.ts new file mode 100644 index 0000000..b61fb7c --- /dev/null +++ b/scripts/generate-manifest.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env bun +/** + * Emit dist-bin/manifest.json with sha256 checksums + download URLs for every + * binary in dist-bin/. Consumed by release-binaries.yml after the binaries are + * built. Downstream consumers (Houston, etc.) pin against this manifest. + * + * Usage: + * bun run scripts/generate-manifest.ts + */ +import { createHash } from 'node:crypto'; +import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const version = process.argv[2]; +if (!version) { + console.error('Usage: bun run scripts/generate-manifest.ts '); + process.exit(1); +} + +const baseUrl = `https://github.com/stripe/link-cli/releases/download/${version}`; +const distDir = 'dist-bin'; +const files = readdirSync(distDir).filter((f) => f.startsWith('link-cli-')); + +const binaries: Record = + {}; + +for (const file of files) { + const target = file.replace(/^link-cli-/, '').replace(/\.exe$/, ''); + const buf = readFileSync(join(distDir, file)); + const sha256 = createHash('sha256').update(buf).digest('hex'); + binaries[target] = { file, sha256, url: `${baseUrl}/${file}` }; +} + +const manifest = { + version, + generated_at: new Date().toISOString(), + binaries, +}; + +writeFileSync( + join(distDir, 'manifest.json'), + `${JSON.stringify(manifest, null, 2)}\n`, +); +console.log(JSON.stringify(manifest, null, 2));