(息抜きコーディング)if 文をごにょごにょ

ts-pattern 便利そう

こちらの記事を読んで

qiita.com

へー。ts-pattern ってライブラリがあるんだー便利そう

github.com

と思った

興味ある

んで、ts-pattern も興味はあるんだけど、それとは別でちょっと興味があったのが、例として書いてあるコード:

  const gameName = (() => {
    if (game.genre === 'rpg') {
      if (game.company === 'enix') {
        return 'dq1';
      } else if (game.company === 'nintendo') {
        return 'pokemon';
      }
      return 'ff1';
    } else if (game.genre === 'simulation') {
      if (game.company === 'maxis') {
        return 'simcity';
      }
      return '';
    }
    return '';
  })();

「これだと読みにくいから ts-pattern 使ったら分かりやすくなったよー!」ってお話なんだけど、↑の路線のままでも、もうちょっと読みやすくはできそうかなぁって思ったので遊んでみた。元記事に対しての反対意見ではなくて、ただコード例で遊んだだけのお話です!

その1:else をなくす

全部 return してるから else をなくしても大丈夫。↑のコードは好きじゃないけど、これなら自分はありかな

  const gameName = (() => {
    if (game.genre === 'rpg' && game.company === 'enix') {
      return 'dq1';
    }

    if (game.genre === 'rpg' && game.company === 'nintendo') {
      return 'pokemon';
    }

    if (game.genre === 'rpg') {
      return 'ff1';
    }

    if (game.genre === 'simulation' && game.company === 'maxis') {
      return 'simcity';
    }

    return '';
  })();

その2:game. をなくす

パラメータの分割代入を使ってちょっとすっきり

  const gameName = (({ genre, company }) => {
    if (genre === 'rpg' && company === 'enix') {
      return 'dq1';
    }

    if (genre === 'rpg' && company === 'nintendo') {
      return 'pokemon';
    }

    if (genre === 'rpg') {
      return 'ff1';
    }

    if (genre === 'simulation' && company === 'maxis') {
      return 'simcity';
    }

    return '';
  })(game);

その3:if を一行で書く

コーディングルールでだめな場合も多いとは思うけど

  const gameName = (({ genre, company }) => {
    if (genre === 'rpg' && company === 'enix') return 'dq1';
    if (genre === 'rpg' && company === 'nintendo') return 'pokemon';
    if (genre === 'rpg') return 'ff1';
    if (genre === 'simulation' && company === 'maxis') return 'simcity';
    return '';
  })(game);

ts-pattern を使った場合がこう↓だから、だいぶ近くなったかなー

  const gameName = match(game)
    .with({ genre: 'rpg', company: 'enix' }, () => 'dq1')
    .with({ genre: 'rpg', company: 'nintendo' }, () => 'pokemon')
    .with({ genre: 'rpg' }, () => 'ff1')
    .with({ genre: 'simulation', company: 'maxis' }, () => 'simcity')
    .otherwise(() => '');

それだけ!面白かった!

おまけ

コードを変えるのに簡単なテストを書いて使ったのだ

console.assert(getGameName({ genre: 'rpg', company: 'enix' }) === 'dq1')
console.assert(getGameName({ genre: 'rpg', company: 'nintendo' }) === 'pokemon')
console.assert(getGameName({ genre: 'rpg', company: 'square' }) === 'ff1')
console.assert(getGameName({ genre: 'rpg' }) === 'ff1')
console.assert(getGameName({ genre: 'simulation', company: 'maxis' }) === 'simcity')
console.assert(getGameName({ genre: 'simulation', company: 'other' }) === '')
console.assert(getGameName({ genre: 'other', company: 'enix' }) === '')

開発中のコミュニケーションには色んなところで想像が入り込む

例えば「会議が多い」という意見に、どう対応しよう?

普通に考えたら「無駄な会議を減らそう!」かな

だけど、できれば僕は「だからどうしたいと思ってるんですか?」というのをその意見をくれた人に確認したい

十中八九「会議を減らしたい」ってことなんだろうとは思いつつ、でも「会議が多いから、」のあとには色んなことが想像できる

  • (順当→)会議を減らしたい
  • それぞれの会議に自分が参加しないといけない理由を知りたい
  • 会議が多くて他のことをやる時間がないから、自分の担当タスクを減らしたい
  • 会議が多いのはいいんだけど、間に休憩が欲しい
  • 「そっかぁ、それは大変だよね。ありがとう」って話を聞いて欲しいだけ
  • もしかしたら「だから、充実してるんです」って可能性もなくはない

