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
21 changes: 21 additions & 0 deletions docs/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,27 @@ Remove with `crev init --uninstall` or `rm .git/hooks/pre-push`.

---

## CI / GitHub Actions (`crev init --ci`)

`crev init --ci` writes `.github/workflows/crev.yml` directly (creates directories, overwrites if exists).

Pass `--model` to get the correct secret name in the output:
```sh
crev init --ci --model gemini-2.0-flash # prints: Name: GEMINI_API_KEY
crev init --ci --model gpt-4o # prints: Name: OPENAI_API_KEY
crev init --ci # prints: Name: ANTHROPIC_API_KEY (default)
```

**Workflow triggers:**
- `pull_request: [opened]` — runs automatically when a PR is opened
- `issue_comment: /crev` — runs on demand when someone comments `/crev` on a PR; reacts with 👀 immediately to acknowledge

**Permissions:** `pull-requests: write`, `contents: read`

**Comment behaviour:** finds an existing `**crev` comment and updates it (PATCH) — no spam on repeated triggers. Falls back to creating a new comment if none exists.

---

## Review history (`src/history.rs`)

SQLite database at:
Expand Down
52 changes: 41 additions & 11 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ enum Commands {
/// Print a GitHub Actions workflow to stdout
#[arg(long)]
ci: bool,

/// Model to use in CI (determines which API key secret is shown)
#[arg(long)]
model: Option<String>,
},

/// Show review history and recurring patterns
Expand Down Expand Up @@ -170,8 +174,9 @@ async fn main() -> Result<()> {
dry_run,
uninstall,
ci,
model,
} => {
run_init(force, hooks_only, dry_run, uninstall, ci)?;
run_init(force, hooks_only, dry_run, uninstall, ci, model.as_deref())?;
}

Commands::History { patterns, clear } => {
Expand Down Expand Up @@ -436,15 +441,27 @@ async fn run_review(
Ok(())
}

fn run_init(force: bool, hooks_only: bool, dry_run: bool, uninstall: bool, ci: bool) -> Result<()> {
/// Returns (secret_env_var, model_name) for the given model string.
fn ci_secret_and_model(model: Option<&str>) -> (&'static str, String) {
match model {
Some(m) if m.starts_with("gpt") || m.starts_with("o1") || m.starts_with("o3") || m.starts_with("o4") => ("OPENAI_API_KEY", m.to_string()),
Some(m) if m.starts_with("gemini") => ("GEMINI_API_KEY", m.to_string()),
Some(m) if m.starts_with("claude") => ("ANTHROPIC_API_KEY", m.to_string()),
Some(m) => ("ANTHROPIC_API_KEY", m.to_string()), // Ollama/local — remind about key anyway
None => ("ANTHROPIC_API_KEY", "claude-sonnet-4-6".to_string()),
}
}

fn run_init(force: bool, hooks_only: bool, dry_run: bool, uninstall: bool, ci: bool, ci_model: Option<&str>) -> Result<()> {
let repo_root = find_git_root(&std::env::current_dir()?)?;

if ci {
let (secret_name, model_name) = ci_secret_and_model(ci_model);
let workflows_dir = repo_root.join(".github/workflows");
std::fs::create_dir_all(&workflows_dir)?;
let yml_path = workflows_dir.join("crev.yml");
let existed = yml_path.exists();
std::fs::write(&yml_path, ci_workflow_content())?;
std::fs::write(&yml_path, ci_workflow_content(&model_name, secret_name))?;
if existed {
println!("Updated {}", yml_path.display());
} else {
Expand All @@ -453,7 +470,7 @@ fn run_init(force: bool, hooks_only: bool, dry_run: bool, uninstall: bool, ci: b
println!();
println!("Add your API key as a repository secret:");
println!(" Repo → Settings → Secrets and variables → Actions → New repository secret");
println!(" Name: ANTHROPIC_API_KEY (or OPENAI_API_KEY / GEMINI_API_KEY)");
println!(" Name: {}", secret_name);
println!();
println!("Then commit and push:");
println!(" git add .github/workflows/crev.yml");
Expand Down Expand Up @@ -553,15 +570,16 @@ fn run_update() -> Result<()> {
Ok(())
}

fn ci_workflow_content() -> &'static str {
fn ci_workflow_content(model: &str, secret_name: &str) -> String {
// Note: ${{ }} expressions are GitHub Actions syntax — they stay as-is in the output.
r#"# Save as .github/workflows/crev.yml
# Required secret: set one of ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY
# in repo Settings → Secrets and variables → Actions, then update the env + --model below.
// __MODEL__ and __SECRET__ are substituted at generation time.
let template = r#"# .github/workflows/crev.yml — generated by `crev init --ci`
# Required secret: __SECRET__ (repo Settings → Secrets and variables → Actions)
#
# Triggers:
# - Automatically on PR open
# - On demand: comment '/crev' on any PR to re-run
# Note: the workflow runs for every new comment; the job-level `if` filters to '/crev' only.
name: crev code review
on:
pull_request:
Expand All @@ -579,7 +597,16 @@ jobs:
github.event.comment.body == '/crev')
permissions:
pull-requests: write
contents: read
steps:
- name: Acknowledge /crev comment
if: github.event_name == 'issue_comment'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
--method POST -f content=eyes

- name: Resolve PR metadata
id: pr
env:
Expand Down Expand Up @@ -612,11 +639,11 @@ jobs:

- name: Run crev review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
__SECRET__: ${{ secrets.__SECRET__ }}
run: |
crev review \
--commits ${{ steps.pr.outputs.base }}..${{ steps.pr.outputs.head }} \
--model claude-sonnet-4-6 \
--model __MODEL__ \
--json > findings.json

- name: Post findings to PR
Expand Down Expand Up @@ -713,7 +740,10 @@ jobs:
print(f'Blocking merge: {len(high)} HIGH finding(s)')
sys.exit(1)
"
"#
"#;
template
.replace("__MODEL__", model)
.replace("__SECRET__", secret_name)
}

fn run_history(patterns: bool, clear: bool) -> Result<()> {
Expand Down