Skip to content

Conversation

hmacr
Copy link
Contributor

@hmacr hmacr commented Sep 26, 2025

Noticed that we don't check org membership when determining external pull request contributions.

Related task: https://linear.app/appwrite/issue/SER-355/fix-external-vcs-deployments

Summary by CodeRabbit

  • New Features

    • Adds organization-membership checks to determine whether a pull request contributor is internal or external for organization-owned repositories.
  • Bug Fixes

    • Reduces misclassification of contributors on organization repositories by using organization membership when computing contributor status; behavior for non-organization repositories remains unchanged.
  • Tests

    • Added tests to validate organization membership checks and contributor classification.

Copy link

coderabbitai bot commented Sep 26, 2025

Walkthrough

Adds an abstract organization-membership method to the VCS Adapter, implements it in the GitHub adapter, updates pull_request handling to compute the "external" flag differently for organization-owned repositories, and adds a unit test for membership checks.

Changes

Cohort / File(s) Summary
VCS Adapter API
src/VCS/Adapter.php
Added abstract method isUserMemberOfOrganization(string $username, string $organization): bool.
GitHub Adapter implementation & PR external logic
src/VCS/Adapter/Git/GitHub.php
Added isUserMemberOfOrganization implementation; refactored pull_request handling to detect organization-owned repos and set external as the negation of organization membership; preserved existing head-user vs repo-owner comparison for user-owned repos.
Tests
tests/VCS/Adapter/GitHubTest.php
Added testIsUserMemberOfOrganization asserting positive and negative membership cases.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Webhook as GitHub Webhook
  participant Adapter as GitHub Adapter
  participant API as GitHub API

  rect rgba(235,245,255,0.6)
  Webhook->>Adapter: pull_request payload
  Adapter->>Adapter: isOrgRepository(repo.owner)
  alt owner is Organization
    Adapter->>API: isUserMemberOfOrganization(username, org)
    API-->>Adapter: isMember (bool)
    Adapter->>Adapter: external = !isMember
  else owner is User
    Adapter->>Adapter: external = pr.head.user.login != repo.owner.login
  end
  Adapter-->>Webhook: proceed handling PR with computed external
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I twitch my nose at orgs and names,
I check the list of membership claims.
If you're inside, you pass the door,
If not, you hop outside to explore.
Tests snug, code neat — a rabbit's cheer! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly describes the main change of adding an organization membership check to determine external PR status, following conventional commit style and clearly conveying the purpose.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hmacr/fix-external-pr-determination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.


public function isUserMemberOfOrganization(string $username, string $organization): bool
{
$url = "/orgs/{$organization}/memberships/{$username}";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API needs "Org: read member" permission for the GitHub app, so that's something we need to change for Appwrite's GH app before deploying this.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/VCS/Adapter/Git/GitHub.php (1)

696-701: Tighten membership evaluation to “active” state and handle non-200 explicitly.

Relying on any 2xx can misclassify pending/invited states. Use 200 + state=active.

-    public function isUserMemberOfOrganization(string $username, string $organization): bool
-    {
-        $url = "/orgs/{$organization}/memberships/{$username}";
-        $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]);
-        return $response['headers']['status-code'] >= 200 && $response['headers']['status-code'] < 300;
-    }
+    public function isUserMemberOfOrganization(string $username, string $organization): bool
+    {
+        $url = "/orgs/{$organization}/memberships/{$username}";
+        $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]);
+        $status = (int)($response['headers']['status-code'] ?? 0);
+        if ($status !== 200) {
+            return false;
+        }
+        return strtolower($response['body']['state'] ?? '') === 'active';
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e665ea and 59711d7.

📒 Files selected for processing (3)
  • src/VCS/Adapter.php (1 hunks)
  • src/VCS/Adapter/Git/GitHub.php (2 hunks)
  • tests/VCS/Adapter/GitHubTest.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/VCS/Adapter.php (1)
src/VCS/Adapter/Git/GitHub.php (1)
  • isUserMemberOfOrganization (696-701)
tests/VCS/Adapter/GitHubTest.php (2)
src/VCS/Adapter.php (1)
  • isUserMemberOfOrganization (262-262)
src/VCS/Adapter/Git/GitHub.php (1)
  • isUserMemberOfOrganization (696-701)
src/VCS/Adapter/Git/GitHub.php (1)
src/VCS/Adapter.php (2)
  • isUserMemberOfOrganization (262-262)
  • call (278-387)
🪛 GitHub Actions: Tests
tests/VCS/Adapter/GitHubTest.php

[error] 347-347: Utopia\Tests\VCS\Adapter\GitHubTest::testIsUserMemberOfOrganization: Failed asserting that false is true.

🔇 Additional comments (1)
src/VCS/Adapter/Git/GitHub.php (1)

696-701: Ensure GitHub App has “Organization members: read” permission.

This endpoint requires org membership read for the app installation token; otherwise, it returns 403/404 and you’ll treat all as external.

Comment on lines +255 to +263
/**
* Check if user is a member of an organization
*
* @param string $username Username of the user
* @param string $organization Name of the organization
* @return bool True if user is a member of the organization, false otherwise
*/
abstract public function isUserMemberOfOrganization(string $username, string $organization): bool;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

