diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a07342ee96cdc..ff4fa1527e93a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,6 +128,9 @@ jobs: - name: ensure line endings are correct run: src/ci/scripts/verify-line-endings.sh if: success() && !env.SKIP_JOB + - name: ensure backported commits are in upstream branches + run: src/ci/scripts/verify-backported-commits.sh + if: success() && !env.SKIP_JOB - name: run the build run: src/ci/scripts/run-build-from-ci.sh env: @@ -499,6 +502,9 @@ jobs: - name: ensure line endings are correct run: src/ci/scripts/verify-line-endings.sh if: success() && !env.SKIP_JOB + - name: ensure backported commits are in upstream branches + run: src/ci/scripts/verify-backported-commits.sh + if: success() && !env.SKIP_JOB - name: run the build run: src/ci/scripts/run-build-from-ci.sh env: @@ -609,6 +615,9 @@ jobs: - name: ensure line endings are correct run: src/ci/scripts/verify-line-endings.sh if: success() && !env.SKIP_JOB + - name: ensure backported commits are in upstream branches + run: src/ci/scripts/verify-backported-commits.sh + if: success() && !env.SKIP_JOB - name: run the build run: src/ci/scripts/run-build-from-ci.sh env: diff --git a/src/ci/github-actions/ci.yml b/src/ci/github-actions/ci.yml index 59a1d06dabd21..6417f5a984ad5 100644 --- a/src/ci/github-actions/ci.yml +++ b/src/ci/github-actions/ci.yml @@ -206,6 +206,10 @@ x--expand-yaml-anchors--remove: run: src/ci/scripts/verify-line-endings.sh <<: *step + - name: ensure backported commits are in upstream branches + run: src/ci/scripts/verify-backported-commits.sh + <<: *step + - name: run the build run: src/ci/scripts/run-build-from-ci.sh env: diff --git a/src/ci/scripts/verify-backported-commits.sh b/src/ci/scripts/verify-backported-commits.sh new file mode 100755 index 0000000000000..1023e4b0e2837 --- /dev/null +++ b/src/ci/scripts/verify-backported-commits.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# Ensure commits in beta are in master & commits in stable are in beta + master. +set -euo pipefail +IFS=$'\n\t' + +source "$(cd "$(dirname "$0")" && pwd)/../shared.sh" + +# We don't care about commits that predate this automation check, so we pass a +# `` argument to `git cherry`. +BETA_LIMIT="53fd98ca776cb875bc9e5514f56b52eb74f9e7a9" +STABLE_LIMIT="a178d0322ce20e33eac124758e837cbd80a6f633" + +verify_backported_commits_main() { + ci_base_branch=$(ciBaseBranch) + + if [[ "$ci_base_branch" != "beta" && "$ci_base_branch" != "stable" ]]; then + echo 'Skipping. This is only run when merging to the beta or stable branches.' + exit 0 + fi + + echo 'git: unshallowing the repository so we can check commits' + git fetch \ + --no-tags \ + --no-recurse-submodules \ + --progress \ + --prune \ + --unshallow + + if [[ $ci_base_branch == "beta" ]]; then + verify_cherries master "$BETA_LIMIT" \ + || exit 1 + + elif [[ $ci_base_branch == "stable" ]]; then + (verify_cherries master "$STABLE_LIMIT" \ + & verify_cherries beta "$STABLE_LIMIT") \ + || exit 1 + + fi +} + +# Verify all commits in `HEAD` are backports of a commit in . See +# https://git-scm.com/docs/git-cherry for an explanation of the arguments. +# +# $1 = +# $2 = +verify_cherries() { + # commits that lack a `backport-of` comment. + local no_backports=() + # commits with an incorrect `backport-of` comment. + local bad_backports=() + + commits=$(git cherry "origin/$1" HEAD "$2") + + if [[ -z "$commits" ]]; then + echo "All commits in \`HEAD\` are present in \`$1\`" + return 0 + fi + + commits=$(echo "$commits" | grep '^\+' | cut -c 3-) + + while read sha; do + # Check each commit in .. + backport_sha=$(get_backport "$sha") + + if [[ "$backport_sha" == "nothing" ]]; then + echo "✓ \`$sha\` backports nothing" + continue + fi + + if [[ -z "$backport_sha" ]]; then + no_backports+=("$sha") + continue + fi + + if ! is_in_master "$backport_sha"; then + bad_backports+=("$sha") + continue + fi + + echo "✓ \`$sha\` backports \`$backport_sha\`" + done <<< "$commits" + + failure=0 + + if [ ${#no_backports[@]} -ne 0 ]; then + echo 'Error: Could not find backports for all commits.' + echo + echo 'All commits in \`HEAD\` are required to have a corresponding upstream commit.' + echo 'It looks like the following commits:' + echo + for commit in "${no_backports[@]}"; do + echo " $commit" + done + echo + echo "do not match any commits in \`$1\`. If this was intended, add the text" + echo '\`backport-of: \`' + echo 'somewhere in the message of each of these commits.' + echo + failure=1 + fi + + if [ ${#bad_backports[@]} -ne 0 ]; then + echo 'Error: Found incorrectly marked commits.' + echo + echo 'The following commits:' + echo + for commit in "${bad_backports[@]}"; do + echo " $commit" + done + echo + echo 'have commit messages marked \`backport-of: \`, but the SHA is not in' + echo '\`master\`.' + echo + failure=1 + fi + + return $failure +} + +# Get the backport of a commit. It echoes one of: +# +# 1. A SHA of the backported commit +# 2. The string "nothing" +# 3. An empty string +# +# $1 = +get_backport() { + # This regex is: + # + # ^.* - throw away any extra starting characters + # backport-of: - prefix + # \s\? - optional space + # \(\) - capture group + # [a-f0-9]\+\|nothing - a SHA or the text 'nothing' + # .* - throw away any extra ending characters + # \1 - replace it with the first match + # {s//\1/p;q} - print the first occurrence and quit + # + git show -s --format=%B "$1" \ + | sed -n '/^.*backport-of:\s\?\([a-f0-9]\+\|nothing\).*/{s//\1/p;q}' +} + +# Check if a commit is in master. +# +# $1 = +is_in_master() { + git merge-base --is-ancestor "$1" origin/master 2> /dev/null +} + +verify_backported_commits_main diff --git a/src/tools/cherry-pick.sh b/src/tools/cherry-pick.sh new file mode 100755 index 0000000000000..90539a96389b0 --- /dev/null +++ b/src/tools/cherry-pick.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -euo pipefail +IFS=$'\n\t' + +print_error() { + echo "Error: \`$1\` is not a valid commit. To debug, run:" + echo + echo " git rev-parse --verify $1" + echo +} + +full_sha() { + git rev-parse \ + --verify \ + --quiet \ + "$1^{object}" || print_error $1 +} + +commit_message_with_backport_note() { + message=$(git log --format=%B -n 1 $1) + echo $message | awk "NR==1{print; print \"\n(backport-of: $1)\"} NR!=1" +} + +cherry_pick_commit() { + sha=$(full_sha $1) + git cherry-pick $sha > /dev/null + git commit \ + --amend \ + --file <(commit_message_with_backport_note $sha) +} + +for arg ; do + cherry_pick_commit $arg +done