-
Notifications
You must be signed in to change notification settings - Fork 10
feat: Add Homebrew channel and install-aware updates #196
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
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
d2a3ed1
feat!: Ship the CLI as a Bun standalone binary
gjtorikian 01e6225
feat: Add Linux musl and Windows arm64 binary targets
gjtorikian b6253e7
ci(release): Skip already-published npm packages on re-run
gjtorikian 0a0ede4
fix(build): Detect musl for host-default build targets
gjtorikian 1a0abae
fix: Abort stalled Agent SDK downloads and retry once
gjtorikian 689d15f
fix: Re-verify the Agent SDK sha256 in verify-assets
gjtorikian 134766b
test: Cover the skills extraction rename-race recovery
gjtorikian 37070d3
docs: Explain the react-devtools-core devDependency
gjtorikian ea053d1
Merge branch 'main' into to-bun
gjtorikian 74b2baa
test: Smoke test the npm distribution against a local registry
gjtorikian ac363bd
test: Smoke the command contract on every release binary
gjtorikian d59f91f
test: Add authenticated command smoke against staging
gjtorikian f9ccfd0
test: Surface API errors in the authenticated smoke and allow a stagi…
gjtorikian 3166781
fix: Treat malformed stored credentials as logged out
gjtorikian 054ba88
fix: Report declined installs honestly to machine consumers
gjtorikian 6618f4c
test: Bump the Next.js fixture above the installer minimum
gjtorikian 7e21e24
fix: Validate stored blobs in hasCredentials
gjtorikian 740251b
fix: Harden the Agent SDK first-run download
gjtorikian 72510c1
feat: Add Homebrew channel and install-aware updates
gjtorikian e0a561f
fix: Classify Windows npm paths in install detection
gjtorikian ae5c7d7
ci: Push the Homebrew formula via the SDK Automation Bot
gjtorikian 89a81fc
Merge branch 'main' into homebrew-distribution
gjtorikian 237a414
ci: Restore id-token permission and inherit secrets in the release call
gjtorikian a04935c
ci: Split the Bun CI workflow into lint and test workflows
gjtorikian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| name: Lint | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| lint: | ||
| name: Lint | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 | ||
|
|
||
| - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # 2.0.2 | ||
| with: | ||
| bun-version: 1.3.14 | ||
|
|
||
| - name: Install | ||
| run: bun install --frozen-lockfile | ||
|
|
||
| - name: Lint | ||
| run: bun run lint | ||
|
|
||
| - name: Format | ||
| run: bun run format:check | ||
|
|
||
| - name: Typecheck | ||
| run: bun run typecheck |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| // Generates the Homebrew formula (dist/homebrew/workos.rb) from the already-built | ||
| // platform binaries in dist/ (release asset names). The release workflow pushes | ||
| // the result to the workos/homebrew-tap repo, so `brew install workos/tap/workos` | ||
| // tracks each GitHub release. | ||
| // | ||
| // Homebrew downloads the raw release binary (no tarball) and installs it as | ||
| // `workos`; the sha256 for each platform is computed here from the same | ||
| // artifacts the GitHub release ships, so the tap can never drift from the bits. | ||
| // | ||
| // WORKOS_HOMEBREW_VERSION version to stamp; defaults to the repo package.json | ||
| // version. Prerelease versions (containing `-`) are | ||
| // rejected — brew users track stable only. | ||
| import { createHash } from 'node:crypto'; | ||
| import { existsSync, mkdirSync, rmSync } from 'node:fs'; | ||
| import { readFile, writeFile } from 'node:fs/promises'; | ||
| import { join } from 'node:path'; | ||
|
|
||
| const projectRoot = join(import.meta.dirname, '..'); | ||
| const distDir = join(projectRoot, 'dist'); | ||
| const outDir = join(distDir, 'homebrew'); | ||
|
|
||
| const rootPackageJson = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8')) as { | ||
| version: string; | ||
| license?: string; | ||
| }; | ||
| const version = process.env.WORKOS_HOMEBREW_VERSION ?? rootPackageJson.version; | ||
| if (!/^\d+\.\d+\.\d+$/.test(version)) { | ||
| throw new Error(`Refusing to generate a Homebrew formula for non-stable version: ${version}`); | ||
| } | ||
|
|
||
| // Hardcoded rather than reused from package.json: Homebrew's audit wants a desc | ||
| // with no leading article and without the formula name, unlike the npm-facing | ||
| // package description. | ||
| const DESCRIPTION = 'Install AuthKit integrations and manage WorkOS resources'; | ||
| const LICENSE = rootPackageJson.license ?? 'MIT'; | ||
| const HOMEPAGE = 'https://github.com/workos/cli'; | ||
| const DOWNLOAD_BASE = `https://github.com/workos/cli/releases/download/v${version}`; | ||
|
|
||
| // Homebrew macOS + Linuxbrew (glibc) only — no musl or Windows. `on_arm`/ | ||
| // `on_intel` blocks map straight onto these four release assets. | ||
| const PLATFORMS: Array<{ os: 'macos' | 'linux'; cpu: 'arm' | 'intel'; asset: string }> = [ | ||
| { os: 'macos', cpu: 'arm', asset: 'workos-darwin-arm64' }, | ||
| { os: 'macos', cpu: 'intel', asset: 'workos-darwin-x64' }, | ||
| { os: 'linux', cpu: 'arm', asset: 'workos-linux-arm64' }, | ||
| { os: 'linux', cpu: 'intel', asset: 'workos-linux-x64' }, | ||
| ]; | ||
|
|
||
| const missing = PLATFORMS.filter((p) => !existsSync(join(distDir, p.asset))); | ||
| if (missing.length > 0) { | ||
| throw new Error(`Missing platform binaries in dist/: ${missing.map((p) => p.asset).join(', ')}`); | ||
| } | ||
|
|
||
| async function sha256(asset: string): Promise<string> { | ||
| return createHash('sha256') | ||
| .update(await readFile(join(distDir, asset))) | ||
| .digest('hex'); | ||
| } | ||
|
|
||
| const shas = new Map<string, string>(); | ||
| for (const platform of PLATFORMS) { | ||
| shas.set(platform.asset, await sha256(platform.asset)); | ||
| } | ||
|
|
||
| // One `on_arm`/`on_intel` stanza; each pins the URL + sha and installs the raw | ||
| // binary (named after the URL's basename) as `workos`. | ||
| function stanza(cpu: 'arm' | 'intel', asset: string): string { | ||
| return [ | ||
| ` on_${cpu} do`, | ||
| ` url "${DOWNLOAD_BASE}/${asset}"`, | ||
| ` sha256 "${shas.get(asset)}"`, | ||
| '', | ||
| ' def install', | ||
| ` bin.install "${asset}" => "workos"`, | ||
| ' end', | ||
| ' end', | ||
| ].join('\n'); | ||
| } | ||
|
|
||
| const macos = PLATFORMS.filter((p) => p.os === 'macos'); | ||
| const linux = PLATFORMS.filter((p) => p.os === 'linux'); | ||
|
|
||
| const formula = `# typed: false | ||
| # frozen_string_literal: true | ||
|
|
||
| # Generated by workos/cli release automation (scripts/gen-homebrew-formula.ts). | ||
| # DO NOT EDIT — changes are overwritten on the next release. | ||
| class Workos < Formula | ||
| desc "${DESCRIPTION}" | ||
| homepage "${HOMEPAGE}" | ||
| version "${version}" | ||
| license "${LICENSE}" | ||
|
|
||
| on_macos do | ||
| ${macos.map((p) => stanza(p.cpu, p.asset)).join('\n')} | ||
| end | ||
|
|
||
| on_linux do | ||
| ${linux.map((p) => stanza(p.cpu, p.asset)).join('\n')} | ||
| end | ||
|
|
||
| test do | ||
| assert_match version.to_s, shell_output("#{bin}/workos --version") | ||
| end | ||
| end | ||
| `; | ||
|
|
||
| rmSync(outDir, { recursive: true, force: true }); | ||
| mkdirSync(outDir, { recursive: true }); | ||
| await writeFile(join(outDir, 'workos.rb'), formula); | ||
|
|
||
| console.log(`Generated Homebrew formula (v${version}) at ${join(outDir, 'workos.rb')}:`); | ||
| for (const platform of PLATFORMS) { | ||
| console.log(` ${platform.os}/${platform.cpu} ${platform.asset} ${shas.get(platform.asset)}`); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { detectInstallMethod, upgradeNotice } from './install-method.js'; | ||
|
|
||
| describe('detectInstallMethod', () => { | ||
| it.each([ | ||
| ['/opt/homebrew/Cellar/workos/0.18.0/bin/workos', 'homebrew'], // Apple Silicon | ||
| ['/usr/local/Cellar/workos/0.18.0/bin/workos', 'homebrew'], // Intel mac | ||
| ['/home/linuxbrew/.linuxbrew/Cellar/workos/0.18.0/bin/workos', 'homebrew'], // Linuxbrew | ||
| ['/opt/homebrew/bin/workos', 'homebrew'], // unresolved symlink on PATH | ||
| ] as const)('detects homebrew from %s', (execPath, expected) => { | ||
| expect(detectInstallMethod(execPath)).toBe(expected); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ['/usr/local/lib/node_modules/@workos/cli-darwin-arm64/bin/workos', 'npm'], // global prefix | ||
| ['/Users/dev/.nvm/versions/node/v20.11.0/lib/node_modules/@workos/cli-linux-x64/bin/workos', 'npm'], // nvm | ||
| ['C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\@workos\\cli-win32-x64\\bin\\workos.exe', 'npm'], // Windows | ||
| ] as const)('detects npm from %s', (execPath, expected) => { | ||
| expect(detectInstallMethod(execPath)).toBe(expected); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ['/usr/local/bin/workos', 'download'], // manual copy onto PATH | ||
| ['/Users/dev/Downloads/workos-darwin-arm64', 'download'], // ran from Downloads | ||
| ['/Users/dev/.local/bin/workos', 'download'], | ||
| ['C:\\Users\\me\\bin\\workos.exe', 'download'], // Windows manual copy | ||
| ] as const)('falls back to download from %s', (execPath, expected) => { | ||
| expect(detectInstallMethod(execPath)).toBe(expected); | ||
| }); | ||
| }); | ||
|
|
||
| describe('upgradeNotice', () => { | ||
| it('suggests brew for homebrew installs', () => { | ||
| expect(upgradeNotice('homebrew')).toBe('Upgrade: brew upgrade workos'); | ||
| }); | ||
|
|
||
| it('suggests npm for npm installs', () => { | ||
| expect(upgradeNotice('npm')).toBe('Upgrade: npm install -g workos@latest'); | ||
| }); | ||
|
|
||
| it('links to GitHub Releases for downloaded binaries', () => { | ||
| expect(upgradeNotice('download')).toBe('Download: https://github.com/workos/cli/releases/latest'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // Detects how this CLI binary was installed so the "update available" notice | ||
| // can suggest the matching upgrade command instead of a one-size-fits-all | ||
| // GitHub link. The only runtime signal is process.execPath — for a Bun | ||
| // standalone binary that is the path to the compiled executable itself (not a | ||
| // runtime + script), which is exactly the file a package manager laid down. | ||
| export type InstallMethod = 'homebrew' | 'npm' | 'download'; | ||
|
|
||
| const RELEASES_URL = 'https://github.com/workos/cli/releases/latest'; | ||
|
|
||
| /** | ||
| * Infer the install channel from the running binary's path. | ||
| * | ||
| * - `homebrew`: Homebrew stages every formula under a versioned Cellar dir and | ||
| * symlinks it onto PATH. execPath resolves to the real Cellar path on macOS | ||
| * (`/opt/homebrew`, `/usr/local`) and Linuxbrew (`/home/linuxbrew/.linuxbrew`); | ||
| * the prefix checks also catch an unresolved `/opt/homebrew/bin` symlink. | ||
| * - `npm`: the npm launcher spawns the platform package's binary, which lives | ||
| * under `node_modules/@workos/cli-<platform>/bin/workos`. | ||
| * - `download`: a binary pulled straight from GitHub Releases onto PATH. | ||
| * | ||
| * @param execPath overridable for testing; defaults to the running binary. | ||
| */ | ||
| export function detectInstallMethod(execPath: string = process.execPath): InstallMethod { | ||
| // Normalize Windows separators first: the npm launcher's binary lives at | ||
| // …\npm\node_modules\@workos\cli-win32-x64\bin\workos.exe, which the | ||
| // POSIX-style markers below would otherwise miss. | ||
| const path = execPath.replaceAll('\\', '/'); | ||
| if (path.includes('/Cellar/') || path.includes('/opt/homebrew/') || path.includes('/.linuxbrew/')) { | ||
| return 'homebrew'; | ||
| } | ||
| if (path.includes('/node_modules/')) { | ||
| return 'npm'; | ||
| } | ||
| return 'download'; | ||
| } | ||
|
|
||
| /** | ||
| * The upgrade line shown under an "Update available" notice, matched to how the | ||
| * running binary was installed. | ||
| */ | ||
| export function upgradeNotice(method: InstallMethod = detectInstallMethod()): string { | ||
| switch (method) { | ||
| case 'homebrew': | ||
| return 'Upgrade: brew upgrade workos'; | ||
| case 'npm': | ||
| return 'Upgrade: npm install -g workos@latest'; | ||
| case 'download': | ||
| return `Download: ${RELEASES_URL}`; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.