プルリクエスト内のファイルを変更検知するCI支援ツールを作った話

# 何を作ったのか

[Sakanax](https://sakanax.github.io/sakanax-docs/): .sakanax.ymlに記述したファイルがPullRequest内に含まれているか判定するCLIツール

![image.png](https://qiita-image-store.s3.amazonaws.com/0/163591/1c985c73-baac-06c7-b045-89a669fd54c5.png)


以下のように.sakanax.ymlに特定したいファイルを記入しておくと

```.sakanax.yml
detect_files:
  - cloudformation.yaml
```

コミットIDを指定することで検索することができます。

```bash
$ sakanax detect_file --sha aed28c5aa60398fda946878978168890b0x0007a
[INFO] detected [["cloudformation.yaml"]]
```

sakanaxは指定されたコミットIDを含むPullRequestを探し、そのPullRequestに含まれている（差分として表れている）ファイルの中から.sakanax.ymlに含まれているかどうかを判断します。

動作イメージ

![sakanax.gif](https://qiita-image-store.s3.amazonaws.com/0/163591/28cefdf6-67a0-3f2e-8e78-3a72d9274a27.gif)


# なぜ作ろうと思ったか

Infrastructure as Codeとクラウドが当たり前になりすぎて、アプリケーションをGithubのリポジトリで管理する際にインフラとリポジトリを分けているからバージョンの差異が生まれてしまい、困ったといったケースはあるあるではないでしょうか？

あるサービスを開発していて、そのリポジトリをアプリ管理リポジトリとインフラ管理リポジトリに分けていたとしましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/163591/43db15f8-8af2-05c5-ceaf-d9fcba58bbf7.png)


例えばこのサービスにS3にファイルをアップロードする機能があったとすると、アプリの設定ファイルにはS3のパスを指定しなければいけないことになります。もし仮にインフラのリポジトリでS3のパスを変更したとするとアプリに影響が及ぶためにリポジトリ間の依存を考慮してサービスを開発しなければなりません。
S3だけならまだしもクラウドサービスを使用しているとアプリとインフラをどちらも意識して開発しなければいけないシチュエーションばかりです。

そこで、リポジトリは１つにしてしまおうという方針を考えます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/163591/a1d088ac-0239-623d-61c0-2423a6f0ca29.png)

するとどうでしょうか？これで全て解決でしょうか？

実はこのリポジトリ構成の場合、CIに影響が及びます。
コミット毎にビルド・テストを行うと思いますが、アプリのソースコードしか変更していないのにも関わらずインフラのテストも走ってしまう。あるいはインフラのコードを１行変えただけなのにアプリのテストも行われてしまうなんてことが起こります。

これではテストが非常に非効率ですよね。
それを回避しようと[Sakanax](https://sakanax.github.io/sakanax-docs/)を作成しました。

.infra.yml内に特定のテストを行いたいファイル（インフラ系のソースなど）を記入しておき、

```.infra.yml
detect_files:
  - web.yaml
  - s3.yaml
```

sakanaxコマンドで引数として指定します。detect_filesでファイルが検出されたらテストを行いましょう。

```.bash
#!/bin/bash

RESULT=$(sakanax detect_files --sha <COMMIT ID> --config .infra.yml)

if [ "${RESULT}" == false ]; then
  echo "[INFO] Could not find the file contained in .infra.yml."
else
  echo "[INFO] Test the detected file"
  # インフラ系のテストを行う
fi
```

これでこの問題は解消できそうです。

# 何を使って開発したか？

今回Sakanaxを開発するにあたって使用したツール群（Gem）

| ツール名    | 説明              |
|:------------|:------------------|
| [Thor](https://github.com/erikhuda/thor)        | RubyでCLIツールを簡単に作れるように<br>してくれるツール |
| [octokit](https://github.com/octokit/octokit.rb)     | Github SDK for Ruby |
| [tty-spinner](https://github.com/piotrmurach/tty-spinner) | CLIツールでスピナーを使用できる |

# Gemを開発してrubygems.orgにリリースするまで

Gemを作ってリリースするところまで簡単にご紹介です。

## Gemプロジェクトの作成

```bash
$ bundle gem sakanax
```

以下のファイルが`bundle gem`で生成されます。

```bash
$ tree sakanax
sakanax
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── lib
│   ├── sakanax
│   │   └── version.rb
│   └── sakanax.rb
└── sakanax.gemspec
```


## .gemspecの作成

`bundle gem`コマンドでGemプロジェクトを作成するとsakanax.gemspecのようなファイルができますので記入していきましょう。

```.rb
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'sakanax/version'

Gem::Specification.new do |spec|
  spec.name          = 'sakanax' <-- Gem の名前
  spec.version       = Sakanax::VERSION <-- lib/sakanax/version.rb に記載
  spec.authors       = ['daisuke-awaji']
  spec.email         = ['gee.awa@gmail.com']

  ↓ 説明文とか（rubygems.orgに表示されます）
  spec.summary       = 'Sakanax is simple cli tool to detect github pull request files.' 
  spec.description   = 'Sakanax is a CLI tool for identifying files that are up in Github PullRequest. For example, specifying a commit ID specifies a PullRequest including the commit ID, and also acquires a list of files included in the PullRequest.'
  spec.homepage      = 'https://github.com/sakanax/sakanax'
  spec.license       = 'MIT'

  ↓ この辺はコメントアウト
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
  # to allow pushing to a single host or delete this section to allow pushing to any host.
  # if spec.respond_to?(:metadata)
  #   spec.metadata['allowed_push_host'] = "Set to 'http://mygemserver.com'"
  # else
  #   raise 'RubyGems 2.0 or newer is required to protect against ' \
  #     'public gem pushes.'
  # end

  # Specify which files should be added to the gem when it is released.
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
    `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
  end
  spec.bindir        = 'exe'
  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
  spec.require_paths = ['lib']
  
  ↓ 開発するために必要なgemを記載
  spec.add_development_dependency 'bundler', '~> 1.16'
  spec.add_development_dependency 'rake', '~> 10.0'
  ↓ 開発しているGemが実行されるために必要なGemを記載
  spec.add_runtime_dependency 'octokit', '~> 4.0'
  spec.add_runtime_dependency 'thor', '~> 0.19.1'
  spec.add_runtime_dependency 'tty-spinner'
end
```

## 実際のプロダクトコードを開発

`exe/sakanax`が最初に実行されます。ここからスタートです。
今回はCLIツールを作成するのでCLIクラスを実行させます。

```ruby:exe/sakanax
#!/usr/bin/env ruby

require "sakanax"

Sakanax::CLI.start
```

本体は`lib/`配下に記載していきます。
Thorの実装方法は[こちらの公式ドキュメント](http://whatisthor.com/)が非常に参考になります。

```lib/sakanax/cli.rb
require 'sakanax'
require 'thor'
require 'octokit'
require 'tty-spinner'
require_relative 'github'

module Sakanax
  # CLI Class
  class CLI < Thor
    class_option :help, type: :boolean, aliases: '-h', desc: 'help message.'
    class_option 'config', type: :string, required: false

    def initialize(*args)
      super
      config = File.join(Dir.pwd, '.sakanax.yml') if config.nil?
      @github = Github.new(config)
      @sha = options['sha']
      spinner = TTY::Spinner.new(':spinner ', format: :dots_2)
      spinner.auto_spin
    end

    desc 'version', 'version'
    def version
      puts "sakanax #{Sakanax::VERSION}"
    end

    ...
```


## rubygems.orgのアカウントの作成

プロジェクトが完成したら https://rubygems.org/sign_up でアカウントを作成します。

## .gem/credentialsにAPIキーをセット

rubygems.orgからAPIキーを取得します。
https://rubygems.org/profile/edit の画面からAPIキーを取得して以下のように`.gem/credentials`にセットします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/163591/18faa74a-2655-eece-9bb9-b906e4e668a4.png)


```yaml:.gem/credentials
---
:rubygems_api_key: <APIキー>
```

## rubygems.orgにGemをリリース

最後にリリースを行いましょう。継続的デリバリーを考えるならCIサービス（TravisやCircle）などを使用して
「Githubのmasterブランチにマージされたら以下のコマンドを実行する。」などのような運用をすればいいです。

```bash
$ bundle exec rake release
```

https://rubygems.org/gems/sakanax

最終的に以下のようにリリースされればOKです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/163591/d51f47aa-46a4-ba32-882f-6d17e20627c5.png)