-
Notifications
You must be signed in to change notification settings - Fork 34
feat: ship per-arch standalone binaries on each release #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you pin these actions to a specific SHA? That's considered best practice in a workflow like this - I think it'd be
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - 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 }}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doesn't this script need two params?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same feedback as above:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Attach binaries to release | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gh release upload "${{ steps.tag.outputs.tag }}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same, replace with |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| node_modules/ | ||
| dist/ | ||
| dist-bin/ | ||
| *.log | ||
| *.tgz | ||
| .DS_Store | ||
|
|
||
| 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, | ||
| }); | ||
|
Comment on lines
+72
to
+78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bun compiled executables auto-load .env from the current working directory. This means a malicious .env in the working directory could override LINK_API_BASE_URL or LINK_AUTH_BASE_URL to redirect API calls. We need to disable this behavior; we could wrap the entry point here in a small wrapper that disables env pick up i.e. Can you give this a shot and confirm that a local |
||
| 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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run:steps interpolate ${{ }} expressions directly into shell commands. Per https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections, these should use intermediate env: vars to avoid shell injection via crafted tag names.