diff --git a/.github/actions/cora-review-simple/action.yml b/.github/actions/cora-review-simple/action.yml new file mode 100644 index 0000000..ba7808a --- /dev/null +++ b/.github/actions/cora-review-simple/action.yml @@ -0,0 +1,195 @@ +name: 'Cora AI Code Review' +description: 'Run cora AI code review on a PR diff โ€” posts a PR comment. Uses GitHub Secrets directly (no Infisical required). SARIF upload is optional.' + +inputs: + base-branch: + description: 'Base branch to compare against (default: origin/develop)' + required: false + default: 'origin/develop' + severity: + description: 'Minimum severity to report (info, minor, major, critical)' + required: false + default: 'major' + cora-version: + description: 'cora-cli version tag (default: latest release)' + required: false + default: 'latest' + upload-sarif: + description: 'Upload SARIF to GitHub Code Scanning (default: false)' + required: false + default: 'false' + cora-api-key: + description: 'LLM API key (from secrets.CORA_API_KEY)' + required: true + cora-base-url: + description: 'LLM API base URL (from secrets.CORA_BASE_URL)' + required: true + cora-model: + description: 'LLM model ID (from secrets.CORA_MODEL)' + required: true + github-token: + description: 'GitHub token for PR comments and SARIF upload' + required: true + +runs: + using: 'composite' + steps: + - name: Resolve cora-cli version + id: resolve + shell: bash + run: | + if [ "${{ inputs.cora-version }}" = "latest" ]; then + VERSION=$(curl -sL https://api.github.com/repos/ajianaz/cora-cli/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])") + else + VERSION="${{ inputs.cora-version }}" + fi + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "Resolved cora-cli version: $VERSION" + + - name: Install cora-cli + shell: bash + run: | + ARCH=$(uname -m) + if [ "$ARCH" = "x86_64" ]; then + ASSET="cora-x86_64-unknown-linux-gnu" + elif [ "$ARCH" = "aarch64" ]; then + ASSET="cora-aarch64-unknown-linux-gnu" + else + echo "Unsupported architecture: $ARCH" + exit 1 + fi + curl -sL "https://github.com/ajianaz/cora-cli/releases/download/${{ steps.resolve.outputs.VERSION }}/${ASSET}-${{ steps.resolve.outputs.VERSION }}.tar.gz" | tar xz -C /usr/local/bin cora + chmod +x /usr/local/bin/cora + cora --version + + - name: Run cora review + id: review + shell: bash + env: + CORA_API_KEY: ${{ inputs.cora-api-key }} + CORA_BASE_URL: ${{ inputs.cora-base-url }} + CORA_MODEL: ${{ inputs.cora-model }} + run: | + cora review \ + --base ${{ inputs.base-branch }} \ + --format sarif \ + --severity ${{ inputs.severity }} \ + --quiet \ + > cora-results.sarif 2>/dev/null + echo "Cora review complete ($(wc -c < cora-results.sarif) bytes)" + + - name: Upload SARIF to GitHub Code Scanning + if: inputs.upload-sarif == 'true' + continue-on-error: true + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: cora-results.sarif + category: cora-review + + - name: Post PR comment + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + env: + GH_TOKEN: ${{ inputs.github-token }} + with: + script: | + const fs = require('fs'); + + let sarifContent; + try { + sarifContent = JSON.parse(fs.readFileSync('cora-results.sarif', 'utf8')); + } catch (e) { + sarifContent = null; + } + + let body; + if (!sarifContent || !sarifContent.runs || sarifContent.runs.length === 0) { + body = `## ๐Ÿ” Cora AI Code Review\n\nโœ… **No issues found.** Code looks good!`; + } else { + const results = sarifContent.runs[0].results || []; + if (results.length === 0) { + body = `## ๐Ÿ” Cora AI Code Review\n\nโœ… **No issues found.** Code looks good!`; + } else { + const grouped = {}; + for (const r of results) { + const sev = (r.level || 'note').charAt(0).toUpperCase() + (r.level || 'note').slice(1); + if (!grouped[sev]) grouped[sev] = []; + grouped[sev].push(r); + } + + const severityOrder = ['Error', 'Warning', 'Note']; + let table = ''; + + for (const sev of severityOrder) { + if (!grouped[sev]) continue; + const icon = sev === 'Error' ? '๐Ÿ”ด' : sev === 'Warning' ? '๐ŸŸก' : '๐Ÿ”ต'; + table += `\n### ${icon} ${sev} (${grouped[sev].length})\n\n`; + for (const r of grouped[sev]) { + const loc = r.locations?.[0]?.physicalLocation; + const file = loc?.artifactLocation?.uri || 'unknown'; + const line = loc?.region?.startLine || '?'; + const msg = r.message?.text || r.message?.markdown || 'No message'; + table += `- \`${file}:${line}\` โ€” ${msg}\n`; + } + } + + const hasErrors = (grouped['Error'] || []).length > 0; + const hasWarnings = (grouped['Warning'] || []).length > 0; + const status = hasErrors ? 'โŒ **Blocked** โ€” critical issues found.' : + hasWarnings ? 'โš ๏ธ **Review recommended** โ€” warnings found.' : + 'โœ… **Passed** โ€” only informational notes.'; + + body = `## ๐Ÿ” Cora AI Code Review\n\n${status}\n${table}\n---\n_Review powered by [cora-cli](https://github.com/ajianaz/cora-cli) ยท BYOK ยท MIT_`; + } + } + + // Find existing cora comment and update it, or create new + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.startsWith('## ๐Ÿ” Cora AI Code Review') + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + - name: Check for blocking issues + if: always() + shell: bash + run: | + if [ -f cora-results.sarif ]; then + ERRORS=$(python3 -c " + import json, sys + try: + data = json.load(open('cora-results.sarif')) + results = data.get('runs', [{}])[0].get('results', []) + errors = [r for r in results if r.get('level') == 'error'] + print(len(errors)) + except: + print(0) + " 2>/dev/null || echo "0") + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Cora found $ERRORS blocking issue(s)." + exit 1 + fi + fi + echo "No blocking issues." diff --git a/.github/actions/cora-review/action.yml b/.github/actions/cora-review/action.yml index a6ef384..a4198a7 100644 --- a/.github/actions/cora-review/action.yml +++ b/.github/actions/cora-review/action.yml @@ -1,5 +1,5 @@ name: 'Cora AI Code Review' -description: 'Run cora AI code review on a PR diff โ€” posts a PR comment. SARIF upload is optional.' +description: 'Run cora AI code review on a PR diff โ€” posts a PR comment. Uses Infisical OIDC for LLM secrets (zero keys in GitHub). SARIF upload is optional.' inputs: base-branch: @@ -49,6 +49,18 @@ runs: env-slug: ${{ inputs.infisical-env }} domain: ${{ inputs.infisical-domain }} + - name: Resolve cora-cli version + id: resolve + shell: bash + run: | + if [ "${{ inputs.cora-version }}" = "latest" ]; then + VERSION=$(curl -sL https://api.github.com/repos/ajianaz/cora-cli/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])") + else + VERSION="${{ inputs.cora-version }}" + fi + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "Resolved cora-cli version: $VERSION" + - name: Install cora-cli shell: bash run: | @@ -61,7 +73,7 @@ runs: echo "Unsupported architecture: $ARCH" exit 1 fi - curl -sL "https://github.com/ajianaz/cora-cli/releases/download/${{ inputs.cora-version }}/${ASSET}-${{ inputs.cora-version }}.tar.gz" | tar xz -C /usr/local/bin cora + curl -sL "https://github.com/ajianaz/cora-cli/releases/download/${{ steps.resolve.outputs.VERSION }}/${ASSET}-${{ steps.resolve.outputs.VERSION }}.tar.gz" | tar xz -C /usr/local/bin cora chmod +x /usr/local/bin/cora cora --version diff --git a/README.md b/README.md index 6b3ba1f..e4d1854 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ **Cora** is a fast, opinionated CLI tool that uses LLMs to review your code changes โ€” directly in your terminal, CI/CD pipeline, or git hooks. +๐ŸŒ [Website](https://codecora.dev) + --- @@ -275,38 +277,194 @@ providers: ## ๐Ÿ”— CI/CD Integration -### GitHub Actions +Cora ships with **two reusable composite actions** for GitHub Actions. Both handle downloading the binary from GitHub Releases, running the review, posting PR comments, and optionally uploading SARIF. The difference is how LLM secrets are provided. + +### Which Action to Use? + +| | **cora-review** | **cora-review-simple** | +|--|:--|:--| +| Secret source | Infisical (OIDC) | GitHub Secrets | +| Keys in GitHub? | โŒ Zero | โœ… 3 secrets | +| Setup complexity | Infisical account needed | Copy-paste | +| Best for | Teams with Infisical | Quick setup, personal repos | + +--- + +### Option A: `cora-review` โ€” Infisical OIDC (recommended) + +Zero API keys stored in GitHub. Secrets are pulled at runtime from [Infisical](https://infisical.com/) via OIDC. + +#### Step 1: Copy the action + +```bash +mkdir -p .github/actions/cora-review +curl -sL https://raw.githubusercontent.com/ajianaz/cora-cli/develop/.github/actions/cora-review/action.yml \ + -o .github/actions/cora-review/action.yml +``` + +#### Step 2: Add required secrets + +| Secret | Description | +|--------|-------------| +| `INFISICAL_IDENTITY_ID` | Infisical OIDC identity ID | +| `CORA_API_KEY` | LLM API key (set in Infisical, not GitHub) | +| `CORA_BASE_URL` | API base URL (set in Infisical, not GitHub) | +| `CORA_MODEL` | Model ID (set in Infisical, not GitHub) | + +#### Step 3: Add the CI job ```yaml -# .github/workflows/review.yml -name: Code Review +# .github/workflows/ci.yml +name: CI on: pull_request: - types: [opened, synchronize, reopened] + branches: [develop] + +permissions: + contents: read + security-events: write + pull-requests: write + id-token: write # Required for Infisical OIDC jobs: - review: + cora-review: + name: Cora Review runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + security-events: write + pull-requests: write + id-token: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Install cora-cli - run: cargo install cora-cli + - uses: ./.github/actions/cora-review + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + infisical-identity-id: ${{ secrets.INFISICAL_IDENTITY_ID }} + # Optional: + # upload-sarif: 'true' # Enable GitHub Code Scanning + # cora-version: 'v0.1.2' # Pin version (default: latest) + # severity: 'warning' # Minimum severity (default: major) + # base-branch: 'origin/main' # Diff target (default: origin/develop) +``` + +--- + +### Option B: `cora-review-simple` โ€” GitHub Secrets + +Set API keys directly as GitHub repository secrets. No external service needed. + +#### Step 1: Copy the action + +```bash +mkdir -p .github/actions/cora-review-simple +curl -sL https://raw.githubusercontent.com/ajianaz/cora-cli/develop/.github/actions/cora-review-simple/action.yml \ + -o .github/actions/cora-review-simple/action.yml +``` + +#### Step 2: Add required secrets + +Go to your repo โ†’ **Settings โ†’ Secrets and variables โ†’ Actions โ†’ New repository secret**: + +| Secret | Example Value | +|--------|---------------| +| `CORA_API_KEY` | `sk-...` (OpenAI, Anthropic, etc.) | +| `CORA_BASE_URL` | `https://api.openai.com/v1` | +| `CORA_MODEL` | `gpt-4o` | + +#### Step 3: Add the CI job - - name: Run code review - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: | - cora review --base origin/develop --format sarif --output-file results.sarif +```yaml +# .github/workflows/ci.yml +name: CI + +on: + pull_request: + branches: [develop] + +permissions: + contents: read + security-events: write + pull-requests: write - - name: Upload SARIF to GitHub Code Scanning - if: always() - uses: github/codeql-action/upload-sarif@v3 +jobs: + cora-review: + name: Cora Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + security-events: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: ./.github/actions/cora-review-simple with: - sarif_file: results.sarif + github-token: ${{ secrets.GITHUB_TOKEN }} + cora-api-key: ${{ secrets.CORA_API_KEY }} + cora-base-url: ${{ secrets.CORA_BASE_URL }} + cora-model: ${{ secrets.CORA_MODEL }} + # Optional: + # upload-sarif: 'true' # Enable GitHub Code Scanning + # cora-version: 'v0.1.2' # Pin version (default: latest) + # severity: 'warning' # Minimum severity (default: major) + # base-branch: 'origin/main' # Diff target (default: origin/develop) +``` + +--- + +### Configuration Reference (both actions) + +| Input | Default | Description | +|-------|---------|-------------| +| `github-token` | *(required)* | `secrets.GITHUB_TOKEN` for PR comments | +| `cora-version` | `latest` | Version tag or `latest` for auto-resolve via API | +| `upload-sarif` | `false` | Upload SARIF to GitHub Code Scanning | +| `severity` | `major` | Minimum severity: `info`, `minor`, `major`, `critical` | +| `base-branch` | `origin/develop` | Branch to diff against | + +### How It Works + +``` +PR opened + โ†’ Resolve cora version (GitHub API if "latest", or use pinned tag) + โ†’ Download cora binary from GitHub Releases + โ†’ Fetch LLM secrets (Infisical OIDC or GitHub Secrets) + โ†’ Run cora review --base --format sarif + โ†’ Parse SARIF โ†’ post structured PR comment (updates on re-run) + โ†’ Optionally upload SARIF to GitHub Code Scanning + โ†’ Exit 1 if error-level findings found (blocks merge) +``` + +### Block Merge on Critical Findings + +Add `Cora Review` as a required status check in your **Branch Protection** or **Repository Ruleset**. When cora finds error-level issues, the job fails and merge is blocked. + +### Pre-commit Hook + +Add cora as a git pre-commit hook for instant feedback: + +```bash +cora hook install +``` + +Or add it manually to `.git/hooks/pre-commit`: + +```bash +#!/bin/sh +cora review --quiet --severity critical +if [ $? -ne 0 ]; then + echo "โŒ Code review found critical issues. Commit blocked." + exit 1 +fi ``` ### GitLab CI @@ -326,34 +484,42 @@ code-review: - if: $CI_PIPELINE_SOURCE == "merge_request_event" ``` -### Pre-commit Hook +### โš ๏ธ Troubleshooting -Add cora as a git pre-commit hook for instant feedback: +
+Common issues and solutions -```bash -# Install as pre-commit hook -cora hook install +**Q: `tar: Child returned status 1` โ€” binary download fails** -# Review only staged files before each commit -# This runs automatically on `git commit` +Release archives contain a binary named `cora` (since v0.1.2). Older releases used the target triple name. Always use `latest` or pin to `>=v0.1.2`. -# Remove the hook -cora hook uninstall -``` +**Q: `latest` version doesn't resolve** -Or add it manually to `.git/hooks/pre-commit`: +The action resolves `latest` via GitHub API: `GET /repos/ajianaz/cora-cli/releases/latest` โ†’ extracts `tag_name`. This requires internet access on the runner. If rate-limited, pin a specific version instead. + +**Q: Branch protection blocks merge but no failing checks visible in UI** + +GitHub has two layers: **Branch Protection** (API) and **Repository Rulesets** (UI). Ruleset checks may not appear in the PR checks list. Verify via API: ```bash -#!/bin/sh -# cora-cli pre-commit hook -cora review --quiet --severity critical -if [ $? -ne 0 ]; then - echo "โŒ Code review found critical issues. Commit blocked." - echo " Run 'cora review' to see details, or use 'git commit --no-verify' to skip." - exit 1 -fi +gh api repos/{owner}/{repo}/rulesets/{id} +gh api repos/{owner}/{repo}/branches/{branch}/protection ``` +**Q: Chicken-and-egg โ€” action needs new binary but can't merge to release it** + +Temporarily remove `Cora Review` from your ruleset's required checks, merge the fix, release, then re-add. + +**Q: Private repos get 403 on Branch Protection API** + +GitHub Free doesn't support Branch Protection API for private repos. Use **Repository Rulesets** instead. + +**Q: Should cora-cli review itself in CI?** + +No โ€” the action downloads the released binary, which doesn't include in-progress changes. Self-review always reviews the *previous* release against itself. + +
+ ## ๐Ÿ†š Positioning: How Cora Compares | Feature | **cora-cli** | AI Agent IDE Tools | Standard Linters |