tech.guitarrapc.cóm

Technical updates

.NET Core でgitなどSCM情報をCIで埋め込んでアプリケーションに表示する方法と選択

開発中、リリースのいずれにおいても「今どのバージョンなのか」という情報は重要な情報です。 とはいえ、実際に埋め込みたいのはバージョンというより「ソースコード」とくに「コミット」と連動する情報、加えて「ビルド」と紐づく情報もほしいでしょう。 どのようにすれば実現できるか、これを.NET Coreをベースに考えてみましょう。

真新しいことはなく、よく昔からあるやつを今ならどうやるかというメモです。 前回の知識があれば簡単です。

TL;DR;

  • shallow-cloneが不要なアプリケーションでは、GitVersioningがおすすめ(お任せできる)
  • shallow-cloneが必要なアプリケーションでは、GitInfoがおすすめ(お任せできる)
  • Git tagをベースにCIでバージョンを埋め込む方法もある(csprojの調整やビルドでの埋め込みが必要)
  • ビルド時にスクリプトを実行して実行時にjsonを読み込みDIすることももちろん可能(生成スクリプト、型セーフにあつかうためのクラス、DIが必要)
  • 独自にAssetmbly Attributeを埋めることもできる(csprojとの調整が必要)

GitHub

この記事の内容は、GitHubで公開しています。 実コードのサンプルが見たい場合に利用してください。

github.com

なぜSCM情報を埋め込みたいのか

CI/CDを組むと、継続的にアプリケーションが展開されるようになります。 継続的にアプリケーションが展開されるとき、SCM / CI / CD / Applicationの4か所を通りますが、普段意識するのは末端となるSCMとApplicationの2か所でしょう。

結果、「Git commitをしたらアプリケーションが展開される。動いているアプリケーションをみたらGit Commit情報が知りたい」となるでしょう。 今動いているアプリケーションが「どのコミットによるものなのか/PRによるものなのか」を「Git、Build、Deployの順にたどることなく、アプリケーションをみるだけでGitにとぶ」ことが可能になります。

.NET Framework におけるSCM情報の埋め込み方法

Golangを含め、各種言語にはその言語でやりやすいやり方があります。 .NET Coreの場合を考える前に、過去、.NET Frameworkではどうやっていたのかを振り返って、考え方の遷移をみてみます。

.NET Frameworkにおいては、 Properties/AssemblyInfo.csが常に存在したためここへの直接的なアプローチが多く使われていました。

しかしこのようにAssemblyInfo.csを手で触るのは避けたいでしょう。*1

gozuk16.hatenablog.com

そこで、Visual Studioワークフローとも相性がいい .csprojにMSBuild taskでフックする方法がよく利用されます。*2

http://nowfromhome.com/msbuild-add-Git-commit-hash-to-assemblyinfonowfromhome.com

また、柔軟に埋め込み内容を調整できるためPreBuildでスクリプトを回してAssemblyInfo.csに書き込む方法もよく使われていました。*3

blog.shibayan.jp

埋め込みはAssemblyInfo.csへのテンプレーティングにすぎないので、T4という手もありますがあまり使われません。*4

qiita.com

もちろん、適当はpublic staticなプロパティを用意しておいてsedをかませるのでもいいでしょう。

いずれにしても、「各種方法で特定の値をAssemblyInfo.csに埋め込み、ビルドを通してAssemblyのInformationalVersionに埋め込む、アプリの実行時はアセンブリ自身から情報を拾っていた」というのが大筋の流れです。

これらは .NET Coreでも筋は同じですが、微妙に事情が変わります。

.NET Core でSCM情報をどこに仕込むと楽なのか

.NET Coreで大きく変わったことは2つあります。

1つは.csprojがSDKベースのフォーマットになったことです。 前回の記事で見た通りProperties/AssemblyInfo.csがビルド時に自動生成されるため、バージョンを埋め込む時に今までとは違った考慮が必要になりました。 何も考えずに.NET Frameworkと同様にAssetblyInfo.csを生成しようとすると属性の重複やあと勝ちにより意図しない挙動が生じます。

tech.guitarrapc.com

もう1つの転換が、WebHost/GenericHostとConfiguration ProviderとDIのフレームワーク化です。 .NET Coreでは、.NET Frameworkでつかわれていたapp.configの仕組みから、xml/json/引数/環境変数など各種読込先を個別に読み込む仕組みに変わりました。 また、WebHost/GenericHostにより任意のファイルからタイプセーフにクラスマッピングし、DIで各種処理へ差し込むことも容易にできるようになっています。

以上の2点から、.NET Coreでバージョンを仕込むには2つの方法がよく利用されています。

  • AssemblyInfoに差し込まれる仕組みをフックする
  • jsonなどを生成して、DIを経由してランタイムで読み込む

これを前提情報に、バージョンの埋め込みを見ていきましょう。

GitVersioningを使ったGit情報埋め込み

Git情報を埋め込んだり、アプリのバージョンを自動でやってほしいときは、Nerdbank.GitVersioningが使いやすいでしょう。

