hnwの日記

Travis CIのcron jobsを使ってGitHubに定期的にcommitする方法

みなさん、Travis CI使ってますか?Trais CIはクラウドCIサービスの1つで、GitHub上で公開しているOSSを自動テストする目的であれば定番中の定番といっていいサービスです。

ところで、さいきん私の公開しているプロジェクト「hnw/wsoui」で以下のことを実現したいと考えました。

  • ネット上のデータを加工してGoの連想配列の形で提供したい
  • 自動更新したい
    • 情報ソースは不定期に更新される
    • そこそこ最新のデータを反映していてほしい

これを実現する方法は何種類か思いつきますが、今回Travis CI のcron jobsを使って1週間に1回、更新があったときだけgit commitするような仕組みを作りましたので、これを紹介します。

Travis CI のcron jobs

Travis CIでは、gitリポジトリに新たなcommitがpushされるたびにテストスクリプトが実行されます。これにより、万一バグがあった場合でも早期に気付くことができたり、どのcommitでエンバグしたか調べる材料になったりするわけです。

また、commitごとのテスト実行とは別に、定期的にテストを行うような機能「Cron Jobs」も提供されています。これはTravis CIのコンソール画面「Settings」「Cron Jobs」から設定できます。ビルド間隔は日次、週次、月次のいずれか一つを選択できます。

本来cron jobsは依存ライブラリのバージョンアップなどで以前通っていたテストが失敗するようになっていないかを定期的にチェックする機能です。今回のようにcron jobsのときだけ別の処理を行うというのは変則的な使い方ではありますが、色々と便利な応用が考えられるのではないでしょうか。

通常のテストと自動更新との共存

さて、Travis CI上でジョブを定期実行できることはわかりました。テストスクリプト内で環境変数を見ればcron jobsかどうか判定できますから、「定期実行されたときだけ自動更新を実行する」という今回の目標は十分実現できそうです。

とはいえ、テストスクリプトにテスト以外の処理を追記してしまうと保守性が下がってしまうかもしれません。もう少し綺麗に実現する方法はないのでしょうか。

2018/6時点ではβ機能ですが、Travis CIにはBuild Stageという仕組みがあり、「cron jobsのときだけ実行」をシンプルに実現することができます。具体的には、.travis.ymlを次のように記述できます。

jobs:
  include:
    - stage: 'Test'
      script:
        - go test -v .
    - stage: 'Update Check'
      if: type = cron
      script:
        - [cron jobsのときに実行したいコマンド1] ...
        - [cron jobsのときに実行したいコマンド2] ...

このように記述すると普段はテストだけを実行し、cron jobsのときだけテスト成功後に「Update Check」処理が走ります。小さなことですが、別の処理を別の場所に記述できた方が安心ですよね。


git commitするために

さて、ここまでの知識でTravis CI上でcurlでデータを取ってきて変換スクリプトを動かすところまではできそうです。あとは生成されたファイルをgit commitするだけですが、これが意外と面倒です。

まずは対象リポジトリのデプロイキーを用意します。これはSSH鍵ペアを新規作成した上で、公開鍵をGitHubのリポジトリページから「Settings」「Deploy keys」で設定します。

$ ssh-keygen -t ed25519 -f /tmp/id_ed25519
$ cat /tmp/id_ed25519.pub
ssh-ed25519 ******************************************************************** [email protected]

今回のようにデプロイキーで外部からpushするような場合は「Allow write access」は必須になりますので注意してください。

次に、先ほど生成した秘密鍵を暗号化してリポジトリにアップロードします。まずtravisコマンドをインストールしてそれを利用します。

$ gem install travis
(snip)
$ travis encrypt-file /tmp/id_ed25519
Detected repository as hnw/wsoui, is this correct? |yes|
encrypting /tmp/id_ed25519 for hnw/wsoui
storing result as id_ed25519.enc
storing secure env variables for decryption

Please add the following to your build script (before_install stage in your .travis.yml, for instance):

    openssl aes-256-cbc -K $encrypted_****_key -iv $encrypted_****_iv -in id_ed25519.enc -out /tmp/id_ed25519 -d

Pro Tip: You can add it automatically by running with --add.

Make sure to add id_ed25519.enc to the git repository.
Make sure not to add /tmp/id_ed25519 to the git repository.
Commit all changes to your .travis.yml.
$ git add id_ed25519.enc

暗号化した秘密鍵はTravis CI上で復号して$HOME/.ssh/以下にコピーして使います。設定ファイル.travis.ymlは最終的に次のようになりました。

language: go

go:
  - "1.10"

jobs:
  include:
    - stage: 'Test'
      script:
        - go test -v .
    - stage: 'Update Check'
      if: type = cron
      script:
        - git checkout master
        - curl "https://code.wireshark.org/****" > /tmp/manuf
        - scripts/oui-convert.pl /tmp/manuf > ouidata.go
        - go test -v .
        - openssl aes-256-cbc -K $encrypted_****_key -iv $encrypted_****_iv -in id_ed25519.enc -out $HOME/.ssh/id_ed25519 -d
        - chmod 600 $HOME/.ssh/id_ed25519
        - scripts/push-if-updated.sh ouidata.go

ここで注意すべきことですが、Travis CIのテスト対象は特定commitでありGitでいうところの「detached HEAD」と呼ばれる状態になっているため、そのままでは元のリポジトリにpushできません。そのため、まずmasterブランチに切り替えるという処理を行っています。ちょっとしたTIPSですが、私は少々ハマりました。

Travis CI上で自動更新を行うメリット

上記設定により、Travis CI上で自プロジェクトの自動更新ができるようになりました。しかし、cronで実行するだけであれば、VPSを利用することもできます。わざわざ手間をかけてTravis CIで実行するメリットは何でしょうか。

個人的には、GitHub+Travis CI環境では実行に必要な全ファイルが公開されていること、また実行ログが残ることがメリットだと考えています。

今回は自動生成したソースコードを自動commitしているわけですから、おかしな内容がcommitされてしまう可能性もゼロではありません。そんな場合でも、必要ファイルとログが公開されていれば、赤の他人であっても原因をつきとめて修正してPull Requestを送ることが可能です。

こんな過疎プロジェクトで誰もPull Request送らないでしょ、というツッコミもあるとは思いますが、可能性って大事だと思うんですよね。

まとめ

  • Travis CIのcron jobsを使うと定期実行ジョブをソースコードつき・ログつきで公開できる
    • インターネット上で更新され続けている何かを変換してgit commitするような用途には非常に良いのでは?たとえば日本の祝日カレンダーライブラリなど。
'); $entries_chunk.insertBefore(sections[0]); } else { chunk_id += 1; var $prev_entries_chunk = $entries_chunk; var $read_more_link = $('

これ以前の記事を表示する

'); $read_more_link.on('click', {chunk_id: chunk_id}, function(e){ $(e.target).hide(); $(this).remove(); $('#entries-chunk-' + e.data.chunk_id).fadeIn("slow"); }); $prev_entries_chunk.append($read_more_link); var $entries_chunk = $('
'); $entries_chunk.hide(); $entries_chunk.insertAfter($prev_entries_chunk); } } $(sections[i]).appendTo($entries_chunk); } });