From f1ded9b094a44bb6f333ca66f7722a82cd70c84b Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 16 Jun 2026 11:31:09 +0100 Subject: [PATCH] feat(gatekeeper): add optional exempt_paths input Adds a backward-compatible `exempt_paths` input to the reusable gatekeeper workflow: a newline-separated list of regexes matched against changed file paths. A bot-authored PR is exempt from the 2-human-approval requirement when every changed file matches either the built-in docs patterns or one of these expressions. Defaults to empty, so existing callers (docs-only exemption) are unaffected. The internal `docs_only` signal is generalized to `exempt`. This lets consuming repos carve out low-risk paths (e.g. a web app) without forking the workflow. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ai-gatekeeper.yaml | 51 ++++++++++++++++++---------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ai-gatekeeper.yaml b/.github/workflows/ai-gatekeeper.yaml index b0f485a..7ef20f6 100644 --- a/.github/workflows/ai-gatekeeper.yaml +++ b/.github/workflows/ai-gatekeeper.yaml @@ -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 @@ -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 @@ -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 @@ -63,35 +76,39 @@ 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 ' 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<> "$GITHUB_OUTPUT" @@ -99,7 +116,7 @@ jobs: 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=$( @@ -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="" 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" \