Files
DeepSeek-TUI/.github/workflows/codewhale-review.yml
Hunter Bown cec8f8bf17 ci(review): make a Codewhale-review non-run visible on the PR; add Model Studio to the key ladder (#5740)
* ci(review): make a non-run visible on the PR and let Model Studio carry the review

The 'Codewhale review' check was green on every PR today while zero reviews posted: the DeepSeek review key returned HTTP 402 Insufficient Balance and the workflow downgraded provider errors to a warning with exit 0. Now a provider-side non-run leaves one idempotent PR note (status line only, never model output) and deletes it once a real review lands, and MODELSTUDIO_API_KEY joins the key ladder ahead of DeepSeek so the review can run on the founder's Alibaba Model Studio credit (DeepSeek V4 Pro / Qwen 3.8). Still advisory; still never blocks a PR.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* ci(review): keep the non-run note inside the YAML block scalar

The BODY heredoc-style continuation lines at column 0 terminated the
run: block, so the workflow failed actionlint syntax-check (line 249).
Build the body with a single printf instead.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* ci(review): silence SC2016 on the literal-backtick printf format

actionlint's shellcheck pass flagged codewhale-review.yml:135:9 with
SC2016 at script line 113. The backticks in that printf format string are
literal Markdown for the PR comment body, not command substitution, so the
format string must stay single-quoted and the finding is a false positive.

Suppress it at the one line rather than adding -ignore SC2016 to the
workflow-lint args, which would blind every other workflow to real
unexpanded-expression bugs.

Verified locally:
  actionlint -ignore SC2129 -ignore SC2221 -ignore SC2222 \
    .github/workflows/codewhale-review.yml
fails on the parent commit and exits 0 here.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* ci(review): let the non-run note actually post

Three findings from the review of this PR.

1. issues: write.

The new lifecycle calls `gh api -X PATCH/DELETE repos/.../issues/comments/{id}`
-- the issue-comment endpoint -- and every one of them is `|| true` or
`|| echo ::warning::`, deliberately, so a comment problem never fails the job.
That makes a missing permission silent, which is precisely the failure this PR
exists to close: a non-run passing for a clean review. The permission is one
line and matches every other workflow in .github/workflows that touches
comments.

2. The skip notice omitted MODELSTUDIO_API_KEY.

HAS_ANY_KEY already counts it, so a Model-Studio-only repo would see the
workflow run while the skip notice claimed no key could enable it.

3. REASON could quote a status the branch did not match on.

The guard matches 401|402|403|408|429|5xx but REASON re-scanned for any
`HTTP [0-9]{3}` and took the first hit, so output containing an earlier
unrelated status would put that code in the PR comment. REASON now reuses the
guard's own class.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

---------

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
2026-08-31 23:34:34 -07:00

266 lines
14 KiB
YAML

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"