Secure a GitHub PR Review Bot
Build a GitHub App that reviews pull requests without permission to merge.
Introduction
30 Second Summary
Automated code reviews can save time. However, a reviewer with the wrong credentials can change the code it was meant to inspect.
In this project, you will create a public GitHub repository with a review identity that can comment on a pull request without gaining merge access. You will also make every approval expire when new code changes what was reviewed.
What You'll Build
You will demo a pull request where the bot can review code while GitHub keeps merge control with a human.
By the end of this project, you'll have:
- A bot-authored review on a public pull request that proves your GitHub App can join the discussion.
- A refused merge response that proves the same credentials cannot merge the code.
- Stale-review protection that removes an approval when a later commit changes the reviewed code.
- Secret Mission: An optional challenge to push your skills further.
Are there any prerequisites?
You need a GitHub account plus git on your laptop. You also need a way to send an authenticated HTTP request.
A public repository keeps this project free.
Before We Start
A review bot is only safe when its identity has fewer permissions than the person who owns the repository. You need an account that can administer the project before you create that restricted identity.
This step prepares your GitHub account for repository administration. It also gives your terminal the tools needed to create commits and send authenticated API requests.
In this step, get ready to:
- Confirm that your GitHub account can create repositories.
- Configure your local Git commit identity.
- Authenticate GitHub CLI for REST API requests.
Prepare your GitHub account
A verified personal account can create repositories. The owner of a personal repository has full administrative control over its settings.
- Visit the GitHub homepage in your browser.
- Choose the tab below that matches what you see.
✔️ I see my profile picture
Your profile picture in the upper-right corner confirms that you are signed in. Continue below to check your email verification.
ⓧ I see a sign-in page
- Go to the GitHub sign-in page.
- Enter your GitHub account details.
- Click Sign in.
- Confirm that your profile picture appears in the upper-right corner.
ⓧ I need an account
- Go to the GitHub sign-up page.
- Follow the prompts to create a personal account.
- Open the verification email from GitHub.
- Click the verification link in the email.
- Confirm that GitHub displays your signed-in profile picture.
- Click your profile picture in the upper-right corner.
- Select Settings.
- Select Emails in the Access section.
- Confirm that your primary email address has completed verification.
- Select Resend verification email if GitHub still offers that action.
- Open the new verification email if you requested one.
- Click the verification link in that email.
Your account is ready to own the public repository. That owner role gives you the administrative control needed for the permission tests later.
Install and configure Git
Git records a name and email address inside every commit. Configuring that identity now keeps the branch history tied to your GitHub account.
- Press Cmd+Space on macOS or the Windows key on Windows.
- Type Terminal on macOS or PowerShell on Windows.
- Press Enter.
- Check whether Git is available by running this command:
git --version
What does this command do?
The command asks Git to print its installed version. A version number confirms that your terminal can find the Git executable.
✔️ I see a Git version
Git is installed. Continue below to confirm the installation before configuring your commit identity.
ⓧ Command not found
macOS
- Install Git with Xcode Command Line Tools by running this command:
xcode-select --install
What does this command do?
The command opens the macOS installer for Xcode Command Line Tools. That package includes Git.
- Approve the installation in the macOS dialog.
- Wait for the installation to finish.
Installer not opening?
Check whether another Xcode Command Line Tools installation is already running. You can also use the official Git installation options for macOS.
Help me install Git on macOS.
Windows
- Install Git through WinGet by running this command in PowerShell:
winget install --id Git.Git -e --source winget
What does this command do?
WinGet downloads the Git for Windows package from its configured source. The installer makes Git available to your computer.
- Complete any installer prompts that appear.
- Close the entire PowerShell window after installation.
- Press the Windows key.
- Type PowerShell.
- Press Enter to start a fresh terminal.
WinGet unavailable?
Use the official Git for Windows installer if your computer cannot run WinGet. Open a fresh PowerShell window after the installer finishes.
Help me install Git on Windows.
- Confirm that the current terminal can find Git by running the version check again:
git --version
What should you see?
You should see Git followed by a version number. This proves that the current terminal can run Git commands.
- Set the author name and email for your future commits by running these commands:
git config --global user.name "[[GIT_NAME="name for your commits"]]"
git config --global user.email [[GIT_EMAIL="email associated with your GitHub account"]]
What do these commands configure?
- The user.name setting stores the author name placed in your commits.
- The user.email setting stores the author email placed in your commits.
- The --global option applies these values to repositories used by your account on this computer.
Before you check, which two identity values do you expect Git to return?
- Read your global Git configuration by running this command:
git config --global --list
What does this check prove?
The command lists settings saved for your local user account. It lets you verify the identity that Git places in future commits.
You should see entries beginning with user.name= and user.email=. The values should match the identity you entered.
Missing your Git identity?
- Run the configuration commands again if either entry is missing.
- Keep quotation marks around a name that contains spaces.
- Check the spelling of the email address if Git shows the wrong value.
Help me fix my Git identity configuration.
Install and authenticate GitHub CLI
GitHub CLI provides an authenticated client for the GitHub REST API. You will use that client to prove what different identities can do.
- Check whether GitHub CLI is available by running this command:
gh --version
What does this command do?
The command asks GitHub CLI to print its installed version. A version number confirms that the gh command is available.
✔️ I see a GitHub CLI version
GitHub CLI is installed. Continue below to confirm it from your current terminal.
ⓧ Command not found
macOS
- Install GitHub CLI through Homebrew by running this command:
brew install gh
What does this command do?
Homebrew downloads the supported GitHub CLI package. It makes the gh command available in your terminal.
Homebrew unavailable?
- Use the official precompiled GitHub CLI installer if your terminal cannot find Homebrew.
- Download the universal macOS installer from the release assets.
- Complete the installer prompts.
Help me install GitHub CLI on macOS.
Windows
- Install GitHub CLI through WinGet by running this command in PowerShell:
winget install --id GitHub.cli --source winget
What does this command do?
WinGet installs the supported GitHub CLI package. The installer updates the system path for new terminal windows.
- Close the entire Windows Terminal window after installation.
- Press the Windows key.
- Type PowerShell.
- Press Enter to start a fresh terminal.
WinGet installation failing?
- Use the official GitHub CLI installer if WinGet is unavailable.
- Download the Windows installer that matches your computer.
- Open a fresh PowerShell window after installation.
Help me install GitHub CLI on Windows.
- Confirm that the current terminal can find GitHub CLI by running the version check again:
gh --version
What should you see?
You should see GitHub CLI followed by a version number. This proves that the current terminal can run gh commands.
The next command starts a browser-based authorization flow. Your GitHub password does not need to be pasted into the terminal or saved in the project.
- Start the GitHub CLI login flow by running this command:
- Choose GitHub.com when the host prompt appears.
- Choose HTTPS when the Git protocol prompt appears.
- Approve the option to authenticate Git with your GitHub credentials.
- Choose the browser-based authentication option.
gh auth login
How does this login work?
GitHub CLI opens a web authorization flow for your signed-in account. After approval, the CLI uses the resulting authentication for GitHub commands.
- Complete the authorization in your browser.
- Return to your terminal after GitHub confirms the authorization.
Before you run the final checks, do you expect the account status and the authenticated API request to succeed?
- Verify the active account and API client by running these commands:
gh auth status
gh api /user
What do these checks prove?
- The first command tests the active account and its authentication state.
- The second command makes an authenticated request for your GitHub user data.
- Together, they prove that your terminal can send authenticated REST API requests.
You should see a successful authentication status followed by JSON containing your GitHub login. That response confirms that the API client is ready.
Authentication check failing?
- Run the login command again if the status check reports no active account.
- Confirm that the browser authorized the same account you checked earlier.
- Check your network connection if the API request cannot reach GitHub.
Help me troubleshoot GitHub CLI authentication.
Your setup checkpoint
GitHub CLI or equivalent authenticated REST API client available.
Signed in account with repository administration permissions.
Git installed and configured with a user name and email.
Your setup is ready. Next, you create the public repository and open the pull request that the review bot must inspect.
Create a Repository and Pull Request
A review identity needs a real change to inspect before you can test its permissions. This isolated project keeps the security experiment away from your other work.
You will create a public GitHub repository with main as its starting branch. You will use Git to send a small change through a separate branch into an open pull request.
In this step, get ready to:
- Create a public repository with main as its default branch.
- Push a committed change from review-bot-test.
- Open pull request #1 against main.
Create and clone the repository
The repository gives every later permission test the same controlled target. Initializing it with a README creates the first commit on the default branch.
- Return to GitHub in your browser.
- Select the + menu in the upper-right corner.
- Click New repository.
- Select your personal account from the Owner dropdown.
- Enter pr-review-bot-demo in the Repository name field.
- Select Public under Choose visibility.
- Turn Add README on.
- Click Create repository.
Your new repository page loads with README.md in the file list. The repository header identifies it as public.
- Confirm that the branch dropdown shows main.
See a different branch name?
- Select the branch dropdown on the repository page.
- Click View all branches.
- Open the dropdown beside the current default branch.
- Click Rename branch.
- Enter main as the new name.
- Click Rename branch.
GitHub supports customized default branch names. This project uses main so every later permission check targets the same branch.
Help me fix the default branch name.
Your safety sandbox is in place. The permission tests now stay separate from your other repositories.
- Click Code above the file list.
- Select HTTPS.
- Copy the repository URL.
- Record the copied URL here: your repository URL.
- Switch back to the terminal from earlier.
- Move to your Desktop by running this command:
cd ~/Desktop
Why move to the Desktop?
This command makes your Desktop the starting location for the clone. You will be able to find the new pr-review-bot-demo folder without searching your computer.
- Clone the public repository by running this command:
git clone [[REPO_URL="your repository URL"]]
What does cloning create?
- The command downloads the repository into a new pr-review-bot-demo folder.
- Git names the GitHub repository connection origin.
- Git checks out the default main branch.
You will see cloning progress followed by a completed prompt. The new folder now contains README.md.
- Enter the cloned folder and confirm its current branch by running these commands:
cd pr-review-bot-demo
git branch --show-current
What did the branch check prove?
- The first command moves your terminal into the local clone.
- The second command prints the branch currently checked out.
You should see main in the output. Your local clone now matches the repository's default branch.
Clone did not complete?
- Check that the recorded repository URL ends with pr-review-bot-demo.git.
- Confirm that the repository page still shows Public.
- Remove any incomplete pr-review-bot-demo folder before repeating the clone command.
Help me troubleshoot the repository clone.
Push a change on a separate branch
A separate branch gives the review bot a proposed change without touching main. The one-line README update makes the pull request easy to inspect.
- Create and switch to review-bot-test by running this command:
git switch -c review-bot-test
What does this command do?
The -c option creates a branch from your current main commit. Git switches your working files to the new branch immediately.
You should see confirmation that Git switched to review-bot-test.
The next command adds one visible line to README.md. Choose the tab that matches your terminal.
macOS
- Append the test line to README.md by running this command:
printf '%s\n' 'Review bot test change.' >> README.md
What does this command do?
The command appends Review bot test change. to the existing README. The append operator preserves the content already in the file.
Windows
- Append the test line to README.md by running this command in PowerShell:
Add-Content -Path .\README.md -Value "Review bot test change."
What does this command do?
PowerShell appends Review bot test change. to the existing README. The original README content remains intact.
- Inspect the branch and changed file by running this command:
git status -sb
How does the short status help?
The short status includes your current branch at the top. It also lists each changed file in a compact format.
You should see review-bot-test on the first line. You should also see README.md marked as modified.
- Record the README change as a commit by running this command:
git commit -a -m "Add review bot test change"
What does the commit capture?
- The -a option stages the modified tracked README.
- The -m option records a short description with the snapshot.
You will see a commit summary naming review-bot-test and reporting one changed file.
Before you push, which local branch do you expect Git to send to GitHub?
- Push the committed branch to GitHub by running this command:
git push -u origin review-bot-test
What does the push establish?
Git sends the commit to a remote branch named review-bot-test. The -u option links your local branch to that remote branch for future synchronization.
You should see output confirming that Git created or updated the remote review-bot-test branch. The output also confirms that tracking is configured.
Branch did not push?
- Check that the terminal is inside the pr-review-bot-demo folder.
- Confirm that your authenticated GitHub account owns pr-review-bot-demo.
- Follow the authentication prompt if Git asks you to authorize the push.
Help me troubleshoot the branch push.
Open pull request #1
A pull request compares a head branch containing proposed work with a base branch that receives the work. This comparison becomes the target for the review bot in the next steps.
Before you open the comparison, which branch do you expect to use as the base branch?
- Return to the pr-review-bot-demo repository page in your browser.
- Click the Pull requests tab.
- Click New pull request.
- Select main from the base branch dropdown.
- Select review-bot-test from the compare branch dropdown.
Why does branch direction matter?
The base branch is the destination for approved changes. The compare branch contains the commit you want reviewed.
This direction keeps main unchanged while the pull request remains open.
The comparison page should show the added README line. That visible diff proves the pushed branch is ahead of main.
- Click Create pull request.
- Enter Add review bot test change in the title field.
- Enter Adds a small change for the review bot permission test. in the description field.
- Click Create pull request.
The pull request page should show #1 with review-bot-test targeting main. Its status remains open for the permission tests ahead.
That is the test target ready. Next, you will register a GitHub App whose permissions allow reviews while withholding contents write access.
Create a Least-Privilege GitHub App
Your open pull request gives the review bot something real to inspect. The next risk is the identity you hand to that bot.
A personal access token can inherit broad permissions from its owner. This step creates a GitHub App that can write pull request reviews. Its repository contents permission stays read-only.
In this step, get ready to:
- Register a GitHub App with pull request write access and read-only contents access.
- Install the app only on the `pr-review-bot-demo` repository.
- Prove the installation token carries the intended permission boundary.
Register the review identity
A GitHub App has its own repository permissions. That separate identity lets you grant review access without handing over your personal account permissions.
Why a GitHub App?
A personal access token follows access granted to a person. A GitHub App installation receives the app's selected permissions for specific repositories.
This boundary gives the review bot enough access to comment on pull requests. Read-only contents access blocks changes to repository contents.
- Switch back to GitHub in the browser from earlier.
- Click your profile picture in the upper-right corner.
- Click Settings.
- Click Developer settings in the left sidebar.
- Click GitHub Apps in the left sidebar.
- Click New GitHub App.
- Enter PR Review Only Bot in the GitHub App name field.
- Paste the URL of the pr-review-bot-demo repository into the Homepage URL field.
Why disable webhooks?
This project sends API requests directly from your terminal. The app does not need to receive webhook events for that workflow.
- Deselect Active in the Webhook section.
- Set Contents under Repository permissions to Read-only.
- Set Pull requests under Repository permissions to Read & write.
- Select Only on this account under Where can this GitHub App be installed?.
- Click Create GitHub App.
Good progress. The review bot now has an identity whose configured contents access stops at reading.
Still on the registration form?
- Confirm that Homepage URL contains the full repository URL.
- Confirm that the webhook Active checkbox is cleared.
- Check that your account is allowed to register GitHub Apps.
Get help with the registration form.
Create a private key and install the app
Credential handling can feel risky. GitHub downloads the private key to your computer because it only stores the public portion.
Keep the private key outside your local pr-review-bot-demo clone. The key can authenticate as the app.
- Find the Client ID near the top of the app's General settings page.
- Record the client ID here: your GitHub App client ID.
- Scroll to the Private keys section.
- Click Generate a private key.
Your browser downloads a .pem file. This is the private key that signs requests from the app.
- Record the full path to the downloaded key here: path to your downloaded PEM file.
Protect the private key
Never commit the .pem file to the repository. Anyone with this key can authenticate as the app.
- Click Install App in the left sidebar.
- Click Install next to your account.
- Select Only select repositories.
- Open the Select repositories dropdown.
- Select pr-review-bot-demo.
- Click Install.
The installation page now lists only pr-review-bot-demo. The app has no installation access to your other repositories.
Generate a token and verify its permissions
A JSON Web Token proves the app's identity to GitHub. You use that proof to request an installation access token for the installed repository.
The following script signs the JWT locally with your private key. It stores the result in GH_TOKEN without printing the credential.
macOS
- Return to the terminal from earlier.
- Generate the JWT in your current terminal session by pasting this script:
client_id="[[CLIENT_ID="your GitHub App client ID"]]" # Identifies the app that signs this JWT
pem=$( cat "[[PEM_PATH="path to your downloaded PEM file"]]" ) # Loads the private key without printing it
now=$(date +%s)
iat=$((${now} - 60)) # Issues 60 seconds in the past
exp=$((${now} + 600)) # Expires 10 minutes in the future
b64enc() { openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n'; }
header_json='{
"typ":"JWT",
"alg":"RS256"
}'
header=$( echo -n "${header_json}" | b64enc )
payload_json="{
\"iat\":${iat},
\"exp\":${exp},
\"iss\":\"${client_id}\"
}"
payload=$( echo -n "${payload_json}" | b64enc )
header_payload="${header}"."${payload}"
signature=$(
openssl dgst -sha256 -sign <(echo -n "${pem}") \
<(echo -n "${header_payload}") | b64enc
)
JWT="${header_payload}"."${signature}"
export GH_TOKEN="$JWT"
What does this script do?
- The time claims give the JWT a short validity window.
- The RS256 signature proves that the JWT came from the matching private key.
- The final line places the JWT in GH_TOKEN for the next API request.
Windows
- Return to PowerShell from earlier.
- Generate the JWT in your current PowerShell session by pasting this script:
# Identify the app and load its private key
$client_id = "[[CLIENT_ID="your GitHub App client ID"]]"
$private_key_path = "[[PEM_PATH="path to your downloaded PEM file"]]"
# Encode the JWT header
$header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
alg = "RS256"
typ = "JWT"
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
# Add a short validity window and the app identity
$payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-10).ToUnixTimeSeconds()
exp = [System.DateTimeOffset]::UtcNow.AddMinutes(10).ToUnixTimeSeconds()
iss = $client_id
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
# Sign the JWT with the private key
$rsa = [System.Security.Cryptography.RSA]::Create()
$rsa.ImportFromPem((Get-Content $private_key_path -Raw))
$signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes("$header.$payload"), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_')
$jwt = "$header.$payload.$signature"
$env:GH_TOKEN = $jwt
What does this script do?
- The time claims give the JWT a short validity window.
- The RS256 signature proves that the JWT came from the matching private key.
- The final line places the JWT in GH_TOKEN for the next API request.
Before you query the installation, which permission do you expect to remain read-only?
- Retrieve the installed permission list by running this command:
gh api /app/installations --jq '.[0] | {id, repository_selection, permissions: {contents: .permissions.contents, pull_requests: .permissions.pull_requests}}'
What does this check prove?
- A successful response proves GitHub accepted the JWT signed by your private key.
- The repository_selection field reports whether the installation covers every repository or selected repositories.
- The permissions object reports the installed access levels that GitHub enforces.
You should see repository_selection set to selected. You should also see contents set to read.
The pull_requests value should be write. The id value identifies this installation.
Permission request rejected?
- Regenerate the JWT if several minutes passed before you ran the request.
- Confirm that CLIENT_ID matches the app's Client ID.
- Confirm that PEM_PATH points to the private key generated for PR Review Only Bot.
Help me debug the GitHub App JWT request.
- Record the returned installation ID here: your app installation ID.
The JWT authenticates the app itself. The next request exchanges that proof for a token scoped to the installation on pr-review-bot-demo.
macOS
- Replace the JWT in GH_TOKEN with a new installation token by running this command:
export GH_TOKEN="$(gh api --method POST /app/installations/[[INSTALLATION_ID="your app installation ID"]]/access_tokens --jq .token)"
How is the token protected?
The command requests an installation token through the authenticated API client. It stores the secret in the current terminal session without printing it.
Windows
- Replace the JWT in GH_TOKEN with a new installation token by running this command:
$env:GH_TOKEN = gh api --method POST /app/installations/[[INSTALLATION_ID="your app installation ID"]]/access_tokens --jq .token
How is the token protected?
The command requests an installation token through the authenticated API client. It stores the secret in the current PowerShell session without printing it.
Before you make the final request, do you expect the installation token to list every repository in your account?
- Verify the installation token's repository scope by running this command:
gh api /installation/repositories --jq '.repositories[].name'
What does this final request prove?
This endpoint uses the installation token currently stored in GH_TOKEN. A successful response proves the token can authenticate as the installed app.
The returned repository list reveals the token's usable scope. Only the selected repository should be present.
You should see only pr-review-bot-demo in the output. That's the boundary working: the bot has a valid token for one repository with contents kept read-only.
Repository missing from the response?
- Return to the app installation settings to confirm pr-review-bot-demo is selected.
- Generate a fresh installation token after correcting the repository selection.
- Keep the terminal session open so GH_TOKEN remains available.
Help me verify the GitHub App installation token.
Your least-privilege review identity is ready. Next, you'll use its installation token to post a review comment on pull request #1.
Post a Review as the App
Your GitHub App is installed on pr-review-bot-demo. Its installation access token carries the permissions you verified earlier.
A permission list proves the configuration. A bot-authored review proves that the identity can perform useful work on pull request #1.
In this step, get ready to:
- Submit a comment-only review to pull request #1 with the app installation token.
- Inspect the API response for the review author and state.
- Confirm that the review is visibly authored by PR Review Only Bot.
Prepare the app-authenticated request
The GitHub REST API groups pull request feedback into reviews. The COMMENT event submits feedback without approving the change or requesting changes.
GitHub CLI can send the request with the installation token from earlier. Its filtered response makes the author and review state visible in your terminal.
Why use the installation token?
A personal access token sends the request under your user identity. The installation token makes the request under the app identity.
GitHub evaluates the request against the app permissions. This keeps the review action inside the boundary you configured.
- Record the account name that owns pr-review-bot-demo here: your GitHub username.
Post the review comment
The token is a sensitive credential. This request uses it for authentication without adding it to the repository.
Before you send the request, predict whether the returned author will be your user account or the app.
- Supply the app credential by replacing your-installation-access-token-here with the installation access token from earlier:
gh api --method POST "repos/[[GITHUB_OWNER=\"your GitHub username\"]]/pr-review-bot-demo/pulls/1/reviews" --header "Authorization: Bearer your-installation-access-token-here" --raw-field body="Automated review: the change is clear and ready for a human decision." --raw-field event="COMMENT" --jq "{author: .user.login, state: .state, body: .body}"
What does this request do?
- The POST method creates a review through /repos/{owner}/{repo}/pulls/{pull_number}/reviews.
- The authorization header makes the request with the app installation token.
- The body field supplies the review text.
- The event field submits the review as a comment.
- The --jq filter displays the user.login, review state, and review body.
- Inspect the filtered response printed by GitHub CLI.
You should see an object containing the bot account as the author. You should also see the review state and the exact comment body you submitted.
Request rejected or missing a response?
- Generate a fresh installation access token through the process from earlier if authentication fails. Installation access tokens expire after one hour.
- Confirm that the installed permission list still shows pull_requests with write access.
- Confirm that PR Review Only Bot remains installed on pr-review-bot-demo if the API cannot find the repository.
Help me diagnose the review request.
Verify the bot identity
The API response confirms that GitHub accepted the review. The pull request page gives you visible proof of which identity authored it.
Before you refresh the pull request, predict which name should appear beside the new review.
- Switch back to the browser tab showing pull request #1 in pr-review-bot-demo.
- Refresh the pull request page.
- Scroll to the latest review entry.
You should see Automated review: the change is clear and ready for a human decision. The author beside it should be PR Review Only Bot.
That is the proof you came for: the bot can participate in review under its own identity.
Your app can now leave review feedback. Next, you will ask the same identity to merge the pull request and test the boundary around write access.
Prove the App Cannot Merge
Your GitHub App has already proved it can review pull request #1. Its review comment is visible under the bot's identity.
A permission list only describes the intended boundary. In this step, you will test that boundary through the GitHub REST API using the same installation token.
In this step, get ready to:
- Send a merge request for pull request #1 as PR Review Only Bot.
- Trace the API response back to the app's permission boundary.
- Record the result before checking the pull request's final state.
Use the app identity
The merge endpoint asks GitHub to change the pull request's base branch. Authenticating with the installation token makes the request come from PR Review Only Bot.
- Record the GitHub username that owns pr-review-bot-demo: your GitHub username.
- Return to the authenticated API client from earlier.
Protect the installation token
The installation token is a live credential. Anyone who obtains it can act with the app's granted permissions.
- Paste the token only into your local API client.
- Keep the command line containing the token out of screenshots.
Before you run this, do you think the identity that posted the review comment can also complete the merge?
- Test whether the app can merge pull request #1 by replacing the plain token placeholder with the installation token from earlier before running this command:
curl -L \
-X PUT \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer your-installation-access-token" \
-H "X-GitHub-Api-Version: 2026-03-10" \
https://api.github.com/repos/[[GITHUB_OWNER="your GitHub username"]]/pr-review-bot-demo/pulls/1/merge
What does this request test?
- The PUT method asks GitHub to perform the merge operation.
- The /repos/{owner}/{repo}/pulls/{pull_number}/merge endpoint targets pull request #1 in pr-review-bot-demo.
- The Authorization: Bearer header authenticates the request as the app installation.
- The X-GitHub-Api-Version: 2026-03-10 header selects the documented API version.
- Read the response body returned by the API.
- Check whether the response reports a permission or access problem.
You should see a JSON error response that refuses the merge because the app lacks repository contents write permission. The response does not report a successful merge.
Did the request fail differently?
- Generate a fresh installation token with the process from earlier if the request cannot authenticate.
- Check that the repository owner matches your GitHub username if the API cannot locate the repository.
- Confirm that you replaced the token placeholder with the app installation token instead of personal credentials.
Help me diagnose the unexpected response from my GitHub pull request merge request.
Trace the permission boundary
The app's Pull requests write permission covered its review comment. Merging changes main through an endpoint that requires Contents write permission.
Your verified permission list grants Contents read-only access. The refusal is a live demonstration of least privilege.
- Compare the refusal response with the verified app permission list from earlier.
- Identify Contents read-only as the boundary that prevented the merge.
- Leave the app permissions unchanged.
Save proof and confirm the pull request
The refusal response is the strongest proof that the boundary works. Your token is still a live credential, so protect it while capturing that result.
- Scroll the terminal until the command line containing the token is out of view.
- Keep the API refusal response visible.
- Take a screenshot of the refusal response.
Before you check the pull request, do you expect it to be open or merged?
- Return to the pull request #1 page from earlier.
- Refresh the page.
You should see that pull request #1 is still open. The review comment authored by PR Review Only Bot remains visible.
You proved the boundary. PR Review Only Bot can comment while GitHub blocks its merge request.
Next, you will make an approval expire whenever new code lands on the branch.
Require Fresh Pull Request Approval
Your GitHub App can leave review comments. The API has already refused its attempt to merge.
A valid approval can become unsafe when someone pushes different code afterwards. In this step, you will use a repository ruleset to discard that stale approval as soon as the pull request changes.
In this step, get ready to:
- Protect main with an active approval ruleset.
- Approve pull request #1 as the signed-in user.
- Push another commit to prove that the approval becomes stale.
Create the approval ruleset
The ruleset protects main at the repository boundary. It requires one approval for the current pull request diff.
- Return to the pr-review-bot-demo repository in your browser.
- Click Settings in the repository navigation.
- Select Rules in the left sidebar.
- Select Rulesets.
- Click New ruleset.
- Choose New branch ruleset.
- Enter Require Fresh Approval in the Ruleset name field.
- Set the enforcement status to Active.
Why use active enforcement?
An active ruleset evaluates changes as soon as you create it. That lets the next approval test prove the protection with a real pull request.
- Remove Repository admin from the Bypass list if it appears there.
- Click Add a target under Target branches.
- Choose the option that includes the default branch.
- Select Require a pull request before merging under Branch protections.
- Set the number of required approvals to 1.
- Select Dismiss stale pull request approvals when new commits are pushed.
- Click Create at the bottom of the page.
That protection is now active. The Require Fresh Approval ruleset targets the default branch with no administrator bypass.
Why dismiss stale approvals?
GitHub records the state of the pull request diff when someone approves it. A later code change creates a new diff that the reviewer has not approved.
- Return to pull request #1.
- Refresh the pull request page.
You should see that the pull request now needs one approving review before it satisfies the rule.
Ruleset not affecting the pull request?
- Confirm that the ruleset status is Active.
- Confirm that the target includes the default branch main.
- Confirm that Repository admin does not appear in the bypass list.
Help me diagnose why my GitHub ruleset is not requiring approval on pull request #1.
Approve the current pull request
The current diff is ready for its first human approval. This creates the review state that the next commit must invalidate.
- Click Files changed on pull request #1.
- Review the diff to confirm it contains the expected branch change.
- Click Review changes above the changed code.
- Select Approve.
- Click Submit review.
Good. Pull request #1 now has the approval required by the ruleset.
Approval not satisfying the rule?
- Confirm that the review appears under the signed-in account with repository administration permissions.
- Confirm that you selected Approve before submitting the review.
- Refresh the pull request to load the latest review state.
Help me check why my pull request approval does not satisfy the active ruleset.
Push a change after approval
Your local Git clone still contains the review-bot-test branch. A code-modifying commit on that branch updates the open pull request diff.
- Return to the terminal you used with the local pr-review-bot-demo clone.
- Switch to review-bot-test by running this command:
git switch review-bot-test
What does this command do?
- The git switch command moves your working tree to the existing pull request branch.
- Any new commit now lands on review-bot-test.
You should see confirmation that review-bot-test is the current branch.
Cannot switch branches?
Confirm that your terminal is inside the existing local clone. Uncommitted local changes can also prevent a safe branch switch.
Help me switch to the existing review-bot-test branch without losing local work.
- Record a code-modifying commit by running these commands:
echo "info about this project" >> README.md
git add README.md && git commit -m "Add README"
What do these commands do?
- The first command appends a line to README.md so the pull request diff changes.
- The git add command stages that file.
- The git commit command records the staged change on review-bot-test.
You should see a new commit summary in the terminal with README.md listed as changed.
Commit not created?
- Confirm that the terminal is inside the pr-review-bot-demo clone.
- Confirm that the current branch is review-bot-test.
- Check that your configured Git name and email are still available.
Help me diagnose why Git did not create the new README commit.
Before you push, do you expect the existing approval to keep counting after the pull request diff changes?
- Push the new commit to the open pull request branch by running this command:
git push --set-upstream origin HEAD
What does this command do?
- The git push command sends the current branch commit to the origin remote.
- The HEAD reference points to the current review-bot-test branch.
- The upstream setting connects future pushes to the same remote branch.
You should see the push update the remote review-bot-test branch.
Push rejected?
- Confirm that your authenticated Git account has write access to pr-review-bot-demo.
- Confirm that origin points to the public repository from earlier.
- Confirm that the commit exists on review-bot-test.
Help me diagnose why Git cannot push my new review-bot-test commit to origin.
- Return to pull request #1 in your browser.
- Refresh the pull request page.
Your earlier approval no longer counts. The pull request now lacks the fresh approval required to merge the updated code.
That is the subtle hole closed. A reviewer must approve the code that is currently in the pull request.
Your ruleset now rejects stale trust. Next up, you will approve the updated diff before merging it and inspect how the ruleset evaluated each attempt.
Merge with a Fresh Approval
You proved that the active GitHub ruleset discards an approval after the reviewed code changes. Pull request #1 is blocked because its current commit set has no valid approval.
You will approve the updated commit set before merging it into main. You will then inspect Rule Insights beside the saved refusal from the GitHub App.
In this step, get ready to:
- Approve the current changes in pull request #1.
- Merge pull request #1 into main.
- Inspect Rule Insights for enforcement activity.
Approve the current changes
A fresh approval is attached to the latest commit set. That review gives the active rule a current decision to evaluate.
- Return to pull request #1 in pr-review-bot-demo.
- Click Files changed.
- Review the additional commit's diff.
- Click Review changes above the changed code.
- Select Approve.
- Click Submit review.
You should see a new approving review attached to the current version of pull request #1. The required approval check now passes for the latest code.
That fresh approval now covers the exact code waiting to enter main.
Merge the approved pull request
The active rule now has the approval it requires. The human merge path can proceed without changing the bot's restricted permissions.
Merging updates main, with the fresh approval acting as your deliberate checkpoint.
Before you try the merge, do you expect GitHub to keep blocking it after the fresh approval?
- Click the Conversation tab.
- Scroll to the merge box at the bottom of pull request #1.
- Click Merge pull request.
- Keep the default commit message.
- Click Confirm merge.
You should see pull request #1 marked as merged into main.
You closed the review loop. A fresh human decision has now carried the updated code through the protected merge path.
Merge Still Blocked?
- Refresh pull request #1.
- Confirm that the newest approval appears after the additional commit.
- Check the pull request for another unmet repository requirement.
Help me diagnose why pull request #1 is still blocked after a fresh approval.
The browser merge updates the remote repository. Your local Git clone needs the same history on its main branch.
- Return to the terminal from earlier.
- Bring the merged history into local main by running these commands:
git switch main
git pull
What Do These Commands Do?
- The first command switches your local checkout to main.
- The second command integrates the remote changes into the current branch.
- Check the terminal output for a successful update to local main.
Your local checkout is now on main with the commits from pull request #1.
Local Main Not Updating?
- Commit any local edits with your existing Git workflow before retrying the branch switch.
- Confirm that local main tracks its remote branch if the pull cannot identify an upstream.
Help me sync the merged pull request into my local main branch.
Inspect the ruleset evidence
Rule Insights records actions checked against repository rulesets. Its timeline lets you inspect whether an evaluated action passed or failed.
Before you filter the timeline, which rule result do you expect from the final merge?
- Return to the main page of pr-review-bot-demo.
- Click Settings under the repository name.
- Click Rules under Code and automation in the left sidebar.
- Click Insights.
- Select Require Fresh Approval from the ruleset filter.
- Select main from the branch filter.
- Choose a time period that covers this project session.
- Inspect the entries created around your final merge.
You should see recent evaluation activity for main under Require Fresh Approval. The passing final merge confirms that the current approval satisfied the rule.
No Recent Rule Activity?
- Clear any actor filter that hides the final merge.
- Widen the selected time period to include this project session.
- Confirm that you are viewing the settings for pr-review-bot-demo.
Help me find the pull request activity in GitHub Rule Insights.
The pull request timeline retains the stale dismissal plus the fresh approval. Your saved API response remains the separate proof that the app's merge attempt was refused.
Your bot can leave review feedback. Your protected main accepts the merge only after a fresh human approval.
Secret mission
Audit the Bot's Access Boundary
Turn the existing bot evidence into a four-part access audit. Produce a handoff another engineer can verify without changing the repository.
Clean Up Your Resources
Clean Up Your Resources
This project uses a public GitHub repository, so the setup has no ongoing project cost. Choose whether to keep the review boundary ready, pause the app's access, or remove the demo entirely.
Resources you used:
- The GitHub App installation for PR Review Only Bot on pr-review-bot-demo.
- The public pr-review-bot-demo repository with its active ruleset, merged pull request, review history, and ruleset insights.
- The local Git clone named pr-review-bot-demo with the main and review-bot-test branches.
Keep everything running
No action is needed. Choose this if you want the review boundary ready for future coding agents.
- Keep pr-review-bot-demo public so the active ruleset continues protecting main at no cost.
- Retain the pull request history so the bot comment, refused merge evidence, stale approval dismissal, and final merge remain available.
- Keep PR Review Only Bot installed only on pr-review-bot-demo.
- Use this repository as a safe starting point when you connect another coding agent.
Pause - I'll come back to this later
Remove the app installation to pause bot access while keeping the repository, ruleset, pull request history, and local clone. The app controls sit inside repository settings, so this route is easy to miss.
- In GitHub, return to pr-review-bot-demo.
- Click Settings in the repository navigation.
- Select GitHub Apps under Integrations in the sidebar.
- Click Configure beside PR Review Only Bot.
- Scroll to the Danger zone section.
- Click Uninstall.
- Confirm the app uninstall.
You should no longer see PR Review Only Bot in the repository's installed app list. That's a clean pause: the merged pull request and its review history stay visible.
Delete - I don't want to use this again
Repository deletion is permanent, so save your refused merge proof before touching the deletion controls. Removing the repository also removes its active ruleset, pull request history, bot comment, and ruleset insights.
- Save a screenshot of the refused merge evidence outside pr-review-bot-demo.
- In GitHub, return to pr-review-bot-demo.
- Click Settings in the repository navigation.
- Select GitHub Apps under Integrations in the sidebar.
- Click Configure beside PR Review Only Bot.
- Scroll to the Danger zone section.
- Click Uninstall.
- Confirm the app uninstall.
The app installation is now gone. That closes its access to pr-review-bot-demo before the repository disappears.
- Return to pr-review-bot-demo.
- Click Settings in the repository navigation.
- Select General in the sidebar.
- Scroll to the Danger Zone section.
- Click Delete this repository.
- Enter your-account-name/pr-review-bot-demo in the repository confirmation field.
- Confirm the permanent repository deletion.
You should return to your repositories area. The pr-review-bot-demo repository should no longer appear.
The hosted demo is now gone, but the local clone still occupies space on your computer. Use the instructions for your operating system to remove it.
macOS
- In Finder, go to the location where you cloned pr-review-bot-demo.
- Select the pr-review-bot-demo folder.
- Open the File menu.
- Click Move to Trash.
- Open Trash.
- Click Empty.
- Confirm the permanent deletion.
You should no longer see the local pr-review-bot-demo folder in Finder.
Windows
- In File Explorer, go to the location where you cloned pr-review-bot-demo.
- Select the pr-review-bot-demo folder.
- Press the Delete key.
- Open Recycle Bin.
- Click Empty Recycle Bin.
- Confirm the permanent deletion.
You should no longer see the local pr-review-bot-demo folder in File Explorer.
Nice Work!
Nice Work!
Excellent work! Your review-only GitHub App can comment on pull requests without gaining merge access.
What you learned:
- Configured least-privilege app permissions that allow pull request comments while keeping repository contents read-only. You also read the installation permission list back to confirm the boundary.
- Posted a bot-authored review comment on pull request #1. You reused the same installation token for a merge attempt that the GitHub API refused.
- Protected main with the Require Fresh Approval ruleset. You watched an approval disappear after a new commit. A fresh user approval then allowed pull request #1 to merge. Ruleset insights confirmed the evaluation activity.
- Secret Mission: Extended the least-privilege review workflow through the optional challenge.
Ready to quiz yourself?