From 78952c9c9762e0c6f821afed3829aa4877267032 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:11:57 -0400 Subject: [PATCH] fix: Use auto-approved users allowlist for queue eligibility Refactor queue eligibility to use an allowlist of auto-approved GitHub user IDs instead of per-category contribution thresholds and URL-based category resolution. Add auto_approved_users.json and implement normalize_user_id and load_auto_approved_user_ids; simplify evaluate_queue_eligibility to check the allowlist and emit user_id in outputs. Update CLI args, GitHub Actions workflow invocation, and tests to match the new behavior and remove the old contributors/threshold logic. --- .github/workflows/auto-update-db.yml | 2 +- auto_approved_users.json | 14 ++ src/queue_eligibility.py | 202 +++++------------- tests/unit/test_queue_eligibility.py | 297 ++++++++++++--------------- 4 files changed, 203 insertions(+), 312 deletions(-) create mode 100644 auto_approved_users.json diff --git a/.github/workflows/auto-update-db.yml b/.github/workflows/auto-update-db.yml index bee0d85a220..bc937684a31 100644 --- a/.github/workflows/auto-update-db.yml +++ b/.github/workflows/auto-update-db.yml @@ -95,7 +95,7 @@ jobs: if: github.event.label.name == 'request-theme' env: ISSUE_AUTHOR_USER_ID: ${{ github.event.issue.user.id }} - run: uv run --no-sync python -m src.queue_eligibility --submission-file submission.json --database-root database + run: uv run --no-sync python -m src.queue_eligibility - name: Update id: update diff --git a/auto_approved_users.json b/auto_approved_users.json new file mode 100644 index 00000000000..0033051ce26 --- /dev/null +++ b/auto_approved_users.json @@ -0,0 +1,14 @@ +[ + { + "user_id": 30657709, + "username": "LuckyNoS7evin" + }, + { + "user_id": 42013603, + "username": "ReenigneArcher" + }, + { + "user_id": 88998541, + "username": "jjnether" + } +] diff --git a/src/queue_eligibility.py b/src/queue_eligibility.py index a052625f40c..8c21dd5652d 100644 --- a/src/queue_eligibility.py +++ b/src/queue_eligibility.py @@ -6,21 +6,10 @@ import os from dataclasses import dataclass from pathlib import Path -from urllib.parse import urlparse +from typing import Iterable -CONTRIBUTION_THRESHOLD = 15 -IGDB_HOST = 'igdb.com' -TMDB_HOST = 'themoviedb.org' - -CATEGORY_BY_HOST_AND_PATH = { - (IGDB_HOST, 'games'): 'games', - (IGDB_HOST, 'collections'): 'game_collections', - (IGDB_HOST, 'franchises'): 'game_franchises', - (TMDB_HOST, 'movie'): 'movies', - (TMDB_HOST, 'collection'): 'movie_collections', - (TMDB_HOST, 'tv'): 'tv_shows', -} +AUTO_APPROVED_USERS_FILE = 'auto_approved_users.json' @dataclass(frozen=True) @@ -32,169 +21,102 @@ class QueueEligibilityResult: ---------- queue_eligible : bool Whether the submission should receive the queue label. - category : str - Database category derived from the submission URL. - contribution_count : int - Existing contribution count for the submitter in the derived category. + user_id : str + GitHub user id of the issue author. reason : str Machine-readable reason for the eligibility decision. """ queue_eligible: bool - category: str - contribution_count: int + user_id: str reason: str -def get_submission_category(database_url: str) -> str: +def normalize_user_id(user_id: object) -> str: """ - Return the database category for a submission URL. + Normalize a GitHub user id for matching. Parameters ---------- - database_url : str - Database URL from the issue submission. + user_id : object + GitHub user id to normalize. Returns ------- str - Top-level database category directory. - - Raises - ------ - ValueError - If the URL cannot be mapped to a known database category. - """ - parsed_url = urlparse(database_url.strip()) - hostname = parsed_url.hostname or '' - hostname = hostname.removeprefix('www.') - path_parts = [part for part in parsed_url.path.split('/') if part] - - if not path_parts: - raise ValueError(f'Unsupported database URL: {database_url}') - - category = CATEGORY_BY_HOST_AND_PATH.get((hostname, path_parts[0])) - if not category: - raise ValueError(f'Unsupported database URL: {database_url}') - - return category - - -def get_contribution_count(contributor_data: dict, user_id: str) -> int: - """ - Return a contributor's total contributions from contributor metadata. - - Parameters - ---------- - contributor_data : dict - Parsed contents of a category ``contributors.json`` file. - user_id : str - GitHub user ID to look up. - - Returns - ------- - int - Sum of the contributor's added and edited item counts, or zero when the - metadata is missing or malformed. + Trimmed user id string. """ - if not isinstance(contributor_data, dict): - return 0 + if user_id is None: + return '' - contributor = contributor_data.get(str(user_id), {}) - if not isinstance(contributor, dict): - return 0 + return str(user_id).strip() - try: - return int(contributor.get('items_added', 0)) + int(contributor.get('items_edited', 0)) - except (TypeError, ValueError): - return 0 - -def load_contribution_count(database_root: Path, category: str, user_id: str) -> int: +def load_auto_approved_user_ids(auto_approved_users_file: Path) -> frozenset[str]: """ - Load a contributor's category contribution count from disk. + Load auto-approved GitHub user ids from a JSON file. Parameters ---------- - database_root : pathlib.Path - Root directory containing category database folders. - category : str - Category directory to inspect. - user_id : str - GitHub user ID to look up. + auto_approved_users_file : pathlib.Path + JSON file containing auto-approved user objects. Returns ------- - int - Contributor count for the category, or zero when the contributors file - is absent or unreadable. + frozenset[str] + Normalized GitHub user ids. """ - contributors_file = database_root / category / 'contributors.json' - if not contributors_file.exists(): - return 0 - try: - with contributors_file.open() as contributor_f: - contributor_data = json.load(contributor_f) + with auto_approved_users_file.open(encoding='utf-8') as auto_approved_users_f: + auto_approved_users = json.load(auto_approved_users_f) except (OSError, json.JSONDecodeError): - return 0 + return frozenset() + + if not isinstance(auto_approved_users, list): + return frozenset() - return get_contribution_count(contributor_data=contributor_data, user_id=user_id) + return frozenset( + normalized_user_id + for user in auto_approved_users + if isinstance(user, dict) + if (normalized_user_id := normalize_user_id(user.get('user_id', ''))) + ) -def evaluate_queue_eligibility(submission: dict, - database_root: Path, - user_id: str, - threshold: int = CONTRIBUTION_THRESHOLD) -> QueueEligibilityResult: +def evaluate_queue_eligibility(user_id: object, + auto_approved_user_ids: Iterable[object]) -> QueueEligibilityResult: """ Evaluate whether a submission is eligible to be queued automatically. Parameters ---------- - submission : dict - Parsed issue submission data. - database_root : pathlib.Path - Root directory containing category database folders. - user_id : str - GitHub user ID of the issue author. - threshold : int, optional - Minimum prior category contribution count that must be exceeded. + user_id : object + GitHub user id of the issue author. + auto_approved_user_ids : Iterable[object] + GitHub user ids eligible for automatic queueing. Returns ------- QueueEligibilityResult Eligibility decision and supporting metadata. """ - if not user_id: + normalized_user_id = normalize_user_id(user_id=user_id) + if not normalized_user_id: return QueueEligibilityResult( queue_eligible=False, - category='', - contribution_count=0, + user_id='', reason='missing-user-id', ) - try: - category = get_submission_category(database_url=submission['database_url']) - except (KeyError, ValueError): - return QueueEligibilityResult( - queue_eligible=False, - category='', - contribution_count=0, - reason='unsupported-category', - ) - - contribution_count = load_contribution_count( - database_root=database_root, - category=category, - user_id=user_id, - ) - queue_eligible = contribution_count > threshold - reason = 'eligible' if queue_eligible else 'below-threshold' + queue_eligible = normalized_user_id in { + normalize_user_id(user_id=auto_approved_user_id) + for auto_approved_user_id in auto_approved_user_ids + } + reason = 'auto-approved-user' if queue_eligible else 'not-auto-approved-user' return QueueEligibilityResult( queue_eligible=queue_eligible, - category=category, - contribution_count=contribution_count, + user_id=normalized_user_id, reason=reason, ) @@ -214,8 +136,7 @@ def write_github_outputs(result: QueueEligibilityResult) -> None: """ outputs = { 'queue_eligible': str(result.queue_eligible).lower(), - 'category': result.category, - 'contribution_count': str(result.contribution_count), + 'user_id': result.user_id, 'reason': result.reason, } output_lines = [f'{key}={value}' for key, value in outputs.items()] @@ -239,9 +160,7 @@ def parse_args() -> argparse.Namespace: Parsed command-line arguments. """ parser = argparse.ArgumentParser(description='Check whether a submission is eligible for queueing.') - parser.add_argument('--submission-file', default='submission.json') - parser.add_argument('--database-root', default='database') - parser.add_argument('--threshold', type=int, default=CONTRIBUTION_THRESHOLD) + parser.add_argument('--auto-approved-users-file', default=AUTO_APPROVED_USERS_FILE) parser.add_argument('--user-id', default=os.environ.get('ISSUE_AUTHOR_USER_ID', '')) return parser.parse_args() @@ -255,24 +174,13 @@ def main() -> None: None """ args = parse_args() - - try: - with open(args.submission_file) as submission_f: - submission = json.load(submission_f) - except (OSError, json.JSONDecodeError): - result = QueueEligibilityResult( - queue_eligible=False, - category='', - contribution_count=0, - reason='submission-read-error', - ) - else: - result = evaluate_queue_eligibility( - submission=submission, - database_root=Path(args.database_root), - user_id=args.user_id, - threshold=args.threshold, - ) + auto_approved_user_ids = load_auto_approved_user_ids( + auto_approved_users_file=Path(args.auto_approved_users_file), + ) + result = evaluate_queue_eligibility( + user_id=args.user_id, + auto_approved_user_ids=auto_approved_user_ids, + ) write_github_outputs(result=result) diff --git a/tests/unit/test_queue_eligibility.py b/tests/unit/test_queue_eligibility.py index 175cb319699..37d5cdf5e3c 100644 --- a/tests/unit/test_queue_eligibility.py +++ b/tests/unit/test_queue_eligibility.py @@ -1,160 +1,143 @@ # standard imports import json import sys - -# lib imports -import pytest +from pathlib import Path # local imports from src import queue_eligibility -@pytest.mark.parametrize('database_url, category', [ - ('https://www.igdb.com/games/goldeneye-007', 'games'), - ('https://www.igdb.com/collections/james-bond', 'game_collections'), - ('https://www.igdb.com/franchises/james-bond', 'game_franchises'), - ('https://www.themoviedb.org/movie/10378-big-buck-bunny', 'movies'), - ('https://www.themoviedb.org/collection/645-james-bond-collection', 'movie_collections'), - ('https://www.themoviedb.org/tv/1930-the-beverly-hillbillies', 'tv_shows'), -]) -def test_get_submission_category(database_url, category): - assert queue_eligibility.get_submission_category(database_url=database_url) == category +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] -def test_get_submission_category_raises_for_unsupported_url(): - with pytest.raises(ValueError): - queue_eligibility.get_submission_category(database_url='https://example.com/movie/1') +def write_auto_approved_users_file(tmp_path, auto_approved_users): + auto_approved_users_file = tmp_path / 'auto_approved_users.json' + auto_approved_users_file.write_text(json.dumps(auto_approved_users), encoding='utf-8') + return auto_approved_users_file -def test_get_submission_category_raises_for_missing_path(): - with pytest.raises(ValueError): - queue_eligibility.get_submission_category(database_url='https://www.igdb.com') +def test_load_auto_approved_user_ids_loads_user_ids(tmp_path): + auto_approved_users_file = write_auto_approved_users_file(tmp_path, [ + { + 'user_id': 42013603, + 'username': 'ReenigneArcher', + }, + { + 'user_id': ' 88998541 ', + 'username': 'renamed-user', + }, + ]) + auto_approved_user_ids = queue_eligibility.load_auto_approved_user_ids( + auto_approved_users_file=auto_approved_users_file, + ) -def test_get_contribution_count_fails_closed_for_bad_data(): - assert queue_eligibility.get_contribution_count(contributor_data=[], user_id='1234') == 0 - assert queue_eligibility.get_contribution_count(contributor_data={'1234': []}, user_id='1234') == 0 - assert queue_eligibility.get_contribution_count( - contributor_data={'1234': {'items_added': 'bad'}}, - user_id='1234', - ) == 0 + assert auto_approved_user_ids == frozenset({'42013603', '88998541'}) -def test_evaluate_queue_eligibility_requires_more_than_threshold(tmp_path): - category_dir = tmp_path / 'movies' - category_dir.mkdir() - contributors_file = category_dir / 'contributors.json' - contributors_file.write_text(json.dumps({ - 'exact': { - 'items_added': 5, - 'items_edited': 10, +def test_root_auto_approved_users_file_contains_expected_user_ids(): + auto_approved_user_ids = queue_eligibility.load_auto_approved_user_ids( + auto_approved_users_file=REPOSITORY_ROOT / queue_eligibility.AUTO_APPROVED_USERS_FILE, + ) + + assert auto_approved_user_ids == frozenset({'42013603', '88998541', '30657709'}) + + +def test_load_auto_approved_user_ids_ignores_malformed_entries(tmp_path): + auto_approved_users_file = write_auto_approved_users_file(tmp_path, [ + [], + { + 'username': 'missing-id', }, - 'eligible': { - 'items_added': 6, - 'items_edited': 10, + { + 'user_id': None, + 'username': 'null-id', }, - })) - - submission = { - 'database_url': 'https://www.themoviedb.org/movie/10378-big-buck-bunny', - } + { + 'user_id': '1234', + 'username': 'valid-user', + }, + ]) - exact_result = queue_eligibility.evaluate_queue_eligibility( - submission=submission, - database_root=tmp_path, - user_id='exact', + auto_approved_user_ids = queue_eligibility.load_auto_approved_user_ids( + auto_approved_users_file=auto_approved_users_file, ) - eligible_result = queue_eligibility.evaluate_queue_eligibility( - submission=submission, - database_root=tmp_path, - user_id='eligible', + + assert auto_approved_user_ids == frozenset({'1234'}) + + +def test_load_auto_approved_user_ids_fails_closed_for_missing_or_bad_file(tmp_path): + missing_file = tmp_path / 'missing.json' + bad_file = tmp_path / 'bad.json' + bad_file.write_text('{', encoding='utf-8') + + assert queue_eligibility.load_auto_approved_user_ids(auto_approved_users_file=missing_file) == frozenset() + assert queue_eligibility.load_auto_approved_user_ids(auto_approved_users_file=bad_file) == frozenset() + + +def test_load_auto_approved_user_ids_fails_closed_for_non_list_json(tmp_path): + auto_approved_users_file = write_auto_approved_users_file(tmp_path, { + 'user_id': '1234', + }) + + auto_approved_user_ids = queue_eligibility.load_auto_approved_user_ids( + auto_approved_users_file=auto_approved_users_file, ) - assert exact_result.contribution_count == 15 - assert not exact_result.queue_eligible - assert eligible_result.contribution_count == 16 - assert eligible_result.queue_eligible - - -def test_evaluate_queue_eligibility_uses_submission_category(tmp_path): - movie_dir = tmp_path / 'movies' - tv_dir = tmp_path / 'tv_shows' - movie_dir.mkdir() - tv_dir.mkdir() - (movie_dir / 'contributors.json').write_text(json.dumps({ - '1234': { - 'items_added': 20, - 'items_edited': 0, - }, - })) - (tv_dir / 'contributors.json').write_text(json.dumps({ - '1234': { - 'items_added': 1, - 'items_edited': 0, - }, - })) + assert auto_approved_user_ids == frozenset() + +def test_evaluate_queue_eligibility_allows_auto_approved_user_id(): result = queue_eligibility.evaluate_queue_eligibility( - submission={ - 'database_url': 'https://www.themoviedb.org/tv/1930-the-beverly-hillbillies', - }, - database_root=tmp_path, - user_id='1234', + user_id='42013603', + auto_approved_user_ids={'42013603'}, ) - assert result.category == 'tv_shows' - assert result.contribution_count == 1 - assert not result.queue_eligible + assert result.queue_eligible + assert result.user_id == '42013603' + assert result.reason == 'auto-approved-user' -def test_evaluate_queue_eligibility_fails_closed_without_user(tmp_path): +def test_evaluate_queue_eligibility_normalizes_user_ids(): result = queue_eligibility.evaluate_queue_eligibility( - submission={'database_url': 'https://www.themoviedb.org/movie/10378-big-buck-bunny'}, - database_root=tmp_path, - user_id='', + user_id=88998541, + auto_approved_user_ids={' 88998541 '}, ) - assert not result.queue_eligible - assert result.reason == 'missing-user-id' + assert result.queue_eligible + assert result.user_id == '88998541' -@pytest.mark.parametrize('submission', [ - {}, - {'database_url': 'https://example.com/movie/1'}, -]) -def test_evaluate_queue_eligibility_fails_closed_for_bad_submission(tmp_path, submission): +def test_evaluate_queue_eligibility_rejects_users_outside_allowlist(): result = queue_eligibility.evaluate_queue_eligibility( - submission=submission, - database_root=tmp_path, user_id='1234', + auto_approved_user_ids={'42013603'}, ) assert not result.queue_eligible - assert result.reason == 'unsupported-category' + assert result.user_id == '1234' + assert result.reason == 'not-auto-approved-user' -def test_load_contribution_count_returns_zero_for_missing_file(tmp_path): - contribution_count = queue_eligibility.load_contribution_count( - database_root=tmp_path, - category='movies', - user_id='1234', +def test_evaluate_queue_eligibility_does_not_match_username(): + result = queue_eligibility.evaluate_queue_eligibility( + user_id='ReenigneArcher', + auto_approved_user_ids={'42013603'}, ) - assert contribution_count == 0 - + assert not result.queue_eligible + assert result.reason == 'not-auto-approved-user' -def test_load_contribution_count_fails_closed_for_bad_contributor_file(tmp_path): - category_dir = tmp_path / 'movies' - category_dir.mkdir() - (category_dir / 'contributors.json').write_text('{') - contribution_count = queue_eligibility.load_contribution_count( - database_root=tmp_path, - category='movies', - user_id='1234', +def test_evaluate_queue_eligibility_fails_closed_without_user(): + result = queue_eligibility.evaluate_queue_eligibility( + user_id='', + auto_approved_user_ids={'42013603'}, ) - assert contribution_count == 0 + assert not result.queue_eligible + assert result.user_id == '' + assert result.reason == 'missing-user-id' def test_write_github_outputs_writes_to_output_file(tmp_path, monkeypatch): @@ -164,17 +147,15 @@ def test_write_github_outputs_writes_to_output_file(tmp_path, monkeypatch): queue_eligibility.write_github_outputs( result=queue_eligibility.QueueEligibilityResult( queue_eligible=True, - category='movies', - contribution_count=16, - reason='eligible', + user_id='42013603', + reason='auto-approved-user', ), ) assert output_file.read_text() == ( 'queue_eligible=true\n' - 'category=movies\n' - 'contribution_count=16\n' - 'reason=eligible\n' + 'user_id=42013603\n' + 'reason=auto-approved-user\n' ) @@ -184,67 +165,60 @@ def test_write_github_outputs_prints_without_output_file(monkeypatch, capsys): queue_eligibility.write_github_outputs( result=queue_eligibility.QueueEligibilityResult( queue_eligible=False, - category='tv_shows', - contribution_count=1, - reason='below-threshold', + user_id='1234', + reason='not-auto-approved-user', ), ) captured = capsys.readouterr() assert captured.out == ( 'queue_eligible=false\n' - 'category=tv_shows\n' - 'contribution_count=1\n' - 'reason=below-threshold\n' + 'user_id=1234\n' + 'reason=not-auto-approved-user\n' ) -def test_parse_args_uses_cli_and_env(monkeypatch): - monkeypatch.setenv('ISSUE_AUTHOR_USER_ID', '1234') +def test_parse_args_uses_cli_value(monkeypatch): + monkeypatch.setenv('ISSUE_AUTHOR_USER_ID', 'env-user-id') monkeypatch.setattr(sys, 'argv', [ 'queue_eligibility', - '--submission-file', - 'custom-submission.json', - '--database-root', - 'custom-database', - '--threshold', - '3', + '--auto-approved-users-file', + 'custom-auto-approved-users.json', + '--user-id', + 'cli-user-id', ]) args = queue_eligibility.parse_args() - assert args.submission_file == 'custom-submission.json' - assert args.database_root == 'custom-database' - assert args.threshold == 3 - assert args.user_id == '1234' + assert args.auto_approved_users_file == 'custom-auto-approved-users.json' + assert args.user_id == 'cli-user-id' + + +def test_parse_args_defaults_to_env_value(monkeypatch): + monkeypatch.setenv('ISSUE_AUTHOR_USER_ID', 'env-user-id') + monkeypatch.setattr(sys, 'argv', ['queue_eligibility']) + + args = queue_eligibility.parse_args() + + assert args.auto_approved_users_file == queue_eligibility.AUTO_APPROVED_USERS_FILE + assert args.user_id == 'env-user-id' def test_main_writes_queue_eligible_result(tmp_path, monkeypatch): - submission_file = tmp_path / 'submission.json' - output_file = tmp_path / 'github_output' - database_root = tmp_path / 'database' - category_dir = database_root / 'movies' - category_dir.mkdir(parents=True) - - submission_file.write_text(json.dumps({ - 'database_url': 'https://www.themoviedb.org/movie/10378-big-buck-bunny', - })) - (category_dir / 'contributors.json').write_text(json.dumps({ - '1234': { - 'items_added': 16, - 'items_edited': 0, + auto_approved_users_file = write_auto_approved_users_file(tmp_path, [ + { + 'user_id': 42013603, + 'username': 'ReenigneArcher', }, - })) - + ]) + output_file = tmp_path / 'github_output' monkeypatch.setenv('GITHUB_OUTPUT', str(output_file)) monkeypatch.setattr(sys, 'argv', [ 'queue_eligibility', - '--submission-file', - str(submission_file), - '--database-root', - str(database_root), + '--auto-approved-users-file', + str(auto_approved_users_file), '--user-id', - '1234', + '42013603', ]) queue_eligibility.main() @@ -252,18 +226,13 @@ def test_main_writes_queue_eligible_result(tmp_path, monkeypatch): assert 'queue_eligible=true' in output_file.read_text() -def test_main_fails_closed_for_unreadable_submission(monkeypatch, capsys): +def test_main_fails_closed_for_missing_user(monkeypatch, capsys): monkeypatch.delenv('GITHUB_OUTPUT', raising=False) - monkeypatch.setattr(sys, 'argv', [ - 'queue_eligibility', - '--submission-file', - 'missing-submission.json', - '--user-id', - '1234', - ]) + monkeypatch.delenv('ISSUE_AUTHOR_USER_ID', raising=False) + monkeypatch.setattr(sys, 'argv', ['queue_eligibility']) queue_eligibility.main() captured = capsys.readouterr() assert 'queue_eligible=false' in captured.out - assert 'reason=submission-read-error' in captured.out + assert 'reason=missing-user-id' in captured.out