Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
@@ -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 }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

doesn't this script need two params?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — second arg was an optional URL override but nothing ever passed it, so it was dead flexibility. Removed in 3ee1633: script now takes one required version arg, with the GitHub releases URL hardcoded inline.


- 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
dist-bin/
*.log
*.tgz
.DS_Store
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<tag>/manifest.json
```

The manifest reports the `version`, file names, sha256 checksums, and download URLs for every target.

### Use with agents

Install the skill:
Expand Down
78 changes: 78 additions & 0 deletions scripts/build-binary.ts
Original file line number Diff line number Diff line change
@@ -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 <target>
*
* Targets: darwin-arm64 | darwin-x64 | linux-x64 | windows-x64
*
* Output is written to dist-bin/link-cli-<target>[.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,
});
44 changes: 44 additions & 0 deletions scripts/generate-manifest.ts
Original file line number Diff line number Diff line change
@@ -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 <version>
*/
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 <version>');
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<string, { file: string; sha256: string; url: string }> =
{};

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));