New abstract contract looks good; verify all concrete adapters implement it.

Ensure no other non-abstract adapters are missing this method.

Run to confirm coverage:


🏁 Script executed:

#!/bin/bash
# Find concrete adapters that might need to implement the new abstract method
rg -nP --type=php -C2 'class\s+\w+\s+extends\s+Utopia\\VCS\\Adapter\\\w+' || true
rg -nP --type=php -C2 '\bfunction\s+isUserMemberOfOrganization\s*\('

Length of output: 669


🏁 Script executed:

#!/bin/bash
# Check each concrete adapter for implementation of isUserMemberOfOrganization
find src/VCS/Adapter -type f -name '*.php' ! -name 'Adapter.php' | while read -r file; do
  if grep -qP 'function\s+isUserMemberOfOrganization\s*\(' "$file"; then
    echo "$file: implements"
  else
    echo "$file: MISSING isUserMemberOfOrganization"
  fi
done

Length of output: 102


Implement isUserMemberOfOrganization in Git adapter
src/VCS/Adapter/Git.php is missing the new abstract method isUserMemberOfOrganization(…); add it to satisfy the Adapter contract.

🤖 Prompt for AI Agents
In src/VCS/Adapter/Git.php around where other Adapter methods are implemented
(add near existing user/org checks), implement the new abstract method
isUserMemberOfOrganization(string $username, string $organization): bool to
satisfy the Adapter contract; the method should call the underlying Git provider
API/client to check whether the given username is a member of the specified
organization and return true or false, gracefully handle API errors (catch
exceptions and return false or propagate according to existing error-handling
patterns used in this adapter), and include appropriate logging consistent with
other methods.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/VCS/Adapter/Git/GitHub.php (1)

644-650: Guard against missing usernames/org to avoid bad API calls.

If either authorUsername or owner is empty, default to external and skip the API call.

Apply this diff:

-                if ($isOrgRepository) {
-                    $external = !$this->isUserMemberOfOrganization($authorUsername, $owner);
-                } else {
-                    $external = $authorUsername !== $owner;
-                }
+                if ($isOrgRepository) {
+                    if (empty($authorUsername) || empty($owner)) {
+                        $external = true;
+                    } else {
+                        $external = !$this->isUserMemberOfOrganization($authorUsername, $owner);
+                    }
+                } else {
+                    $external = ($authorUsername === '') ? true : ($authorUsername !== $owner);
+                }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59711d7 and 4913203.

📒 Files selected for processing (2)
  • src/VCS/Adapter/Git/GitHub.php (2 hunks)
  • tests/VCS/Adapter/GitHubTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/VCS/Adapter/GitHubTest.php
🧰 Additional context used
🧬 Code graph analysis (1)
src/VCS/Adapter/Git/GitHub.php (1)
src/VCS/Adapter.php (2)
  • isUserMemberOfOrganization (262-262)
  • call (278-387)
🔇 Additional comments (2)
src/VCS/Adapter/Git/GitHub.php (2)

644-650: Good fix: membership-based external check for org repos.

You now correctly derive authorUsername and decide external by checking org membership. This addresses the prior logic bug.


697-702: GitHub App permission requirement (read org members).

This endpoint needs the app permission “Organization members: Read”. Ensure it’s enabled before rollout; otherwise membership checks will 404/403 and everything becomes “external” (safe default).

Comment on lines +697 to +702
public function isUserMemberOfOrganization(string $username, string $organization): bool
{
$url = "/orgs/{$organization}/memberships/{$username}";
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]);
return $response['headers']['status-code'] >= 200 && $response['headers']['status-code'] < 300;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Security bug: treating pending org invites as internal.

The memberships API returns 2xx for both active and pending memberships. You must ensure state === 'active'. Also, avoid failing event parsing on transient API errors; default to “not a member”.

Apply this diff:

-    public function isUserMemberOfOrganization(string $username, string $organization): bool
-    {
-        $url = "/orgs/{$organization}/memberships/{$username}";
-        $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]);
-        return $response['headers']['status-code'] >= 200 && $response['headers']['status-code'] < 300;
-    }
+    public function isUserMemberOfOrganization(string $username, string $organization): bool
+    {
+        if ($username === '' || $organization === '') {
+            return false;
+        }
+        try {
+            $url = "/orgs/{$organization}/memberships/{$username}";
+            $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]);
+            $status = $response['headers']['status-code'] ?? 0;
+            if ($status >= 200 && $status < 300) {
+                return (($response['body']['state'] ?? '') === 'active');
+            }
+            return false;
+        } catch (\Throwable $e) {
+            return false;
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function isUserMemberOfOrganization(string $username, string $organization): bool
{
$url = "/orgs/{$organization}/memberships/{$username}";
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]);
return $response['headers']['status-code'] >= 200 && $response['headers']['status-code'] < 300;
}
public function isUserMemberOfOrganization(string $username, string $organization): bool
{
if ($username === '' || $organization === '') {
return false;
}
try {
$url = "/orgs/{$organization}/memberships/{$username}";
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]);
$status = $response['headers']['status-code'] ?? 0;
if ($status >= 200 && $status < 300) {
return (($response['body']['state'] ?? '') === 'active');
}
return false;
} catch (\Throwable $e) {
return false;
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant