Skip extra approvals for docs-only PRs #74
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |