diff --git a/docs/context.md b/docs/context.md index 1af7ead..23844e2 100644 --- a/docs/context.md +++ b/docs/context.md @@ -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: diff --git a/src/main.rs b/src/main.rs index a2d6d86..12b84a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, }, /// Show review history and recurring patterns @@ -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 } => { @@ -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 { @@ -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"); @@ -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: @@ -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: @@ -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 @@ -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<()> {