Repository: hub4j/github-api
Pull request #1821 (2024-03-20 18:47:06)

Be a lot more flexible as for GHMemberChanges values
Unfortunately, things are not consistent between added and edited for the `member` event.

added provide a permission of some sort and a role name (sometimes...) edited only provides permission and it's not exactly the same ones

For now, I think we need to return strings to avoid issues until GitHub adjusts the API.

@bitwiseman you were entirely right, it's a big mess. My tests didn't show it, I don't know exactly why, maybe it depends on the roles/repositories or if you are using GH or GH Enterprise.

I kept the backwards compatibility and did a translation that is as good as possible.

Unfortunately, I have a user who is blocked by this so if it makes sense to you and you think it goes in the right direction, I would appreciate it being merged and if you could release a new release (sorry I know it's annoying but this inconsistency is really problematic and it was hard to figure it out :/).



Patch:
diff --git a/src/main/java/org/kohsuke/github/GHMemberChanges.java b/src/main/java/org/kohsuke/github/GHMemberChanges.java
index 9f0e5d572..6de93c5dc 100644
--- a/src/main/java/org/kohsuke/github/GHMemberChanges.java
+++ b/src/main/java/org/kohsuke/github/GHMemberChanges.java
@@ -1,5 +1,6 @@
 package org.kohsuke.github;
 
+import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.kohsuke.github.internal.EnumUtils;
 
@@ -11,6 +12,8 @@ public class GHMemberChanges {
 
     private FromToPermission permission;
 
+    private FromRoleName roleName;
+
     /**
      * Get changes to permission.
      *
@@ -20,6 +23,18 @@ public class GHMemberChanges {
         return permission;
     }
 
+    /**
+     * Get changes to the role name.
+     * <p>
+     * Apparently, it is recommended to use this rather than permission if defined. But it will only be defined when
+     * adding and not when editing.
+     *
+     * @return changes to role name
+     */
+    public FromRoleName getRoleName() {
+        return roleName;
+    }
+
     /**
      * Changes to permission.
      */
@@ -34,9 +49,9 @@ public class GHMemberChanges {
          *
          * @return the from
          */
-        public GHOrganization.Permission getFrom() {
-            return EnumUtils
-                    .getNullableEnumOrDefault(GHOrganization.Permission.class, from, GHOrganization.Permission.UNKNOWN);
+        @WithBridgeMethods(value = GHOrganization.Permission.class, adapterMethod = "stringToOrgPermission")
+        public String getFrom() {
+            return from;
         }
 
         /**
@@ -44,9 +59,42 @@ public class GHMemberChanges {
          *
          * @return the to
          */
-        public GHOrganization.Permission getTo() {
-            return EnumUtils
-                    .getNullableEnumOrDefault(GHOrganization.Permission.class, to, GHOrganization.Permission.UNKNOWN);
+        @WithBridgeMethods(value = GHOrganization.Permission.class, adapterMethod = "stringToOrgPermission")
+        public String getTo() {
+            return to;
+        }
+
+        @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification = "Bridge method of getFrom and getTo")
+        private Object stringToOrgPermission(String permissionType, Class type) {
+            switch (permissionType) {
+                case "admin" :
+                    return GHOrganization.Permission.ADMIN;
+                case "none" :
+                    return GHOrganization.Permission.UNKNOWN;
+                case "read" :
+                    return GHOrganization.Permission.PULL;
+                case "write" :
+                    return GHOrganization.Permission.PUSH;
+                default :
+                    return EnumUtils.getNullableEnumOrDefault(GHPermissionType.class, to, GHPermissionType.UNKNOWN);
+            }
+        }
+    }
+
+    /**
+     * Changes to role name.
+     */
+    public static class FromRoleName {
+
+        private String to;
+
+        /**
+         * Gets the to.
+         *
+         * @return the to
+         */
+        public String getTo() {
+            return to;
         }
     }
 }


Test patch:
diff --git a/src/test/java/org/kohsuke/github/BridgeMethodTest.java b/src/test/java/org/kohsuke/github/BridgeMethodTest.java
index eb9138eda..f30aea7f8 100644
--- a/src/test/java/org/kohsuke/github/BridgeMethodTest.java
+++ b/src/test/java/org/kohsuke/github/BridgeMethodTest.java
@@ -60,6 +60,15 @@ public class BridgeMethodTest extends Assert {
 
         verifyBridgeMethods(GHTeam.class, "getId", int.class, long.class, String.class);
 
+        verifyBridgeMethods(GHMemberChanges.FromToPermission.class,
+                "getTo",
+                String.class,
+                GHOrganization.Permission.class);
+        verifyBridgeMethods(GHMemberChanges.FromToPermission.class,
+                "getFrom",
+                String.class,
+                GHOrganization.Permission.class);
+
         // verifyBridgeMethods(GitHub.class, "getMyself", GHMyself.class, GHUser.class);
 
     }
diff --git a/src/test/java/org/kohsuke/github/GHEventPayloadTest.java b/src/test/java/org/kohsuke/github/GHEventPayloadTest.java
index 937e6d8e0..c346d368d 100644
--- a/src/test/java/org/kohsuke/github/GHEventPayloadTest.java
+++ b/src/test/java/org/kohsuke/github/GHEventPayloadTest.java
@@ -4,7 +4,6 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.kohsuke.github.GHCheckRun.Conclusion;
 import org.kohsuke.github.GHCheckRun.Status;
-import org.kohsuke.github.GHOrganization.Permission;
 import org.kohsuke.github.GHProjectsV2Item.ContentType;
 import org.kohsuke.github.GHProjectsV2ItemChanges.FieldType;
 import org.kohsuke.github.GHTeam.Privacy;
@@ -1715,8 +1714,8 @@ public class GHEventPayloadTest extends AbstractGitHubWireMockTest {
         assertThat(member.getLogin(), is("yrodiere"));
 
         GHMemberChanges changes = memberPayload.getChanges();
-        assertThat(changes.getPermission().getFrom(), is(Permission.ADMIN));
-        assertThat(changes.getPermission().getTo(), is(Permission.TRIAGE));
+        assertThat(changes.getPermission().getFrom(), is("admin"));
+        assertThat(changes.getPermission().getTo(), is("triage"));
     }
 
     /**
@@ -1741,7 +1740,33 @@ public class GHEventPayloadTest extends AbstractGitHubWireMockTest {
 
         GHMemberChanges changes = memberPayload.getChanges();
         assertThat(changes.getPermission().getFrom(), is(nullValue()));
-        assertThat(changes.getPermission().getTo(), is(Permission.ADMIN));
+        assertThat(changes.getPermission().getTo(), is("admin"));
+    }
+
+    /**
+     * Member added with role name defined.
+     *
+     * @throws Exception
+     *             the exception
+     */
+    @Test
+    public void member_added_role_name() throws Exception {
+        final GHEventPayload.Member memberPayload = GitHub.offline()
+                .parseEventPayload(payload.asReader(), GHEventPayload.Member.class);
+
+        assertThat(memberPayload.getAction(), is("added"));
+
+        assertThat(memberPayload.getOrganization().getLogin(), is("gsmet-bot-playground"));
+        assertThat(memberPayload.getRepository().getName(), is("github-automation-with-quarkus-demo-playground"));
+
+        GHUser member = memberPayload.getMember();
+        assertThat(member.getId(), is(412878L));
+        assertThat(member.getLogin(), is("yrodiere"));
+
+        GHMemberChanges changes = memberPayload.getChanges();
+        assertThat(changes.getPermission().getFrom(), is(nullValue()));
+        assertThat(changes.getPermission().getTo(), is("write"));
+        assertThat(changes.getRoleName().getTo(), is("maintain"));
     }
 
     /**
diff --git a/src/test/resources/org/kohsuke/github/GHEventPayloadTest/member_added_role_name.json b/src/test/resources/org/kohsuke/github/GHEventPayloadTest/member_added_role_name.json
new file mode 100644
index 000000000..f5ab1a007
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHEventPayloadTest/member_added_role_name.json
@@ -0,0 +1,169 @@
+{
+    "action": "added",
+    "member": {
+        "login": "yrodiere",
+        "id": 412878,
+        "node_id": "MDQ6VXNlcjQxMjg3OA==",
+        "avatar_url": "https://avatars.githubusercontent.com/u/412878?v=4",
+        "gravatar_id": "",
+        "url": "https://api.github.com/users/yrodiere",
+        "html_url": "https://github.com/yrodiere",
+        "followers_url": "https://api.github.com/users/yrodiere/followers",
+        "following_url": "https://api.github.com/users/yrodiere/following{/other_user}",
+        "gists_url": "https://api.github.com/users/yrodiere/gists{/gist_id}",
+        "starred_url": "https://api.github.com/users/yrodiere/starred{/owner}{/repo}",
+        "subscriptions_url": "https://api.github.com/users/yrodiere/subscriptions",
+        "organizations_url": "https://api.github.com/users/yrodiere/orgs",
+        "repos_url": "https://api.github.com/users/yrodiere/repos",
+        "events_url": "https://api.github.com/users/yrodiere/events{/privacy}",
+        "received_events_url": "https://api.github.com/users/yrodiere/received_events",
+        "type": "User",
+        "site_admin": false
+    },
+    "changes": { "permission": { "to": "write" }, "role_name": { "to": "maintain" } },
+    "repository": {
+        "id": 493568123,
+        "node_id": "R_kgDOHWtAew",
+        "name": "github-automation-with-quarkus-demo-playground",
+        "full_name": "gsmet-bot-playground/github-automation-with-quarkus-demo-playground",
+        "private": false,
+        "owner": {
+            "login": "gsmet-bot-playground",
+            "id": 81260024,
+            "node_id": "MDEyOk9yZ2FuaXphdGlvbjgxMjYwMDI0",
+            "avatar_url": "https://avatars.githubusercontent.com/u/81260024?v=4",
+            "gravatar_id": "",
+            "url": "https://api.github.com/users/gsmet-bot-playground",
+            "html_url": "https://github.com/gsmet-bot-playground",
+            "followers_url": "https://api.github.com/users/gsmet-bot-playground/followers",
+            "following_url": "https://api.github.com/users/gsmet-bot-playground/following{/other_user}",
+            "gists_url": "https://api.github.com/users/gsmet-bot-playground/gists{/gist_id}",
+            "starred_url": "https://api.github.com/users/gsmet-bot-playground/starred{/owner}{/repo}",
+            "subscriptions_url": "https://api.github.com/users/gsmet-bot-playground/subscriptions",
+            "organizations_url": "https://api.github.com/users/gsmet-bot-playground/orgs",
+            "repos_url": "https://api.github.com/users/gsmet-bot-playground/repos",
+            "events_url": "https://api.github.com/users/gsmet-bot-playground/events{/privacy}",
+            "received_events_url": "https://api.github.com/users/gsmet-bot-playground/received_events",
+            "type": "Organization",
+            "site_admin": false
+        },
+        "html_url": "https://github.com/gsmet-bot-playground/github-automation-with-quarkus-demo-playground",
+        "description": "Example repository with a demo GitHub app (github-automation-with-quarkus-demo-app) installed",
+        "fork": false,
+        "url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground",
+        "forks_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/forks",
+        "keys_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/keys{/key_id}",
+        "collaborators_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/collaborators{/collaborator}",
+        "teams_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/teams",
+        "hooks_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/hooks",
+        "issue_events_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/issues/events{/number}",
+        "events_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/events",
+        "assignees_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/assignees{/user}",
+        "branches_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/branches{/branch}",
+        "tags_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/tags",
+        "blobs_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/git/blobs{/sha}",
+        "git_tags_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/git/tags{/sha}",
+        "git_refs_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/git/refs{/sha}",
+        "trees_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/git/trees{/sha}",
+        "statuses_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/statuses/{sha}",
+        "languages_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/languages",
+        "stargazers_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/stargazers",
+        "contributors_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/contributors",
+        "subscribers_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/subscribers",
+        "subscription_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/subscription",
+        "commits_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/commits{/sha}",
+        "git_commits_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/git/commits{/sha}",
+        "comments_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/comments{/number}",
+        "issue_comment_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/issues/comments{/number}",
+        "contents_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/contents/{+path}",
+        "compare_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/compare/{base}...{head}",
+        "merges_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/merges",
+        "archive_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/{archive_format}{/ref}",
+        "downloads_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/downloads",
+        "issues_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/issues{/number}",
+        "pulls_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/pulls{/number}",
+        "milestones_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/milestones{/number}",
+        "notifications_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/notifications{?since,all,participating}",
+        "labels_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/labels{/name}",
+        "releases_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/releases{/id}",
+        "deployments_url": "https://api.github.com/repos/gsmet-bot-playground/github-automation-with-quarkus-demo-playground/deployments",
+        "created_at": "2022-05-18T08:07:30Z",
+        "updated_at": "2022-05-19T10:46:46Z",
+        "pushed_at": "2023-07-07T08:22:15Z",
+        "git_url": "git://github.com/gsmet-bot-playground/github-automation-with-quarkus-demo-playground.git",
+        "ssh_url": "git@github.com:gsmet-bot-playground/github-automation-with-quarkus-demo-playground.git",
+        "clone_url": "https://github.com/gsmet-bot-playground/github-automation-with-quarkus-demo-playground.git",
+        "svn_url": "https://github.com/gsmet-bot-playground/github-automation-with-quarkus-demo-playground",
+        "homepage": null,
+        "size": 5,
+        "stargazers_count": 0,
+        "watchers_count": 0,
+        "language": null,
+        "has_issues": true,
+        "has_projects": true,
+        "has_downloads": true,
+        "has_wiki": true,
+        "has_pages": false,
+        "has_discussions": false,
+        "forks_count": 0,
+        "mirror_url": null,
+        "archived": false,
+        "disabled": false,
+        "open_issues_count": 0,
+        "license": {
+            "key": "apache-2.0",
+            "name": "Apache License 2.0",
+            "spdx_id": "Apache-2.0",
+            "url": "https://api.github.com/licenses/apache-2.0",
+            "node_id": "MDc6TGljZW5zZTI="
+        },
+        "allow_forking": true,
+        "is_template": false,
+        "web_commit_signoff_required": false,
+        "topics": [],
+        "visibility": "public",
+        "forks": 0,
+        "open_issues": 0,
+        "watchers": 0,
+        "default_branch": "main",
+        "custom_properties": {}
+    },
+    "organization": {
+        "login": "gsmet-bot-playground",
+        "id": 81260024,
+        "node_id": "MDEyOk9yZ2FuaXphdGlvbjgxMjYwMDI0",
+        "url": "https://api.github.com/orgs/gsmet-bot-playground",
+        "repos_url": "https://api.github.com/orgs/gsmet-bot-playground/repos",
+        "events_url": "https://api.github.com/orgs/gsmet-bot-playground/events",
+        "hooks_url": "https://api.github.com/orgs/gsmet-bot-playground/hooks",
+        "issues_url": "https://api.github.com/orgs/gsmet-bot-playground/issues",
+        "members_url": "https://api.github.com/orgs/gsmet-bot-playground/members{/member}",
+        "public_members_url": "https://api.github.com/orgs/gsmet-bot-playground/public_members{/member}",
+        "avatar_url": "https://avatars.githubusercontent.com/u/81260024?v=4",
+        "description": null
+    },
+    "sender": {
+        "login": "gsmet",
+        "id": 1279749,
+        "node_id": "MDQ6VXNlcjEyNzk3NDk=",
+        "avatar_url": "https://avatars.githubusercontent.com/u/1279749?v=4",
+        "gravatar_id": "",
+        "url": "https://api.github.com/users/gsmet",
+        "html_url": "https://github.com/gsmet",
+        "followers_url": "https://api.github.com/users/gsmet/followers",
+        "following_url": "https://api.github.com/users/gsmet/following{/other_user}",
+        "gists_url": "https://api.github.com/users/gsmet/gists{/gist_id}",
+        "starred_url": "https://api.github.com/users/gsmet/starred{/owner}{/repo}",
+        "subscriptions_url": "https://api.github.com/users/gsmet/subscriptions",
+        "organizations_url": "https://api.github.com/users/gsmet/orgs",
+        "repos_url": "https://api.github.com/users/gsmet/repos",
+        "events_url": "https://api.github.com/users/gsmet/events{/privacy}",
+        "received_events_url": "https://api.github.com/users/gsmet/received_events",
+        "type": "User",
+        "site_admin": false
+    },
+    "installation": {
+        "id": 16779846,
+        "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTY3Nzk4NDY="
+    }
+}
\ No newline at end of file