Skip to content

Commit 1adb1ef

Browse files
committed
Add rebase CI
1 parent 7a5504a commit 1adb1ef

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

.github/scripts/rebase_local.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
3+
set -eux
4+
5+
upstream_repo=$1
6+
# of the form [<branch>:<rebase-target>], e.g.:
7+
# BA:master B1:master B2:B1 B3:B2
8+
spec=$2
9+
10+
{
11+
12+
git remote add upstream "${upstream_repo}" || true
13+
git fetch upstream
14+
15+
output_branches=()
16+
17+
# rebase each branch
18+
mkdir -p rebase
19+
for branch_spec in ${spec} ; do
20+
branch=$(echo "${branch_spec}" | awk -F ':' '{ print $1 }')
21+
(
22+
rebase_target=$(echo "${branch_spec}" | awk -F ':' '{ print $2 }')
23+
git checkout "${branch}"
24+
common_ancestor=$(git merge-base "${branch}" "origin/${rebase_target}")
25+
[ -e rebase/"${branch}" ] && exit 1
26+
mkdir -p rebase/"${branch}"
27+
cd rebase/"${branch}"
28+
echo "${common_ancestor}" > BASE_COMMIT
29+
git format-patch "${common_ancestor}".."${branch}"
30+
if compgen -G "*.patch" > /dev/null; then
31+
git reset --hard "upstream/master"
32+
git am --3way *.patch
33+
fi
34+
)
35+
output_branches+=( "${branch}" )
36+
done
37+
38+
# sync master with upstream
39+
git checkout master
40+
git reset --hard upstream/master
41+
output_branches+=( "master" )
42+
43+
} >&2
44+
45+
echo "${output_branches[*]}"
46+

.github/scripts/rebase_spec.sh

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/bin/bash
2+
3+
set -eu
4+
5+
upstream_repo=$1
6+
rebase_branch=$2
7+
8+
branch_list=( )
9+
declare -A branch_map
10+
11+
# @FUNCTION: die
12+
# @USAGE: [msg]
13+
# @DESCRIPTION:
14+
# Exits the shell script with status code 2
15+
# and prints the given message in red to STDERR, if any.
16+
die() {
17+
(>&2 red_message "$1")
18+
exit 2
19+
}
20+
21+
# @FUNCTION: red_message
22+
# @USAGE: <msg>
23+
# @DESCRIPTION:
24+
# Print a red message.
25+
red_message() {
26+
printf "\\033[0;31m%s\\033[0m\\n" "$1"
27+
}
28+
29+
# @FUNCTION: array_contains
30+
# @USAGE: <arr_ref> <val>
31+
# @DESCRIPTION:
32+
# Checks whether the array reference contains the given value.
33+
# @RETURN: 0 if value exists, 1 otherwise
34+
array_contains() {
35+
local -n arr=$1
36+
local val=$2
37+
shift 2
38+
if [[ " ${arr[*]} " =~ [[:space:]]${val}[[:space:]] ]]; then
39+
return 0
40+
else
41+
return 1
42+
fi
43+
}
44+
45+
max_backtrack=10
46+
47+
# @FUNCTION: backtrack
48+
# @USAGE: <map_ref> <start_key> <abort_value>
49+
# @DESCRIPTION:
50+
# Backtrack dependencies through an array list.
51+
# E.g. given an associated array with key value pairs of:
52+
# B1 -> M
53+
# B2 -> B1
54+
# B3 -> B2
55+
#
56+
# ...if we pass B3 as start_key and M as abort_value, then
57+
# we receive the flattened ordered list "B1 B2 B3"
58+
# @STDOUT: space separated list of backtracked values
59+
backtrack() {
60+
backtrack_ 0 "$1" "$2" "$3"
61+
}
62+
63+
# internal to track backtrack depth
64+
backtrack_() {
65+
local depth=$1
66+
if [[ $depth -gt $max_backtrack ]] ; then
67+
die "Dependency backtracking too deep... aborting!"
68+
fi
69+
shift 1
70+
71+
if [[ $1 != map ]] ; then
72+
local -n map=$1
73+
fi
74+
75+
local base=$2
76+
local abort_value=$3
77+
local value
78+
79+
if [ "${base}" = "${abort_value}" ] ; then
80+
return
81+
fi
82+
83+
value=${map[$base]}
84+
85+
if [ "${value}" = "${abort_value}" ] ; then
86+
if ! array_contains branch_list "${base}" ; then
87+
echo "${base}"
88+
fi
89+
else
90+
if array_contains branch_list "${base}" ; then
91+
backtrack_ $((depth++)) map "${map[$value]}" "${abort_value}"
92+
else
93+
echo "$(backtrack_ $((depth++)) map "${map[$base]}" "${abort_value}")" "${base}"
94+
fi
95+
fi
96+
}
97+
98+
{
99+
100+
git remote add upstream "${upstream_repo}" || true
101+
git fetch upstream "${rebase_branch}"
102+
103+
# create branch rebase tree
104+
while IFS= read -r branch || [[ -n $branch ]]; do
105+
rebase_target=$(git branch --merged "${branch}" --sort="ahead-behind:${branch}" --format="%(refname:short)" | awk 'NR==2{print;exit}')
106+
107+
# check that rebase target is equal or later than local master
108+
if [ "${rebase_target}" != "master" ] ; then
109+
git for-each-ref --contains="master" --format="%(refname)" -- "refs/heads/${branch}" |
110+
grep --quiet "^refs/heads/${branch}$" ||
111+
die "rebase target ${rebase_target} seems to be older in the history than the master branch ...aborting!"
112+
fi
113+
114+
branch_map["${branch}"]="${rebase_target}"
115+
done < <(gh pr list --label rebase --state open --json headRefName --template '{{range .}}{{tablerow .headRefName}}{{end}}')
116+
117+
} >&2
118+
119+
# flatten recursively
120+
for key in "${!branch_map[@]}"; do
121+
value=${branch_map[$key]}
122+
if [ "${value}" = "${rebase_branch}" ] ; then
123+
if ! array_contains branch_list "${key}" ; then
124+
branch_list+=( "${key}" )
125+
fi
126+
else
127+
# shellcheck disable=SC2207
128+
branch_list+=( $(backtrack branch_map "$key" "${rebase_branch}") )
129+
fi
130+
done
131+
unset key
132+
133+
result=( )
134+
for key in "${branch_list[@]}"; do
135+
result+=( "${key}:${branch_map[$key]}" )
136+
done
137+
echo "${result[@]}"
138+

