GtiHub Actionsでfork元の更新を定期的にfetchする

forkしてちょっとだけ変更を加えてそのまま放置…
その間にも、fork元はコミットが進んでいるかも。
fork元の先進的な変更点は取り入れたいけれど、自分が変更したところは上書きされたくない!

そんなときはGitHub Actionsを使ってみようという話。

この記事は、SLP KBIT AdventCalendar2022 5日目の記事です。

adventar.org

おおまかな手順

  1. clone
  2. git remote add upstream https://github.com/upstream/repository.git
  3. git fetch upstream main
  4. git rebase -X ours upstream/main main
  5. git push -f origin main

とすると、fork元の変更を取り入れた後に、自分の変更が加えられる。

これを毎日実行すると、fork元で更新があれば取り入れ、自分が変更したコミットはその上に積まれる。

注意点:

  • 自分で変更を加えたファイルは、fork元で更新があってもそれを検知できない(変更差分の判定から除外するため)
    • 変更したファイルが多いと不適
  • actions/checkout の with: fetch-depth: 0 を設定しておかなければrebaseに失敗する(デフォルトではコミット履歴は1つしか取得しないため)
  • workflowファイルが更新された場合、tokenが secrets.GITHUB_TOKEN だと失敗する(権限がないため)

更新の有無を確認する

自分が変更を加えたファイルを除き、変更があるか否かを確認する。

自分が変更したファイルはここにパスを追記して更新の確認から除外しておかなければならない。

git diff main upstream/main --name-only  -- ':!own/modified/file'

force pushする

rebase したことにより、コミット履歴が書き換わっている。
よってpush時はforceオプションを使用する。

--force-with-leaseを使ってもいいが、fetchしているため使用するメリットはない。

更新がなかった場合のearly exit

更新がなかった場合はさっさと処理を終了したい。
しかしexit 0としても次のstepは実行されてしまう。
かといってexit 1などと異常終了するとactionが失敗したとみなされ、失敗メールがうっとおしい。

そのため今回は変数に更新成功か失敗かを記し、以降のstepではその変数をifで確認して実行させるように対処している。

Example

README.md に変更を加えて、他のファイルはfork元の変更を取り入れる場合。
毎日03:12にチェックを行う。

update用のactionを .github/workflows/update.yaml に作成:

on:
  schedule:
    - cron: '12 3 * * *'

env:
  UPSTREAM_URL: "https://github.com/upstream/repository.git"

jobs:
  update:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
          token: ${{ secrets.PAT }}

      - name: update
        id: update
        run: |
          git config --local user.name "${GITHUB_ACTOR}"
          # メールアドレスをこれにするとbotとしてコミットされる
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git remote add upstream "${{ env.UPSTREAM_URL }}"
          git fetch --quiet upstream main
          # 自分が変更したファイルは更新の確認から除外する
          if [ -n "$(git diff main upstream/main --name-only -- ':!README.md' ':!.github/workflows/update.yaml')" ]; then
            echo "Update to follow the upstream repository."
            git rebase -X ours upstream/main main
            git remote set-url origin "https://${{ secrets.PAT }}@github.com/${GITHUB_REPOSITORY}.git"
            git push --force-with-lease origin ${GITHUB_REF#refs/heads/}
            echo "::set-output name=UPDATE_STAT::ok"
          else
            echo "No update found."
            echo "::set-output name=UPDATE_STAT::fail"
            exit 0
          fi

      - name: update success report
        if: ( steps.update.outputs.UPDATE_STAT == 'ok' )
        run: echo successflly updated!

      - name: no update report
        if: ( steps.update.outputs.UPDATE_STAT == 'fail' )
        run: echo not updated.