Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions .github/workflows/ai-gatekeeper.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ on:
types: [submitted, dismissed]
workflow_dispatch:
workflow_call:
inputs:
exempt_paths:
description: >-
Optional newline-separated list of regular expressions matched against
changed file paths (repo-relative, e.g. `apps/price-terminal/...`). A
bot-authored PR is exempt from the human-approval requirement when every
changed file matches either the built-in docs patterns or one of these
expressions. Patterns are passed to `jq`'s `test()`; add `(?i)` for
case-insensitive matching. Defaults to empty (docs-only exemption only).
type: string
required: false
default: ""

permissions:
contents: read
Expand All @@ -31,6 +43,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
EXEMPT_PATHS: ${{ inputs.exempt_paths }}
run: |
set -euo pipefail

Expand All @@ -39,7 +52,7 @@ jobs:
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"
echo "exempt=false" >> "$GITHUB_OUTPUT"
exit 0
fi

Expand All @@ -63,43 +76,47 @@ jobs:
printf 'summary=%s\n' "Not a bot-authored PR. Skipping human-review enforcement." >> "$GITHUB_OUTPUT"
echo "details=" >> "$GITHUB_OUTPUT"
echo "is_bot=false" >> "$GITHUB_OUTPUT"
echo "docs_only=false" >> "$GITHUB_OUTPUT"
echo "exempt=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "is_bot=true" >> "$GITHUB_OUTPUT"

# 2b. Docs-only changes skip additional approvals
# 2b. Exempt changes skip additional approvals. A file is exempt when it
# matches a built-in docs pattern or a caller-supplied `exempt_paths`
# regex. A PR is exempt only when *every* changed file is exempt.
PR_FILES=$(gh api "repos/$REPO/pulls/$PR/files" --paginate)
DOCS_ONLY=$(
echo "$PR_FILES" | jq -r '
EXTRA_PATTERNS=$(printf '%s' "${EXEMPT_PATHS:-}" | jq -R -s 'split("\n") | map(select(length > 0))')
EXEMPT=$(
echo "$PR_FILES" | jq --argjson extra "$EXTRA_PATTERNS" -r '

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Slurp paginated file pages before evaluating exemptions

When an exempt_paths PR spans more than one GitHub API page, this jq invocation processes each gh api --paginate page as a separate JSON array/object (the GitHub CLI docs for --paginate say to use --slurp to wrap all pages). An all-exempt changeset then sets EXEMPT to multiple lines such as true\ntrue, so the later [ "$EXEMPT" = "true" ] check fails and the bot PR still requires two approvals. This breaks the new exemption for large generated/app-only updates; slurp and flatten the paginated pages before applying all(...).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, but I believe this is a false positive. gh api --paginate already merges JSON-array responses into a single array, so jq runs once and EXEMPT is a single value — --slurp does the opposite (it nests pages into an array-of-pages).

Verified empirically against a 14-item array forced into 7 pages:

$ gh api ".../labels?per_page=2" --paginate | jq -r 'length'
14

If pages were emitted separately, that would print 2 seven times; instead jq sees one merged array of 14. The pre-existing docs-only exemption already relies on this same auto-merge, and this PR doesn't change the gh api --paginate fetch. So no change is needed here.

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)(\\..*)?$"))
all(.[]; .filename as $f | (
($f | test("(?i)^(docs|doc)/"))
or ($f | test("(?i)\\.(md|mdx|markdown|rst|adoc)$"))
or ($f | test("(?i)(^|/)(README|CHANGELOG)(\\..*)?$"))
or any($extra[]; . as $p | $f | test($p))
)) | tostring
end
'
)

if [ "$DOCS_ONLY" = "true" ]; then
if [ "$EXEMPT" = "true" ]; then
FILES_LIST=$(echo "$PR_FILES" | jq -r '[.[].filename] | map("- " + .) | join("\n")')
echo "docs_only=true" >> "$GITHUB_OUTPUT"
echo "exempt=true" >> "$GITHUB_OUTPUT"
echo "conclusion=success" >> "$GITHUB_OUTPUT"
printf 'summary=%s\n' "Docs-only changes detected. Additional approvals are not required." >> "$GITHUB_OUTPUT"
printf 'summary=%s\n' "Exempt changes detected (docs or caller-exempted paths). Additional approvals are not required." >> "$GITHUB_OUTPUT"
{
echo "details<<EOF"
echo "Docs-only files:"
echo "Exempt 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"
echo "exempt=false" >> "$GITHUB_OUTPUT"

# 3. Reduce to latest review per human
LATEST_REVIEWS_JSON=$(
Expand Down Expand Up @@ -191,15 +208,15 @@ jobs:
APPROVERS: ${{ steps.evaluate.outputs.approvals_list }}
MISSING: ${{ steps.evaluate.outputs.approvals_missing }}
CONCLUSION: ${{ steps.evaluate.outputs.conclusion }}
DOCS_ONLY: ${{ steps.evaluate.outputs.docs_only }}
EXEMPT: ${{ steps.evaluate.outputs.exempt }}
run: |
set -euo pipefail
MARKER="<!-- bot-proxy-gatekeeper-comment -->"
APPROVER_LINE="Current approvals: ${APPROVERS:-none}"
if [ "$DOCS_ONLY" = "true" ]; then
if [ "$EXEMPT" = "true" ]; then
MSG=$(printf "%s\n%s" \
"$MARKER" \
"📝 **Docs-only changes**: additional approvals are not required.")
"📝 **Exempt changes** (docs or exempted paths): additional approvals are not required.")
elif [ "$CONCLUSION" = "success" ]; then
MSG=$(printf "%s\n%s\n\n**%s**" \
"$MARKER" \
Expand Down