埼玉大学の研究室における JuliaLang 向け GitLab CI の整備をした話

# 本日は

Julia アドベントカレンダー $N=13$ 日目の記事です．

https://qiita.com/advent-calendar/2023/julia

GitLab で開発をする際に用いる CI ファイル `.gitlab-ci.yml` を整備した話を書こうと思います．GitHub Actions の `.github/workflows/CI.yml` に対応するものです．

## 記事を書くことになった背景

タイトルにある大学の研究室というのは 品岡 寛 先生が運営する研究室のことです．研究室の概要は下記のリンクからどうぞ．

https://shinaoka.github.io/

> 量子多体理論から量子/古典情報理論を駆使して、物性物理向けの計算物理学的手法を開発しています。

私は Julia などのソフトウェアに関する業務をしております．研究室が管理している一部のソースコードは諸般の事情により GitLab で管理されています．複数の Julia で書かれたプライベートリポジトリが格納されています．

業務の一環として CI(継続的インテグレーション) を行うファイルの整備（俗にいう盆栽）から始めました．MR（マージリクエスト）を出した時に自動テストができるようにする仕組みを実現できます．ローカル環境では `Pkg.test()` は通るのですが下記のような項目は手が追いついてない状態でした．

- GitLab Runner のところで本質的でない箇所の失敗
    - 例えばプライベートリポジトリを管理するレジストリのクローンや依存関係の取得の失敗
- `Documenter.jl` によって生成されたドキュメントのデプロイなど

この記事を執筆してる現時点では main ブランチはテストがパスをしているグリーンのバッジが見えているはずですし，ドキュメントも研究室関連のメンバーからは見られるようになっているはずです．（研究室の皆様，不備があれば専用のチャンネルでメンションくださいまし．）

### 誰か知見書かない？（書くことになりました）

今月はアドカレシーズンですし，下記のようにディレクトリ構成の紹介がありました．

https://zenn.dev/hyrodium/articles/2320ce7ce6b8e8

記事に最後には以下のことが書かれています:

> 本記事で列挙したもの以外にも色々なパッケージ構成があると思います。
> たとえば、以下の項目には意図的に触れませんでした。
> 一つのリポジトリで複数のパッケージを管理する方法
> Documenter.jl以外でのドキュメント生成
> GitLabを使った場合のディレクトリ構成
>「このファイルも便利だから用意した方が良いよ！」などあればコメントください！

「GitLab で運営してる知見せっかくなので誰か書きません？」と提案したところ作業者本人（私）が適任だろうということで書くことになりました．やったね！執筆ネタが一つ増えたよ！

## プライベートな環境で CI を回すために必要なこと

### レジストリのセットアップ

自分自身のコードで研究・仕事が閉じれば楽ですが，世の中そうはいきません．どんどん複雑になっていきますし，規模もどんどん膨れ上がっていきます．他の開発したパッケージを利用し研究を進めるのが普通になってきているでしょう．


研究室の GitLab で管理しているコードは使い捨てのものではなく，共同研究者や学生などが依存パッケージとして用いている程度の汎用的なコードが管理されています．民間企業で言えば社員向けの共通ライブラリに相当します．パッケージは LocalRegistry.jl から作られたレジストリに必要な情報を登録し，利用者はそれを追加すれば安定版のパッケージを利用できるようになります．レジストリの追加は次のようにします．

```julia
julia> # 学生や研究者が行う手順
julia> ] # Pkg REPL に移動
pkg> registry add git@gitlab.com:<研究室の組織名>/<研究室が管理するレジストリ>.git # 初回だけ
```

これは研究室に入った初日にすれば良いです．以後は OSS パッケージと同様にプライベートリポジトリで管理されているパッケージを導入できます．

```julia
julia> # 先輩からこのパッケージを入れろと言われたのでとりあえず入れる
julia> ]
pkg> add PrivatePackage
julia> using PrivatePackage
julia> # おお，なんかわかんないけれど動いた
```

LocalRegistry.jl については過去に書いた記事が参考になると思います．

https://zenn.dev/terasakisatoshi/articles/73d23fd9cf4851

### 開発者がすること

利用するだけなら上記の作業でOKですが，CI を整えたりする場合は GitLab の `.gitlab-ci.yml` を整えるなど盆栽が必要です．CI を実行する GitLab Runner は素朴な環境からスタートします．素朴な環境に

