Files
composiohq__composio/.github/workflows/cli.install-health-check.yml
DakshM on Exe (exe.dev) 0a1464d5e8 perf(cli): move the compiler and tokenizer out of the executable
`composio --version` goes from 288ms to 199ms, peak RSS from 97.8MB to
77.3MB, and the executable from 85.9MB to 79.7MB. Every command benefits.

A compiled Bun binary parses its whole embedded bundle before the first
line of JavaScript runs, and #4468 had already made sure the TypeScript
compiler and the tokenizer rank table were never *evaluated* unless
`generate`, `run`, or a large `execute` response needed them. They were
still *parsed* on every start: the compiler alone was 44% of the
executable's JavaScript and the o200k rank table another 28%, so
`--version` spent ~75ms reading code it could never call.

Both now ship as companion modules next to the executable, through the
mechanism `composio run` already uses for its own runtime helpers:

- `generation-runtime.mjs` carries `src/generation/*`, the `composio run`
  source rewrites, `typescript`, `@composio/ts-builders` and
  `openapi-typescript`. `generate ts`, `generate py` and `run` load it
  with `loadInstalledCompanionModule`; from a source checkout the loader
  resolves the `.ts` next to `run-companion-modules.ts` instead, so tests
  and `bun run src/bin.ts` need no build step.
- `execute-output-encoder-runtime.mjs` carries `js-tiktoken/lite` and the
  rank table. `execute` loads it only once a response exceeds the 10KB
  byte pre-filter.

A companion bundles its own copy of `effect`, and a fiber cannot run
primitives built by another copy of the runtime, so nothing Effect-shaped
crosses the boundary: the generation companion exposes plain functions
and promises, runs its pipelines on its own runtime, and returns failures
as values that `src/generation/errors.ts` rebuilds as the CLI's own error
classes, stack included. Generated output is byte-identical to #4468 for
`generate ts`, `generate ts --transpiled` and `generate py`.

Both modules join `RUN_COMPANION_MODULE_BASENAMES`, so the build, release
packaging, install verification, `upgrade` and the self-repair download
pick them up unchanged. The three hand-maintained uninstall lists and the
upgrade E2E fixture gain the two file names.

Two smaller startup costs go with it:

- `src/constants.ts` imported `constants` from `@composio/core`'s root
  entry for two strings and two URLs, which evaluated the whole SDK at
  startup (~25ms of module-scope work, mostly zod schemas). The four
  values are spelled out and pinned to core's by a test.
- `tool-file-uploads.ts` imported three core helpers at module scope that
  only a file upload reaches; they are imported on that path now.

The binary build gains a guard: after bundling the companions it bundles
`src/bin.ts` once more unminified and fails if the executable's graph
reaches `typescript`, `js-tiktoken`, core's root entry, `src/generation/*`
or a companion entry. Without it a stray static import would put the
compiler back into the executable with nothing to notice.

Building also surfaced that `assertBundledRuntimeFiles` blanked string
literals to same-length runs of spaces, which made the import patterns'
`^\s*` backtrack quadratically across the compiler's multi-megabyte
embedded lib strings and stalled the build for over ten minutes. String
bodies are dropped now. (The check itself has never matched a specifier,
since the specifiers it looks for are the string literals it removes;
that is left as it was.)

Measured on the pinned toolchain, Bun 1.4.1+4661e494f, linux-x64, best
of 15, telemetry disabled, both binaries built in the same session:

  composio --version       288ms -> 199ms
  tools execute --help     287ms -> 202ms
  peak RSS                 97.8MB -> 77.3MB
  executable               85.9MB -> 79.7MB
  executable JavaScript    8.3MB -> 2.1MB (minified)

The `execute` tail after `execute.tool_call.end` is unchanged for
responses under 10KB (~10ms) and ~20ms slower above it (351 -> 374ms),
which is the on-demand parse of the 2.2MB encoder companion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx9gEjuiHux2weiHjdNcDs
2026-09-14 15:52:49 +02:00

