Skip to content

Commit 3809e76

Browse files
committed
Add rebase CI
1 parent 4beb9d8 commit 3809e76

File tree

4 files changed

+304
-0
lines changed

4 files changed

+304
-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+
# sync master with upstream
18+
git checkout master
19+
git reset --hard upstream/master
20+
output_branches+=( "master" )
21+
22+
# rebase each branch
23+
mkdir -p rebase
24+
for branch_spec in ${spec} ; do
25+
branch=$(echo "${branch_spec}" | awk -F ':' '{ print $1 }')
26+
(
27+
rebase_target=$(echo "${branch_spec}" | awk -F ':' '{ print $2 }')
28+
git checkout "${branch}"
29+
common_ancestor=$(git merge-base "${branch}" "origin/${rebase_target}")
30+
[ -e rebase/"${branch}" ] && exit 1
31+
mkdir -p rebase/"${branch}"
32+
cd rebase/"${branch}"
33+
echo "${common_ancestor}" > BASE_COMMIT
34+
git format-patch "${common_ancestor}".."${branch}"
35+
if compgen -G "*.patch" > /dev/null; then
36+
git reset --hard "${rebase_target}"
37+
git am --3way *.patch
38+
fi
39+
)
40+
output_branches+=( "${branch}" )
41+
done
42+
43+
} >&2
44+
45+
echo "${output_branches[*]}"
46+

.github/scripts/rebase_spec.sh

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

.github/workflows/rebase.yaml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
outputs:
21+
rebase_output_json: ${{ steps.rebase.outputs.branches_json }}
22+
rebase_output: ${{ steps.rebase.outputs.branches }}
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
persist-credentials: false
29+
30+
- id: rebase
31+
name: rebase
32+
run: |
33+
set -eux
34+
35+
gh repo set-default stable-haskell/cabal
36+
git config checkout.defaultRemote origin
37+
38+
# required to apply patches
39+
git config user.email "[email protected]"
40+
git config user.name "GitHub CI"
41+
42+
# this does not push
43+
branch_spec=$(bash .github/scripts/rebase_spec.sh ${{ env.UPSTREAM_REPO }} master)
44+
rebased_branches=( $(bash .github/scripts/rebase_local.sh ${{ env.UPSTREAM_REPO }} "${branch_spec}") )
45+
46+
echo "branches_json=$(jq --compact-output --null-input '$ARGS.positional' --args -- "${rebased_branches[@]}")" >> "$GITHUB_OUTPUT"
47+
echo "branches=${rebased_branches[*]}" >> "$GITHUB_OUTPUT"
48+
shell: bash
49+
env:
50+
GH_TOKEN: ${{ github.token }}
51+
52+
- if: always()
53+
name: backup
54+
run: |
55+
git checkout -f master || true
56+
git archive master > backup.tar
57+
tar -rf backup.tar .git rebase
58+
59+
- if: always()
60+
name: Upload artifact
61+
uses: actions/upload-artifact@v4
62+
with:
63+
if-no-files-found: error
64+
retention-days: 7
65+
name: backup
66+
path: |
67+
./backup.tar
68+
69+
release-workflow:
70+
needs: ["rebase"]
71+
uses: ./.github/workflows/reusable-release.yml
72+
with:
73+
branches: ${{ needs.rebase.outputs.rebase_output_json }}
74+
ghc: "9.6.7"
75+
cabal: "3.12.1.0"
76+
77+
push-job:
78+
runs-on: ubuntu-latest
79+
needs: [rebase, release-workflow]
80+
steps:
81+
- run: |
82+
set -eux
83+
84+
# now push to temp branches
85+
for branch in ${{ needs.rebase.outputs.rebase_output_json }} ; do
86+
git checkout "${branch}"
87+
remote_branch="${{ env.BRANCH_PREFIX }}/${branch}"
88+
echo "git push https://${{ secrets.REBASE_PAT }}@github.com/${{ github.repository }}.git ${branch}:${remote_branch}"
89+
done
90+
91+
shell: bash
92+
env:
93+
GH_TOKEN: ${{ github.token }}
94+
95+
notify-job:
96+
runs-on: ubuntu-latest
97+
needs: [release-workflow]
98+
if: ${{ always() && contains(needs.*.result, 'failure') }}
99+
steps:
100+
- name: Checkout code
101+
uses: actions/checkout@v4
102+
103+
# create an issue with a link to the workflow run on failure
104+
- run: |
105+
set -eux
106+
gh repo set-default stable-haskell/cabal
107+
for issue in $(gh issue list --label rebase-failure --json url -q '.[] | .url') ; do
108+
gh issue close "${issue}"
109+
done
110+
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 }}"
111+
env:
112+
GH_TOKEN: ${{ github.token }}
113+

.github/workflows/release.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- stable-haskell/master
7+
- sh-rebase/**
78
tags:
89
- 'v*'
910
- 'cabal-install-*'

0 commit comments

Comments
 (0)