くりにっき

フルスタックキュアエンジニアです

俺的CircleCI Orbs開発ベストプラクティス

これは CircleCI Advent Calendar 201820日目です。

qiita.com

sue445/ruby-orbs を作って1ヶ月くらい経ちましたが、Orbをメンテしやすくするためのテクニックがいくつか分かってきたので紹介したいと思います

sue445.hatenablog.com

sue445.hatenablog.com

リリースごとにgitのtagを打つ

  • パッチバージョンのアップデート(+0.0.1)はmasterブランチへのコミットの度にリリース&tag pushする
  • メジャーバージョンのアップデート(+1.0.0)やマイナーバージョンのアップデート(+0.1.0)はローカルでgitのtagを作ってpushし、そこからCircleCI上でOrbをリリースする

この運用が自分の中で一番しっくりきています。

理由

  • バージョンごとにリポジトリにtagが打たれていると、GitHub上でのバージョン間の差分が見やすいため
  • パッチバージョンのアップデートは基本全自動で問題なさそうだが、メジャーバージョンやマイナーバージョンのアップデートは人間の意思が介在するのでローカルで作業をしたい(要出典)

後者に関してはローカルから circleci increment majorcircleci increment minor を叩いても実現はできます。

ただし自分の場合作業環境を複数台持っている(PCではなくサーバに入って作業することも多い)ので、各環境にcircleci cliトークンを置くよりはgitでtag pushしたら自動でそのバージョンのOrbをリリースできるようにするのがスマートだと思ったので実装しました。

実装方法

ソースはここにあります

masterブランチへのコミットの度に自動でOrbをリリース&tag pushする

関連するソースは下記です

version: 2.1

orbs:
  orb-tools: circleci/orb-tools@2.0.2

jobs:
  push_latest_version_tag:
    docker:
      - image: circleci/ruby:2.5
    working_directory: ~/app
    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "43:39:3a:d1:90:49:a4:85:db:17:41:ce:88:55:44:92"
      - run: curl -fLSs https://circle.ci/cli | sudo bash
      - run:
          name: Setup git
          command: |
            git config push.default current
            git config user.email "$EMAIL"
            git config user.name "CircleCI"
      - run: ruby .circleci/push_latest_version_tag.rb sue445/ruby-orbs

workflows:
  version: 2

  btd:
    jobs:
      (略)

      - orb-tools/increment:
          orb-path: packed/orb.yml
          orb-ref: "sue445/ruby-orbs"
          segment: "patch"
          publish-token-variable: "$CIRCLECI_API_TOKEN"
          attach-workspace: true
          checkout: false
          requires: [orb-tools/test-in-builds]
          filters:
            branches:
              only: master

      - push_latest_version_tag:
          requires: [orb-tools/increment]
          filters:
            branches:
              only: master
            tags:
              ignore: /.*/

解説

  • orb-tools/increment でOrbのパッチバージョンを上げて、Orbリリース後にpush_latest_version_tag.rbを実行
    • ローカルからtagをpushした時にもincrementが走ると嫌なので、念の為 tags:ignore: /.*/ で明示的に除外しています。(なくてよかったかも)
  • push_latest_version_tag.rbhttps://github.com/sue445/circleci-ruby-orbs/blob/1.3.10/.circleci/push_latest_version_tag.rb を参照
    • Orb名を渡したら最新バージョンを取得してgitのtagを作ってpushしています
    • ビルドスクリプトなのでできればシェルでやりたかったのですが、標準出力を正規表現でparseするのが難しかったのでrubyに逃げました
    • 別途管理画面から Checkout SSH keysでuser keyを追加して、CircleCIのジョブからリポジトリにpushできるようにしてください

ローカルからtagをpushしたら自動でOrbをリリースする

関連するソースは下記です

workflows:
  version: 2

  release:
    jobs:
      - orb-tools/pack:
          source-dir: src/
          destination-orb-path: packed/orb.yml
          workspace-path: packed/orb.yml
          artifact-path: packed/orb.yml
          filters:
            tags:
              only: /^[0-9.]+\.0$/
            branches:
              ignore: /.*/

      - orb-tools/publish:
          orb-path: packed/orb.yml
          orb-ref: "sue445/ruby-orbs@${CIRCLE_TAG}"
          publish-token-variable: "$CIRCLECI_API_TOKEN"
          attach-workspace: true
          checkout: false
          requires: [orb-tools/pack]
          filters:
            tags:
              only: /^[0-9.]+\.0$/
            branches:
              ignore: /.*/

解説

tag pushに反応して $CIRCLE_TAG に対応したバージョンでOrbをリリースしています。

filters:branches:ignore: /.*/ で全てのbranchを明示的に除外しておかないと、ブランチpush時にもreleaseワークフローが実行され $CIRCLE_TAG が取れないのでエラーになります

fiters:tags:only: /^[0-9.]+\.0$/ のような正規表現にすることで、1.2.3 -> 1.2.4 のようなCircleCIからのパッチバージョンリリースでは実行されずに、1.2.3 -> 1.3.0 のようなローカルからのtag pushのみで実行されるようになります

この設定をしておかないと

  1. masterブランチをpush
  2. CircleCIでマイナーバージョンアップデート
  3. push_latest_version_tagでOrbの最新バージョンを取得してtag push
  4. 同じtagが作成済みなのでエラーになる
    • Orbのバージョンもtagもちゃんと作られているのに、コミットのビルドステータスが赤になるので気持ち悪い

ということになるので必要です。

ディレクトリ構成について

CircleCIのOrbは基本的に1つのyamlファイルです。

機能が少ないうちはいいのですが、機能が増えてくるとyamlファイルが巨大になって見通しが悪くなってしまいます。

      - orb-tools/pack:
          source-dir: src/
          destination-orb-path: packed/orb.yml
          workspace-path: packed/orb.yml
          artifact-path: packed/orb.yml

のように書くことで、src/にある階層化されたyamlファイルをいい感じに1つのyamlファイルにマージしてくれます。

説明すると長くなるので詳しくは下記PRを見てください。

https://github.com/sue445/circleci-ruby-orbs/pull/30/files

examplesを書く

Orbsのソースコードから実際の使い方を読み解くのは最初のうちは難しいです。GitHubみたいにREADMEがあればサンプルコードを書く余地があるのですが、Registryには(今のところ)そういう機能がありません。

しかし、examples を書くことでRegistryのUsage Examplesに表示させることができます。

Orbのexampleの記載

examples:
  bundle-install:
    description: |
      Run `bundle install` using cache.
    usage:
      jobs:
        rspec:
          docker:
          - image: circleci/ruby
          steps:
          - checkout
          - ruby-orbs/bundle-install:
              bundle_clean: true
              bundle_extra_args: ""
              bundle_gemfile: Gemfile
              bundle_jobs: 4
              bundle_path: vendor/bundle
              bundle_retry: 3
              cache_key_prefix: v1-bundle
              restore_bundled_with: true
          - run: bundle exec rspec
      orbs:
        ruby-orbs: sue445/ruby-orbs@1.3.0
      version: 2.1

Registryでの表示

https://circleci.com/orbs/registry/orb/sue445/ruby-orbs#usage-bundle-install

f:id:sue445:20181208225055p:plain

Orbsの書き方の学習方法

公式ドキュメントはこちらになります https://circleci.com/docs/2.0/creating-orbs/

しかしドキュメント化されてない機能もある*1ため、公式のOrbsのソースを読むのが一番です。

公式Orbsのソースは https://github.com/CircleCI-Public 配下のリポジトリにまとまっているのですが、僕は普段下記のリポジトリを参考にしています。

*1:自分が見つけきれていない説もある