170 lines
9.1 KiB
YAML

name: CLI Install Health Check
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
permissions:
contents: read # gh release list reads the published releases
jobs:
health-check:
name: Test CLI Installation
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
# Resolve the newest PUBLISHED stable @composio/cli release — exactly what install.sh and
# the /releases/latest redirect (humans) resolve and download from.
#
# Drafts are excluded here, so this only ever resolves a fully-published release — an
# in-flight one stays invisible until its assets are verified and attached.
- name: Resolve newest published release
id: resolve
run: |
set -euo pipefail
tag=$(gh release list --repo "${{ github.repository }}" --limit 1000 --exclude-drafts --exclude-pre-releases --json tagName \
--jq '.[] | select(.tagName | startswith("@composio/cli@")) | .tagName' \
| sed -n '1p')
if [[ -z "$tag" ]]; then
echo "::error::No published @composio/cli release found"
exit 1
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "Latest published release: ${tag}"
# Install the PINNED newest tag. This is the path that 404s when a release ships without
# assets. The no-arg `curl | sh` below is asset-aware and silently falls back to the
# previous good release, so it would stay green even when the newest release is broken —
# which is exactly why the previous canary never caught the recurring outage.
- name: Install newest stable (pinned — fails on a release with missing assets)
run: |
set -euo pipefail
# Install-only leg: this step's signal is asset availability, so it opts out of
# automatic shell setup with COMPOSIO_INSTALL_SHELL=none. Default-flow (auto)
# coverage stays with the no-arg leg below.
curl -fsSL https://composio.dev/install | COMPOSIO_INSTALL_SHELL=none sh -s -- "${{ steps.resolve.outputs.tag }}"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Verify pinned installation
run: |
set -euo pipefail
ls -la "$HOME/.composio" || true
test -x "$HOME/.composio/composio"
test -L "$HOME/.local/bin/composio"
which composio
composio --version
# Secondary: the default no-arg flow most users run, exactly as they run it — including
# automatic shell setup (the default infers the shell from $SHELL; the idempotent rc
# edit on this ephemeral runner is harmless). Asset-aware and self-healing, so it is a
# coverage net rather than the primary signal.
#
# This leg is also the canary for the auto shell-setup default itself: shell-setup
# failure is non-fatal in install.sh (exit 0 + warning), so without asserting the
# actual contract below, a broken default would install a working binary and still
# report green. SHELL=/bin/bash pins deterministic auto-resolution on this runner, and
# the assertions mirror cli.test-installation.yml's checks — the managed PATH block in
# ~/.bashrc, plus that a login shell can actually resolve the binary.
#
# COMPOSIO_BIN_DIR is pinned to a NON-default directory ($HOME/.composio-canary/bin)
# to isolate managed-PATH reachability: Ubuntu's skel ~/.profile already adds
# ~/.local/bin to PATH, and the ~/.bash_profile the installer seeds keeps sourcing
# ~/.profile — so with the default bin dir the login-shell probe below would find
# composio even if the managed block did nothing. No startup file but the managed
# block ever adds the canary dir, so resolving composio from it proves the block.
#
# Deployment coupling: like the production legs in cli.install-e2e.yml, this installs
# from the PRODUCTION installer (composio.dev/install), so the shell-setup assertions
# only pass once the auto-default ships there. This workflow is schedule/workflow_dispatch
# only (no pull_request trigger), so it never runs against a pre-deploy PR or branch.
- name: Install via default no-arg flow
run: |
set -euo pipefail
rm -f \
"$HOME/.local/bin/composio" \
"$HOME/.composio/composio" \
"$HOME/.composio/release-tag.txt" \
"$HOME/.composio/run-helpers-runtime.mjs" \
"$HOME/.composio/run-subagent-shared.mjs" \
"$HOME/.composio/run-subagent-acp.mjs" \
"$HOME/.composio/run-subagent-legacy.mjs" \
"$HOME/.composio/run-subagent-output-mcp.mjs" \
"$HOME/.composio/generation-runtime.mjs" \
"$HOME/.composio/execute-output-encoder-runtime.mjs"
rm -rf \
"$HOME/.composio/services" \
"$HOME/.composio/acp-adapters" \
"$HOME/.composio/local-tools-binaries"
# Torn down separately, and deliberately not part of the block above:
# test/release-workflow.test.ts pins that block against INSTALL.md's
# documented uninstall, and the canary bin dir is this workflow's own
# isolation directory rather than something a user would ever remove.
rm -rf "$HOME/.composio-canary"
curl -fsSL https://composio.dev/install | SHELL=/bin/bash COMPOSIO_BIN_DIR="$HOME/.composio-canary/bin" sh
test -x "$HOME/.composio/composio"
test -L "$HOME/.composio-canary/bin/composio"
"$HOME/.composio-canary/bin/composio" --version
# The pinned leg's default entry point was removed above and must stay
# absent: the canary bin dir is the only place this install may surface
# the binary, so nothing outside the managed block can resolve it.
test ! -e "$HOME/.local/bin/composio"
# Assert the auto shell-setup contract: the managed PATH block landed in ~/.bashrc.
if grep -Fqx '# Composio CLI' "$HOME/.bashrc" \
&& grep -Fqx 'export PATH="$HOME/.composio-canary/bin:$PATH"' "$HOME/.bashrc"; then
echo "✅ ~/.bashrc gained the managed Composio PATH block via automatic shell setup"
else
echo "❌ ~/.bashrc missing the managed Composio PATH block after the no-argument install"
echo "=== ~/.bashrc contents ==="
cat "$HOME/.bashrc" || true
exit 1
fi
# Assert the login-mode startup file (the file a login bash actually reads —
# ~/.bashrc is not it) also carries the managed block. Mirrors install.sh's
# bash_login_path_file selection: first existing of ~/.bash_profile or
# ~/.bash_login, else the ~/.bash_profile the installer creates.
if [ ! -f "$HOME/.bash_profile" ] && [ -f "$HOME/.bash_login" ]; then
login_file="$HOME/.bash_login"
else
login_file="$HOME/.bash_profile"
fi
if grep -Fqx '# Composio CLI' "$login_file" \
&& grep -Fqx 'export PATH="$HOME/.composio-canary/bin:$PATH"' "$login_file"; then
echo "✅ $login_file gained the managed Composio PATH block via automatic shell setup"
else
echo "❌ $login_file missing the managed Composio PATH block after the no-argument install"
echo "=== $login_file contents ==="
cat "$login_file" || true
exit 1
fi
# Assert a login shell actually resolves the binary through the MANAGED
# PATH block. env -i drops the step's inherited PATH (GITHUB_PATH from the
# pinned-tag step), and the non-default canary bin dir defeats the other
# leak: ~/.profile — which the installer-seeded ~/.bash_profile keeps
# sourcing — prepends ~/.local/bin, never $HOME/.composio-canary/bin. The
# probe therefore requires the resolved path to be the canary entry point.
# This mirrors the harness's assert_login_bash_path clean-environment probe.
resolved=$(env -i HOME="$HOME" PATH=/usr/bin:/bin bash -ilc 'command -v composio' || true)
if [ "$resolved" = "$HOME/.composio-canary/bin/composio" ] \
&& env -i HOME="$HOME" PATH=/usr/bin:/bin bash -ilc 'composio --version'; then
echo "✅ composio resolves to the managed canary bin dir via a login bash shell"
else
echo "❌ composio did not resolve to \$HOME/.composio-canary/bin/composio via a login bash shell (got: ${resolved:-nothing})"
exit 1
fi
- name: Send Slack Notification (Failure)
if: failure()
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
with:
webhook: ${{ secrets.SLACK_RELEASE_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "⚠️ CLI Install Health Check Failed!\n*Repository:* ${{ github.repository }}\n*Workflow:* ${{ github.workflow }}\n*Run:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Commit:* ${{ github.sha }}"
}