Skip to content

Commit 0fca61d

Browse files
ci: label external-contributor PRs (#9641)
## Description Adds a new GitHub Actions workflow, `Label External Contributors`, that runs whenever a pull request is opened and applies the existing `external-contributor` label when: * the PR is **not** authored by a bot (`user.type == 'Bot'` or login ending in `[bot]`), **and** * either of the following is true (logical OR): * the author is **not** a member of the `warpdotdev` GitHub organization, **or** * the PR head repository is a fork (`pr.head.repo.full_name != pr.base.repo.full_name`). The workflow triggers on `pull_request_target` so the `GITHUB_TOKEN` has the `pull-requests: write` permission needed to label PRs that come from forks. The top-level workflow token stays read-only and only the labeling job widens permissions. The script never checks out the PR's code — it only reads the event payload and calls a couple of REST endpoints — so the `pull_request_target` trigger is safe. Org membership is determined via the GitHub REST membership API. If that call cannot be performed (for example, when `GITHUB_TOKEN` lacks org-membership context for a private member), we fall back to the PR's `author_association` field (`MEMBER` or `OWNER` is treated as internal). The same heuristic was applied retroactively to all currently-open PRs in the repo. The retrospective pass evaluated 148 open PRs, skipped 20 bot-authored PRs, labeled 89 external-contributor PRs (all of which were forks from non-org members), and left 39 internal-author PRs untouched. ## Linked Issue N/A — direct request from the team Slack channel. - [x] Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes). ## Screenshots / Videos Not applicable: this PR only adds a CI workflow. ## Testing * Validated the workflow YAML parses cleanly. * Exercised the heuristic locally across all 148 currently-open PRs (89 expected to be labeled external, 20 bots skipped, 39 internal — all matched expectations). * Verified after retrospectively applying labels that `gh pr list --state open --label external-contributor` now returns 89 PRs, matching the dry-run prediction. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode _Conversation: https://staging.warp.dev/conversation/cf25ce7e-4d2d-4880-a3a3-8c4242f7c0d5_ _Run: https://oz.staging.warp.dev/runs/019ddf8f-1365-7da4-9c47-75aee57c151c_ _This PR was generated with [Oz](https://warp.dev/oz)._ --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent 10ec3d1 commit 0fca61d

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# ======================================================================================
2+
# Workflow: Label External Contributors
3+
# ======================================================================================
4+
# Usage:
5+
# - Runs whenever a pull request is opened.
6+
# - Adds the `external-contributor` label to the PR if either of the following is
7+
# true (logical OR), and the PR is not authored by a bot:
8+
# 1. The PR author is not a member of the `warpdotdev` GitHub organization.
9+
# 2. The PR head repository is a fork (i.e. it does not belong to the same
10+
# repository as the base).
11+
#
12+
# Notes:
13+
# - The workflow triggers on `pull_request_target` rather than `pull_request` so
14+
# that it has the `pull-requests: write` permission needed to apply labels even
15+
# when the PR is opened from a fork. Because we never check out the PR's code
16+
# and only read the event payload, this trigger is safe.
17+
# - Org membership is determined via the GitHub REST API. We fall back to the
18+
# PR's `author_association` field when the API call cannot be performed (for
19+
# example, when the GITHUB_TOKEN lacks org-membership context for private
20+
# members).
21+
# ======================================================================================
22+
23+
name: Label External Contributors
24+
25+
on:
26+
pull_request_target:
27+
types: [opened]
28+
29+
# Default to a read-only token. The job below widens permissions explicitly.
30+
permissions:
31+
contents: read
32+
33+
jobs:
34+
label-external-contributor:
35+
name: Label external-contributor PRs
36+
runs-on: ubuntu-latest
37+
permissions:
38+
pull-requests: write
39+
steps:
40+
- name: Determine and apply external-contributor label
41+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
42+
with:
43+
script: |
44+
const pr = context.payload.pull_request;
45+
const author = pr.user.login;
46+
47+
// Ignore PRs authored by bots.
48+
if (pr.user.type === 'Bot' || author.endsWith('[bot]')) {
49+
console.log(`Skipping bot user: ${author}`);
50+
return;
51+
}
52+
53+
// The PR comes from a fork if its head repo differs from its base repo.
54+
const isFork =
55+
!pr.head.repo ||
56+
pr.head.repo.full_name !== pr.base.repo.full_name;
57+
58+
// Check whether the author is a member of the warpdotdev org. The
59+
// membership API requires the requester to be a member of the org.
60+
// When it cannot return a definitive result, fall back to the PR's
61+
// author_association field, which GitHub computes based on the
62+
// author's relationship to the repository.
63+
let isOrgMember =
64+
pr.author_association === 'MEMBER' ||
65+
pr.author_association === 'OWNER';
66+
try {
67+
await github.rest.orgs.checkMembershipForUser({
68+
org: 'warpdotdev',
69+
username: author,
70+
});
71+
isOrgMember = true;
72+
} catch (error) {
73+
console.log(
74+
`checkMembershipForUser failed (${error.status}); ` +
75+
`falling back to author_association="${pr.author_association}"`,
76+
);
77+
}
78+
79+
const isExternal = !isOrgMember || isFork;
80+
console.log(
81+
`PR #${pr.number} by ${author}: ` +
82+
`isFork=${isFork}, isOrgMember=${isOrgMember}, ` +
83+
`isExternal=${isExternal}`,
84+
);
85+
86+
if (!isExternal) {
87+
return;
88+
}
89+
90+
await github.rest.issues.addLabels({
91+
owner: context.repo.owner,
92+
repo: context.repo.repo,
93+
issue_number: pr.number,
94+
labels: ['external-contributor'],
95+
});
96+
console.log(`Labeled PR #${pr.number} as external-contributor`);

0 commit comments

Comments
 (0)