diff --git a/.github/labeler.yml b/.github/labeler.yml index a27da3e815b..caceb8ae472 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,188 +1,319 @@ -"Category: Admin": +"Category: CI": + - .github/workflows/**/* + + +"Category: Cogs - Admin": # Source - redbot/cogs/admin/* # Docs - docs/cog_guides/admin.rst -"Category: Alias": + - docs/.resources/admin/**/* +"Category: Cogs - Alias": # Source - redbot/cogs/alias/* # Docs - docs/cog_guides/alias.rst -"Category: Audio Cog": + # Tests + - redbot/pytest/alias.py + - tests/cogs/test_alias.py + - docs/.resources/alias/**/* +"Category: Cogs - Audio": + # Source - any: - # Source - redbot/cogs/audio/**/* - # Docs - - docs/cog_guides/audio.rst - all: - "!redbot/cogs/audio/**/locales/*" -"Category: Bank API": - # Source - - redbot/core/bank.py - # Docs - - docs/framework_bank.rst -"Category: Bot Core": - # Source - - redbot/* - - redbot/core/__init__.py - - redbot/core/_debuginfo.py - - redbot/core/_diagnoser.py - - redbot/core/_sharedlibdeprecation.py - - redbot/core/bot.py - - redbot/core/checks.py - - redbot/core/cli.py - - redbot/core/cog_manager.py - - redbot/core/core_commands.py - - redbot/core/data_manager.py - - redbot/core/errors.py - - redbot/core/events.py - - redbot/core/global_checks.py - - redbot/core/settings_caches.py # Docs - - docs/framework_apikeys.rst - - docs/framework_bot.rst - - docs/framework_cogmanager.rst - - docs/framework_datamanager.rst - - docs/framework_events.rst - - docs/cog_guides/cog_manager_ui.rst - - docs/cog_guides/core.rst -"Category: CI": - - .github/workflows/* -"Category: Cleanup Cog": + - docs/cog_guides/audio.rst +"Category: Cogs - Bank": [] # historical label for a removed cog +"Category: Cogs - Cleanup": # Source - redbot/cogs/cleanup/* # Docs - docs/cog_guides/cleanup.rst -"Category: Command Module": - # Source - - any: - # Source - - redbot/core/commands/* - # Docs - - docs/framework_checks.rst - - docs/framework_commands.rst - all: - - "!redbot/core/commands/help.py" -"Category: Config": - # Source - - redbot/core/drivers/* - - redbot/core/config.py - # Docs - - docs/framework_config.rst -"Category: CustomCom": +"Category: Cogs - CustomCommands": # Source - redbot/cogs/customcom/* # Docs - docs/cog_customcom.rst - docs/cog_guides/customcommands.rst -"Category: Dev Cog": +"Category: Cogs - Dev": # Source - redbot/core/dev_commands.py # Docs - docs/cog_guides/dev.rst -"Category: Docs": - - docs/**/* -"Category: Downloader": +"Category: Cogs - Downloader": # Source - redbot/cogs/downloader/* # Docs - docs/cog_guides/downloader.rst -"Category: Economy Cog": + # Tests + - redbot/pytest/downloader.py + - redbot/pytest/downloader_testrepo.* + - tests/cogs/downloader/**/* +"Category: Cogs - Economy": # Source - redbot/cogs/economy/* # Docs - docs/cog_guides/economy.rst -"Category: Filter": + # Tests + - redbot/pytest/economy.py + - tests/cogs/test_economy.py +"Category: Cogs - Filter": # Source - redbot/cogs/filter/* # Docs - docs/cog_guides/filter.rst -"Category: General Cog": +"Category: Cogs - General": # Source - redbot/cogs/general/* # Docs - docs/cog_guides/general.rst -"Category: Help": - - redbot/core/commands/help.py -"Category: i18n": - # Source - - redbot/core/i18n.py - # Locale files - - redbot/**/locales/* - # Docs - - docs/framework_i18n.rst -"Category: Image": +"Category: Cogs - Image": # Source - redbot/cogs/image/* # Docs - docs/cog_guides/image.rst -"Category: Meta": - - ./* - - .github/* - - .github/ISSUE_TEMPLATE/* - - .github/PULL_REQUEST_TEMPLATE/* - - schema/* - - tools/* -"Category: Mod Cog": +"Category: Cogs - Mod": # Source - redbot/cogs/mod/* # Docs - docs/cog_guides/mod.rst -"Category: Modlog API": - # Source - - redbot/core/generic_casetypes.py - - redbot/core/modlog.py - # Docs - - docs/framework_modlog.rst -"Category: Modlog Cog": + # Tests + - redbot/pytest/mod.py + - tests/cogs/test_mod.py +"Category: Cogs - Modlog": # Source - redbot/cogs/modlog/* # Docs - docs/cog_guides/modlog.rst -"Category: Mutes Cog": +"Category: Cogs - Mutes": # Source - redbot/cogs/mutes/* # Docs - docs/cog_guides/mutes.rst -"Category: Permissions": +"Category: Cogs - Permissions": # Source - redbot/cogs/permissions/* # Docs - docs/cog_guides/permissions.rst - docs/cog_permissions.rst -"Category: Reports Cog": + # Tests + - redbot/pytest/permissions.py + - tests/cogs/test_permissions.py +"Category: Cogs - Reports": # Source - redbot/cogs/reports/* # Docs - docs/cog_guides/reports.rst -"Category: RPC/ZMQ API": - # Source - - redbot/core/rpc.py - # Docs - - docs/framework_rpc.rst -"Category: Streams": +"Category: Cogs - Streams": # Source - redbot/cogs/streams/* # Docs - docs/cog_guides/streams.rst -"Category: Tests": - - redbot/pytest/* - - tests/**/* -"Category: Trivia Cog": +"Category: Cogs - Trivia": # Source - redbot/cogs/trivia/* # Docs - docs/cog_guides/trivia.rst - docs/guide_trivia_list_creation.rst -"Category: Trivia Lists": + - docs/.resources/trivia/**/* + # Tests + - tests/cogs/test_trivia.py +"Category: Cogs - Trivia - Lists": - redbot/cogs/trivia/data/lists/* -"Category: Utility Functions": +"Category: Cogs - Warnings": # Source - - redbot/core/utils/* + - redbot/cogs/warnings/* + # Docs + - docs/cog_guides/warnings.rst + + +"Category: Core - API - Audio": [] # potential future feature +"Category: Core - API - Bank": + # Source + - redbot/core/bank.py + # Docs + - docs/framework_bank.rst +"Category: Core - API - Commands Package": + # Source + - any: + - redbot/core/commands/* + - "!redbot/core/commands/help.py" + # this isn't in commands package but it just re-exports things from it + - redbot/core/checks.py + # Docs + - docs/framework_checks.rst + - docs/framework_commands.rst + # Tests + - tests/core/test_commands.py +"Category: Core - API - Config": + # Source + - any: + - redbot/core/drivers/**/* + - "!redbot/core/drivers/**/locales/*" + - redbot/core/config.py + # Docs + - docs/framework_config.rst + # Tests + - tests/core/test_config.py +"Category: Core - API - Other": + # Source + - redbot/__init__.py + - redbot/core/__init__.py + - redbot/core/cog_manager.py # TODO: privatize cog manager module + - redbot/core/data_manager.py + - redbot/core/errors.py + # Docs + - docs/framework_cogmanager.rst # TODO: privatize cog manager module + - docs/framework_datamanager.rst + # Tests + - redbot/pytest/cog_manager.py # TODO: privatize cog manager module + - redbot/pytest/data_manager.py + - tests/core/test_cog_manager.py + - tests/core/test_data_manager.py + - tests/core/test_version.py +"Category: Core - API - Utils Package": + # Source + - any: + - redbot/core/utils/* + - "!redbot/core/utils/_internal_utils.py" # Docs - docs/framework_utils.rst -"Category: Warnings": + # Tests + - tests/core/test_utils.py +"Category: Core - Bot Class": # Source - - redbot/cogs/warnings/* + - redbot/core/bot.py # Docs - - docs/cog_guides/warnings.rst + - docs/framework_apikeys.rst + - docs/framework_bot.rst +"Category: Core - Bot Commands": + # Source + - redbot/core/core_commands.py + - redbot/core/_diagnoser.py + # Docs + - docs/.resources/cog_manager_ui/**/* + - docs/cog_guides/cog_manager_ui.rst + - docs/cog_guides/core.rst +"Category: Core - Command-line Interfaces": + - redbot/__main__.py + - redbot/launcher.py + - redbot/logging.py + - redbot/core/_debuginfo.py + - redbot/core/cli.py + - redbot/setup.py +"Category: Core - Help": + - redbot/core/commands/help.py +"Category: Core - i18n": + # Source + - redbot/core/i18n.py + # Locale files + - redbot/**/locales/* + # Docs + - docs/framework_i18n.rst +"Category: Core - Modlog": + # Source + - redbot/core/generic_casetypes.py + - redbot/core/modlog.py + # Docs + - docs/framework_modlog.rst +"Category: Core - Other Internals": + # Source + - redbot/core/_sharedlibdeprecation.py + - redbot/core/events.py + - redbot/core/global_checks.py + - redbot/core/settings_caches.py + - redbot/core/utils/_internal_utils.py + # Tests + - redbot/pytest/__init__.py + - redbot/pytest/core.py + - tests/core/test_installation.py +"Category: Core - RPC/ZMQ": + # Source + - redbot/core/rpc.py + # Docs + - docs/framework_rpc.rst + # Tests + - redbot/pytest/rpc.py + - tests/core/test_rpc.py + - tests/rpc_test.html + + +"Category: Docker": [] # potential future feature + + +"Category: Docs - Changelogs": + - docs/changelog_*.rst + - docs/release_notes_*.rst +"Category: Docs - For Developers": + - docs/framework_events.rst + - docs/guide_cog_creation.rst + - docs/guide_cog_creators.rst + - docs/guide_migration.rst + - docs/guide_publish_cogs.rst +"Category: Docs - Install Guides": + - docs/about_venv.rst + - docs/autostart_*.rst + - docs/.resources/bot-guide/**/* + - docs/bot_application_guide.rst + - docs/install_guides/**/* + - docs/update_red.rst +"Category: Docs - Other": + - docs/host-list.rst + - docs/index.rst + - docs/version_guarantees.rst + - README.md +"Category: Docs - User Guides": + - docs/getting_started.rst + - docs/intents.rst + - docs/red_core_data_statement.rst + # TODO: move these to `docs/.resources/getting_started` subfolder + - docs/.resources/red-console.png + - docs/.resources/code-grant.png + - docs/.resources/instances-ssh-button.png + - docs/.resources/ssh-output.png + + +"Category: Meta": + # top-level files + - any: + - '*' + - '!README.md' + # .gitattributes files + - '**/.gitattributes' + # GitHub configuration files, with the exception of CI configuration + - .github/* + - .github/ISSUE_TEMPLATE/* + - .github/PULL_REQUEST_TEMPLATE/* + # documentation configuration, extensions, scripts, templates, etc. + - docs/conf.py + - docs/_ext/**/* + - docs/_html/**/* + - docs/make.bat + - docs/Makefile + - docs/prolog.txt + - docs/_templates/**/* + # empty file + - redbot/cogs/__init__.py + # can't go more meta than that :) + # TODO: remove this useless file + - redbot/meta.py + # py.typed file + - redbot/py.typed + # requirements files + - requirements/* + # schema files + - schema/* + # tests configuration, global fixtures, etc. + - tests/conftest.py + - tests/__init__.py + - tests/*/__init__.py + # repository tools + - tools/* + + +# "Category: RPC/ZMQ methods": [] # can't be matched by file patterns + + +"Category: Vendored Packages": + - redbot/vendored/**/* diff --git a/.github/workflows/auto_labeler_issues.yml b/.github/workflows/auto_labeler_issues.yml index 6d4ce5a27b9..2dc30274d7f 100644 --- a/.github/workflows/auto_labeler_issues.yml +++ b/.github/workflows/auto_labeler_issues.yml @@ -7,8 +7,7 @@ permissions: issues: write jobs: - build: - + apply_triage_label_to_issues: runs-on: ubuntu-latest steps: - name: Apply Triage Label diff --git a/.github/workflows/auto_labeler_pr.yml b/.github/workflows/auto_labeler_pr.yml index 759c27b5430..8939e91f386 100644 --- a/.github/workflows/auto_labeler_pr.yml +++ b/.github/workflows/auto_labeler_pr.yml @@ -1,16 +1,27 @@ name: Auto Labeler - PRs on: pull_request_target: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled permissions: pull-requests: write jobs: - build: + label_pull_requests: runs-on: ubuntu-latest steps: - name: Apply Type Label uses: actions/labeler@v4 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - sync-labels: "" # this is a temporary workaround, see #4844 + sync-labels: true + + - name: Label documentation-only changes. + uses: Jackenmen/label-doconly-changes@v1 + env: + LDC_LABELS: Docs-only diff --git a/.github/workflows/check_label_pattern_exhaustiveness.yaml b/.github/workflows/check_label_pattern_exhaustiveness.yaml new file mode 100644 index 00000000000..8cef9dad4c8 --- /dev/null +++ b/.github/workflows/check_label_pattern_exhaustiveness.yaml @@ -0,0 +1,23 @@ +name: Check label pattern exhaustiveness +on: + pull_request: + push: + +jobs: + check_label_pattern_exhaustiveness: + name: Check label pattern exhaustiveness + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.8" + - name: Install script's pre-requirements + run: | + python -m pip install -U pip + python -m pip install -U pathspec pyyaml rich + - name: Check label pattern exhaustiveness + run: | + python .github/workflows/scripts/check_label_pattern_exhaustiveness.py diff --git a/.github/workflows/scripts/check_label_pattern_exhaustiveness.py b/.github/workflows/scripts/check_label_pattern_exhaustiveness.py new file mode 100644 index 00000000000..1982b0b6936 --- /dev/null +++ b/.github/workflows/scripts/check_label_pattern_exhaustiveness.py @@ -0,0 +1,215 @@ +import itertools +import operator +import os +import subprocess +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional +from typing_extensions import Self + +import rich +import yaml +from rich.console import Console, ConsoleOptions, RenderResult +from rich.tree import Tree +from pathspec import PathSpec +from pathspec.patterns.gitwildmatch import GitWildMatchPattern + + +ROOT_PATH = Path(__file__).resolve().parents[3] + + +class Matcher: + def __init__(self, *, any: Iterable[str] = (), all: Iterable[str] = ()) -> None: + self.any_patterns = tuple(any) + self.any_specs = self._get_pathspecs(self.any_patterns) + self.all_patterns = tuple(all) + self.all_specs = self._get_pathspecs(self.all_patterns) + + def __repr__(self) -> str: + return f"Matcher(any={self.any_patterns!r}, all={self.all_patterns!r})" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return ( + self.any_patterns == other.any_patterns and self.all_patterns == other.all_patterns + ) + return NotImplemented + + def __hash__(self) -> int: + return hash((self.any_patterns, self.all_patterns)) + + @classmethod + def _get_pathspecs(cls, patterns: Iterable[str]) -> List[PathSpec]: + return tuple( + PathSpec.from_lines(GitWildMatchPattern, cls._get_pattern_lines(pattern)) + for pattern in patterns + ) + + @staticmethod + def _get_pattern_lines(pattern: str) -> List[str]: + # an approximation of actions/labeler's minimatch globs + if pattern.startswith("!"): + pattern_lines = ["*", f"!/{pattern[1:]}"] + else: + pattern_lines = [f"/{pattern}"] + if pattern.endswith("*") and "**" not in pattern: + pattern_lines.append(f"!/{pattern}/") + return pattern_lines + + @classmethod + def get_label_matchers(cls) -> Dict[str, List[Self]]: + with open(ROOT_PATH / ".github/labeler.yml", encoding="utf-8") as fp: + label_definitions = yaml.safe_load(fp) + label_matchers: Dict[str, List[Matcher]] = {} + for label_name, matcher_definitions in label_definitions.items(): + matchers = label_matchers[label_name] = [] + for idx, matcher_data in enumerate(matcher_definitions): + if isinstance(matcher_data, str): + matchers.append(cls(any=[matcher_data])) + elif isinstance(matcher_data, dict): + matchers.append( + cls(any=matcher_data.pop("any", []), all=matcher_data.pop("all", [])) + ) + if matcher_data: + raise RuntimeError( + f"Unexpected keys at index {idx} for label {label_name!r}: " + + ", ".join(map(repr, matcher_data)) + ) + elif matcher_data is not None: + raise RuntimeError(f"Unexpected type at index {idx} for label {label_name!r}") + + return label_matchers + + +class PathNode: + def __init__(self, parent_tree: Tree, path: Path, *, label: Optional[str] = None) -> None: + self.parent_tree = parent_tree + self.path = path + self.label = label + + def __rich__(self) -> str: + if self.label is not None: + return self.label + return self.path.name + + +class DirectoryTree: + def __init__(self, label: str) -> None: + self.root = Tree(PathNode(Tree(""), Path(), label=label)) + self._previous = self.root + + def __bool__(self) -> bool: + return bool(self.root.children) + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield from self.root.__rich_console__(console, options) + + def add(self, file: Path) -> Tree: + common_path = Path(os.path.commonpath([file.parent, self._previous.label.path])) + + parent_tree = self._previous + while parent_tree != self.root and parent_tree.label.path != common_path: + parent_tree = parent_tree.label.parent_tree + + for part in file.relative_to(common_path).parts: + if parent_tree.label.path.name == "locales": + if not parent_tree.children: + parent_tree.add(PathNode(parent_tree, parent_tree.label.path / "*.po")) + continue + parent_tree = parent_tree.add(PathNode(parent_tree, parent_tree.label.path / part)) + + self._previous = parent_tree + return parent_tree + + +class App: + def __init__(self) -> None: + self.exit_code = 0 + self.label_matchers = Matcher.get_label_matchers() + self.tracked_files = [ + Path(filename) + for filename in subprocess.check_output( + ("git", "ls-tree", "-r", "HEAD", "--name-only"), encoding="utf-8", cwd=ROOT_PATH + ).splitlines() + ] + self.matches_per_label = {label_name: set() for label_name in self.label_matchers} + self.matches_per_file = [] + self.used_matchers = set() + + def run(self) -> int: + old_cwd = os.getcwd() + try: + os.chdir(ROOT_PATH) + self._run() + finally: + os.chdir(old_cwd) + return self.exit_code + + def _run(self) -> None: + self._collect_match_information() + self._show_matches_per_label() + self._show_files_without_labels() + self._show_files_with_multiple_labels() + self._show_unused_matchers() + + def _collect_match_information(self) -> None: + tmp_matches_per_file = {file: [] for file in self.tracked_files} + + for file in self.tracked_files: + for label_name, matchers in self.label_matchers.items(): + matched = False + for matcher in matchers: + if all( + path_spec.match_file(file) + for path_spec in itertools.chain(matcher.all_specs, matcher.any_specs) + ): + self.matches_per_label[label_name].add(file) + matched = True + self.used_matchers.add(matcher) + if matched: + tmp_matches_per_file[file].append(label_name) + + self.matches_per_file = sorted(tmp_matches_per_file.items(), key=operator.itemgetter(0)) + + def _show_matches_per_label(self) -> None: + for label_name, files in self.matches_per_label.items(): + top_tree = DirectoryTree(f"{label_name}:") + for file in sorted(files): + top_tree.add(file) + rich.print(top_tree) + print() + + def _show_files_without_labels(self) -> None: + top_tree = DirectoryTree("\n--- Not matched ---") + for file, labels in self.matches_per_file: + if not labels: + top_tree.add(file) + if top_tree: + self.exit_code = 1 + rich.print(top_tree) + else: + print("--- All files match at least one label's patterns ---") + + def _show_files_with_multiple_labels(self) -> None: + top_tree = DirectoryTree("\n--- Matched by more than one label ---") + for file, labels in self.matches_per_file: + if len(labels) > 1: + tree = top_tree.add(file) + for label_name in labels: + tree.add(label_name) + if top_tree: + rich.print(top_tree) + else: + print("--- None of the files are matched by more than one label's patterns ---") + + def _show_unused_matchers(self) -> None: + for label_name, matchers in self.label_matchers.items(): + for idx, matcher in enumerate(matchers): + if matcher not in self.used_matchers: + print( + f"--- Matcher {idx} for label {label_name!r} does not match any files! ---" + ) + self.exit_code = 1 + + +if __name__ == "__main__": + raise SystemExit(App().run())