Skip to content

chore: use the public name Codewhale in prose (identifiers untouched) #179

chore: use the public name Codewhale in prose (identifiers untouched)

chore: use the public name Codewhale in prose (identifiers untouched) #179

name: Codewhale PR Review
# Advisory AI code review by Codewhale itself (`codewhale review --pr --post`)
# on every non-draft PR: one COMMENT review with a summary body plus inline
# line comments, anchored to the PR head SHA. CODEOWNERS (@Hmbown) stays the
# human owner — this review posts alongside it and never approves.
#
# Setup — full step-by-step guide in docs/GITHUB_APP.md. Summary:
# 1. Key: Settings -> Secrets and variables -> Actions -> New repository
# secret `CODEWHALE_API_KEY`. This is the canonical name: it is your
# Codewhale account's review key, not any one vendor's. The workflow maps
# it into whatever env var the configured provider expects.
# BYOK alternative: set the provider's own key instead
# (`ZAI_API_KEY`, `DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`,
# `ANTHROPIC_API_KEY`); any one of them is enough.
# 2. Which agent runs the review: repository variables
# `CODEWHALE_REVIEW_PROVIDER` (e.g. `zai`) and `CODEWHALE_REVIEW_MODEL`
# (e.g. `GLM-5.3`). Both optional — see "Route selection" below.
# 3. Optional identity: to post as the Codewhale Agent GitHub App instead
# of the workflow's github-token identity, set repository variable
# `CODEWHALE_APP_ID` and secret `CODEWHALE_APP_PRIVATE_KEY`; the job
# mints an installation token via actions/create-github-app-token.
# 4. Optional output budget: repository variable
# `CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS` — see "Output budget" below.
# Until at least one accepted key exists the job no-ops with a notice (stays
# green), so this workflow is safe to merge before it is configured.
#
# Actions-syntax note (why the `env:` hoist below exists):
# the `secrets` context is NOT available in a job-level `if:`. It IS
# available in a job-level `env:`, and step-level `if:` can read the `env`
# context. So the key-presence test is evaluated once into
# `env.HAS_ANY_KEY` at job scope and every step gates on that string.
# Only non-secret booleans live at job scope; the key values themselves are
# injected into the single step that needs them.
#
# Route selection:
# `CODEWHALE_REVIEW_PROVIDER` is passed straight through as
# `codewhale review --provider <name>`, which pins the route. Without it a
# model offered by more than one configured route hard-errors
# ("available from configured provider route(s): openrouter, zai"). When
# the variable is unset the provider is inferred from which key is present.
#
# Output budget:
# GLM-5.3 is a reasoning model: it emits `reasoning_content` before
# `content`, and both are charged against `max_tokens`. A small cap
# therefore yields an EMPTY review rather than an error. The CLI's
# automatic cap (64K) is already generous, so this workflow sets no cap by
# default; `CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS` can override it but is
# rejected below a floor that leaves no room for the answer. The run step
# also fails loudly on a zero-length review instead of reporting success.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [master, main]
concurrency:
group: codewhale-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
codewhale-review:
name: Codewhale review
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
env:
# `secrets` is unavailable in a job-level `if:` but allowed here; these
# are booleans about presence, never key material.
HAS_ANY_KEY: ${{ secrets.CODEWHALE_API_KEY != '' || secrets.ZAI_API_KEY != '' || secrets.DEEPSEEK_API_KEY != '' || secrets.OPENROUTER_API_KEY != '' || secrets.ANTHROPIC_API_KEY != '' }}
HAS_APP_KEY: ${{ secrets.CODEWHALE_APP_PRIVATE_KEY != '' }}
permissions:
contents: read
pull-requests: write
steps:
- name: Skip when no review key is configured
if: env.HAS_ANY_KEY != 'true'
run: |
echo "::notice::No Codewhale review key is set — skipping. Add repository secret CODEWHALE_API_KEY (or a provider key: ZAI_API_KEY / DEEPSEEK_API_KEY / OPENROUTER_API_KEY / ANTHROPIC_API_KEY) to enable it."
- name: Checkout repository
if: env.HAS_ANY_KEY == 'true'
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Mint Codewhale Agent app token
if: env.HAS_ANY_KEY == 'true' && env.HAS_APP_KEY == 'true' && vars.CODEWHALE_APP_ID != ''
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.CODEWHALE_APP_ID }}
private-key: ${{ secrets.CODEWHALE_APP_PRIVATE_KEY }}
- name: Install Rust toolchain
if: env.HAS_ANY_KEY == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Install native build deps
if: env.HAS_ANY_KEY == 'true'
run: |
for i in 1 2 3; do
sudo apt-get update && break
echo "apt-get update failed (attempt $i); retrying in 15s"
sleep 15
done
sudo apt-get install -y libdbus-1-dev pkg-config
- name: Cache cargo build
if: env.HAS_ANY_KEY == 'true'
uses: Swatinem/rust-cache@v2
- name: Build codewhale
if: env.HAS_ANY_KEY == 'true'
run: cargo build --release -p codewhale-cli
- name: Run Codewhale PR review
if: env.HAS_ANY_KEY == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
# Canonical Codewhale account key. Mapped below into whichever
# provider env var the configured route expects.
CODEWHALE_API_KEY: ${{ secrets.CODEWHALE_API_KEY }}
# BYOK fallbacks: a provider key used directly, no mapping needed.
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CODEWHALE_REVIEW_PROVIDER: ${{ vars.CODEWHALE_REVIEW_PROVIDER }}
CODEWHALE_REVIEW_MODEL: ${{ vars.CODEWHALE_REVIEW_MODEL }}
CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS: ${{ vars.CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -uo pipefail
# --- Which agent reviews this PR -------------------------------
# Explicit repository variable wins. Otherwise: the canonical
# Codewhale key defaults to the z.ai Coding Plan route (the route
# verified end-to-end), and a BYOK-only repo gets the provider whose
# key it actually set.
PROVIDER="${CODEWHALE_REVIEW_PROVIDER:-}"
if [ -z "$PROVIDER" ]; then
if [ -n "${CODEWHALE_API_KEY:-}" ]; then PROVIDER=zai
elif [ -n "${ZAI_API_KEY:-}" ]; then PROVIDER=zai
elif [ -n "${DEEPSEEK_API_KEY:-}" ]; then PROVIDER=deepseek
elif [ -n "${OPENROUTER_API_KEY:-}" ]; then PROVIDER=openrouter
elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then PROVIDER=anthropic
fi
fi
if [ -z "$PROVIDER" ]; then
echo "::error::No review key resolved to a provider. This should be unreachable (HAS_ANY_KEY was true)."
exit 1
fi
# --- Map the canonical key onto that provider's env var ---------
# Names only are ever printed; values never are.
KEY_VAR=""
case "$PROVIDER" in
zai|z-ai|zhipu|glm) KEY_VAR=ZAI_API_KEY ;;
deepseek|deepseek-cn) KEY_VAR=DEEPSEEK_API_KEY ;;
openrouter) KEY_VAR=OPENROUTER_API_KEY ;;
anthropic|claude) KEY_VAR=ANTHROPIC_API_KEY ;;
*)
# No mapping for a custom provider. With an account key set that
# is a real misconfiguration (hard error below). BYOK-only repos
# are fine — the provider's own secret is used directly — so say
# so without a red ::error:: annotation on a correct config.
if [ -z "${CODEWHALE_API_KEY:-}" ]; then
echo "::warning::CODEWHALE_REVIEW_PROVIDER='${PROVIDER}' has no CODEWHALE_API_KEY mapping in this workflow, and none is needed: no account key is set, so the provider's own BYOK secret is used directly."
else
echo "::error::CODEWHALE_REVIEW_PROVIDER='${PROVIDER}' has no CODEWHALE_API_KEY mapping in this workflow. Set that provider's own key as a repository secret, or add the mapping here."
fi
KEY_VAR=""
;;
esac
if [ -n "${CODEWHALE_API_KEY:-}" ]; then
if [ -z "$KEY_VAR" ]; then
exit 1
fi
# The canonical account key is authoritative for the chosen route.
export "$KEY_VAR=$CODEWHALE_API_KEY"
echo "Review key: CODEWHALE_API_KEY -> ${KEY_VAR} (provider: ${PROVIDER})"
else
echo "Review key: BYOK provider secret (provider: ${PROVIDER})"
fi
# --- Output budget ---------------------------------------------
# GLM-5.3 spends max_tokens on reasoning_content before it emits any
# content, so an undersized cap returns an empty review, not an
# error. Unset means "use the CLI's automatic 64K cap".
BUDGET="${CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS:-}"
if [ -n "$BUDGET" ]; then
case "$BUDGET" in
''|*[!0-9]*)
echo "::error::CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS must be a positive integer (got '${BUDGET}')."
exit 1 ;;
esac
if [ "$BUDGET" -lt 8192 ]; then
echo "::error::CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS=${BUDGET} is below the 8192 floor. A reasoning model (GLM-5.3) would spend the whole budget on reasoning_content and return an empty review."
exit 1
fi
export CODEWHALE_MAX_OUTPUT_TOKENS="$BUDGET"
echo "Output budget: CODEWHALE_MAX_OUTPUT_TOKENS=${BUDGET}"
else
echo "Output budget: CLI automatic cap (no override set)"
fi
# --- Run --------------------------------------------------------
REVIEW_ARGS=(--pr "$PR_NUMBER" --post --provider "$PROVIDER")
if [ -n "${CODEWHALE_REVIEW_MODEL:-}" ]; then
REVIEW_ARGS+=(--model "$CODEWHALE_REVIEW_MODEL")
fi
set +e
OUTPUT=$(./target/release/codewhale review "${REVIEW_ARGS[@]}" 2>&1)
STATUS=$?
set -e
echo "$OUTPUT"
if [ "$STATUS" -eq 0 ]; then
# A reasoning model that spent its whole budget before emitting
# content exits 0 with nothing to say. That is a failure, not a
# clean review — never report it as one.
if [ -z "$(printf '%s' "$OUTPUT" | tr -d '[:space:]')" ]; then
echo "::error::Codewhale review produced empty output with exit 0. If the model is a reasoning model, raise CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS."
exit 1
fi
exit 0
fi
# The review is advisory: a provider-side outage (balance, auth,
# rate limit, upstream 5xx) must not block the PR. Real review
# failures still fail the job with the original exit status.
if echo "$OUTPUT" | grep -qE 'LLM error: HTTP (401|402|403|408|429|5[0-9][0-9])'; then
echo "::warning::Codewhale review could not run (provider error; see log above). The PR is not blocked — provider funding/config is founder-gated."
exit 0
fi
exit "$STATUS"