Skip to content

Hard-gate PR #81 GitHub App ruleset mutations with a protected github-admin environment #82

Description

@primeinc

Related PR

Problem

PR #81 adds a GitHub App control-plane lane and a branch-rulesets-upsert job that can create or update repository rulesets. The bot/App is the actor that performs the mutation, but the missing boundary is when that bot is allowed to receive mutation authority.

Right now the workflow relies mostly on branch/ref guards, path filters, a typed confirmation for manual dispatch, and GitHub App token minting. Those are useful, but they are not the same thing as GitHub Actions environment protection. The missing environment layer is the approval/secret-release gate GitHub provides specifically for jobs that should not start, and should not receive protected secrets, until protection rules pass.

Evidence reviewed

Claim Evidence label Notes
PR #81 introduces a GitHub App control-plane lane for ghapp/repo-admin and ruleset operation workflows. Direct evidence PR #81 overview describes the GitHub App control-plane lane, tracked ruleset specs, lane guards, and ruleset check/upsert workflow.
00e-branch-rulesets.yml has a check job and an upsert job. Direct evidence Current PR head file splits read-only drift checking from mutation.
The upsert job runs on main for push and for workflow_dispatch when operation == 'upsert'. Direct evidence Current upsert.if condition allows both trigger paths.
The upsert job currently does not declare an environment: key. Direct evidence Current upsert job has name, runs-on, if, and steps, but no environment declaration.
The upsert job mints a GitHub App token and then calls the repository rulesets API to create/update live rulesets. Direct evidence Current Create GitHub App token step requests permission-administration: write; upsert_ruleset calls POST /repos/${REPO}/rulesets or PUT /repos/${REPO}/rulesets/${id}.
The push path self-bootstraps with enforcement=active. Direct evidence Current comments and expression set push-triggered enforcement to active.
The PR review specifically flags missing environment: on the live ruleset mutation job. Direct evidence Copilot comment on PR #81 says the branch-rulesets-upsert job performs live ruleset mutations and should declare an environment such as github-admin.
Repo-side github-admin environment configuration is currently unverified. Missing proof I have PR/workflow evidence, not repository Settings evidence. This issue must require proof instead of assuming the environment exists or is protected.

Canonical GitHub behavior that matters

GitHub environments are not only for human deployments. They are a job-level gate. A job that references an environment must pass that environment's protection rules before the job starts and before environment secrets are available.

Canonical docs:

Important doc points:

  1. A workflow job can reference an environment.
  2. The job is subject to the environment's configured protection rules.
  3. Environment secrets are only available to jobs that use the environment, and only after configured protection rules pass.
  4. Required reviewers can approve or reject waiting jobs.
  5. GitHub can auto-create a referenced environment if it does not already exist, but an auto-created environment may have no protection rules or secrets configured.
  6. deployment: false can use environment gating without creating deployment records, while required reviewers and wait timers still apply.
  7. Custom deployment protection-rule Apps require a deployment object; therefore deployment: false is incompatible with custom deployment protection rules.

Why this matters

1. Bot-run does not mean ungated

The GitHub App should still be the actor performing the mutation. The environment gate controls when the job is allowed to start and when the job can receive environment-scoped credentials. That distinction matters because the bot can be technically correct and still do the wrong thing at machine speed.

Without the environment gate, the upsert path can reach the token-minting step as soon as the workflow trigger and if condition allow it. That means approval is modeled indirectly through branch/path/confirmation logic rather than through the canonical GitHub environment mechanism built for approval and secret release.

2. YAML alone does not prove protection

Adding environment: github-admin is necessary, but not sufficient. If github-admin does not already exist, or exists without protection rules, the YAML can look correct while the real gate is hollow. GitHub explicitly documents that a workflow referencing a missing environment can create that environment, and that the newly created environment may have no protection rules or secrets.

So the fix has two parts:

  • workflow YAML: attach only the mutation job to github-admin
  • repo settings: pre-create and configure github-admin with actual protection rules

Skipping the second part is how we get a beautiful fake lock drawn on a door that still opens. Try not to let the YAML cosplay as security.