- 必要なコマンドを導入 (`git` コマンドとか)
- プライベートリポジトリの取得ができるようにする
- セットアップ（レジストリが導入できるようにする）
- パッケージのテスト
- main ブランチが更新されたらドキュメントのデプロイ

などの事柄を GitLab Runner の環境で整える必要があります．GitLab Runner はデフォルトだと `.gitlab-ci.yml` に書いてあることに従って動作するので人間は `.gitlab-ci.yml` のお作法を知る必要があります．

## 作った `.gitlab-ci.yml` について

`開発者がすること` で書いた事柄を `.gitlab-ci.yml` に落とし込むと次のようになります．

```yml
before_script:
  - which git || (apt-get update -qq && apt-get install --no-install-recommends -qqqy git)
  - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "git@gitlab.com:"
  - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/" --add
  - |
      julia -e '
      using Pkg
      Pkg.Registry.add(
        RegistrySpec(url = "https://github.com/JuliaRegistries/General.git")
      )

variables:
  CI_JULIA_CACHE_DIR: ${CI_PROJECT_DIR}/julia_pkg
  JULIA_DEPOT_PATH: ${CI_JULIA_CACHE_DIR}
cache:
  key:
    files:
      - Project.toml
      - docs/Project.toml
    prefix: ${CI_JOB_NAME}
  paths:
    - ${CI_JULIA_CACHE_DIR}

.script:
  script:
    - |
      julia --project=@. -e '
        using Pkg
        Pkg.build()
        Pkg.test(coverage=true)'
.coverage:
  coverage: /Test coverage (\d+\.\d+%)/
  after_script:
    - |
      julia -e '
        using Pkg
        Pkg.add("Coverage")
        using Coverage
        c, t = get_summary(process_folder())
        using Printf
        @printf "Test coverage %.2f%%\n" 100c / t'
Julia 1.9:
  image: julia:1.9
  extends:
    - .script
    - .coverage
.doctest:
  script:
    - |
      julia --project=docs -e '
        using Pkg
        Pkg.develop(PackageSpec(path=pwd()))
        Pkg.instantiate()
        using Documenter: doctest
        using PrivatePackage # PrivatePackage は各自のパッケージ名とする
        doctest(PrivatePackage) # PrivatePackage は各自のパッケージ名とする
        include("docs/make.jl")'
doctest:
  image: julia:1.6
  extends:
    - .doctest
pages:
  image: julia:1.6
  stage: deploy
  extends:
    - .doctest
  after_script:
    - mkdir -p public
    - mv docs/build public/dev
  artifacts:
    paths:
      - public
  only:
    - main
```

## やってること

### `before_script` に関して

`before_script` は `doctest`, `pages`, `Julia 1.9` という名前で指定したジョブが実行される前に行われるコマンドを指定します．下記のリンクの `ianfiske` さんのコメントがとても役に立ちました．

https://discourse.julialang.org/t/use-private-registry-in-gitlab-ci-job/36179

例えば `Julia 1.9` という名前のジョブは `image: julia:1.9` で指定しているように Julia 1.9 の環境からスタートします．これは Docker イメージとして配布されている `julia:1.9` のことです．このイメージ内には `git` コマンドがないのでそれを入れています．

