From 079dc48ba0819d6e5247328213a2a1290fbe9c2b Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 13 Apr 2026 10:18:16 -0400 Subject: [PATCH] ci: use GitHub App for community label org membership check (#24149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes #23343. ## Problem `author_association` on `pull_request_target` events is unreliable: - Returns `CONTRIBUTOR` instead of `MEMBER` when both apply ([actions/github-script#643](https://github.com/actions/github-script/issues/643)). - Returns `NONE` for members with private org visibility ([community#18690](https://github.com/orgs/community/discussions/18690)). This causes org members to incorrectly receive the `community` label. ## Approach Replace the `author_association` check with an explicit `orgs.checkMembershipForUser()` API call, which reliably detects both public and private org members. Uses a dedicated **GitHub App** via `actions/create-github-app-token` instead of a PAT. The App only needs **Organization > Members: Read** permission. Installation tokens are short-lived (1 hour) and auto-rotated — no long-lived secrets to worry about. ### Setup required A repo/org admin needs to: 1. Create a GitHub App with only **Organization > Members: Read** permission. 2. Install it on the `coder` org. 3. Store the App ID as a repository variable: `ORG_MEMBERSHIP_APP_ID`. 4. Store the App's private key as a repository secret: `ORG_MEMBERSHIP_APP_PRIVATE_KEY`. > [!NOTE] > Generated by Coder Agents --------- Co-authored-by: Jakub Domeracki --- .github/workflows/contrib.yaml | 45 ++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/.github/workflows/contrib.yaml b/.github/workflows/contrib.yaml index bf81ece746..6e670f8332 100644 --- a/.github/workflows/contrib.yaml +++ b/.github/workflows/contrib.yaml @@ -30,16 +30,28 @@ jobs: if: >- ${{ github.event_name == 'pull_request_target' && - github.event.action == 'opened' && - github.event.pull_request.author_association != 'MEMBER' && - github.event.pull_request.author_association != 'COLLABORATOR' && - github.event.pull_request.author_association != 'OWNER' + github.event.action == 'opened' }} steps: + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + app-id: ${{ vars.ORG_MEMBERSHIP_APP_ID }} + private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }} - name: Add community label uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} with: + # Default GITHUB_TOKEN handles label writes via the + # `github` object (needs pull-requests: write). The App + # token is scoped to members: read only and used via a + # separate Octokit client for the membership check. script: | + const { Octokit } = require("@octokit/rest") + const orgClient = new Octokit({ auth: process.env.APP_TOKEN }) + const params = { issue_number: context.issue.number, owner: context.repo.owner, @@ -52,10 +64,27 @@ jobs: return } - console.log( - 'Adding "community" label for author association "%s".', - context.payload.pull_request.author_association, - ) + // author_association can be unreliable: it returns + // CONTRIBUTOR instead of MEMBER when both apply, and + // returns NONE for members with private org visibility. + // Use the org membership API as the source of truth. + // See: https://github.com/actions/github-script/issues/643 + const author = context.payload.pull_request.user.login + try { + await orgClient.orgs.checkMembershipForUser({ + org: context.repo.owner, + username: author, + }) + console.log('Author "%s" is an org member, skipping.', author) + return + } catch (error) { + if (error.status !== 404 && error.status !== 302) { + throw error + } + } + + console.log('Adding "community" label for author "%s".', author) + // Uses the default GITHUB_TOKEN via the `github` object. await github.rest.issues.addLabels({ ...params, labels: ["community"],