GitHub Actionを使って複数レポジトリ間でファイルを同期する

GitHubの複数レポジトリで使用している共通のスクリプトやworkflowなど、メンテの際にPRして回るのが面倒だったので自動でPRが作られる様にしてみた。

# 検討したけど没にした案たち

- git submodule / git subtree: 特定ディレクトリにまとまってないので :no_good: 
- post-commit hook: ローカル環境に依存するので :no_good: 
  - 参考: https://stackoverflow.com/questions/19198787/how-to-conveniently-sync-a-file-
- 良さげなgithub action: https://medium.com/@adrianjost/how-to-sync-github-actions-across-repositories-f0b09dc9a9d8
  - masterに直コミットされるぽいので :no_good: 

# 採用案

便利なライブラリ[octopull](https://github.com/alvaropinot/octopull)を見つけたのでこれを使ってGitHub Workflowでやる :octocat: 


# サンプルコード

インラインで解説コメントしてあります。

## [GitHub Action](https://github.com/pomu0325/sandbox/blob/main/.github/workflows/octopull.yml)


```yaml:.github/workflows/octopull.yml
name: レポジトリ共通ファイルの同期

on:
  pull_request:
    branches:
      - main
    types: [closed]
    paths:  # 同期対象のファイルが含まれている場合のみ起動する
      - 'hello.sh'
      - 'scripts/**'
      - '.github/workflows/**'
jobs:
  octopull:
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true # PRのclose時には発動させない
    steps:
    - uses: actions/checkout@v2
    - name: sync files
      env:
        GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # 他レポジトリを操作するのでpersonal access tokenをsecretsに登録して使う
      run: |
        npm install octopull

        # npmに登録されてるのが壊れてるので無理やりmv
        mv node_modules/octopull/lib/api/ node_modules/octopull/
        mv node_modules/octopull/lib/index.js node_modules/octopull/

        # PRのdescriptionの複数行をシェルスクリプトで使える様にjqでバラす
        # 参考: https://medium.com/agorapulse-stories/how-to-work-with-multiline-string-variables-in-github-actions-23f56447d209
        pr_body=$(jq '.pull_request.body' $GITHUB_EVENT_PATH)
        pr_body="${pr_body%\"}"
        pr_body="${pr_body#\"}"

        node scripts/octopull.js '${{ github.event.pull_request.head.repo.name }}' '${{ github.event.pull_request.head.ref }}' '${{ github.event.pull_request.title }}' "$pr_body\n\n---\n\nCreated via ${{ github.event.pull_request.html_url }}"
```

## [GitHub Actionで実行するスクリプト](https://github.com/pomu0325/sandbox/blob/main/scripts/octopull.js)

```js:scripts/octopull.js
const octopull = require('octopull')

const current_repo = process.argv[2]

const repos = [  // 同期したいレポジトリが増えたらここに追加していく
  'sandbox',
  'sandbox2',
].filter(repo => repo != current_repo).map(repo => { // 自分のレポジトリに再帰的にPR作られない様に除外
  return {
    owner: 'pomu0325',
    repo: repo,
    defaultBranch: 'main',
    platform: 'github'
  }
})

const branch = process.argv[3]
const title = process.argv[4]
const message = process.argv[5]

const options = {
  files: [  // 同期したいファイルをここに追加していく
    'hello.sh',
    'scripts/octopull.js',
    '.github/workflows/octopull.yml',
  ],
  branch: branch, // autogenerated if blank
  message: title, // autogenerated if blank
  // OPTIONAL
  pullRequest: {
    title: title, // autogenerated if blank
    body: message.replace(/\\n/g, '\n'), // autogenerated if blank
    // OPTIONAL
    // assignees: ['username1', 'username2']
    // reviewers: ['username3', 'username4']
  }
}

repos.forEach((config) => octopull.commit(config, options))
```

# 実行例

- マージしたPR: https://github.com/pomu0325/sandbox/pull/3
- 起動されたGitHub Action: https://github.com/pomu0325/sandbox/runs/1516555378?check_suite_focus=true
- 作られたPR: https://github.com/pomu0325/sandbox2/pull/2

# 注意事項

- Personal Access Tokenを作る際に、`repo`だけだと`.github/workflows`以下のファイルのコミットで権限足りなくて404エラーになってしまうので`workflow`の権限が必要![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9490/3475c97b-9685-ca4c-f63b-801364d2841d.png)

- Organizationのレポジトリの場合、`GH_TOKEN`はOrganization Secretsへ登録しておくとどのレポジトリでも使えて便利

# オマケ
ローカル環境でGitHub Actionの動作確認をするには[act](https://github.com/nektos/act)が便利です。

- `pull_request-closed.json`にwebhookで飛んでくるJSONを保存しておいて、イベントデータとして`-e`で指定。
- `jq`が`act`がデフォルトで使うDockerイメージに入ってないのでデカいけどGitHub Actionで使われるイメージに近いやつを`-P`で指定（参考: https://github.com/nektos/act#alternative-runner-images )

```sh
act -s GITHUB_TOKEN=hogehoge -v -e pull_request-closed.json -P ubuntu-latest=nektos/act-environments-ubuntu:18.04 pull_request
```