Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 65 additions & 28 deletions .github/actions/cora-review/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: 'Cora AI Code Review'
description: 'Run cora AI code review on a PR diff — posts a PR comment with findings. 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:
Expand Down Expand Up @@ -41,7 +41,7 @@ runs:
using: 'composite'
steps:
- name: Fetch LLM secrets from Infisical
uses: Infisical/secrets-action@v1.0.9
uses: Infisical/secrets-action@8a06c1bdcd5b8635d510c52d4b57a92c1ccef785 # v1.0.9
with:
method: 'oidc'
identity-id: ${{ inputs.infisical-identity-id }}
Expand All @@ -52,17 +52,34 @@ runs:
- name: Resolve cora-cli version
id: resolve
shell: bash
env:
INPUT_VERSION: ${{ inputs.cora-version }}
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'])")
if [ "$INPUT_VERSION" = "latest" ]; then
for i in 1 2 3; do
VERSION=$(curl -sfL --connect-timeout 10 \
https://api.github.com/repos/ajianaz/cora-cli/releases/latest \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tag_name', ''))" 2>/dev/null)
if [ -n "$VERSION" ]; then
break
fi
echo "Attempt $i failed, retrying..."
sleep 5
done
if [ -z "$VERSION" ]; then
echo "::warning::Failed to resolve latest cora-cli version from GitHub API, using fallback"
VERSION="v0.1.7"
fi
else
VERSION="${{ inputs.cora-version }}"
VERSION="$INPUT_VERSION"
fi
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved cora-cli version: $VERSION"

- name: Install cora-cli
shell: bash
env:
CORA_VERSION: ${{ steps.resolve.outputs.VERSION }}
run: |
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
Expand All @@ -73,7 +90,27 @@ runs:
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
curl -sL "https://github.com/ajianaz/cora-cli/releases/download/${CORA_VERSION}/${ASSET}-${CORA_VERSION}.tar.gz" \
-o /tmp/cora.tar.gz
curl -sL "https://github.com/ajianaz/cora-cli/releases/download/${CORA_VERSION}/checksums-sha256.txt" \
-o /tmp/cora-checksums.txt || true
if [ -f /tmp/cora-checksums.txt ] && [ -s /tmp/cora-checksums.txt ]; then
EXPECTED=$(grep "${ASSET}-${CORA_VERSION}.tar.gz" /tmp/cora-checksums.txt | awk '{print $1}' || true)
ACTUAL=$(sha256sum /tmp/cora.tar.gz | awk '{print $1}')
if [ -n "$EXPECTED" ] && [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::Checksum verification failed for ${ASSET}-${CORA_VERSION}.tar.gz"
echo "Expected: $EXPECTED"
echo "Actual: $ACTUAL"
exit 1
elif [ -z "$EXPECTED" ]; then
echo "::warning::No checksum found for ${ASSET}-${CORA_VERSION}.tar.gz in checksums file"
fi
rm -f /tmp/cora-checksums.txt
else
echo "::warning::No checksums file found, skipping verification"
fi
tar xzf /tmp/cora.tar.gz -C /usr/local/bin cora
rm -f /tmp/cora.tar.gz
chmod +x /usr/local/bin/cora
cora --version

Expand All @@ -84,40 +121,31 @@ runs:
CORA_API_KEY: ${{ env.CORA_API_KEY }}
CORA_BASE_URL: ${{ env.CORA_BASE_URL }}
CORA_MODEL: ${{ env.CORA_MODEL }}
INPUT_BASE: ${{ inputs.base-branch }}
INPUT_SEVERITY: ${{ inputs.severity }}
run: |
set +e
cora review \
--base ${{ inputs.base-branch }} \
--base "$INPUT_BASE" \
--format sarif \
--severity ${{ inputs.severity }} \
--severity "$INPUT_SEVERITY" \
--quiet \
> cora-results.sarif 2>cora-stderr.log
CORA_EXIT=$?
set -e

if [ $CORA_EXIT -ne 0 ] && [ $CORA_EXIT -ne 2 ]; then
echo "::warning::cora exited with code $CORA_EXIT"
cat cora-stderr.log || true
fi

if [ -f cora-results.sarif ] && [ -s cora-results.sarif ]; then
echo "Cora review complete ($(wc -c < cora-results.sarif) bytes)"
else
echo "::warning::cora produced no SARIF output, skipping review"
echo '{}' > cora-results.sarif
> cora-results.sarif 2>cora-stderr.log || true
echo "Cora review complete ($(wc -c < cora-results.sarif) bytes)"
if [ -s cora-stderr.log ]; then
echo "::warning::Cora stderr: $(cat cora-stderr.log)"
fi

- name: Upload SARIF to GitHub Code Scanning
if: inputs.upload-sarif == 'true'
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@f52b05f4acaaa234e44466e66d29050e135ea9ef # v4.36.0
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
uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
env:
GH_TOKEN: ${{ inputs.github-token }}
with:
Expand All @@ -133,11 +161,21 @@ runs:

let body;
if (!sarifContent || !sarifContent.runs || sarifContent.runs.length === 0) {
body = `## 🔍 Cora AI Code Review\n\n✅ **No issues found.** Code looks good!`;
let fileSize = 0;
try {
fileSize = fs.statSync('cora-results.sarif').size;
} catch (e) {
fileSize = 0;
}
if (fileSize < 10) {
body = `## 🔍 Cora AI Code Review\n\n⚠️ **Review could not complete.** Cora produced an empty result. Check the workflow logs for errors.\n\n---\n_Review powered by [cora-cli](https://github.com/ajianaz/cora-cli) · BYOK · MIT_`;
} else {
body = `## 🔍 Cora AI Code Review\n\n✅ **No issues found.** Code looks good!\n\n---\n_Review powered by [cora-cli](https://github.com/ajianaz/cora-cli) · BYOK · MIT_`;
}
} else {
const results = sarifContent.runs[0].results || [];
if (results.length === 0) {
body = `## 🔍 Cora AI Code Review\n\n✅ **No issues found.** Code looks good!`;
body = `## 🔍 Cora AI Code Review\n\n✅ **No issues found.** Code looks good!\n\n---\n_Review powered by [cora-cli](https://github.com/ajianaz/cora-cli) · BYOK · MIT_`;
} else {
const grouped = {};
for (const r of results) {
Expand Down Expand Up @@ -172,7 +210,6 @@ runs:
}
}

// 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,
Expand Down
28 changes: 0 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

permissions:
contents: read
security-events: write
pull-requests: write
id-token: write

jobs:
check:
name: Check
Expand Down Expand Up @@ -65,25 +59,3 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo build --release --workspace

cora-review:
name: Cora Review
runs-on: ubuntu-latest
needs: [check, fmt, clippy, test]
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

- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2

- uses: ./.github/actions/cora-review
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
infisical-identity-id: ${{ secrets.INFISICAL_IDENTITY_ID }}
34 changes: 34 additions & 0 deletions .github/workflows/cora-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Cora AI Code Review

on:
pull_request:
branches: [develop]
types: [opened, synchronize, ready_for_review, reopened]

concurrency:
group: cora-review-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write
id-token: write
security-events: write

jobs:
cora-review:
name: Cora Review
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
persist-credentials: false

- name: Run Cora AI Code Review
uses: ./.github/actions/cora-review
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
infisical-identity-id: ${{ secrets.INFISICAL_IDENTITY_ID }}
Loading