Daggerを使ってNode.jsのHello worldをCIしてみる

## Daggerとは
CICDというとGithub Actions、CircleCI、AWS CodeBuildのようなクラウドサービスを利用するケースが多いと思いますが、Daggerは、ローカル環境でも動作させることができるCICDツールになります。Daggerを使う利点は以下のようなことがあげられます。

- ローカル開発で動かすことができるため、開発者が簡単に試すことができる
- CICDサービス上でも動作することができるため、Daggerで処理を共通化することでロックインを減らせます

https://github.com/dagger/dagger



## インストール

OSによって手順が違うので以下の公式ドキュメントを参考にインストールしてください。

https://docs.dagger.io/install/

## Hello worldで試してみる

早速、Hello worldでDaggerを体験してみましょう。今回使用する開発言語はNode.js(TypeScript)にします。Dagger自体は特定の開発言語に依存するツールではありません。公式でもjavaやgoのサンプルがありますので参考にしてください。

https://github.com/dagger/examples

今回の開発環境は以下の通りです。
- dagger: v0.2.36
- nodejs: v16.13.1
- typescript: 4.8.4
- jest: 29.2.0

### プロジェクト作成〜動くまで

以下のコマンドを実行し、プロジェクト作成を行います。

```bash
mkdir dagger-hello && cd dagger-hello
npm init -y
npm install -D typescript @types/node ts-node rimraf
npx tsc --init --rootDir src --outDir build
mkdir src && touch src/index.ts
```

src/index.tsに以下のコードを書きます。

```tsx:index.ts
export function hello() {
  console.log("Hello, world!");
}

hello();
```

package.jsonのスクリプト部分を以下のように修正します。

```json:package.json(抜粋)
"scripts": {
    "start:dev": "ts-node ./src/index.ts",
    "build": "rimraf ./build && tsc",
    "start": "node ./build/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
```

`npm run start:dev`で動かしましょう。Hello, world!が表示されれば成功です。

```bash
npm run start:dev                

> dagger-hello@1.0.0 start:dev
> ts-node ./src/index.ts

Hello world!
```

### テストを書く

せっかくCIを動かすのでテストを書きましょう。以下のコマンドで必要なライブラリのインストールと設定ファイルの作成を行います。

```bash
npm i jest @types/jest ts-jest -D
npx ts-jest config:init
mkdir tests && touch tests/index.test.ts
```

jest.config.jsに以下の記述を追加し、テストのルートディレクトリを指定します。

```js:jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
  roots: ["<rootDir>/tests"],
};
```

tsconfig.jsonに以下の記述を追加し、テストファイルをビルド対象外にします。

```json:tsconfig.json(抜粋)
{
  "compilerOptions": {
    ...
  },
  "exclude": ["tests/**/*.test.ts"]
}
```

tests/index.test.tsにテストを書きます。

```tsx:index.test.ts
import { hello } from "../src/index";

test("Test Hello, world!", () => {
  console.log = jest.fn();
  hello();
  expect(console.log).toHaveBeenCalledWith("Hello, world!");
});
```

package.jsonを書き換えて`npm run test`でテストが動くようにしましょう。

```json:package.json(抜粋)
"scripts": {
    "start:dev": "ts-node ./src/index.ts",
    "build": "tsc",
    "start": "node ./build/index.js",
    "test": "jest"
  },
```

`npm run test` を実行してテストが成功すればOKです。

```console
npm run test

> dagger-hello@1.0.0 test
> jest

  console.log
    Hello, world!

      at main (src/index.ts:2:11)

 PASS  tests/index.test.ts
  ✓ Test Hello, world! (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.291 s, estimated 2 s
Ran all test suites.
```

## daggerでCIする

以下のコマンドでdaggerの初期化を行います。

```console
dagger project init
dagger project update
```

dagger.cueファイルを作成し、以下の内容をコピペします。

```go
package main

import (
	"dagger.io/dagger"
	"dagger.io/dagger/core"
	"universe.dagger.io/bash"
	"universe.dagger.io/docker"
)

dagger.#Plan & {
	actions: {
		build: {
			// コードをチェックアウトします
			checkoutCode: core.#Source & {
				path: "."
			}
			// CIを動かすコンテナをpullします
			pull: docker.#Pull & {
				source: "node:lts"
			}
			// コンテナにコードをコピーします
			copy: docker.#Copy & {
				input:    pull.output
				contents: checkoutCode.output
			}
			// コンテナ内でモジュールをインストールします
			install: bash.#Run & {
				input: copy.output
				script: contents: """
					npm install
					# npm ci
					"""
			}
			// コンテナで実行するスクリプト
			buildTest: bash.#Run & {
				input: install.output
				script: contents: """
					npm run build
					npm run test
					"""
			}
		}
	}
}
```

`dagger do build` で実行します。全てにチェックがつけば成功です！

```console
dagger do build
[✔] actions.build.checkoutCode                                                                             40.7s
[✔] actions.build.pull                                                                                      2.9s
[✔] actions.build.install.script                                                                            0.3s
[✔] actions.build.buildTest.script                                                                          0.3s
[✔] actions.build.copy                                                                                     20.6s
[✔] actions.build.install                                                                                   3.0s
[✔] actions.build.buildTest                                                                                12.9s
```

## 感想

プロジェクト毎に使える環境やサービスが違うということもあるので、Daggerで共通化できるというのは魅力的に感じました。Github Actionsのプラグインのように色々なことができるのかというと分かりませんが、CIがこんな簡単に動くってのがテストコードを書くモチベーションアップにもつながりそうです。