ci: Add guardrails for release backporting (#8449)

This commit is contained in:
Ayaz Salikhov
2026-10-01 13:35:58 +00:00
committed by GitHub
parent eed6527946
commit a5c76fde2d
6 changed files with 175 additions and 2 deletions

View File

@@ -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 <base>..<head> 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 <head> does not already contain.
# Usage: .github/scripts/releasing/check-no-copied-release-commits.sh <base> <head>
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <base> <head>"
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 "<patch-id> <commit>".
RELEASE_PATCHES=$(patch_ids "${BRANCHES[@]}" --not "${HEAD}")
PR_PATCHES=$(patch_ids "${BASE}..${HEAD}")
# Each line is "<patch-id> <release commit> <PR commit>".
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

View File

@@ -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 <develop> within a few days, see RELEASING.md.
# Usage: .github/scripts/releasing/check-releases-merged.sh <develop>
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <develop>"
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 <develop> 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."

View File

@@ -13,7 +13,6 @@ on:
- ready_for_review
branches:
- develop
- "release-*"
- "release/*"
- "staging/*"

View File

@@ -13,7 +13,6 @@ on:
- ready_for_review
branches:
- develop
- "release-*"
- "release/*"
- "staging/*"

View File

@@ -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"

View File

@@ -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