From 1446191e254c2f37cf92fa1c9e0324f5bb75ea63 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Mon, 3 Aug 2026 16:54:52 +0200 Subject: [PATCH 1/2] fix: synchronize catalog and match advice --- .github/workflows/sync-catalog.yml | 26 +++++++++ CONTRIBUTING.md | 2 +- README.md | 2 + catalog.json | 20 ++++++- leaderboard/1v1.json | 16 +++++- leaderboard/bots/Orbit-1.0.2.json | 16 ++++++ leaderboard/melee.json | 16 +++++- leaderboard/twinduel.json | 16 +++++- matchmaking/matches_needed-1v1.json | 2 +- matchmaking/matches_needed-melee.json | 2 +- matchmaking/matches_needed-twinduel.json | 2 +- matchmaking/pairings-1v1.json | 2 +- matchmaking/pairings-melee.json | 2 +- matchmaking/pairings-twinduel.json | 2 +- scripts/aggregate.py | 13 +++-- scripts/sync_catalog.py | 71 ++++++++++++++++++++++++ site/data/bots/Orbit-1.0.2.json | 16 ++++++ site/data/leaderboard/1v1.json | 16 +++++- site/data/leaderboard/melee.json | 16 +++++- site/data/leaderboard/twinduel.json | 16 +++++- tests/test_rumble_data.py | 30 +++++++++- 21 files changed, 276 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/sync-catalog.yml create mode 100644 leaderboard/bots/Orbit-1.0.2.json create mode 100644 scripts/sync_catalog.py create mode 100644 site/data/bots/Orbit-1.0.2.json diff --git a/.github/workflows/sync-catalog.yml b/.github/workflows/sync-catalog.yml new file mode 100644 index 0000000..89088e6 --- /dev/null +++ b/.github/workflows/sync-catalog.yml @@ -0,0 +1,26 @@ +name: Synchronize Rumble bot catalog + +on: + schedule: + - cron: '23 * * * *' + workflow_dispatch: + +permissions: + contents: write + +jobs: + synchronize: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python scripts/sync_catalog.py --root . + - name: Commit synchronized catalog + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add catalog.json + git diff --cached --quiet || git commit -m 'chore: synchronize Rumble bot catalog' + git push diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 784c940..5209981 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Create an issue titled `[result] `, apply the `result Each participant supplies cataloged `name` and `version`, `isTeam`, a 1224-system `rank`, `totalScore`, `survival`, `lastSurvivorBonus`, `bulletDamage`, `bulletKillBonus`, `ramDamage`, `ramKillBonus`, `firstPlaces`, `secondPlaces`, and `thirdPlaces`. Scores and place counts are non-negative signed 32-bit integers. `isTeam`, result-entry count, rank placement, and place-count totals must match the selected ranked game type. -The submitting issue account and `clientId` must be registered. Every bot must be active in `catalog.json`; the catalog is synchronized from the reviewed Rumble bot catalog by maintainers. The drain workflow validates each record independently, keeps valid records when neighboring records are rejected, and closes every processed issue with a receipt line for every record. +The submitting issue account and `clientId` must be registered. Every bot must be active in `catalog.json`; the hourly synchronization workflow copies the reviewed Rumble bot catalog declared by that file. The drain workflow validates each record independently, keeps valid records when neighboring records are rejected, and closes every processed issue with a receipt line for every record. ## Rules diff --git a/README.md b/README.md index 6a9ceb1..857f209 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Run the focused suite with `python -m unittest discover -s tests -v`. It uses only Python's standard library. Run `python scripts/aggregate.py --root .` to regenerate projections from the current facts. +`catalog.json` is a local, reviewed copy of the generated Rumble bot catalog. Run `python scripts/sync_catalog.py --root .` to refresh it from its declared HTTPS source; the scheduled workflow performs the same synchronization hourly. + ## Submit results Before a client can submit ranked results, its forge account must be registered through a reviewed pull request adding `clients/.json`. The client then creates an issue labelled `result-submission`, with a `[result]` title and exactly one fenced JSON batch envelope. See [CONTRIBUTING.md](CONTRIBUTING.md) for the contract and limits. diff --git a/catalog.json b/catalog.json index 7f75f0e..b10bf14 100644 --- a/catalog.json +++ b/catalog.json @@ -1,5 +1,21 @@ { + "bots": [ + { + "addedAt": "2026-08-02", + "authors": [ + "Tank Royale Rumble maintainers" + ], + "name": "Orbit", + "owner": "flemming-n-larsen", + "path": "bots/python/Orbit", + "platform": "Python", + "sourceHash": "sha256:86b63e60f231c8f736ec7dc7867c93966a59a63bb3409326516879236555cf36", + "status": "active", + "version": "1.0.2" + } + ], "schemaVersion": 1, - "source": "https://github.com/robocode-dev/rumble-bots/blob/main/bots/index.json", - "bots": [] + "source": "https://raw.githubusercontent.com/robocode-dev/rumble-bots/main/bots/index.json", + "sourceCommit": "c735e6ff422e8795ffdfb0e65ec7910e9b3f943e", + "sourceGeneratedAt": "2026-08-02T15:27:36Z" } diff --git a/leaderboard/1v1.json b/leaderboard/1v1.json index ad7aeaa..f264b7a 100644 --- a/leaderboard/1v1.json +++ b/leaderboard/1v1.json @@ -1,7 +1,19 @@ { "behaviorVersion": 1, - "entries": [], + "entries": [ + { + "aps": 0.0, + "battles": 0, + "bot": "Orbit 1.0.2", + "epoch": 1, + "name": "Orbit", + "owner": "flemming-n-larsen", + "pairings": 0, + "platform": "Python", + "version": "1.0.2" + } + ], "gameType": "1v1", - "projectionId": "cd37104818ec54159fc51791c76a882498cc4f4bd35882656dc154b69a334eff", + "projectionId": "9f3e49dfac935fe42da4d43ec1cfe58290d95dc964af6eee41c79132f1682948", "schemaVersion": 1 } diff --git a/leaderboard/bots/Orbit-1.0.2.json b/leaderboard/bots/Orbit-1.0.2.json new file mode 100644 index 0000000..e7b5986 --- /dev/null +++ b/leaderboard/bots/Orbit-1.0.2.json @@ -0,0 +1,16 @@ +{ + "entry": { + "aps": 0.0, + "battles": 0, + "bot": "Orbit 1.0.2", + "epoch": 1, + "name": "Orbit", + "owner": "flemming-n-larsen", + "pairings": 0, + "platform": "Python", + "version": "1.0.2" + }, + "gameType": "twinduel", + "projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984", + "schemaVersion": 1 +} diff --git a/leaderboard/melee.json b/leaderboard/melee.json index 983a688..5650cca 100644 --- a/leaderboard/melee.json +++ b/leaderboard/melee.json @@ -1,7 +1,19 @@ { "behaviorVersion": 1, - "entries": [], + "entries": [ + { + "aps": 0.0, + "battles": 0, + "bot": "Orbit 1.0.2", + "epoch": 1, + "name": "Orbit", + "owner": "flemming-n-larsen", + "pairings": 0, + "platform": "Python", + "version": "1.0.2" + } + ], "gameType": "melee", - "projectionId": "f0fd8d04f5abc28cf69e07f93fae2115fa4eab5520508b99c2c6ad9d0ae3a522", + "projectionId": "edbe2d5f2b2ef4139b78c9842f3b743f3a78220f5827781e61ef33a3f0fc522a", "schemaVersion": 1 } diff --git a/leaderboard/twinduel.json b/leaderboard/twinduel.json index c150c79..0fac270 100644 --- a/leaderboard/twinduel.json +++ b/leaderboard/twinduel.json @@ -1,7 +1,19 @@ { "behaviorVersion": 1, - "entries": [], + "entries": [ + { + "aps": 0.0, + "battles": 0, + "bot": "Orbit 1.0.2", + "epoch": 1, + "name": "Orbit", + "owner": "flemming-n-larsen", + "pairings": 0, + "platform": "Python", + "version": "1.0.2" + } + ], "gameType": "twinduel", - "projectionId": "70c8ddb725f278462b2c4b3618704b0156fcf029133384ccd35885d5ae02602d", + "projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984", "schemaVersion": 1 } diff --git a/matchmaking/matches_needed-1v1.json b/matchmaking/matches_needed-1v1.json index 3aa042c..10c89e2 100644 --- a/matchmaking/matches_needed-1v1.json +++ b/matchmaking/matches_needed-1v1.json @@ -1,7 +1,7 @@ { "gameType": "1v1", "priorityPairs": [], - "projectionId": "cd37104818ec54159fc51791c76a882498cc4f4bd35882656dc154b69a334eff", + "projectionId": "9f3e49dfac935fe42da4d43ec1cfe58290d95dc964af6eee41c79132f1682948", "schemaVersion": 1, "targetSamplesPerPairing": 6 } diff --git a/matchmaking/matches_needed-melee.json b/matchmaking/matches_needed-melee.json index e19fe39..ece68e1 100644 --- a/matchmaking/matches_needed-melee.json +++ b/matchmaking/matches_needed-melee.json @@ -1,7 +1,7 @@ { "gameType": "melee", "priorityPairs": [], - "projectionId": "f0fd8d04f5abc28cf69e07f93fae2115fa4eab5520508b99c2c6ad9d0ae3a522", + "projectionId": "edbe2d5f2b2ef4139b78c9842f3b743f3a78220f5827781e61ef33a3f0fc522a", "schemaVersion": 1, "targetSamplesPerPairing": 6 } diff --git a/matchmaking/matches_needed-twinduel.json b/matchmaking/matches_needed-twinduel.json index 632377b..e2424d6 100644 --- a/matchmaking/matches_needed-twinduel.json +++ b/matchmaking/matches_needed-twinduel.json @@ -1,7 +1,7 @@ { "gameType": "twinduel", "priorityPairs": [], - "projectionId": "70c8ddb725f278462b2c4b3618704b0156fcf029133384ccd35885d5ae02602d", + "projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984", "schemaVersion": 1, "targetSamplesPerPairing": 6 } diff --git a/matchmaking/pairings-1v1.json b/matchmaking/pairings-1v1.json index 95c7796..b65a3a8 100644 --- a/matchmaking/pairings-1v1.json +++ b/matchmaking/pairings-1v1.json @@ -1,6 +1,6 @@ { "gameType": "1v1", "pairings": [], - "projectionId": "cd37104818ec54159fc51791c76a882498cc4f4bd35882656dc154b69a334eff", + "projectionId": "9f3e49dfac935fe42da4d43ec1cfe58290d95dc964af6eee41c79132f1682948", "schemaVersion": 1 } diff --git a/matchmaking/pairings-melee.json b/matchmaking/pairings-melee.json index b347d02..2f96c54 100644 --- a/matchmaking/pairings-melee.json +++ b/matchmaking/pairings-melee.json @@ -1,6 +1,6 @@ { "gameType": "melee", "pairings": [], - "projectionId": "f0fd8d04f5abc28cf69e07f93fae2115fa4eab5520508b99c2c6ad9d0ae3a522", + "projectionId": "edbe2d5f2b2ef4139b78c9842f3b743f3a78220f5827781e61ef33a3f0fc522a", "schemaVersion": 1 } diff --git a/matchmaking/pairings-twinduel.json b/matchmaking/pairings-twinduel.json index ce0c912..94caa83 100644 --- a/matchmaking/pairings-twinduel.json +++ b/matchmaking/pairings-twinduel.json @@ -1,6 +1,6 @@ { "gameType": "twinduel", "pairings": [], - "projectionId": "70c8ddb725f278462b2c4b3618704b0156fcf029133384ccd35885d5ae02602d", + "projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984", "schemaVersion": 1 } diff --git a/scripts/aggregate.py b/scripts/aggregate.py index 1857c6a..c5e3d63 100644 --- a/scripts/aggregate.py +++ b/scripts/aggregate.py @@ -74,11 +74,14 @@ def aggregate_game_type(records: list[dict[str, Any]], catalog: list[dict[str, A relevant = [record for record in records if record.get("gameType") == game_type and record.get("engine", {}).get("behaviorVersion") == behavior_version] shares: dict[tuple[str, str], dict[tuple[tuple[str, str], ...], list[float]]] = defaultdict(lambda: defaultdict(list)) pairing_counts: dict[tuple[tuple[str, str], ...], int] = defaultdict(int) + pair_sample_counts: dict[tuple[tuple[str, str], tuple[str, str]], int] = defaultdict(int) for record in relevant: participants = record["participants"] total = sum(float(participant["totalScore"]) for participant in participants) bots = tuple(sorted(identity(participant) for participant in participants)) pairing_counts[bots] += 1 + for pair in combinations(bots, 2): + pair_sample_counts[pair] += 1 for participant in participants: bot = identity(participant) if bot in eligible: @@ -98,12 +101,10 @@ def aggregate_game_type(records: list[dict[str, Any]], catalog: list[dict[str, A pairs = [{"bots": [f"{name} {version}" for name, version in pair], "battles": count} for pair, count in sorted(pairing_counts.items())] pairings = {"schemaVersion": 1, "projectionId": projection_id, "gameType": game_type, "pairings": pairs} priority_pairs = [] - if game_type == "1v1": - for first, second in combinations(sorted(eligible), 2): - pair = tuple(sorted((first, second))) - count = pairing_counts.get(pair, 0) - if count < TARGET_SAMPLES_PER_PAIRING: - priority_pairs.append({"bots": [f"{name} {version}" for name, version in pair], "have": count, "reason": "new-bot" if count == 0 else "under-sampled"}) + for pair in combinations(sorted(eligible), 2): + count = pair_sample_counts.get(pair, 0) + if count < TARGET_SAMPLES_PER_PAIRING: + priority_pairs.append({"bots": [f"{name} {version}" for name, version in pair], "have": count, "reason": "new-bot" if count == 0 else "under-sampled"}) needed = {"schemaVersion": 1, "projectionId": projection_id, "gameType": game_type, "targetSamplesPerPairing": TARGET_SAMPLES_PER_PAIRING, "priorityPairs": priority_pairs} return leaderboard, pairings, needed diff --git a/scripts/sync_catalog.py b/scripts/sync_catalog.py new file mode 100644 index 0000000..32262a2 --- /dev/null +++ b/scripts/sync_catalog.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Synchronize the published Rumble bot catalog into this repository.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Callable, Any +from urllib.request import Request, urlopen + +from common import read_json, write_json + +Fetch = Callable[[str], bytes] + + +def fetch_source(url: str) -> bytes: + """Fetch the declared catalog source with an explicit user agent.""" + request = Request(url, headers={"User-Agent": "tank-royale-rumble-data-catalog-sync"}) + with urlopen(request, timeout=30) as response: + return response.read() + + +def source_catalog(url: str, fetch: Fetch) -> dict[str, Any]: + """Load and minimally validate the generated Rumble bot catalog.""" + try: + source = json.loads(fetch(url).decode("utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + raise ValueError(f"could not load catalog source `{url}`: {error}") from error + if not isinstance(source, dict) or source.get("schemaVersion") != 1 or not isinstance(source.get("bots"), list): + raise ValueError("catalog source has an unsupported schema") + return source + + +def synchronized_catalog(catalog: dict[str, Any], fetch: Fetch = fetch_source) -> dict[str, Any]: + """Return the locally stored catalog refreshed from its declared source.""" + source_url = catalog.get("source") + if not isinstance(source_url, str) or not source_url.startswith("https://"): + raise ValueError("catalog.json must declare an HTTPS source URL") + source = source_catalog(source_url, fetch) + return { + "schemaVersion": 1, + "source": source_url, + "sourceCommit": source.get("commit"), + "sourceGeneratedAt": source.get("generatedAt"), + "bots": source["bots"], + } + + +def sync(root: Path, fetch: Fetch = fetch_source) -> None: + """Refresh catalog.json from its declared generated source.""" + catalog_path = root / "catalog.json" + write_json(catalog_path, synchronized_catalog(read_json(catalog_path), fetch)) + + +def main() -> int: + """Synchronize catalog.json from the command line.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, required=True) + arguments = parser.parse_args() + try: + sync(arguments.root.resolve()) + except (OSError, ValueError) as error: + print(f"catalog synchronization failed: {error}") + return 1 + print("synchronized catalog") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/site/data/bots/Orbit-1.0.2.json b/site/data/bots/Orbit-1.0.2.json new file mode 100644 index 0000000..e7b5986 --- /dev/null +++ b/site/data/bots/Orbit-1.0.2.json @@ -0,0 +1,16 @@ +{ + "entry": { + "aps": 0.0, + "battles": 0, + "bot": "Orbit 1.0.2", + "epoch": 1, + "name": "Orbit", + "owner": "flemming-n-larsen", + "pairings": 0, + "platform": "Python", + "version": "1.0.2" + }, + "gameType": "twinduel", + "projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984", + "schemaVersion": 1 +} diff --git a/site/data/leaderboard/1v1.json b/site/data/leaderboard/1v1.json index ad7aeaa..f264b7a 100644 --- a/site/data/leaderboard/1v1.json +++ b/site/data/leaderboard/1v1.json @@ -1,7 +1,19 @@ { "behaviorVersion": 1, - "entries": [], + "entries": [ + { + "aps": 0.0, + "battles": 0, + "bot": "Orbit 1.0.2", + "epoch": 1, + "name": "Orbit", + "owner": "flemming-n-larsen", + "pairings": 0, + "platform": "Python", + "version": "1.0.2" + } + ], "gameType": "1v1", - "projectionId": "cd37104818ec54159fc51791c76a882498cc4f4bd35882656dc154b69a334eff", + "projectionId": "9f3e49dfac935fe42da4d43ec1cfe58290d95dc964af6eee41c79132f1682948", "schemaVersion": 1 } diff --git a/site/data/leaderboard/melee.json b/site/data/leaderboard/melee.json index 983a688..5650cca 100644 --- a/site/data/leaderboard/melee.json +++ b/site/data/leaderboard/melee.json @@ -1,7 +1,19 @@ { "behaviorVersion": 1, - "entries": [], + "entries": [ + { + "aps": 0.0, + "battles": 0, + "bot": "Orbit 1.0.2", + "epoch": 1, + "name": "Orbit", + "owner": "flemming-n-larsen", + "pairings": 0, + "platform": "Python", + "version": "1.0.2" + } + ], "gameType": "melee", - "projectionId": "f0fd8d04f5abc28cf69e07f93fae2115fa4eab5520508b99c2c6ad9d0ae3a522", + "projectionId": "edbe2d5f2b2ef4139b78c9842f3b743f3a78220f5827781e61ef33a3f0fc522a", "schemaVersion": 1 } diff --git a/site/data/leaderboard/twinduel.json b/site/data/leaderboard/twinduel.json index c150c79..0fac270 100644 --- a/site/data/leaderboard/twinduel.json +++ b/site/data/leaderboard/twinduel.json @@ -1,7 +1,19 @@ { "behaviorVersion": 1, - "entries": [], + "entries": [ + { + "aps": 0.0, + "battles": 0, + "bot": "Orbit 1.0.2", + "epoch": 1, + "name": "Orbit", + "owner": "flemming-n-larsen", + "pairings": 0, + "platform": "Python", + "version": "1.0.2" + } + ], "gameType": "twinduel", - "projectionId": "70c8ddb725f278462b2c4b3618704b0156fcf029133384ccd35885d5ae02602d", + "projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984", "schemaVersion": 1 } diff --git a/tests/test_rumble_data.py b/tests/test_rumble_data.py index 97804b5..309a00f 100644 --- a/tests/test_rumble_data.py +++ b/tests/test_rumble_data.py @@ -12,9 +12,10 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) -from aggregate import aggregate +from aggregate import aggregate, aggregate_game_type from compact import compact from ingest import ingest +from sync_catalog import synchronized_catalog class RumbleDataTests(unittest.TestCase): @@ -123,6 +124,33 @@ def testRDA001_IntegrationPositive_twinduel_requires_team_result_entries(self) - ]}) self.assertTrue(ingest(self.root, {**self.envelope(), "results": [record]}, account="alice")[0].accepted) + def testRDA001_IntegrationPositive_every_ranked_type_advises_under_sampled_pairs(self) -> None: + catalog = json.loads((self.root / "catalog.json").read_text(encoding="utf-8"))["bots"] + record = self.envelope()["results"][0] + for game_type in ("1v1", "twinduel", "melee"): + _, _, needed = aggregate_game_type([record | {"gameType": game_type}], catalog, game_type, behavior_version=1) + alpha_bravo = next(pair for pair in needed["priorityPairs"] if pair["bots"] == ["Alpha 1.0", "Bravo 1.0"]) + self.assertEqual(1, alpha_bravo["have"]) + self.assertEqual("under-sampled", alpha_bravo["reason"]) + + def testRDA001_IntegrationPositive_catalog_sync_admits_published_bot_results(self) -> None: + source = { + "schemaVersion": 1, + "generatedAt": "2026-08-03T12:00:00Z", + "commit": "source-commit", + "bots": [ + {"name": "Orbit", "version": "1.0.2", "platform": "Python", "owner": "orbit-owner", "status": "active"}, + {"name": "Bravo", "version": "1.0", "platform": "Python", "owner": "bravo-owner", "status": "active"}, + ], + } + self.write("catalog.json", {"schemaVersion": 1, "source": "https://example.test/bots/index.json", "bots": []}) + synchronized = synchronized_catalog(json.loads((self.root / "catalog.json").read_text(encoding="utf-8")), lambda _: json.dumps(source).encode("utf-8")) + self.write("catalog.json", synchronized) + record = self.envelope()["results"][0] + record["participants"][0].update({"name": "Orbit", "version": "1.0.2"}) + outcome = ingest(self.root, {**self.envelope(), "results": [record]}, account="alice")[0] + self.assertTrue(outcome.accepted) + def testRDA003_IntegrationPositive_compaction_preserves_deterministic_projection(self) -> None: ingest(self.root, self.envelope(), account="alice") aggregate(self.root) From 7109ace0f5e9a3bb7e2bfb8672cb7dcb17d770eb Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Mon, 3 Aug 2026 16:56:19 +0200 Subject: [PATCH 2/2] fix: regenerate projections after catalog sync --- .github/workflows/sync-catalog.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-catalog.yml b/.github/workflows/sync-catalog.yml index 89088e6..40a7613 100644 --- a/.github/workflows/sync-catalog.yml +++ b/.github/workflows/sync-catalog.yml @@ -17,10 +17,11 @@ jobs: with: python-version: '3.12' - run: python scripts/sync_catalog.py --root . - - name: Commit synchronized catalog + - run: python scripts/aggregate.py --root . + - name: Commit synchronized catalog and projections run: | git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add catalog.json - git diff --cached --quiet || git commit -m 'chore: synchronize Rumble bot catalog' + git add catalog.json leaderboard matchmaking clients.json site/data + git diff --cached --quiet || git commit -m 'chore: synchronize Rumble bot catalog and projections' git push