diff --git a/.github/scripts/releasing/check-no-copied-release-commits.sh b/.github/scripts/releasing/check-no-copied-release-commits.sh
new file mode 100755
index 0000000000..abffea876b
--- /dev/null
+++ b/.github/scripts/releasing/check-no-copied-release-commits.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# Exit the script as soon as an error occurs.
+set -euo pipefail
+
+# This script fails if the commits in ..
copy commits from a release or staging branch
+# (e.g. by rebasing or cherry-picking them) instead of merging that branch, see RELEASING.md.
+# Commits are compared by patch-id,
+# and only against release and staging commits that does not already contain.
+# Usage: .github/scripts/releasing/check-no-copied-release-commits.sh
+
+if [ "$#" -ne 2 ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+BASE=$1
+HEAD=$2
+
+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
+
+# Each line is " ".
+RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")
+PR_PATCHES=$(patch_ids "${BASE}..${HEAD}")
+
+# Each line is " ".
+COPIES=$(join <(echo "${RELEASE_PATCHES}") <(echo "${PR_PATCHES}"))
+if [ -z "${COPIES}" ]; then
+ echo "No copied release commits found."
+ exit 0
+fi
+
+echo "These commits copy release commits instead of merging them:"
+while read -r _ RELEASE_COMMIT PR_COMMIT; do
+ echo " $(git log -1 --format='%h %s' "${PR_COMMIT}") (copies ${RELEASE_COMMIT:0:10})"
+done <<<"${COPIES}"
+echo
+echo "Merge the release tag (or 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
new file mode 100755
index 0000000000..75751a8e3b
--- /dev/null
+++ b/.github/scripts/releasing/check-releases-merged.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+# Exit the script as soon as an error occurs.
+set -euo pipefail
+
+# This script fails if a final release (a tag like 3.4.0) on a release branch
+# is not merged back into within a few days, see RELEASING.md.
+# Usage: .github/scripts/releasing/check-releases-merged.sh
+
+if [ "$#" -ne 1 ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+DEVELOP=$1
+GRACE_DAYS=3
+
+mapfile -t MERGED_ARGS < <(git for-each-ref --format='--merged=%(refname)' 'refs/remotes/*/release/*')
+if [ "${#MERGED_ARGS[@]}" -eq 0 ]; then
+ echo "Error: No release branches found."
+ exit 1
+fi
+
+# Tags on a release branch that does not contain.
+TAGS=$(git for-each-ref --format='%(refname:short) %(creatordate:unix)' \
+ "${MERGED_ARGS[@]}" --no-merged="${DEVELOP}" 'refs/tags/[0-9]*')
+
+MISSING=0
+while read -r TAG CREATED; do
+ if ! [[ "${TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ continue
+ fi
+ if (($(date +%s) - CREATED < GRACE_DAYS * 86400)); then
+ echo "${TAG}: not merged yet, still within the ${GRACE_DAYS}-day grace period."
+ else
+ echo "${TAG}: not merged into develop."
+ MISSING=1
+ fi
+done <<<"${TAGS}"
+
+if [ "${MISSING}" -ne 0 ]; then
+ echo
+ echo "Merge the missing releases back into develop, see RELEASING.md."
+ exit 1
+fi
+echo "No releases past the grace period are missing from develop."
diff --git a/.github/workflows/check-pr-description.yml b/.github/workflows/check-pr-description.yml
index f8e7b6cdc4..1ffc50c0ba 100644
--- a/.github/workflows/check-pr-description.yml
+++ b/.github/workflows/check-pr-description.yml
@@ -13,7 +13,6 @@ on:
- ready_for_review
branches:
- develop
- - "release-*"
- "release/*"
- "staging/*"
diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml
index cc80982440..d033f9df69 100644
--- a/.github/workflows/check-pr-title.yml
+++ b/.github/workflows/check-pr-title.yml
@@ -13,7 +13,6 @@ on:
- ready_for_review
branches:
- develop
- - "release-*"
- "release/*"
- "staging/*"
diff --git a/.github/workflows/check-releases-merged.yml b/.github/workflows/check-releases-merged.yml
new file mode 100644
index 0000000000..0b65d9761f
--- /dev/null
+++ b/.github/workflows/check-releases-merged.yml
@@ -0,0 +1,60 @@
+# This workflow checks that every final release on a release branch
+# has been merged back into develop, see RELEASING.md.
+name: Check releases merged
+
+on:
+ schedule:
+ # 06:47 UTC every Monday.
+ - cron: "47 6 * * 1"
+ push:
+ branches:
+ - "develop"
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ shell: bash
+
+env:
+ OUTPUT_FILE: /tmp/releases-merged.txt
+ ISSUE_FILE: /tmp/releases-merged-issue.md
+
+jobs:
+ releases-merged:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ # Needed to open an issue on scheduled failures.
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ # The check needs the full history of develop, the release branches and tags.
+ fetch-depth: 0
+
+ - name: Check releases are merged into develop
+ run: |
+ set -o pipefail
+ .github/scripts/releasing/check-releases-merged.sh origin/develop | tee "${OUTPUT_FILE}"
+
+ - name: Prepare issue body
+ if: ${{ failure() && github.event_name == 'schedule' && github.repository == 'XRPLF/rippled' }}
+ run: |
+ {
+ echo '```'
+ cat "${OUTPUT_FILE}"
+ echo '```'
+ } >"${ISSUE_FILE}"
+
+ - name: Create issue
+ if: ${{ failure() && github.event_name == 'schedule' && github.repository == 'XRPLF/rippled' }}
+ uses: XRPLF/actions/create-issue@2b8bc36af85b88bca0dd7bfac2e2dc05f94ad712
+ with:
+ title: "Releases not merged back into develop"
+ body_file: ${{ env.ISSUE_FILE }}
+ labels: "Bug"
diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml
index 62a995d864..7457ca32cd 100644
--- a/.github/workflows/on-pr.yml
+++ b/.github/workflows/on-pr.yml
@@ -150,6 +150,27 @@ jobs:
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-check-rename.yml
+ # Runs regardless of the changed files.
+ # PRs into staging branches are skipped, since fixes may be cherry-picked between release lines.
+ check-release-commits:
+ needs: should-run
+ if: ${{ github.event.pull_request.base.ref == 'develop' || github.event.merge_group.base_ref == 'refs/heads/develop' }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ # The check needs the full history of the release and staging branches.
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Check for copied release commits
+ 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 }}
+ run: .github/scripts/releasing/check-no-copied-release-commits.sh "${BASE}" "${HEAD}"
+
clang-tidy:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
@@ -230,6 +251,7 @@ jobs:
- check-autogen
- check-levelization
- check-rename
+ - check-release-commits
- clang-tidy
- build-test
- rust