さらに「会議を減らしたい」だったとして、その内容も

  • 「無駄な会議が多いから減らしたい」かもしれないし
  • 「開催頻度を減らしてもいい会議があるのではないか?」かもしれないし
  • どれも重要な会議だから、権限を委譲するところから始めたい、かもしれない

もしその対応をするんだったら、できるだけ本人の要望に応えたいと思うので、ここで一歩踏み込んで「こういう想像ができてしまうんですけど、どれですか?」って聞いておきたい

「そんなに深く考えてない」って場合は「じゃあ、その気持ちはどこから来てますか?どうなったら良さそうですか?」って一緒にふかぼりたい

「会議が多い」という意見に対して、想像で「減らしたいってことだと思う」「いや、こういうことなんじゃないか?」とか「減らすなら、こうするべきなんじゃないか?」とか本人不在で話をしている時間は、僕にとっては(なんか無駄な話をしてるなぁ)って時間。本人に確認する以外になくない?って思ってしまう

というようなことを

ふろしきfmの最新話を聴きながら考えた。なんか今回のエピソードは、難しい話題がたくさん出てきた気がする。聴き終わったあと頭が混乱してたw難しくて面白い話題たち

あいだに自分の想像をはさみたくないんよなぁ。いや、はさみたくないってより、想像をはさんだなら、それを自分の想像だと認識しておいて、必要な場合は事実を元にして確認したい。か

開発の現場ではよくある話

こういう話って、開発の現場ではよくある。「自分が相手の言葉から行間を補完してしまう」というもの。「相手がこう言ったんだから、当然こういう意味だろう」で話を進めてしまうもの1

こういうのが要件定義や概要設計あたりで出てくると、とてもめんどくさいことになる。そもそもの方向性が全く変わってくるし、みんなの頑張りにも無駄な作業が発生してしまうから。「あの人がこうしたいって言った!」なのか「あの人はこうしたいってことだと思う!」では全く違うのだ

だから「事実」と「想像」は分けて認識しておきたい。想像しながら話を進めることは悪いことじゃない。だけど、それが「自分の想像」なのか「実際に相手の口から出た言葉」なのかを区別しておきたい。「自分の想像」なんだったら、それを「相手の言葉」として聞きだして確証を得てから進めたい

自分の「なんでこんなこともわからないの?って思われるの嫌だなぁ」っていう気持ちとのたたかいがここにある

「すみません。『会議が多い』というのは具体的にはどういう意図ですか?『会議が多いけどまだまだ増やしていいよ』ってことですか?」「え?あぁ(笑)。いえ、多いから減らしたいってことです」「ありがとうございます!会議の人数をですか?」「いえ、自分が参加しなくても良い会議がありそうなので、そこには不参加にさせてもらいたいなと思っています」「なるほど、ありがとうございます!」

それと同様に

ここまでの話は「自分が相手の言葉から想像で進めてしまう」の話だったんだけど、それが相手側にも同様に発生しうる。自分の言葉から「相手が想像で間を埋めてしまう」というもの

そうすると何が困るかというと相手の「それで問題ないです!」という言葉を元に(相手がそう言ったので!これは自分の想像ではないぞー!)って進めてたら、その相手の認識が違った、っていうケースがあるから

だから、相手ができるだけ想像を挟み込む必要がないように、具体例を入れたり、簡単な絵を描いたり、あとは、明らかに相手の意図と異なることを言って、相手から「そうじゃなくて、こうですよ」という言葉を聞くようにする、みたいにしたりする

特に、自分の発する言葉ができるだけひとつの意味として捉えられるように気をつけようとは思っている

あとは、相手に情報を伝えきれてなくて「あのときはこういう前提だったからAだったけど、そういう前提ならBじゃないとだめだよ」って場合もあるので、前提となる認識合わせも結構意識するかな

それでも認識違いはある

自分の想像を気をつけて減らして、相手にできるだけ誤解を与えないように気をつけてたとして、それでも認識の違いは残る

だから、それでも起こる認識違いのために、言葉だけじゃなくて、できるだけ早い段階でモノを間に置いて話をする

  • プロトタイプを見てもらったり
  • 実際の動くものを見てもらったり(そういう確認が早い段階でできるように開発を進める)

そうすることで、お互いの認識の違いに早い段階で気づくことができて「「なるほどー!そういうことか!」」ってなる

サイン

「と思います」って言われたり、自分が「だと思う」って言ってしまったら「『思う』ってことは、それは想像じゃない?相手の口から『こうしたい』って聞いたわけじゃないんだったら、確認したい!」ってなる

これは、ウェブサービスの開発者として心配性なのと関係してそう。「多分大丈夫」は全然安心できない。「え?それ動かして確認した?」ってなる。それと似てる。「明らかに問題ないでしょ!めんどくさ」って思われてるかもしれないんだけど「ごめん、問題ないとは思うんだけど、ほんとにただの念の為に、動作確認させてくれー!」ってなる

