diff --git a/.github/workflows/real-test.yaml b/.github/workflows/real-test.yaml new file mode 100644 index 0000000..9ff7fe0 --- /dev/null +++ b/.github/workflows/real-test.yaml @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2025 DB Systel GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +name: Test with real GitHub org + +on: + pull_request: + workflow_dispatch: + +jobs: + selftest: + runs-on: ubuntu-24.04 + env: + GITHUB_APP_ID: ${{ secrets.TEST_GITHUB_APP_ID }} + GITHUB_APP_PRIVATE_KEY: ${{ secrets.TEST_GITHUB_APP_PRIVATE_KEY }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: ./.github/actions/poetrybuild + - name: Replace data in config files + run: | + sed -i "s/TEST_GITHUB_ORG/${{ secrets.TEST_GITHUB_ORG }}/g" tests/data/config/org_files/*.yaml + sed -i "s/TEST_USER/${{ secrets.TEST_USER }}/g" tests/data/config/org_files/*.yaml + sed -i "s/TEST_USER/${{ secrets.TEST_USER }}/g" tests/data/config/teams_files/*.yaml + - name: Prepare for first run + run: | + mkdir -p tests/data/config/teams + cp tests/data/config/teams_files/teams_changes.yaml tests/data/config/teams/teams.yaml + cp tests/data/config/org_files/org_changes.yaml tests/data/config/org.yaml + - name: Run 1 (changes) - dry run + run: poetry run gh-org-mgr sync -c tests/data/config/ --dry -vv + - name: Run 1 (changes) - prod run + run: poetry run gh-org-mgr sync -c tests/data/config/ -vv + - name: Prepare for second run, reverting to original state + run: | + cp tests/data/config/teams_files/teams_orig.yaml tests/data/config/teams/teams.yaml + cp tests/data/config/org_files/org_orig.yaml tests/data/config/org.yaml + - name: Run 2 (revert) - dry run + run: poetry run gh-org-mgr sync -c tests/data/config/ --dry -vv + - name: Run 2 (revert) - prod run + run: poetry run gh-org-mgr sync -c tests/data/config/ -vv diff --git a/gh_org_mgr/_gh_org.py b/gh_org_mgr/_gh_org.py index 59c37d7..264ef04 100644 --- a/gh_org_mgr/_gh_org.py +++ b/gh_org_mgr/_gh_org.py @@ -8,14 +8,12 @@ import sys from dataclasses import asdict, dataclass, field -from github import ( - Auth, - Github, +from github import Auth, Github, GithubIntegration +from github.GithubException import ( + BadCredentialsException, GithubException, - GithubIntegration, UnknownObjectException, ) -from github.GithubException import BadCredentialsException from github.NamedUser import NamedUser from github.Organization import Organization from github.Repository import Repository @@ -54,6 +52,7 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines current_repos_collaborators: dict[Repository, dict[str, str]] = field(default_factory=dict) configured_repos_collaborators: dict[str, dict[str, str]] = field(default_factory=dict) archived_repos: list[Repository] = field(default_factory=list) + unconfigured_teams: list[Team] = field(default_factory=list) unconfigured_team_repo_permissions: dict[str, dict[str, str]] = field(default_factory=dict) stats: OrgChanges = field(default_factory=OrgChanges) @@ -296,7 +295,18 @@ def create_missing_teams(self, dry: bool = False): for team, attributes in self.configured_teams.items(): if team not in existent_team_names: if parent := attributes.get("parent"): # type: ignore - parent_id = self.org.get_team_by_slug(sluggify_teamname(parent)).id + try: + parent_id = self.org.get_team_by_slug(sluggify_teamname(parent)).id + except UnknownObjectException: + if dry: + logging.debug( + "For team %s, the configured parent team's ('%s') ID wasn't found, " + "probably because it should be created but it's a dry-run. " + "We set a default ID of 424242", + team, + parent, + ) + parent_id = 424242 logging.info("Creating team '%s' with parent ID '%s'", team, parent_id) self.stats.create_team(team) @@ -594,31 +604,38 @@ def sync_teams_members(self, dry: bool = False) -> None: # pylint: disable=too- team.name, ) - def get_unconfigured_teams( + def get_and_delete_unconfigured_teams( self, dry: bool = False, delete_unconfigured_teams: bool = False ) -> None: """Get all teams that are not configured locally and optionally remove them""" # Get all teams that are not configured locally - unconfigured_teams: list[Team] = [] for team in self.current_teams: if team.name not in self.configured_teams: - unconfigured_teams.append(team) + self.unconfigured_teams.append(team) - if unconfigured_teams: + if self.unconfigured_teams: if delete_unconfigured_teams: - for team in unconfigured_teams: + for team in self.unconfigured_teams: logging.info("Deleting team '%s' as it is not configured locally", team.name) self.stats.delete_team(team=team.name, deleted=True) if not dry: - team.delete() + try: + team.delete() + except UnknownObjectException as e: + logging.info( + "Team '%s' could not be deleted, probably because it was already " + "deleted as part of a parent team. " + "Error: %s", + team.name, + e, + ) else: - unconfigured_teams_str = [team.name for team in unconfigured_teams] logging.warning( "The following teams of your GitHub organisation are not " "configured locally: %s. Taking no action about these teams.", - ", ".join(unconfigured_teams_str), + ", ".join([team.name for team in self.unconfigured_teams]), ) - for team in unconfigured_teams: + for team in self.unconfigured_teams: self.stats.delete_team(team=team.name, deleted=False) def get_members_without_team( diff --git a/gh_org_mgr/manage.py b/gh_org_mgr/manage.py index 09da494..a907602 100644 --- a/gh_org_mgr/manage.py +++ b/gh_org_mgr/manage.py @@ -145,7 +145,7 @@ def main(): org.sync_teams_members(dry=args.dry) # Report and act on teams that are not configured locally log_progress("Checking for unconfigured teams...") - org.get_unconfigured_teams( + org.get_and_delete_unconfigured_teams( dry=args.dry, delete_unconfigured_teams=cfg_app.get("delete_unconfigured_teams", False), ) diff --git a/tests/data/config/.gitignore b/tests/data/config/.gitignore new file mode 100644 index 0000000..b68bb86 --- /dev/null +++ b/tests/data/config/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 DB Systel GmbH +# SPDX-License-Identifier: CC0-1.0 + +org.yaml +teams/teams.yaml diff --git a/tests/data/config/app.yaml b/tests/data/config/app.yaml new file mode 100644 index 0000000..ed3e8fc --- /dev/null +++ b/tests/data/config/app.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 DB Systel GmbH +# SPDX-License-Identifier: CC0-1.0 + +# ------------------------------------------------------------------------------ +# General configuration for this program +# ------------------------------------------------------------------------------ + +# github_token: ghp_test +# github_app_id: 12345 +# github_app_private_key: | +# ------BEGIN RSA PRIVATE KEY----- +# ... +# ------END RSA PRIVATE KEY----- + +# Delete teams that are not configured +delete_unconfigured_teams: true + +# Remove members that are not configured +remove_members_without_team: true diff --git a/tests/data/config/org_files/org_changes.yaml b/tests/data/config/org_files/org_changes.yaml new file mode 100644 index 0000000..f304d8d --- /dev/null +++ b/tests/data/config/org_files/org_changes.yaml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2025 DB Systel GmbH +# SPDX-License-Identifier: CC0-1.0 + +# ------------------------------------------------------------------------------ +# General configuration for the GitHub organisation +# ------------------------------------------------------------------------------ + +# Name of the GitHub organisation +org_name: TEST_GITHUB_ORG +org_owners: + - TEST_USER + +# Default settings. Will be overridden if set in a team. +# If neither defaults nor team settings are present: +# - when creating a new team, will take GitHub's defaults. +# - when syncing settting of a team, will not touch the current status. +defaults: + team: + # Description of a team + # description: "" + # Level of privacy of a team. Can be "secret" or "closed" + privacy: "secret" + # Notification setting of a team. Can be "notifications_enabled" or "notifications_disabled" + notification_setting: "notifications_enabled" diff --git a/tests/data/config/org_files/org_orig.yaml b/tests/data/config/org_files/org_orig.yaml new file mode 100644 index 0000000..2a07c19 --- /dev/null +++ b/tests/data/config/org_files/org_orig.yaml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2025 DB Systel GmbH +# SPDX-License-Identifier: CC0-1.0 + +# ------------------------------------------------------------------------------ +# General configuration for the GitHub organisation +# ------------------------------------------------------------------------------ + +# Name of the GitHub organisation +org_name: TEST_GITHUB_ORG +org_owners: + - TEST_USER + +# Default settings. Will be overridden if set in a team. +# If neither defaults nor team settings are present: +# - when creating a new team, will take GitHub's defaults. +# - when syncing settting of a team, will not touch the current status. +defaults: + team: + # Description of a team + # description: "" + # Level of privacy of a team. Can be "secret" or "closed" + privacy: "closed" + # Notification setting of a team. Can be "notifications_enabled" or "notifications_disabled" + notification_setting: "notifications_enabled" diff --git a/tests/data/config/teams_files/teams_changes.yaml b/tests/data/config/teams_files/teams_changes.yaml new file mode 100644 index 0000000..4b0e673 --- /dev/null +++ b/tests/data/config/teams_files/teams_changes.yaml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2025 DB Systel GmbH +# SPDX-License-Identifier: CC0-1.0 + +Test1: + description: First test team with a new description + privacy: closed + member: + - TEST_USER + +Test2: + parent: Test1 + privacy: closed + description: Second test team + maintainer: + - TEST_USER + +Test3: + notification_setting: notifications_disabled + privacy: closed + +Test3-child: + parent: Test3 + description: Child of Test3 + privacy: closed + member: + - TEST_USER + +Test4: + member: diff --git a/tests/data/config/teams_files/teams_orig.yaml b/tests/data/config/teams_files/teams_orig.yaml new file mode 100644 index 0000000..e93c58a --- /dev/null +++ b/tests/data/config/teams_files/teams_orig.yaml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2025 DB Systel GmbH +# SPDX-License-Identifier: CC0-1.0 + +Test1: + description: First test team + member: + +Test2: + privacy: secret + description: Second test team + member: