Skip to content

admin: GitHub App control-plane lane (ghapp/repo-admin)#81

Open
primeinc wants to merge 36 commits into
mainfrom
ghapp/repo-admin
Open

admin: GitHub App control-plane lane (ghapp/repo-admin)#81
primeinc wants to merge 36 commits into
mainfrom
ghapp/repo-admin

Conversation

@primeinc

@primeinc primeinc commented May 11, 2026

Copy link
Copy Markdown
Owner

This pull request introduces a comprehensive overhaul and documentation of the GitHub Actions workflow triggers, concurrency patterns, and required status checks for the repository. It establishes clear, canonical patterns for workflow triggers, concurrency settings, and ruleset enforcement for the main and next branches, and splits out key gates into their own workflows for clarity and maintainability.

The most important changes are:

Branch Protection Rulesets

  • Added .github-stars/control-plane/rulesets/protect-main-release-only.json and .github-stars/control-plane/rulesets/protect-next.json to define protection rules for the main and next branches, specifying required status checks, allowed merge methods, and other branch protection settings. [1] [2]

Workflow Triggers and Concurrency Documentation

  • Added .github-stars/control-plane/workflow-triggers.md with detailed, citation-backed documentation on when to use each workflow trigger (pull_request, push, pull_request_target, merge_group, workflow_dispatch), how concurrency should be configured, and which patterns are banned for security or correctness reasons.

Workflow Refactoring and Modernization

  • Updated .github/workflows/00-ci.yml:
    • Renamed the workflow and job to bun gate and all gates pass.
    • Added concurrency control to cancel in-progress runs on new pushes.
    • Extended triggers to include both main and next branches for pull_request and push.
    • Moved workflow-linting (footgun guards) to a dedicated workflow, referencing it in the summary. [1] [2] [3]
  • Added .github/workflows/00a-do-not-merge-yet.yml as a dedicated workflow to block merges when the DoNotMergeYet label is present, with proper concurrency and trigger configuration.

Required Status Checks Alignment

  • Ensured that the required status checks in the new branch protection rulesets match the job names in the workflows, and clarified this mapping in the documentation. [1] [2] [3]

Security and Best Practices

  • Explicitly banned risky patterns such as use of pull_request_target, mixed-credential expressions, and leaking sensitive information in workflow outputs, both through documentation and workflow structure.

These changes significantly improve the repository's workflow reliability, security, and maintainability, while providing clear documentation for future contributors.

primeinc and others added 19 commits May 10, 2026 20:35
…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>
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>
Copilot AI review requested due to automatic review settings May 11, 2026 03:30
gemini-code-assist[bot]

This comment was marked as outdated.

This comment was marked as outdated.

@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: b23f57ef37

ℹ️ 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 thread .github/workflows/00f-sync-protected-branches-with-main.yml Outdated
primeinc and others added 4 commits May 10, 2026 23:41
…eric msgs

Three real fixes from the PR #81 review pass (codex P1 + 2 Copilot
mediums):

1) [chatgpt-codex P1, 00f:200] Fix FF detection inverted compare
   semantics. The compare API returns ahead_by/behind_by relative to
   `compare/{base}...{head}` order:
     ahead_by  = commits in head not in base
     behind_by = commits in base not in head

   Previous code called `compare/${branch}...main` then read
   behind_by as "branch behind main" — exactly inverted. With
   compare/branch...main: ahead_by = commits-in-main-not-in-branch
   (i.e. branch BEHIND), behind_by = commits-in-branch-not-in-main
   (i.e. branch AHEAD). The caller's variable names assumed the
   correct semantic, so a cleanly fast-forwardable branch was being
   reported as divergent and skipped.

   Fix: ff_state() now uses `compare/main...${branch}` so the
   API's ahead_by/behind_by directly match the caller's
   "branch ahead/behind main" semantic. Comment block expanded to
   make the API direction explicit so future-me doesn't re-invert it.

