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にとぶことが可能」になります。

途中のCI情報、CD情報も知りたいかもしれませんがここでは主題ではないので省略します。*1

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

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

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

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

gozuk16.hatenablog.com

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

nowfromhome.com

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

blog.shibayan.jp

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

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 で書きましたがbashでもcsx でも pythonでも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:スクリプト使いたくないですね

*5:このためにT4したくないのは当然だと思います