7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZOZOAdvent Calendar 2024

Day 7

GitHub リポジトリ B からリポジトリ A に Pull Request を積んでいく GitHub Actions

Last updated at Posted at 2024-12-07

本記事は ZOZO Advent Calendar 2024 シリーズ 2 の 7 日目の記事です。

やりたいこと

以下のような状況を考えます。

  • 共通ライブラリが管理されている GitHub リポジトリ A がある
  • 共通ライブラリを利用する GitHub リポジトリ B, C がある
  • リポジトリ B, C ではリポジトリ A のコードを git subtree で取り込んでいる

ここではリポジトリ B で共通ライブラリ(リポジトリ A のコード)の変更を行った際に、その変更内容をリポジトリ A に対して Pull-Request を作成する GitHub Actions を作ります。

結論

まずは完成形を示します。リポジトリ B で作成した PR がマージされたタイミングで起動する Action の想定です。

name: Create PR to other repository

on:
  pull_request:
    branches:
      - "main"
    types: [closed]

env:
  # BOT として使用する GitHub Apps の App ID
  APP_ID: 1234567

jobs:
  # git subtree で取り込んでいる repository-a のファイル変更を検知する
  get-changed-files:
    if: ${{ github.event.pull_request.merged == true }}
    runs-on: ubuntu-22.04
    outputs:
      changed_files: ${{ steps.changed-files.outputs.all_changed_files }}

    steps:
    - name: Check out repository code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
        persist-credentials: false
        ref: refs/heads/${{ github.event.pull_request.base.ref }}

    - name: Get changed files
      id: changed-files
      uses: tj-actions/changed-files@v45
      with:
        json: true
        quotepath: false
        files: |
          repository-a/**

    - name: Show changed files
      run: |
        echo "${{ steps.changed-files.outputs.all_changed_files }}"

  # repository-a に PR を作成する
  create-pr-to-repository-A:
    if: ${{ !failure() && needs.get-changed-files.outputs.changed_files != '[]' && github.event.pull_request.merged == true }}
    needs: get-changed-files
    runs-on: ubuntu-22.04
    permissions:
      contents: write
      id-token: write
    env:
      LIBRARY_UPDATE_PR_BRANCH: feature/update-library-via-ci

    steps:
    - name: Check out to this repo
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
        path: repository-b

    - uses: actions/create-github-app-token@v1
      id: app-token
      with:
        app-id: ${{ env.APP_ID }}
        private-key: ${{ secrets.GITHUB_APPS_PRIVATE_KEY }}
        owner: ${{ github.repository_owner }}

    - name: Check out to repository-a
      uses: actions/checkout@v4
      with:
        token: ${{ steps.app-token.outputs.token }}
        repository: repository-a
        path: repository-a

    - name: Get GitHub App User ID
      id: get-user-id
      env:
        GH_TOKEN: ${{ steps.app-token.outputs.token }}
      run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"

    - name: Configure git environment and switch branch
      working-directory: repository-a
      run: |
        set -eux
        git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
        git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
        git fetch origin
        git switch -c $LIBRARY_UPDATE_PR_BRANCH origin/$LIBRARY_UPDATE_PR_BRANCH || git switch -c $LIBRARY_UPDATE_PR_BRANCH

    - name: Sync files to repository-a
      run: |
        rsync -r --delete repository-b/repository-a/ repository-a/ --exclude '.git' --exclude '.github/'

    - name: Show git diff
      working-directory: repository-a
      run: |
        set -eux
        git status
        git branch
        git add .
        git diff --cached --name-status

    - name: Create pr to repository-a
      env:
        TOKEN: ${{ steps.app-token.outputs.token }}
        REVIEWER: Reviewer-name-as-you-like
      working-directory: repository-a
      run: |
        set -eux
        git commit -m "Update repository-a from repository-b's PR ${{github.event.pull_request.html_url}}"
        git push origin HEAD
        echo ${TOKEN} > token.txt
        gh auth login --with-token < token.txt

        pr_list=$(gh pr list --head $LIBRARY_UPDATE_PR_BRANCH --json=number,body)
        if [[ $(echo "$pr_list" | jq '.[] | length') -eq 0 ]]; then
          gh pr create \
            --title "Update `repository-a` via repository-b repo" \
            --body "- [ ] ${{github.event.pull_request.html_url}}" \
            --reviewer ${REVIEWER}
        else
          echo $pr_list | jq -r '.[0].body' > pr_body.txt
          echo "- [ ] ${{github.event.pull_request.html_url}}" >> pr_body.txt
          gh pr edit --body "$(cat pr_body.txt)"
        fi

上記の Action についてポイントをいくつかピックアップして解説します。

subtree で取り込んでいるコードの変更を検知する

tj-actions/changed-files という Action を使ってファイルの変更を検出しています。

今回作成した Action では「repository-b のリポジトリルートで repository-a というディレクトリ名でリポジトリ A のコードを管理している」という状況を想定しているため、repository-a/** で変更されたファイルを選択しています。

$ tree .
.
├── README-b.md
├── repository-a
│   ├── README-a.md
│   └── src
│       └── file
└── src
    └── file

リポジトリ A に checkout して変更内容を sync させる

以下の手順でリポジトリ B での変更内容を反映したブランチを作成しています。

  1. repository-a というディレクトリ名でリポジトリ A に checkout
  2. リポジトリ A で特定のリモートブランチに switch(リモートブランチが存在しなければ新規に作成)
  3. rsync コマンドでリポジトリ B での変更内容をリポジトリ A のブランチに反映

上記の流れを踏まえつつ Action を読むと理解が捗ります。

結論に記載した Action から抜粋
    - name: Check out to repository-a
      uses: actions/checkout@v4
      with:
        token: ${{ steps.app-token.outputs.token }}
        repository: repository-a
        path: repository-a

    - name: Get GitHub App User ID
      id: get-user-id
      env:
        GH_TOKEN: ${{ steps.app-token.outputs.token }}
      run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"

    - name: Configure git environment and switch branch
      working-directory: repository-a
      run: |
        set -eux
        git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
        git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
        git fetch origin
        git switch -c $LIBRARY_UPDATE_PR_BRANCH origin/$LIBRARY_UPDATE_PR_BRANCH || git switch -c $LIBRARY_UPDATE_PR_BRANCH

    - name: Sync files to repository-a
      run: |
        rsync -r --delete repository-b/repository-a/ repository-a/ --exclude '.git' --exclude '.github/'

リポジトリ B での変更内容をリポジトリ A の特定の PR に積んでいく

リポジトリ B で PR をマージするたびにリポジトリ A で新しい PR が生まれて PR が乱立することを避けます。

これは「既に PR が存在していれば PR 編集・PRが存在していなければ新規作成」を実現するシェルスクリプトを書いているだけです。

PR の body にはリポジトリ B の PR へのリンクをチェックボックス形式で記載しています。

結論に記載した Action から抜粋
    - name: Create pr to repository-a
      env:
        TOKEN: ${{ steps.app-token.outputs.token }}
        REVIEWER: Reviewer-name-as-you-like
      working-directory: repository-a
      run: |
        set -eux
        git commit -m "Update repository-a from repository-b's PR ${{github.event.pull_request.html_url}}"
        git push origin HEAD
        echo ${TOKEN} > token.txt
        gh auth login --with-token < token.txt

        pr_list=$(gh pr list --head $LIBRARY_UPDATE_PR_BRANCH --json=number,body)
        if [[ $(echo "$pr_list" | jq '.[] | length') -eq 0 ]]; then
          gh pr create \
            --title "Update `repository-a` via repository-b repo" \
            --body "- [ ] ${{github.event.pull_request.html_url}}" \
            --reviewer ${REVIEWER}
        else
          echo $pr_list | jq -r '.[0].body' > pr_body.txt
          echo "- [ ] ${{github.event.pull_request.html_url}}" >> pr_body.txt
          gh pr edit --body "$(cat pr_body.txt)"
        fi

さいごに

「共通ライブラリに対して PR を投げる」という GitHub Actions でした。
別なリポジトリに変更内容を反映するようなユースケースは稀によくあるので便利な action かと思います。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?