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
163 changes: 163 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
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 @@ -58,3 +64,160 @@
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo build --release

cora-review:
name: Cora Review
runs-on: ubuntu-latest
needs: [check, fmt, clippy, test]
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

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

- name: Fetch LLM secrets from Infisical
uses: Infisical/secrets-action@v1.0.9

Check failure

Code scanning / Cora

Hardcoded Infisical identity-id in workflow Error

The Infisical identity-id is hardcoded directly in the workflow file. While OIDC federation is used (good), the identity-id is a sensitive reference that should ideally be stored as a repository secret or variable rather than committed in plaintext. If this identity is ever rotated, every workflow file needs updating.
with:
method: "oidc"
identity-id: "6bd2b8d8-a9a3-4331-8b37-bf2764fc320b"
project-slug: "github-actions"
env-slug: "prod"
domain: "https://infisical.ajianaz.dev"

- name: Run cora review
run: |
cargo build --release
echo "=== CORA REVIEW (SARIF) ===" >> $GITHUB_STEP_SUMMARY

Check failure

Code scanning / Cora

SARIF upload may fail on empty/invalid file Error

The || true on the cora review command means cora-results.sarif may be empty or contain stderr output (non-JSON). The SARIF upload step runs with if: always(), so it will attempt to upload a potentially invalid/empty file, which will cause the github/codeql-action/upload-sarif step to fail. While if: always() ensures it runs, the upload action itself will error on malformed SARIF.

Check failure

Code scanning / Cora

SARIF upload may fail on empty/invalid file Error

The command uses > cora-results.sarif 2>&1 which redirects both stdout and stderr into the SARIF file. If cora outputs any warnings or errors to stderr, the file will contain non-JSON content prepended/appended to the SARIF JSON, making it completely invalid for both the SARIF upload and the PR comment parsing.

Check failure

Code scanning / Cora

SARIF upload may fail on empty/invalid file Error

The || true means the cora command can fail without creating the output file. Subsequent steps that read cora-results.sarif (upload SARIF, post PR comment, check for blocking issues) will fail when the file doesn't exist. The PR comment step handles this with try/catch, but the SARIF upload and blocking check steps don't properly guard against a missing file.
./target/release/cora review \
--base origin/develop \
--format sarif \
--severity major \
--quiet \
> cora-results.sarif 2>&1 || true

echo "### 🔍 Cora AI Code Review" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -s cora-results.sarif ]; then
echo '```json' >> $GITHUB_STEP_SUMMARY
cat cora-results.sarif >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
else
echo "No issues found." >> $GITHUB_STEP_SUMMARY
fi

- name: Upload SARIF to GitHub Code Scanning
if: always()
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
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 {
// Group by severity
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()
run: |
if [ -f cora-results.sarif ]; then
# Check for error-level results in SARIF
ERRORS=$(cat cora-results.sarif | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
results = data.get('runs', [{}])[0].get('results', [])
errors = [r for r in results if r.get('level') in ('error', '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). Review the Code Scanning results."
exit 1
fi
fi
echo "No blocking issues."
75 changes: 42 additions & 33 deletions src/formatters/sarif.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use anyhow::Result;
use chrono::Utc;
use serde_json::{Value, json};

use crate::engine::{ReviewIssue, ReviewResponse, ScanResponse, Severity};
Expand Down Expand Up @@ -27,24 +26,33 @@ impl Formatter for SarifFormatter {

/// Build a SARIF v2.1.0 document from a list of issues.
fn build_sarif(issues: &[ReviewIssue]) -> Value {
let rules: Vec<Value> = issues
.iter()
.map(|issue| {
json!({
"id": issue.issue_type.as_ref().map(|t| t.to_string()).unwrap_or_else(|| "unknown".to_string()),
"shortDescription": {
"text": issue.title.clone()
},
"fullDescription": {
"text": issue.body.clone()
},
"helpUri": null,
"defaultConfiguration": {
"level": severity_to_sarif_level(&issue.severity)
}
})
})
.collect();
// Deduplicate rules by id (multiple issues can share same rule)
let mut rule_map = serde_json::Map::new();
for issue in issues {
let rule_id = issue
.issue_type
.as_ref()
.map(|t| t.to_string())
.unwrap_or_else(|| "unknown".to_string());
if !rule_map.contains_key(&rule_id) {
rule_map.insert(
rule_id.clone(),
json!({
"id": rule_id,
"shortDescription": {
"text": issue.title.clone()
},
"fullDescription": {
"text": issue.body.clone()
},
"defaultConfiguration": {
"level": severity_to_sarif_level(&issue.severity)
}
}),
);
}
}
let rules: Vec<Value> = rule_map.into_values().collect();

let results: Vec<Value> = issues
.iter()
Expand Down Expand Up @@ -73,17 +81,23 @@ fn build_sarif(issues: &[ReviewIssue]) -> Value {

// Add fix suggestion if available
if let Some(ref fix) = issue.suggested_fix {
let mut replacements = Vec::new();
replacements.push(json!({
"description": {
"text": "Suggested fix"
}
// We don't have a full replacement spec, but we can include the text
}));
result["fixes"] = json!([{
"description": {
"text": fix.clone()
}
},
"artifactChanges": [{
"artifactLocation": {
"uri": issue.file.clone()
},
"replacements": [{
"deletedRegion": {
"startLine": issue.line.unwrap_or(1)
},
"insertedContent": {
"text": fix.clone()
}
}]
}]
}]);
}

Expand All @@ -103,12 +117,7 @@ fn build_sarif(issues: &[ReviewIssue]) -> Value {
"rules": rules
}
},
"results": results,
"invocation": {
"executionSuccessful": true,
"startTimeUtc": Utc::now().to_rfc3339(),
"endTimeUtc": Utc::now().to_rfc3339()
}
"results": results
}]
})
}
Expand Down
Loading