Skip to content

admin: add GitHub App branch ruleset control plane#80

Closed
primeinc wants to merge 18 commits into
mainfrom
admin/ghapp-rulesets
Closed

admin: add GitHub App branch ruleset control plane#80
primeinc wants to merge 18 commits into
mainfrom
admin/ghapp-rulesets

Conversation

@primeinc

Copy link
Copy Markdown
Owner

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-rulesets was created from current main SHA f80e3388590f6a101836f083672ff2c474f89651.
  • Compared main...admin/ghapp-rulesets: branch is 6 commits ahead, 0 behind, with only ruleset/control-plane additions.
  • Read tracked ruleset specs:
    • .github-stars/control-plane/rulesets/protect-next.json
    • .github-stars/control-plane/rulesets/protect-main-release-only.json
  • Read workflow files:
    • .github/workflows/00c-main-release-guard.yml
    • .github/workflows/00e-branch-rulesets.yml

Upstream canonical refs read

  • GitHub repository rulesets REST API docs: repository rulesets are managed through /repos/{owner}/{repo}/rulesets; create/update requires Administration write permission for GitHub Apps.
  • GitHub repository rulesets management docs: rulesets can be imported from JSON files and managed through UI/API.
  • GitHub ruleset bypass actor shape: App bypass actors are Integration actors and require a numeric actor id.

Mapping table

Local change Local refs read Upstream canonical source Local adaptation Proof / gate
Track protect-next ruleset spec JSON spec read GitHub ruleset JSON/API shape Canonical repo spec uses enforcement: disabled and no hardcoded App ID PR diff + future branch-rulesets check
Track protect-main-release-only ruleset spec JSON spec read GitHub ruleset JSON/API shape Adds required main-release-guard status check PR diff + future branch-rulesets check
Add main-release-guard workflow Workflow read GitHub Actions PR metadata context Allows only repo-owned next -> main CI/actionlint after PR opens
Add branch-rulesets workflow Workflow read GitHub REST ruleset API + App token action Renders sole App bypass actor from vars.GH_APP_ID; check/upsert modes CI/actionlint after PR opens

Evidence labels

  • Direct evidence: added files in this branch; compare result shows 4 added files only.
  • Weak inference: disabled -> check -> upsert -> active rollout is the safest non-Enterprise substitute for evaluate.
  • Unsupported: live rulesets have not yet been created/updated; this PR only adds the executable control-plane.
  • Blocked: workflow cannot be manually dispatched until merged to the default branch.

Test plan

  • CI/actionlint passes on this PR.
  • Merge to main.
  • Run branch-rulesets with operation=check, enforcement=disabled.
  • Expected first result if rulesets do not exist: missing rulesets.
  • Run branch-rulesets with operation=upsert, enforcement=disabled.
  • Re-run operation=check, enforcement=disabled.
  • When verified, run operation=upsert, enforcement=active.

Notes

  • The tracked JSON specs intentionally do not hardcode the GitHub App numeric ID.
  • The workflow injects the sole bypass actor at runtime from vars.GH_APP_ID.
  • evaluate is intentionally not used because it is Enterprise-gated in this context.

@primeinc primeinc requested a review from Copilot May 11, 2026 00:43
@primeinc primeinc marked this pull request as ready for review May 11, 2026 00:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +29 to +33
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"required_status_checks": [
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-rulesets workflow that renders tracked JSON specs, compares them to live rulesets, and optionally creates/updates them via the GitHub REST API.
  • Adds a main-release-guard workflow intended to be used as a required status check for PRs targeting main.
  • Adds two tracked ruleset specs for next and main under .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.

Comment on lines +34 to +43
- 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

Comment on lines +128 to +129
id=$(gh api "/repos/${REPO}/rulesets?includes_parents=false" \
--jq ".[] | select(.name == \"${name}\") | .id" \
Comment on lines +169 to +172
id=$(gh api "/repos/${REPO}/rulesets?includes_parents=false" \
--jq ".[] | select(.name == \"${name}\") | .id" \
| head -n 1 || true)

Comment on lines +187 to +188
gh api "/repos/${REPO}/rulesets/${id}" > "${actual}.raw"
normalize_ruleset < "${actual}.raw" > "${actual}"

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment on lines +35 to +43
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:
Comment on lines +24 to +33
- 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#*/}"
@primeinc

Copy link
Copy Markdown
Owner Author

/gemini review

@codex review

@primeinc

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +23 to +24
"require_last_push_approval": false,
"required_approving_review_count": 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
"require_last_push_approval": false,
"required_approving_review_count": 0,
"require_last_push_approval": true,
"required_approving_review_count": 1,

Comment on lines +23 to +24
"require_last_push_approval": false,
"required_approving_review_count": 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
"require_last_push_approval": false,
"required_approving_review_count": 0,
"require_last_push_approval": true,
"required_approving_review_count": 1,

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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>
@primeinc

Copy link
Copy Markdown
Owner Author

Operator-required manual repo settings

The protection-stage diff at 1d54bb24 is brief-compliant and CI-green where designed. The two FAILing checks (do-not-merge-yet, main-release-guard) are deliberate gates per the brief — they prove the protection mechanisms work.

Before any of the App-shaped workflows (00e-branch-rulesets, 00f-sync-next-with-main) can mint App tokens, the operator needs to land these settings:

1. vars.GH_APP_ID — REQUIRED, currently missing

Workflows: 00e-branch-rulesets.yml (line 56, 68), 00f-sync-next-with-main.yml (line 49)

Set:

gh variable set GH_APP_ID --body 3663316

(App ID 3663316 per docs/automation/bot-naming.md. The 00e workflow has a ^[0-9]+$ regex guard that fails loud with a clear error if this is unset, so the absence is fail-loud not silent-broken.)

2. secrets.GH_APP_PRIVATE_KEY — should already exist; verify

Used by 00e + 00f token mint via actions/create-github-app-token@v3. Verify:

gh secret list | grep GH_APP_PRIVATE_KEY

(Cannot enumerate values via API. The auth-doctor work in src/auth/setup-doctor.ts implies it exists for the existing fetch/sync workflows.)

3. github-admin deployment environment

Used by 00e-branch-rulesets.yml line 36 only when operation=upsert. Configure via repo Settings → Environments → New environment → github-admin. Add required reviewers (the operator) so live ruleset upserts go through a manual review.

Until this environment exists, 00e workflow_dispatch operation=upsert will fail at the environment-resolution step.

4. Live ruleset activation — DEFERRED

Per the brief: "Do not activate live rulesets." The current ruleset specs are pinned at enforcement: disabled. When the operator wants live enforcement, run 00e-branch-rulesets.yml workflow_dispatch operation=upsert enforcement=active confirm_upsert=APPLY_RULESETS.

Cross-references

@primeinc primeinc closed this May 11, 2026
@primeinc primeinc deleted the admin/ghapp-rulesets branch May 11, 2026 03:17
primeinc added a commit that referenced this pull request May 11, 2026
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>
@primeinc

Copy link
Copy Markdown
Owner Author

Closed in favor of #81 after the canonical branch name was finalized as ghapp/repo-admin (was admin/ghapp-rulesets then briefly ghapp/next). Same diff + the v2 rework that pinned ghapp/repo-admin as the canonical control-plane lane. Continue at #81.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants