Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6196c3e
ci(review): give the Cora Review job 30 minutes (#677)
ajianaz Oct 16, 2026
00094a0
feat(hook): add the commit gate core, verify and Claude Code guard (#…
ajianaz Oct 16, 2026
6bda34e
feat(hook): pre-commit hook v2 accepts a validated diff and upgrades …
ajianaz Oct 16, 2026
3060d40
docs(hook): document the commit gate, skill and changelog (#689)
ajianaz Oct 16, 2026
92efe66
ci(hook): adopt the commit gate in this repo and fail on empty review…
ajianaz Oct 16, 2026
10532c8
fix(config): validate custom rules, accept language names, add a dry …
ajianaz Oct 16, 2026
4df0e4e
fix(ignore): warn about cora-ignore markers that match nothing and ad…
ajianaz Oct 16, 2026
6dab195
chore: remove CLA gate (fleet CLA retirement, uteke#1548) (#694)
ajianaz Oct 22, 2026
960db05
chore(deps): fix dependency-audit findings (#695) (#696)
ajianaz Oct 22, 2026
5bad288
fix(mcp): report how much of the project a scan_finalize verdict cove…
ajianaz Oct 27, 2026
b65ada2
fix(rules): flag only deliberate TODO markers, skip eprintln and inva…
ajianaz Oct 27, 2026
9e1a1f8
ci(review): use Anthropic workload identity federation, fall back to …
ajianaz Oct 27, 2026
85c5b90
ci: add an Anthropic workload identity federation smoke test (#699)
ajianaz Oct 27, 2026
ca4e189
fix(llm): retry without temperature when the model rejects it (#702)
ajianaz Oct 27, 2026
8bcddb8
fix(rules): stop flagging Token::default() and show only reportable f…
ajianaz Oct 27, 2026
6d1b215
fix(cli): hide CORA_BASE_URL, CORA_MODEL and CORA_PROVIDER values in …
ajianaz Oct 27, 2026
5d5fbab
feat(scan): opt-in clone hints, --min-severity and a per-severity sum…
ajianaz Oct 27, 2026
f1dbd30
feat(ignore): path-scoped ignore.rules entries (#706)
ajianaz Oct 27, 2026
670b8e8
fix(rules): treat every module of a binary-only package as program co…
ajianaz Oct 27, 2026
15c6180
feat(mcp): scan_prepare selection inputs, source-first ordering, plan…
ajianaz Oct 27, 2026
36a8dba
chore(release): v0.20.0
ajianaz Oct 27, 2026
34ff4a8
chore(release): add the #686 scan_prepare entry to the v0.20.0 notes
ajianaz Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/anthropic-wif-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Anthropic WIF smoke test

# Checks that this repository can exchange its GitHub OIDC token for a
# short-lived Anthropic access token (workload identity federation) and that
# the token works the way cora sends it: `Authorization: Bearer <token>` to the
# OpenAI-compatible `/chat/completions` endpoint. Prints HTTP statuses only,
# never a token.
#
# Manual only (Actions tab -> Run workflow), so it costs nothing on normal PRs.
# The federation rule must allow this workflow's OIDC subject (for a manual run
# on a branch that is `...:ref:refs/heads/<branch>`, not `...:pull_request`).

on:
workflow_dispatch:

permissions:
id-token: write
contents: read

jobs:
smoke:
name: Anthropic WIF smoke
runs-on: ubuntu-latest
timeout-minutes: 5
env:
# Identifiers, not credentials, but this repository is public: they live
# in organization/repository secrets or variables, not in this file.
FEDERATION_RULE_ID: ${{ secrets.ANTHROPIC_FEDERATION_RULE_ID || vars.ANTHROPIC_FEDERATION_RULE_ID }}
ORGANIZATION_ID: ${{ secrets.ANTHROPIC_ORGANIZATION_ID || vars.ANTHROPIC_ORGANIZATION_ID }}
SERVICE_ACCOUNT_ID: ${{ secrets.ANTHROPIC_SERVICE_ACCOUNT_ID || vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
WORKSPACE_ID: ${{ secrets.ANTHROPIC_WORKSPACE_ID || vars.ANTHROPIC_WORKSPACE_ID }}
MODEL: ${{ secrets.ANTHROPIC_MODEL || vars.ANTHROPIC_MODEL || 'claude-opus-5-5' }}
steps:
- name: Require the federation variables
run: |
missing=0
for name in FEDERATION_RULE_ID ORGANIZATION_ID SERVICE_ACCOUNT_ID WORKSPACE_ID; do
if [ -z "${!name}" ]; then
echo "::error::Secret or variable ANTHROPIC_${name} is empty or missing."
missing=1
fi
done
exit "$missing"

- name: Exchange the GitHub OIDC token for an Anthropic access token
run: |
# Same step as in cora-review.yml: the OIDC token stays in this shell.
jwt=$(curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" | jq -r '.value // empty')
if [ -z "$jwt" ]; then
echo "::error::Could not get a GitHub OIDC token (is id-token: write set?)."
exit 1
fi
echo "::add-mask::$jwt"
# Claims are identifiers (subject, repository, event), not secrets.
payload=$(printf '%s' "$jwt" | cut -d. -f2 | tr '_-' '/+')
while [ $(( ${#payload} % 4 )) -ne 0 ]; do payload="${payload}="; done
printf '%s' "$payload" | base64 -d | jq '{sub, repository, event_name}'
body=$(jq -n \
--arg jwt "$jwt" \
--arg rule "$FEDERATION_RULE_ID" \
--arg org "$ORGANIZATION_ID" \
--arg sa "$SERVICE_ACCOUNT_ID" \
--arg ws "$WORKSPACE_ID" \
'{grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: $jwt, federation_rule_id: $rule, organization_id: $org,
service_account_id: $sa, workspace_id: $ws}')
code=$(curl -sS -o exchange.json -w '%{http_code}' \
https://api.anthropic.com/v1/oauth/token \
-H "content-type: application/json" --data "$body")
echo "token exchange: HTTP $code"
if [ "$code" != "200" ]; then
jq -r '.error | "\(.type): \(.message)"' exchange.json || true
exit 1
fi
token=$(jq -r '.access_token' exchange.json)
rm -f exchange.json
echo "::add-mask::$token"
echo "ACCESS_TOKEN=$token" >> "$GITHUB_ENV"

- name: Call the OpenAI-compatible endpoint the way cora does
run: |
code=$(curl -sS -o compat.json -w '%{http_code}' \
https://api.anthropic.com/v1/chat/completions \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
--data "$(jq -n --arg m "$MODEL" '{model: $m, max_tokens: 16, messages: [{role: "user", content: "Reply with the single word ok."}]}')")
echo "chat/completions with Bearer: HTTP $code"
if [ "$code" = "200" ]; then
echo "reply length: $(jq -r '.choices[0].message.content | length' compat.json)"
else
jq -r '.error | "\(.type): \(.message)"' compat.json || true
exit 1
fi
104 changes: 0 additions & 104 deletions .github/workflows/cla-check.yml

This file was deleted.

85 changes: 73 additions & 12 deletions .github/workflows/cora-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,84 @@ permissions:
contents: read
pull-requests: write
security-events: write
id-token: write # workload identity federation with Anthropic (no stored API key)

jobs:
cora-review:
name: Cora Review
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 30
env:
# Anthropic workload identity federation (organization secrets or variables).
# When all four are set, the job exchanges its GitHub OIDC token for a
# short-lived Anthropic access token instead of using CORA_API_KEY.
FEDERATION_RULE_ID: ${{ secrets.ANTHROPIC_FEDERATION_RULE_ID || vars.ANTHROPIC_FEDERATION_RULE_ID }}
ORGANIZATION_ID: ${{ secrets.ANTHROPIC_ORGANIZATION_ID || vars.ANTHROPIC_ORGANIZATION_ID }}
SERVICE_ACCOUNT_ID: ${{ secrets.ANTHROPIC_SERVICE_ACCOUNT_ID || vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
WORKSPACE_ID: ${{ secrets.ANTHROPIC_WORKSPACE_ID || vars.ANTHROPIC_WORKSPACE_ID }}
steps:
# SECURITY: this job checks out untrusted PR code while holding
# CORA_API_KEY (pull_request_target). A PR-supplied .cora.yaml could set
# provider.base_url and exfiltrate the key. CORA_BASE_URL (env) always
# takes precedence over project config, so require it to be pinned.
- name: Require pinned CORA_BASE_URL
- name: Exchange the GitHub OIDC token for an Anthropic access token
id: wif
if: env.FEDERATION_RULE_ID != '' && env.ORGANIZATION_ID != '' && env.SERVICE_ACCOUNT_ID != '' && env.WORKSPACE_ID != ''
continue-on-error: true
run: |
# The OIDC token stays in this step's shell: it is never exported to the
# job environment, so later steps (checkout of the PR, the review action)
# cannot see it. Only the short-lived Anthropic access token is exported.
jwt=$(curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" | jq -r '.value // empty')
if [ -z "$jwt" ]; then
echo "::warning::Could not get a GitHub OIDC token; falling back to the CORA_* secrets."
exit 1
fi
echo "::add-mask::$jwt"
body=$(jq -n \
--arg jwt "$jwt" \
--arg rule "$FEDERATION_RULE_ID" \
--arg org "$ORGANIZATION_ID" \
--arg sa "$SERVICE_ACCOUNT_ID" \
--arg ws "$WORKSPACE_ID" \
'{grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: $jwt, federation_rule_id: $rule, organization_id: $org,
service_account_id: $sa, workspace_id: $ws}')
code=$(curl -sS -o exchange.json -w '%{http_code}' \
https://api.anthropic.com/v1/oauth/token \
-H "content-type: application/json" --data "$body")
if [ "$code" != "200" ]; then
echo "::warning::Anthropic token exchange failed (HTTP $code: $(jq -r '.error.type // "unknown"' exchange.json 2>/dev/null)); falling back to the CORA_* secrets."
rm -f exchange.json
exit 1
fi
token=$(jq -r '.access_token' exchange.json)
rm -f exchange.json
echo "::add-mask::$token"
echo "ACCESS_TOKEN=$token" >> "$GITHUB_ENV"
echo "Using a short-lived Anthropic access token (workload identity federation)."

# SECURITY: this job checks out untrusted PR code while holding a credential
# (pull_request_target). A PR-supplied .cora.yaml could set provider.base_url
# and exfiltrate it. CORA_BASE_URL (env) always takes precedence over
# project config, so the base URL is pinned here, and the Anthropic token
# is only ever paired with api.anthropic.com (never with CORA_BASE_URL).
- name: Require the review secrets
env:
CORA_API_KEY: ${{ secrets.CORA_API_KEY }}
CORA_BASE_URL: ${{ secrets.CORA_BASE_URL }}
CORA_MODEL: ${{ secrets.CORA_MODEL }}
run: |
if [ -z "$CORA_BASE_URL" ]; then
echo "::error::Secret CORA_BASE_URL must be set so a PR-supplied .cora.yaml cannot redirect the API key."
exit 1
# No federation token: the stored secrets are required. An empty secret
# must fail the job: a skipped or fallback review is not a pass.
if [ -n "${ACCESS_TOKEN:-}" ]; then
exit 0
fi
missing=0
for name in CORA_API_KEY CORA_BASE_URL CORA_MODEL; do
if [ -z "${!name}" ]; then
echo "::error::Secret $name is empty or missing, and workload identity federation is not available. Cora Review cannot run, and that must not pass silently."
missing=1
fi
done
exit "$missing"

- name: Checkout PR head
uses: actions/checkout@v4
Expand All @@ -44,6 +103,8 @@ jobs:
uses: codecoradev/cora-review-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
cora-api-key: ${{ secrets.CORA_API_KEY }}
cora-base-url: ${{ secrets.CORA_BASE_URL }}
cora-model: ${{ secrets.CORA_MODEL }}
# With a federation token, key, base URL and model switch together so the
# token is only ever sent to api.anthropic.com.
cora-api-key: ${{ env.ACCESS_TOKEN || secrets.CORA_API_KEY }}
cora-base-url: ${{ env.ACCESS_TOKEN && 'https://api.anthropic.com/v1' || secrets.CORA_BASE_URL }}
cora-model: ${{ env.ACCESS_TOKEN && (secrets.ANTHROPIC_MODEL || vars.ANTHROPIC_MODEL || 'claude-opus-5-5') || secrets.CORA_MODEL }}
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ code structure are in `AGENT.md`.
- CI runs the same gates: `cargo fmt --all -- --check` and
`cargo clippy --all-targets --features tree-sitter -- -D warnings`.
- User-visible changes go under `## [Unreleased]` in `CHANGELOG.md`.
- Before committing: review the staged diff (`cora review --staged`, or the
`/cora-review` skill) and run `scripts/setup-hooks.sh` once per clone so the
pre-commit hook enforces the same gates. Never `--no-verify` past a finding.

## Conventions enforced by CI

- Branch names start with one of `feat/ fix/ docs/ chore/ perf/ security/
refactor/ test/ build/ ci/ release/`.
- PR bodies contain `## What`, `## Why` and `## Testing`.
- Conventional Commits; the PR title becomes the squash message. Commits are
signed off (`git commit -s`) for the CLA.
- Conventional Commits; the PR title becomes the squash message.

## Gotchas

Expand Down
Loading
Loading