From a5c76fde2d774e81d71649104f5d74bc7f9c84b0 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 1 Oct 2026 13:35:58 +0000 Subject: [PATCH] ci: Add guardrails for release backporting (#8449) --- .../check-no-copied-release-commits.sh | 47 +++++++++++++++ .../releasing/check-releases-merged.sh | 46 ++++++++++++++ .github/workflows/check-pr-description.yml | 1 - .github/workflows/check-pr-title.yml | 1 - .github/workflows/check-releases-merged.yml | 60 +++++++++++++++++++ .github/workflows/on-pr.yml | 22 +++++++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100755 .github/scripts/releasing/check-no-copied-release-commits.sh create mode 100755 .github/scripts/releasing/check-releases-merged.sh create mode 100644 .github/workflows/check-releases-merged.yml 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