github-release: CircleCI を触り始めて一週間で Orb を公開してみた話

この記事は [CircleCI Advent Calendar 2018](https://qiita.com/advent-calendar/2018/circleci) の 12 日目の記事です！（遅刻しました）

昨日は @nozo_moto さんの「[VueJS と Firebase と CircleCIで楽々CI CD環境作る話](https://qiita.com/nozo_moto/items/2030b412acf7ce68fc97)」でした。


## 経緯 ＆ やりたいこと

私の研究室で公開しているツール[^1] では CircleCI で自動テストを行っています。
どうせならテストだけでなく，ビルドして [GitHub Releases](https://help.github.com/articles/about-releases/) で公開するところまで CI にやらせようという話になり，CircleCI は未経験でしたが，勉強も兼ねて私が実装を担当することにしました。

[^1]: https://github.com/kusumotolab/kGenProg

実現方法を色々と調査しているうちに，CircleCI の以下の現状に気付きました：

- GitHub Releases を管理する統一的な方法が存在していないこと[^2]
- [Orb](https://circleci.com/docs/2.0/orb-intro/#section=configuration) という処理をモジュール化して共有する機能が最近利用可能になったこと
- まだ誰も GitHub Releases を管理する orb を作成・公開していないこと

[^2]: 自前で実装する方法については，CircleCI 公式ブログの次の記事で紹介されています：[Automate GitHub Releases with CircleCI](https://circleci.com/blog/publishing-to-github-releases-via-circleci/)

GitHub Releases の管理というコードは誰が書いても同じようなものになるはずであり，orb として共有べきものです。
まだ誰も作っていないなら，自分が作って公開してしまおう！と思い立ったのが経緯です。

この記事では，CircleCI を触り始めて一週間の素人が，orb を実装して公開するまでの軌跡をつらつらと書いておきます。
これから orb を作る人の参考になれば幸いです。

なお，今回は orb 内部でコマンドラインツールの [ghr](https://github.com/tcnksm/ghr) や [github-release](https://github.com/aktau/github-release) を利用しています。
どちらも CLI で GitHub Releases を操作することができる Go 製の OSS です。


### 作成した Orb

[h-matsuo/github-release](https://circleci.com/orbs/registry/orb/h-matsuo/github-release#usage-examples) という名前で orb registry に登録しています。
ソースコードは [GitHub](https://github.com/h-matsuo/circleci-orb-github-release) に上げています。


## Orb に実装する要素の洗い出し

[ドキュメント](https://circleci.com/docs/2.0/using-orbs/#section=configuration)によると，Orb には次の 3 つの要素を含めることができます：

- Executor
- Command
- Job


### Executor

**Executor** では CircleCI ジョブを実行する環境を定義できます。
CircleCI 2.1 で新しく追加された機能です。

今回作成する orb は内部で Go ツールを利用するため，Go がインストールされた環境で実行する必要があります。
`default` という名前の executor を定義しておくことで，ユーザに orb の実行環境を示唆できます。

```yml:orb.yml
executors:
  default:
    parameters:
      tag:
        description: Pick a specific circleci/golang image variant.
        type: string
        default: "latest"
    docker:
      - image: circleci/golang:<< parameters.tag >>
```

上記例では Go のバージョンをパラメータで指定できるようにしています。
デフォルトでは Docker イメージ `circleci/golang` の最新版が使われます。

これにより，ユーザは `<Orb名>/default` という名前で executor を指定することができるようになります：

```yml:.circleci/config.yml
orbs:
  github-release: h-matsuo/github-release@0.1.3
jobs:
  build:
    executor: github-release/default
```

`default` executor は [aws-cli](https://circleci.com/orbs/registry/orb/circleci/aws-cli) などの CircleCI 公式の orb のいくつかでも定義されていますので，実行環境を指定したい場合に有効でしょう。


### Command

CircleCI で実際に行う一連の処理（step）をまとめたものが **command** です。
よく使う，あるいは共通の処理を command として切り出すことで再利用が可能となります。
こちらも CircleCI 2.1 から追加されました。

Orb を作成する際は，ユーザに提供したい機能をその単位毎に command として実装するのが自然だと思います。

今回は GitHub Releases の作成および削除機能を提供するため，それぞれ `create` / `delete` という名前の command としてロジックを記述します。

```yml:orb.yml
commands:
  # ここでは delete コマンドのみ記載
  delete:
    description: Delete an existing release.
    parameters:
      # GitHub トークンや削除するタグ等のパラメータを指定できるようにする
      github-token-variable:
        description: Environment variable containing your GitHub personal access token.
        type: string
        default: $GITHUB_TOKEN
      tag:
        description: Git tag for the release.
        type: string
  steps:
    - internal__install-deps # github-release コマンドをインストール
    - run:
        name: "[github-release/delete] Delete the specified release"
        command: |
          github-release delete \
            --security-token << parameters.github-token-variable >> \
            --tag << parameters.tag >>
```

長くなるため詳細なソースコードは省略します（[GitHub](https://github.com/h-matsuo/circleci-orb-github-release/blob/master/src/orb.yml) をご覧ください）。


### Job

**Job** は上記 executor と command をまとめたものです。
つまり，CircleCI の処理を実行する環境と，実際の処理を定義したものが job になります。

実例として CircleCI の公式 orb の一つ [docker-publish](https://circleci.com/orbs/registry/orb/circleci/docker-publish) を見てみます。
この orb は Docker イメージのビルドやレジストリへのプッシュなどを機能として持ちます。
![docker-publish.jpg](https://qiita-image-store.s3.amazonaws.com/0/126427/1786118b-0ba3-7a39-daae-97235467038b.jpeg)
左メニューにこの orb で定義されている job や command の一覧が書いてあります。
ビルドやプッシュはそれぞれ `build` / `deploy` という command として実装されていますが，それらを一気に行う `publish` という job も定義されていることが分かります。

このように，orb を作成する際は，以下のような場合に job として提供してあげると便利です[^3] 。

- ある command を実行するにあたり必要な前処理・後処理がある
- 複数の command を決まった順序で呼び出すユースケースが存在する


[^3]: なお [公式ドキュメント](https://circleci.com/docs/2.0/creating-orbs/#designing-orbs) では，フレキシブルでなくなるため，orb に job のみを定義することは避けるように書かれています。

なお，今回作成した orb は非常にシンプルな機能しか持たないため，job は定義していません。


## ドキュメントの記述

第三者に利用してもらうためには，ドキュメントもしっかりと書くべきですね。


### Description の記述

Orb のソース中では，おおよそすべての要素に description を記述することができます。

```yml:orb.yml
description: |
  Create / delete GitHub's Releases.
  Issues & PRs: https://github.com/h-matsuo/circleci-orb-github-release
```

![orb-descriptions.jpg](https://qiita-image-store.s3.amazonaws.com/0/126427/6de2e28a-0372-da73-62df-5c4960fada8d.jpeg)

上記のような orb そのものの description 以外にも，executor / command / job はもちろん，各 parameter にも description を付けられます。


### Usage Examples の記述

ソースのトップレベルに `examples` として記述したものは，usage examples としてドキュメント化されます。

```yml:orb.yml
examples:
  delete:
    description: |
      Delete an existing release.
      Make sure you set $GITHUB_TOKEN to your GitHub personal access token in advance.
    usage:
      version: 2.1
      orbs:
        github-release: h-matsuo/github-release@0.1.1
      jobs:
        build:
          description: Delete the release tagged `vX.Y.Z`.
          executor: github-release/default
          steps:
            - github-release/delete:
                tag: vX.Y.Z
```

![usage-examples.jpg](https://qiita-image-store.s3.amazonaws.com/0/126427/566a5edf-0a41-7938-4a67-ba5452676a85.jpeg)
公式ドキュメントにはこれについての記述が見当たらなかったのですが，利用例があると格段に使いやすくなるので，できるだけ記述した方が良いでしょう。


## 完成 ＆ 公開

こうして完成したのが [h-matsuo/github-release](h-matsuo/github-release) という orb です（[GitHub](https://github.com/h-matsuo/circleci-orb-github-release)）。

例えば，Gradle プロジェクトでビルドした jar ファイルを GitHub Releases に登録する場合は次のように書けます：

```yml:.circleci/config.yml
# 注：予めリポジトリへの書き込み権限を持った GitHub Personal Access Token を
# CircleCI の環境変数 $GITHUB_TOKEN として登録しておく必要があります。

version: 2.1

# 利用する orb を指定する
orbs:
  github-release: h-matsuo/github-release@0.1.3

jobs:

  # `gradle build` で jar ファイル生成
  build:
    docker:
      - image: circleci/openjdk:8-jdk
    steps:
      - checkout
      - run: gradle build
      # 生成物を保存しておく
      - persist_to_workspace:
          root: .
          paths:
            - ./build/libs/product.jar

  # h-matsuo/github-release で GitHub Releases に公開
  release:
    executor: github-release/default
    steps:
      # 生成物を取り出す
      - attach_workspace:
          at: .
      # `create` command で公開する
      - github-release/create:
          tag: vX.Y.Z
          title: Version vX.Y.Z
          description: This release is version vX.Y.Z .
          file-path: ./build/libs/product.jar

workflows:
  version: 2
  build-and-release:
    jobs:
      - build
      - release:
          requires:
            - build
```

Orb の公開手順については，1 日目の @miyajan さんが [非常に詳しくまとめてくださっている](https://www.kaizenprogrammer.com/entry/2018/12/01/111145#Orbs-を公開する) のでここでは省略します。


## 困った点


### Private な要素を持てない

Orb に書いた executor / command / job は，すべて公開され orb のユーザが利用できます。
裏返せば，**private な command などを定義できない** ということです。

Orb 内部で繰り返し行う処理があるとき，それらも command としてまとめたくなります。
このような「orb の内部でしか使わず，ユーザからは直接使ってほしくないような処理」をまとめたいときに少し困りました。

結局，`internal__*` というように内部実装向けの command であることを明示する prefix を付けることでお茶を濁しましたが……。
Orb 内でしか使えない private な要素が持てるようになると，コードの見通しが良くなるように思います。


### Dynamic な Conditional Step がない

CircleCI は 2.1 から when / unless という step（[conditional steps](https://circleci.com/docs/2.0/reusing-config/#defining-conditional-steps)）が追加され，これにより条件分岐が可能となりました。

しかし，残念ながらこれはワークフローが実際に実行される前に評価される **静的なもの** であり，**実行時の動的な評価を必要とする条件分岐はできません**。

Orb のような汎用モジュールを作成する場合，より多くの状況やパターンに対応するため，前の step の結果に応じて次の step の処理を変えたり，条件分岐させたりしたくなります。
現状 CircleCI でそのような動的な分岐をするには，シェルスクリプト等でガリガリと書くしかありません。
またこの場合は処理を一つの step で完結させる必要があり，さらに他の orb や CircleCI のビルトインコマンドを用いることができないためコードの再利用性が低くなってしまいます。

Orb の開発側としては，[Ansible の playbook のような動的な条件分岐](https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html) ができるようになって欲しいですね。


## 気を付けたい点


### 一度公開した Orb は消せない

[公式ドキュメント](https://circleci.com/docs/2.0/creating-orbs/#deleting-production-orbs) にもあるように，一度 production orb として公開してしまうと，**原則取り下げることができません**。

GitHub 等ではうっかり credentials を push してしまっても，force push したりリポジトリそのものを非公開にしたりすることで取り下げが可能ですが，CircleCI の orb ではそれができません。

自分が利用している orb がある日使えなくなるのは信頼性に関わるため，orb が消せないという仕様自体は良いと思います。
しかし，ついうっかり credentials を上げてクラウド破産したりしてしまわないように注意が必要です。


## おまけ：CircleCI CLI にコントリビュートした話

作成した orb の自動リリース用設定ファイルを書いている途中，`circleci` コマンドにバグを発見しました（[issue #213](https://github.com/CircleCI-Public/circleci-cli/issues/213)）。

[ドキュメント](https://circleci.com/docs/2.0/creating-orbs/#development-and-production-orbs-versioning-semantics) によると，スラッシュ（`/`）も orb の development バージョンとして有効な文字に含まれているはずなのですが，実際にスラッシュを含むバージョン名で `circleci publish` すると怒られるというバグでした：

```sh
# dev:myinitials/mybranch というバージョン名で publish
$ circleci orb publish src/orb.yml hoge/fuga@dev:myinitials/mybranch
Error: Invalid orb reference 'hoge/fuga@dev:myinitials/mybranch': Expected a namespace, orb and version in the format 'namespace/orb@version'
```

コマンドのソースコードを見てみると，バージョン名のバリデーション処理に余計な部分があり，そこを削除するだけで問題なく動くことが分かりました。

そこでプルリクエストを出してみた（[#214](https://github.com/CircleCI-Public/circleci-cli/pull/214)）ところ，めでたくマージされた……という話でした。

コミット内容もたった 3 行削除しただけなので大したことないのですが，OSS にコントリビュートした経験があまりなかったため非常に嬉しかったです。

これを機に，来年は有名な OSS へもどんどんコントリビュートしていけたらいいなと思っています。


---


明日は @monkut さんの担当です。よろしくお願いします！