3. The current push path is the highest-risk path

workflow_dispatch has confirm_upsert=APPLY_RULESETS, which is a fat-finger guard. The push path explicitly skips that typed confirmation and uses enforcement=active by default. That may be intentional self-bootstrap behavior, but it is exactly why the environment gate matters.

If push-to-main touching ruleset specs can auto-upsert active rulesets, the missing question is: what approval event authorizes the bot to receive the mutation credential? The answer should be: the github-admin environment approval/protection rules.

4. Credential placement is part of the gate

The environment only controls secrets that are environment-scoped and only jobs that reference that environment. If GH_APP_PRIVATE_KEY remains available as a repo/org secret to ungated jobs, then the environment is not actually controlling credential release for the mutation path.

The desired shape is: mutation credentials needed for ruleset writes are scoped to github-admin where feasible, and only the upsert job references that environment.

5. Read-only checks should not casually mint write-capable credentials

The current check job is documented as read-only, but it also mints an App token with permission-administration: write. That may be required depending on GitHub's ruleset API behavior and App permission model, but it should not remain an accidental assumption.

The right follow-up is to test whether check can use the minimum permission required for ruleset reads. If permission-administration: read works, use it for check; keep write only for upsert. If GitHub requires write for the current path, document that explicitly in the workflow comments and summary.

6. Installation verification should not leak installation scope

The 00i-gh-app-credentials.yml workflow currently calls /installation/repositories without pagination and prints the full repo list. Copilot flagged both issues. This is adjacent to the same environment/credential-control theme: do not list and log more installation scope than needed.

Use the repo-specific installation verification path where possible, or paginate/slurp without printing the full repository list. The check should prove "this App is installed on this repo," not emit a directory of everything else the App can see.

Required fixes

A. Attach only the mutation job to github-admin

Patch 00e-branch-rulesets.yml so only jobs.upsert declares the environment.

Recommended shape for an admin bot gate that is not a real deployment:

upsert:
  name: branch-rulesets-upsert
  runs-on: ubuntu-latest
  environment:
    name: github-admin
    deployment: false
  if: |
    github.ref == 'refs/heads/main' && (
      github.event_name == 'push' ||
      (github.event_name == 'workflow_dispatch' && inputs.operation == 'upsert')
    )

Use plain environment: github-admin instead if we intentionally want deployment records or plan to use custom deployment protection-rule Apps. Do not use deployment: false with custom deployment protection-rule Apps because GitHub documents that those require deployment objects.

B. Pre-create and configure the repo environment

In repository settings, create/configure:

Environment name: github-admin
Required reviewers: enabled
Prevent self-review: enabled if a separate reviewer exists / is practical
Administrator bypass: disabled if this is meant to be a hard gate
Deployment branches/tags: selected branches only -> main
Environment secrets: GH_APP_PRIVATE_KEY and other mutation-only credentials where feasible
Environment variables: mutation-only vars if needed; keep non-secret GH_APP_ID wherever repo policy prefers

Why:

  • Required reviewers provide the explicit approval step before the bot receives mutation authority.
  • Prevent self-review avoids one actor triggering and approving the same mutation path, when a separate reviewer exists.
  • Branch restriction makes the repo-side environment policy match the workflow's refs/heads/main guard.
  • Disabling admin bypass prevents the emergency hatch from becoming the normal path.
  • Environment-scoped secrets make the approval gate control credential release, not merely job labeling.

C. Validate whether check can use read-only App authority

Current direct evidence: check is read-only by design but mints an App token with permission-administration: write.

Action:

  1. Try permission-administration: read for the check job.
  2. Confirm GET /repos/{owner}/{repo}/rulesets and GET /repos/{owner}/{repo}/rulesets/{id} still work.
  3. If it works, keep read for check and write only for upsert.
  4. If it fails, keep write but add a workflow comment explaining why the read-only drift check still requires a write-capable token.

Why:

A read-only job with write-capable credentials is still a credential exposure surface. The fact that the bash path does not mutate today is not the same as the job lacking mutation authority.

D. Decide whether push self-bootstrap should really force active

