cruise 0.1.86

YAML-driven coding agent workflow orchestrator
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:
        # This runs before the `token` step resolves an App/exchange token,
        # so it can only ever see the raw input or the workflow's own token.
        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'
      # A failed comment post must not skip the actual cruise run; finalize.sh
      # already tolerates an empty COMMENT_ID.
      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