CircleCI Config SDK を使って JS でビルド設定を書くー

はいどーも。こんばんは。CircleCI のシーバです

CircleCI Config SDK

何日か前に CircleCI Config SDK が発表されましたー!わーい!やったー!

って喜んでみたものの、どういうことかあんまりよくわかってない!(えー)

ので、とりあえず触ってみることにした。TypeScript や JavaScript で CircleCI の設定ファイルを生成できるみたい

仕組み

例えば Job はこんな感じで定義できる

const testJob = new CircleCI.Job("test", dockerNode.reuse());
testJob.addStep(new CircleCI.commands.Checkout());
testJob.addStep(new CircleCI.commands.Run({
  command: "npm install && npm run test"
}));

TypeScript で SDK が作られてるので型の情報が出てきて便利。同じ感じで Workflow とかも定義して、最終的にはその定義を YAML として書き出すことができる

ほうほうそれで?

書き出すことができるのは分かったけど、どんな風に使うんだろう?って思いながらぼーっとブログを読みながら考えてみた。こういうことかな?

  • SDK を使って設定ファイルを生成する関数を作ると、似たような YAML を何度も手で書かなくて済むようにできる
  • さらに、モジュール化して NPM レジストリにアップロードすることで、その関数やツールを共有できる

ふむふむ。面白いかもしれない

設定ファイル生成のタイミング

静的に config.yml を生成してその生成された設定ファイルをコミットすることもできるし、Dynamic Config と組み合わせて CircleCI の実行時に動的に生成することもできる

静的生成は、プロジェクトの雛形を生成するときに対話形式で config.yml を生成するようなツールを作るのに使えそうかなぁ。動的生成は、実行時の情報を使って設定ファイルを生成できて便利そう

とりあえず触ってみよう

ということで、雰囲気だけは分かったので、やってみよう。動的な生成の方で。まずは、モジュールを作って NPM レジストリにアップロードしてみるー

ブログの記事に書いてあるサンプルにちょこちょこ手を入れてこんな感じになった

const CircleCI = require("@circleci/circleci-config-sdk");
const fs = require('fs');

const nodeConfig = new CircleCI.Config();

// Node executor
const dockerNode = new CircleCI.executors
  .DockerExecutor("cimg/node:lts")
  .toReusable("docker-node");
nodeConfig.addReusableExecutor(dockerNode);

// Test Job
const testJob = new CircleCI.Job("test", dockerNode.reuse());
testJob.addStep(new CircleCI.commands.Checkout());
testJob.addStep(new CircleCI.commands.Run({
  command: "npm install && npm run test"
}));
nodeConfig.addJob(testJob);

// Deploy Job
const deployJob = new CircleCI.Job("deploy", dockerNode.reuse());
deployJob.addStep(new CircleCI.commands.Checkout());
deployJob.addStep(new CircleCI.commands.Run({ command: "npm run deploy" }));
nodeConfig.addJob(deployJob);

// Workflow
const nodeWorkflow = new CircleCI.Workflow("node-test-deploy");
nodeConfig.addWorkflow(nodeWorkflow);

const wfTestJob = new CircleCI.workflow.WorkflowJob(testJob);
nodeWorkflow.jobs.push(wfTestJob);
const wfDeployJob = new CircleCI.workflow.WorkflowJob(deployJob, {
  requires: ["test"], filters: { branches: { ignore: ["/.*/"] } }
});
nodeWorkflow.jobs.push(wfDeployJob);

/**
 * Exports a CircleCI config for a node project
 */
module.exports = function writeNodeConfig(deployTag, configPath) {
  wfTestJob.parameters = {
    ...wfTestJob.parameters, filters: { tags: { only: deployTag } }
  };
  wfDeployJob.parameters.filters.tags = { only: deployTag };
  fs.writeFile(configPath, nodeConfig.stringify(), (err) => {
    if (err) console.error(err);
  })
}

型情報があるから書きやすいのは分かるんだけど、JS で書いて読みやすいのかなぁ?って思ってたら、実際に書いて眺めてみるとわりと悪くない

んで、これを使って試しに設定ファイルを生成してみると

❯ node
Welcome to Node.js v18.2.0.
Type ".help" for more information.
> const writeNodeConfig = require('.');
undefined
> writeNodeConfig('/v.*/', './config.yml')
undefined

こういう YAML ファイルができた

❯ cat config.yml
# This configuration has been automatically generated by the CircleCI Config SDK.
# For more information, see https://github.com/CircleCI-Public/circleci-config-sdk-ts
# SDK Version: 0.0.0-development

version: 2.1
setup: false
executors:
  docker-node:
    docker:
      - image: cimg/node:lts
    resource_class: medium
jobs:
  test:
    executor:
      name: docker-node
    steps:
      - checkout
      - run:
          command: npm install && npm run test
  deploy:
    executor:
      name: docker-node
    steps:
      - checkout
      - run:
          command: npm run deploy
workflows:
  node-test-deploy:
    jobs:
      - test:
          filters:
            tags:
              only: /v.*/
      - deploy:
          requires:
            - test
          filters:
            branches:
              ignore:
                - /.*/
            tags:
              only: /v.*/

ふむふむ。んで、NPM レジストリにアップロードしておいた

それを使うプロジェクト

次は、今アップロードしたモジュールを使って CircleCI を動かすプロジェクトを作る

Dynamic Config を使うので、こういう構成にして

ちょっと分かりにくいけど、.circleci/dynamic の下に、ビルド用の JS プロジェクトが置いてある。下の方にある package.json はビルド対象のプロジェクトのやつ

ビルド用の index.js はこれだけ

const path = require("path");
const writeNodeConfig = require("@bufferings/hello-circleci-config-sdk");

writeNodeConfig("/v.*/", path.join(__dirname, "../dynamicConfig.yml"));

なるほどなぁ。これだけかー

で Dynamic Config の setup 用の config.yml は、こうなる

version: 2.1
orbs:
  continuation: circleci/continuation@0.3.1
  node: circleci/node@5.0.2
setup: true
jobs:
  generate-config:
    executor: node/default
    steps:
      - checkout
      - node/install-packages:
          app-dir: .circleci/dynamic
      - run:
          name: Generate config
          command: node .circleci/dynamic/index.js
      - continuation/continue:
          configuration_path: .circleci/dynamicConfig.yml
workflows:
  dynamic-workflow:
    jobs:
      - generate-config

CircleCI でプロジェクトを登録して、プロジェクト設定 > Advanced > Dynamic Config をオンにして実行

動いたーヽ(=´▽`=)ノ

感想

  • JS で書くのは思ってたより心地よい。型情報があるのでサクサク書ける
  • NPM レジストリにアップロードして共通化するのはとても便利そう
  • アップロードしない場合でも、たくさんのサービスを持ってて同じような YAML を書きまくってるモノレポみたいな環境だと、いっこ関数を定義しておいてそれをうまく使えばとても楽そう

一方で

  • YAML だと読めば何をするかが分かる安心感があるけど、SDK で生成する場合は結局どうなるのか分かりにくいだろうなと思う
  • なので、あまり複雑なことをやりすぎると手に負えなくなりそう

でも

  • 複雑な処理をうまく関数で包んで隠すことができると、また新しい考えが生まれてきそう

ってところかな。実際に使うときは TypeScript で ES Modules なスタイルで書きたい気持ち

ぜひ使ってみてくださいー!