だから、要件や仕様に関して話をする相手としても、プログラマーとしても、僕はとてもめんどくさい人だと思う。ただ、わりと「そういうつもりじゃなかった」は少ないんじゃないかなぁ

「確認したい」は、確認することが好きという意味ではない

以前にもこのブログで書いたことがあるけど、僕は「相手の立場にたってものごとを考えなさい」とか「1を言われたら10を分かりなさい」と言われて育ったので、この「ふつう分かるでしょ?」とか「空気読んでよ」ということに対する質問をするのがあまり得意ではない

え?「会議が多い」って言ったんだから「減らしたい」ってことだってふつう分かるでしょ?どうして確認が必要なの?

って顔をされると(あとでチョコレート買って食べていいから頑張れ自分!)って気持ちになる。それでも、想像で動いてしまって「本人の意図が全く別だった」ということは往々にしてあったので、確認したい、という気持ちが強い(そして「めんどくさいと思われてるかも」ってのは僕の想像であって、事実を元にはしていない)

心配性かつめんどくさがりの僕にとっては、それを確認しない方があとあともっとめんどくさいから(そしてその段階では、エンジニアしかカバーできないことが多い)

とは言いつつ「あー、まためんどくさい質問きたー、どうしてこの人理解が遅いの?って思われてるかもしれない」って思ってしまって、実際はそう思ってないとは思うんだけど、言えないときも結構ある

特に何の結論もないんだけど

だらだらと思ったことを書いたー!明日はドラクエ11してだらだら過ごすー!(夏休み)


  1. そういうのを意図的にか無意識でか利用して、明言や自分の責任を避ける人もいるんだけど、それは今回の話とは別で!

GitLab と CircleCI の連携を試す

昨日、GitLab SaaS と CircleCI の連携が発表されたので試すー!

www.publickey1.jp

CircleCI でエンジニアとして働いてるけど、今日のこれは単に CircleCI のことが好きで触ってみて書いてる。趣味

参考ドキュメント

英語版は「プレビュー」の文字がなくなってるから、日本語版も近いうちに更新されるんじゃないかと思います! → (2022-07-29 追記) すでに更新されてました!はやいー!

ドキュメントを参考にしつつ、自分の好みの流れで書いてみる

1-1. GitLab のプロジェクトを作る

GitLab 使うの初めてだからドキドキしつつサインアップしてフリープランで

空のプロジェクトでもいいんだけどちょっとなんかある方がいいかなと思ったので Spring のテンプレートからプロジェクトを作った

このテンプレートだとデフォルトブランチが master なので main に変更しておいた。なんとなく

1-2. config.yml を作る

Web 上でごにょごにょできそうな雰囲気なので、そのまま .circleci/config.yml を作って最低限の情報を書いた

https://gitlab.com/bufferings/my-awesome-project/-/blob/main/.circleci/config.yml

version: 2.1
jobs:
  build:
    docker:
      - image: cimg/base:2022.07
    steps:
      - run:
          name: The First Step
          command: |
            echo 'Hello World!'

1-3. パーソナルアクセストークンを作る

GitLab がフリープランの場合、CircleCI との連携には現在のところ OAuth じゃなくてアクセストークンを使わないといけないみたいなので、GitLab でパーソナルアクセストークンを作る

https://gitlab.com/-/profile/personal_access_tokens

Select scopes のところは api だけを選んだ

2022-07-29 追記ここから =====

公式ドキュメントに次のように書いてありました:

アクセストークンを下記手順で使用する場合、 api スコープと write_permissions スコープを選択する必要があります。

write_permissions スコープも必要みたいです!すみません!

2022-07-29 追記ここまで =====

これで GitLab 側の準備は OK。次は CircleCI 側

2-1. CircleCI に GitLab 連携用の Organization を作る

GitLab 連携は新しい仕組みになってる

これまでは GitHub や BitBucket の Organization が反映されてたけど、GitLab 連携では自分で Organization を作る

CircleCI の画面の左上のプルダウンから Create New Organization を選択

"Hello GitLab" って名前にしといた

2-2. 作った Organization に Project を登録する

これも GitHub のときは、自動でその Organization 内のリポジトリがすべて表示されて設定ができたけど、GitLab 連携では自分で Project を登録する

