diff --git a/.github/workflows/publish-catalog.yml b/.github/workflows/publish-catalog.yml index 7cf54c6..2cf03ff 100644 --- a/.github/workflows/publish-catalog.yml +++ b/.github/workflows/publish-catalog.yml @@ -1,11 +1,21 @@ name: Publish bot catalog on: - pull_request: - types: [closed] + push: + branches: [main] + paths: + - 'bots/**' + workflow_dispatch: + inputs: + pull_request_number: + description: Merged bot submission PR to republish + required: true + type: number permissions: contents: write + pull-requests: read + checks: read concurrency: group: publish-catalog @@ -13,31 +23,55 @@ concurrency: jobs: publish: - if: github.event.pull_request.merged && github.event.pull_request.base.ref == 'main' runs-on: ubuntu-latest steps: + - name: Resolve and verify merged submission + id: submission + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + INPUT_PR_NUMBER: ${{ inputs.pull_request_number }} + run: | + set -euo pipefail + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + [[ "$GITHUB_REF" == "refs/heads/main" ]] || { echo "Dispatch this publisher from main." >&2; exit 1; } + pr_number="$INPUT_PR_NUMBER" + else + prs="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls")" + pr_number="$(jq -r '[.[] | select(.merged_at != null and .base.ref == "main")] | if length == 1 then .[0].number else empty end' <<< "$prs")" + fi + [[ "$pr_number" =~ ^[0-9]+$ ]] || { echo "Could not identify exactly one merged bot submission pull request." >&2; exit 1; } + pr="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}")" + jq -e '.merged_at != null and .base.ref == "main"' <<< "$pr" >/dev/null || { echo "The selected pull request must be merged into main." >&2; exit 1; } + head_sha="$(jq -r '.head.sha' <<< "$pr")" + owner="$(jq -r '.user.login' <<< "$pr")" + [[ "$head_sha" =~ ^[0-9a-f]{40}$ && "$owner" =~ ^[A-Za-z0-9-]+$ ]] || { echo "Unexpected pull request identity." >&2; exit 1; } + successful_validation="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/check-runs" --jq '[.check_runs[] | select(.name == "validate" and .app.slug == "github-actions" and .conclusion == "success")] | length')" + [[ "$successful_validation" -gt 0 ]] || { echo "The merged pull request has no successful validate check for its head commit." >&2; exit 1; } + { + echo "pull_request_number=$pr_number" + echo "head_sha=$head_sha" + echo "owner=$owner" + } >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v4 with: ref: main - uses: actions/setup-python@v5 with: python-version: '3.12' - # Java bots run from source (Java 22+ multi-file launch); match the Rumble's JVM. - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '25' - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - uses: actions/setup-node@v4 - with: - node-version: '24' - - run: python -m pip install -r templates/python/StarterBot/requirements.txt - - run: python scripts/validate_bot.py --root . --owner '${{ github.event.pull_request.user.login }}' --smoke --generate - - run: | + # The separate pull_request validation ran bot smoke without write access. This publisher + # runs only on trusted main and performs static catalog generation; it never launches bot code. + - name: Regenerate catalog without running bots + env: + BOT_OWNER: ${{ steps.submission.outputs.owner }} + run: python scripts/validate_bot.py --root . --owner "$BOT_OWNER" --generate + - name: Commit and publish generated catalog + run: | git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git add bots/index.json bots/owners.json - git diff --cached --quiet || git commit -m 'chore: regenerate bot catalog' - git push + if ! git diff --cached --quiet; then + git commit -m 'chore: regenerate bot catalog' + git push origin HEAD:main + fi diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 02bc216..ac90cc1 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,6 +8,7 @@ on: - templates/** - tests/** - .github/workflows/validate.yml + - .github/workflows/publish-catalog.yml permissions: contents: read diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 9e12504..31b206c 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -7,3 +7,13 @@ Moderators maintain `bots/banned.json` through reviewed pull requests. A ban can 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. Once each quarter, a moderator performs a fork drill: fork the repository, enable Actions, run validation, and regenerate the catalog without credentials other than the forge-provided token. Record the outcome in a GitHub issue. + +## GitHub Actions permissions + +The pull-request validation workflow requests `contents: read`. Public fork pull requests receive a read-only `GITHUB_TOKEN` even if a workflow asks for write access, and GitHub may require a maintainer with write permission to approve a first-time contributor's workflow run. Approve the validation run only after checking the proposed source; never grant the validation workflow write access to the upstream repository. + +After a bot pull request is merged, the publisher runs on a push to `main` that changes `bots/**`. It uses the associated merged pull request to check that the submission's `validate` check passed, then checks out trusted `main` and runs static catalog generation without `--smoke`. The publisher must never execute a submitted bot launcher with its write token. Its `GITHUB_TOKEN` needs `contents: write`, `pull-requests: read`, and `checks: read`, as declared in `.github/workflows/publish-catalog.yml`; it uses no personal access token or repository secret. + +For publication to work, GitHub Actions must be enabled and the repository or organization policy must permit these token scopes. The repository's **Settings → Actions → General → Workflow permissions** should allow read and write permissions for `GITHUB_TOKEN` (currently enabled for this repository); the workflow itself still grants only the scopes listed above. If `main` is protected by branch rules, allow `github-actions[bot]` to push the generated catalog files, or publishing will fail. Recheck these settings if organization policy or branch rules change. + +GitHub may hold a fork contributor's validation run for approval by a maintainer with write access. The publisher does not need `pull_request_target` or a privileged fork workflow. To recover a merged submission whose publication failed, a maintainer can run **Publish bot catalog** from the `main` branch with its merged pull-request number. The dispatch verifies that it merged into `main` and that its head commit has a successful `validate` check before it regenerates the catalog using the PR author's account as owner. diff --git a/README.md b/README.md index 191c9b6..df9a735 100644 --- a/README.md +++ b/README.md @@ -15,5 +15,5 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full submission, ownership, lice `bots/index.json` and `bots/owners.json` are generated by CI and must never be edited in a pull request. A catalog entry contains the bot identity, owner, source path, source-tree SHA-256, active lifecycle status, and `teamMembers`. Individual bots publish an empty member list; a TwinDuel team publishes exactly two active ` ` member identities, so a member release always forces the team entry to be resubmitted. A team directory contains nothing but its own JSON, its members may live under different platform directories, and its published platform is the members' shared platform or `Mixed`. Only `active` entries are eligible for matchmaking. -This repository is designed to be forkable: its validator is standard-library Python and GitHub Actions only invokes that script. The only forge seam is the workflow that supplies the pull-request author to the validator and publishes generated files after merge. +This repository is designed to be forkable: its validator is standard-library Python. Pull-request validation runs bot smoke checks with read-only permissions; after merge, a trusted `main` publisher verifies the merged pull request passed validation and regenerates catalog data without launching bot code. See [moderator governance](GOVERNANCE.md#github-actions-permissions) for required Actions permissions and recovery. Source-only catalog for ranked Tank Royale Rumble bots