Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d2a3ed1
feat!: Ship the CLI as a Bun standalone binary
gjtorikian Jul 16, 2026
01e6225
feat: Add Linux musl and Windows arm64 binary targets
gjtorikian Jul 16, 2026
b6253e7
ci(release): Skip already-published npm packages on re-run
gjtorikian Jul 20, 2026
0a0ede4
fix(build): Detect musl for host-default build targets
gjtorikian Jul 20, 2026
1a0abae
fix: Abort stalled Agent SDK downloads and retry once
gjtorikian Jul 20, 2026
689d15f
fix: Re-verify the Agent SDK sha256 in verify-assets
gjtorikian Jul 20, 2026
134766b
test: Cover the skills extraction rename-race recovery
gjtorikian Jul 20, 2026
37070d3
docs: Explain the react-devtools-core devDependency
gjtorikian Jul 20, 2026
ea053d1
Merge branch 'main' into to-bun
gjtorikian Jul 20, 2026
74b2baa
test: Smoke test the npm distribution against a local registry
gjtorikian Jul 20, 2026
ac363bd
test: Smoke the command contract on every release binary
gjtorikian Jul 20, 2026
d59f91f
test: Add authenticated command smoke against staging
gjtorikian Jul 20, 2026
f9ccfd0
test: Surface API errors in the authenticated smoke and allow a stagi…
gjtorikian Jul 20, 2026
3166781
fix: Treat malformed stored credentials as logged out
gjtorikian Jul 20, 2026
054ba88
fix: Report declined installs honestly to machine consumers
gjtorikian Jul 20, 2026
6618f4c
test: Bump the Next.js fixture above the installer minimum
gjtorikian Jul 20, 2026
7e21e24
fix: Validate stored blobs in hasCredentials
gjtorikian Jul 21, 2026
740251b
fix: Harden the Agent SDK first-run download
gjtorikian Jul 21, 2026
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
93 changes: 76 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,94 @@ on:

jobs:
ci:
name: Node ${{ matrix.node }}
name: Bun
runs-on: ubuntu-latest
strategy:
matrix:
node: [20, 22, 24]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0

- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # 6.0.9

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 6.4.0
- uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # 2.0.2
with:
node-version: ${{ matrix.node }}
cache: pnpm
bun-version: 1.3.14

- name: Install
run: pnpm install
run: bun install --frozen-lockfile

- name: Lint
run: pnpm lint
run: bun run lint

- name: Format
run: pnpm format:check
run: bun run format:check

- name: Typecheck
run: pnpm typecheck

- name: Build
run: pnpm build
run: bun run typecheck

- name: Test
run: pnpm test
run: bun run test

- name: Check committed integration manifest is fresh
run: git diff --exit-code src/integrations/_manifest.ts

# Compile the same baseline variant that release.yml ships for linux-x64,
# not the host-default "modern" build — they embed different Bun runtimes.
- name: Build standalone binary
env:
WORKOS_BUILD_TARGET: bun-linux-x64-baseline
run: bun run build

# The full command contract (exit codes, structured JSON errors, JSON
# output — see scripts/command-smoke.sh) must hold standalone: no Bun,
# no Node.js, no node_modules, and no network.
- name: Smoke test command contract without Bun, Node.js, or network
run: >-
docker run --rm --network none
--volume "${PWD}/dist/workos:/workos:ro"
--volume "${PWD}/scripts/command-smoke.sh:/command-smoke.sh:ro"
--env HOME=/tmp/home
debian:bookworm-slim
sh /command-smoke.sh /workos

# `internal verify-assets` performs the first-run download of the pinned
# Agent SDK executable (checksum-verified) and spawns it, so a broken
# download path fails every PR here instead of on a user's first
# `workos install`. Needs network; still no Bun/Node/node_modules.
- name: Smoke test agent runtime first-run download
run: >-
docker run --rm
--volume "${PWD}/dist/workos:/workos:ro"
--env HOME=/tmp/home
debian:bookworm-slim
sh -eu -c
'/workos internal verify-assets --json'

# Authenticated command execution against a real WorkOS staging
# environment: list plus a create → get → delete round-trip. The secret
# is a dedicated staging-environment API key; fork PRs receive no
# secrets, so this skips there instead of failing.
- name: Smoke test authenticated commands
env:
WORKOS_API_KEY: ${{ secrets.WORKOS_SMOKE_API_KEY }}
# Optional: set when the smoke key belongs to a non-default API host
# (e.g. an internal staging deployment).
WORKOS_SMOKE_API_URL: ${{ secrets.WORKOS_SMOKE_API_URL }}
run: |
if [ -z "$WORKOS_API_KEY" ]; then
echo "Skipped: WORKOS_SMOKE_API_KEY secret not available (fork PR or not yet configured)"
exit 0
fi
if [ -n "$WORKOS_SMOKE_API_URL" ]; then
export WORKOS_API_URL="$WORKOS_SMOKE_API_URL"
fi
sh scripts/command-smoke.sh ./dist/workos

# Publish the generated packages to a throwaway local registry and run
# the real user flows — `npx workos`, `npm install -g workos`, platform
# selection, and the launcher's no-binary error path — from a hermetic
# environment. This exercises the npm machinery the in-repo launcher
# check can't (registry fetch, optionalDependencies, npx cache/bin).
- name: Smoke test npm distribution against a local registry
env:
WORKOS_NPM_ALLOW_MISSING: '1'
run: |
cp dist/workos dist/workos-linux-x64
bun run ./scripts/gen-npm-packages.ts
bun run ./scripts/npm-dist-smoke.ts
1 change: 0 additions & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ on:
permissions:
contents: write
pull-requests: write
id-token: write

jobs:
release-please:
Expand Down
235 changes: 221 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,56 +1,263 @@
name: Release

# Release flow: release-please creates a DRAFT release (with its tag pushed
# up front via force-tag-creation), then this workflow cross-compiles every
# target, smoke tests each binary on real hardware for its platform, and only
# publishes the draft once all of them pass. Users never see a release whose
# binaries are missing or broken; on any failure the draft stays invisible and
# the previous release remains `latest`.

on:
workflow_call:
inputs:
tag_name:
description: Release tag name (e.g., v0.12.0-beta.1)
type: string
default: ''
required: true
# Manual escape hatch: re-run the build/smoke/publish for an existing
# draft release tag if a previous run failed.
workflow_dispatch:
inputs:
tag_name:
description: Release tag name (e.g., v0.12.0-beta.1)
type: string
required: true

concurrency:
group: release-${{ inputs.tag_name }}

permissions:
contents: read

jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: bun-darwin-arm64
asset: workos-darwin-arm64
- target: bun-darwin-x64-baseline
asset: workos-darwin-x64
- target: bun-linux-x64-baseline
asset: workos-linux-x64
- target: bun-linux-arm64
asset: workos-linux-arm64
- target: bun-linux-x64-musl-baseline
asset: workos-linux-x64-musl
- target: bun-linux-arm64-musl
asset: workos-linux-arm64-musl
- target: bun-windows-x64-baseline
asset: workos-windows-x64.exe
- target: bun-windows-arm64
asset: workos-windows-arm64.exe
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0
with:
ref: ${{ inputs.tag_name }}

- uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # 2.0.2
with:
bun-version: 1.3.14

- name: Install all target-specific dependencies
run: bun install --frozen-lockfile --os="*" --cpu="*"

# prebuild regenerates the manifests with the same WORKOS_BUILD_TARGET,
# so the pinned Agent SDK download metadata (version, URL, checksum)
# matches the compile target.
- name: Build standalone binary
env:
WORKOS_BUILD_TARGET: ${{ matrix.target }}
WORKOS_BUILD_OUTFILE: ./dist/${{ matrix.asset }}
run: bun run build

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 7.0.1
with:
name: ${{ matrix.asset }}
path: dist/${{ matrix.asset }}
if-no-files-found: error

smoke:
name: Smoke test ${{ matrix.asset }}
needs: build
strategy:
fail-fast: false
matrix:
include:
- asset: workos-darwin-arm64
runner: macos-15
- asset: workos-darwin-x64
runner: macos-15-intel
- asset: workos-linux-x64
runner: ubuntu-24.04
- asset: workos-linux-arm64
runner: ubuntu-24.04-arm
# musl binaries smoke inside Alpine containers on matching-arch
# runners — real musl libc, native CPU, no glibc on the path.
- asset: workos-linux-x64-musl
runner: ubuntu-24.04
musl: true
- asset: workos-linux-arm64-musl
runner: ubuntu-24.04-arm
musl: true
- asset: workos-windows-x64.exe
runner: windows-latest
- asset: workos-windows-arm64.exe
runner: windows-11-arm
runs-on: ${{ matrix.runner }}
steps:
# Checked out so the command-contract smoke script matches the tag
# whose binaries are under test.
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0
with:
ref: ${{ inputs.tag_name }}

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
with:
name: ${{ matrix.asset }}

# Diagnostics only — the hard gate is executing the binary below. Bun
# ad-hoc signs cross-compiled Mach-O binaries; a regression there (see
# oven-sh/bun#29120) makes the exec step SIGKILL on Apple Silicon.
- name: Inspect macOS code signature
if: startsWith(matrix.runner, 'macos')
run: codesign -dvvv "./${{ matrix.asset }}" || true

