From 009eb27437a6d9cea90fc5b40d6eed9dee329665 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" <2179727+flemming-n-larsen@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:41:38 +0200 Subject: [PATCH 1/3] Let Rumble moderator team review escalations and recruit volunteers --- .../ISSUE_TEMPLATE/moderator-volunteer.yml | 37 +++++++++++++ .github/workflows/manual-moderation.yml | 2 +- CODEOWNERS | 18 +++---- CONTRIBUTING.md | 2 +- GOVERNANCE.md | 20 ++++--- moderation/moderate.py | 52 +++++++++++++------ tests/test_moderation.py | 33 +++++++++--- 7 files changed, 125 insertions(+), 39 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/moderator-volunteer.yml diff --git a/.github/ISSUE_TEMPLATE/moderator-volunteer.yml b/.github/ISSUE_TEMPLATE/moderator-volunteer.yml new file mode 100644 index 0000000..57c5a12 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/moderator-volunteer.yml @@ -0,0 +1,37 @@ +name: Volunteer as a Rumble moderator +description: Offer to help review submissions and keep the Tank Royale Rumble fair. +title: "moderator volunteer: " +body: + - type: markdown + attributes: + value: "This is a public issue. Do not include credentials or private information. Read the [moderator guide](https://robocode.dev/rumble/moderator-guide.html) before applying." + - type: textarea + id: experience + attributes: + label: Relevant experience + description: Describe your Robocode or Tank Royale experience, the languages you can review, and your experience with GitHub pull requests. + validations: + required: true + - type: textarea + id: availability + attributes: + label: Availability + description: How often can you review submissions or help with the quarterly recovery drill? + validations: + required: true + - type: textarea + id: example + attributes: + label: Example of your review work + description: Link a review, bug report, or code contribution, or explain how you would investigate a suspicious bot submission. + validations: + required: true + - type: checkboxes + id: commitments + attributes: + label: Moderator commitments + options: + - label: I will give reasons for decisions and keep required checks and branch rules in place. + required: true + - label: I will disclose conflicts of interest and let another moderator decide cases involving my own bots or account. + required: true diff --git a/.github/workflows/manual-moderation.yml b/.github/workflows/manual-moderation.yml index c7f2020..589a1c6 100644 --- a/.github/workflows/manual-moderation.yml +++ b/.github/workflows/manual-moderation.yml @@ -6,7 +6,7 @@ on: jobs: resolve: - if: ${{ github.event.issue.pull_request && github.event.comment.user.login == 'flemming-n-larsen' && startsWith(github.event.comment.body, '/moderation approve ') }} + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/moderation approve ') }} runs-on: ubuntu-latest permissions: contents: read diff --git a/CODEOWNERS b/CODEOWNERS index fde0542..c9a3899 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,10 +1,10 @@ # Human review is required for automation, policy, templates, and publisher inputs. -/.github/ @flemming-n-larsen -/moderation/ @flemming-n-larsen -/scripts/ @flemming-n-larsen -/templates/ @flemming-n-larsen -/CODEOWNERS @flemming-n-larsen -/GOVERNANCE.md @flemming-n-larsen -/CONTRIBUTING.md @flemming-n-larsen -/README.md @flemming-n-larsen -/bots/banned.json @flemming-n-larsen +/.github/ @robocode-dev/rumble-moderators +/moderation/ @robocode-dev/rumble-moderators +/scripts/ @robocode-dev/rumble-moderators +/templates/ @robocode-dev/rumble-moderators +/CODEOWNERS @robocode-dev/rumble-moderators +/GOVERNANCE.md @robocode-dev/rumble-moderators +/CONTRIBUTING.md @robocode-dev/rumble-moderators +/README.md @robocode-dev/rumble-moderators +/bots/banned.json @robocode-dev/rumble-moderators diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a329197..e526982 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Each bot directory's `license` field is its license grant for the complete direc The first merged pull request for a bot name reserves that name for its submitting forge account. Only that account, or an account later registered in `bots/owners.json` by an already registered account, may submit later versions. A source change requires a version bump; only the latest version remains active. Each owner has five active entries by default, including a TwinDuel team. -Use `python scripts/validate_bot.py --root . --owner --smoke` before opening a pull request. CI is authoritative. A bot-only PR can be merged automatically after source and security checks and a completed one-round battle. Keep launcher scripts faithful to the published starter; modified launchers, team metadata, security uncertainty and unrelated file changes go to Flemming. If the bot does not run, the PR receives a changes-requested review explaining why; push a fix to rerun moderation. +Use `python scripts/validate_bot.py --root . --owner --smoke` before opening a pull request. CI is authoritative. A bot-only PR can be merged automatically after source and security checks and a completed one-round battle. Keep launcher scripts faithful to the published starter; modified launchers, team metadata, security uncertainty and unrelated file changes go to the Tank Royale Rumble moderators. If the bot does not run, the PR receives a changes-requested review explaining why; push a fix to rerun moderation. Start from the [runnable templates and copy-and-rename instructions](templates/README.md). They include pinned dependencies, both launchers and the required `RUMBLE_SMOKE_READY ` branch. The validator requires Python 3.11+ and `sh`; Windows users can install Git for Windows and make its shell available. It sets `RUMBLE_SMOKE=1`, invokes each individual bot's `.sh` launcher and requires a successful exit with the marker within 20 seconds. This verifies startup rather than a server handshake. Commit each `.sh` with executable mode `100755`, using `git update-index --chmod=+x` after staging it. Team entries contain JSON only and skip individual-bot launcher checks. diff --git a/GOVERNANCE.md b/GOVERNANCE.md index e44454c..6473cf6 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,11 +1,17 @@ # Governance -The `robocode-dev` organization owns this repository. Flemming is currently the human moderator; recruiting at least three people with ownership and CODEOWNERS review rights remains a future goal, not a condition for accepting ranked submissions. GitHub moderation automatically approves and merges a new bot or version update only when the pull request changes one bot directory, the source and dependency checks find no restricted behavior, and the bot completes one round against a trusted reference. A failed bot receives a changes-requested review with a diagnostic. Security uncertainty, infrastructure failure, TwinDuel team metadata, and non-bot changes stay blocked for Flemming's decision. +The `robocode-dev` organization owns this repository. Human review belongs to the Tank Royale Rumble moderators, represented by the organization's `rumble-moderators` team. Recruiting more qualified moderators remains a goal, not a condition for accepting ranked submissions. GitHub moderation automatically approves and merges a new bot or version update only when the pull request changes one bot directory, the source and dependency checks find no restricted behavior, and the bot completes one round against a trusted reference. A failed bot receives a changes-requested review with a diagnostic. Security uncertainty, infrastructure failure, TwinDuel team metadata, and non-bot changes stay blocked for the moderator team's decision. Moderators maintain `bots/banned.json` through reviewed pull requests. A ban can cover an account or a bot and can be temporary. CI prevents a banned account from submitting and excludes disqualified bots from the generated catalog. Facts in the future `rumble-data` repository are never deleted merely because an entry is disqualified. Moderators resolve name-squatting, lost-account recovery, license complaints, and appeal requests case by case. A credible copyright complaint removes the bot from the working tree and disqualifies it pending resolution. +## Join the moderator team + +Volunteers start with the [public moderator request form](https://github.com/robocode-dev/rumble-bots/issues/new?template=moderator-volunteer.yml). They describe their Rumble experience, Bot API languages they can review, GitHub review experience, availability, and an example of their judgment. A working knowledge of at least one official language is enough when the volunteer seeks a second reviewer for unfamiliar code. Existing moderators check the applicant's ability to interpret source, validation and battle logs, apply bot ownership and license rules, protect privileged credentials from untrusted bot code, and explain a decision with evidence. They may ask for an example review without granting repository access. + +Accepted volunteers must disclose conflicts of interest and leave their own bots, account disputes, and appeals to another moderator. They must not waive failing checks or bypass branch rules. An organization owner initially grants the existing team Write access to this repository so its members can review and merge under the branch rule; the team already has Write access to `rumble-data`. A team maintainer or organization owner may then add accepted volunteers without making the project creator the sole operator. Removing a member from the team revokes their team-based moderation authority. The [public moderator guide](https://robocode.dev/rumble/moderator-guide.html) explains the work and application path. + Once each quarter, a moderator performs a fork drill: fork the repository, enable Actions, run validation, and regenerate the catalog. Record the outcome in a GitHub issue. A fork can run the source validator without moderator credentials; automatic review and merge require the installed moderation App. ## Moderator App registration @@ -15,7 +21,7 @@ Create the organization-owned GitHub App at **robocode-dev → Settings → Deve | Field | Value | | --- | --- | | GitHub App name | `Tank Royale Rumble Moderator` | -| Description (displayed to users) | `Reviews Rumble bot submissions, runs a test battle, and automatically merges submissions that pass. Escalates uncertain cases to the human moderator.` | +| Description (displayed to users) | `Reviews Rumble bot submissions, runs a test battle, and automatically merges submissions that pass. Escalates uncertain cases to the Tank Royale Rumble moderators.` | | Homepage URL | `https://github.com/robocode-dev/rumble-bots` | | Redirect URI / Callback URL | Leave empty; the workflow uses installation tokens, not user OAuth. | | Request user authorization during installation | Off. | @@ -26,11 +32,11 @@ Create the organization-owned GitHub App at **robocode-dev → Settings → Deve Upload the [moderator App avatar](assets/tank-royale-rumble-moderator-avatar.png) under the App's display settings. This square, transparent PNG is the approved logo and is under GitHub's 1 MB upload limit. -Set repository permissions **Contents**, **Pull requests**, **Commit statuses**, and **Issues** to **Read & write**. Leave other permissions at their defaults and subscribe to no webhook events. Create the App, then install it with **Only select repositories → `robocode-dev/rumble-bots`**. Do not grant it access to `tank-royale`, `rumble-data`, or all organization repositories. +Set repository permissions **Contents**, **Pull requests**, **Commit statuses**, and **Issues** to **Read & write**, and organization **Members** to **Read**. Leave other permissions at their defaults and subscribe to no webhook events. Create the App, then install it with **Only select repositories → `robocode-dev/rumble-bots`**. Do not grant it access to `tank-royale`, `rumble-data`, or all organization repositories. Generate one private key under the App's **Credentials → Key pairs** page. Store the App ID in the `rumble-bots` Actions secret `RUMBLE_MODERATOR_APP_ID` and the full PEM contents in `RUMBLE_MODERATOR_PRIVATE_KEY` at **rumble-bots → Settings → Secrets and variables → Actions → Repository secrets**. Never commit the PEM file. After the secrets are saved, check the installation and secret names, then delete the downloaded local PEM copy if it is no longer needed. Rotate the key through the same App settings and update the secret before revoking the old key. -The organization installation created on 2026-10-20 has App ID `5402066`, slug `tank-royale-rumble-moderator`, and installation ID `173538338`. GitHub reports selected-repository access and the four write permissions above. Both named repository secrets were set on 2026-10-20; their values are not recorded here. The initial documentation-PR run verified that the App could obtain a token, set `bot-moderation` to pending, and comment as `tank-royale-rumble-moderator[bot]` when a PR changes protected documentation. +The organization installation created on 2026-10-20 has App ID `5402066`, slug `tank-royale-rumble-moderator`, and installation ID `173538338`. GitHub reported selected-repository access and the four repository write permissions when it was installed; organization Members read must be added and the installation permission update approved before active team membership can be checked. Both named repository secrets were set on 2026-10-20; their values are not recorded here. The initial documentation-PR run verified that the App could obtain a token, set `bot-moderation` to pending, and comment as `tank-royale-rumble-moderator[bot]` when a PR changes protected documentation. ## Catalog publisher App registration @@ -48,14 +54,14 @@ The `pull_request_target` moderation workflow runs trusted code from `main` for Repository Actions policy `8130`, **Allow trusted bot moderation event**, was activated on 2026-10-20. It targets only `.github/workflows/moderate.yml` and permits only `pull_request_target` for that workflow. -Install a dedicated GitHub App on **only** `robocode-dev/rumble-bots`, with Contents, Pull requests, Commit statuses and Issues repository permissions set to write. Store its ID and private key as `RUMBLE_MODERATOR_APP_ID` and `RUMBLE_MODERATOR_PRIVATE_KEY` repository secrets. The verdict job obtains a short-lived installation token. It approves and merges only the tested head SHA after the required `bot-moderation` status succeeds; new commits invalidate that result. A failed bot gets a failing status and `REQUEST_CHANGES`. Ambiguous and non-bot PRs receive a pending status and a comment naming Flemming and the reason. Flemming may review a pending PR and comment `/moderation approve ` to make its current head green; Flemming still merges that PR manually. Do not add the moderation App to branch-rule bypasses. +The moderator App is installed on **only** `robocode-dev/rumble-bots`, with Contents, Pull requests, Commit statuses and Issues repository permissions set to write. Grant it organization **Members: Read** as well so it can check active membership in the existing `rumble-moderators` team; approve the installation permission update before enabling the team-based command. Store its ID and private key as `RUMBLE_MODERATOR_APP_ID` and `RUMBLE_MODERATOR_PRIVATE_KEY` repository secrets. The verdict job obtains a short-lived installation token. It approves and merges only the tested head SHA after the required `bot-moderation` status succeeds; new commits invalidate that result. A failed bot gets a failing status and `REQUEST_CHANGES`. Ambiguous and non-bot PRs receive a pending status and a comment mentioning the team and the reason. An active team member may review a pending PR and comment `/moderation approve ` to make its current head green; the moderator still merges that PR manually. A failed membership lookup cannot clear a pending status. Do not add the moderation App to branch-rule bypasses. The automatic security screen is a conservative filter, not proof that arbitrary bot source is harmless; clients still execute bots in their own sandbox. -The active [main ruleset](https://github.com/robocode-dev/rumble-bots/rules/25580362), `Rumble main moderation guard` (ID `25580362`), applies to the default branch. It requires pull requests, one approval, a successful `bot-moderation` status from Moderator App ID `5402066`, and code-owner approval for paths listed in `CODEOWNERS`; new commits dismiss existing approvals. Approval of the latest push and resolution of every review thread are not required, strict up-to-date checks are off, and merge, squash, and rebase are allowed. GitHub's extra approval for unattributed Copilot pull requests is disabled so it cannot add a second approval to this App-managed flow. The ruleset also blocks branch deletion and force pushes. Flemming (`flemming-n-larsen`, user ID `2179727`) is the only bypass actor and can bypass only through a pull request, because GitHub does not count an author's approval of their own PR. Use this exception only after human review; neither App can bypass the ruleset, and direct pushes are not permitted. The live moderation and catalog-publication path passed on PRs #22 and #28 before the ruleset was activated on 2026-10-21. +The active [main ruleset](https://github.com/robocode-dev/rumble-bots/rules/25580362), `Rumble main moderation guard` (ID `25580362`), applies to the default branch. It requires pull requests, one approval, a successful `bot-moderation` status from Moderator App ID `5402066`, and code-owner approval for paths listed in `CODEOWNERS`; new commits dismiss existing approvals. Approval of the latest push and resolution of every review thread are not required, strict up-to-date checks are off, and merge, squash, and rebase are allowed. GitHub's extra approval for unattributed Copilot pull requests is disabled so it cannot add a second approval to this App-managed flow. The ruleset also blocks branch deletion and force pushes. One organization-owner account retains a pull-request-only bypass for recovery because GitHub does not count an author's approval of their own PR. Use this exception only after human review; neither App can bypass the ruleset, and direct pushes are not permitted. The live moderation and catalog-publication path passed on PRs #22 and #28 before the ruleset was activated on 2026-10-21. After a bot pull request is merged by the App, the publisher runs on a push to `main` that changes a bot directory. It checks that the merged PR's exact head had a successful `bot-moderation` status, then checks out trusted `main` and runs static catalog generation without executing a submitted launcher. The moderator App pushes only a `catalog/pr-*` branch. The separate Publisher App opens its pull request, and the workflow dispatches catalog verification. Verification reads the pull request as data without executing its contents, checks its source submission and exact generated files against trusted `main`, then uses the moderator App to approve and merge only the verified head. The workflow's `GITHUB_TOKEN` needs `contents: write` for dispatch and `pull-requests: read` plus `statuses: read` for source verification; it uses no personal access token. Generated catalog files have no CODEOWNERS entry because the moderator App's verified approval satisfies the review requirement; policy and workflow files remain human-owned. For publication to work, GitHub Actions must be enabled and the repository or organization policy must permit these token scopes and the `repository_dispatch` verification event. The repository's **Settings → Actions → General → Workflow permissions** already allows read and write permissions for `GITHUB_TOKEN`. The organization keeps **Allow GitHub Actions to create and approve pull requests** disabled; the separate Publisher App opens the PR instead. Neither App needs a direct-push bypass on `main`. Recheck these settings if organization policy or branch rules change. -On a `main` push, the publisher retries the commit-to-PR lookup for up to 55 seconds to allow GitHub to publish the association. To recover a merged submission whose publication still failed, a maintainer can run **Publish bot catalog** from `main` with its merged PR number. The dispatch verifies that it merged into `main` and that its head commit passed moderation before it regenerates the catalog using the PR author's account as owner. If the target branch advances during verification, the catalog PR remains blocked and the publisher must rerun against current `main`. If several bot submissions merge before publication finishes, verification holds the generated PR for Flemming rather than guessing which submitter owns each unpublished bot; reconcile ownership before publishing. +On a `main` push, the publisher retries the commit-to-PR lookup for up to 55 seconds to allow GitHub to publish the association. To recover a merged submission whose publication still failed, a maintainer can run **Publish bot catalog** from `main` with its merged PR number. The dispatch verifies that it merged into `main` and that its head commit passed moderation before it regenerates the catalog using the PR author's account as owner. If the target branch advances during verification, the catalog PR remains blocked and the publisher must rerun against current `main`. If several bot submissions merge before publication finishes, verification holds the generated PR for the moderator team rather than guessing which submitter owns each unpublished bot; reconcile ownership before publishing. diff --git a/moderation/moderate.py b/moderation/moderate.py index 5eb034a..1f63b70 100644 --- a/moderation/moderate.py +++ b/moderation/moderate.py @@ -10,31 +10,50 @@ import re import sys from urllib.error import HTTPError +from urllib.parse import quote from urllib.request import Request, urlopen CONTEXT = "bot-moderation" -MODERATOR = "flemming-n-larsen" +ORG = "robocode-dev" +MODERATOR_TEAM = "rumble-moderators" +MODERATOR_MENTION = f"@{ORG}/{MODERATOR_TEAM}" class GitHubError(Exception): - pass + def __init__(self, message: str, status: int | None = None): + super().__init__(message) + self.status = status -def api(method: str, path: str, payload: dict | None = None): +def request(method: str, path: str, payload: dict | None = None): token = os.environ["GH_TOKEN"] data = json.dumps(payload).encode() if payload is not None else None - request = Request( - f"https://api.github.com/repos/{os.environ['GITHUB_REPOSITORY']}/{path}", + http_request = Request( + f"https://api.github.com/{path}", data=data, method=method, headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2026-03-10", "Content-Type": "application/json"}, ) try: - with urlopen(request, timeout=20) as response: + with urlopen(http_request, timeout=20) as response: body = response.read() return json.loads(body) if body else None except HTTPError as error: - raise GitHubError(f"GitHub {method} {path}: HTTP {error.code}: {error.read()[:400].decode(errors='replace')}") from error + raise GitHubError(f"GitHub {method} {path}: HTTP {error.code}: {error.read()[:400].decode(errors='replace')}", error.code) from error + + +def api(method: str, path: str, payload: dict | None = None): + return request(method, f"repos/{os.environ['GITHUB_REPOSITORY']}/{path}", payload) + + +def active_moderator(login: str) -> bool: + try: + membership = request("GET", f"orgs/{ORG}/teams/{MODERATOR_TEAM}/memberships/{quote(login, safe='')}") + except GitHubError as error: + if error.status == 404: + return False + raise + return isinstance(membership, dict) and membership.get("state") == "active" def status(sha: str, state: str, description: str, run_url: str = "") -> None: @@ -64,15 +83,15 @@ def automated(number: int, sha: str, kind: str, reason: str, run_url: str, base_ return if base_sha and pr["base"]["sha"] != base_sha: status(sha, "pending", "Base changed during moderation", run_url) - comment(number, f"@{MODERATOR} The target branch changed while `{sha[:12]}` was being tested. Please review the current base and retry or resolve with `/moderation approve `.") + comment(number, f"{MODERATOR_MENTION} The target branch changed while `{sha[:12]}` was being tested. Please review the current base and retry or resolve with `/moderation approve `.") return reason = reason.strip()[:1200] or "Moderation did not produce a reason" if pr["draft"] or kind not in {"clean", "catalog", "reject", "escalate"}: kind = "escalate" reason = "Draft PR or missing verdict: " + reason if kind == "escalate": - status(sha, "pending", "Flemming must review this PR", run_url) - comment(number, f"@{MODERATOR} Moderation needs your decision for `{sha[:12]}`: {reason}\n\nThe required check stays pending. After reviewing, use `/moderation approve ` or request changes.") + status(sha, "pending", "Human moderator review required", run_url) + comment(number, f"{MODERATOR_MENTION} Moderation needs your decision for `{sha[:12]}`: {reason}\n\nThe required check stays pending. After reviewing, use `/moderation approve ` or request changes.") return if kind == "reject": status(sha, "failure", "Bot validation or battle failed", run_url) @@ -98,7 +117,7 @@ def automated(number: int, sha: str, kind: str, reason: str, run_url: str, base_ raise GitHubError("GitHub did not confirm the merge") except GitHubError as error: status(sha, "error", "Automatic merge needs maintainer attention", run_url) - comment(number, f"@{MODERATOR} The bot passed moderation, but automatic merge failed for `{sha[:12]}`: {error}") + comment(number, f"{MODERATOR_MENTION} The bot passed moderation, but automatic merge failed for `{sha[:12]}`: {error}") raise @@ -109,14 +128,17 @@ def human_override(number: int, sha: str, comment_id: int) -> None: entry = api("GET", f"issues/comments/{comment_id}") body = entry["body"].strip() match = re.fullmatch(r"/moderation approve\s+(.{10,500})", body, re.DOTALL) - if entry["user"]["login"].casefold() != MODERATOR or not match: - raise GitHubError("Only Flemming may approve an escalated PR with a stated reason") + if not match: + raise GitHubError("A moderation approval needs a stated reason") + moderator = entry["user"]["login"] + if not active_moderator(moderator): + raise GitHubError("Only an active Tank Royale Rumble moderator may approve an escalated PR") combined = api("GET", f"commits/{sha}/status") moderation = next((item for item in combined["statuses"] if item["context"] == CONTEXT), None) if moderation is None or moderation["state"] != "pending": raise GitHubError("Only a pending human escalation may be approved manually") - status(sha, "success", "Human moderation by Flemming") - comment(number, f"Flemming accepted the escalated moderation for `{sha[:12]}`: {match.group(1).strip()}\n\nMerge remains a human action.") + status(sha, "success", "Human moderator review passed") + comment(number, f"@{moderator} accepted the escalated moderation for `{sha[:12]}`: {match.group(1).strip()}\n\nMerge remains a human action.") def main() -> int: diff --git a/tests/test_moderation.py b/tests/test_moderation.py index 0bf4193..8248c18 100644 --- a/tests/test_moderation.py +++ b/tests/test_moderation.py @@ -122,7 +122,7 @@ def fake_api(method, path, payload=None): for _, path, payload in calls)) self.assertFalse(any(path == "pulls/12/merge" for _, path, _ in calls)) - def test_uncertain_result_requires_flemming(self): + def test_uncertain_result_requires_human_moderators(self): calls = [] def fake_api(method, path, payload=None): @@ -133,11 +133,11 @@ def fake_api(method, path, payload=None): moderate.automated(12, SHA, "escalate", "suspicious network code", "") self.assertTrue(any(path == f"statuses/{SHA}" and payload["state"] == "pending" for _, path, payload in calls)) - self.assertTrue(any(path == "issues/12/comments" and "@flemming-n-larsen" in payload["body"] + self.assertTrue(any(path == "issues/12/comments" and moderate.MODERATOR_MENTION in payload["body"] for _, path, payload in calls)) self.assertFalse(any(path == "pulls/12/merge" for _, path, _ in calls)) - def test_only_flemming_can_clear_a_pending_escalation(self): + def test_active_team_member_can_clear_a_pending_escalation(self): calls = [] def fake_api(method, path, payload=None): @@ -145,13 +145,16 @@ def fake_api(method, path, payload=None): if path == "pulls/12": return self.pr if path == "issues/comments/42": - return {"body": "/moderation approve Reviewed the extra source", "user": {"login": "flemming-n-larsen"}} + return {"body": "/moderation approve Reviewed the extra source", "user": {"login": "reviewer"}} if path == f"commits/{SHA}/status": return {"statuses": [{"context": moderate.CONTEXT, "state": "pending"}]} return {} - with patch.object(moderate, "api", side_effect=fake_api): + with patch.object(moderate, "api", side_effect=fake_api), patch.object( + moderate, "active_moderator", return_value=True + ) as membership: moderate.human_override(12, SHA, 42) + membership.assert_called_once_with("reviewer") self.assertTrue(any(path == f"statuses/{SHA}" and payload["state"] == "success" for _, path, payload in calls)) self.assertFalse(any(path == "pulls/12/merge" for _, path, _ in calls)) @@ -164,9 +167,27 @@ def fake_api(method, path, payload=None): return {"body": "/moderation approve Looks good to me", "user": {"login": "someone-else"}} raise AssertionError("No status or merge may be written") - with patch.object(moderate, "api", side_effect=fake_api): + with patch.object(moderate, "api", side_effect=fake_api), patch.object( + moderate, "active_moderator", return_value=False + ) as membership: with self.assertRaises(moderate.GitHubError): moderate.human_override(12, SHA, 42) + membership.assert_called_once_with("someone-else") + + def test_team_membership_must_be_active(self): + for state, expected in (("active", True), ("pending", False)): + with self.subTest(state=state), patch.object(moderate, "request", return_value={"state": state}) as request: + self.assertIs(moderate.active_moderator("reviewer"), expected) + request.assert_called_once_with("GET", "orgs/robocode-dev/teams/rumble-moderators/memberships/reviewer") + + def test_absent_or_unavailable_team_membership_cannot_approve(self): + with patch.object(moderate, "request", side_effect=moderate.GitHubError("missing", 404)): + self.assertFalse(moderate.active_moderator("outsider")) + with patch.object(moderate, "request", return_value=None): + self.assertFalse(moderate.active_moderator("reviewer")) + with patch.object(moderate, "request", side_effect=moderate.GitHubError("forbidden", 403)): + with self.assertRaises(moderate.GitHubError): + moderate.active_moderator("reviewer") class CatalogVerificationTest(unittest.TestCase): From 170af612c8c2a068d1ba280e545bbb58e19cd5ae Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" <2179727+flemming-n-larsen@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:45:16 +0200 Subject: [PATCH 2/3] Link Git for Windows and permitted bot licenses --- CONTRIBUTING.md | 2 +- README.md | 2 +- templates/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e526982..c98685b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,6 @@ The first merged pull request for a bot name reserves that name for its submitti Use `python scripts/validate_bot.py --root . --owner --smoke` before opening a pull request. CI is authoritative. A bot-only PR can be merged automatically after source and security checks and a completed one-round battle. Keep launcher scripts faithful to the published starter; modified launchers, team metadata, security uncertainty and unrelated file changes go to the Tank Royale Rumble moderators. If the bot does not run, the PR receives a changes-requested review explaining why; push a fix to rerun moderation. -Start from the [runnable templates and copy-and-rename instructions](templates/README.md). They include pinned dependencies, both launchers and the required `RUMBLE_SMOKE_READY ` branch. The validator requires Python 3.11+ and `sh`; Windows users can install Git for Windows and make its shell available. It sets `RUMBLE_SMOKE=1`, invokes each individual bot's `.sh` launcher and requires a successful exit with the marker within 20 seconds. This verifies startup rather than a server handshake. Commit each `.sh` with executable mode `100755`, using `git update-index --chmod=+x` after staging it. Team entries contain JSON only and skip individual-bot launcher checks. +Start from the [runnable templates and copy-and-rename instructions](templates/README.md). They include pinned dependencies, both launchers and the required `RUMBLE_SMOKE_READY ` branch. The validator requires Python 3.11+ and `sh`; Windows users can install [Git for Windows](https://gitforwindows.org/) and make its shell available. It sets `RUMBLE_SMOKE=1`, invokes each individual bot's `.sh` launcher and requires a successful exit with the marker within 20 seconds. This verifies startup rather than a server handshake. Commit each `.sh` with executable mode `100755`, using `git update-index --chmod=+x` after staging it. Team entries contain JSON only and skip individual-bot launcher checks. Copyright complaints, unsafe code, impersonation, and disputes are handled under [GOVERNANCE.md](GOVERNANCE.md). Do not include secrets in source, bot configuration, issues, or pull-request text. diff --git a/README.md b/README.md index 88fa405..2117dca 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ See the [bot author guide](https://robocode.dev/rumble/bot-author-guide) for the 1. Choose a [runnable bot template](templates/README.md) and follow its copy-and-rename instructions into `bots///`. 2. Include `.json`, `.sh`, `.cmd`, and source code that uses an official Tank Royale Bot API. -3. Declare one permitted SPDX license: `MIT`, `Apache-2.0`, `BSD-3-Clause`, or `GPL-3.0-or-later`. +3. Declare one permitted SPDX license: [MIT](https://choosealicense.com/licenses/mit/), [Apache-2.0](https://choosealicense.com/licenses/apache-2.0/), [BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/), or [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html). The [Choose a License comparison](https://choosealicense.com/licenses/) explains their common permissions and conditions. 4. Run `python scripts/validate_bot.py --root . --owner --smoke` before opening a pull request. Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full submission, ownership, licensing, and moderation rules. The [template guide](templates/README.md#smoke-check-and-executable-scripts) explains smoke markers, runtime prerequisites and executable bits. JSON-only TwinDuel teams use the [team template](templates/README.md#json-only-twinduel-team). Local smoke checks only prove source startup; GitHub moderation runs an isolated, complete one-round battle before a clean bot-only PR can be merged automatically. diff --git a/templates/README.md b/templates/README.md index ca9ea2c..5b637f7 100644 --- a/templates/README.md +++ b/templates/README.md @@ -55,7 +55,7 @@ The validator passes its own Python interpreter to Python launchers through `RUM ## Smoke check and executable scripts -Install Git and make `sh` available; on Windows, use Git for Windows with Git Bash's `sh` on `PATH`. Run the validator from the repository root: +Install Git and make `sh` available; on Windows, use [Git for Windows](https://gitforwindows.org/) with Git Bash's `sh` on `PATH`. Run the validator from the repository root: ```shell python scripts/validate_bot.py --root . --owner YOUR_GITHUB_ACCOUNT --smoke From 8a196b8fdbcb4c14d7594470eff95acb9a869cd8 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" <2179727+flemming-n-larsen@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:03:40 +0200 Subject: [PATCH 3/3] Record approved moderator App team membership permission --- GOVERNANCE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 6473cf6..d2f370a 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -36,7 +36,7 @@ Set repository permissions **Contents**, **Pull requests**, **Commit statuses**, Generate one private key under the App's **Credentials → Key pairs** page. Store the App ID in the `rumble-bots` Actions secret `RUMBLE_MODERATOR_APP_ID` and the full PEM contents in `RUMBLE_MODERATOR_PRIVATE_KEY` at **rumble-bots → Settings → Secrets and variables → Actions → Repository secrets**. Never commit the PEM file. After the secrets are saved, check the installation and secret names, then delete the downloaded local PEM copy if it is no longer needed. Rotate the key through the same App settings and update the secret before revoking the old key. -The organization installation created on 2026-10-20 has App ID `5402066`, slug `tank-royale-rumble-moderator`, and installation ID `173538338`. GitHub reported selected-repository access and the four repository write permissions when it was installed; organization Members read must be added and the installation permission update approved before active team membership can be checked. Both named repository secrets were set on 2026-10-20; their values are not recorded here. The initial documentation-PR run verified that the App could obtain a token, set `bot-moderation` to pending, and comment as `tank-royale-rumble-moderator[bot]` when a PR changes protected documentation. +The organization installation created on 2026-10-20 has App ID `5402066`, slug `tank-royale-rumble-moderator`, and installation ID `173538338`. Its organization Members read permission was added and the installation update approved on 2026-10-22; GitHub's organization-installations API then reported `members: read` alongside the four repository write permissions and selected-repository access. Both named repository secrets were set on 2026-10-20; their values are not recorded here. The initial documentation-PR run verified that the App could obtain a token, set `bot-moderation` to pending, and comment as `tank-royale-rumble-moderator[bot]` when a PR changes protected documentation. ## Catalog publisher App registration @@ -54,7 +54,7 @@ The `pull_request_target` moderation workflow runs trusted code from `main` for Repository Actions policy `8130`, **Allow trusted bot moderation event**, was activated on 2026-10-20. It targets only `.github/workflows/moderate.yml` and permits only `pull_request_target` for that workflow. -The moderator App is installed on **only** `robocode-dev/rumble-bots`, with Contents, Pull requests, Commit statuses and Issues repository permissions set to write. Grant it organization **Members: Read** as well so it can check active membership in the existing `rumble-moderators` team; approve the installation permission update before enabling the team-based command. Store its ID and private key as `RUMBLE_MODERATOR_APP_ID` and `RUMBLE_MODERATOR_PRIVATE_KEY` repository secrets. The verdict job obtains a short-lived installation token. It approves and merges only the tested head SHA after the required `bot-moderation` status succeeds; new commits invalidate that result. A failed bot gets a failing status and `REQUEST_CHANGES`. Ambiguous and non-bot PRs receive a pending status and a comment mentioning the team and the reason. An active team member may review a pending PR and comment `/moderation approve ` to make its current head green; the moderator still merges that PR manually. A failed membership lookup cannot clear a pending status. Do not add the moderation App to branch-rule bypasses. +The moderator App is installed on **only** `robocode-dev/rumble-bots`, with Contents, Pull requests, Commit statuses and Issues repository permissions set to write and organization **Members: Read**. Keep the Members permission and installation approval in place so it can check active membership in the existing `rumble-moderators` team. Store its ID and private key as `RUMBLE_MODERATOR_APP_ID` and `RUMBLE_MODERATOR_PRIVATE_KEY` repository secrets. The verdict job obtains a short-lived installation token. It approves and merges only the tested head SHA after the required `bot-moderation` status succeeds; new commits invalidate that result. A failed bot gets a failing status and `REQUEST_CHANGES`. Ambiguous and non-bot PRs receive a pending status and a comment mentioning the team and the reason. An active team member may review a pending PR and comment `/moderation approve ` to make its current head green; the moderator still merges that PR manually. A failed membership lookup cannot clear a pending status. Do not add the moderation App to branch-rule bypasses. The automatic security screen is a conservative filter, not proof that arbitrary bot source is harmless; clients still execute bots in their own sandbox.