-
Notifications
You must be signed in to change notification settings - Fork 2
refactor(ci): extract cora review into composite action #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,169 @@ | ||||||||||||||||||||||||
| name: 'Cora AI Code Review' | ||||||||||||||||||||||||
| description: 'Run cora AI code review on a PR diff — uploads SARIF to Code Scanning and posts a PR comment.' | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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' | ||||||||||||||||||||||||
| infisical-identity-id: | ||||||||||||||||||||||||
| description: 'Infisical OIDC identity ID (from secret INFISICAL_IDENTITY_ID)' | ||||||||||||||||||||||||
| required: true | ||||||||||||||||||||||||
| infisical-project: | ||||||||||||||||||||||||
| description: 'Infisical project slug' | ||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||
| default: 'github-actions' | ||||||||||||||||||||||||
| infisical-env: | ||||||||||||||||||||||||
| description: 'Infisical environment slug' | ||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||
| default: 'prod' | ||||||||||||||||||||||||
| infisical-domain: | ||||||||||||||||||||||||
| description: 'Infisical domain URL' | ||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||
| default: 'https://infisical.ajianaz.dev' | ||||||||||||||||||||||||
| github-token: | ||||||||||||||||||||||||
| description: 'GitHub token for PR comments and SARIF upload' | ||||||||||||||||||||||||
| required: true | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| runs: | ||||||||||||||||||||||||
| using: 'composite' | ||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||
| - name: Fetch LLM secrets from Infisical | ||||||||||||||||||||||||
| uses: Infisical/secrets-action@v1.0.9 | ||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||||||||||||||||||||||||
| method: 'oidc' | ||||||||||||||||||||||||
Check failureCode scanning / Cora Stderr redirected to /dev/null hides errors Error
The > cora-results.sarif 2>/dev/null redirection means the step's exit code comes from cora itself. If cora exits non-zero (e.g., crash), the file may be empty or partial. However, subsequent steps use if: always() and will still run, potentially uploading an invalid/empty SARIF file or posting a misleading 'No issues found' comment. The old workflow used || true which was explicit about ignoring failures; the new version silently ignores them.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| identity-id: ${{ inputs.infisical-identity-id }} | ||||||||||||||||||||||||
Check failureCode scanning / Cora Stderr redirected to /dev/null hides errors Error
The 2>/dev/null on the cora review command suppresses all stderr output. If cora fails to start, crashes, or encounters configuration issues, there will be no diagnostic output in the workflow logs. Combined with the if: always() conditions on subsequent steps, failures will be silently swallowed and the workflow may report misleading results.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| project-slug: ${{ inputs.infisical-project }} | ||||||||||||||||||||||||
| env-slug: ${{ inputs.infisical-env }} | ||||||||||||||||||||||||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||||||||||||||||||||||||
| domain: ${{ inputs.infisical-domain }} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Run cora review | ||||||||||||||||||||||||
| shell: bash | ||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||
| cargo build --release | ||||||||||||||||||||||||
| ./target/release/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: always() && hashFiles('cora-results.sarif') != '' | ||||||||||||||||||||||||
| uses: github/codeql-action/upload-sarif@v4 | ||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||
| sarif_file: cora-results.sarif | ||||||||||||||||||||||||
| category: cora-review | ||||||||||||||||||||||||
|
Comment on lines
+56
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes—hashFiles returns an empty string when no files match the pattern. However, for a zero-byte (empty) file that matches the pattern, hashFiles returns the hash of the empty file content (not an empty string). Evidence: - GitHub’s docs/runner behavior documents that hashFiles may return an empty string when the pattern does not match any file. This is described/added via a documentation change request explaining “if the path does not match any known path, an empty string would be returned” and references the runner issue for that behavior [1][2]. - In the runner implementation, hashFiles collects files by the provided glob patterns; if the search yields no matches, the function’s return value becomes empty (i.e., it does not compute a content hash) [3]. So the “empty string” return is tied to “no matching files,” not “matched file has 0 bytes.” (A zero-byte file is still a matched file, so it’s hashed; the empty file’s content hash would be the hash of an empty byte sequence, not blank.) [3][1] Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/actions/cora-review/action.yml"
echo "== File: $FILE =="
nl -ba "$FILE" | sed -n '1,140p'Repository: ajianaz/cora-cli Length of output: 172 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/actions/cora-review/action.yml"
echo "== File: $FILE =="
nl -ba "$FILE" | sed -n '1,140p'Repository: ajianaz/cora-cli Length of output: 172 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/actions/cora-review/action.yml"
echo "== Relevant section: $FILE =="
# print with line numbers using cat -n (nl unavailable)
cat -n "$FILE" | sed -n '35,90p'Repository: ajianaz/cora-cli Length of output: 2566 Fix 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Post PR comment | ||||||||||||||||||||||||
| if: always() && github.event_name == 'pull_request' | ||||||||||||||||||||||||
| uses: actions/github-script@v7 | ||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||
| GH_TOKEN: ${{ inputs.github-token }} | ||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||
|
Comment on lines
+63
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes. actions/github-script v7 uses its github-token input (defaulting to the workflow’s github.token / default GITHUB_TOKEN) to authenticate the pre-authenticated Octokit client it exposes to your script.[1][2][3][4] No evidence that v7 reads a GH_TOKEN environment variable. The documented/declared default is github-token = ${{ github.token }} (not GH_TOKEN), and the README describes using the workflow’s GITHUB_TOKEN by default, with an explicit github-token input for a different token.[2][3][4] What this means: - If you do not set with: github-token, github-script v7’s Octokit client will use the default workflow token (github.token / GITHUB_TOKEN scoped to the current repo).[2][3] - Setting with: github-token overrides that token (e.g., a PAT from secrets).[1][2] - GH_TOKEN is not mentioned as an input source for github-script’s authentication in the v7 docs/action.yml; only github-token / github.token is.[4][2] Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/actions/cora-review/action.yml"
echo "== File = $FILE =="
nl -ba "$FILE" | sed -n '1,220p'Repository: ajianaz/cora-cli Length of output: 173 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/actions/cora-review/action.yml"
echo "== Show relevant sections from $FILE =="
echo "-- Lines 40-90 --"
cat -n "$FILE" | sed -n '40,90p'
echo "-- Lines 105-155 --"
cat -n "$FILE" | sed -n '105,155p'
echo "-- Inputs section (if any) --"
cat -n "$FILE" | sed -n '1,60p'Repository: ajianaz/cora-cli Length of output: 7003 Wire
🔧 Proposed fix - name: Post PR comment
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
- env:
- GH_TOKEN: ${{ inputs.github-token }}
with:
+ github-token: ${{ inputs.github-token }}
script: |
const fs = require('fs');📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| 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). Review the Code Scanning results." | ||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
| echo "No blocking issues." | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,6 +70,11 @@ jobs: | |
| 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 | ||
|
Comment on lines
+73
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -la "$FILE" || true
echo
echo "== Tail/head around permissions sections =="
# Show top-level permissions and nearby lines
sed -n '1,140p' "$FILE" | nl -ba | sed -n '1,140p'
echo
echo "== Permissions blocks with line numbers =="
rg -n "^[[:space:]]*permissions:" -n "$FILE" || true
echo
echo "== Extract full permissions block(s) =="
# crude extraction: from each "permissions:" line, print until next non-indented top-level key
# Use awk to print blocks starting at permissions
awk '
BEGIN{inblock=0}
/^[[:space:]]*permissions:/ {
inblock=1; print "----"; print NR ":" $0; next
}
inblock==1 {
# stop when encountering a line that looks like a new top-level key (no leading spaces) or a job id
if ($0 ~ /^[^[:space:]]/ && $0 !~ /^permissions:/) { inblock=0; next }
if ($0 ~ /^[[:space:]]*$/) { print NR ":" $0; next }
# keep indented permission lines
if ($0 ~ /^[[:space:]]+[a-zA-Z0-9_-]+:[[:space:]]*(read|write|)?/ || $0 ~ /^[[:space:]]+[a-zA-Z0-9_-]+:[[:space:]]*(true|false)?/) { print NR ":" $0; next }
# also keep comments/blank lines inside block
if ($0 ~ /^[[:space:]]*`#/`) { print NR ":" $0; next }
# otherwise keep for a bit; heuristic
print NR ":" $0
}
' "$FILE"
echoRepository: ajianaz/cora-cli Length of output: 278 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
wc -l "$FILE"
echo
echo "== Top-level permissions (line-numbered) =="
awk '
BEGIN{p=0}
/^[[:space:]]*permissions:[[:space:]]*$/ {p=1; print "----"; print FNR ":" $0; next}
p==1 {
if ($0 ~ /^[^[:space:]]/ && $0 !~ /^permissions:/) {p=0}
else { print FNR ":" $0 }
}
' "$FILE"
echo
echo "== All occurrences of 'permissions:' (line-numbered) =="
awk '/^[[:space:]]*permissions:/ {print FNR ":" $0}' "$FILE"
echo
echo "== Excerpt around lines 1-120 (to locate jobs/permissions context) =="
awk 'NR>=1 && NR<=120 {printf "%5d: %s\n", NR, $0}' "$FILE" | sed -n '1,120p'Repository: ajianaz/cora-cli Length of output: 3582 Remove write-scoped workflow permissions so other jobs don’t inherit elevated access. Top-level 🔐 Proposed fix permissions:
contents: read
- security-events: write
- pull-requests: write
- id-token: write
...
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🤖 Prompt for AI Agents |
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
|
|
@@ -78,146 +83,7 @@ jobs: | |
| - uses: dtolnay/rust-toolchain@stable | ||
| - uses: Swatinem/rust-cache@v2 | ||
|
|
||
| - name: Fetch LLM secrets from Infisical | ||
| uses: Infisical/secrets-action@v1.0.9 | ||
| - uses: ./.github/actions/cora-review | ||
| 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 | ||
| ./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." | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| infisical-identity-id: ${{ secrets.INFISICAL_IDENTITY_ID }} | ||
Uh oh!
There was an error while loading. Please reload this page.