This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 069cd208c6 ci: give GitHub Actions bypass on the release-branch
ruleset (#8379)
069cd208c6 is described below
commit 069cd208c68d3f3a8e50d583a0a0ff7e00a4e536
Author: Meng Wang <[email protected]>
AuthorDate: Fri Sep 11 14:11:59 2026 +0000
ci: give GitHub Actions bypass on the release-branch ruleset (#8379)
### What changes were proposed in this PR?
The Merge Queue ruleset requires every change into `release/*` to arrive
as a PR with one approving review, green required checks, and a pass
through the merge queue. Right for people — but it also blocks
`direct-backport-push.yml`, whose fast path pushes clean cherry-picks;
every such push has been rejected since 2026-07-24, and five backports
were silently lost (#8377).
This splits the ruleset in two, rule-for-rule identical: `Merge Queue`
keeps `~DEFAULT_BRANCH`, and a new `Merge Queue (release)` carries the
three release branches plus a `bypass_actors` entry for the GitHub
Actions app (`actor_id: 15368`). The split exists because a bypass is
ruleset-wide — kept in one ruleset, it would let workflows push `main`
too.
Scope, stated precisely: the bypass exempts actions performed as the
Actions app — any workflow's `GITHUB_TOKEN`, not just the backport
workflow, since rulesets cannot scope a bypass to one workflow. People
and PATs still face every rule on every branch; `main` gets no bypass;
force pushes and branch deletion stay blocked for everyone, Actions
included, by `Default Branch Protection`.
Ordering inside the file is load-bearing: asfyaml applies rulesets in
file order, so `Merge Queue (release)` is created before `Merge Queue`
stops covering the release branches. If GitHub rejects the new ruleset,
the apply aborts with today's protections fully intact — no failure path
leaves the release branches uncovered.
The bypass alone would not revive the fast path: since #4676 the push
job checked out with `AUTO_MERGE_TOKEN`, so GitHub evaluated its pushes
as that PAT's owner — every pre-ruleset direct push shows a person as
the pusher — and an Actions-app bypass would not cover them. The push
job now uses the default `GITHUB_TOKEN`, which the bypass does cover,
and dispatches `Required Checks` on the pushed branch explicitly, since
a `GITHUB_TOKEN` push starts no push-triggered runs while
`workflow_dispatch` is the documented exception that always creates one.
The conflict path keeps the PAT: it pushes unprotected `backport/*`
branches, where the opened PR's CI must still trigger.
### Any related issues, documentation, discussions?
Closes #8377. #8378 took the PR-plus-auto-merge route to the same
problem and is closed in favor of trying the bypass first. What lands on
a release branch through this path is still only a cherry-pick of a
commit that passed main's full CI and, once #8096 lands, its release
manager's approving review.
### How was this PR tested?
`.asf.yaml` and the workflows parse, and the structural check is now
committed instead of run once: `.github/scripts/test_asf_rulesets.sh`
(picked up by build.yml's glob-discovered infra tests) asserts the two
rulesets' `rules` blocks stay deep-equal and that `.asf.yaml` and every
workflow parse under a duplicate-key-strict loader, with PyYAML pinned
in `amber/dev-requirements.txt` — the file the infra job installs; every
failure path (duplicate key, rules drift, bypass on main, bypass
tampered, ruleset reorder, missing PyYAML) was verified red before
trusting the green. asfyaml treats a ruleset carrying
`target`/`rules`/`bypass_actors` as a raw payload and forwards it
verbatim (`_RAW_RULESET_KEYS` in `feature/github/rulesets.py`; its
upstream tests assert the POST payload carries `bypass_actors`).
What cannot be proven before merge is GitHub accepting the Actions app
as a bypass actor on this org: the same payload on a personal repository
is rejected with "Actor GitHub Actions integration must be part of the
ruleset source or owner organization", and no ASF repository uses an
Integration bypass actor yet — hence the fail-safe ordering above. After
Infra applies the merged file, `GET /repos/apache/texera/rulesets`
should list `Merge Queue (release)`; if it does not, the apply failed
closed and nothing changed. The next clean backport is the end-to-end
test.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (claude-fable-5)
---
.asf.yaml | 60 +++++++++++++-
.github/scripts/test_asf_rulesets.sh | 122 +++++++++++++++++++++++++++++
.github/workflows/direct-backport-push.yml | 36 +++++++--
.github/workflows/required-checks.yml | 2 +
amber/dev-requirements.txt | 1 +
5 files changed, 214 insertions(+), 7 deletions(-)
diff --git a/.asf.yaml b/.asf.yaml
index 42316db44a..7e4f86a375 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -69,20 +69,76 @@ github:
rebase: false
rulesets:
- - name: Merge Queue
+ # Rule-for-rule identical to "Merge Queue" below; split out so the bypass
+ # here stays off main. The bypass exempts actions performed as the GitHub
+ # Actions app — i.e. any workflow's GITHUB_TOKEN, which is what
+ # direct-backport-push.yml's fast path pushes with (#8377). It cannot be
+ # scoped to a single workflow. People and PATs still face every rule.
+ #
+ # Listed BEFORE "Merge Queue" deliberately: asfyaml applies rulesets in
+ # file order, so this one is created before that one stops covering the
+ # release branches. If GitHub rejects this ruleset, the apply aborts with
+ # the old protections fully intact; the failure order never leaves the
+ # release branches uncovered.
+ - name: "Merge Queue (release)"
target: branch
enforcement: active
conditions:
ref_name:
exclude: []
include:
- - "~DEFAULT_BRANCH"
# Merge queue rules do NOT support wildcard ref patterns, so
# release branches must be listed explicitly (not release/*).
# Add each release line here as it is cut.
- "refs/heads/release/v1.1"
- "refs/heads/release/v1.2"
- "refs/heads/release/v1.3"
+ bypass_actors:
+ # The GitHub Actions app.
+ - actor_id: 15368
+ actor_type: Integration
+ bypass_mode: always
+ rules:
+ - type: deletion
+ - type: non_fast_forward
+ - type: merge_queue
+ parameters:
+ merge_method: SQUASH
+ max_entries_to_build: 2
+ min_entries_to_merge: 2
+ max_entries_to_merge: 5
+ min_entries_to_merge_wait_minutes: 3
+ grouping_strategy: HEADGREEN
+ check_response_timeout_minutes: 45
+ - type: pull_request
+ parameters:
+ allowed_merge_methods:
+ - squash
+ dismiss_stale_reviews_on_push: false
+ require_code_owner_review: false
+ require_last_push_approval: false
+ required_approving_review_count: 1
+ required_review_thread_resolution: true
+ - type: required_linear_history
+ - type: required_status_checks
+ parameters:
+ strict_required_status_checks_policy: false
+ required_status_checks:
+ - context: Required Checks
+ - context: Check License Headers
+ - context: Validate PR title
+
+ - name: Merge Queue
+ target: branch
+ enforcement: active
+ conditions:
+ ref_name:
+ exclude: []
+ include:
+ # Release branches carry these same rules in "Merge Queue
+ # (release)" above — a separate ruleset because its Actions
+ # bypass must not extend to main.
+ - "~DEFAULT_BRANCH"
rules:
- type: deletion
- type: non_fast_forward
diff --git a/.github/scripts/test_asf_rulesets.sh
b/.github/scripts/test_asf_rulesets.sh
new file mode 100755
index 0000000000..66bd29c3dc
--- /dev/null
+++ b/.github/scripts/test_asf_rulesets.sh
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Invariants over the CI configuration that a plain YAML parse cannot see.
+#
+# 1. "Merge Queue" and "Merge Queue (release)" in .asf.yaml must carry
+# identical rules: they are one policy split across two rulesets only so
+# the release half can hold an Actions bypass that must not reach main.
+# Nothing else keeps the copies from drifting apart.
+# 2. .asf.yaml and every workflow must parse with a duplicate-key-strict
+# loader. PyYAML silently keeps the last duplicate, but GitHub's loader
+# rejects the file, so a duplicated trigger key passes local checks and
+# then stops the workflow from ever starting.
+
+set -uo pipefail
+
+command -v python3 >/dev/null || { echo "python3 is required to run these
tests" >&2; exit 1; }
+# Runners ship python3 but not necessarily PyYAML (see release_branches.py);
+# CI installs it via amber/dev-requirements.txt.
+python3 -c 'import yaml' 2>/dev/null || { echo "PyYAML is required (pip
install pyyaml)" >&2; exit 1; }
+
+cd "$(git rev-parse --show-toplevel)"
+
+python3 - <<'EOF'
+import glob
+import sys
+
+import yaml
+
+
+class StrictLoader(yaml.SafeLoader):
+ pass
+
+
+def no_duplicates(loader, node, deep=False):
+ seen = set()
+ for key_node, _ in node.value:
+ key = loader.construct_object(key_node, deep=deep)
+ if key in seen:
+ raise yaml.YAMLError(
+ f"duplicate key {key!r} at line {key_node.start_mark.line + 1}"
+ )
+ seen.add(key)
+ return yaml.SafeLoader.construct_mapping(loader, node, deep)
+
+
+StrictLoader.add_constructor(
+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, no_duplicates
+)
+
+failures = []
+
+files = sorted(glob.glob(".github/workflows/*.yml")) + [".asf.yaml"]
+for path in files:
+ with open(path) as fh:
+ try:
+ yaml.load(fh, StrictLoader)
+ except yaml.YAMLError as exc:
+ failures.append(f"{path}: {exc}")
+
+with open(".asf.yaml") as fh:
+ ruleset_list = [
+ r for r in yaml.safe_load(fh)["github"]["rulesets"] if isinstance(r,
dict)
+ ]
+rulesets = {r.get("name"): r for r in ruleset_list}
+main_rs = rulesets.get("Merge Queue")
+release_rs = rulesets.get("Merge Queue (release)")
+if main_rs is None or release_rs is None:
+ failures.append(
+ ".asf.yaml: expected rulesets named 'Merge Queue' and 'Merge Queue
(release)'"
+ )
+elif main_rs["rules"] != release_rs["rules"]:
+ failures.append(
+ ".asf.yaml: 'Merge Queue' and 'Merge Queue (release)' rules differ -- "
+ "these are one policy in two rulesets; change both or neither"
+ )
+if main_rs is not None and "bypass_actors" in main_rs:
+ failures.append(
+ ".asf.yaml: 'Merge Queue' must not carry bypass_actors -- "
+ "keeping the bypass off main is what the split exists for"
+ )
+ACTIONS_APP = [{"actor_id": 15368, "actor_type": "Integration", "bypass_mode":
"always"}]
+if release_rs is not None and release_rs.get("bypass_actors") != ACTIONS_APP:
+ failures.append(
+ ".asf.yaml: 'Merge Queue (release)' bypass_actors must be exactly the "
+ "GitHub Actions app -- widen this list and the test together,
deliberately"
+ )
+names = [r.get("name") for r in ruleset_list]
+if main_rs is not None and release_rs is not None and names.index(
+ "Merge Queue (release)"
+) > names.index("Merge Queue"):
+ failures.append(
+ ".asf.yaml: 'Merge Queue (release)' must be listed before 'Merge
Queue' -- "
+ "asfyaml applies rulesets in file order, and creating the release
ruleset "
+ "before shrinking the main one is what keeps a rejected run fail-safe"
+ )
+
+for failure in failures:
+ print(f"FAIL: {failure}")
+if failures:
+ sys.exit(1)
+print(
+ f"OK: {len(files)} files duplicate-key clean; "
+ "Merge Queue rules identical; bypass only on the release ruleset; "
+ "release ruleset listed first"
+)
+EOF
diff --git a/.github/workflows/direct-backport-push.yml
b/.github/workflows/direct-backport-push.yml
index b67fcc90fc..149c608556 100644
--- a/.github/workflows/direct-backport-push.yml
+++ b/.github/workflows/direct-backport-push.yml
@@ -342,6 +342,15 @@ jobs:
needs: discover
if: ${{ needs.discover.outputs.has_push == 'true' }}
runs-on: ubuntu-latest
+ permissions:
+ # Everything this job's steps call, and nothing more: push the
+ # cherry-pick and comment on the commit (contents), dispatch Required
+ # Checks (actions), set the per-target commit status (statuses),
+ # annotate the original PR (issues).
+ actions: write
+ contents: write
+ issues: write
+ statuses: write
name: "backport #${{ matrix.pr_number }} to ${{ matrix.target }}"
strategy:
fail-fast: false
@@ -356,11 +365,12 @@ jobs:
uses: actions/checkout@v7
with:
fetch-depth: 0
- # Use AUTO_MERGE_TOKEN (fine-grained PAT) so the push to the release
- # branch retriggers workflows on that branch. GITHUB_TOKEN-authored
- # pushes are excluded from triggering downstream workflows, which
- # silences post-merge CI on backport commits.
- token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }}
+ # Push with the default GITHUB_TOKEN: the release rulesets admit the
+ # GitHub Actions app as a bypass actor, while a PAT-authored push is
+ # evaluated as that person and rejected. A GITHUB_TOKEN push starts
+ # no downstream workflows, so the step after the cherry-pick
+ # dispatches Required Checks itself — workflow_dispatch runs are the
+ # documented exception that GITHUB_TOKEN may create.
- name: Cherry-pick merge commit onto target branch
id: cherry_pick
env:
@@ -571,6 +581,22 @@ jobs:
log "new_sha=${new_sha}"
echo "new_sha=${new_sha}" >> "$GITHUB_OUTPUT"
+ - name: Run Required Checks on the pushed release branch
+ if: success()
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TARGET_BRANCH: ${{ matrix.target }}
+ run: |
+ # The GITHUB_TOKEN push above starts no push-triggered workflows;
+ # dispatch the run the release branch would otherwise have gotten.
+ # Best-effort: the backport itself has landed, so a dispatch
+ # failure must not demote this job to failed -- the failure
+ # reporter below would then claim a landed backport was lost.
+ if ! gh workflow run required-checks.yml \
+ --repo "${GITHUB_REPOSITORY}" --ref "${TARGET_BRANCH}"; then
+ echo "::warning::Could not start Required Checks on
${TARGET_BRANCH}; start it manually from the Actions tab."
+ fi
+
- name: Annotate original PR and commit on success
if: success()
uses: actions/github-script@v9
diff --git a/.github/workflows/required-checks.yml
b/.github/workflows/required-checks.yml
index 1db8a52668..42b31d37fd 100644
--- a/.github/workflows/required-checks.yml
+++ b/.github/workflows/required-checks.yml
@@ -30,6 +30,8 @@ on:
- labeled
- unlabeled
merge_group:
+ # Also dispatched by direct-backport-push.yml after its GITHUB_TOKEN push
+ # to a release branch, which starts no push-triggered runs.
workflow_dispatch:
permissions:
diff --git a/amber/dev-requirements.txt b/amber/dev-requirements.txt
index 848104e776..593c70d130 100644
--- a/amber/dev-requirements.txt
+++ b/amber/dev-requirements.txt
@@ -42,4 +42,5 @@ textual==8.2.8
# Reads bin/k8s/values.yaml in bin/k8s/tests/test_helm_values.sh. That check
fails
# rather than skipping when this is missing, so the suite cannot go green by
accident.
+# Also read by .github/scripts/test_asf_rulesets.sh (infra job shell tests).
PyYAML==6.0.2