Current direct evidence: push to main touching ruleset specs or this workflow renders ENFORCEMENT=active.

Action options:

  • Option 1: Keep push self-bootstrap active, but require github-admin environment approval before the upsert job starts.
  • Option 2: Change push path to preserve tracked JSON/default disabled, and require manual workflow_dispatch with operation=upsert and enforcement=active for activation.

Recommended default: Option 1 only if the repo-side github-admin environment is truly protected and secrets are environment-scoped. Otherwise use Option 2 until the environment is proven.

Why:

A push-triggered active upsert is powerful. It may be the intended admin-lane bootstrap, but without the environment gate it turns branch/path matching into the practical approval layer. That is thinner than the canonical GitHub protection model.

E. Fix App installation verification in 00i-gh-app-credentials.yml

Replace the full installation repo listing behavior.

Preferred shape:

  • Verify only the current repository installation, if the endpoint/token allows it.
  • If listing is unavoidable, use --paginate --slurp and do not print the full repo list.
  • Log only success/failure for THIS_REPO.

Why:

The purpose is to prove the App is installed on this repo. Printing every repository visible to the installation increases log exposure and makes the check noisier than the proof requires. Also, without pagination, an installation that can access more than the first page of repos can produce a false negative.

Acceptance criteria

  • jobs.upsert in .github/workflows/00e-branch-rulesets.yml declares environment: github-admin or environment: { name: github-admin, deployment: false }.
  • jobs.check does not declare environment: unless we intentionally decide the drift check also needs environment-scoped credential release.
  • github-admin exists in repository Environments before merge or before the workflow is relied on.
  • github-admin has required reviewers configured, or the issue explicitly documents why this repo cannot use required reviewers yet.
  • github-admin has branch/tag restrictions aligned to main, unless explicitly rejected with rationale.
  • Admin bypass policy is explicitly decided and documented.
  • Mutation credentials used by the upsert job are environment-scoped where feasible.
  • The upsert job reaches a waiting/protection state before minting the mutation token when environment reviewers are configured.
  • Push-triggered active enforcement is either environment-gated or changed to avoid automatic activation.
  • 00i-gh-app-credentials.yml no longer prints the full installation repository list.
  • 00i-gh-app-credentials.yml no longer risks a false negative from unpaginated /installation/repositories.
  • The workflow summary states that environment approval authorizes bot credential release, not that a human performs the mutation.

Non-goals / separate cleanup from the same review wave

The following PR #81 comments are useful but not part of this environment-control issue unless we choose to fold them into the same patch:

  • Fix summary text in 00f-sync-protected-branches-with-main.yml to match compare/main...{branch} semantics.
  • Rename bun gate / bun web build surfaces if the implementation actually uses pnpm or npm.
  • Update workflow-triggers.md from "Eight workflow files" to "Ten workflow files".

Those are correctness/documentation cleanup items. This issue is specifically about the missing github-admin environment gate and the bot credential-release boundary.

Validation plan

  1. Run/trigger the ruleset workflow on a path that would execute upsert.
  2. Confirm the job pauses for github-admin protection before token minting.
  3. Confirm the job cannot access environment-scoped mutation secrets before approval.
  4. Approve the environment as an allowed reviewer.
  5. Confirm token minting occurs only after approval.
  6. Confirm ruleset upsert still verifies post-write state.
  7. Confirm check remains read-only and uses the narrowest workable App permission.
  8. Confirm Actions logs do not disclose the full GitHub App installation repository list.

Evidence labels / uncertainty

  • Direct evidence: PR admin: GitHub App control-plane lane (ghapp/repo-admin) #81 metadata, PR admin: GitHub App control-plane lane (ghapp/repo-admin) #81 review threads, current PR head contents of 00e-branch-rulesets.yml and 00i-gh-app-credentials.yml.
  • Weak inference: github-admin should be configured with required reviewers, branch restriction to main, and environment-scoped mutation credentials because those are the GitHub environment controls that map to this bot mutation path.
  • Missing proof: repository Settings -> Environments state is not visible here; the issue must be closed only after someone verifies the actual github-admin environment configuration, not merely after adding YAML.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions