Skip to content

Skip extra approvals for docs-only PRs #74

Skip extra approvals for docs-only PRs

Skip extra approvals for docs-only PRs #74

Workflow file for this run

name: Bot Proxy Gatekeeper
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
pull_request_review:
types: [submitted, dismissed]
workflow_dispatch:
permissions:
contents: read
pull-requests: write
issues: write
checks: write
concurrency:
group: gatekeeper-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
enforce-human-review:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Analyze Authorship and Approvals
id: evaluate
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
if [ -z "${PR:-}" ]; then
echo "conclusion=success" >> "$GITHUB_OUTPUT"
printf 'summary=%s\n' "No PR context; skipping gatekeeper checks." >> "$GITHUB_OUTPUT"
echo "details=" >> "$GITHUB_OUTPUT"
echo "is_bot=false" >> "$GITHUB_OUTPUT"
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# 1. Fetch PR Data
PR_DATA=$(gh pr view "$PR" --repo "$REPO" --json author,body,commits,reviews,headRefOid)
HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid')
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
# 2. Only enforce when Devin is the PR author
IS_BOT_AUTHOR=$(
echo "$PR_DATA" | jq '
(.author.login | endswith("[bot]"))
or (.author.login == "app/devin-ai-integration")
or (.author.is_bot == true)
'
)
if [ "$IS_BOT_AUTHOR" != "true" ]; then
echo "conclusion=success" >> "$GITHUB_OUTPUT"
printf 'summary=%s\n' "Not a Devin-authored PR. Skipping human-review enforcement." >> "$GITHUB_OUTPUT"
echo "details=" >> "$GITHUB_OUTPUT"
echo "is_bot=false" >> "$GITHUB_OUTPUT"
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "is_bot=true" >> "$GITHUB_OUTPUT"
# 2b. Docs-only changes skip additional approvals
PR_FILES=$(gh api "repos/$REPO/pulls/$PR/files" --paginate)
DOCS_ONLY=$(
echo "$PR_FILES" | jq -r '
if length == 0 then
"false"
else
all(.[]; (
(.filename | test("(?i)^(docs|doc)/"))
or (.filename | test("(?i)\\.(md|mdx|markdown|rst|adoc)$"))
or (.filename | test("(?i)(^|/)(README|CHANGELOG)(\\..*)?$"))
)) | tostring
end
'
)
if [ "$DOCS_ONLY" = "true" ]; then
FILES_LIST=$(echo "$PR_FILES" | jq -r '[.[].filename] | map("- " + .) | join("\n")')
echo "docs_only=true" >> "$GITHUB_OUTPUT"
echo "conclusion=success" >> "$GITHUB_OUTPUT"
printf 'summary=%s\n' "Docs-only changes detected. Additional approvals are not required." >> "$GITHUB_OUTPUT"
{
echo "details<<EOF"
echo "Docs-only files:"
echo "$FILES_LIST"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "approvals_list=" >> "$GITHUB_OUTPUT"
echo "approvals_missing=0" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "docs_only=false" >> "$GITHUB_OUTPUT"
# 3. Reduce to latest review per human
LATEST_REVIEWS_JSON=$(
echo "$PR_DATA" | jq -c '
[
.reviews[]
| select(.author.login | endswith("[bot]") | not)
| {login: .author.login, state: .state, submittedAt: .submittedAt}
]
| sort_by(.submittedAt)
| group_by(.login)
| map(.[-1])
'
)
APPROVERS=$(
echo "$LATEST_REVIEWS_JSON" | jq -r '
[.[] | select(.state=="APPROVED") | .login]
| unique
| join(", ")
'
)
COUNT=$(
echo "$LATEST_REVIEWS_JSON" | jq -r '
[.[] | select(.state=="APPROVED") | .login]
| unique
| length
'
)
DETAILS=$(
echo "$LATEST_REVIEWS_JSON" | jq -r '
if length == 0 then
"No human reviews yet."
else
[.[] | "- @" + .login + ": " + .state] | join("\n")
end
'
)
MISSING=$((2 - COUNT))
if [ "$MISSING" -lt 0 ]; then
MISSING=0
fi
echo "approvals.human_unique=$COUNT" >> "$GITHUB_OUTPUT"
echo "approvals_list=$APPROVERS" >> "$GITHUB_OUTPUT"
echo "approvals_missing=$MISSING" >> "$GITHUB_OUTPUT"
{
echo "details<<EOF"
echo "$DETAILS"
echo "EOF"
} >> "$GITHUB_OUTPUT"
# 4. Enforcement Logic
if [ "$COUNT" -lt 2 ]; then
echo "conclusion=failure" >> "$GITHUB_OUTPUT"
printf 'summary=%s\n' "2 unique human approvals required. Current: $COUNT. Missing: $MISSING." >> "$GITHUB_OUTPUT"
exit 0
fi
echo "conclusion=success" >> "$GITHUB_OUTPUT"
printf 'summary=%s\n' "Security requirements satisfied. Unique human approvals: $COUNT." >> "$GITHUB_OUTPUT"
- name: Publish Gatekeeper Check
if: steps.evaluate.outputs.head_sha != ''
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ steps.evaluate.outputs.head_sha }}
CONCLUSION: ${{ steps.evaluate.outputs.conclusion }}
SUMMARY: ${{ steps.evaluate.outputs.summary }}
DETAILS: ${{ steps.evaluate.outputs.details }}
run: |
set -euo pipefail
gh api --method POST "repos/$REPO/check-runs" \
-f name="Bot Proxy Gatekeeper (Human Review)" \
-f head_sha="$HEAD_SHA" \
-f status="completed" \
-f conclusion="$CONCLUSION" \
-f output[title]="Bot Proxy Gatekeeper (Human Review)" \
-f output[summary]="$SUMMARY" \
-f output[text]="$DETAILS"
- name: Post Security Comment
if: steps.evaluate.outputs.is_bot == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
APPROVERS: ${{ steps.evaluate.outputs.approvals_list }}
MISSING: ${{ steps.evaluate.outputs.approvals_missing }}
CONCLUSION: ${{ steps.evaluate.outputs.conclusion }}
DOCS_ONLY: ${{ steps.evaluate.outputs.docs_only }}
run: |
set -euo pipefail
MARKER="<!-- bot-proxy-gatekeeper-comment -->"
APPROVER_LINE="Current approvals: ${APPROVERS:-none}"
if [ "$DOCS_ONLY" = "true" ]; then
MSG=$(printf "%s\n%s" \
"$MARKER" \
"📝 **Docs-only changes**: additional approvals are not required.")
elif [ "$CONCLUSION" = "success" ]; then
MSG=$(printf "%s\n%s\n\n**%s**" \
"$MARKER" \
"✅ **Review Requirements Satisfied**: 2 different human approvals are present." \
"$APPROVER_LINE")
else
MSG=$(printf "%s\n%s\n\n**%s**\n**Missing:** %s" \
"$MARKER" \
"⚠️ **Review Required**: A **human** is required to review bot PRs. 2 different human approvals are required before this PR can be merged." \
"$APPROVER_LINE" \
"${MISSING:-2}")
fi
COMMENT_ID=$(
gh api "repos/$REPO/issues/$PR/comments" \
--jq ".[] | select(.body | contains(\"$MARKER\")) | .id" \
| head -n 1
)
if [ -n "${COMMENT_ID:-}" ]; then
gh api --method PATCH "repos/$REPO/issues/comments/$COMMENT_ID" -f body="$MSG"
else
gh api --method POST "repos/$REPO/issues/$PR/comments" -f body="$MSG"
fi