開発中、リリースのいずれにおいても「今どのバージョンなのか」という情報は重要な情報です。 とはいえ、実際に埋め込みたいのはバージョンというより「ソースコード」とくに「コミット」と連動する情報、加えて「ビルド」と紐づく情報もほしいでしょう。 どのようにすれば実現できるか、これを.NET Coreをベースに考えてみましょう。
真新しいことはなく、よく昔からあるやつを今ならどうやるかというメモです。 前回の知識があれば簡単です。
- TL;DR;
- GitHub
- なぜSCM情報を埋め込みたいのか
- .NET Framework におけるSCM情報の埋め込み方法
- .NET Core でSCM情報をどこに仕込むと楽なのか
- GitVersioningを使ったGit情報埋め込み
- GitInfoを使ったGit情報埋め込み
- スクリプトでjsonを生成してDIする
TL;DR;
- shallow-cloneが不要なアプリケーションでは、GitVersioningがおすすめ(お任せできる)
- shallow-cloneが必要なアプリケーションでは、GitInfoがおすすめ(お任せできる)
- Git tagをベースにCIでバージョンを埋め込む方法もある(csprojの調整やビルドでの埋め込みが必要)
- ビルド時にスクリプトを実行して実行時にjsonを読み込みDIすることももちろん可能(生成スクリプト、型セーフにあつかうためのクラス、DIが必要)
- 独自にAssetmbly Attributeを埋めることもできる(csprojとの調整が必要)
GitHub
この記事の内容は、GitHubで公開しています。 実コードのサンプルが見たい場合に利用してください。
なぜ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
そこで、Visual Studioワークフローとも相性がいい .csprojにMSBuild taskでフックする方法がよく利用されます。*2
http://nowfromhome.com/msbuild-add-Git-commit-hash-to-assemblyinfonowfromhome.com
また、柔軟に埋め込み内容を調整できるためPreBuildでスクリプトを回してAssemblyInfo.csに書き込む方法もよく使われていました。*3
埋め込みはAssemblyInfo.csへのテンプレーティングにすぎないので、T4という手もありますがあまり使われません。*4
もちろん、適当は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を生成しようとすると属性の重複やあと勝ちにより意図しない挙動が生じます。
もう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を使っているプロジェクトでは利用できません。
バージョンのハンドルは、csprojのVersionではなくversion.json
による定義からの自動生成に任せましょう。
Nerdbank.GitVersioning/versionJson.md at master · AArnott/Nerdbank.GitVersioning
利用方法が微妙にわかりにくいため、使うにあたっての注意と.NET Coreでの利用方法をサンプルプロジェクトを使って説明します。
NuGet パッケージを使ってビルド時に自動的にバージョンを埋め込む
.NET Coreで使う場合、nbgv
という .NET Core Global ToolとNuGet Packageの2つの方法があります。
ただ「毎ビルドで自動的にバージョンを埋め込みたいだけ」ならNuGet Packageで十分です。 CLIはもう少し複雑な操作を自動化するのに使います。
dotnet add package Nerdbank.GitVersioning
あとは一度ビルドすると、アプリケーションからThisAssembly
というstatic class経由でアセンブリに埋め込まれたGit Version情報を実行時に参照可能です。
確認しましょう。
実行結果です。
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
CLIを使って、GitVersioningをプロジェクトに導入するにはinstall
コマンドを利用します。
nbgv install
Directory.Build.propsがあるとき、ここにパッケージをadd
として追加するので影響範囲が広がるため注意してください。
個別のプロジェクトフォルダでCLIを使ってinstallして影響範囲を狭めるといいでしょう。
あとは普通にdotnet buildをすると、assemblyinfo.csの生成をフックして、ビルドされたアセンブリにバージョンを埋め込んでくれます。
バージョンのフォーマットは、version.json
の定義に従うので必要に応じてビルド前に生成しましょう。
CLIを使うと、リリースブランチ戦略が簡単に自動化できます。
よくある、reease/v1.x.x
やv1.0.0
のようなブランチを切ってリリースしていく場合、prepare-release
を使うことで自動的にブランチを作りコミットしてくれます。
nbgv prepare-release
バージョン自動生成の裏側
裏側を説明します。NuGet Packageを追加すると、Nerdbank.GitVersioning.Tasks
というmsbuild taskが追加されます。
このタスクによって、AssemblyInfo.csではなくASSEMBLYNAME.AssemblyInfo.cs
とASSEMBLYNAME.Version.cs
をビルド前にobjフォルダへ生成します。
この中で重要なのが、ASSEMBLYNAME.Version.cs
です。中にThisAssembly
という静的クラスが書かれていることがわかります。
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
サンプルプロジェクトで見てみましょう。
NuGetパッケージの導入後、一度ビルドするとThisAssembly
経由でGitバージョン情報にアクセスできます。
出力結果です。
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はビルド時に自動生成されます。 しかしこの自動生成自体を止めたり、特定の属性の出力を止めることはできることを前回の記事でみました。
これを利用して、GitInfoのThisAssemblyを使ってアセンブリバージョンを埋め込んでみましょう。 AssemblyVersion、AssemblyFileVersion、AssemblyInformationalVersionの3つのバージョンがありますが、それぞれにバージョンを指定します。
csprojで重複する属性をAssetmblyInfoに自動生成しないことでビルドが通るようになります。
また、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だとちょっと埋め込み時に工夫がいるので素朴すぎ感があります。
実際に使っているリポジトリを見てみましょう。
csprojにVersion
プロパティ要素を用意しておくことで、dotnet buildでビルドするときに値を差し込むことができます。
あとは、ビルド時にプロパティを指定しましょう。
dotnet build -c Release -p:Version=${CIRCLE_TAG}
このようにすることで、VersionやGit SCMの情報を任意のプロパティに埋めることができます。
CircleCIの場合は、CIRCLE_SHA1
環境変数でSHA1を取り出せます。
スクリプトで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です。
これをビルド時に実行すればversion,json
がプロジェクト直下に配置されます。
ビルド時にjsonを一緒に配置
csprojをいじって、version.json
あったときは、ビルド時にコピーします。
普段version.json
はなくていいので、.gitignoreにしておくといいでしょう。
version.json
アプリ実行時にクラスにバインドする
型セーフに扱うため、マッピングするAppVersionクラスと、バインドを任せるAppVersionServiceを作ります。
あとは、ConfigureServicesメソッドのラムダ内やStartup.csでDIに登録することでViewや各箇所で呼び出すことができます。
DI経由で呼び出し
ViewにRazorで埋め込む場合は、@Inject
でDIからとってくることができます。
あとは、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" }
手間がかかるので幾分素朴すぎ感があります。 別にこんなことをしたくないという。