admin: GitHub App control-plane lane (ghapp/repo-admin)#81
Conversation
…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>
There was a problem hiding this comment.
💡 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".
…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>
…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>
| 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" |
| -H "Accept: application/vnd.github+json" \ | ||
| -H "X-GitHub-Api-Version: 2022-11-28" \ | ||
| /installation/repositories \ | ||
| --jq '.repositories | map(.full_name)') |
| 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 |
| 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 |
| @@ -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 |
This comment has been minimized.
This comment has been minimized.
…fixes Agent-Logs-Url: https://github.com/primeinc/github-stars/sessions/0540959d-fabb-4050-ae01-83273f39a5ec Co-authored-by: primeinc <4395149+primeinc@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
|
@copilot you broke the app install lookup |
…}/installation Agent-Logs-Url: https://github.com/primeinc/github-stars/sessions/105b45c3-159a-4556-bd04-2fecfbfd3942 Co-authored-by: primeinc <4395149+primeinc@users.noreply.github.com>
Fixed in b1450ca. You're right — Reverted to
Defense-in-depth note: |
|
/gemini review |
|
@codex review |
There was a problem hiding this comment.
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.
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>
| - 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` |
There was a problem hiding this comment.
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.
| - 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 |
There was a problem hiding this comment.
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.
| - 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 }} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
…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>
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
mainandnextbranches, and splits out key gates into their own workflows for clarity and maintainability.The most important changes are:
Branch Protection Rulesets
.github-stars/control-plane/rulesets/protect-main-release-only.jsonand.github-stars/control-plane/rulesets/protect-next.jsonto define protection rules for themainandnextbranches, specifying required status checks, allowed merge methods, and other branch protection settings. [1] [2]Workflow Triggers and Concurrency Documentation
.github-stars/control-plane/workflow-triggers.mdwith 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
.github/workflows/00-ci.yml:bun gateandall gates pass.mainandnextbranches forpull_requestandpush..github/workflows/00a-do-not-merge-yet.ymlas a dedicated workflow to block merges when theDoNotMergeYetlabel is present, with proper concurrency and trigger configuration.Required Status Checks Alignment
Security and Best Practices
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.