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
169 changes: 169 additions & 0 deletions .github/actions/cora-review/action.yml
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:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
method: 'oidc'

Check failure

Code 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 failure

Code 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 }}
Comment thread
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does GitHub Actions hashFiles() return an empty string for a zero-byte (empty) file, or only when no files match the pattern?

💡 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 hashFiles guard so empty SARIF doesn’t get uploaded
The step > cora-results.sarif 2>/dev/null || true will still create cora-results.sarif as a zero-byte file when the review command fails. hashFiles('cora-results.sarif') returns a hash for a matched (but empty) file, and only returns '' when the glob matches no files—so if: ... && hashFiles('cora-results.sarif') != '' can pass and upload-sarif will try to upload invalid/empty JSON. Gate the upload on file size (e.g., test -s cora-results.sarif) and/or JSON validity via a preceding check step, then use that output in the upload step’s if.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/cora-review/action.yml around lines 55 - 60, The Upload
SARIF to GitHub Code Scanning step can run on a zero-byte cora-results.sarif
because hashFiles returns non-empty for an existing empty file; add a preceding
check step (e.g., "Check SARIF file") that tests size and/or JSON validity (run
test -s cora-results.sarif && jq -e . cora-results.sarif >/dev/null) and sets an
output like sarif_valid=true/false, then change the Upload SARIF step's if to
depend on that output (e.g., if: steps.check-sarif.outputs.sarif_valid ==
'true') so upload-sarif only runs when cora-results.sarif is non-empty and valid
JSON.


- 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does actions/github-script v7 use the github-token input (default github.token) to authenticate the github Octokit client, and does it read a GH_TOKEN env var?

💡 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 inputs.github-token into actions/github-script@v7 (remove unused env.GH_TOKEN)

actions/github-script@v7 authenticates its Octokit client from with.github-token (defaulting to github.token); it does not read env.GH_TOKEN. As written, inputs.github-token has no effect on the PR-comment API calls, and the GH_TOKEN env var is misleading. Also, the existing dedup check hardcodes c.user.login === 'github-actions[bot]', so once token wiring is corrected, non-default tokens would likely cause the update logic to miss prior comments.

🔧 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Post PR comment
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
env:
GH_TOKEN: ${{ inputs.github-token }}
with:
- name: Post PR comment
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
github-token: ${{ inputs.github-token }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/cora-review/action.yml around lines 62 - 67, In the "Post PR
comment" step using actions/github-script@v7, remove the misleading env GH_TOKEN
and wire the provided inputs.github-token into the action by setting
with.github-token: ${{ inputs.github-token }} so the Octokit client actually
uses the input token; then update the dedup logic that currently checks
c.user.login === 'github-actions[bot]' to a token-agnostic check (for example
compare c.user.login to github.actor or check c.user.type === 'Bot' and a unique
comment marker in c.body) so prior comments posted with non-default tokens are
detected and updated.

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."
150 changes: 8 additions & 142 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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"
echo

Repository: 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 permissions in .github/workflows/ci.yml still grant security-events: write, pull-requests: write, and id-token: write to all jobs (check, fmt, clippy, test, build), so the least-privilege improvement is incomplete even though cora-review has its own job-level permissions.

🔐 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 73 - 77, Top-level workflow
permissions currently grant write scopes (security-events: write, pull-requests:
write, id-token: write) which are inherited by all jobs; remove those
write-scoped entries and restrict the top-level permissions to only minimal read
(e.g., contents: read) so jobs like check, fmt, clippy, test, build do not get
elevated access, then add any required write permissions at the job level (for
example ensure the cora-review job keeps its specific write permissions) so only
jobs that need write scopes receive them.

steps:
- uses: actions/checkout@v4
with:
Expand All @@ -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 }}
Loading