name: Build CLI Binaries on: push: branches: [next] paths: - 'ts/packages/cli/**' - 'ts/packages/cli-local-tools/**' - '.github/actions/setup-node-pnpm-bun/action.yml' - '.github/scripts/cli-release/**' - '.github/workflows/build-cli-binaries.yml' - 'mise.toml' - 'mise.lock' workflow_dispatch: inputs: action: description: 'What to do' type: choice options: - build-beta - promote-stable default: build-beta beta_tag: description: 'Existing beta release tag to promote (only for promote-stable, e.g. @composio/cli@0.2.20-beta.42)' required: false # Least privilege by default: only the release job creates/edits releases and uploads assets. # `prepare` reads releases (gh release list) and `build` uploads workflow artifacts — both work # with read access. The release job opts up to `contents: write` for itself. permissions: contents: read # Print Turborepo telemetry events to the logs instead of sending them. env: TURBO_TELEMETRY_DEBUG: 1 jobs: prepare: name: Resolve Release Metadata runs-on: ubuntu-latest outputs: checkout_ref: ${{ steps.resolve.outputs.checkout_ref }} release_name: ${{ steps.resolve.outputs.release_name }} release_tag: ${{ steps.resolve.outputs.release_tag }} release_version: ${{ steps.resolve.outputs.release_version }} prerelease: ${{ steps.resolve.outputs.prerelease }} make_latest: ${{ steps.resolve.outputs.make_latest }} steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.sha }} - name: Resolve release target id: resolve env: EVENT_NAME: ${{ github.event_name }} ACTION_INPUT: ${{ inputs.action }} BETA_TAG_INPUT: ${{ inputs.beta_tag }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPOSITORY: ${{ github.repository }} RUN_NUMBER: ${{ github.run_number }} COMMIT_SHA: ${{ github.sha }} run: bash .github/scripts/cli-release/resolve-release-target.sh build: name: Build ${{ matrix.artifact }} needs: prepare if: needs.prepare.result == 'success' runs-on: ${{ matrix.runner }} strategy: # Never publish a partial platform set: with fail-fast disabled, the build job's # result is `success` only when EVERY matrix leg passes, so a single failure makes # `needs.build.result != 'success'` and the release job is skipped. It also surfaces # all platform failures at once instead of cancelling siblings on the first error. fail-fast: false matrix: include: - target: bun-linux-x64 artifact: composio-linux-x64 runner: ubuntu-latest local_tools_target: '' - target: bun-linux-arm64 artifact: composio-linux-aarch64 runner: ubuntu-latest local_tools_target: '' - target: bun-darwin-x64 artifact: composio-darwin-x64 runner: macos-15-intel local_tools_target: '' - target: bun-darwin-arm64 artifact: composio-darwin-aarch64 runner: macos-15 local_tools_target: darwin-arm64 steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ needs.prepare.outputs.checkout_ref }} - name: Setup Node.js, pnpm, Bun uses: ./.github/actions/setup-node-pnpm-bun - name: Install dependencies run: pnpm install --frozen-lockfile - name: Setup Swift 6.2 for local-tool sidecars if: matrix.local_tools_target != '' uses: swift-actions/setup-swift@364295d9c23900ce04d4e5cc708387921b4e50f9 # v3 with: swift-version: '6.2' - name: Verify Swift toolchain if: matrix.local_tools_target != '' run: swift --version - name: Build packages run: pnpm build:packages - name: Build local-tool sidecar binaries if: matrix.local_tools_target != '' run: pnpm --filter @composio/cli-local-tools build:local-tool-binaries -- --target ${{ matrix.local_tools_target }} - name: Verify local-tool sidecars if: matrix.local_tools_target != '' run: | test -x ts/packages/cli-local-tools/local-tools-binaries/beeper-imessage/${{ matrix.local_tools_target }}/imessage-cli test -x ts/packages/cli-local-tools/local-tools-binaries/peekaboo/${{ matrix.local_tools_target }}/peekaboo test -x ts/packages/cli-local-tools/local-tools-binaries/composio-native-ui/${{ matrix.local_tools_target }}/composio-native-ui - name: Cross-compile CLI binary working-directory: ts/packages/cli run: pnpm build:binary:cross --target ${{ matrix.target }} - name: Verify binary working-directory: ts/packages/cli run: | ls -la dist/binaries/${{ matrix.artifact }} file dist/binaries/${{ matrix.artifact }} - name: Test binary if: matrix.target == 'bun-linux-x64' working-directory: ts/packages/cli env: COMPOSIO_USER_API_KEY: test-dummy-api-key-for-ci run: | ./dist/binaries/${{ matrix.artifact }} --version ./dist/binaries/${{ matrix.artifact }} --help | head -5 - name: Create archive working-directory: ts/packages/cli env: RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} run: pnpm build:binary:package - name: Verify release metadata in archive working-directory: ts/packages/cli run: unzip -p dist/binaries/${{ matrix.artifact }}.zip ${{ matrix.artifact }}/release-tag.txt | grep -Fx '${{ needs.prepare.outputs.release_tag }}' - name: Verify local-tool sidecars in archive if: matrix.local_tools_target != '' working-directory: ts/packages/cli run: | unzip -Z1 dist/binaries/${{ matrix.artifact }}.zip | grep -F "local-tools-binaries/beeper-imessage/${{ matrix.local_tools_target }}/imessage-cli" unzip -Z1 dist/binaries/${{ matrix.artifact }}.zip | grep -F "local-tools-binaries/peekaboo/${{ matrix.local_tools_target }}/peekaboo" unzip -Z1 dist/binaries/${{ matrix.artifact }}.zip | grep -F "local-tools-binaries/composio-native-ui/${{ matrix.local_tools_target }}/composio-native-ui" - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.artifact }} path: ts/packages/cli/dist/binaries/${{ matrix.artifact }}.zip retention-days: 7 release: name: Release (draft → verify → publish) needs: [prepare, build] runs-on: ubuntu-latest permissions: contents: write # create/edit the GitHub Release and upload assets # fail-fast: false on the build matrix ⇒ this is `success` only if ALL platforms built, # so a partial set can never reach the release path. if: needs.build.result == 'success' # Serialize runs that target the SAME release tag (e.g. two quick pushes, or a re-run # racing the original) so they cannot interleave uploads. Keyed on the resolved tag so # unrelated beta builds are not serialized behind each other. Job-level concurrency may # read `needs.*` outputs (workflow-level cannot). The serialized loser fails loudly at the # "already published" guard in create-or-resume-draft.sh — that red ❌ is by design. concurrency: group: cli-release-${{ needs.prepare.outputs.release_tag }} cancel-in-progress: false env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} RELEASE_NAME: ${{ needs.prepare.outputs.release_name }} PRERELEASE: ${{ needs.prepare.outputs.prerelease }} MAKE_LATEST: ${{ needs.prepare.outputs.make_latest }} CHECKOUT_REF: ${{ needs.prepare.outputs.checkout_ref }} RELEASE_HELPERS_DIR: ${{ github.workspace }}/.release-workflow/.github/scripts/cli-release steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ needs.prepare.outputs.checkout_ref }} - name: Setup Node.js, pnpm, Bun uses: ./.github/actions/setup-node-pnpm-bun - name: Install dependencies run: pnpm install --frozen-lockfile - name: Download all artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: ts/packages/cli/dist/binaries merge-multiple: true # Package skills BEFORE generating checksums so composio-skill.zip is included in # checksums.txt alongside the platform binaries (generate-checksums hashes every .zip # in dist/binaries). - name: Package skill files working-directory: ts/packages/cli env: PRERELEASE: ${{ needs.prepare.outputs.prerelease }} run: | pnpm run validate:skills if [[ "$PRERELEASE" == "true" ]]; then channel=beta else channel=stable fi pnpm run build:skills -- --channel "$channel" --output-dir ./dist/skills cd dist/skills && zip -r ../binaries/composio-skill.zip composio-cli/ - name: Generate checksums working-directory: ts/packages/cli run: bun run ./scripts/generate-checksums.ts - name: Display checksums run: cat ts/packages/cli/dist/binaries/checksums.txt - name: Checkout workflow release helpers uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.workflow_sha }} path: .release-workflow sparse-checkout: | .github/scripts/cli-release sparse-checkout-cone-mode: false # Build the release as a DRAFT first. Drafts fire no `release: published` event and are # excluded from the `/releases/latest` redirect, so no anonymous consumer (install.sh, # Homebrew, the redirect) can ever observe a release before its assets are attached and # verified. The release is only flipped to published as the final step below. - name: Create draft release with all assets run: bash "$RELEASE_HELPERS_DIR/create-or-resume-draft.sh" # Loud failure gate: assert the full canonical asset set is attached AND fully uploaded. # An asset can appear in the list while still processing (`state != "uploaded"`), which is # exactly how a release ends up serving 404s, so we require state == "uploaded". - name: Verify all release assets are present and uploaded run: bash "$RELEASE_HELPERS_DIR/verify-assets.sh" # Publishing is the ONLY step that exposes the release. Do not edit the body here: a known # GitHub PATCH race drops body changes made in the same call as the draft flip, and the # notes were already generated at draft creation. Prerelease status was set on the draft. - name: Publish release (flip draft → published) run: gh release edit "$RELEASE_TAG" --draft=false --latest="$MAKE_LATEST" test-installation: name: Test Installation needs: [prepare, release] if: always() && !cancelled() && needs.release.result == 'success' uses: ./.github/workflows/cli.test-installation.yml with: version: ${{ needs.prepare.outputs.release_tag }} create-install-instructions: name: Create Install Instructions needs: [prepare, release] runs-on: ubuntu-latest if: always() && !cancelled() && needs.release.result == 'success' steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Create installation README run: | RELEASE_TAG="${{ needs.prepare.outputs.release_tag }}" cat > INSTALL.md << 'EOF' # Composio CLI Installation ## Quick Install (Recommended) ### Linux and macOS ```bash curl -fsSL https://raw.githubusercontent.com/ComposioHQ/composio/next/install.sh | bash ``` ### Install specific version ```bash curl -fsSL https://raw.githubusercontent.com/ComposioHQ/composio/next/install.sh | bash -s -- RELEASE_TAG_PLACEHOLDER ``` ## Manual Installation 1. Download the appropriate binary for your platform from the [releases page](https://github.com/ComposioHQ/composio/releases) 2. Extract the binary 3. Move it to a directory in your PATH (e.g., `/usr/local/bin`) 4. Make it executable: `chmod +x composio` ## Alternative: npm/pnpm Installation ```bash npm install -g @composio/cli # or pnpm add -g @composio/cli ``` ## Usage ```bash composio --help composio login composio generate ``` ## Supported Platforms - Linux x64 - Linux ARM64 - macOS x64 (Intel) - macOS ARM64 (Apple Silicon) Windows support coming soon! EOF sed -i.bak "s|RELEASE_TAG_PLACEHOLDER|$RELEASE_TAG|g" INSTALL.md && rm -f INSTALL.md.bak - name: Upload install instructions uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: install-instructions path: INSTALL.md