mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-08 16:02:30 +00:00
restore auto-review & command gating for private org members (#9958)
* fix(ci): gate auto-review on non-fork PR not author_association (skips private members) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): authorize private org members for command workflows via app-token gate Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ea19cc9dc4
commit
98013483c8
@@ -0,0 +1,66 @@
|
||||
name: Check Write Access
|
||||
|
||||
# Authorizes a user to trigger privileged command workflows (/review, /ai, /plan,
|
||||
# /updatesqlx, ...). The webhook author_association reports PRIVATE org members as
|
||||
# CONTRIBUTOR/NONE (only public members show as MEMBER), so command jobs can't gate on
|
||||
# it alone. This mints the internal GitHub App token — which can see private members —
|
||||
# and confirms the user is a member or has write access to the repo. The app token is
|
||||
# minted fresh per run, so unlike the old ORG_ACCESS_TOKEN PAT it never expires.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
username:
|
||||
required: true
|
||||
type: string
|
||||
description: 'The user whose access to verify'
|
||||
trusted_bot:
|
||||
required: false
|
||||
type: string
|
||||
default: 'windmill-internal-app[bot]'
|
||||
description: 'A bot login that is always authorized'
|
||||
outputs:
|
||||
authorized:
|
||||
description: 'true if the user is the trusted bot, an org member, or has repo write access'
|
||||
value: ${{ jobs.check.outputs.authorized }}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
authorized: ${{ steps.check.outputs.authorized }}
|
||||
steps:
|
||||
- name: Mint internal app token
|
||||
id: app
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.INTERNAL_APP_ID }}
|
||||
private-key: ${{ secrets.INTERNAL_APP_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Resolve authorization
|
||||
id: check
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app.outputs.token }}
|
||||
USERNAME: ${{ inputs.username }}
|
||||
TRUSTED_BOT: ${{ inputs.trusted_bot }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ "$USERNAME" = "$TRUSTED_BOT" ]; then
|
||||
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
ORG="${REPO%%/*}"
|
||||
# Org membership resolves private members too (204 = member, 404 = not).
|
||||
if gh api "orgs/$ORG/members/$USERNAME" --silent 2>/dev/null; then
|
||||
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
# Fallback: effective repo permission (also covers outside collaborators).
|
||||
PERM=$(gh api "repos/$REPO/collaborators/$USERNAME/permission" --jq '.permission' 2>/dev/null || echo none)
|
||||
if [ "$PERM" = "admin" ] || [ "$PERM" = "write" ]; then
|
||||
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "authorized=false" >> "$GITHUB_OUTPUT"
|
||||
echo "$USERNAME is neither the trusted bot, an org member, nor a repo writer."
|
||||
fi
|
||||
@@ -11,18 +11,24 @@ on:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude-plan-action:
|
||||
# author_association misses private org members; check-access resolves them via the
|
||||
# internal app token. Both are OR'd below so public members still pass instantly.
|
||||
check-access:
|
||||
if: |
|
||||
(
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/plan')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/plan')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/plan')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '/plan'))
|
||||
) &&
|
||||
(
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association) ||
|
||||
(github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login) == 'windmill-internal-app[bot]'
|
||||
)
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/plan')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/plan')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/plan')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '/plan'))
|
||||
uses: ./.github/workflows/check-write-access.yml
|
||||
with:
|
||||
username: ${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}
|
||||
secrets: inherit
|
||||
|
||||
claude-plan-action:
|
||||
needs: [check-access]
|
||||
if: |
|
||||
needs.check-access.outputs.authorized == 'true' ||
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association)
|
||||
runs-on: ubicloud-standard-4
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
|
||||
@@ -11,18 +11,24 @@ on:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude-code-action:
|
||||
# author_association misses private org members; check-access resolves them via the
|
||||
# internal app token. Both are OR'd below so public members still pass instantly.
|
||||
check-access:
|
||||
if: |
|
||||
(
|
||||
(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/ai') && !startsWith(github.event.comment.body, '/ai-fast')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && startsWith(github.event.comment.body, '/ai') && !startsWith(github.event.comment.body, '/ai-fast')) ||
|
||||
(github.event_name == 'pull_request_review' && startsWith(github.event.review.body, '/ai') && !startsWith(github.event.review.body, '/ai-fast')) ||
|
||||
(github.event_name == 'issues' && startsWith(github.event.issue.body, '/ai') && !startsWith(github.event.issue.body, '/ai-fast'))
|
||||
) &&
|
||||
(
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association) ||
|
||||
(github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login) == 'windmill-internal-app[bot]'
|
||||
)
|
||||
(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/ai') && !startsWith(github.event.comment.body, '/ai-fast')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && startsWith(github.event.comment.body, '/ai') && !startsWith(github.event.comment.body, '/ai-fast')) ||
|
||||
(github.event_name == 'pull_request_review' && startsWith(github.event.review.body, '/ai') && !startsWith(github.event.review.body, '/ai-fast')) ||
|
||||
(github.event_name == 'issues' && startsWith(github.event.issue.body, '/ai') && !startsWith(github.event.issue.body, '/ai-fast'))
|
||||
uses: ./.github/workflows/check-write-access.yml
|
||||
with:
|
||||
username: ${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}
|
||||
secrets: inherit
|
||||
|
||||
claude-code-action:
|
||||
needs: [check-access]
|
||||
if: |
|
||||
needs.check-access.outputs.authorized == 'true' ||
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association)
|
||||
runs-on: ubicloud-standard-8
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -35,12 +35,16 @@ jobs:
|
||||
codex-review:
|
||||
runs-on: ubicloud-standard-2
|
||||
timeout-minutes: 30
|
||||
# A non-fork PR (head.repo.fork == false) can only be opened by someone with push
|
||||
# access to this repo, so fork==false already enforces write access. Do NOT re-add
|
||||
# an author_association gate: the pull_request webhook payload reports private org
|
||||
# members as CONTRIBUTOR/NONE (only public members show as MEMBER), which silently
|
||||
# skips auto-review for every private member.
|
||||
if: |
|
||||
github.event_name == 'workflow_call' ||
|
||||
(
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.head.repo.fork == false &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
|
||||
github.event.pull_request.head.repo.fork == false
|
||||
)
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -5,10 +5,21 @@ on:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
# /command comments can come from anyone; author_association misses private org
|
||||
# members, so check-access resolves them via the internal app token. Runs once and is
|
||||
# OR'd into each job's guard (public members still pass on author_association alone).
|
||||
check-access:
|
||||
if: github.event.issue.pull_request != null && startsWith(github.event.comment.body, '/')
|
||||
uses: ./.github/workflows/check-write-access.yml
|
||||
with:
|
||||
username: ${{ github.event.comment.user.login }}
|
||||
secrets: inherit
|
||||
|
||||
update-sqlx:
|
||||
needs: [check-access]
|
||||
if: >-
|
||||
github.event.issue.pull_request &&
|
||||
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) || github.event.comment.user.login == 'windmill-internal-app[bot]') &&
|
||||
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) || needs.check-access.outputs.authorized == 'true') &&
|
||||
startsWith(github.event.comment.body, '/updatesqlx')
|
||||
runs-on: ubicloud-standard-8
|
||||
permissions:
|
||||
@@ -137,9 +148,10 @@ jobs:
|
||||
})
|
||||
|
||||
demo:
|
||||
needs: [check-access]
|
||||
if: >-
|
||||
github.event.issue.pull_request &&
|
||||
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) || github.event.comment.user.login == 'windmill-internal-app[bot]') &&
|
||||
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) || needs.check-access.outputs.authorized == 'true') &&
|
||||
startsWith(github.event.comment.body, '/demo')
|
||||
runs-on: ubicloud-standard-2
|
||||
permissions:
|
||||
@@ -219,9 +231,10 @@ jobs:
|
||||
fi
|
||||
|
||||
update-ee-ref:
|
||||
needs: [check-access]
|
||||
if: >-
|
||||
github.event.issue.pull_request &&
|
||||
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) || github.event.comment.user.login == 'windmill-internal-app[bot]') &&
|
||||
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) || needs.check-access.outputs.authorized == 'true') &&
|
||||
startsWith(github.event.comment.body, '/eeref')
|
||||
runs-on: ubicloud-standard-2
|
||||
permissions:
|
||||
@@ -307,9 +320,10 @@ jobs:
|
||||
})
|
||||
|
||||
update-docs:
|
||||
needs: [check-access]
|
||||
if: >-
|
||||
github.event.issue.pull_request &&
|
||||
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) || github.event.comment.user.login == 'windmill-internal-app[bot]') &&
|
||||
(contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) || needs.check-access.outputs.authorized == 'true') &&
|
||||
startsWith(github.event.comment.body, '/docs')
|
||||
runs-on: ubicloud-standard-2
|
||||
permissions:
|
||||
|
||||
@@ -33,12 +33,16 @@ jobs:
|
||||
pi-review:
|
||||
runs-on: ubicloud-standard-2
|
||||
timeout-minutes: 30
|
||||
# A non-fork PR (head.repo.fork == false) can only be opened by someone with push
|
||||
# access to this repo, so fork==false already enforces write access. Do NOT re-add
|
||||
# an author_association gate: the pull_request webhook payload reports private org
|
||||
# members as CONTRIBUTOR/NONE (only public members show as MEMBER), which silently
|
||||
# skips auto-review for every private member.
|
||||
if: |
|
||||
github.event_name == 'workflow_call' ||
|
||||
(
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.head.repo.fork == false &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
|
||||
github.event.pull_request.head.repo.fork == false
|
||||
)
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -32,11 +32,16 @@ concurrency:
|
||||
jobs:
|
||||
auto-review:
|
||||
runs-on: ubuntu-latest
|
||||
# A non-fork PR (head.repo.fork == false) can only be opened by someone with push
|
||||
# access to this repo, so fork==false already enforces write access. Do NOT re-add
|
||||
# an author_association gate: the pull_request webhook payload reports private org
|
||||
# members as CONTRIBUTOR/NONE (only public members show as MEMBER), which silently
|
||||
# skips auto-review for every private member.
|
||||
if: |
|
||||
github.event_name == 'workflow_call' ||
|
||||
(
|
||||
(github.event.pull_request.draft == false || github.event.pull_request.ready_for_review == true) &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
|
||||
github.event.pull_request.head.repo.fork == false
|
||||
)
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -42,11 +42,24 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
acknowledge:
|
||||
# author_association misses private org members; check-access resolves them via the
|
||||
# internal app token. Both are OR'd so public members still pass instantly.
|
||||
check-access:
|
||||
needs: [parse]
|
||||
if: needs.parse.outputs.command != ''
|
||||
uses: ./.github/workflows/check-write-access.yml
|
||||
with:
|
||||
username: ${{ github.event.comment.user.login }}
|
||||
secrets: inherit
|
||||
|
||||
acknowledge:
|
||||
needs: [parse, check-access]
|
||||
if: |
|
||||
needs.parse.outputs.command != '' &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
|
||||
(
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
||||
needs.check-access.outputs.authorized == 'true'
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
@@ -63,9 +76,12 @@ jobs:
|
||||
-f content=eyes >/dev/null
|
||||
|
||||
claude:
|
||||
needs: [parse]
|
||||
needs: [parse, check-access]
|
||||
if: |
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) &&
|
||||
(
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
||||
needs.check-access.outputs.authorized == 'true'
|
||||
) &&
|
||||
(needs.parse.outputs.command == 'review' || needs.parse.outputs.command == 'claude')
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -81,9 +97,12 @@ jobs:
|
||||
WINDMILL_EE_PRIVATE_ACCESS: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
|
||||
codex:
|
||||
needs: [parse]
|
||||
needs: [parse, check-access]
|
||||
if: |
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) &&
|
||||
(
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
||||
needs.check-access.outputs.authorized == 'true'
|
||||
) &&
|
||||
(needs.parse.outputs.command == 'review' || needs.parse.outputs.command == 'codex')
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -100,9 +119,12 @@ jobs:
|
||||
WINDMILL_EE_PRIVATE_ACCESS: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
|
||||
pi:
|
||||
needs: [parse]
|
||||
needs: [parse, check-access]
|
||||
if: |
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) &&
|
||||
(
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
||||
needs.check-access.outputs.authorized == 'true'
|
||||
) &&
|
||||
(needs.parse.outputs.command == 'review' || needs.parse.outputs.command == 'pi')
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
Reference in New Issue
Block a user