Auto-approve low-risk PRs workflow (cypress-io/cypress)
The Auto-approve low-risk PRs workflow from cypress-io/cypress, explained and optimized by Latchkey.
CI health: A - excellent
Point runs-on at Latchkey and get job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Auto-approve low-risk PRs workflow from the cypress-io/cypress repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: 'Auto-approve low-risk PRs'
on:
pull_request:
types:
- opened
- edited
- synchronize
branches:
- develop
- release/*
permissions:
contents: read
pull-requests: read
concurrency:
group: auto-approve-low-risk-${{ github.event.pull_request.number }}
# cancel-in-progress: false so a synchronize run's dismissal can't be
# interrupted mid-flight by a subsequent edited event. Per-PR runs queue
# serially. GitHub only retains ONE pending run per group; if multiple
# events fire while a run is in-progress, intermediate events are
# silently dropped - the SHA-based reconciliation in the action step is
# what guarantees we still converge to correct state when that happens.
cancel-in-progress: false
jobs:
auto-approve:
name: Auto-approve low-risk PR
runs-on: ubuntu-latest
if: >-
github.repository == 'cypress-io/cypress' &&
github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Extract risk level and reviewed SHA from CURSOR_SUMMARY
id: risk-check
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const body = context.payload.pull_request.body || '';
core.setOutput('is_low_risk', 'false');
core.setOutput('reviewed_sha', '');
const summaryMatches = [...body.matchAll(/<!-- CURSOR_SUMMARY -->([\s\S]*?)<!-- \/CURSOR_SUMMARY -->/g)];
if (summaryMatches.length === 0) {
core.notice('No CURSOR_SUMMARY section found in PR body.');
return;
}
if (summaryMatches.length > 1) {
core.notice(`Multiple (${summaryMatches.length}) CURSOR_SUMMARY sections found in PR body; refusing to auto-approve.`);
return;
}
const summaryContent = summaryMatches[0][1];
const hasMediumRisk = /\*\*Medium Risk\*\*/i.test(summaryContent);
const hasHighRisk = /\*\*High Risk\*\*/i.test(summaryContent);
if (hasMediumRisk || hasHighRisk) {
core.notice('PR is not low risk.');
return;
}
const hasLowRisk = /\*\*Low Risk\*\*/i.test(summaryContent);
if (!hasLowRisk) {
core.notice('No recognized risk level found in CURSOR_SUMMARY.');
return;
}
// Extract the SHA Cursor Bugbot states it reviewed, from the summary footer:
// "Reviewed by [Cursor Bugbot](...) for commit <40-hex-char-sha>."
const shaMatch = summaryContent.match(/Reviewed by \[Cursor Bugbot\]\([^)]*\) for commit ([0-9a-f]{40})/i);
if (!shaMatch) {
core.notice('CURSOR_SUMMARY is Low Risk but is missing the reviewed-commit footer; cannot verify the review SHA.');
return;
}
core.info(`CURSOR_SUMMARY contains **Low Risk** assessment for ${shaMatch[1].slice(0, 7)}.`);
core.setOutput('is_low_risk', 'true');
core.setOutput('reviewed_sha', shaMatch[1]);
- name: Find existing auto-approval
id: existing-approval
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eo pipefail
LATEST=$(gh api --paginate \
"/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \
--jq '.[] | select(.user.login == "cypress-bot[bot]" and .state == "APPROVED") | [.id, .commit_id] | @tsv' \
| tail -n 1)
if [ -n "$LATEST" ]; then
REVIEW_ID=$(echo "$LATEST" | cut -f1)
COMMIT_ID=$(echo "$LATEST" | cut -f2)
echo "review_id=$REVIEW_ID" >> "$GITHUB_OUTPUT"
echo "commit_id=$COMMIT_ID" >> "$GITHUB_OUTPUT"
echo "::notice::Found existing auto-approval (review #$REVIEW_ID for ${COMMIT_ID:0:7})."
else
echo "review_id=" >> "$GITHUB_OUTPUT"
echo "commit_id=" >> "$GITHUB_OUTPUT"
fi
- name: Decide reconciliation actions
id: action
env:
IS_LOW_RISK: ${{ steps.risk-check.outputs.is_low_risk }}
REVIEWED_SHA: ${{ steps.risk-check.outputs.reviewed_sha }}
REVIEW_ID: ${{ steps.existing-approval.outputs.review_id }}
APPROVAL_SHA: ${{ steps.existing-approval.outputs.commit_id }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
SENDER_TYPE: ${{ github.event.sender.type }}
SENDER_LOGIN: ${{ github.event.sender.login }}
run: |
SHOULD_DISMISS=false
SHOULD_APPROVE=false
DISMISS_REASON=""
# Existing approval is stale if it's for a different SHA or the risk is no longer Low.
# Reconcile on every event so we converge to the correct state even if a synchronize
# event is dropped by concurrency queueing (only one pending run is retained).
if [ -n "$REVIEW_ID" ]; then
if [ "$APPROVAL_SHA" != "$HEAD_SHA" ]; then
SHOULD_DISMISS=true
DISMISS_REASON="New commits pushed (auto-approval was for ${APPROVAL_SHA:0:7}, head is now ${HEAD_SHA:0:7}). Auto-approval dismissed pending Cursor Bugbot re-review of the new head SHA."
elif [ "$IS_LOW_RISK" != "true" ]; then
SHOULD_DISMISS=true
DISMISS_REASON="Cursor Bugbot risk assessment is no longer **Low Risk**. Auto-approval dismissed; manual review required."
fi
fi
# Approve only when:
# - current state is Low Risk
# - the event was triggered by cursor[bot] (i.e., BugBot just wrote/updated the
# summary; human edits to the body don't trigger approval)
# - the SHA Cursor states it reviewed (parsed from the summary footer) matches
# the current head SHA
# - either no approval exists, or the existing one is being dismissed in this run
# (SHA mismatch case)
if [ "$IS_LOW_RISK" = "true" ] \
&& [ "$SENDER_TYPE" = "Bot" ] \
&& [ "$SENDER_LOGIN" = "cursor[bot]" ] \
&& [ "$REVIEWED_SHA" = "$HEAD_SHA" ]; then
if [ -z "$REVIEW_ID" ] || [ "$SHOULD_DISMISS" = "true" ]; then
SHOULD_APPROVE=true
fi
fi
echo "should_dismiss=$SHOULD_DISMISS" >> "$GITHUB_OUTPUT"
echo "should_approve=$SHOULD_APPROVE" >> "$GITHUB_OUTPUT"
echo "dismiss_reason=$DISMISS_REASON" >> "$GITHUB_OUTPUT"
if [ "$SHOULD_DISMISS" = "false" ] && [ "$SHOULD_APPROVE" = "false" ]; then
echo "::notice::No reconciliation needed (is_low_risk=$IS_LOW_RISK, reviewed_sha=${REVIEWED_SHA:0:7}, head_sha=${HEAD_SHA:0:7}, approval=${REVIEW_ID:-none} for ${APPROVAL_SHA:0:7})."
fi
- name: Generate Cypress Bot App token
if: steps.action.outputs.should_dismiss == 'true' || steps.action.outputs.should_approve == 'true'
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.CYPRESS_BOT_APP_ID }}
private-key: ${{ secrets.CYPRESS_BOT_APP_PRIVATE_KEY }}
permission-pull-requests: write
permission-metadata: read
- name: Dismiss stale approval
if: steps.action.outputs.should_dismiss == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DISMISS_MESSAGE: ${{ steps.action.outputs.dismiss_reason }}
run: |
gh api \
--method PUT \
"/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/${{ steps.existing-approval.outputs.review_id }}/dismissals" \
-f message="$DISMISS_MESSAGE"
- name: Verify PR author has write access
if: steps.action.outputs.should_approve == 'true'
id: author-check
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }}
run: |
PERMISSION=$(gh api \
"/repos/${{ github.repository }}/collaborators/$AUTHOR_LOGIN/permission" \
--jq '.permission')
case "$PERMISSION" in
admin|write)
echo "has_write=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "has_write=false" >> "$GITHUB_OUTPUT"
echo "::notice::PR author '$AUTHOR_LOGIN' has '$PERMISSION' permission. Skipping auto-approve."
;;
esac
- name: Approve PR
if: >-
steps.action.outputs.should_approve == 'true' &&
steps.author-check.outputs.has_write == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh api \
--method POST \
"/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \
-f commit_id="${{ github.event.pull_request.head.sha }}" \
-f event="APPROVE"
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: 'Auto-approve low-risk PRs' on: pull_request: types: - opened - edited - synchronize branches: - develop - release/* permissions: contents: read pull-requests: read concurrency: group: auto-approve-low-risk-${{ github.event.pull_request.number }} # cancel-in-progress: false so a synchronize run's dismissal can't be # interrupted mid-flight by a subsequent edited event. Per-PR runs queue # serially. GitHub only retains ONE pending run per group; if multiple # events fire while a run is in-progress, intermediate events are # silently dropped - the SHA-based reconciliation in the action step is # what guarantees we still converge to correct state when that happens. cancel-in-progress: false jobs: auto-approve: timeout-minutes: 30 name: Auto-approve low-risk PR runs-on: latchkey-small if: >- github.repository == 'cypress-io/cypress' && github.event.pull_request.head.repo.full_name == github.repository steps: - name: Extract risk level and reviewed SHA from CURSOR_SUMMARY id: risk-check uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const body = context.payload.pull_request.body || ''; core.setOutput('is_low_risk', 'false'); core.setOutput('reviewed_sha', ''); const summaryMatches = [...body.matchAll(/<!-- CURSOR_SUMMARY -->([\s\S]*?)<!-- \/CURSOR_SUMMARY -->/g)]; if (summaryMatches.length === 0) { core.notice('No CURSOR_SUMMARY section found in PR body.'); return; } if (summaryMatches.length > 1) { core.notice(`Multiple (${summaryMatches.length}) CURSOR_SUMMARY sections found in PR body; refusing to auto-approve.`); return; } const summaryContent = summaryMatches[0][1]; const hasMediumRisk = /\*\*Medium Risk\*\*/i.test(summaryContent); const hasHighRisk = /\*\*High Risk\*\*/i.test(summaryContent); if (hasMediumRisk || hasHighRisk) { core.notice('PR is not low risk.'); return; } const hasLowRisk = /\*\*Low Risk\*\*/i.test(summaryContent); if (!hasLowRisk) { core.notice('No recognized risk level found in CURSOR_SUMMARY.'); return; } // Extract the SHA Cursor Bugbot states it reviewed, from the summary footer: // "Reviewed by [Cursor Bugbot](...) for commit <40-hex-char-sha>." const shaMatch = summaryContent.match(/Reviewed by \[Cursor Bugbot\]\([^)]*\) for commit ([0-9a-f]{40})/i); if (!shaMatch) { core.notice('CURSOR_SUMMARY is Low Risk but is missing the reviewed-commit footer; cannot verify the review SHA.'); return; } core.info(`CURSOR_SUMMARY contains **Low Risk** assessment for ${shaMatch[1].slice(0, 7)}.`); core.setOutput('is_low_risk', 'true'); core.setOutput('reviewed_sha', shaMatch[1]); - name: Find existing auto-approval id: existing-approval env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail LATEST=$(gh api --paginate \ "/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \ --jq '.[] | select(.user.login == "cypress-bot[bot]" and .state == "APPROVED") | [.id, .commit_id] | @tsv' \ | tail -n 1) if [ -n "$LATEST" ]; then REVIEW_ID=$(echo "$LATEST" | cut -f1) COMMIT_ID=$(echo "$LATEST" | cut -f2) echo "review_id=$REVIEW_ID" >> "$GITHUB_OUTPUT" echo "commit_id=$COMMIT_ID" >> "$GITHUB_OUTPUT" echo "::notice::Found existing auto-approval (review #$REVIEW_ID for ${COMMIT_ID:0:7})." else echo "review_id=" >> "$GITHUB_OUTPUT" echo "commit_id=" >> "$GITHUB_OUTPUT" fi - name: Decide reconciliation actions id: action env: IS_LOW_RISK: ${{ steps.risk-check.outputs.is_low_risk }} REVIEWED_SHA: ${{ steps.risk-check.outputs.reviewed_sha }} REVIEW_ID: ${{ steps.existing-approval.outputs.review_id }} APPROVAL_SHA: ${{ steps.existing-approval.outputs.commit_id }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} SENDER_TYPE: ${{ github.event.sender.type }} SENDER_LOGIN: ${{ github.event.sender.login }} run: | SHOULD_DISMISS=false SHOULD_APPROVE=false DISMISS_REASON="" # Existing approval is stale if it's for a different SHA or the risk is no longer Low. # Reconcile on every event so we converge to the correct state even if a synchronize # event is dropped by concurrency queueing (only one pending run is retained). if [ -n "$REVIEW_ID" ]; then if [ "$APPROVAL_SHA" != "$HEAD_SHA" ]; then SHOULD_DISMISS=true DISMISS_REASON="New commits pushed (auto-approval was for ${APPROVAL_SHA:0:7}, head is now ${HEAD_SHA:0:7}). Auto-approval dismissed pending Cursor Bugbot re-review of the new head SHA." elif [ "$IS_LOW_RISK" != "true" ]; then SHOULD_DISMISS=true DISMISS_REASON="Cursor Bugbot risk assessment is no longer **Low Risk**. Auto-approval dismissed; manual review required." fi fi # Approve only when: # - current state is Low Risk # - the event was triggered by cursor[bot] (i.e., BugBot just wrote/updated the # summary; human edits to the body don't trigger approval) # - the SHA Cursor states it reviewed (parsed from the summary footer) matches # the current head SHA # - either no approval exists, or the existing one is being dismissed in this run # (SHA mismatch case) if [ "$IS_LOW_RISK" = "true" ] \ && [ "$SENDER_TYPE" = "Bot" ] \ && [ "$SENDER_LOGIN" = "cursor[bot]" ] \ && [ "$REVIEWED_SHA" = "$HEAD_SHA" ]; then if [ -z "$REVIEW_ID" ] || [ "$SHOULD_DISMISS" = "true" ]; then SHOULD_APPROVE=true fi fi echo "should_dismiss=$SHOULD_DISMISS" >> "$GITHUB_OUTPUT" echo "should_approve=$SHOULD_APPROVE" >> "$GITHUB_OUTPUT" echo "dismiss_reason=$DISMISS_REASON" >> "$GITHUB_OUTPUT" if [ "$SHOULD_DISMISS" = "false" ] && [ "$SHOULD_APPROVE" = "false" ]; then echo "::notice::No reconciliation needed (is_low_risk=$IS_LOW_RISK, reviewed_sha=${REVIEWED_SHA:0:7}, head_sha=${HEAD_SHA:0:7}, approval=${REVIEW_ID:-none} for ${APPROVAL_SHA:0:7})." fi - name: Generate Cypress Bot App token if: steps.action.outputs.should_dismiss == 'true' || steps.action.outputs.should_approve == 'true' id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 with: app-id: ${{ secrets.CYPRESS_BOT_APP_ID }} private-key: ${{ secrets.CYPRESS_BOT_APP_PRIVATE_KEY }} permission-pull-requests: write permission-metadata: read - name: Dismiss stale approval if: steps.action.outputs.should_dismiss == 'true' env: GH_TOKEN: ${{ steps.app-token.outputs.token }} DISMISS_MESSAGE: ${{ steps.action.outputs.dismiss_reason }} run: | gh api \ --method PUT \ "/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/${{ steps.existing-approval.outputs.review_id }}/dismissals" \ -f message="$DISMISS_MESSAGE" - name: Verify PR author has write access if: steps.action.outputs.should_approve == 'true' id: author-check env: GH_TOKEN: ${{ steps.app-token.outputs.token }} AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }} run: | PERMISSION=$(gh api \ "/repos/${{ github.repository }}/collaborators/$AUTHOR_LOGIN/permission" \ --jq '.permission') case "$PERMISSION" in admin|write) echo "has_write=true" >> "$GITHUB_OUTPUT" ;; *) echo "has_write=false" >> "$GITHUB_OUTPUT" echo "::notice::PR author '$AUTHOR_LOGIN' has '$PERMISSION' permission. Skipping auto-approve." ;; esac - name: Approve PR if: >- steps.action.outputs.should_approve == 'true' && steps.author-check.outputs.has_write == 'true' env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | gh api \ --method POST \ "/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \ -f commit_id="${{ github.event.pull_request.head.sha }}" \ -f event="APPROVE"
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Add a job timeout so a hung step cannot burn hours of runner time.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- End-to-end and browser tests
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.