# `internal verify-assets` extracts the embedded skills from the binary,
# checks the keyring native binding loaded, and performs the first-run
# download of the pinned Agent SDK executable (checksum-verified) and
# spawns it — proof this exact artifact works on real hardware for its
# platform.
- name: Run the binary
if: matrix.musl != true
shell: bash
run: |
case "${{ matrix.asset }}" in
*.exe) ;;
*) chmod +x "./${{ matrix.asset }}" ;;
esac
"./${{ matrix.asset }}" --version
"./${{ matrix.asset }}" --help >/dev/null
"./${{ matrix.asset }}" internal verify-assets --json
sh ./scripts/command-smoke.sh "./${{ matrix.asset }}"

# Bun musl builds dynamically link libstdc++/libgcc (same as Node's musl
# builds) — install them like real Alpine users must.
- name: Run the binary (Alpine container)
if: matrix.musl == true
run: |
chmod +x "./${{ matrix.asset }}"
docker run --rm \
--volume "${PWD}/${{ matrix.asset }}:/workos:ro" \
--volume "${PWD}/scripts/command-smoke.sh:/command-smoke.sh:ro" \
--env HOME=/tmp/home \
alpine:3.22 \
sh -eu -c 'apk add --no-cache libstdc++ libgcc >/dev/null;
/workos --version; /workos --help >/dev/null;
/workos internal verify-assets --json;
sh /command-smoke.sh /workos'

publish:
name: Publish to npm
name: Publish release
needs: [build, smoke]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
with:
path: dist
merge-multiple: true

# Assets attach to the draft first, so tag + release + assets go live
# atomically when the draft flips to published. `releases/latest` (and
# the CLI's update check) never advances early or to a partial release.
- name: Upload assets and publish the draft release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ inputs.tag_name }}
run: |
test -n "$TAG_NAME"
gh release upload "$TAG_NAME" dist/* --clobber
gh release edit "$TAG_NAME" --draft=false

# npm is a secondary channel: a thin `workos` launcher plus one
# @workos/cli-<platform>-<arch> package per binary (esbuild/swc/clerk
# pattern), preserving `npm install -g workos` / `npx workos`. Runs after
# the GitHub release publishes so npm never leads the primary channel; if
# it fails, re-run just this job. Auth is npm trusted publishing (OIDC) —
# same as the pre-binary release flow.
publish-npm:
name: Publish npm packages
needs: [smoke, publish]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0
with:
ref: ${{ inputs.tag_name }}

- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # 6.0.9
- uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # 2.0.2
with:
bun-version: 1.3.14

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 6.4.0
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'
cache: pnpm

- name: Install
run: pnpm install
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
with:
path: dist
merge-multiple: true

- name: Generate npm packages
env:
TAG_NAME: ${{ inputs.tag_name }}
run: WORKOS_NPM_VERSION="${TAG_NAME#v}" bun run ./scripts/gen-npm-packages.ts

- name: Build
run: pnpm build
# Pre-publish gate: publish all generated packages to a throwaway local
# registry and drive the real `npx workos` / `npm install -g workos`
# flows (linux-x64 binary executes on this runner). A packaging
# regression fails here instead of after touching npmjs.org.
- name: Smoke test npm distribution against a local registry
run: bun run ./scripts/npm-dist-smoke.ts

- name: Determine npm tag
id: npm-tag
env:
TAG_NAME: ${{ inputs.tag_name }}
run: |
VERSION="${TAG_NAME#v}"

if [[ -z "$VERSION" ]]; then
VERSION="$(node -p "require('./package.json').version")"
fi

if [[ "$VERSION" == *"-"* ]]; then
echo "tag=beta" >> "$GITHUB_OUTPUT"
else
echo "tag=latest" >> "$GITHUB_OUTPUT"
fi

# Platform packages publish before the launcher so the launcher's
# optionalDependencies are always resolvable once it is visible — the
# single glob list keeps that order (cli-* dirs before the workos dir).
# Each publish is skipped when that exact version is already on npm, so a
# partially-published release converges cleanly on a re-run of this job.
- name: Publish
env:
TAG_NAME: ${{ inputs.tag_name }}
run: |
sed -i '/_authToken/d' "$NPM_CONFIG_USERCONFIG"
unset NODE_AUTH_TOKEN
pnpm publish --tag ${{ steps.npm-tag.outputs.tag }} --access public --no-git-checks --provenance
VERSION="${TAG_NAME#v}"
for dir in dist/npm/@workos/cli-* dist/npm/workos; do
pkg=$(node -p "require('./$dir/package.json').name")
if npm view "$pkg@$VERSION" version >/dev/null 2>&1; then
echo "$pkg@$VERSION already published, skipping"
continue
fi
(cd "$dir" && npm publish --access public --provenance --tag ${{ steps.npm-tag.outputs.tag }})
done
Loading
Loading