# Gemini CLI on GitHub and in CI
You can have Gemini read every pull request your team opens, leave a structured review, and never once open a browser tab. That is the promise of running Gemini CLI inside GitHub Actions: the same agentic CLI you use locally, now triggered by repository events and bounded by the guardrails you set.
This lesson is about wiring that up properly. Not a demo that posts "looks good to me," but a reviewer that runs in CI, respects least privilege, and fails loudly when something is wrong.
You already know Gemini Code Assist as the IDE companion that completes code and answers questions in your editor. That lives where a human is typing.
Gemini CLI is different in one way that matters here: it is a headless, scriptable agent. It reads files, runs tools, and produces output from a command line. Anything that can run a shell, including a CI runner, can run it. That is exactly what GitHub Actions gives you: an ephemeral Linux box that spins up on an event like pull_request.
So the architecture is simple. A PR opens. GitHub starts a runner. The runner installs Gemini CLI, hands it the diff and a prompt, and the CLI calls the Gemini APIAPI (Flash or Pro depending on how heavy the job is) and writes a review back to the PR.
Google ships a maintained GitHub Action so you do not script the install yourself. The canonical setup lives in the google-github-actions/run-gemini-cli repository. It wraps the CLI, handles auth, and exposes the prompt and tools as workflow inputs.
You authenticate one of two ways:
For a team repo, prefer the second. For a weekend project, the APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition → key is fine.
Here is a complete, runnable workflow. It triggers when a PR is opened or updated, runs Gemini CLI with a focused review prompt, and is locked down with the permissions and concurrency settings I will explain right after.
name: Gemini PR Review
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
concurrency:
group: gemini-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Gemini code review
uses: google-github-actions/run-gemini-cli@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
gemini_api_key: ${{ secrets.GEMINI_API_KEY }}
prompt: |
Review the diff for this pull request. Focus only on:
correctness bugs, security issues, and missing error handling.
Skip style nits. For each finding, cite the file and line.
If you find nothing serious, say so in one sentence.That is the whole thing. The run-gemini-cli action does the heavy lifting: it reads the PR context, runs the model against your prompt, and posts the result as a comment using the GITHUB_TOKEN.
A few deliberate choices in that file are worth slowing down for.
fetch-depth: 0By default actions/checkout does a shallow clone with only the latest commit. A reviewer that cannot see the diff against the base branch is useless. Setting fetch-depth: 0 pulls the full history so the CLI can compute and reason over the actual changes.
Notice the prompt names exactly three categories and explicitly tells the model to skip style nits. This is the single biggest lever on review quality. A vague prompt ("review this PR") produces noisy, low-trust comments that your team will mute within a week. A scoped prompt produces signal.
Treat the prompt like a code review checklist you would hand a new hire. Name what counts, name what to ignore, and demand citations so a human can verify each claim.
A model that can comment on PRs is mostly harmless. A model that can run arbitrary tools, push commits, or read your secrets is not. CI is exactly where a sloppy setup turns into an incident. Here are the guardrails that matter, in priority order.
The permissions block in the workflow is your first wall. The default GITHUB_TOKEN in many orgs is far more powerful than a reviewer needs.
permissions:
contents: read
pull-requests: writecontents: read lets the action read the code. pull-requests: write lets it comment. Nothing else. The job cannot push to branches, cannot edit workflows, cannot touch releases. If a prompt injection in a malicious PR tried to get the agent to do more, the tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.View full definition → simply does not have the scope.
This is the one that bites teams. The pull_request event from a forked repository runs with a read-only tokentokenA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.View full definition → and no access to secrets, by design, so an attacker cannot open a PR that exfiltrates your GEMINI_API_KEY.
There is a tempting alternative, pull_request_target, which runs in the context of the base repo and does have secrets. Do not reachreachThe number of unique people exposed to your message in a given period. Unlike impressions, reach counts each person once, no matter how often they see it.View full definition → for it casually to "fix" reviews on forks. Running untrusted PR code with access to your secrets is the classic GitHub Actions footgun. If you must support forks, gate the job behind a manual labeled trigger so a maintainer reviews the diff before any privileged run.
Choose your model tier on purpose. For most PR review work, Gemini Flash is the right call: fast, cheap, and more than capable of catching correctness and security issues in a diff. Reserve Gemini Pro for jobs that need deep multi-file reasoning, like architectural review of a large feature branch.
Combine that with timeout-minutes on the job and cancel-in-progress concurrency (so a force-push does not stack three reviews on one PR) and your spend stays predictable.
Keep the review as a comment, not a required status check that blocks merges, at least at first. An LLMLLMA Large Language Model is an AI system trained on vast text data to predict and generate language, enabling tasks like writing, summarizing, and answering questions.View full definition → reviewer is an extra pair of eyes, not a gate. If you later promote it to a blocking check, scope that check narrowly (for example, only fail on findings the model labels as security-critical) and keep a human override path.
PR review is the obvious first job, but the same action handles more.
Point a scheduled workflow at your open issues and ask the CLI to label and prioritize them. The prompt does the classification, and issues: write permission lets it apply labels. This is genuinely useful on a busy repo where untriaged issues pile up overnight.
You can trigger the action on issue_comment and have it respond when someone writes @gemini-cli followed by a question. A contributor asks "why does this test fail on Windows?" in a comment, the workflow fires, and Gemini answers inline with context from the repo.
The pattern is the same every time: an event triggers the runner, a scoped prompt plus the right permissions defines what the agent may do, and the output goes back to GitHub. Once you internalize that loop, you can build any of these in an afternoon.
Knowledge check
1. What is the key characteristic of Gemini CLI that makes it suitable for running inside GitHub Actions?
2. According to the lesson, where does the canonical, Google-maintained setup for running Gemini CLI in CI live?
3. In the Gemini model family, what typically distinguishes Flash from Pro when choosing one for a CI job?
4. Select ALL correct answers about authenticating Gemini CLI within the GitHub Action.
Sélectionnez toutes les réponses correctes.
5. Select ALL correct answers describing the architecture of running Gemini CLI as a PR reviewer in CI.
Sélectionnez toutes les réponses correctes.
A repo-wide prompt is good. A repo-wide prompt that knows your conventions is better. Gemini CLI reads a GEMINI.md file from your repository as standing context, the same way it does when you run it locally. Drop one at the root and the CI reviewer inherits your house rules without you stuffing them into the workflow YAML.
# Project review guidelines
- This is a TypeScript monorepo. Flag any use of `any`.
- All database calls must go through `src/db/client.ts`. Flag direct queries.
- Public API changes require a changeset file. Flag PRs that add endpoints without one.
- Never suggest disabling ESLint rules to make a check pass.Now your review prompt can stay short and generic, because the project-specific knowledge lives in version control next to the code it governs. When conventions change, you edit GEMINI.md in a normal PR, and the reviewer that reviews that PR is already following the new rules.
This separation matters: the workflow defines *capability* (what the agent is allowed to do), and GEMINI.md defines *policy* (what good looks like in this codebase). Keep them apart.
One workflow habit will save you a lot of failed Actions runs: develop the prompt locally before you commit it. Run Gemini CLI on your own machine against a real branch first.
git checkout feature/payment-retry
gemini -p "Review the diff against main. Focus on correctness, \
security, and error handling. Cite file and line for each finding."Iterate on the wording until the output is genuinely useful. Only then paste that prompt into the workflow. CI is a slow, awkward place to debug prompt phrasing because every change is a commit and a multi-minute run. Your laptop is instant.
google-github-actions/run-gemini-cli into a pull_request workflow rather than scripting the CLI install yourself, and prefer Vertex AI with Workload Identity Federation over a long-lived APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition → key for team repos.contents: read and pull-requests: write, never reachreach for pull_request_target