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
106 changes: 77 additions & 29 deletions .github/workflows/diffscope.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: DiffScope Review
on:
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, ready_for_review, review_requested]

permissions:
contents: read
Expand All @@ -20,61 +20,109 @@ jobs:
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}


- uses: actions/setup-node@v5
with:
node-version: 24
cache: npm
cache-dependency-path: web/package-lock.json

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

- name: Build frontend
working-directory: web
run: |
npm ci
npm run build

- name: Build current DiffScope binary
run: cargo build --release

- name: Get PR diff
id: diff
run: |
git fetch origin ${{ github.base_ref }} --depth=1
git diff origin/${{ github.base_ref }}...HEAD > pr.diff

- name: Check API key
id: check_key

- name: Select review provider
id: provider
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
if [ -z "${{ secrets.OPENAI_API_KEY }}" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "::notice::DiffScope review skipped — OPENAI_API_KEY secret not configured"
if [ -n "${ANTHROPIC_API_KEY}" ]; then
{
echo "configured=true"
echo "model=anthropic/claude-opus-4.5"
echo "adapter=anthropic"
} >> "$GITHUB_OUTPUT"
elif [ -n "${OPENROUTER_API_KEY}" ]; then
{
echo "configured=true"
echo "model=anthropic/claude-opus-4.5"
echo "adapter=openrouter"
} >> "$GITHUB_OUTPUT"
elif [ -n "${OPENAI_API_KEY}" ]; then
{
echo "configured=true"
echo "model=gpt-4o"
echo "adapter=openai"
} >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "::notice::DiffScope review skipped; no ANTHROPIC_API_KEY, OPENROUTER_API_KEY, or OPENAI_API_KEY secret is configured"
fi

- name: Check image available
id: check_image
- name: Run DiffScope from this branch
if: steps.provider.outputs.configured == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
docker pull ghcr.io/evalops/diffscope:latest 2>/dev/null && echo "available=true" >> "$GITHUB_OUTPUT" || echo "available=false" >> "$GITHUB_OUTPUT"

- name: Notice image unavailable
if: steps.check_image.outputs.available == 'false'
run: echo "::notice::DiffScope review skipped — image ghcr.io/evalops/diffscope:latest not available (merge to main publishes it)"
./target/release/diffscope \
--model "${{ steps.provider.outputs.model }}" \
--adapter "${{ steps.provider.outputs.adapter }}" \
--output-format json \
review --diff pr.diff --output comments.json

- name: Run DiffScope
if: steps.check_key.outputs.skip != 'true' && steps.check_image.outputs.available == 'true'
run: |
docker run --rm \
-e OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
-v "$PWD":/workspace -w /workspace \
ghcr.io/evalops/diffscope:latest \
review --diff pr.diff --output-format json --output comments.json
- name: Upload review artifact
if: always() && steps.provider.outputs.configured == 'true'
uses: actions/upload-artifact@v4
with:
name: diffscope-review-${{ github.event.pull_request.number }}
path: comments.json
if-no-files-found: ignore

- name: Post comments
if: steps.check_key.outputs.skip != 'true' && steps.check_image.outputs.available == 'true'
if: steps.provider.outputs.configured == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('comments.json')) {
core.notice('DiffScope did not produce comments.json');
return;
}
const comments = JSON.parse(fs.readFileSync('comments.json', 'utf8'));
const headSha = context.payload.pull_request.head.sha;
const fallback = [];

for (const comment of comments) {
if (!comment.file_path || !comment.line_number || comment.line_number < 1) {
continue;
}
const body = [
`**${comment.severity}**: ${comment.content}`,
comment.suggestion ? `\nSuggestion: ${comment.suggestion}` : ''
].join('');
try {
await github.rest.pulls.createReviewComment({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: `**${comment.severity}**: ${comment.content}`,
body,
commit_id: headSha,
path: comment.file_path,
line: comment.line_number,
Expand All @@ -90,7 +138,7 @@ jobs:
"Some review comments could not be placed inline and are listed below:",
"",
...fallback.slice(0, 100)
].join("\\n");
].join("\n");
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
Expand Down
34 changes: 28 additions & 6 deletions .github/workflows/eval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,27 @@ env:
CARGO_TERM_COLOR: always

jobs:
contracts:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2
- name: Run always-on review contract tests
run: |
cargo test fetch_additional_context_rejects_absolute_and_parent_traversal_outside_base
cargo test fetch_context_rejects_parent_traversal
cargo test resolve_pattern_repositories_rejects_parent_traversal_local_paths
cargo test resolve_pattern_repositories_rejects_absolute_local_paths_outside_repo
cargo test build_verification_prompt_sanitizes_adversarial_finding_text
cargo test execute_dag_retries_retryable_nodes
cargo test execute_dag_applies_node_timeout

eval:
runs-on: ubuntu-latest
timeout-minutes: 60
needs: contracts
steps:
- name: Check eval secret
id: secret-check
Expand Down Expand Up @@ -59,6 +77,7 @@ jobs:
--model gpt-4o-mini \
--temperature 0 \
--fixtures eval/fixtures \
--artifact-dir /tmp/eval-baseline-artifacts \
--output /tmp/eval-baseline.json

- name: Run eval thresholds on current branch
Expand All @@ -71,14 +90,15 @@ jobs:
--temperature 0 \
--fixtures eval/fixtures \
--output eval-current.json \
--artifact-dir eval-artifacts \
--baseline /tmp/eval-baseline.json \
--max-micro-f1-drop 0.20 \
--min-micro-f1 0.20 \
--min-verification-health 0.80 \
--max-micro-f1-drop 0.15 \
--min-micro-f1 0.25 \
--min-verification-health 0.85 \
--min-rule-f1 sec.shell.injection=0.10 \
--min-rule-f1 reliability.unwrap_panic=0.10 \
--max-rule-f1-drop sec.shell.injection=0.25 \
--max-rule-f1-drop reliability.unwrap_panic=0.25
--max-rule-f1-drop sec.shell.injection=0.20 \
--max-rule-f1-drop reliability.unwrap_panic=0.20

- name: Upload eval reports
if: ${{ always() && steps.secret-check.outputs.configured == 'true' }}
Expand All @@ -87,8 +107,10 @@ jobs:
name: eval-reports
path: |
eval-current.json
eval-artifacts/**
/tmp/eval-baseline.json
/tmp/eval-baseline-artifacts/**

- name: Skip message
if: ${{ steps.secret-check.outputs.configured != 'true' }}
run: echo "Skipping eval workflow because OPENAI_API_KEY secret is not configured."
run: echo "Skipping live eval workflow because OPENAI_API_KEY secret is not configured; offline contract tests still ran."
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name = "diffscope"
version = "0.5.28"
edition = "2021"
rust-version = "1.88"
build = "build.rs"
authors = ["Jonathan Haas <jonathan@haas.holdings>"]
description = "A composable code review engine with smart analysis, confidence scoring, and professional reporting"
license = "Apache-2.0"
Expand Down
14 changes: 11 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ branding:

inputs:
model:
description: 'LLM model to use (e.g., gpt-4o, ollama:codellama)'
description: 'LLM model to use (e.g., anthropic/claude-opus-4.5, openai/gpt-4o, ollama:codellama)'
required: false
default: 'gpt-4o'
default: 'anthropic/claude-opus-4.5'
output-format:
description: 'Output format (json, patch, markdown)'
required: false
default: 'json'
openai-api-key:
description: 'OpenAI API key (can also use OPENAI_API_KEY env var)'
required: false
anthropic-api-key:
description: 'Anthropic API key (can also use ANTHROPIC_API_KEY env var)'
required: false
openrouter-api-key:
description: 'OpenRouter API key (can also use OPENROUTER_API_KEY env var)'
required: false

runs:
using: 'docker'
Expand All @@ -28,4 +34,6 @@ runs:
- '--output-format'
- ${{ inputs.output-format }}
env:
OPENAI_API_KEY: ${{ inputs.openai-api-key }}
OPENAI_API_KEY: ${{ inputs.openai-api-key }}
ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }}
OPENROUTER_API_KEY: ${{ inputs.openrouter-api-key }}
18 changes: 18 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use std::fs;
use std::path::Path;

fn main() {
println!("cargo:rerun-if-changed=web/dist");

let dist = Path::new("web/dist");
if dist.exists() {
return;
}

fs::create_dir_all(dist).expect("failed to create web/dist fallback directory");
fs::write(
dist.join("index.html"),
r#"<!doctype html><html><head><meta charset="utf-8"><title>DiffScope</title></head><body>Frontend not built. Run npm run build in web/ for the full UI.</body></html>"#,
)
.expect("failed to write web/dist fallback index.html");
}
36 changes: 36 additions & 0 deletions charts/diffscope/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ spec:
- configMapRef:
name: {{ include "diffscope.fullname" . }}
env:
- name: DIFFSCOPE_SERVER_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "diffscope.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.serverApiKey }}
optional: true
- name: DIFFSCOPE_API_KEY
valueFrom:
secretKeyRef:
Expand All @@ -122,6 +128,36 @@ spec:
name: {{ include "diffscope.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.anthropicApiKey }}
optional: true
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "diffscope.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.githubToken }}
optional: true
- name: DIFFSCOPE_GITHUB_APP_ID
valueFrom:
secretKeyRef:
name: {{ include "diffscope.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.githubAppId }}
optional: true
- name: DIFFSCOPE_GITHUB_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: {{ include "diffscope.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.githubPrivateKey }}
optional: true
- name: DIFFSCOPE_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: {{ include "diffscope.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.githubWebhookSecret }}
optional: true
- name: DIFFSCOPE_AUTOMATION_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: {{ include "diffscope.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.automationWebhookSecret }}
optional: true
livenessProbe:
httpGet:
path: /api/status
Expand Down
18 changes: 18 additions & 0 deletions charts/diffscope/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ metadata:
{{- include "diffscope.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- if .Values.secrets.serverApiKey }}
DIFFSCOPE_SERVER_API_KEY: {{ .Values.secrets.serverApiKey | quote }}
{{- end }}
{{- if .Values.secrets.diffscopeApiKey }}
DIFFSCOPE_API_KEY: {{ .Values.secrets.diffscopeApiKey | quote }}
{{- end }}
Expand All @@ -19,4 +22,19 @@ stringData:
{{- if .Values.secrets.anthropicApiKey }}
ANTHROPIC_API_KEY: {{ .Values.secrets.anthropicApiKey | quote }}
{{- end }}
{{- if .Values.secrets.githubToken }}
GITHUB_TOKEN: {{ .Values.secrets.githubToken | quote }}
{{- end }}
{{- if .Values.secrets.githubAppId }}
DIFFSCOPE_GITHUB_APP_ID: {{ .Values.secrets.githubAppId | quote }}
{{- end }}
{{- if .Values.secrets.githubPrivateKey }}
DIFFSCOPE_GITHUB_PRIVATE_KEY: {{ .Values.secrets.githubPrivateKey | quote }}
{{- end }}
{{- if .Values.secrets.githubWebhookSecret }}
DIFFSCOPE_WEBHOOK_SECRET: {{ .Values.secrets.githubWebhookSecret | quote }}
{{- end }}
{{- if .Values.secrets.automationWebhookSecret }}
DIFFSCOPE_AUTOMATION_WEBHOOK_SECRET: {{ .Values.secrets.automationWebhookSecret | quote }}
{{- end }}
{{- end }}
14 changes: 14 additions & 0 deletions charts/diffscope/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,31 @@ diffscope:

# -- API keys (stored in a Secret)
secrets:
# API key for protected DiffScope server mutation routes.
serverApiKey: ""
# Legacy/top-level LLM API key fallback. Prefer provider-specific keys below.
diffscopeApiKey: ""
openaiApiKey: ""
openrouterApiKey: ""
anthropicApiKey: ""
githubToken: ""
githubAppId: ""
githubPrivateKey: ""
githubWebhookSecret: ""
automationWebhookSecret: ""
# Use an existing secret instead of creating one
existingSecret: ""
existingSecretKeys:
serverApiKey: "DIFFSCOPE_SERVER_API_KEY"
diffscopeApiKey: "DIFFSCOPE_API_KEY"
openaiApiKey: "OPENAI_API_KEY"
openrouterApiKey: "OPENROUTER_API_KEY"
anthropicApiKey: "ANTHROPIC_API_KEY"
githubToken: "GITHUB_TOKEN"
githubAppId: "DIFFSCOPE_GITHUB_APP_ID"
githubPrivateKey: "DIFFSCOPE_GITHUB_PRIVATE_KEY"
githubWebhookSecret: "DIFFSCOPE_WEBHOOK_SECRET"
automationWebhookSecret: "DIFFSCOPE_AUTOMATION_WEBHOOK_SECRET"

# -- Non-secret environment config
config:
Expand Down
Loading
Loading