.github/workflows/rebase.yaml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Rebase against upstream
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * *'
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
issues: write
11+
pull-requests: read
12+
13+
jobs:
14+
rebase:
15+
name: Rebase now!
16+
runs-on: ubuntu-latest
17+
env:
18+
UPSTREAM_REPO: https://github.com/haskell/cabal.git
19+
BRANCH_PREFIX: sh-rebase
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v4
23+
with:
24+
fetch-depth: 0
25+
persist-credentials: false
26+
27+
- run: |
28+
set -eux
29+
30+
gh repo set-default stable-haskell/cabal
31+
git config checkout.defaultRemote origin
32+
33+
# required to apply patches
34+
git config user.email "[email protected]"
35+
git config user.name "GitHub CI"
36+
37+
# this does not push
38+
branch_spec=$(bash .github/scripts/rebase_spec.sh ${{ env.UPSTREAM_REPO }} master)
39+
rebased_branches=$(bash .github/scripts/rebase_spec.sh ${{ env.UPSTREAM_REPO }} "${branch_spec}")
40+
41+
# now push to temp branches
42+
for branch in ${branches} ; do
43+
git checkout "${branch}"
44+
remote_branch="${{ env.BRANCH_PREFIX }}/${branch}"
45+
git push https://${{ secrets.REBASE_PAT }}@github.com/${{ github.repository }}.git "${branch}:${remote_branch}"
46+
done
47+
48+
shell: bash
49+
env:
50+
GH_TOKEN: ${{ github.token }}
51+
52+
53+
# create an issue with a link to the workflow run on failure
54+
- if: failure()
55+
run: |
56+
set -eux
57+
gh repo set-default stable-haskell/cabal
58+
for issue in $(gh issue list --label rebase-failure --json url -q '.[] | .url') ; do
59+
gh issue close "${issue}"
60+
done
61+
gh issue create --title "Rebase failed on $(date -u +"%Y-%m-%d")" --label rebase-failure --body "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
62+
env:
63+
GH_TOKEN: ${{ github.token }}
64+
65+
# we always want the original repo and the patches that were applied
66+
# for debugging and backup reasons
67+
- if: always()
68+
run: |
69+
git checkout -f master || true
70+
git archive master > backup.tar
71+
tar -rf backup.tar .git rebase
72+
- if: always()
73+
name: Upload backup
74+
uses: actions/upload-artifact@v4
75+
with:
76+
if-no-files-found: error
77+
retention-days: 7
78+
name: backup
79+
path: |
80+
./backup.tar

0 commit comments

Comments
 (0)