From 14367c0422ae818ad54603a5a796613a8994def7 Mon Sep 17 00:00:00 2001 From: Taylor Blau Date: Tue, 4 Aug 2026 09:51:48 -0700 Subject: [PATCH] ci: enforce branch-specific Codex merge methods --- .github/CODEX.md | 47 ++++++++++++++++--------- .github/rulesets/codex-branch.json | 3 ++ .github/rulesets/codex-meta-linear.json | 20 +++++++++++ .github/rulesets/codex-meta.json | 3 ++ .github/workflows/codex-branch.sh | 3 ++ t/t9905-codex-branch.sh | 22 +++++++++--- 6 files changed, 76 insertions(+), 22 deletions(-) create mode 100644 .github/rulesets/codex-meta-linear.json diff --git a/.github/CODEX.md b/.github/CODEX.md index 87ac1f0ab2520a..9677634c6f1455 100644 --- a/.github/CODEX.md +++ b/.github/CODEX.md @@ -60,10 +60,12 @@ the retired topic's commits should be discarded or transferred to a child. A topic may be the head of a pull request whose base is `codex`. Use a same-repository `??/codex/*` head. The topic ruleset keeps these branches after -their pull requests are merged. Run a refresh before force-rewriting a topic -whose pull request was merged directly into `codex`: commits above the last -recorded output must still be reachable from an active topic. A clean merge or -fast-forward from a retained topic is accepted; squash commits, unrelated +their pull requests are merged. For ordinary pull-request merges, the `codex` +ruleset permits only the normal merge method, so each pull request contributes +a two-parent merge commit. Run a refresh before force-rewriting a topic whose +pull request was merged directly into `codex`: commits above the last recorded +output must still be reachable from an active topic. The controller accepts a +clean merge or fast-forward from a retained topic; squash commits, unrelated direct commits, octopus merges, and merge-only edits must first be extracted into an active topic. @@ -401,26 +403,37 @@ pusher. Integration subjects remain `Merge into codex`. ## Repository rulesets -Create or update the repository rulesets to match the three JSON files. Do not +Create or update the repository rulesets to match the four JSON files. Do not layer a duplicate over an existing matching ruleset: a bypass in the new -ruleset does not bypass another applicable ruleset. For a missing ruleset, use -**Settings > Rules > Rulesets > New ruleset > Import a ruleset**. +ruleset does not bypass another applicable ruleset. The separate linear-history +ruleset for `meta` is intentional. For a missing ruleset, use **Settings > +Rules > Rulesets > New ruleset > Import a ruleset**. -In `openai/git`, edit the existing **Protect generated Codex branch** ruleset -to add the exact-user publisher bypass, keep the existing topic ruleset aligned -with its recipe, and import only the missing `meta` ruleset. Verify that exactly -one active ruleset covers each of `codex`, `??/codex/*`, and `meta`. +In `openai/git`, update the existing **Protect generated Codex branch** and +**Protect Codex controller branch** rulesets to match their recipes, keep the +existing topic ruleset aligned with its recipe, and import only the missing +**Keep Codex controller history linear** ruleset. Verify that exactly one +policy ruleset covers each of `codex`, `??/codex/*`, and `meta`, plus the +separate linear-history ruleset on `meta`. - `.github/rulesets/codex-topics.json` matches `??/codex/*` and blocks deletion, preserving topic heads after pull-request merges. - `.github/rulesets/codex-branch.json` protects `codex` with pull-request, - review, deletion, and force-push rules. Its exact `ttaylorr-oai` user bypass - permits local publication; the organization-admin entry remains for - break-glass access. + review, deletion, and force-push rules, and permits only normal merge commits + for pull requests. Its exact `ttaylorr-oai` user bypass permits local + publication; the organization-admin entry remains for break-glass access. - `.github/rulesets/codex-meta.json` protects the `meta` controller with - pull-request, review, deletion, and force-push rules. Its matching exact-user - bypass lets the same atomic push advance the generated state; the - organization-admin entry remains for break-glass access. + pull-request, review, deletion, and force-push rules, and permits only rebase + merges. Its matching exact-user bypass lets the same atomic push advance the + generated state; the organization-admin entry remains for break-glass + access. +- `.github/rulesets/codex-meta-linear.json` rejects merge commits on `meta`. + It has no bypass because generated state updates are already linear. + +The merge-method restrictions govern ordinary pull-request merges. A listed +bypass actor can override them. The separate, bypass-free linear-history rule +still prevents every actor from introducing a merge commit on `meta`; a bypass +actor could nevertheless choose a linear squash rather than a rebase. Rulesets cannot require a pull request head to match `??/codex/*`; reviewers must enforce that convention. Do not require topic heads to be up to date with diff --git a/.github/rulesets/codex-branch.json b/.github/rulesets/codex-branch.json index 8f86556380a231..e66ba010952104 100644 --- a/.github/rulesets/codex-branch.json +++ b/.github/rulesets/codex-branch.json @@ -21,6 +21,9 @@ { "type": "pull_request", "parameters": { + "allowed_merge_methods": [ + "merge" + ], "require_code_owner_review": false, "require_last_push_approval": true, "dismiss_stale_reviews_on_push": false, diff --git a/.github/rulesets/codex-meta-linear.json b/.github/rulesets/codex-meta-linear.json new file mode 100644 index 00000000000000..0b62ee90e30925 --- /dev/null +++ b/.github/rulesets/codex-meta-linear.json @@ -0,0 +1,20 @@ +{ + "name": "Keep Codex controller history linear", + "target": "branch", + "source_type": "Repository", + "enforcement": "active", + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "refs/heads/meta" + ] + } + }, + "rules": [ + { + "type": "required_linear_history" + } + ], + "bypass_actors": [] +} diff --git a/.github/rulesets/codex-meta.json b/.github/rulesets/codex-meta.json index 962269f0efa943..9c0aaf045fbf70 100644 --- a/.github/rulesets/codex-meta.json +++ b/.github/rulesets/codex-meta.json @@ -21,6 +21,9 @@ { "type": "pull_request", "parameters": { + "allowed_merge_methods": [ + "rebase" + ], "require_code_owner_review": false, "require_last_push_approval": true, "dismiss_stale_reviews_on_push": false, diff --git a/.github/workflows/codex-branch.sh b/.github/workflows/codex-branch.sh index 56e6843de6e1d9..7937596cb3739f 100755 --- a/.github/workflows/codex-branch.sh +++ b/.github/workflows/codex-branch.sh @@ -200,6 +200,7 @@ legacy_control_paths_unchanged () ( .github/CODEX.md \ .github/rulesets/codex-branch.json \ .github/rulesets/codex-meta.json \ + .github/rulesets/codex-meta-linear.json \ .github/rulesets/codex-topics.json \ .github/workflows/codex-topic.yml \ .github/workflows/codex.yml \ @@ -218,6 +219,7 @@ meta_control_paths_unchanged () ( .github/CODEX.md \ .github/rulesets/codex-branch.json \ .github/rulesets/codex-meta.json \ + .github/rulesets/codex-meta-linear.json \ .github/rulesets/codex-topics.json \ .github/workflows/codex-topic.yml \ .github/workflows/codex-branch.sh \ @@ -2376,6 +2378,7 @@ topic_control_paths_unchanged () ( .github/CODEX.md \ .github/rulesets/codex-branch.json \ .github/rulesets/codex-meta.json \ + .github/rulesets/codex-meta-linear.json \ .github/rulesets/codex-topics.json \ .github/workflows/codex-topic.yml \ .github/workflows/codex.yml \ diff --git a/t/t9905-codex-branch.sh b/t/t9905-codex-branch.sh index a2ff337e222141..0a99e1c165984e 100755 --- a/t/t9905-codex-branch.sh +++ b/t/t9905-codex-branch.sh @@ -365,10 +365,22 @@ test_expect_success 'refresh only prepares an immutable local-publish artifact' test 1 = "$(grep -c "301000140" "$rules")" && test_grep "\"actor_type\": \"User\"" "$rules" && ! grep -F "DeployKey" "$rules" || return 1 - done + done && + branch_rules="$codex_root/.github/rulesets/codex-branch.json" && + test 1 = "$(grep -c "\"merge\"" "$branch_rules")" && + test 0 = "$(grep -c "\"squash\"" "$branch_rules")" && + test 0 = "$(grep -c "\"rebase\"" "$branch_rules")" && + meta_rules="$codex_root/.github/rulesets/codex-meta.json" && + test 0 = "$(grep -c "\"merge\"" "$meta_rules")" && + test 0 = "$(grep -c "\"squash\"" "$meta_rules")" && + test 1 = "$(grep -c "\"rebase\"" "$meta_rules")" && + linear="$codex_root/.github/rulesets/codex-meta-linear.json" && + test_grep "\"type\": \"required_linear_history\"" "$linear" && + test_grep "\"bypass_actors\": \[\]" "$linear" && + ! grep -F "actor_type" "$linear" ' -test_expect_success 'topics cannot change the meta branch ruleset' ' +test_expect_success 'topics cannot change the meta linear-history ruleset' ' git init --bare control-path.git && test_create_repo control-path-source && ( @@ -382,9 +394,9 @@ test_expect_success 'topics cannot change the meta branch ruleset' ' git switch -c aa/codex/control-path && mkdir -p .github/rulesets && - write untrusted .github/rulesets/codex-meta.json && - git add .github/rulesets/codex-meta.json && - git commit -m "change meta branch ruleset" && + write untrusted .github/rulesets/codex-meta-linear.json && + git add .github/rulesets/codex-meta-linear.json && + git commit -m "change meta linear-history ruleset" && git switch master && write master master-file &&