diff --git a/.github/workflows/guild_new_pr_notify.yml b/.github/workflows/guild_new_pr_notify.yml index babab1c8b9c..8aad061e5f8 100644 --- a/.github/workflows/guild_new_pr_notify.yml +++ b/.github/workflows/guild_new_pr_notify.yml @@ -40,18 +40,23 @@ jobs: with: github-token: ${{ steps.app-token.outputs.token }} script: | - const GUILD_TEAM_SLUG = 'guild-cohort-2'; + // Guild cohort members are outside collaborators holding this custom + // repository role, not members of an org team. + const GUILD_ROLE_NAME = 'Guild Assign issues/PRs'; const pr = context.payload.pull_request; const author = pr.user.login; const isGuildMember = async (username) => { try { - const response = await github.rest.teams.getMembershipForUserInOrg({ - org: 'zed-industries', - team_slug: GUILD_TEAM_SLUG, + const response = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: 'zed-industries', + repo: 'zed', username }); - return response.data.state === 'active'; + // role_name is the effective (highest) role; for cohort outside + // collaborators that is the custom role. Built-in roles come back + // lowercased and won't match. + return (response.data.role_name || '').toLowerCase() === GUILD_ROLE_NAME.toLowerCase(); } catch (error) { if (error.status === 404) { return false; diff --git a/.github/workflows/pr_issue_labeler.yml b/.github/workflows/pr_issue_labeler.yml index f295dcb4f9a..524d301aecc 100644 --- a/.github/workflows/pr_issue_labeler.yml +++ b/.github/workflows/pr_issue_labeler.yml @@ -41,7 +41,9 @@ jobs: const STAFF_TEAM_SLUG = 'staff'; const FIRST_CONTRIBUTION_LABEL = 'first contribution'; const GUILD_LABEL = 'guild'; - const GUILD_TEAM_SLUG = 'guild-cohort-2'; + // Guild cohort members are outside collaborators holding this custom + // repository role, not members of an org team. + const GUILD_ROLE_NAME = 'Guild Assign issues/PRs'; const COMMUNITY_CHAMPION_LABEL = 'community champion'; const COMMUNITY_CHAMPIONS = [ '0x2CA', @@ -138,7 +140,25 @@ jobs: }; const isStaffMember = (author) => isTeamMember(STAFF_TEAM_SLUG, author); - const isGuildMember = (author) => isTeamMember(GUILD_TEAM_SLUG, author); + + const isGuildMember = async (author) => { + try { + const response = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: 'zed-industries', + repo: 'zed', + username: author + }); + // role_name is the effective (highest) role; for cohort outside + // collaborators that is the custom role. Built-in roles come back + // lowercased and won't match. + return (response.data.role_name || '').toLowerCase() === GUILD_ROLE_NAME.toLowerCase(); + } catch (error) { + if (error.status !== 404) { + throw error; + } + return false; + } + }; const getIssueLabels = () => { if (listIncludesAuthor(COMMUNITY_CHAMPIONS, author)) { diff --git a/script/github-guild-board.py b/script/github-guild-board.py index 1fe4c34baaf..7bdd8ab3a2a 100644 --- a/script/github-guild-board.py +++ b/script/github-guild-board.py @@ -38,7 +38,10 @@ RETRY_DELAY_SECONDS = 5 GITHUB_API_URL = "https://api.github.com" REPO_OWNER = "zed-industries" REPO_NAME = "zed" -GUILD_TEAM_SLUG = "guild-cohort-2" +# Cohort members are outside collaborators on the repo holding this custom +# repository role, rather than members of an org team. Rotating the cohort is +# then just adding/removing collaborators, with no org seats involved. +GUILD_ROLE_NAME = "Guild Assign issues/PRs" STATUS_FIELD = "Status" STATUS_IN_PROGRESS = "In Progress" @@ -137,15 +140,19 @@ def github_rest_get_paginated(path): @lru_cache(maxsize=None) def is_guild_member(username): response = requests.get( - f"{GITHUB_API_URL}/orgs/{REPO_OWNER}/teams/{GUILD_TEAM_SLUG}/memberships/{username}", + f"{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/collaborators/{username}/permission", headers=GITHUB_HEADERS, timeout=30, ) + # 404 means the user isn't a collaborator on the repo at all. if response.status_code == 404: return False response.raise_for_status() - # A pending invitation reports state "pending"; only active members count. - return response.json().get("state") == "active" + # role_name is the effective (highest) role for the user. For a cohort of + # outside collaborators whose only grant is this custom role, that is the + # custom role's name; built-in roles come back lowercased and won't match. + role_name = response.json().get("role_name") or "" + return role_name.lower() == GUILD_ROLE_NAME.lower() def issue_comments(issue_number):