Skip to content

web(i18n): one-way ceiling on isZh branching (#5519) #245

web(i18n): one-way ceiling on isZh branching (#5519)

web(i18n): one-way ceiling on isZh branching (#5519) #245

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.MODELSTUDIO_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
# `gh api .../issues/comments/{id}` PATCH/DELETE below is the issue-comment
# endpoint. Every call is `|| true` or `|| echo ::warning::`, so a missing
# permission would fail silently — the exact "non-run passes for a clean
# review" failure this workflow exists to close.
issues: 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 / MODELSTUDIO_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 }}
# Alibaba Model Studio Token Plan (DeepSeek V4 Pro / Qwen 3.8 on the
# founder's credit); all Model Studio kinds read MODELSTUDIO_API_KEY.
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_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 "${MODELSTUDIO_API_KEY:-}" ]; then PROVIDER=modelstudio-token-plan
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
MARK="<!-- codewhale-review-nonrun -->"
STALE=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate --jq ".[] | select(.body | startswith(\"${MARK}\")) | .id" 2>/dev/null | head -1 || true)
if [ -n "$STALE" ]; then
gh api -X DELETE "repos/${GITHUB_REPOSITORY}/issues/comments/${STALE}" >/dev/null 2>&1 || true
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
REASON=$(echo "$OUTPUT" | grep -oE 'LLM error: HTTP (401|402|403|408|429|5[0-9][0-9])[^"]{0,80}' | head -1)
echo "::warning::Codewhale review could not run (${REASON}). The PR is not blocked — provider funding/config is founder-gated."
# Silence is not success: leave one visible, idempotent note on the
# PR so a non-run never passes for a clean review. Only the HTTP
# status line is quoted, never the model output.
MARK="<!-- codewhale-review-nonrun -->"
# Single printf: column-0 continuation lines would terminate the
# YAML block scalar (actionlint syntax-check failure at :249).
# The backticks below are literal Markdown for the PR comment, not
# command substitution; the format string must stay single-quoted.
# shellcheck disable=SC2016
BODY=$(printf '%s\n\n## Codewhale review did not run\n\n`codewhale review --pr %s` (provider: `%s`) could not reach the model: `%s`.\n%s' "$MARK" "$PR_NUMBER" "$PROVIDER" "$REASON" "This is a provider funding/config problem, not a finding about this PR. The check stays advisory; a maintainer with secret access needs to fund or rotate the review key (see \`.github/workflows/codewhale-review.yml\`). Re-run the workflow after that.")
EXISTING=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate --jq ".[] | select(.body | startswith(\"${MARK}\")) | .id" 2>/dev/null | head -1 || true)
if [ -n "$EXISTING" ]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING}" -f body="$BODY" >/dev/null 2>&1 || echo "::warning::could not update the non-run note"
else
gh pr comment "$PR_NUMBER" --body "$BODY" >/dev/null 2>&1 || echo "::warning::could not post the non-run note"
fi
exit 0
fi
exit "$STATUS"