.circleci/config.yml を分割しておく

circleci-cli では workflows を解釈しないため、ローカルで circleci config.yml のテストをしようとすると手間がある。そこで build ごとに yml を分割しテスト、それを config.yml に反映したい。

複数の yml を連結してくれるような仕組みは circleci にはない (と思う) 。かといって手連結はめんどくさいので、とりいそぎ pre-commit で連結して config.yml を生成するようにするスクリプトを用意する。

/.circleci
  - _config.yml
  - apply.yml
  - validate.yml
- pre_commit.rb

という構成で

_config.yml

workflows を設定しておく。

version: 2
jobs:
workflows:
  version: 2
  test:
    jobs:
      - validate:
          filters:
            branches:
              ignore: master
      - apply:
          filters:
            branches:
              only: master

各 build を分割する

workflows にある #{build_name}.yml という名前で yml を作成する。今回は applyvalidate が必要なので apply.ymlvalidate.yml になる。config.yml として valid にしておかないと circleci-cli でテストできないのできっちり書く。

apply.yml

version: 2
jobs:
  build:
    parallelism: 1
    docker:
      - image: ruby
    steps:
      - checkout
      - run:
          name: work
          command: apply

validate.yml

version: 2
jobs:
  build:
    parallelism: 1
    docker:
      - image: ruby
    steps:
      - checkout
      - run:
          name: work
          command: validate

pre-commit

.git/hooks/pre-commit

#!/usr/bin/env sh
bundle exec ruby "$(git rev-parse --show-toplevel)/pre_commit.rb"

pre_commit.rb

.circleci 配下にある yml を連結する。

require 'yaml'
class Combine
  CONFIG_PATH = '.circleci/config.yml'
  CONFIG_BASE_PATH = '.circleci/_config.yml'
  CONFIG_CHILDREN_PATH = '.circleci/**/*.yml'
  def execute!
    check!
    prepare!
    File.write(
      CONFIG_PATH,
      base_configuration.merge('jobs' => combined_configuration).to_yaml
    )
    commit!
  rescue ConfigurationHasChange => e
    puts "\e[31m#{e.message}\e[0m"
    exit(1)
  end
  private
  def check!
    return unless File.exist?(CONFIG_PATH)
    raise ConfigurationHasChange if config_yml_has_change?
  end
  def config_yml_has_change?
    return false unless `git status`.match?(/modified:.+#{CONFIG_PATH}/)
    `git diff HEAD^ -- #{CONFIG_PATH}` != ''
  end
  def prepare!
    File.delete(CONFIG_PATH)
  rescue => e
    puts "#{e} (But ignore this error.)"
  end
  def base_configuration
    YAML.load_file(CONFIG_BASE_PATH)
  end
  def configurations
    Dir.glob(CONFIG_CHILDREN_PATH)
  end
  def pick_name(f)
    File.basename(f).split('.').shift
  end
  def combined_configuration
    configurations.inject({}) do |a, f|
      name = pick_name(f)
      if name == '_config'
        a
      else
        a.merge(name => YAML.load_file(f)['jobs']['build'])
      end
    end
  end
  def commit!
    `git add #{CONFIG_PATH}`
  end
  class ConfigurationHasChange < RuntimeError
    def message
      <<-EOS
"#{CONFIG_PATH}" has change.
DO NOT update "#{CONFIG_PATH}" manually.
Any configuration must be split out to "\#{BUILD_NAME}.yml".
      EOS
    end
  end
end
Combine.new.execute! if $0 == __FILE__

結果

連結された config.yml が作成され、 git add される。変更が入っていれば commit に含まれるはずだ。

---
version: 2
jobs:
  validate:
    parallelism: 1
    docker:
    - image: ruby
    steps:
    - checkout
    - run:
        name: work
        command: validate
  apply:
    parallelism: 1
    docker:
    - image: ruby
    steps:
    - checkout
    - run:
        name: work
        command: apply
workflows:
  version: 2
  test:
    jobs:
    - validate:
        filters:
          branches:
            ignore: master
    - apply:
        filters:
          branches:
            only: master