Review GitHub Pull Requests with Gemini

Create an automated workflow that reviews pull requests using GitHub actions

Introduction

This project has been archived

We're no longer updating this project - we recommend checking out our active projects instead!

If you've already completed this project, don't worry! Your documentation is still available for you to check out and share.

If you have any questions, leave a question in our Discord and we'll get back to you ASAP!

⚡️ 30 Second Summary

Code reviews for pull requests are one of the biggest bottlenecks in software development. Instead of waiting for another person to review your code, we're increasingly seeing the use of AI as an initial code reviewer. But how do we actually set this up?

In this project, you'll build a GitHub Actions workflow that automatically reviews every pull request with Gemini, posting helpful feedback as a PR comment before your team even sees it.

What you'll build

You'll build a Python script that will ensure pull requests in GitHub are reviewed by Gemini. Gemini will then leave comments directly on the pull request.

By the end of this project, you'll have:

  • 🤖 A Python script that sends code diffs to the Gemini API and formats review feedback.
  • 🔐 Secure API key management using GitHub Secrets.
  • 🔄 An automated workflow that triggers AI reviews on every pull request.
  • 💬 PR comments with AI-generated code review insights posted automatically.
  • 💎 Secret Mission: Auto-label PRs based on review severity (critical, warning, clean).

Are there any prerequisites?

This project is part of the CI/CD GitHub Workflow series.

It is strongly recommended that you complete Project 1 (Your First GitHub Actions Workflow) first.

Not sure if this project is right for you? Check if it matches your goals

If you're up for a bit of a challenge, quiz yourself on the key concepts up ahead in this project.

This project is part of a series:

  1. Part 1: Your First GitHub Actions Workflow
  2. Part 2: You are here!
  3. Part 3: Increase Test Coverage with AI
  4. Part 4: Coming soon

Access Gemini

Gemini is an essential ingredient in today's project, and right now we don't have access to it. Time to get the key!

In this step, you're going to:

✔️ I've completed Part 1

Epic!

Let's run through a quick checklist as a reminder...just mentally check you have the following:

  • Your forked ai-cicd-github repository on GitHub
  • The project cloned locally in Cursor
  • A working CI workflow in .github/workflows/ci.yml

All set? Excellent!

Next question...

ⓧ I need to catch up

No worries!

Let's get you up to speed with what we did in Part 1: Your First GitHub Actions Workflow.

First, we'll set up the starter repo

Wait - what is a "starter repo"?

This starter GitHub repo is pre-written code for ai-cicd-github. To use this code, you'll need a GitHub account.

So the first big question is....do you already have a GitHub account?

✔️ Yep, I have a GitHub account

You're all set!

Venture onwards brave traveller.

ⓧ Nope, I need to create one

  • Head to github.com and click Sign up.
  • Sign up with Google, Apple, or fill in the form with your email, password, and username.
  • Click Create account and verify your email address.

Great, now you have a GitHub account! Let's fork the starter repo below.

  • Click the green Create fork button.
  • Click the green <> Code button
  • Look for the copy icon, and copy the URL.

Nice work! Now we need to take this URL and use it to get the code for our app locally.

We'll do this using Cursor.

Do you already have Cursor on your machine?

I already have Cursor downloaded

  • Open Cursor from your Applications folder (Mac) or Start menu (Windows).
  • Wait for Cursor to fully launch.
  • If prompted to update, go ahead and update to the latest version.

Onwards! Let's login.

I don't have Cursor yet

🍎 macOS
  • Download the macOS installer
Not sure what to choose?

On a Mac you may need to know your chip architecture (e.g., Apple Silicon, Intel) to select the correct version.

You can find this by selecting the Apple Menu > About This Mac.

🖼️ Windows
  • Download the Windows installer.
  • Once the download is complete, open your Downloads folder.
  • Click the CursorUserSetup...exe file.
  • You might see a security warning since it's an app downloaded from the internet. This is normal!
  • Select Run to proceed.
  • The setup wizard will start.
  • Accept the license agreement.
  • Select Next.
  • Choose the destination location for the installation. The default is usually fine.
  • Select Next.
  • Choose the Start Menu folder. Again, the default is fine.
  • Select Next.
  • In the Select Additional Tasks screen, we recommend keeping the default options checked:
    • Register Cursor as an editor for supported file types
    • Add to PATH
  • Select Next.
  • You're now ready to install.
  • Select Install.