項目 情報
GitHub AArnott/Nerdbank.GitVersioning
アセンブリバージョンの生成方法 自動(Git Height) + CLI引数
SCM 情報の取得 可能
バージョンフォーマットの指定 version.json
shallow clone での利用 ×

このライブラリは、.NET以外にもNodeでも使え、VSIXでも埋め込みに利用できます。

このライブラリはとても使いやすいのですが、2つ注意がいります。 Git heightを利用しているため、実行時に全コミット履歴を辿ります。そのため、shallow cloneと共存が不可能です。shallow cloneを使っているプロジェクトでは利用できません。

github.com

バージョンのハンドルは、csprojのVersionではなくversion.jsonによる定義からの自動生成に任せましょう。

Nerdbank.GitVersioning/versionJson.md at master · AArnott/Nerdbank.GitVersioning

利用方法が微妙にわかりにくいため、使うにあたっての注意と.NET Coreでの利用方法をサンプルプロジェクトを使って説明します。

github.com

NuGet パッケージを使ってビルド時に自動的にバージョンを埋め込む

.NET Coreで使う場合、nbgvという .NET Core Global ToolとNuGet Packageの2つの方法があります。

ただ「毎ビルドで自動的にバージョンを埋め込みたいだけ」ならNuGet Packageで十分です。 CLIはもう少し複雑な操作を自動化するのに使います。

dotnet add package Nerdbank.GitVersioning

www.nuget.org

あとは一度ビルドすると、アプリケーションからThisAssemblyというstatic class経由でアセンブリに埋め込まれたGit Version情報を実行時に参照可能です。 確認しましょう。

gist.github.com

実行結果です。

AssemblyConfiguration: Debug
AssemblyFileVersion: 0.0.10.14829
AssemblyInformationalVersion: 0.0.10+ed39ef6655
AssemblyName: NerdGitVersioningConsole
AssemblyTitle: NerdGitVersioningConsole
AssemblyVersion: 0.0.0.0
GitCommitId: ed39ef6655ebc044d8925f8c62aa09a4ceb0ea8c
RootNamespace: NerdGitVersioningConsole

バージョン書式はVersion.jsonで調整できるので、リファレンスみつつ適当にやるかお任せするといいでしょう。 あんまり頑張ろうとするとつらくなります。

ほかにもいくつかのSaaS型CIのビルド情報からバージョンを埋める機能もありますが、CircleCiはありません。

Nerdbank.GitVersioning/cloudbuild.md at master · AArnott/Nerdbank.GitVersioning

nbgv CLI を使ってバージョンを埋め込む

CLIを使うと、プロジェクトにNuGet Packageを適用することと、リリース用ブランチを切ってコミットさせることが簡単に自動化できます。 CIでGitVersioningを動的に導入してバージョンをはかせるときはCLIが使いやすいでしょう。 もしプロジェクトにNuGetパッケージを追加してコミットしてよく、リリースブランチ戦略も取ってないならCLIは不要です。

CLIは .NET Global Toolなので、dotnet sdkが入っていればコマンド1つでCLIを利用可能になります。

dotnet tool install -g nbgv

www.nuget.org

CLIを使って、GitVersioningをプロジェクトに導入するにはinstallコマンドを利用します。

nbgv install

Directory.Build.propsがあるとき、ここにパッケージをaddとして追加するので影響範囲が広がるため注意してください。 個別のプロジェクトフォルダでCLIを使ってinstallして影響範囲を狭めるといいでしょう。

あとは普通にdotnet buildをすると、assemblyinfo.csの生成をフックして、ビルドされたアセンブリにバージョンを埋め込んでくれます。 バージョンのフォーマットは、version.jsonの定義に従うので必要に応じてビルド前に生成しましょう。

CLIを使うと、リリースブランチ戦略が簡単に自動化できます。 よくある、reease/v1.x.xv1.0.0のようなブランチを切ってリリースしていく場合、prepare-releaseを使うことで自動的にブランチを作りコミットしてくれます。

nbgv prepare-release

バージョン自動生成の裏側

裏側を説明します。NuGet Packageを追加すると、Nerdbank.GitVersioning.Tasksというmsbuild taskが追加されます。 このタスクによって、AssemblyInfo.csではなくASSEMBLYNAME.AssemblyInfo.csASSEMBLYNAME.Version.csをビルド前にobjフォルダへ生成します。

この中で重要なのが、ASSEMBLYNAME.Version.csです。中にThisAssemblyという静的クラスが書かれていることがわかります。

gist.github.com

GitInfoを使ったGit情報埋め込み

ただGit情報を埋め込みたいだけの場合、GitInfoはNerdbank.GitVersioningよりもシンプルにやりたいことをやってくれます。

項目 情報
GitHub kzu/GitInfo: Git and SemVer Info from MSBuild, C# and VB
アセンブリバージョンの生成方法 Assetmbly attribute で自分で指定可能
SCM 情報の取得 可能
バージョンフォーマットの指定 GitInfo.txt
shallow clone での利用

Gitの情報を拾ってきて埋めるだけなので、シンプルにできているのが最大のメリットです。

シンプルに利用する

ただGit情報を参照するだけならNuGet Packageを導入するだけでできます。

