diff --git a/.github/scripts/push.sh b/.github/scripts/push.sh new file mode 100644 index 00000000000..45d092852d1 --- /dev/null +++ b/.github/scripts/push.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -eux + +upstream_repo=$1 +base_repo=$2 +output_branches=$3 + +git remote add upstream "${upstream_repo}" || true +git fetch upstream master + +# push each branch +while IFS= read -r branch || [[ -n $branch ]]; do + git checkout "${branch}" + echo "git push -f ${base_repo} ${branch}" + # git push -f "${base_repo}" "${branch}" +done < <(echo "${output_branches}") + diff --git a/.github/scripts/rebase.sh b/.github/scripts/rebase.sh new file mode 100644 index 00000000000..3f0b5ba695b --- /dev/null +++ b/.github/scripts/rebase.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -eux + +upstream_repo=$1 + +{ + +git remote add upstream "${upstream_repo}" || true +git fetch upstream + +output_branches=() + +# rebase each branch +mkdir -p rebase +while IFS= read -r branch || [[ -n $branch ]]; do + ( + git checkout "${branch}" + common_ancestor=$(git merge-base "${branch}" "origin/master") + [ -e rebase/"${branch}" ] && exit 1 + mkdir -p rebase/"${branch}" + cd rebase/"${branch}" + echo "${common_ancestor}" > BASE_COMMIT + git format-patch "${common_ancestor}".."${branch}" + if compgen -G "*.patch" > /dev/null; then + git reset --hard "upstream/master" + git am --3way *.patch + fi + ) + output_branches+=( "${branch}" ) +done < <(gh pr list --label rebase --state open --json headRefName --template '{{range .}}{{tablerow .headRefName}}{{end}}') + +# sync master with upstream +git checkout master +git reset --hard upstream/master +output_branches+=( "master" ) + +} >&2 + +( + IFS=$'\n' + echo "${output_branches[*]}" +) + diff --git a/.github/workflows/rebase.yaml b/.github/workflows/rebase.yaml new file mode 100644 index 00000000000..32fd4331a2c --- /dev/null +++ b/.github/workflows/rebase.yaml @@ -0,0 +1,74 @@ +name: Rebase against upstream + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: read + +jobs: + rebase: + name: Rebase now! + runs-on: ubuntu-latest + env: + UPSTREAM_REPO: https://github.com/haskell/cabal.git + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - run: | + set -eux + + gh repo set-default stable-haskell/cabal + git config checkout.defaultRemote origin + + # required to apply patches + git config user.email "ci@users.noreply.github.com" + git config user.name "GitHub CI" + + # this does not push + branches=$(bash .github/scripts/rebase.sh ${{ env.UPSTREAM_REPO }}) + + # now push + git checkout ${{ github.ref_name }} + bash .github/scripts/push.sh ${{ env.UPSTREAM_REPO }} https://${{ secrets.REBASE_PAT }}@github.com/${{ github.repository }}.git "${branches}" + shell: bash + env: + GH_TOKEN: ${{ github.token }} + + + # create an issue with a link to the workflow run on failure + - if: failure() + run: | + set -eux + gh repo set-default stable-haskell/cabal + for issue in $(gh issue list --label rebase-failure --json url -q '.[] | .url') ; do + gh issue close "${issue}" + done + 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 }}" + env: + GH_TOKEN: ${{ github.token }} + + # we always want the original repo and the patches that were applied + # for debugging and backup reasons + - if: always() + run: | + git checkout -f master || true + git archive master > backup.tar + tar -rf backup.tar .git rebase + - if: always() + name: Upload backup + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + retention-days: 7 + name: backup + path: | + ./backup.tar