開発中、リリースのいずれにおいても「今どのバージョンなのか」という情報は重要な情報です。 とはいえ、実際に埋め込みたいのはバージョンというより「ソースコード」とくに「コミット」と連動する情報、加えて「ビルド」と紐づく情報もほしいでしょう。 どのようにすれば実現できるか、これを.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にとぶことが可能」になります。
途中のCI情報、CD情報も知りたいかもしれませんがここでは主題ではないので省略します。*1
.NET Framework におけるSCM情報の埋め込み方法
Golangを含め、各種言語にはその言語でやりやすいやり方があります。 .NET Core の場合を考える前に、過去、.NET Framework ではどうやっていたのかを振り返って、考え方の遷移をみてみます。
.NET Framework においては、 Properties/AssemblyInfo.cs
が常に存在したためここへの直接的なアプローチが多く使われていました。
しかしこのようにAssemblyInfo.cs を手で触るのは避けたいでしょう。*2
そこで、Visual Studio ワークフローとも相性がいい .csproj に MSBuild task でフックする方法がよく利用されていたように思います。*3
また、柔軟に埋め込み内容を調整できるためPreBuild でスクリプトを回して AssemblyInfo.cs に書き込む方法もよく使われていました。*4
埋め込みはAssemblyInfo.cs へのテンプレーティングにすぎないので、T4という手もありますがあまり使われてないように思います。*5
もちろん、適当は 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 で書きましたがbashでもcsx でも pythonでも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" }
手間がかかるので幾分素朴すぎ感があります。 別にこんなことをしたくないという。