dotnet add package Nerdbank.GitInfo

サンプルプロジェクトで見てみましょう。

github.com

NuGetパッケージの導入後、一度ビルドするとThisAssembly経由でGitバージョン情報にアクセスできます。

gist.github.com

出力結果です。

Branch: master
BaseTag:
Commit: ed39ef6
Commits: 10
IsDirty: True
Sha: ed39ef6655ebc044d8925f8c62aa09a4ceb0ea8c
Tag:
Major: 0
Minor: 0
Patch: 0
DashLabel:
Label:
Major: 0
Minor: 0
Patch: 10
Source: Default

特徴的なのが、リポジトリのコミット総数を埋め込んでおりSemVerのPatchバージョンにこれを埋め込みます。 また、ファイルバージョンは何もしません。

バージョンの埋め込みも行う

.NET CoreでAssemblyInfo.csはビルド時に自動生成されます。 しかしこの自動生成自体を止めたり、特定の属性の出力を止めることはできることを前回の記事でみました。

tech.guitarrapc.com

これを利用して、GitInfoのThisAssemblyを使ってアセンブリバージョンを埋め込んでみましょう。 AssemblyVersion、AssemblyFileVersion、AssemblyInformationalVersionの3つのバージョンがありますが、それぞれにバージョンを指定します。

gist.github.com

csprojで重複する属性をAssetmblyInfoに自動生成しないことでビルドが通るようになります。

gist.github.com

また、GitInfoはこのままではFileVersionは0.0.0なので、GitInfo.txtをプロジェクトの同一階層においてバージョンを指定します。

0.0.1

実行してみると意図したとおりにバージョンが書き込まれていることがわかります。

Branch: master
BaseTag:
Commit: ed39ef6
Commits: 1
IsDirty: True
Sha: ed39ef6655ebc044d8925f8c62aa09a4ceb0ea8c
Tag:
Major: 0
Minor: 0
Patch: 1
DashLabel:
Label:
Major: 0
Minor: 0
Patch: 2
Source: File
assemblyVersion: 0.0.1.0
fileVersion: 0.0.2
productVersion: 0.0.1+ed39ef6

SourceがFileになっており、GitInfoが読み込まれたとわかります。

Git tag をベースに埋め込む

この方法は、アセンブリやNuGetパッケージのバージョンを指定するのに最も簡素な方法の1つです。 SCMだとちょっと埋め込み時に工夫がいるので素朴すぎ感があります。

実際に使っているリポジトリを見てみましょう。

github.com

csprojにVersionプロパティ要素を用意しておくことで、dotnet buildでビルドするときに値を差し込むことができます。

gist.github.com

あとは、ビルド時にプロパティを指定しましょう。

dotnet build -c Release -p:Version=${CIRCLE_TAG}

このようにすることで、VersionやGit SCMの情報を任意のプロパティに埋めることができます。 CircleCIの場合は、CIRCLE_SHA1環境変数でSHA1を取り出せます。

Using Environment Variables - CircleCI

スクリプトでjsonを生成してDIする

csprojに頼らずバージョンを指定したいときには、.NET Coreがjsonなど任意のファイルをコンフィグとして読み込み、DIで指せることが利用できます。 流れは単純です。

  • scm情報をCIでjsonに吐き出し
  • ビルド時にjsonを一緒に配置
  • アプリ実行時にDI
  • DI経由で呼び出し

scm情報をCIでjsonに吐き出し

プロジェクトに次のSet-GitAppVersion.ps1スクリプトをContentRoot直下に突っ込みます。 これは、CIでスクリプトを実行してSCMの情報をもったjsonを作るコマンドを並べただけです。 仮にPowerShellで書きましたがjson作ればなんでもokです。

gist.github.com

これをビルド時に実行すればversion,jsonがプロジェクト直下に配置されます。

ビルド時にjsonを一緒に配置

csprojをいじって、version.jsonあったときは、ビルド時にコピーします。

gist.github.com

普段version.jsonはなくていいので、.gitignoreにしておくといいでしょう。

version.json

アプリ実行時にクラスにバインドする

型セーフに扱うため、マッピングするAppVersionクラスと、バインドを任せるAppVersionServiceを作ります。

gist.github.com

あとは、ConfigureServicesメソッドのラムダ内やStartup.csでDIに登録することでViewや各箇所で呼び出すことができます。

gist.github.com

DI経由で呼び出し

ViewにRazorで埋め込む場合は、@InjectでDIからとってくることができます。

gist.github.com

あとは、metaのhtml5で紹介されたdata-* を使ったり

    <meta name="application-name" content="Nextscape.Holojector.AssetGenerator.Web" data-version="@shortHash" data-deployment="@lastUpdate" />

jsonブロックを吐き出してもいいでしょう。

    <script id="version" type="application/json">
        {
        "ShortHash": "@shortHash",
        "LastUpdate": "@lastUpdate"
        }

手間がかかるので幾分素朴すぎ感があります。 別にこんなことをしたくないという。

*1:手はやりたくない筆頭

*2:いわゆるド定番

*3:スクリプト使いたくないですね

*4:このためにT4したくないのは当然