2) [Copilot 00f:106] find_release_pr's "Multiple open PRs" error
   message went to stdout while callers do `... 2>/dev/null`. The
   stderr redirect didn't help because the annotation was on
   stdout, and stdout is captured by command substitution. Net
   effect: silent failure on the multi-PR error path.

   Fix: open FD 3 to the script's real stderr at the top of the
   step (`exec 3>&2`). Helper functions now emit their `::error::`
   annotations + diagnostic lists to FD 3, which survives both the
   caller's command substitution and the `2>/dev/null` swallow on
   the happy path.

3) [Copilot 00f:229] Two summary strings hardcoded "PR #79 remains
   blocked..." This will rot the moment another downstream PR
   exists. Replace with generic "downstream PRs targeting either
   lane remain blocked..." in both the error path and the summary
   section heading.

Not addressed in this commit (with reasons):

- [Copilot 00e:36] `environment: ${{ inputs.operation == 'upsert'
  && 'github-admin' || '' }}` empty-string env on the check path.
  Per ../refs/github/docs/data/reusables/actions/jobs/section-using-
  environments-for-jobs.md, the docs describe valid forms (single
  name string, or object with name+url) but do not document
  empty-string behavior. actionlint accepts the YAML. The 00e
  workflow is dispatch-only and the brief explicitly defers live
  upsert ("Do not activate live rulesets"), so the empty-string
  path is currently unreachable. If/when 00e fires in upsert mode
  and the empty-string env breaks at runtime, split into two jobs
  (render + upsert, only upsert declares environment).

- [gemini approval-count = 1, both rulesets] Brief explicitly says
  "No `require_code_owner_review` unless a real separate
  reviewer/team exists. It does not." With one actor,
  required_approving_review_count: 1 is unsatisfiable. Brief-aligned
  value is 0.

- [gemini hardcoded App ID 3663316 in tracked JSON, both rulesets]
  Brief explicitly says "Tracked JSON must not hardcode numeric
  App IDs." Bypass actor is rendered at runtime in 00e from
  vars.GH_APP_ID. Tracked specs MUST stay with empty bypass_actors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per ../refs/github/docs/data/reusables/actions/jobs/section-using-
environments-for-jobs.md, the `environment:` job key takes a single
name string or an object with name+url — empty-string expressions
are not documented. The previous shape:

  environment: ${{ inputs.operation == 'upsert' && 'github-admin' || '' }}

worked under actionlint but is undocumented behavior at runtime.
Per Copilot review on PR #81 (00e:36), split into two jobs so only
the upsert job declares `environment: github-admin`:

- check  job: always runs on workflow_dispatch (any operation), no
  environment gate. Read-only — render specs, diff against live
  rulesets, fail on drift. Drives the green-or-red signal for the
  operator's `check` invocation.
- upsert job: needs check, runs only when `inputs.operation ==
  'upsert'`, declares `environment: github-admin` (canonical single
  name string form). Validates the APPLY_RULESETS confirmation
  string, then renders + creates-or-updates each ruleset, then
  re-fetches and diffs to verify the upsert landed cleanly.

Bash logic is duplicated inline rather than sourced from a shared
`.github/workflows/scripts/branch-rulesets-lib.sh` — sourcing isn't
the canonical Actions pattern, and would require expanding the
admin-lane path-scope allow-list to a non-rulesets directory. Two
~100-line bash blocks is the right shape for this surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vars.GH_APP_ID + secrets.GH_APP_PRIVATE_KEY match the installed App

Per the user's question 'where does the App ID come from': the
canonical answer per ../refs/github/docs/.../*.md is the App
settings page in browser
(https://github.com/settings/apps/<app-slug>). The programmatic
route requires JWT auth (GET /app), which requires knowing the App
ID — chicken/egg without a verified-good ID.

This workflow inverts: mint an installation token via
actions/create-github-app-token@v3. The mint step fails loud with a
JWT signature error if vars.GH_APP_ID is wrong (or if it doesn't
match the private key). Successful mint + GET /installation/repositories
confirming this repo is in the App's installation scope = both
vars are correct AND the App is installed where expected.

Run from the Actions tab via workflow_dispatch on
ghapp/repo-admin or after this PR merges to main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
primeinc and others added 4 commits May 11, 2026 00:52
…lugs

The single Admin Lane / Ready to merge job did three different
things at once. Extracted into 3 workflows with slugs that name
the dependency surface (gh-action for the github-builtin-only
checks; gh-app for the one that needs the App token):

  gh-action branch staleness / head matches main
  gh-action file allowlist / only allowed files
  gh-app credentials / token + install

Files:
- 00d-gh-action-branch-staleness.yml  (was 00d-admin-branch-sync-guard.yml)
- 00h-gh-action-file-allowlist.yml    (new)
- 00i-gh-app-credentials.yml          (new)

Updated protect-main-release-only.json: replaced the old
'Ready to merge' required context with the three new contexts
'head matches main', 'only allowed files', 'token + install'.

Each check has unique left-slug + sentence right-side. UI renders
as a status sentence per check, no jargon, no name/name duplication.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…workflows

Canonical answer to "when pull_request vs push vs pull_request_target
vs merge_group, and where does concurrency belong" — grounded in
../refs/github/docs and ../refs/github/github-well-architected.

Doctrine now lives at .github-stars/control-plane/workflow-triggers.md
and binds every gate workflow's trigger + concurrency choice to a
cited section of the canonical docs.

Concurrency added to all 10 workflows under .github/workflows/00*.yml:
- gates (00, 00a, 00b, 00c, 00d, 00h, 00i, 00j):
    cancel-in-progress: true   (newer run wins; older is wasted compute)
- mutations (00e ruleset upsert, 00f branch sync):
    cancel-in-progress: false  (never cancel mid-PATCH; non-transactional)

Group key is the canonical `${{ github.workflow }}-${{ github.ref }}`
per data/reusables/actions/actions-group-concurrency.md L122-126 —
unique per workflow, well-defined for both pull_request and push refs.

Trigger doctrine confirmed (NOT changed):
- pull_request stays on every required-status-check workflow. Removing
  it would silently break the ruleset's required_status_checks gate.
- pull_request + push: [main, next] is canonical, NOT an anti-pattern.
  The two events fire on disjoint refs (refs/pull/N/merge vs
  refs/heads/<branch>) — no duplicate run during PR review. The
  github docs themselves publish push: [main] standalone as the
  canonical concurrency example.
- pull_request_target stays banned per github-well-architected
  application-security/recommendations/actions-security/index.md L88
  (pwn-request risk). Zero workflows in this repo use it.
- merge_group deferred until a merge queue is enabled on main.

Also folds in pre-existing slug renames on 00b + the two ruleset
specs (Build succeeds -> build succeeds, Label cleared ->
DoNotMergeYet absent, etc.) so the required_status_checks contexts
match the lowercase job names committed in the prior split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
00e (P1 — bootstrap deadlock):
  upsert had `needs: check`, but `check_ruleset` returns 1 when the
  ruleset does not yet exist (`echo "::error::ruleset missing: ..."`).
  First-ever creation could never run — check fails red, upsert never
  starts. Drop `needs: check`. Upsert is independent: it does its own
  pre-state lookup (ruleset_id_by_name -> empty => POST, non-empty =>
  PUT) and post-upsert verification (upsert_and_verify diffs rendered
  spec against live ruleset). Authorization remains the
  `environment: github-admin` deployment-protection gate, not the
  check job.

00f (P2 — next FF policy parity with admin):
  Old behavior: no open `next -> main` PR -> red. Only
  `ghapp/repo-admin` got the FF-only PATCH fallback.
  New behavior (per user direction "successful pull to main should FF
  both branches"): both lanes use the same reconcile policy:
    1. open PR present       -> PUT /pulls/{n}/update-branch
    2. no PR, FF-able        -> PATCH /git/refs/heads/<lane> force=false
    3. no PR, divergent      -> red, refuse blind push
    4. multiple open PRs     -> red, refuse to choose
  Refactored the two near-identical lane blocks into one
  `reconcile_lane <lane> <prefix>` helper so the policy parity is
  visible in the code, not just in the comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two coupled fixes that close the bootstrap gap in PR #81:

1) Self-bootstrap trigger.

Add `push: [main]` (paths-filtered to the ruleset specs and 00e
itself) to 00e. When PR #81 merges, the same merge commit fires 00e
on main and upserts both rulesets live with enforcement=active —
no human dispatch click required.

Two trigger paths now coexist:
  - push: bootstrap. enforcement=active. APPLY_RULESETS guard
    skipped (the paths-filter + ref-guard + App-token mint are the
    authorization).
  - workflow_dispatch: human ops. enforcement=inputs.enforcement.
    APPLY_RULESETS typed-string guard still required.

The check job's enforcement env, the upsert job's enforcement env,
and both summary blocks all use the same fallback expression
`github.event_name == 'workflow_dispatch' && inputs.enforcement || 'active'`
so the rendered spec on the push path matches what gets PUT.

2) Drop `environment: github-admin`.

Per ../refs/github/docs/content/actions/how-tos/deploy/configure-and-
manage-deployments/manage-environments.md L95: "Running a workflow
that references an environment that does not exist will create an
environment with the referenced name. ... the newly created
environment will not have any protection rules or secrets
configured."

Nothing in this repo configures `github-admin`. The line was theater —
on first run it would silently create an empty environment with no
gates. Removed from the upsert job, removed from both summary blocks,
removed from the file header comment. Authorization for the mutation
is the combination of the ref-guard, the App-token mint (only works
if the App is installed with Administration: write), the paths-filter
on push, and the APPLY_RULESETS typed-string guard on dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 11, 2026 06:02

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 13 out of 13 changed files in this pull request and generated 8 comments.

Comment thread .github/workflows/00e-branch-rulesets.yml
Comment thread .github/workflows/00e-branch-rulesets.yml
Comment on lines +324 to +325
echo "- Fast-forward fallback for \`ghapp/repo-admin\` when no open PR: \`PATCH /repos/{owner}/{repo}/git/refs/heads/{branch}\` with \`force=false\` (FF-only)"
echo "- Compare API: \`GET /repos/{owner}/{repo}/compare/{branch}...main\` for FF-state inspection"
Comment on lines +74 to +77
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/installation/repositories \
--jq '.repositories | map(.full_name)')
Comment on lines +73 to +85
repos=$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/installation/repositories \
--jq '.repositories | map(.full_name)')

echo "Installed on:"
echo "${repos}" | jq -r '.[] | " - \(.)"'

if ! echo "${repos}" | jq -e --arg r "${THIS_REPO}" '. | index($r)' >/dev/null; then
echo "::error::App (vars.GH_APP_ID=${{ vars.GH_APP_ID }}) is NOT installed on ${THIS_REPO}. Check the App's installation scope at https://github.com/settings/apps/<app-slug>/installations."
exit 1
fi
Comment thread .github/workflows/00-ci.yml Outdated
Comment on lines 21 to 24
gate:
# `pnpm gate` is the single readiness command per issue #69 lesson 1.
name: all gates pass
# `bun run gate` is the single readiness command per issue #69 lesson 1.
# Sub-stages: typecheck, test, validate (taxonomy + schema), generated
Comment thread .github/workflows/00b-web-ci.yml Outdated
@@ -1,16 +1,18 @@
---
name: 'Web CI'
name: 'bun web build'

## 4. Workflow-by-workflow mapping

Eight workflow files in `.github/workflows/00*.yml`, mapped against
@primeinc

This comment has been minimized.

This comment has been minimized.

@primeinc

Copy link
Copy Markdown
Owner Author

@copilot you broke the app install lookup

Copilot AI review requested due to automatic review settings May 11, 2026 06:29
@primeinc primeinc removed the request for review from Copilot May 11, 2026 06:29

Copilot AI commented May 11, 2026

Copy link
Copy Markdown
Contributor

@copilot you broke the app install lookup

Fixed in b1450ca. You're right — GET /repos/{owner}/{repo}/installation requires JWT (App-level) auth and rejects the installation access token that actions/create-github-app-token@v3 produces, so the probe would always 401/404.

Reverted to /installation/repositories (which IS accessible with the installation token) but tightened both Copilot-flagged failure modes:

  • --paginate so installations with >30 repos don't false-negative.
  • --jq '.repositories[].full_name' | grep -Fxq "${THIS_REPO}" — no list ever reaches the log (no leak), only a single ok/error line.

Defense-in-depth note: create-github-app-token with repositories: ${{ github.event.repository.name }} already fails to mint a token if the App isn't installed on this repo, so reaching this step proves we have a token for at least this repo; the grep is the explicit observable proof.

@primeinc primeinc requested a review from Copilot May 11, 2026 06:32
@primeinc

Copy link
Copy Markdown
Owner Author

/gemini review

@primeinc

Copy link
Copy Markdown
Owner Author

@codex 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 branch protection rulesets for the main and next branches, alongside a comprehensive documentation file outlining the repository's workflow trigger and concurrency doctrine. The review feedback highlights the need to enable ruleset enforcement to align with the documentation and recommends strengthening security by requiring code owner reviews and at least one approval for pull requests.

Comment thread .github-stars/control-plane/rulesets/protect-main-release-only.json
Comment thread .github-stars/control-plane/rulesets/protect-next.json
Comment thread .github-stars/control-plane/rulesets/protect-main-release-only.json Outdated
Comment thread .github-stars/control-plane/rulesets/protect-next.json Outdated
Comment thread .github-stars/control-plane/workflow-triggers.md Outdated
primeinc and others added 2 commits May 11, 2026 02:36
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

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 13 out of 13 changed files in this pull request and generated 9 comments.

Comment on lines +108 to +116
- 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 }}
# Read-only drift check. `GET /repos/{owner}/{repo}/rulesets`

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Same as 00f:71 reply. Both GH_APP_ID (numeric, here) and GH_APP_CLIENT_ID (legacy, 01/02) exist as repo vars. Numeric ID required for ruleset bypass_actors. Verified by green 00i check. Intentional.

Comment on lines +63 to +71
- 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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Both vars exist: gh variable list shows GH_APP_ID=3663316 (numeric, used here) and GH_APP_CLIENT_ID=Iv23liRZxVz4rlcQnAKt (used by 01/02). Numeric ID is required for ruleset bypass_actors[].actor_id (00e). 00i token mint is GREEN on this PR. Intentional, not changing.

Comment thread .github/workflows/00f-sync-protected-branches-with-main.yml
Comment on lines +41 to +60
- name: Validate vars.GH_APP_ID format (ghapp/repo-admin only)
if: github.event.pull_request.head.ref == env.ADMIN_LANE_BRANCH
env:
GH_APP_ID: ${{ vars.GH_APP_ID }}
run: |
set -euo pipefail
if ! [[ "${GH_APP_ID}" =~ ^[0-9]+$ ]]; then
echo "::error::vars.GH_APP_ID must be the numeric GitHub App ID. Got: '${GH_APP_ID}'"
exit 1
fi
echo "vars.GH_APP_ID format ok: ${GH_APP_ID}"

- name: Mint App installation token (proves App ID + private key match)
id: app-token
if: github.event.pull_request.head.ref == env.ADMIN_LANE_BRANCH
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 }}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Same as 00f:71 / 00e:116 replies. GH_APP_ID and GH_APP_CLIENT_ID both exist; numeric form required for ruleset bypass_actors. This very check (token + install) is what proves the var resolves correctly — currently green on PR #81. Intentional.

Comment thread .github/workflows/00j-gh-action-workflow-lint.yml Outdated
Comment thread .github/workflows/00j-gh-action-workflow-lint.yml Outdated
Comment thread .github/workflows/00c-main-release-guard.yml Outdated
Comment thread .github-stars/control-plane/workflow-triggers.md Outdated
Comment thread .github-stars/control-plane/workflow-triggers.md Outdated

@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: b1450cad36

ℹ️ 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 thread .github/workflows/00f-sync-protected-branches-with-main.yml Outdated
…oc drift

Four real issues from Copilot/Codex/Gemini reviews; ignoring code-owner
and review-count complaints per the protection-stage brief.

00f — URL-encode `${lane}`/`${branch}` in REST path segments.
  Per Copilot 00f:185 + Codex 00f:212 (P1). The admin lane name
  `ghapp/repo-admin` contains `/`, so raw interpolation produced
  `/branches/ghapp/repo-admin` which the GitHub API parses as a
  multi-segment path → 404. Per
  ../refs/github/docs/content/rest/using-the-rest-api/troubleshooting-
  the-rest-api.md L74: "any path parameters must be URL encoded. For
  example, any slashes in the parameter value must be replaced with
  `%2F`."
  Added a `urlencode()` helper using `jq -rn @uri` and applied it to
  all 5 path-segment interpolations: /branches/{lane} (3 sites),
  /git/refs/heads/{lane} (1 site), /compare/main...{branch} (1 site).
  PR-update-branch path was unaffected — uses PR number, no slashes.

00c — fix wrong filename reference in comment.
  Per Copilot 00c:12. Comment referenced `00d-admin-branch-sync-guard.yml`
  which doesn't exist in the repo (post-split, that responsibility
  lives in 00h-gh-action-file-allowlist.yml + 00d-gh-action-branch-
  staleness.yml).

00j — preserve source line numbers in lint guards.
  Per Copilot 00j:97 + 00j:132. Both the mixed-credential and
  blocked-org guards stripped comment lines via `grep -v` before
  `grep -nE`, which produced shifted line numbers in error output —
  reviewers chasing a violation would land on the wrong line.
  Refactored to grep the original file with `-n`, then awk-filter
  out hits whose matched content is a comment line, preserving the
  source line number throughout.

workflow-triggers.md — fix doc/code drift.
  Per Copilot review-comments-md:178 + 206 + Gemini 204. Table row
  for 00-ci.yml said `pnpm gate`; file says `bun gate`. The 00e
  self-bootstrap note said push activates with enforcement=active;
  per current code (issue #82 doctrine) push reads enforcement from
  the tracked spec JSON and the github-admin environment approval
  is what releases credentials. Updated both.

actionlint.yaml — ignore stale-schema false positive.
  actionlint 1.7.12 (latest) does not yet ship the
  `environment.deployment` key in its schema, even though
  `deployment: false` is canonical per 4 separate ../refs github docs
  (section-using-environments-for-jobs.md L57-67,
  deploy-to-environment.md L60, control-deployments.md L69,
  create-custom-protection-rules.md L45). 00e uses it per issue #82
  to gate credential release without emitting a deployment record.
  Added a path-scoped `ignore` entry alongside the existing
  `models` permission and SC2034/SC2015 ignores.

Dismissed (not changed):
- vars.GH_APP_ID vs vars.GH_APP_CLIENT_ID (00e:116, 00f:71, 00i:60):
  both vars exist in repo (verified `gh variable list`); 00i token
  mint is GREEN on PR #81. The numeric ID is required for the
  ruleset bypass_actors[].actor_id field. Intentional.
- Hardcode App ID into spec JSON bypass_actors (rulesets:64,70):
  brief says tracked JSON must not hardcode App IDs; bypass_actors
  is rendered at runtime from vars.GH_APP_ID by 00e's render_spec.
- Required review count = 0 (rulesets:24,58): solo-owner repo,
  per brief. Merge-time governance comes from required_status_checks.
- Push-default to disabled / environment missing (00e:59, 00e:336):
  resolved by issue #82 doctrine commit (3c1dbe6) — push inherits
  spec JSON, upsert job has `environment: github-admin` with
  `deployment: false`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

3 participants