From 59e8000a07cdbad042c88ba6b99a40bdcd77302f Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Mon, 27 Jul 2026 14:56:39 -0500 Subject: [PATCH 1/2] [Infra] Pin squash-only merges with PR-title messages in bootstrap. The bootstrap script now patches the repository merge policy before upserting rulesets: squash is the only allowed merge method, and squash commits take the pull request's title ("title (#N)") and description, for direct merges and the merge queue alike. Without the explicit PATCH, GitHub's default commit-message settings leave queue-built squash commits titled "Merge pull request #N". Ported from the tex-core copy (nouprax/tex-core#20). markdown-core carries the bootstrap script only as the embedded copy in docs/repository-setup-template.md, so the block lands there. Co-Authored-By: Claude Fable 5 --- docs/repository-setup-template.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/repository-setup-template.md b/docs/repository-setup-template.md index 0a8c9d9..3cccc1d 100644 --- a/docs/repository-setup-template.md +++ b/docs/repository-setup-template.md @@ -896,6 +896,17 @@ if [ "$actual_default" != "$DEFAULT_BRANCH" ]; then exit 1 fi +# Linear history: squash is the only merge method, and every squash commit +# is titled by the pull request ("title (#N)" + description body). The +# merge queue builds its commits from these same settings. +gh api --method PATCH "repos/$GH_REPO" \ + -F allow_squash_merge=true \ + -F allow_merge_commit=false \ + -F allow_rebase_merge=false \ + -f squash_merge_commit_title=PR_TITLE \ + -f squash_merge_commit_message=PR_BODY >/dev/null +echo "merge policy: squash-only, PR-title squash messages" + upsert_ruleset() { local name=$1 local payload=$2 From 00abd2f7aeedca61a0fb707e55dd79725aa2874e Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Mon, 27 Jul 2026 16:21:40 -0500 Subject: [PATCH 2/2] [Infra] Carry the merge queue and admin bypass; describe defaults honestly. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live "main quality gates" ruleset carries a merge queue (SQUASH, ALLGREEN) and a repository-admin bypass that the embedded bootstrap recipe knew nothing about — a re-run would have stripped both. The recipe now reproduces them behind MERGE_QUEUE / MAIN_ADMIN_BYPASS variables defaulting to the live shape. The merge-policy wording no longer presents the squash title/message settings as enforcement: they are repository defaults a direct merge can edit, applied mechanically only by merge-queue commits. Co-Authored-By: Claude Fable 5 --- docs/repository-setup-template.md | 47 +++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/repository-setup-template.md b/docs/repository-setup-template.md index 3cccc1d..da8fdfb 100644 --- a/docs/repository-setup-template.md +++ b/docs/repository-setup-template.md @@ -857,6 +857,8 @@ TAG_RULESET_NAME=${TAG_RULESET_NAME:-release tag protection} TAG_PATTERN=${TAG_PATTERN:-v*.*.*} RULESET_ENFORCEMENT=${RULESET_ENFORCEMENT:-evaluate} PREVENT_SELF_REVIEW=${PREVENT_SELF_REVIEW:-false} +MERGE_QUEUE=${MERGE_QUEUE:-true} +MAIN_ADMIN_BYPASS=${MAIN_ADMIN_BYPASS:-true} case "$RULESET_ENFORCEMENT" in disabled|evaluate|active) ;; @@ -868,6 +870,16 @@ case "$PREVENT_SELF_REVIEW" in *) echo "PREVENT_SELF_REVIEW must be true or false" >&2; exit 2 ;; esac +for flag in MERGE_QUEUE MAIN_ADMIN_BYPASS; do + case "${!flag}" in + true|false) ;; + *) + echo "$flag must be true or false" >&2 + exit 2 + ;; + esac +done + tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT @@ -896,16 +908,17 @@ if [ "$actual_default" != "$DEFAULT_BRANCH" ]; then exit 1 fi -# Linear history: squash is the only merge method, and every squash commit -# is titled by the pull request ("title (#N)" + description body). The -# merge queue builds its commits from these same settings. +# Linear history: squash is the only merge method. The title/message +# settings are repository defaults — a direct merge can still edit them +# before confirming — while merge-queue commits apply them mechanically +# ("title (#N)" + description body). gh api --method PATCH "repos/$GH_REPO" \ -F allow_squash_merge=true \ -F allow_merge_commit=false \ -F allow_rebase_merge=false \ -f squash_merge_commit_title=PR_TITLE \ -f squash_merge_commit_message=PR_BODY >/dev/null -echo "merge policy: squash-only, PR-title squash messages" +echo "merge policy: squash-only; squash messages default to the PR title" upsert_ruleset() { local name=$1 @@ -976,14 +989,20 @@ if [ "$policy_count" -ne 1 ]; then exit 1 fi +# The merge queue keeps every PR's required checks green against the +# latest default branch and squashes with the repository's message +# defaults. The admin bypass mirrors the operator's standing exception; +# disable either through its variable for the hardened shape. jq -n \ --arg name "$MAIN_RULESET_NAME" \ - --arg enforcement "$RULESET_ENFORCEMENT" '{ + --arg enforcement "$RULESET_ENFORCEMENT" \ + --argjson queue "$MERGE_QUEUE" \ + --argjson admin_bypass "$MAIN_ADMIN_BYPASS" '{ name: $name, target: "branch", enforcement: $enforcement, conditions: {ref_name: {exclude: [], include: ["~DEFAULT_BRANCH"]}}, - rules: [ + rules: ([ {type: "deletion"}, {type: "non_fast_forward"}, {type: "pull_request", parameters: { @@ -1001,8 +1020,20 @@ jq -n \ {context: "CodeQL gate"} ] }} - ], - bypass_actors: [] + ] + (if $queue then [ + {type: "merge_queue", parameters: { + merge_method: "SQUASH", + grouping_strategy: "ALLGREEN", + max_entries_to_build: 5, + min_entries_to_merge: 1, + max_entries_to_merge: 5, + min_entries_to_merge_wait_minutes: 5, + check_response_timeout_minutes: 60 + }} + ] else [] end)), + bypass_actors: (if $admin_bypass then [ + {actor_id: 5, actor_type: "RepositoryRole", bypass_mode: "always"} + ] else [] end) }' >"$tmp/main-ruleset.json" upsert_ruleset "$MAIN_RULESET_NAME" "$tmp/main-ruleset.json"