画面左上のプルダウンからさっき作った Organization を選んだ状態で、左メニューから Projects を選択、Create Project をクリック

  1. Project 名を入力。普通はリポジトリの名前と同じにしておいたら良いと思うけど、今回は違う名前だとどう見えるのかを見たかったので違う名前にした
  2. GitLab フリープランなので、アクセストークンを選択
  3. 手順 1-3 で作ったパーソナルアクセストークンを入力
  4. Connect ボタンを押すと GitLab と連携をしてリポジトリ一覧が表示されるので
  5. さっき作った GitLab のリポジトリを選択して
  6. Create Project を押すと、プロジェクトのパイプライン一覧画面が開く

これで連携完了ヽ(=´▽`=)ノ

GitLab でリポジトリに変更を入れる

GitLab の画面から適当に更新コミットを入れると。。。

動いたー!

せっかく Spring のテンプレートを選んだけど、ちょっともう眠いからこれで良しとする!

知っておくと便利かもしれないこと

流れ

GitLab のリポジトリをプロジェクトとして登録するときに、こういう設定が自動で登録されている

Project Settings の

  • Configuration に GitLab のリポジトリの設定ファイルが登録されていて
  • Triggers に GitLab のリポジトリからの更新を受けて起動されるトリガーが登録されていて

GitLab 側の

  • Webhook に、そのトリガーの Webhook をキックする設定が登録されている

ふだんは特に意識しなくても良い部分だと思うけど、何か問題があったときにこの流れを知ってると便利かもしれない

新しいコンセプト

さっき少しふれたけど、Organization と Project にこれまでとはちょっと違うコンセプトが導入されている

  • Project
  • Organization
    • 複数のプロジェクトを管理する
    • ユーザーを管理することができる

Coming soon

まだサポートされていない機能があったりするので Coming soon な機能の一覧には目を通しておいたほうが良さそう

https://circleci.com/docs/ja/gitlab-integration#coming-soon

こんなところかな。GitLab 便利そうー!面白かった。

CUE で YAML をマージするときの動きを確認 後編

前編はこちら

bufferings.hatenablog.com

CUE で YAML をマージするときのルールを自分の言葉で説明すると以下の通りだった

  • YAMLYAML の Leaf までのパス: 値 と捉えたときに、それらのすべての Leaf が任意の順番でマージされる
  • ただし、その Leaf に対するスカラー値またはシーケンス値がひとつに決まらなければエラーになる

今回は、どうしてそうなるのか?について簡単にメモを残しておこうと思う

いったん CUE 形式に変換される

YAML を マージするときに僕が使っているのは、こういうコマンド

❯ cue export a.yml b.yml --out yaml

流れはこう

  1. YAML ファイルを CUE 形式に変換する
  2. 変換した複数の CUE 形式のデータをマージ
  3. マージされた CUE 形式のデータを YAML で出力

コマンドの最後につけてる --out yaml をはずすと、マージされたものが CUE 形式のまま出力される

❯ cue export a.yml b.yml
{
    "sample": {
        "foo": "foo value",
        "bar": "bar value"
    }
}

ここで少し CUE について見てみる

CUE は型と値を区別せずに扱う

CUE には型と値を区別せずに扱うという特徴があり、通常なら値を書く部分に、型や条件を書くこともできる

foo: {
    name: string
    count: < 10
}

この例は

  • foo: name: の値は文字列でなければならない
  • foo: count: の値は10未満の数値でなければならない

という制約を表している。そして、foo: name:foo: count: には、これらの条件を満たす値しか設定できない

CUE では同じ要素を複数回定義することができて、その場合はお互いに矛盾しないかの検査が行われる

なので、こういう定義は条件が矛盾しないので OK で

foo: {
    name: string
    count: < 10
}

foo: {
    name: "orange"
    count: 2
}

値も指定してあるので export するとこうなる

❯ cue export foo.cue
{
    "foo": {
        "name": "orange",
        "count": 2
    }
}

でも、たとえば、10以上の値を count に指定すると矛盾するのでエラーになる

foo: {
    name: string
    count: < 10
}

foo: {
    name: "orange"
    count: 10
}
❯ cue export foo.cue
foo.count: invalid value 10 (out of bound <10):
    ./foo.cue:3:9
    ./foo.cue:8:9

だから、CUE を使うと、条件を指定したスキーマを用意しておいて、実際の値がそれらの条件を満たしているかを検査することができる

そして、ひとつのパスに対して、複数の異なる値が指定されると、それらの値が矛盾してしまうので、エラーになる

foo: {
    name: "orange"
}

foo: {
    name: "apple"
}
❯ cue export foo.cue 
foo.name: conflicting values "apple" and "orange":
    ./foo.cue:2:8
    ./foo.cue:6:8

CUE の仕様はなんかもっと色々あって、Lattice 束 (そく)とか出てきて、僕はよくわかってない!のだけど、YAML のことを考えるのには、もうこれだけ分かってたら十分

ちなみに、書いてある順番は関係ないので、先に値があって、その次に条件が書いてあっても動きは同じになる

YAML には値しかない

YAML の値の部分に指定されたものは、値として扱われて、型にはならないので、こう書いても↓

foo:
  name: string

文字列という型にはならずに "string" という値が指定されていることになる。だから、↓のような YAML を CUE に変換すると

foo:
  name: orange
  count: 5

こうなって

{
    "foo": {
        "name": "orange",
        "count": 5
    }
}

もう、これで foo: name:foo: count: の値は決定してしまい、これ以外の値を同じパスに指定することはできない。だから、↓これは CUE に変換するときにエラーになる

foo:
  name: orange
  count: 5
---
foo:
  count: 6
❯ cue export a.yml
foo.count: conflicting values 6 and 5:
    ./a.yml:3:11
    ./a.yml:6:11

という CUE の仕様から、YAML をマージするときは、こういう動きになるのだった

  • YAMLYAML の Leaf までのパス: 値 と捉えたときに、それらのすべての Leaf が任意の順番でマージされる
  • ただし、その Leaf に対するスカラー値またはシーケンス値がひとつに決まらなければエラーになる

おしまい

おまけ:CUE の機能を利用してみる

読み込むファイルは YAML と CUE を混ぜても別に問題ないので、YAML をマージしつつ、CUE で要素を差し込むなんてこともできる

a.yml

version: 2.1

jobs:
  service1-say-hello:
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!1"

b.yml

version: 2.1

jobs:
  service2-say-hello:
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!2"

これに CUE ファイルとしてこんな c.cue を混ぜると。。。

jobs: {
    [string]: {
        docker:
        [{image: "cimg/base:stable"}]
    }
}

すべての job に差し込むことができる

❯ cue export a.yml b.yml c.cue --out yaml
version: 2.1
jobs:
  service1-say-hello:
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!1
    docker:
      - image: cimg/base:stable
  service2-say-hello:
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!2
    docker:
      - image: cimg/base:stable

面白いね

でも、んー、僕はたぶん使わないかなぁ。YAML 同士のマージならまだ雰囲気で使えるけど、CUE の機能を使うとなると CUE の仕様を把握する必要があるし、最初に書いた人は大丈夫だけど、そのうち誰も分からないけど動いてる・・・ってなりそうだから(じゃあこんな機能を紹介しなくていいのでは!?

これで一連の CircleCI の設定ファイル分割の話と、CUE の話はおしまい!楽しかったー!

水曜日の勉強会で

このあたりのことを実際にお見せしますー!よかったら来てね

circleci.connpass.com

CUE で YAML をマージするときの動きを確認

CircleCI の config.yml を分割できる(というかマージできる)Orb を作ったので

bufferings.hatenablog.com

今日は、その Orb の裏側の「CUE で YAML をマージするときの動き」を確認してみるー!

CUE で YAML をマージする

こんなファイルがあるときに

foo.yml

sample:
  foo: "foo value"

bar.yml

sample:
  bar: "bar value"

こう書くと CUE で YAML をマージすることができる

❯ cue export foo.yml bar.yml --out yaml
sample:
  foo: foo value
  bar: bar value

--- を使って、↓こう書いても同じなので、今日の記事の中ではこの形式で書こうと思う

foobar.yml

sample:
  foo: "foo value"
---
sample:
  bar: "bar value"
❯ cue export foobar.yml --out yaml 
sample:
  foo: foo value
  bar: bar value

どうマージされるのか?

とてもシンプルで、これだけだった↓

  • YAMLYAMLのLeafまでのパス: 値 と捉えたときに、それらのすべてのLeafが任意の順番でマージされる
  • ただし、そのLeafに対する値がひとつに決まらなければエラーになる

注意:自分の頭の中の理解を自分の言葉で説明しているので、YAMLのLeafまでのパス: 値 とか Leaf は、正式に定義されている言葉じゃないです

YAMLLeafまでのパス?

こんな YAML があったときに

a: 3
b:
  c: "foo"

こんな風に考えることができる

a: 3
b: c: "foo"

この、a:b: c: のことを YAMLのLeafまでのパス と、この記事の中では呼ぶことにする。3"foo" がそのLeafの値。

パスが違う場合はマージされる

パスが違う場合はマージされる

a: 1
---
b: 2
❯ cue export foobar.yml --out yaml
a: 1
b: 2

階層構造を持っている場合でもパスがそれぞれ違うなら問題なくマージされる

a:
  b: "bbb"
  c: "ccc"
---
a:
  d: "ddd"
❯ cue export foobar.yml --out yaml
a:
  b: bbb
  d: ddd
  c: ccc

値がひとつに決まらなければエラーになる

複数の YAML に同じパスの Leaf が定義してあって、それらの値が異なる場合はエラーになる

a: 1
---
a: 2
❯ cue export foobar.yml --out yaml
a: conflicting values 2 and 1:
    ./foobar.yml:1:5
    ./foobar.yml:3:5

a: 1a: 2 で値がひとつに決まらないからエラーになる

階層構造を持っている場合も同じ

a:
  b: "bbb"
---
a:
  b: "ddd"
❯ cue export foobar.yml --out yaml
a.b: conflicting values "ddd" and "bbb":
    ./foobar.yml:2:7
    ./foobar.yml:5:7

a: b: の値がひとつに決まらなくてエラーになる

値がひとつに決まる場合はエラーにはならない

じゃあ、値がひとつに決まる場合はどうなるの?というと、ひとつの Leaf として出力される

a: 1
---
a: 1
❯ cue export foobar.yml --out yaml
a: 1

階層構造の場合も同じ

a:
  b: "bbb"
---
a:
  b: "bbb"
❯ cue export foobar.yml --out yaml
a:
  b: bbb

値がシーケンスの場合

これまではスカラー値だけを見てきたけど、シーケンス値の場合でも同じで、値が異なる場合にはエラーになるし

a:
  - 1
  - 2
---
a:
  - 1
❯ cue export foobar.yml --out yaml
a: incompatible list lengths (1 and 2)

同じ値ならエラーにはならない

a:
  - 1
  - 2
---
a:
  - 1
  - 2
❯ cue export foobar.yml --out yaml
a:
  - 1
  - 2

シーケンス値を縦に書くと少し分かりにくいので、こう書くと、値ということが分かりやすいかもしれない

a: [1, 2]
---
a: [1]
❯ cue export foobar.yml --out yaml
a: incompatible list lengths (1 and 2)

もちろん、シーケンスが別の Leaf の値であればマージされる

a:
  - 1
  - 2
---
b:
  - 1
❯ cue export foobar.yml --out yaml
a:
  - 1
  - 2
b:
  - 1

つまり「複数のファイルで、同じパスのLeafにそれぞれ異なるシーケンス値を定義していても、それらのシーケンスが結合されることはなく、エラーになる」

マージのルール(再掲)

ということで、とてもシンプルだった:

  • YAMLYAMLのLeafまでのパス: 値 と捉えたときに、それらのすべてのLeafが任意の順番でマージされる
  • ただし、そのLeafに対するスカラー値またはシーケンス値がひとつに決まらなければエラーになる

CircleCI の設定ファイル的にはどうなの?

CircleCI の設定ファイルを、最初に書いた Orb でマージするときにどうなるかというと、例えば↓こういうファイルがあるときに

version: 2.1

jobs:
  service1-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!1"

workflows:
  service1-say-hello-workflow:
    when: << pipeline.parameters.build-service1 >>
    jobs:
      - service1-say-hello
version: 2.1

jobs:
  service2-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!2"

workflows:
  service2-say-hello-workflow:
    when: << pipeline.parameters.build-service2 >>
    jobs:
      - service2-say-hello
  • version: は値がひとつに決まるのでその値が使用される
  • jobs: service1-say-hello:jobs: service2-say-hello: は別のノードになるので、それぞれが jobs の下にぶらさがる
  • workflows も同様

だからマージすると、こうなる

version: 2.1
jobs:
  service1-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!1
  service2-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!2
workflows:
  service1-say-hello-workflow:
    when: << pipeline.parameters.build-service1 >>
    jobs:
      - service1-say-hello
  service2-say-hello-workflow:
    when: << pipeline.parameters.build-service2 >>
    jobs:
      - service2-say-hello

同じ名前のジョブやワークフローを書いてしまった場合は?

例えば、間違って複数のファイルに同じ名前のワークフロー定義を書いてしまった場合は、大体の場合は jobs などの値が異なってエラーになると思う

service-say-hello-workflow:
  jobs:
    - service-say-hello
---
service-say-hello-workflow:
  jobs:
    - service-say-hello
    # jobs の内容が違う
    - notify-something

jobs のシーケンス値が異なるので、エラーになる

❯ cue export foobar.yml --out yaml
"service-say-hello-workflow".jobs: incompatible list lengths (1 and 2)

ただ、Leafの内容が全く同じで、別の要素が追加されている場合は、マージされるので注意が必要。↓こういう風に書いてしまった場合

service-say-hello-workflow:
  jobs:
    - service-say-hello
---
service-say-hello-workflow:
  # 要素を追加
  when: << pipeline.parameters.build-service1 >>
  jobs:
    - service-say-hello

マージされてしまう

❯ cue export foobar.yml --out yaml
service-say-hello-workflow:
  when: << pipeline.parameters.build-service1 >>
  jobs:
    - service-say-hello

CircleCI の設定ファイルを分割するときの基本的な方針

基本的な方針としては、各ファイルごとにネームスペース的なプレフィックスを付けるようにして、ジョブやワークフローの名前が重複しないようにするのが良さそう

どういう仕様で、こんな動きをするの?

CUE の仕様について触れる必要があるので、また次回にでもー!

2022-07-25 後編書いた

bufferings.hatenablog.com

CircleCI の大きな config.yml を分割しちゃおう!

config.yml を分割できる Orb を作ったよー

Split Config Orb という Orb を作った

こないだからちょこちょこ試してたやつを Orb にしたのだ。この Orb を使うと config.yml を分割できる。Orb にしたから簡単に使えるよー!

config.yml が大きいから分割したいー!って場合や、モノレポで複数サービスを入れてるから各サービスごとに config.yml を書きたい!って場合に使えるかなぁって思ってる

もしちょっとでも興味があったら、実際に使ってみてフィードバックをいただけると嬉しいです。フィードバックを元にして機能をブラッシュアップできるといいなと思ってます!GitHub の Issue でも Twitter でメンションくれても OK です!

じゃ、さっそく使ってみようー!

簡単に使えるよー

こんな風に config.yml を書いておくだけ!

version: 2.1

setup: true

orbs:
  split-config: bufferings/split-config@0.1.0

workflows:
  generate-config:
    jobs:
      - split-config/generate-config:
          find-config-regex: .*/\.circleci/config\.yml

これで CircleCI が起動したときに、ここに書いてある正規表現にマッチする設定ファイルをかき集めてきて、ひとつにマージしてから、パイプラインを実行するのだ!

あ、その前に、いっこだけやっとくことがあるんだった!

ダイナミックコンフィグを有効化してね

マージした設定ファイルを使ってパイプラインを起動するのに ダイナミックコンフィグ という機能を使ってるのだけど、デフォルトではオフになってるので この手順にしたがって オンにしといてください!

おわりー!これだけー!以下は、他にこんなことができるよーとかの紹介

集めてくるファイルの指定方法

ファイルを集めてくるのには、↑の例みたいに正規表現で指定することもできるし、「そんなに変更されるわけじゃないから固定リストで指定したいなぁ」って人は↓こんな感じでも大丈夫

version: 2.1

setup: true

orbs:
  # Please specify the latest version
  split-config: bufferings/split-config@1.2.3

workflows:
  generate-config:
    jobs:
      - split-config/generate-config:
          fixed-config-paths: |
            ./common/.circleci/config.yml
            ./service1/.circleci/config.yml
            ./service2/.circleci/config.yml
            ./service3/.circleci/config.yml

パスは、リポジトリのルートからの相対パスでお願いします!

path-filtering Orb との組み合わせ

path-filtering という Orb を使うと、例えば、モノレポで複数のサービスがひとつのリポジトリに入ってて「変更があったサービスのワークフローだけ実行したいのだ!」みたいなことが実現できる。この path-filtering と組み合わせて使う場合はこんな感じ

version: 2.1

setup: true

orbs:
  # Please specify the latest version
  split-config: bufferings/split-config@1.2.3
  path-filtering: circleci/path-filtering@1.2.3

workflows:
  generate-config:
    jobs:
      - split-config/generate-config:
          find-config-regex: .*/\.circleci/.*\.yml
          generated-config-path: /tmp/generated_config.yml
          continuation: false
          post-steps:
            - persist_to_workspace:
                root: /tmp
                paths:
                  - generated_config.yml
      - path-filtering/filter:
          workspace_path: /tmp
          config-path: /tmp/generated_config.yml
          mapping: |
            service1/.* build-service1 true
            service2/.* build-service2 true
          requires:
            - split-config/generate-config

やってるのは、マージした設定ファイルを path-filtering Orb に渡すってこと。ジョブ間の受け渡しになるので ワークスペース を使って渡してる

どうやってマージしてるの?

マージには CUE (https://cuelang.org/) を使ってる

CUE は設定用の言語みたいな感じで、型の定義だったり、モジュール化だったりと、色んなことができるんだけど、その中のほんとに一部の機能を利用してマージしてるのだ。使ってる機能は

  • YAML ファイルを読み込んで CUE 形式に変換する
  • 複数の CUE 形式のデータをマージする
  • CUE 形式のデータを YAML 形式で出力する

というもの。これで、複数の YAML を取り込んでマージされた YAML を出力してる

例えば、こんな YAML

version: 2.1

jobs:
  service1-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!1"

workflows:
  service1-say-hello-workflow:
    jobs:
      - service1-say-hello

こんな YAML

version: 2.1

jobs:
  service2-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: "Say hello"
          command: "echo Hello, World!1"

workflows:
  service2-say-hello-workflow:
    jobs:
      - service2-say-hello

マージすると、こうなる↓かしこいー!

❯ cue export service1.yml service2.yml --out yaml
version: 2.1
jobs:
  service1-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!1
  service2-say-hello:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Say hello
          command: echo Hello, World!1
workflows:
  service1-say-hello-workflow:
    jobs:
      - service1-say-hello
  service2-say-hello-workflow:
    jobs:
      - service2-say-hello

この CUE のマージの機能がかなり面白いので、興味ある方は、ぜひ CUE のドキュメントをチェックしてみてください!僕も、この辺りはもう少し細かく見ていきたいなって思ってる

制約

この Orb には、その仕組みに起因する制約があるので、注意してほしい

制約1: Workflow や Job の名前がかぶらないようにしないといけない

最終的には1つの YAML ファイルとして書き出す仕組みなので、名前が重複しちゃだめって制約がある

制約2: YAML のアンカーとエイリアスは、ひとつの YAML ファイル内で完結してる必要がある

CUE が YAML を読み込むときにアンカーとエイリアスを解析してしまうので、複数ファイルにまたがったアンカー・エイリアスは利用できない なので、あるファイルでアンカーを定義しておいて、別のファイルでエイリアスを使う、みたいなことはできない

興味がある人向け

この Orb は裏側で CUE を使ってはいるんだけど「使う人はできるだけ YAML のことしか考えなくていいようにしたい!」って思いながら作った

なので、YAML のことだけを考えてもらったら大丈夫なんだけど、実は CUE ファイルを渡しても読み込んでくれて、最終的には YAML で出力される。だから、実は CircleCI の設定を CUE で定義することもできてしまうのだー。こんなふうに↓

package config

version: "2.1"

jobs: {
    "service1-say-hello": {
        docker: [
            {image: "cimg/base:stable"},
        ]
        steps: [
            "checkout",
            {
                run: {
                    name:    "Say hello"
                    command: "echo Hello, World!1"
                }
            },
        ]
    }
}

workflows: {
    "service1-say-hello-workflow":
    {
        jobs: [
            "common-say-hello",
            "service1-say-hello",
        ]
    }
}

で、config.ymlcue ファイルを読み込むようにしてあげれば OK

version: 2.1

setup: true

orbs:
  # Please specify the latest version
  path-filtering: circleci/path-filtering@1.2.3

workflows:
  generate-config:
    jobs:
      - split-config/generate-config:
          find-config-regex: .*/\.circleci/config\.cue

CUE の機能を使って何か面白いことできないかなぁ?ってのにちょっとだけ興味あるから、これも遊んでみようかなぁって思ってる

Working Example

って、ここまで書いたようなことを、GitHub の README に書いてるからよかったらチェックしてみてくださいー

で、それぞれの例のところに、実際にそれを使った Example Project へのリンクがあるから、どんな感じか見てみたい方はどうぞ!

使ってみてー!

使ってみてくれたら嬉しいー!フィードバックくれたらもっと嬉しいー!

大阪のミートアップで喋りますー

来週の 7/27(水) に大阪でオフラインミートアップを開催する予定なんですけど、そこで、この Orb の紹介をしようと思いますー!よかったら来てください!(ただ、いま COVID-19 の感染者がどんどん増えている状況なので、中の人とよく相談して考えたいと思ってます

circleci.connpass.com

#furoshiki_fm の二人のすごいところ・違うところ・同じところ

なんと、もういっかい furoshiki_fm に行ってきました!わいわい

前回は自分がゲストとしてお話をさせてもらいました↓(まだの方は、ぜひ聴いてくださいー

bufferings.hatenablog.com

この記事の中で

僕の話よりも、ふたりの話をききたかった気持ちがある

って書いてたら「じゃあ、その話で、もっかいくる?」って声をかけてくれたのだ。なんとー!「えー!いいのー?ぜひー!」って行ってきました(実際はとても丁寧に声をかけてくださいましたw

ということで、今回は「僕がお二人に聞いてみたかったこと」をテーマにして参加させてもらいました

  • お二人の魅力
  • 何を大切にしているか
  • そのためにどういうことを意識しているか
  • お互いのことをどんな風に捉えているか

などなど、ふろしきのお二人の魅力の秘密をたくさん教えてもらって、なるほどー!そっかー!だからかー!ってなって、とても楽しかったです。ぜひ聴いて、感想を #furoshiki_fm つけてつぶやいてください!

やったー!