diff --git a/.github/scripts/releasing/check-merge-back-commits.sh b/.github/scripts/releasing/check-merge-back-commits.sh
new file mode 100755
index 0000000000..3df9f6f3e7
--- /dev/null
+++ b/.github/scripts/releasing/check-merge-back-commits.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+
+# Exit the script as soon as an error occurs.
+set -euo pipefail
+
+# This script fails if
merges a release back into ,
+# but also adds commits that aren't on a release or staging branch, see RELEASING.md.
+# Merge commits are allowed.
+# Usage: .github/scripts/releasing/check-merge-back-commits.sh
+
+if [ "$#" -ne 2 ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+BASE=$1
+HEAD=$2
+
+SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
+# shellcheck source=.github/scripts/releasing/common.sh
+source "${SCRIPT_DIR}/common.sh"
+load_release_branches
+
+# A PR is a merge-back if some of its commits are on a release or staging branch.
+PR_COUNT=$(git rev-list --no-merges --count "${BASE}..${HEAD}")
+NEW_COUNT=$(git rev-list --no-merges --count "${BASE}..${HEAD}" --not "${BRANCHES[@]}")
+if ((NEW_COUNT == PR_COUNT)); then
+ echo "This PR doesn't merge a release back."
+ exit 0
+fi
+if ((NEW_COUNT == 0)); then
+ echo "This merge-back adds no commits of its own."
+ exit 0
+fi
+
+echo "This PR merges a release back, but also adds commits that aren't on a release or staging branch:"
+git log --no-merges --format=' %h %s' "${BASE}..${HEAD}" --not "${BRANCHES[@]}"
+echo
+echo "Make these changes in a separate PR, see RELEASING.md."
+exit 1
diff --git a/.github/scripts/releasing/check-no-copied-release-commits.sh b/.github/scripts/releasing/check-no-copied-release-commits.sh
index abffea876b..c85f9dec2d 100755
--- a/.github/scripts/releasing/check-no-copied-release-commits.sh
+++ b/.github/scripts/releasing/check-no-copied-release-commits.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
@@ -21,11 +21,10 @@ patch_ids() {
git log --no-merges --patch --no-color --no-ext-diff "$@" | git patch-id --stable | sort
}
-mapfile -t BRANCHES < <(git for-each-ref --format='%(refname)' 'refs/remotes/*/release/*' 'refs/remotes/*/staging/*')
-if [ "${#BRANCHES[@]}" -eq 0 ]; then
- echo "Error: No release or staging branches found."
- exit 1
-fi
+SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
+# shellcheck source=.github/scripts/releasing/common.sh
+source "${SCRIPT_DIR}/common.sh"
+load_release_branches
# Each line is " ".
RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")
diff --git a/.github/scripts/releasing/check-no-merge-back.sh b/.github/scripts/releasing/check-no-merge-back.sh
new file mode 100755
index 0000000000..51cff7de16
--- /dev/null
+++ b/.github/scripts/releasing/check-no-merge-back.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+# Exit the script as soon as an error occurs.
+set -euo pipefail
+
+# This script fails if contains release or staging commits that does not,
+# i.e. if merges a release back into .
+# Used in the merge queue, which squashes PRs and would drop the merge commit, see RELEASING.md.
+# Usage: .github/scripts/releasing/check-no-merge-back.sh
+
+if [ "$#" -ne 2 ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+BASE=$1
+HEAD=$2
+
+SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
+# shellcheck source=.github/scripts/releasing/common.sh
+source "${SCRIPT_DIR}/common.sh"
+load_release_branches
+
+RELEASE_COMMITS=$(git rev-list "${BRANCHES[@]}" --not "${BASE}")
+HEAD_COMMITS=$(git rev-list "${BASE}..${HEAD}")
+# The release commits in , newest first.
+MERGED=$(grep -xF -f <(echo "${RELEASE_COMMITS}") <<<"${HEAD_COMMITS}" || true)
+if [ -z "${MERGED}" ]; then
+ echo "No release commits are merged back."
+ exit 0
+fi
+
+echo "This PR merges $(wc -l <<<"${MERGED}" | tr -d ' ') release commits back, e.g.:"
+head -5 <<<"${MERGED}" | xargs git log --no-walk --format=' %h %s'
+echo
+echo "Merge-backs must not go through the merge queue, which squashes them."
+echo "Fast-forward develop to the PR branch instead, see RELEASING.md."
+exit 1
diff --git a/.github/scripts/releasing/check-releases-merged.sh b/.github/scripts/releasing/check-releases-merged.sh
index 75751a8e3b..950634ea8f 100755
--- a/.github/scripts/releasing/check-releases-merged.sh
+++ b/.github/scripts/releasing/check-releases-merged.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# Exit the script as soon as an error occurs.
set -euo pipefail
diff --git a/.github/scripts/releasing/common.sh b/.github/scripts/releasing/common.sh
new file mode 100644
index 0000000000..308bb93d72
--- /dev/null
+++ b/.github/scripts/releasing/common.sh
@@ -0,0 +1,12 @@
+# shellcheck shell=bash
+
+# Helpers shared by the release checks in this directory, see RELEASING.md.
+
+# Sets BRANCHES to all release and staging branches, and fails if there are none.
+load_release_branches() {
+ mapfile -t BRANCHES < <(git for-each-ref --format='%(refname)' 'refs/remotes/*/release/*' 'refs/remotes/*/staging/*')
+ if [ "${#BRANCHES[@]}" -eq 0 ]; then
+ echo "Error: No release or staging branches found."
+ exit 1
+ fi
+}
diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml
index 7457ca32cd..69051d5977 100644
--- a/.github/workflows/on-pr.yml
+++ b/.github/workflows/on-pr.yml
@@ -166,10 +166,31 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Check for copied release commits
+ if: ${{ github.event_name == 'pull_request' }}
env:
- BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
- HEAD: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
+ BASE: ${{ github.event.pull_request.base.sha }}
+ HEAD: ${{ github.event.pull_request.head.sha }}
run: .github/scripts/releasing/check-no-copied-release-commits.sh "${BASE}" "${HEAD}"
+ # Runs even if the previous check fails, so that both problems are reported at once.
+ - name: Check merge-back has no new commits
+ if: ${{ !cancelled() && github.event_name == 'pull_request' }}
+ env:
+ BASE: ${{ github.event.pull_request.base.sha }}
+ HEAD: ${{ github.event.pull_request.head.sha }}
+ run: .github/scripts/releasing/check-merge-back-commits.sh "${BASE}" "${HEAD}"
+ # The queue squashes PRs, so check the PR's own branch, named in the queue branch.
+ - name: Check the merge queue doesn't merge a release back
+ if: ${{ github.event_name == 'merge_group' }}
+ env:
+ BASE: ${{ github.event.merge_group.base_sha }}
+ HEAD_REF: ${{ github.event.merge_group.head_ref }}
+ run: |
+ if ! [[ "${HEAD_REF}" =~ /pr-([0-9]+)-[0-9a-f]+$ ]]; then
+ echo "Error: Can't find the PR number in '${HEAD_REF}'."
+ exit 1
+ fi
+ git fetch --no-tags origin "refs/pull/${BASH_REMATCH[1]}/head"
+ .github/scripts/releasing/check-no-merge-back.sh "${BASE}" FETCH_HEAD
clang-tidy:
needs: should-run