name: "Cruise"
description: >-
Trigger the cruise coding agent from a GitHub Issue (@cruise plan / run /
exec / fix). Runs cruise's plan -> implement -> PR workflow (or a direct
exec push) inside GitHub Actions, driving pi_agent_rust directly (sdk: pi),
and reports progress back as a comment.
author: "smartcrabai"
branding:
icon: "anchor"
color: "blue"
inputs:
anthropic_api_key:
description: >-
Anthropic API key used by pi (the sdk: pi backend cruise always drives
in this action). Provide an API key through `anthropic_api_key`,
`openai_api_key`, or `provider_api_keys`, or another pi-supported
provider's key (e.g. KIMI_API_KEY) via `env` -- the gate step fails
clearly when all four credential inputs are empty.
required: false
default: ""
openai_api_key:
description: >-
OpenAI API key used by pi. See `anthropic_api_key` for how the
at-least-one-key requirement is checked.
required: false
default: ""
github_token:
description: >-
Token used for GitHub API calls (permission checks, comments, PRs,
pushes). Leave empty (the default) to let cruise obtain a short-lived
cruise-agent GitHub App installation token via OIDC token exchange (see
`token_exchange_url`), falling back to the workflow's GITHUB_TOKEN when
the App isn't installed or the exchange is unavailable. Set this
explicitly (e.g. to a PAT or your own App's token) to skip the OIDC
exchange entirely and use that token instead.
required: false
default: ""
token_exchange_url:
description: >-
URL of the cruise-agent token-exchange service used to trade this job's
OIDC token for a repository-scoped GitHub App installation token. Empty
disables the exchange (the action then always falls back to
`github_token`/`GITHUB_TOKEN`). See docs/github-actions.md for
self-hosting your own exchange.
required: false
default: "https://cruise-token-exchange.smartcrab.ai/token"
trigger_phrase:
description: 'Phrase that must appear in the comment/issue body to trigger cruise.'
required: false
default: "@cruise"
cruise_version:
description: >-
cruise release to install. "latest" (default) or a specific tag such as
"v0.1.68". Requires cruise v0.1.68 or later (the first release with
`sdk: pi` support, which this action always uses).
required: false
default: "latest"
config:
description: >-
Path (relative to the repository root, or absolute) to a cruise
workflow config YAML, used by the `run`/`plan`/`fix` commands (sets
CRUISE_CONFIG). Empty (default) lets cruise's own resolver pick
cruise.yaml/cruise.yml/.cruise.yaml/.cruise.yml from the checkout, or
its built-in default if none exists. Has no effect on `exec`, which
always uses its own generated `{input}`-only config.
required: false
default: ""
model:
description: >-
Overrides CRUISE_MODEL (the model used for implementation steps), in
pi's model-reference format: "provider/model[:thinking]" (e.g.
"openai-codex/gpt-5.5:xhigh"), a bare model id ("claude-sonnet-4-6"), or
empty to let pi auto-select a provider/model from its built-in
preference order.
required: false
default: ""
plan_model:
description: >-
Overrides CRUISE_PLAN_MODEL (the model used by the `plan`/`fix`
commands' planning step), same format as `model`.
required: false
default: ""
pi_models_json:
description: >-
Raw contents of a pi `models.json` file (custom providers / OpenAI-
compatible endpoints / model registry overrides -- see pi's own model
docs for the schema). When `providers` is also set, this is deep-merged
on top of the document generated from it (this wins on any key both
define -- it's the raw escape hatch) rather than being mutually
exclusive with it. The merged (or standalone) result is written to
`$RUNNER_TEMP/pi-agent/models.json` and `PI_CODING_AGENT_DIR` is
pointed at that directory for the duration of the run. Empty (default)
leaves pi's model resolution untouched unless `providers` is set.
required: false
default: ""
env:
description: >-
Extra environment variables to export into the cruise process, one
`KEY=VALUE` per line (blank lines and lines starting with "#" are
ignored). Each value is masked in the log before being exported.
Reserved names (GITHUB_TOKEN, GH_TOKEN, PI_CODING_AGENT_DIR, PATH,
HOME, SHELL, the git identity vars, the XDG_* vars, and anything
prefixed CRUISE_/GITHUB_/ACTIONS_/RUNNER_) are skipped with a
warning instead of being overridden. ANTHROPIC_API_KEY and
OPENAI_API_KEY are NOT reserved: passing them here, instead of or
alongside the dedicated anthropic_api_key/openai_api_key inputs, is a
supported way to satisfy the credential gate.
required: false
default: ""
providers:
description: >-
JSON object keyed by pi provider id (letters/digits/`.`/`_`/`-`). Each
value requires `api` (one of `anthropic-messages`, `openai-completions`,
`openai-responses`, `google-generative-ai`, `azure-openai-responses`,
`bedrock-converse-stream`, `cohere-chat`), `base_url`, and a non-empty
`models` array (each entry a bare model id string, or an object with
`id` plus optional `name`/`api`/`reasoning`/`input`/`cost`/
`context_window`/`max_tokens`/`headers`/`compat`). Optional
provider-level keys: `headers`, `auth_header`, `compat`, and
`no_auth: true` for a key-less endpoint (rejected for
`bedrock-converse-stream`). See docs/github-actions.md for the full
schema, including `cost`/`compat` field details and the built-in
provider-id collision warning.
required: false
default: ""
provider_api_keys:
description: >-
One `provider-id=API key` per line for `providers`; blank lines and
lines starting with "#" are ignored, and the first "=" separates the
id (keys may contain "="). Required for every `providers` entry that
isn't `no_auth: true`; may be left empty when every entry is
`no_auth`. See docs/github-actions.md.
required: false
default: ""
allowed_bots:
description: >-
Comma-separated list of bot logins (without the "[bot]" suffix) allowed
to trigger cruise, or "*" to allow any bot. Empty (default) blocks all
bot actors.
required: false
default: ""
git_user_name:
description: >-
git user.name used for commits cruise/this action creates. Defaults to
"cruise-agent[bot]" when the run used a cruise-agent App installation
token, otherwise "github-actions[bot]".
required: false
default: ""
git_user_email:
description: >-
git user.email used for commits cruise/this action creates. Defaults to
match `git_user_name`'s default (the cruise-agent App's or
github-actions[bot]'s noreply address). Commits still add the triggering
user as a Co-authored-by trailer when the event payload includes their
login and numeric user id.
required: false
default: ""
outputs:
command:
description: '"run", "exec", "plan", or "fix" -- the command parsed from the mention (empty if the gate skipped the run).'
value: ${{ steps.gate.outputs.command }}
session_id:
description: 'The cruise session ID that was created (empty if skipped/failed before creation).'
value: ${{ steps.run.outputs.session_id }}
pr_url:
description: 'URL of the pull request cruise created (the `run` command only; empty otherwise).'
value: ${{ steps.run.outputs.pr_url }}
commit_url:
description: 'URL of the commit cruise pushed directly to the default branch (the `exec` command only; empty otherwise).'
value: ${{ steps.run.outputs.commit_url }}
plan_comment_url:
description: 'URL of the plan-tracking comment cruise posted or edited (the `plan`/`fix` commands only; empty otherwise).'
value: ${{ steps.run.outputs.plan_comment_url }}
conclusion:
description: '"success", "failure", or "skipped" (mention did not match / actor not authorized).'
value: ${{ steps.finalize.outputs.conclusion }}
used_app:
description: >-
"true" if the run authenticated with a cruise-agent GitHub App
installation token, "false" if it used `github_token`/`GITHUB_TOKEN`,
or an empty string if the gate step skipped the run before the `token`
step ran (mention didn't match / actor not authorized).
value: ${{ steps.token.outputs.used_app }}
runs:
using: "composite"
steps:
- name: Gate (trigger + permission + command parsing)
id: gate
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token || github.token }}
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
ANTHROPIC_API_KEY_INPUT: ${{ inputs.anthropic_api_key }}
OPENAI_API_KEY_INPUT: ${{ inputs.openai_api_key }}
ENV_INPUT: ${{ inputs.env }}
PROVIDER_API_KEYS_INPUT: ${{ inputs.provider_api_keys }}
PROVIDERS_INPUT: ${{ inputs.providers }}
PI_MODELS_JSON: ${{ inputs.pi_models_json }}
run: ${{ github.action_path }}/action/scripts/gate.sh
- name: Resolve GitHub token (cruise-agent App or fallback)
id: token
if: steps.gate.outputs.proceed == 'true'
shell: bash
env:
GH_TOKEN_INPUT: ${{ inputs.github_token }}
TOKEN_EXCHANGE_URL: ${{ inputs.token_exchange_url }}
WORKFLOW_TOKEN: ${{ github.token }}
run: ${{ github.action_path }}/action/scripts/app-token.sh
- name: Set up cruise/pi environment
id: setup_env
if: steps.gate.outputs.proceed == 'true'
shell: bash
env:
ANTHROPIC_API_KEY_INPUT: ${{ inputs.anthropic_api_key }}
OPENAI_API_KEY_INPUT: ${{ inputs.openai_api_key }}
MODEL_INPUT: ${{ inputs.model }}
PLAN_MODEL_INPUT: ${{ inputs.plan_model }}
PI_MODELS_JSON: ${{ inputs.pi_models_json }}
ENV_INPUT: ${{ inputs.env }}
PROVIDERS_INPUT: ${{ inputs.providers }}
PROVIDER_API_KEYS_INPUT: ${{ inputs.provider_api_keys }}
run: ${{ github.action_path }}/action/scripts/setup-env.sh
- name: Record start time
id: timer
if: steps.gate.outputs.proceed == 'true'
shell: bash
run: echo "start_ts=$(date +%s)" >> "$GITHUB_OUTPUT"
- name: Post tracking comment
id: comment_start
if: steps.gate.outputs.proceed == 'true'
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
ENTITY_NUMBER: ${{ steps.gate.outputs.entity_number }}
COMMAND: ${{ steps.gate.outputs.command }}
TRIGGER_ACTOR: ${{ steps.gate.outputs.actor }}
run: ${{ github.action_path }}/action/scripts/comment-start.sh
- name: Install cruise
id: install
if: steps.gate.outputs.proceed == 'true'
shell: bash
env:
CRUISE_VERSION: ${{ inputs.cruise_version }}
run: ${{ github.action_path }}/action/scripts/install.sh
- name: Resolve cruise config
id: config
if: steps.gate.outputs.proceed == 'true'
shell: bash
env:
CONFIG_INPUT: ${{ inputs.config }}
run: ${{ github.action_path }}/action/scripts/resolve-config.sh
- name: Run cruise command
id: run
if: steps.gate.outputs.proceed == 'true'
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
COMMAND: ${{ steps.gate.outputs.command }}
ENTITY_NUMBER: ${{ steps.gate.outputs.entity_number }}
TRIGGER_ACTOR: ${{ steps.gate.outputs.actor }}
TRIGGER_ACTOR_ID: ${{ steps.gate.outputs.actor_id }}
COMMAND_REST_FILE: ${{ steps.gate.outputs.command_rest_file }}
EXEC_CONFIG_PATH: ${{ steps.config.outputs.exec_config_path }}
GIT_USER_NAME_INPUT: ${{ inputs.git_user_name }}
GIT_USER_EMAIL_INPUT: ${{ inputs.git_user_email }}
USED_APP: ${{ steps.token.outputs.used_app }}
run: ${{ github.action_path }}/action/scripts/run.sh
- name: Finalize tracking comment
id: finalize
if: always()
shell: bash
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
PROCEED: ${{ steps.gate.outputs.proceed }}
GATE_ERROR: ${{ steps.gate.outputs.gate_error }}
COMMAND: ${{ steps.gate.outputs.command }}
ENTITY_NUMBER: ${{ steps.gate.outputs.entity_number }}
COMMENT_ID: ${{ steps.comment_start.outputs.comment_id }}
RUN_OUTCOME: ${{ steps.run.outcome }}
START_TS: ${{ steps.timer.outputs.start_ts }}
SESSION_ID: ${{ steps.run.outputs.session_id }}
PR_URL: ${{ steps.run.outputs.pr_url }}
COMMIT_URL: ${{ steps.run.outputs.commit_url }}
PLAN_COMMENT_URL: ${{ steps.run.outputs.plan_comment_url }}
FAIL_REASON: ${{ steps.run.outputs.fail_reason }}
run: ${{ github.action_path }}/action/scripts/finalize.sh
- name: Revoke App installation token
id: revoke
if: always() && steps.token.outputs.used_app == 'true'
continue-on-error: true
shell: bash
env:
TOKEN: ${{ steps.token.outputs.token }}
run: ${{ github.action_path }}/action/scripts/revoke-token.sh
- name: Fail the job if cruise run failed
if: steps.run.outcome == 'failure'
shell: bash
run: |
echo "::error::cruise run failed - see the tracking comment and logs above for details." >&2
exit 1