🍎 macOS
  • Once the download is complete, open your Downloads folder.
  • Click the Cursor .dmg file.
  • Then, drag the Cursor application into your Applications folder to complete the installation.
  • Open your Applications folder.
  • Click on the Cursor icon to open it.
  • You might see a security warning since it's an app downloaded from the internet. This is normal!
  • Select Open to proceed.

Nice! You've successfully installed Cursor.

🖼️ Windows
  • Once the installation is complete, ensure Launch Cursor is checked.
  • Select Finish.

Nice! You've successfully installed Cursor.

I'm already logged in

Awesome, you're all set to continue!

I already have a Cursor account

Fantastic! This will be speedy.

  • Select Log In.

Your browser will open to the Cursor login page.

  • Log in to your existing Cursor account.

You should see a Welcome to Cursor page.

Awesome! You've logged in and are ready to go.

New to Cursor!

  • When Cursor launches for the first time, you'll see a welcome screen.
  • Select Sign up.
  • Your browser will open to the Cursor login page.
  • Select Sign up at the bottom of the screen to create a new account.
  • Create a new account using your email or a social provider like Google or GitHub.
  • Let's fill in your details to create the account!
  • You'll need to verify your email address.
  • Check your inbox for an email from Cursor.
  • Enter the verification code.
  • After creating your account, Cursor will ask if it can log you into its desktop app.
  • Select Yes, Log In to connect your account to the Cursor desktop app.
  • Cursor will now guide you through some setup instructions. This should only take a few minutes!
  • Select skip and continue for importing settings from VS Code (although you're welcome to import them if you use already use VS Code!)
  • Next, customize your theme.
  • Select Continue.
  • Set your preferred keybindings. VS Code is the default.
  • Select Continue.
  • You'll also see Cursor's Data Sharing agreement. By default, Cursor learns from your code. You can disable this by enabling Privacy mode via Cursor settings.
  • Check the box to proceed.
  • Select Continue.
  • Finally, review your settings.
  • Select Continue.

Awesome! You've logged in and are ready to go.

With Cursor ready and waiting, it's time for us to clone our GitHub repo.

  • Press Ctrl/Cmd + Shift + P to open the search bar
  • Search and select Git: Clone
  • Paste the forked repository URL and press Enter.
  • Select a folder to save your project (e.g. Desktop if you're looking for somewhere easy to find!)
  • Click Select as Repository Destination.
  • Click Open when Cursor asks if you'd like to open the cloned repository.

Getting a 'Git not found' error?

That's totally okay! This just means you don't have Git installed on your computer.

  • Head over to git-scm.com/downloads.
  • Download and run the installer for your operating system.
  • Once installed, restart your IDE and try again.

Still having issues? Troubleshoot here or share your error with the NextWork community!

Our project is here! Fantastic. Let's run it!

To run our app, we'll create a virtual environment with Python.

What is a virtual environment

A virtual environment is a self-contained directory that holds a specific packages you might need to install for a project, keeping them separate from other projects.

Create a Python virtual environment

  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Enter:
python -m venv venv

What is 'venv'?

venv is simply short for virtual environment. This command creates a virtual environment named venv in your current directory.

  • Awesome! We've just created a virtual envionment!
  • We can confirm this worked by looking for a new folder named venv/ in your project directory.
  • You can check in the side pannel in Cursor, or by running the following command in your terminal:
    • For macOS/Linux, use ls, and for Windows, use dir.
  • You should see venv in the output.

Help! I'm getting 'Python not found'

No worries, let's troubleshoot!

  • "python not found": Download from python.org and check "Add to PATH" during installation. Then restart Cursor.
  • "python3" works but "python" doesn't: Try python3 -m venv venv instead.

Get help with Python setup or share your error with the NextWork community!

  • With your virtual environment all set up, we can now activate it!
  • Run the following code:

🍎 macOS/Linux

source venv/bin/activate

🖼️ Windows

venv\Scripts\activate
"Running scripts is disabled" error?

Open PowerShell as Administrator and run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser. Then try again.

  • You should see (venv) appear at the beginning of your terminal prompt.
  • Congratulations! You are now working your very own virtual environment. Now let's install our dependencies.

Why do we need to install dependencies?

Think of your virtual environment like a whole new world.

Right now, there's basically nothing in it, so we need to install all the necessary dependencies for this project.

That's the beauty of virtual environments... we install them here so they don't mess up any other projects we have.

  • Install dependencies by running this command:
pip install -r requirements.txt
  • Nice! Requirements added.
  • Next, we'll add some tests to our app - we'll write them manually now so that in our GitHub Actions later, we can set them up to run automatically.

Create a practice test

What practice test are we creating?

We're writing some basic code that takes two numbers, multiplies them, and returns the answer.

This makes it super easy for us to verify if a test passes or fails!

We'll call it the multiply function.

  • Open app.py
  • Add a multiply function:
def multiply(a: int, b: int) -> int:
    """Multiply two numbers together."""
    return a * b
  • Open tests/test_app.py
  • Add the following tests for multiply:
class TestMultiply:
    """Tests for the multiply function."""

    def test_multiply_positive_numbers(self):
        assert multiply(3, 4) == 12

    def test_multiply_by_zero(self):
        assert multiply(5, 0) == 0

    def test_multiply_negative_numbers(self):
        assert multiply(-2, 3) == -6
  • Don't forget to update the import at the top of the file to include multiply:
from app import add, is_even, reverse_string, multiply
  • Save your file.
  • So far so good. Let's verify if our tests pass!
  • Run the following command:
pytest -v

What does the -v flag do?

The -v flag stands for "verbose". It makes pytest show each test name and its result individually, instead of just showing dots for passes and F's for failures.

Woohoo! Passing tests! Last bit, then we're all caught up with Part 1...

Create the CI workflow

What is a CI workflow?

CI stands for Continous Automation. Instead of manually running tests like we did just before, a CI workflow automates the process of checking your code for errors and ensuring tests pass.

💡How often does this run?

Every time a change to your code base is pushed or a pull request is made to the main branch. We're aiming for zero errors pushed to production.

  • Open the workflows folder inside .github.
  • Right-click → New file... → Name it ci.yml.
  • Enter:
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run tests
        run: pytest -v

Don't freak out!

It's not that bad - slow down and read it carefully.

Look at the steps section first. It's simple!

  1. Checkout code: Fetches your repository's code.
  2. Set up Python: Configures your Python environment, specifically version 3.11.
  3. Install dependencies: Installs project dependencies listed in requirements.txt using pip.
  4. Run tests: Executes your project's tests using pytest with verbose output.
  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Commit and push everything:
git add .
git commit -m "Add multiply function, tests, and CI workflow"
git push origin main

Help I'm stuck!

Honestly, Git can be really fiddly sometimes. Let's see what it is...

  • "Permission denied" or "Authentication failed": Go to github.com/settings/tokens and generate a Personal Access Token (classic) with repo scope. Use this token as your password when Git prompts.
  • "Remote rejected": Make sure you're pushing to your forked repo. Check with git remote -v.

Ask for help or share your error with the NextWork community!

  • Go to Actions tab on your GitHub repo and enable workflows if prompted.
  • Your code is now on GitHub with a working CI pipeline.

And you're officially all caught up on Part 1.

Continue below.

Get Your Gemini API Key

Let's get an API key from Google AI Studio. The free tier gives you plenty of requests to build and test your review system.

What is Gemini?

Gemini is Google's large language model.

💡 ...and why do we need an API key?

An API key is a unique identifier that authenticates your requests to a service. Think of it like a password that tells Google "this request is coming from an authorized user." When you call the Gemini API, you include this key so Google knows who's making the request and can track usage.

✔️ I'm already logged in

Excellent! You'll know that you're logged in if you see your account in the bottom left of the screen:

ⓧ Nope, not logged in!

If you're not logged in, you'll see the Google AI Studio home page.

  • Click Get started in the top right.

✔️ I have a Google account

  • Click on your existing Google account to sign in, or enter your existing email.
  • Follow the login flow to login successfully.

ⓧ I need a Google account

No worries - we'll create a Google account now.

  • Select Create account.
  • Select For my personal use.
  • Following the sign up flow, enter in your name.
  • Enter your birthday and gender information - if you're not comfortable, you can make it up!
  • Choose an awesome username.
  • Enter a strong password of your choosing.
  • Verify your account by scanning the QR code with your phone.
  • On your phone, you may have to verify your phone number by sending a SMS.
  • Once your phone number is verified, return to your original device.
  • Add a recovery email if you like. This is optional, so you can also skip it.
  • Review your account info.
  • Select Next.
  • Agree to the terms and conditions.
  • You've made it! You now have a Google Account.
✔️ Account created successfully

Excellent! You'll be able to use your Google account for many different tools and projects - not just this project.

Onwards!

ⓧ What is going on...

If you're having trouble:

  • Make sure you're using a valid email address.
  • Check your email for a verification link if prompted.
  • Try using a different browser if the form isn't working.
Still stuck?

Get help with your error or share your error with the NextWork community!

Welcome to the Google AI Studio dashboard!

  • In the left sidebar, click Get API key.
  • In the top right, click Create API key.
  • A dialog will appear to create your API key.
  • In the name field, enter:
[[YOUR_NAME="enter-your-name-here"]]-[[PROJECT_NAME="enter-your-project-name-here"]] 
  • Click on the Choose an imported project dropdown.
  • Select Create project.
  • In the project name field, enter:
NextWork-[[PROJECT_NAME="enter-your-project-name-here"]] 

This helps you identify what you created this API key for later!

  • Click Create project.
  • Click Create key.
  • Nice! Our API key is all set up and ready to go.
  • Click the Copy icon next to your key to copy it.
  • Now paste your API key somewhere safe on your own laptop - you'll use it later!

Keep your API key secure!

Don't share your API key publicly or commit it to version control. Google has a generous free tier, but if someone else gets hold of your API key, they could drain your account of free credits or even rack up charges!

Add the Gemini dependency to your project

  • Open requirements.txt in Cursor.
  • Add the Gemini SDK package:
google-genai>=1.0.0
  • Your requirements.txt should now look like:
# Production dependencies
google-genai>=1.0.0

# Development/testing dependencies
pytest>=7.0.0
build>=1.0.0
  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If you don't, activate it:

🍎 macOS/Linux

source venv/bin/activate

🖼️ Windows

venv\Scripts\activate
  • Install the new dependency:
pip install -r requirements.txt
  • When it works successfully, you'll see a message in your terminal saying The package installed successfully.
  • Let's build the script.

Something went wrong!

It happens - stay cool as a cucumber. We can fix it.

  • "pip not found": Make sure your virtual environment is activated (you should see (venv) in your terminal).
  • "Could not find a version that satisfies the requirement": Check you typed the package name correctly: google-genai (with a hyphen, not underscore).
  • Network errors: Check your internet connection and try again.

Get help with pip or share your error with the NextWork community!

Generate the AI Review Script

We have access to Gemini! Hurrah!

Now we need to write the actual Python code that will look for differences in our code and send those differences to Gemini.

In this step, you're going to:

  • Build a Python script that sends code diffs to Gemini for review.
  • Make sure we can run it from the terminal.
  • Make sure we can pass it .txt files to review.

What the heck is 'code diff'?

A diff (short for "difference") is a tool's output that shows the changes between two files or sets of data. It highlights what has been added, deleted, or modified.

Create the empty file

First, let's create the Python script that calls Gemini to review code diffs.

  • Open the scripts folder in your project.
  • Right-click → New file... → Name it ai_review.py.

You'll build this script piece by piece, so take your time to really understand what you're doing as you go.

Add Imports and Gemini API

  • We'll start by adding our imports and our beloved Gemini API configuration.
  • Copy and paste this at the top of your file:
from google import genai
import sys

client = genai.Client()

Can you spot the environment variable?

An environment variable is essential for keeping big secrets (like your API key) safe.

Without environment variable, anytime we push code to GitHub anyone could see (and steal) our API key. YIKES.

Using an environment variable means your secret lives in your system's environment, not in the file. In this project, we use environment variables to store our Gemini API key:

  • genai.Client(): Creates a client that connects to the Gemini API. It automatically reads your GEMINI_API_KEY environment variable, so you don't need to pass the key manually.

Write the review function

Next, we'll add a review function to ai_review.py below the imports.

This function sends a code diff to Gemini and returns the AI's review.

Fill in the blanks!

This next code block might look a little different...

The highlighted fields are yours to fill in (if you can).

Read each comment to understand what that line should do, then type your answer into the field. You've got this!

# Define a function that takes a code diff as input
def [[YOUR_FUNCTION_NAME="name for reviewing code"]]([[PASS_IN="what are you passing in?"]]):

    # Write a multi-line f-string prompt that includes {diff_text}
    # Tell Gemini to act as a code reviewer and focus on security, bugs, performance
    prompt = [[YOUR_PROMPT="an f-string prompt that includes {diff_text} and asks Gemini to review for security, bugs, performance"]]

    # Send the prompt to the model and get a response
    response = client.models.generate_content(
        model="[[YOUR_MODEL_NAME="which Gemini model should you use?"]]", contents=[[YOUR_INPUT="what are you passing into the model?"]]
    )

    # Return just the text from the response
    return response.[[YOUR_PROPERTY="what property contains the review text?"]]
  • Add this function at the bottom of your ai_review.py file.

✔️ I've filled it in!

Excellent work!

Now, it's not that we don't trust you... but we do HIGHLY recommend that you check the other tab to compare your version.

Did you get it right?

Let us know in the NextWork community! We'd love to celebrate.

💡 I want a hint!

Here's some python syntax that you'll need to use...

Note

  • f"""...""": You used f-strings in Step 1 (f"Hello {name}"). Triple quotes let the string span multiple lines, which is what you need for writing an AI prompt.
  • client.models.generate_content(model=..., contents=...): Sends your prompt to a specific Gemini model and returns a response object. The response has a .text property containing the AI's answer.

✔️ Aha...I got it!

Yes! (still check your work though in the other tab).

Show me the answer!

def review_code(diff_text):
    """Send a code diff to Gemini for review."""
    prompt = f"""You are an expert code reviewer. Review the following code diff and provide feedback.

Focus on:
1. Security vulnerabilities
2. Bug risks
3. Performance issues
4. Best practice violations

For each issue found, provide:
- Severity: HIGH / MEDIUM / LOW
- Description of the issue
- Suggested fix

If the code looks good, say so.

Code diff to review:

{diff_text}


Provide your review in a clear, structured format."""

    response = client.models.generate_content(
        model="gemini-2.5-flash", contents=prompt
    )
    return response.text
What does this code do?
  • f"""...""": The {diff_text} part gets replaced with whatever diff you pass in. Your prompt can be different from the example, as long as it tells Gemini to review the code and include the diff.
  • client.models.generate_content(model=..., contents=...): Sends your prompt to the specified Gemini model and waits for a response.
  • response.text: Extracts just the text from Gemini's response object.

Pause. Do you really understand what you just created?

It's okay to go back and double check....

Make sure we can run it from the terminal

Okay, we have a script that sends a code diff to Gemini and returns the AI's review.

What's missing?

Well, how the heck do we run this?! We need to make sure we can run our awesome code from the terminal. Here's how it works...

  • Add this block to the bottom of ai_review.py (yes, it's another fill-in-the-blanks):
# Only run this code when the script is executed directly
if [[YOUR_CONDITION="what special variable equals '__main__' when run directly?"]] == "__main__":

    # Check if a filename was passed as a command-line argument
    if len([[YOUR_ARGS_LIST="what list holds command-line arguments?"]]) > 1:

        # Get the filename from sys.argv and read the file
        diff_file = sys.argv[1]
        with open(diff_file, "r") as f:
            diff_content = f.read()

    # If no filename was passed, read from standard input
    else:
        diff_content = [[YOUR_STDIN="how do you read all piped input?"]]

    # Call the review function and print the result
    review = review_code([[YOUR_INPUT="what variable holds the diff text?"]])
    print(review)
  • Add this function at the bottom of your ai_review.py file.

Wtf is going on

The goal of this code is that when we run the following command in our terminal, it will run our script:

python ai_review.py diff.txt

Does that help?

I think I've got it

Epic!

Please don't take offence - but it's 100% worth checking the other tab to compare your version.

Give me the answer!

Here's the full block:

if __name__ == "__main__":
    if len(sys.argv) > 1:
        diff_file = sys.argv[1]
        with open(diff_file, "r") as f:
            diff_content = f.read()
    else:
        diff_content = sys.stdin.read()

    review = review_code(diff_content)
    print(review)

How do we get it to run from the command line?

  • if __name__ == "__main__": This line just says, "Hey, only run the stuff inside this block if I'm the main program being started, not if I'm just a tool another program is borrowing.

Okay, so how are we passing it the text it needs to review?

  • if len(sys.argv) > 1: When you type the command into your terminal, everything after python my_script.py gets put into a list called sys.argv. This line looks for the text we're passing it by looking at that list.

Right! But once it has the text, what does it do with it?

  • review = review_code(diff_content): This takes the text it just got and hands it over to a special function called review_code. This function then does its magic to "review" the text and gives back a result, which is stored in a box called review.

And finally, how do we see what the review function came up with?

  • print(review): This simply displays the result of the review on your screen, right there in your terminal.
  • Save the file. (Ctrl/Cmd + S)
  • Your review_code function should look like this in your editor:
  • Scroll down to the main block at the bottom of the file to check it all matches:

Test your script works

We have our Python script that takes in .txt files, sends them to Gemini, and returns a response - yeehaw!

Shall we check it works?

Before wiring this into GitHub Actions, let's verify the script works on your machine.

In the next step, you're going to:

  • Create a sample diff file.
  • Test the script locally.

Create a sample diff file

  • Right-click the scripts folder
  • Select New file...
  • Name it sample_diff.txt.
  • Paste in this sample diff:
diff --git a/app.py b/app.py
index 1234567..abcdefg 100644
--- a/app.py
+++ b/app.py
@@ -1,5 +1,12 @@
 """Simple utility functions"""

+import sqlite3
+
+def get_user(username):
+    conn = sqlite3.connect("users.db")
+    query = f"SELECT * FROM users WHERE name = '{username}'"
+    return conn.execute(query).fetchone()
+
 def add(a: int, b: int) -> int:
     """Add two numbers together."""
     return a + b

How does this work?

This may look confusing, but it's really quite simple. This piece of code contains a classic SQL injection vulnerability.

💡 What is a SQL injection?

A SQL injection is where instead of inserting a username into a login form, an attacker could input malicious code and harm your app! For example, inserting '; DROP TABLE users; -- instead of a username would delete your database. BIG YIKES.

Let's see if Gemini catches it!

Run your script

Awesome - let's see what happens when you pass your python script the SQL Injection file.

Hopefully it identifies it as an issue!

  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Run the script by pasting the following into your terminal:

🍎 macOS/Linux

GEMINI_API_KEY=[YOUR_GEMINI_API_KEY] python scripts/ai_review.py scripts/sample_diff.txt

🖼️ Windows

set GEMINI_API_KEY=[YOUR_GEMINI_API_KEY]
python scripts/ai_review.py scripts/sample_diff.txt
  • Exciting! Do you see Gemini's structured review mentioning the SQL injection vulnerability?
  • Gemini caught the security flaw.
  • Yay! It works!

That's not what I see!

No worries, let's troubleshoot.

  • "GEMINI_API_KEY not set" or "API key not valid" Double-check you copied the full key from Google AI Studio. Make sure there are no extra spaces.
  • "ModuleNotFoundError: No module named 'google.genai'" Run pip install -r requirements.txt again. Make sure your venv is activated.
  • "Resource exhausted" or "429" You've hit the free tier rate limit. Wait a minute and try again.

Troubleshoot your error in the chat or share your error with the NextWork community!

Now all we need to do is push it to Git, sync up our GitHub actions, and then run it for every PR.

So close.

Push to Git

With your AI review script tested and working locally, it's time to prepare your script for GitHub. To the moon!

Unfortunately, there are two big problems with trying to do this:

  1. You can't commit your API key to GitHub. It's a secret (and very bad practice to share around).
  2. We have no way to connect our super cool Python script with our pull requests - how do we make our script run everytime there's a new PR?

Let's solve both these problems with GitHub Secrets and a GitHub Actions workflow.

In this step, get ready to:

  • Add your Gemini API key as a GitHub Secret.
  • Create a GitHub Actions workflow triggered on pull requests.
  • Push your changes to GitHub.

Set up GitHub Secrets

  • Go to your forked repository on GitHub.
  • Click Settings (the gear icon in the top navigation).

Can't see Settings?

Make sure you're on your forked repo (not the original itsnextwork/ai-cicd-github). Only repo owners can access Settings.

  • In the left sidebar, click Secrets and variables → Actions.
  • Click New repository secret.
  • Set the Name to: GEMINI_API_KEY
  • Paste in your carefully stored Gemini API Key.
  • Click Add secret.

How do GitHub Secrets work?

GitHub Secrets are encrypted environment variables stored in your GitHub repository settings.

The great thing about GitHub Secrets is that they are never exposed in logs or to forked repos. GitHub simply injects the value at runtime and masks it in all output.

Our API Key is now safe in GitHub Secrets so we can use GitHub without sacrificing our security. But there's still one other problem to solve:

How do we make our script run everytime there's a new PR?

Introducing GitHub Actions.

Write the code for your GitHub action

  • Open the .github/workflows folder.
  • Right-click and select New file...
  • Name it pr-review.yml.

So what do we want our GitHub action to do?

  1. First it needs to access our python script
  2. Then it needs to run our python script
  3. Read the PR
  4. Send info to Gemini
  5. Post Gemini's comments on the PR in GitHub
  • Copy and paste the following workflow configuration into your pr-review.yml file:
name: AI Code Review

on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Get PR diff
        run: |
          git diff origin/main...HEAD > pr_diff.txt

      - name: Run AI review
        id: ai-review
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
        run: |
          REVIEW=$(python scripts/ai_review.py pr_diff.txt)
          echo "review<<EOF" >> $GITHUB_OUTPUT
          echo "$REVIEW" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Post review comment
        uses: actions/github-script@v7
        env:
          REVIEW: ${{ steps.ai-review.outputs.review }}
        with:
          script: |
            const review = process.env.REVIEW;

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `## 🤖 AI Code Review\n\n${review}\n\n---\n*Powered by Gemini AI*`
            });

What's going on in this workflow?

  • on: pull_request Triggers when a PR is opened or updated (not on push to main).
  • permissions Explicitly grants write access to PR comments. Without this, the workflow can't post comments.
  • jobs A workflow is composed of one or more jobs. Each job runs independently and can be executed on a different runner (virtual machine). In your workflow, you have one job named 'review'.
  • steps A job is a sequence of steps. Each step can either run a command (like pip install) or run an action (like 'actions/checkout@v4').

Still have questions? Explain this workflow in more detail

  • Save the file. (Ctrl/Cmd + S)

Push your changes

  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Commit and push everything to main:
git add .
git commit -m "Add AI code review script and PR workflow"
git push origin main

Note

If you're EVER stuck - ask the NextWork community. Students like you are already asking questions about this project.

Your workflow is now live on GitHub. Epic!

Next up, let's actually try and run it on GitHub - hopefully it works.

Test Your PR Review Workflow

With your workflow pushed to GitHub, it's time to see how we review PRs in action.

In this step, get ready to:

  • Test your workflow by opening a PR with intentionally bad code.
  • Fix the code and see the review update.

Testing your workflow with a bad PR

Breaking things on purpose!

We're going to open a PR with intentionally insecure code to see if our AI reviewer catches it.

  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Create a new branch:
git checkout -b test/bad-code
  • Create a new file called dangerous.py in your project root:
  • Copy and paste the following code into your dangerous.py file:
import subprocess

def run_command(user_input):
    """Run a shell command from user input."""
    subprocess.call(user_input, shell=True)

API_KEY = "sk-live-abc123def456"

What's wrong with this code?

This code contains critical security issues:

  1. subprocess.call with shell=True and unsanitized user input allows command injection. An attacker could run any system command.
  2. A hardcoded API key in source code means anyone with repo access gets your credentials.
  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Commit and push the branch by running the following commands in your terminal:
git add dangerous.py
git commit -m "Add command execution feature"
git push origin test/bad-code
  • Nice! Your dangerous.py code is successfully pushed to GitHub.
  • Go to your repository on GitHub.
  • Let's create our first pull request!

Do you see...

✔️ I see a 'Compare & pull request' banner

  • Click Compare & pull request.

ⓧ I don't see the banner

No worries! You can create the PR manually:

  • Click the Pull requests tab.
  • Click New pull request.
  • Click the base repository dropdown and select your own branch.
  • Select your branch from the dropdown.
  • Set the compare branch to test/bad-code.
  • You should see the diff with your dangerous code changes.
  • Click Create pull request.
  • It's the big moment of truth! What will happen next?!
  • Have a look on your pull request.
  • Do you see your workflow running?

✔️ Workflow is running

  • SO EXCITING!
  • Wait for the workflow to complete.
  • Once the checks complete, you should see all checks passed.
  • Within a minute or two, you should see a comment from the AI reviewer on your PR, pointing out the security vulnerabilities.
  • Scroll down to see the detailed recommendations for fixing each issue.
  • WOW...

ⓧ Nope, my workflow isn't running

Oh no! Is it any of the following?

  • No workflow appearing: Check the Actions tab. Make sure the pr-review.yml file was pushed to main (not to the test branch).
  • Workflow failed at "Run AI review": Check the logs. The most common issue is the GEMINI_API_KEY secret not being set correctly.
  • Workflow failed at "Post review comment": The permissions block might be missing from your workflow file. Make sure pull-requests: write is included.
  • "Resource not accessible by integration": This means the workflow doesn't have permission to post comments. Add the permissions block at the job level.

Still stuck? You're always welcome to share your error with the NextWork community!

Amazing! It worked!

Now let's see what happens if we feed it code that isn't full of security issues. What will it do then?

Fix the code

Let's see how the review changes when you fix the issues.

How to fix these security issues

The two problems are the shell=True flag (which lets attackers inject commands) and the hardcoded API key. Here's how to fix each one:

  • shlex.split(user_input): Instead of passing raw user input to the shell, this splits the string into a safe list of arguments. Special characters like ; or | can't be exploited because they're treated as literal text, not shell operators.
  • shell=False: Runs the command directly without a shell interpreter, so even if something slips through, there's no shell to interpret dangerous characters.
  • os.environ.get("API_KEY"): You already used this pattern for your Gemini key. Same idea here: read the key from the environment instead of hardcoding it.
  • Update dangerous.py by fixing the two broken lines:
import subprocess
import shlex
import os

def run_command(user_input):
    """Run a shell command safely."""
    args = shlex.split([[YOUR_SPLIT_INPUT="what are you splitting into safe arguments?"]])
    subprocess.call(args, shell=[[YOUR_SHELL_FLAG="should the shell interpret the command?"]])

API_KEY = os.environ.get("[[YOUR_ENV_VAR="name of the environment variable"]]")

Go on! Give it a go...

✔️ I've filled it in

Woohoo! Check the other tab to compare your version.

💡 I want a hint!

Here is a hint!

import subprocess
import shlex
import os

def run_command(user_input):
    """Run a shell command safely."""
    args = shlex.split(user_input)
    subprocess.call(args, shell=False)

API_KEY = os.environ.get("API_KEY")
  • Once your code is all fixed, make sure you save your dangerous.py file.
  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Off we go! Commit and push:
git add dangerous.py
git commit -m "Fix security issues"
git push origin test/bad-code
  • Go back to your PR in GitHub.
  • Refresh your page.
  • The workflow will run again and post a new comment, this time with a much more positive review.
  • The new AI review comment should show a much more positive assessment.

Great success!

You've made a working automated PR review system using GitHub actions and your very own Python script. Congratulations!

Secret mission

Your AI reviewer posts comments, but imagine if it could also triage PRs automatically.

We can use label-based triage with GitHub Labels to prioritize thousands of pull requests per day.

Auto-Label PRs by Severity

Clean Up Your Resources

Clean Up Your Resources

Nice work! Let's clean up the resources we created.

Resources to clean up:

  • Deactivate your Python virtual environment
  • Close your test PR
  • Revoke your Gemini API key (optional)

Deactivate Virtual Environment

  • In your terminal, run the following:
deactivate

Close the test PR

  • Go to your test PR on GitHub.
  • Click Close pull request (without merging).
  • Delete the test/bad-code branch when prompted.

Revoke Gemini API Key

Do I have to revoke my Gemini API key?

Nope! It's totally up to you. The Gemini free tier is quite generous. You're more than welcome to keep it for future projects!

If you don't think you'll use it again anytime soon, then you can revoke it just to have it done.

  • Go to Google AI Studio → Find your key → Click Delete.
  • Let's also remove the GitHub Secret
  • Navigate into GitHub
  • Click into your GitHub Settings
  • Click Secrets and variables → Actions
  • Select delete GEMINI_API_KEY.

That's a wrap!

That's a wrap!

WOOHOOOO! You did it!

Let's recap what you've achieved today:

In this project, you:

  • 🐍 Built a Python script that calls the Gemini API to analyze code diffs.
  • 🔐 Configured GitHub Secrets for secure API key handling in CI/CD.
  • 🤖 Created a GitHub Actions workflow triggered on every pull request.
  • 💬 Posted automated review comments using github-script.
  • 💎 Auto-labeled PRs by AI review severity (Secret Mission).

Want to try the quiz before you go? 💪

High five! We'll see you in the next project. 🙌