`CI_JOB_TOKEN` を利用しプライベートリポジトリにアクセスします．パッケージのインストール実行時には `git clone git@gitlab.com:<組織名>/<パッケージ名>.git` のような処理が内部で動きます．`git` の機能としてある [`insteadOf`](https://git-scm.com/docs/git-config) を利用すると `git clone git@gitlab.com:<組織名>/<パッケージ名>.git` の処理を `git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/<組織名>/<パッケージ名>.git` のように置換してくれます．

`insteadOf` についての日本語の解説は `カック (id:kakku22)` さんのブログが参考になります．

https://kakakakakku.hatenablog.com/entry/2020/03/27/093150

### `cache` に関して

Julia のパッケージを素朴な環境でインストールするとプレコンパイル作業が走ります．依存関係の多くなるとその分プレコンパイルの時間がかかるのでテストを実行するまでに数分待たされるということが起きます．これはストレスが溜まるのでキャッシュの機構を使いましょう．

https://docs.gitlab.com/ee/ci/caching/

```yml
variables:
  CI_JULIA_CACHE_DIR: ${CI_PROJECT_DIR}/julia_pkg
  JULIA_DEPOT_PATH: ${CI_JULIA_CACHE_DIR}
cache:
  key:
    files:
      - Project.toml
      - docs/Project.toml
    prefix: ${CI_JOB_NAME}
  paths:
    - ${CI_JULIA_CACHE_DIR}
```    

`key` で指定している `Project.toml` や `docs/Project.toml` のファイルに変更がなかったら `CI_JULIA_CACHE_DIR` の値で指定しているディレクトリの内容を再利用することができます．Julia の依存関係はデフォルトだと `~/.julia` に入ります．ところが GitLab のキャッシュの仕様によって `CI_PROJECT_DIR` 以外の場所のディレクトリはキャッシュできないらしいです．

> Both artifacts and caches define their paths relative to the project directory, and can’t link to files outside it.

そこで `JULIA_DEPOT_PATH` で `~/.julia` 以下に入る依存関係を制御します．`JULIA_DEPOT_PATH` は下記のドキュメントを参考にしてください．

https://docs.julialang.org/en/v1/manual/environment-variables/#JULIA_DEPOT_PATH

この機構を使うことで 7から9 分 かかっていたジョブが 2から3 分まで短縮することができています．

### `extends` について

Julia 1.9 を用いた `Pkg.test()` は次のようにして実現できます．

```yml
Julia 1.9:
  image: julia:1.9
  extends:
    - .script
    - .coverage
```

`extends` にある `.script` や `.coverage` によって `script` や `after_script` の処理を委譲しています．

```yml
.script:
  script:
    - |
      julia --project=@. -e '
        using Pkg
        Pkg.build()
        Pkg.test(coverage=true)'
.coverage:
  coverage: /Test coverage (\d+\.\d+%)/
  after_script:
    - |
      julia -e '
        using Pkg
        Pkg.add("Coverage")
        using Coverage
        c, t = get_summary(process_folder())
        using Printf
        @printf "Test coverage %.2f%%\n" 100c / t'
```

`.doctest` を拡張させることで `doctest`, `pages` ジョブ内で実行される `doctest(PrivatePackage)` などの実行スクリプトを使い回すことができます．

### 複数バージョンの Julia でテストをしたい

先ほどの例では `Julia 1.9` という名前のジョブで Julia 1.9 の環境でテストをさせるように指示しています．複数のバージョンでテストをしたい場合は次のように `parallel` を使うことができます:

```yml
julia test:
  image: julia:${JULIA_VERSION}
  extends:
    - .script
    - .coverage
  parallel:
    matrix:
      - JULIA_VERSION:
        - "1.6"
        - "1.7"
        - "1.8"
        - "1.9"
```

参考リンク

https://docs.gitlab.com/ee/ci/yaml/#needsparallelmatrix

---

以上で Julia 向けの CI を設定した話を終わります．おまけで下記の3つを追加します．

## トラブルシューティング

`CI_JOB_TOKEN` を利用しているのに特定のプロジェクト（リポジトリ）にアクセスできない.

例 パッケージ A のテストの時にパッケージ B が必要．Bの資源にアクセスできない．

そういう時は下記のリンクを参考にすると良いです．

https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#configure-the-job-token-scope

```
To configure the job token scope:

On the left sidebar, select Search or go to and find your project.
Select Settings > CI/CD.
Expand Token Access.
Toggle Limit access from this project to enabled.
Optional. Add existing projects to the token’s access scope. The user adding a project must have the Maintainer role in both projects.
```

上記の指示に従い B 側の設定画面に移動します．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/6c707091-142e-24cb-7da0-3669202d86d0.png)

`Add project` というところに A 側のプロジェクトを追加します．これで CI を再実行するとうまくいくはずです．

知らないとハマるのでおさえておきましょう．

## コマンド

CLI で CI の再実行，実行中の様子をターミナル確認できます．

https://gitlab.com/gitlab-org/cli

`glab` コマンドをインストール・ローカル環境で認証作業をすると使えます．

```console
$ glab ci trace
```

## VS Code のエクステンション

CI の様子や Issue などを確認することができます．VS Codeユーザならおすすめですね．

https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow

# まとめ

埼玉大学の研究室の GitLab CI 環境を整備しました．Julia + GitLab に関する知見は自分自身も含めあまりなかったのでこのような形で公開できたのは良かったです．品岡研究室の皆さんありがとうございます．

https://shinaoka.github.io/