diff --git a/.github/workflows/teamsync_check_for_changes.yml b/.github/workflows/teamsync_check_for_changes.yml
new file mode 100644
index 0000000000..b766164213
--- /dev/null
+++ b/.github/workflows/teamsync_check_for_changes.yml
@@ -0,0 +1,40 @@
+name: teamSync check for changes
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ check-permissions:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'adopt'
+
+ - name: Build project
+ run: mvn clean install
+
+ - name: Check for yml changes
+ id: files
+ run: |
+ FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} -- 'permissions/*.yml' | xargs echo)
+ if [[ -z "$FILES" ]]; then
+ echo "No changes detected in permissions files."
+ else
+ echo "Changes detected. Processing..."
+ java -jar target/github_team_sync.jar $FILES
+ fi
+ env:
+ GITHUB_OAUTH: ${{ secrets.TEMP_TEAMSYNC_PAT }}
diff --git a/pom.xml b/pom.xml
index d9bdd68eaf..69851178e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,10 +67,32 @@
package
- src/assembly.xml
+ src/assembly/assembly.xml
+
+ GitHub TeamSync
+ package
+
+ single
+
+
+
+ src/assembly/teamSync.xml
+
+
+
+ io.jenkins.infra.repository_permissions_updater.github_team_sync.TeamSyncExecutor
+
+
+
+ jar-with-dependencies
+
+ github_team_sync
+ false
+
+
@@ -234,6 +256,12 @@
gson
2.11.0
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.7.0
+ test
+
diff --git a/src/assembly.xml b/src/assembly.xml
deleted file mode 100644
index b607329407..0000000000
--- a/src/assembly.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
- bin
-
- dir
-
- false
-
-
- runtime
-
-
-
diff --git a/src/assembly/assembly.xml b/src/assembly/assembly.xml
new file mode 100644
index 0000000000..4f8472d44a
--- /dev/null
+++ b/src/assembly/assembly.xml
@@ -0,0 +1,22 @@
+
+ bin
+
+ dir
+
+ false
+
+
+ target/classes/
+ /
+
+ io/jenkins/infra/repository_permissions_updater/github_team_sync/*.class
+
+
+
+
+
+
+ runtime
+
+
+
diff --git a/src/assembly/teamSync.xml b/src/assembly/teamSync.xml
new file mode 100644
index 0000000000..7de51ea285
--- /dev/null
+++ b/src/assembly/teamSync.xml
@@ -0,0 +1,17 @@
+
+ GitHub TeamSync
+
+ jar
+
+ false
+
+
+ target/classes/
+ /
+
+ io/jenkins/infra/repository_permissions_updater/github_team_sync/*.class
+
+
+
+
+
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/AdditionalTeamDefinition.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/AdditionalTeamDefinition.java
new file mode 100644
index 0000000000..6cc6c335c3
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/AdditionalTeamDefinition.java
@@ -0,0 +1,31 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+public class AdditionalTeamDefinition {
+ private String teamName;
+ private Role role;
+
+ public AdditionalTeamDefinition(String teamName, String role) {
+ this.teamName = teamName;
+ this.role = validateRole(role);
+ }
+
+ public String getName() {
+ return teamName;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+
+ private Role validateRole(String role) {
+ if (role == null) {
+ return null;
+ }
+ try {
+ return Role.valueOf(role.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid team role: " + role);
+ }
+ }
+}
+
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/GitHubService.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/GitHubService.java
new file mode 100644
index 0000000000..4707308e0a
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/GitHubService.java
@@ -0,0 +1,29 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.kohsuke.github.GHOrganization;
+import org.kohsuke.github.GHRepository;
+import org.kohsuke.github.GHTeam;
+
+public interface GitHubService {
+
+ GHOrganization getOrganization(String name) throws IOException;
+
+ void addDeveloperToTeam(GHTeam team, String developer) throws IOException;
+
+ void removeDeveloperFromTeam(GHTeam team, String developer) throws IOException;
+
+ Set getCurrentTeamMembers(GHTeam team) throws IOException;
+
+ GHTeam createTeam(String orgName, String teamName, GHTeam.Privacy privacy) throws IOException;
+
+ void updateTeamRole(GHRepository repo, GHTeam ghTeam, Role role) throws IOException;
+
+ GHTeam getTeamFromRepo(String orgName, String repoName, String teamName) throws IOException;
+
+ void removeTeamFromRepository(GHTeam team, GHRepository repo) throws IOException;
+
+ Set getCurrentTeams(GHRepository repo, GHTeam repoTeam) throws IOException;
+}
\ No newline at end of file
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/GitHubServiceImpl.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/GitHubServiceImpl.java
new file mode 100644
index 0000000000..47d2b2a823
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/GitHubServiceImpl.java
@@ -0,0 +1,106 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.kohsuke.github.*;
+
+
+public class GitHubServiceImpl implements GitHubService {
+ private GitHub github;
+
+ private static final Map PERMISSIONS_MAP = Map.of(
+ Role.READ, GHOrganization.Permission.PULL,
+ Role.TRIAGE, GHOrganization.Permission.TRIAGE,
+ Role.WRITE, GHOrganization.Permission.PUSH,
+ Role.MAINTAIN, GHOrganization.Permission.MAINTAIN,
+ Role.ADMIN, GHOrganization.Permission.ADMIN
+ );
+
+ public GitHubServiceImpl(String oauthToken) {
+ try {
+ this.github = new GitHubBuilder().withOAuthToken(oauthToken).build();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public GHTeam getTeamFromRepo(
+ String repoName, String orgName, String teamName) throws IOException {
+ GHOrganization org = github.getOrganization(orgName);
+ GHRepository repo = org.getRepository(repoName);
+ Set teams = ((GHRepository) repo).getTeams();
+
+ for (GHTeam team : teams) {
+ if (team.getName().equals(teamName)) {
+ return team;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public GHOrganization getOrganization(String name) throws IOException {
+ return github.getOrganization(name);
+ }
+
+
+ @Override
+ public void addDeveloperToTeam(GHTeam team, String developer) throws IOException {
+ GHUser user = github.getUser(developer);
+ team.add(user);
+ }
+
+ @Override
+ public void removeDeveloperFromTeam(GHTeam team, String developer) throws IOException {
+ GHUser user = github.getUser(developer);
+ team.remove(user);
+ }
+
+ @Override
+ public Set getCurrentTeamMembers(GHTeam team) throws IOException {
+ Set members = new HashSet<>();
+ for (GHUser member : team.listMembers()) {
+ members.add(member.getLogin());
+ }
+ return members;
+ }
+
+ @Override
+ public GHTeam createTeam(String orgName, String teamName, GHTeam.Privacy privacy) throws IOException {
+ GHOrganization org = github.getOrganization(orgName);
+ return org.createTeam(teamName).privacy(privacy).create();
+ }
+
+ @Override
+ public void updateTeamRole(GHRepository repo, GHTeam ghTeam, Role role) throws IOException {
+ GHOrganization.Permission permission = PERMISSIONS_MAP.get(role);
+ GHOrganization.RepositoryRole repoRole = GHOrganization.RepositoryRole.from(permission);
+ ghTeam.add(repo, repoRole);
+ }
+
+ @Override
+ public void removeTeamFromRepository(GHTeam team, GHRepository repo) throws IOException {
+ team.remove(repo);
+ }
+
+ /**
+ * Retrieves the names of all additional teams associated with the given GitHub repository, excluding the repo team.
+ * This method returns only team names because the current Java GitHub API does not support retrieving roles
+ * that teams hold within specific repositories. Therefore, role-related information is not available.
+ */
+ @Override
+ public Set getCurrentTeams(GHRepository repo, GHTeam repoTeam) throws IOException {
+ Set allTeams = repo.getTeams();
+
+ return allTeams.stream()
+ .filter(team -> !team.equals(repoTeam))
+ .map(GHTeam::getName)
+ .collect(Collectors.toSet());
+ }
+
+}
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/RepoTeamDefinition.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/RepoTeamDefinition.java
new file mode 100644
index 0000000000..620616980e
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/RepoTeamDefinition.java
@@ -0,0 +1,52 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+import java.util.Set;
+
+public class RepoTeamDefinition {
+
+ private String repoName;
+ private String orgName;
+ private String teamName;
+
+ private static final String DEFAULT_ORG_NAME = "jenkinsci";
+ private final Role role = Role.ADMIN;
+ private Set developers;
+ private Set additionalTeams;
+
+ public RepoTeamDefinition(String repoName, String orgName, String teamName,
+ Set developers, Set additionalTeams) {
+ this.repoName = repoName;
+ this.orgName = orgName != null ? orgName : DEFAULT_ORG_NAME;
+ this.teamName = teamName;
+ this.developers = developers;
+ this.additionalTeams = additionalTeams;
+ }
+
+ public RepoTeamDefinition() {
+ }
+
+
+ public String getRepoName() {
+ return repoName;
+ }
+
+ public String getOrgName() {
+ return orgName;
+ }
+
+ public String getTeamName() {
+ return teamName;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+
+ public Set getDevelopers() {
+ return developers;
+ }
+
+ public Set getAdditionalTeams() {
+ return additionalTeams;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/Role.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/Role.java
new file mode 100644
index 0000000000..8f9b16a30c
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/Role.java
@@ -0,0 +1,9 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+public enum Role {
+ READ,
+ TRIAGE,
+ WRITE,
+ MAINTAIN,
+ ADMIN
+}
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/SpecialTeamDefinition.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/SpecialTeamDefinition.java
new file mode 100644
index 0000000000..9f2079f803
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/SpecialTeamDefinition.java
@@ -0,0 +1,32 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+import java.util.Set;
+
+public class SpecialTeamDefinition{
+ private String orgName;
+ private String teamName;
+ private Set developers;
+
+ private static final String DEFAULT_ORG_NAME = "jenkinsci";
+
+ public SpecialTeamDefinition(String orgName, String teamName, Set developers) {
+ this.orgName = orgName != null ? orgName : DEFAULT_ORG_NAME;
+ this.teamName = teamName;
+ this.developers = developers;
+ }
+
+ public SpecialTeamDefinition() {
+ }
+
+ public String getOrgName() {
+ return orgName;
+ }
+
+ public String getTeamName() {
+ return teamName;
+ }
+
+ public Set getDevelopers() {
+ return developers;
+ }
+}
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/TeamSyncExecutor.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/TeamSyncExecutor.java
new file mode 100644
index 0000000000..f944892ed0
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/TeamSyncExecutor.java
@@ -0,0 +1,50 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TeamSyncExecutor {
+ private static final Logger logger = LoggerFactory.getLogger(TeamSyncExecutor.class);
+ private final TeamUpdater teamUpdater;
+ private final YamlTeamManager yamlTeamManager;
+
+ public TeamSyncExecutor(TeamUpdater teamUpdater, YamlTeamManager yamlTeamManager) {
+ this.teamUpdater = teamUpdater;
+ this.yamlTeamManager = yamlTeamManager;
+ }
+
+ public static void main(String[] args) throws IOException {
+ GitHubService gitHubService = new GitHubServiceImpl(System.getenv("GITHUB_OAUTH"));
+ TeamUpdater teamUpdater = new TeamUpdater(gitHubService);
+ YamlTeamManager yamlTeamManager = new YamlTeamManager(gitHubService, "");
+ TeamSyncExecutor executor = new TeamSyncExecutor(teamUpdater, yamlTeamManager);
+
+ executor.run(args);
+ }
+
+ public void run(String[] args) {
+ if (args.length == 0) {
+ throw new IllegalArgumentException("No file path provided.");
+ }
+
+ for (String yamlFilePath : args) {
+ try {
+ logger.info("Processing team configuration for file: " + yamlFilePath);
+ Object team = yamlTeamManager.loadTeam(yamlFilePath);
+
+ if (team instanceof RepoTeamDefinition) {
+ teamUpdater.updateTeam((RepoTeamDefinition) team);
+ } else if (team instanceof SpecialTeamDefinition) {
+ teamUpdater.updateSpecialTeam((SpecialTeamDefinition) team);
+ } else {
+ throw new IllegalArgumentException("Unsupported team definition type.");
+ }
+ } catch (Exception e) {
+ logger.error("Failed to update team for file " + yamlFilePath + ": " + e.getMessage(), e);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/TeamUpdater.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/TeamUpdater.java
new file mode 100644
index 0000000000..de0824b223
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/TeamUpdater.java
@@ -0,0 +1,135 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.kohsuke.github.GHOrganization;
+import org.kohsuke.github.GHRepository;
+import org.kohsuke.github.GHTeam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class TeamUpdater {
+ private static final Logger logger = LoggerFactory.getLogger(TeamUpdater.class);
+
+ private final GitHubService gitHubService;
+
+
+ public TeamUpdater(GitHubService gitHubService) {
+ this.gitHubService = gitHubService;
+ }
+
+ public void updateTeam(RepoTeamDefinition team) {
+ try {
+ String orgName = team.getOrgName();
+ String teamName = team.getTeamName();
+ GHOrganization org = gitHubService.getOrganization(orgName);
+ String repoName = team.getRepoName();
+ GHRepository repo = org.getRepository(repoName);
+ GHTeam ghTeam = org.getTeamByName(teamName);
+ Role teamRole = team.getRole();
+
+
+ // Create team if it doesn't exist
+ if (ghTeam == null) {
+ ghTeam = gitHubService.createTeam(orgName, teamName, GHTeam.Privacy.CLOSED);
+ logger.info("Team '{}' created.", teamName);
+ gitHubService.updateTeamRole(repo, ghTeam, teamRole);
+ logger.info("Team role for '{}' updated to '{}' in repository '{}'.", teamName, teamRole, repo.getName());
+ }
+
+ // Remove team if team name and developers are all empty
+ if ((teamName == null || teamName.trim().isEmpty()) && team.getDevelopers().isEmpty()) {
+ String potentialTeamName = repoName + " Developers";
+ GHTeam currTeam = gitHubService.getTeamFromRepo(orgName, repoName, potentialTeamName);
+ if (currTeam != null) {
+ gitHubService.removeTeamFromRepository(currTeam, repo);
+ }
+ }
+
+ // Update team members if they are different from the yaml definition
+ updateTeamMembers(ghTeam, team.getDevelopers(), teamName);
+ // Update role of other teams in repository if it's different from the yaml definition
+ updateAdditionalTeam(org, repo, ghTeam, team.getAdditionalTeams());
+
+ } catch (IOException e) {
+ logger.error("Error updating team", e);
+ }
+ }
+
+ public void updateSpecialTeam(SpecialTeamDefinition team) {
+ try {
+ String orgName = team.getOrgName();
+ String teamName = team.getTeamName();
+ GHOrganization org = gitHubService.getOrganization(orgName);
+ GHTeam ghTeam = org.getTeamByName(teamName);
+
+ if (ghTeam == null) {
+ throw new IOException("Team not found: " + teamName);
+ } else {
+ updateTeamMembers(ghTeam, team.getDevelopers(), teamName);
+ }
+
+ } catch (Exception e) {
+ logger.error("Error updating special team", e);
+ }
+
+ }
+
+
+ private void updateTeamMembers(GHTeam ghTeam, Set developers, String teamName) throws IOException {
+ Set currentMembers = gitHubService.getCurrentTeamMembers(ghTeam);
+ // Add new developers from yaml file
+ for (String dev : developers) {
+ if (!currentMembers.contains(dev)) {
+ gitHubService.addDeveloperToTeam(ghTeam, dev);
+ logger.info("Developer: '" + dev + "' added to team: " + teamName);
+ }
+ }
+ // Remove developers not in yaml file
+ for (String member : currentMembers) {
+ if (!developers.contains(member)) {
+ gitHubService.removeDeveloperFromTeam(ghTeam, member);
+ logger.info("Developer: '" + member + "' removed from team: " + teamName);
+ }
+ }
+ }
+
+ public void updateAdditionalTeam(
+ GHOrganization org, GHRepository repo, GHTeam repoTeam,
+ Set additionalTeams) throws IOException {
+
+ Set currentTeamMap = gitHubService.getCurrentTeams(repo, repoTeam);
+
+ Set additionalTeamNames = new HashSet<>();
+ // update roles of additional teams from yaml file
+ for (AdditionalTeamDefinition additionalTeam : additionalTeams) {
+ String name = additionalTeam.getName();
+ Role role = additionalTeam.getRole();
+ GHTeam ghTeam = org.getTeamByName(name);
+ additionalTeamNames.add(name);
+
+ if (ghTeam != null && role != null) {
+ gitHubService.updateTeamRole(repo, ghTeam, role);
+ } else if (ghTeam == null) {
+ logger.error("Additional team not found: " + name);
+ }
+ }
+
+ // remove teams that are not in the yaml file
+ for (String currentTeam : currentTeamMap) {
+ GHTeam ghTeam = org.getTeamByName(currentTeam);
+
+ if (!additionalTeamNames.contains(currentTeam)) {
+ // backfill team name if it's the first run
+ ghTeam.remove(repo);
+ }
+ }
+ }
+}
+
+
+
diff --git a/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/YamlTeamManager.java b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/YamlTeamManager.java
new file mode 100644
index 0000000000..a0d93c684d
--- /dev/null
+++ b/src/main/java/io/jenkins/infra/repository_permissions_updater/github_team_sync/YamlTeamManager.java
@@ -0,0 +1,148 @@
+package io.jenkins.infra.repository_permissions_updater.github_team_sync;
+
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Loads and parses YAML configurations into RepoTeamDefinition objects with security validations.
+ */
+
+public class YamlTeamManager {
+ private static final Logger logger = LoggerFactory.getLogger(YamlTeamManager.class);
+
+ // Defines paths for permission and team YAML files
+ private static final Path PERMISSIONS_PATH = Paths.get("permissions").toAbsolutePath().normalize();
+ private static final Path TEAMS_PATH = Paths.get("teams").toAbsolutePath().normalize();
+
+ private final Path resolvedPath;
+ private final Map teamConfig;
+
+ public YamlTeamManager(GitHubService gitHubService, String filePath) throws IOException {
+ this.resolvedPath = resolveFilePath(filePath);
+ this.teamConfig = loadYamlConfiguration(this.resolvedPath);
+ }
+
+ public Object loadTeam(String filePath) throws IOException {
+ Path resolvedPath = resolveFilePath(filePath);
+ Map teamConfig = loadYamlConfiguration(resolvedPath);
+
+ if (filePath.startsWith("permissions/")) {
+ return parsePermissionsTeamDefinition(teamConfig);
+ } else if (filePath.startsWith("teams/")) {
+ return parseTeamsTeamDefinition(teamConfig);
+ } else {
+ throw new IllegalArgumentException("Unsupported file path: " + filePath);
+ }
+ }
+
+ // Resolves and secures a YAML file path, protecting against path traversal attacks.
+ private Path resolveFilePath(String filePath) {
+ Path basePath = filePath.startsWith("permissions/") ? PERMISSIONS_PATH : TEAMS_PATH;
+ Path resolvedPath = basePath.resolve(filePath.replaceFirst("^(permissions/|teams/)", "")).normalize();
+
+ if (!resolvedPath.startsWith(basePath)) {
+ throw new SecurityException("Attempted path traversal out of allowed directory");
+ }
+ if (!resolvedPath.toString().endsWith(".yml")) {
+ throw new SecurityException("Invalid file type");
+ }
+ if (!Files.exists(resolvedPath)) {
+ throw new RuntimeException("File does not exist: " + resolvedPath);
+ }
+ return resolvedPath;
+ }
+
+
+ private static Map loadYamlConfiguration(Path path) {
+ try (FileInputStream inputStream = new FileInputStream(path.toFile())) {
+ Yaml yaml = new Yaml();
+ return yaml.load(inputStream);
+ } catch (Exception e) {
+ logger.error("Failed to load YAML configuration: {}", path, e);
+ throw new RuntimeException("Failed to load YAML configuration: " + path, e);
+ }
+ }
+
+ private static RepoTeamDefinition parsePermissionsTeamDefinition(
+ Map teamConfig) throws IOException {
+
+ // Extract the repo name and org name from the GitHub key
+ // e.g. github: &GH "jenkinsci/commons-lang3-api-plugin"
+ // orgName = jenkinsci, repoName = commons-lang3-api-plugin
+ String repoPath = (String) teamConfig.getOrDefault("github", "");
+ String[] parts = repoPath.split("/");
+ String orgName = parts[0];
+ String repoName = parts[1];
+
+ // Extract the team name and role
+ // Note: repository_team should always be admin
+ // e.g. repository_team: design-library-plugin-developers
+ // teamName = design-library-plugin-developers, teamRole = admin
+ String repoTeamName = (String) teamConfig.get("repository_team");
+
+ // Extract the developers, example:
+ // developers:
+ // - "user1"
+ // - "user2"
+ Set developers = extractDevelopers(teamConfig);
+
+ // If developers is not empty, then team name is required
+ if ((repoTeamName == null || repoTeamName.trim().isEmpty()) && !developers.isEmpty()) {
+ throw new IllegalArgumentException("No valid team name found.");
+ }
+
+
+ // Extract the additional GitHub teams, which can range from zero to any number, example:
+ // additional_github_teams:
+ // - name: sig-ux
+ // role: admin
+ Set additionalTeams = new HashSet<>();
+ Map additionalTeamsInfo = (Map) teamConfig.get("additional_github_teams");
+ if (additionalTeamsInfo != null) {
+ List