admin: add GitHub App branch ruleset control plane#80
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9b060a834
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "type": "required_status_checks", | ||
| "parameters": { | ||
| "strict_required_status_checks_policy": true, | ||
| "required_status_checks": [ | ||
| { |
There was a problem hiding this comment.
Require only checks that run on next-targeted PRs
This protect-next ruleset makes gate, workflow-lint, and build mandatory for refs/heads/next, but those jobs are configured to run only for pull requests into main (.github/workflows/00-ci.yml:4-6 and .github/workflows/00b-web-ci.yml:8-10). Once this ruleset is upserted with enforcement=active, PRs targeting next will be blocked waiting for checks that never start, effectively preventing merges into next.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds an admin-oriented “control plane” for managing GitHub repository rulesets via a GitHub App installation token, with tracked JSON ruleset specs and a workflow that can check drift or upsert rulesets.
Changes:
- Introduces a
branch-rulesetsworkflow that renders tracked JSON specs, compares them to live rulesets, and optionally creates/updates them via the GitHub REST API. - Adds a
main-release-guardworkflow intended to be used as a required status check for PRs targetingmain. - Adds two tracked ruleset specs for
nextandmainunder.github-stars/control-plane/rulesets/.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| .github/workflows/00e-branch-rulesets.yml | New manual control-plane workflow to check/upsert repository rulesets using a GitHub App token. |
| .github/workflows/00c-main-release-guard.yml | New PR guard workflow to enforce next -> main source-branch policy. |
| .github-stars/control-plane/rulesets/protect-next.json | Adds tracked ruleset spec intended to protect the next branch (PR + required checks + history/FF/deletion rules). |
| .github-stars/control-plane/rulesets/protect-main-release-only.json | Adds tracked ruleset spec intended to protect main, including the new main-release-guard required check. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - name: Create GitHub App token | ||
| id: app-token | ||
| uses: actions/create-github-app-token@v3 | ||
| with: | ||
| app-id: ${{ vars.GH_APP_ID }} | ||
| private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
| repositories: ${{ github.event.repository.name }} | ||
| permission-administration: write | ||
|
|
| id=$(gh api "/repos/${REPO}/rulesets?includes_parents=false" \ | ||
| --jq ".[] | select(.name == \"${name}\") | .id" \ |
| id=$(gh api "/repos/${REPO}/rulesets?includes_parents=false" \ | ||
| --jq ".[] | select(.name == \"${name}\") | .id" \ | ||
| | head -n 1 || true) | ||
|
|
| gh api "/repos/${REPO}/rulesets/${id}" > "${actual}.raw" | ||
| normalize_ruleset < "${actual}.raw" > "${actual}" |
| if: github.ref == 'refs/heads/main' | ||
| environment: ${{ inputs.operation == 'upsert' && 'github-admin' || '' }} | ||
| steps: | ||
| - name: Checkout tracked ruleset specs | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Validate upsert authorization | ||
| if: inputs.operation == 'upsert' | ||
| env: |
| - name: Create GitHub App token | ||
| id: app-token | ||
| uses: actions/create-github-app-token@v3 | ||
| with: | ||
| app-id: ${{ vars.GH_APP_ID }} | ||
| private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
| repositories: ${{ github.event.repository.name }} | ||
| permission-contents: write | ||
| permission-pull-requests: write |
| set -euo pipefail | ||
|
|
||
| owner="${REPO%%/*}" | ||
| repo_name="${REPO#*/}" |
|
/gemini review @codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces two new GitHub ruleset configurations, protect-main-release-only.json and protect-next.json, to manage branch protections for the main and next branches. The reviewer recommends strengthening the security of these branches by requiring at least one approving review and ensuring that new pushes require fresh approval, as the current configuration allows merging without any peer review.
| "require_last_push_approval": false, | ||
| "required_approving_review_count": 0, |
There was a problem hiding this comment.
Setting required_approving_review_count to 0 and require_last_push_approval to false allows merging pull requests into the main branch without any peer review or re-approval after new code is pushed. For a ruleset intended to protect the production branch, it is recommended to require at least one approval and ensure that new pushes require fresh approval to maintain code quality and security.
| "require_last_push_approval": false, | |
| "required_approving_review_count": 0, | |
| "require_last_push_approval": true, | |
| "required_approving_review_count": 1, |
| "require_last_push_approval": false, | ||
| "required_approving_review_count": 0, |
There was a problem hiding this comment.
Setting required_approving_review_count to 0 allows merging pull requests into the next branch without any peer review. Consider requiring at least one approval and enabling require_last_push_approval to ensure the integrity of the next branch before it reaches main.
| "require_last_push_approval": false, | |
| "required_approving_review_count": 0, | |
| "require_last_push_approval": true, | |
| "required_approving_review_count": 1, |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d866363750
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| id: app-token | ||
| uses: actions/create-github-app-token@v3 | ||
| with: | ||
| app-id: ${{ vars.GH_APP_ID }} |
There was a problem hiding this comment.
Use configured GitHub App client ID for token creation
Switching the token step to vars.GH_APP_ID introduces a config mismatch with the repo’s documented auth contract (vars.GH_APP_CLIENT_ID + secrets.GH_APP_PRIVATE_KEY). In repositories that only follow that existing setup, this workflow will fail before any PR sync logic runs because the app token cannot be minted. Since this job does not need the numeric App ID for API payloads, keeping the established client-id variable avoids a hard runtime failure for the default configuration.
Useful? React with 👍 / 👎.
…nd-main + auto-sync Per the protection-stage brief, this is one coherent diff that brings admin/ghapp-rulesets to the required final state for the user-owned public repo (no org/team layer, App as the automation/protection actor, main = release lane, next = integration lane). Changes: 1) Delete .github/CODEOWNERS (brief rule #2). Solo-owner CODEOWNERS is fake trust — there is no separate reviewer/team; CODEOWNERS only adds friction without governance. Path-based ownership for a one-actor repo is theatre. 2) Drop require_code_owner_review + required_approving_review_count from both ruleset specs (brief rule #3). Without a separate reviewer the rule is unsatisfiable on every PR; setting review_count=0 + dropping require_code_owner_review reflects the actual governance shape (App + status checks gate, not human approver gate). The App is still the bypass actor for the few cases that need it. 3) Render App bypass with bypass_mode: "pull_request" (brief rule #4). Per the GitHub REST docs: "pull_request means that an actor can only bypass rules on pull requests" and "pull_request is only applicable to branch rulesets." That's strictly tighter than the previous "always" and matches the workflow shape (App bypass exists to close PRs, not to bypass the rule entirely). 4) Add 00d-admin-branch-sync-guard.yml (brief rule #9). Runs on every PR to main; for admin/* heads it queries GET /repos/{owner}/{repo}/compare/{base}...{head} and fails the PR if behind_by > 0. For non-admin heads it passes through (so the check name remains a viable required-status-check on every PR to main without leaving non-admin PRs perpetually pending). Update path: rebase or use the GitHub UI's Update branch button (which calls PUT /pulls/{n}/update-branch with expected_head_sha for the safe path). 5) Add admin-branch-sync-guard to protect-main-release-only.json's required_status_checks. The check is now both wired (workflow exists) and required (ruleset references it). 6) 00f-sync-next-with-main.yml: add `push: branches: [main]` trigger so admin merges to main propagate the next branch automatically via the documented update-branch API (brief rule #10). The existing workflow_dispatch fallback retains check/sync inputs. Operation defaults to `sync` on push; on dispatch the input wins. Removed unused repo_name shell var. What was removed: - .github/CODEOWNERS (entire file; brief rule #2) - require_code_owner_review: true (both rulesets; brief rule #3) - required_approving_review_count: 1 (both rulesets; brief rule #3) - bypass_mode: "always" (replaced with "pull_request"; brief rule #4) - 00f-sync-next-with-main.yml's unused repo_name shell variable Native GitHub primitives used: - Branch rulesets (target: branch) with bypass_actors - bypass_mode: pull_request (App-shaped governance) - required_status_checks rule with strict_required_status_checks_policy - required_linear_history + non_fast_forward + deletion rules - GET /repos/{owner}/{repo}/compare/{base}...{head} for behind-main check - PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch with expected_head_sha for the next-with-main sync (stale-head guard) - create-github-app-token@v3 for short-lived App tokens with minimal scoped permissions per workflow Tracked JSON does not hardcode App IDs; bypass actor is rendered at runtime in 00e-branch-rulesets.yml from vars.GH_APP_ID. Remaining manual repo settings: - Create vars.GH_APP_ID (numeric App ID for the primeinc-github-stars App). The branch-rulesets workflow guards against this with `^[0-9]+$` regex and fails loud. - Configure `github-admin` deployment environment with required reviewers (the brief notes this is the future webhook/custom-deployment-protection-app surface). Until then, upsert is gated only by the workflow's APPLY_RULESETS confirmation and refs/heads/main check. Deferred to a future stage (per brief): external webhook / custom deployment protection app. The github-admin environment is shaped for it; activation requires a separate deployment. Do not merge: brief rule "Do not merge this PR" + "Do not merge PR #79" + "Do not activate live rulesets" all stand. PR #79 unblock condition: this admin PR merges to main, then 00f-sync-next-with-main fires automatically on the push to main and updates the next branch PR's head, then chore/bun-modernization (PR #79's branch) rebases against main + retargets to next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator-required manual repo settingsThe protection-stage diff at Before any of the App-shaped workflows ( 1.
|
Per the user's correction: the canonical admin/control-plane branch
name is `ghapp/repo-admin`, not `ghapp/next`. `next` already means
the integration lane; overloading the name on the admin lane makes
future humans stupider. Branch renamed remote
(admin/ghapp-rulesets -> ghapp/repo-admin) and locally to match.
Three workflow changes form one coherent diff:
1) 00c-main-release-guard.yml: rewrite to accept exactly two
repo-owned source branches into main:
- `next` integration / release lane
- `ghapp/repo-admin` control-plane admin lane
Anything else fails with both allowed lanes named in the error.
Forks fail (BASE_REPO != HEAD_REPO).
2) 00d-admin-branch-sync-guard.yml: tighten head-branch scope from
`admin/*` glob to exactly `ghapp/repo-admin` (env
ADMIN_LANE_BRANCH). Add a SECOND check — path-scope guard — that
fails the PR if any changed file is outside the Medium scope
per the brief:
- .github/workflows/00*.yml
- .github-stars/control-plane/**
- AGENTS.md
- docs/automation/**
- docs/security.md
- .github/PULL_REQUEST_TEMPLATE.md
Pass-through for non-admin heads so this required-status-check
name remains viable on every PR to main.
3) Replace 00f-sync-next-with-main.yml with
00f-sync-protected-branches-with-main.yml. The brief's missing
piece: a push-to-main reconciler that brings forward BOTH long-
lived lanes when main moves, or marks them stale.
- For `next`: prefer GitHub's update-branch API on the open
repo-owned next -> main release PR with expected_head_sha.
If no release PR is open, fail loud (per release-lane policy
— the release PR is the documented surface for next->main
update-branch calls).
- For `ghapp/repo-admin`: prefer update-branch API if an open
admin PR exists. If no open PR, FF-state the branch via
compare API; fast-forward via PATCH /git/refs/heads/{branch}
(force=false) only when ahead_by=0. Divergent histories
fail loud (refuse to blind-push). Up-to-date is recorded.
- GitHub App installation token only. No PAT fallback.
- Workflow fails red if either lane could not be synced; PR #79
remains blocked until both lanes are caught up.
- Summary surfaces main_sha + each lane's before/after SHA or
blocker reason.
What was removed:
- The `admin/*` glob in 00d (replaced by exact `ghapp/repo-admin`)
- The single-lane (next-only) restriction in 00c (now allows
ghapp/repo-admin too)
- 00f-sync-next-with-main.yml entirely (subsumed by the broader
protected-branches reconciler)
- Implicit acceptance of any path on admin PRs (now path-scoped)
Native GitHub primitives used (additions to the prior set):
- `gh api --paginate /repos/.../pulls/{n}/files` for path-scope
enforcement
- bash `extglob`/`globstar` for `**` glob matching against the
allow-list
- `PATCH /repos/{owner}/{repo}/git/refs/heads/{branch}` with
force=false for FF-only branch advancement
- `GET /repos/{owner}/{repo}/compare/{branch}...main` to determine
FF-state before any branch advancement
Operator manual settings (already documented in PR #80 comment):
- vars.GH_APP_ID = 3663316 still required
- secrets.GH_APP_PRIVATE_KEY still required
- github-admin environment still optional (only for live ruleset
upsert)
Do not merge: brief stands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds a dedicated GitHub App-powered branch ruleset control-plane surface, separated from the modernization PR.
This PR adds tracked ruleset JSON specs and workflow plumbing to check or upsert GitHub repository rulesets through the GitHub App installation token.
Read-the-room evidence
Local refs read
admin/ghapp-rulesetswas created from currentmainSHAf80e3388590f6a101836f083672ff2c474f89651.main...admin/ghapp-rulesets: branch is 6 commits ahead, 0 behind, with only ruleset/control-plane additions..github-stars/control-plane/rulesets/protect-next.json.github-stars/control-plane/rulesets/protect-main-release-only.json.github/workflows/00c-main-release-guard.yml.github/workflows/00e-branch-rulesets.ymlUpstream canonical refs read
/repos/{owner}/{repo}/rulesets; create/update requires Administration write permission for GitHub Apps.Integrationactors and require a numeric actor id.Mapping table
protect-nextruleset specenforcement: disabledand no hardcoded App IDbranch-rulesetscheckprotect-main-release-onlyruleset specmain-release-guardstatus checkbranch-rulesetscheckmain-release-guardworkflownext -> mainbranch-rulesetsworkflowvars.GH_APP_ID; check/upsert modesEvidence labels
disabled -> check -> upsert -> activerollout is the safest non-Enterprise substitute forevaluate.Test plan
main.branch-rulesetswithoperation=check,enforcement=disabled.branch-rulesetswithoperation=upsert,enforcement=disabled.operation=check,enforcement=disabled.operation=upsert,enforcement=active.Notes
vars.GH_APP_ID.evaluateis intentionally not used because it is Enterprise-gated in this context.