概要
これはもともとあったMVVMなWPFアプリに自動テストとDependency-Injection(DI)とCI/CDを実装してみた記録です。変更点はWPFとはあまり関係ないので、C#であれば他のフレームワークにも参考になるかもしれません。
前回は自動テストとDIの追加までやりました。
今回はGitHubActionsを使ってCI/CDを追加してみます。
コードをpushするだけで、Github上でビルド・テスト実行・テストカバレッジのアップロード・Releaseページの下書き作成までが自動でできます。
WPFアプリの中身はファイルリネーマーです。アプリの詳細はこちらで。コード行数850行ぐらい、クラス数40個ぐらいのサンプルアプリに毛の生えたぐらいのコードサイズです。
TestフレームワークはxUnit、DIコンテナはMicrosoft.Extensions.DependencyInjection、CIはGitHubActionsを使用しました。
GithubActionsの作成
YAMLファイルの追加
まず、GitHubActionsのソースコードにあたる、YAMLファイルを作成します。
GithubのActionsタブにいくと、オススメのWorkflowが表示されているので、".NET Desktop"を選んで追加します。似たのに".NET"がありますが、こちらはデスクトップアプリケーション以外の.NET用です。
デスクトップアプリケーションに適したWorkflowが追加されるので、これを修正していきます。
そのままGithub上で編集してもよいですし、PullしてローカルでVSCode等を使用して編集してもよいです。
環境変数などの追加
アプリケーションプロジェクトやテストプロジェクトの名前などを指定します。
デフォルトで入っているenv:
の部分を修正します。
env:
App_Name: FileRenamerDiff
Solution_Directory: src
Solution_Path: src/FileRenamerDiff.sln
App_Project_Path: src/FileRenamerDiff/FileRenamerDiff.csproj
Test_Directory: UnitTests
CodeCov_Result: "lcov.xml"
Dump表示の追加
これは必須ではありませんが、GithubActionsをデバッグするときに便利なのでDump表示をsteps
の最初に入れておくことをオススメします。
steps:
# Dump for debug workflow
- name: Dump Github Context
env:
GitHub_Context: ${{ toJson(github) }}
run: echo "${GitHub_Context}"
リストア→ビルド
次にアプリケーションをビルドしますが、その前にリストア(依存関係の解決)します。
ローカルでのビルドやテストでは自動でリストアが走っています。しかしGithubActions上では事前に明示的にリストアして、ビルドやテストではリストア処理をスキップすることでワークフローが効率化できます。
# Restore before build and test
- name: Restore
run: dotnet restore ${{ env.Solution_Path }}
- name: Build with dotnet
run: dotnet build ${{ env.App_Project_Path }} --no-restore
env:
Configuration: ${{ matrix.configuration }}
テスト実行→カバレッジ計算
そしていよいよテストの実行です。今回はテストの実行と共にテストカバレッジの計算までしています。
いくつかのファイルはカバレッジに含めたくないので、除外パターンを指定しています。基本的にView関係のファイル(XAML・コードビハインド・リソースファイル)は除いています。ただしConverterなどはテストできるので含めます。
# Execute all unit tests in the solution
- name: Execute unit tests
run: >
dotnet test ${{ env.Solution_Path }}
--verbosity normal --no-restore
--collect:"XPlat Code Coverage"
/p:CollectCoverage=true /p:CoverletOutputFormat=opencover
/p:ExcludeByFile="**.Designer.cs%2c**.xaml*%2c**.g.cs%2c**.xaml"
-p:coverletOutput=${{ env.CodeCov_Result }}
env:
Configuration: ${{ matrix.configuration }}
そうして計算したカバレッジ結果をアップロードします。
- name: Send coverage result to codecov
uses: codecov/codecov-action@v2.0.3
with:
files: ${{ env.Solution_Directory }}/${{ env.Test_Directory }}/${{ env.CodeCov_Result }}
Release時のみ実行Job
ここまではpushされたら必ず実行する内容でしたが、ここからはRelease時にのみ変更します。
まず、実行条件を分けるので、別のjobにします。
コミットにv---
というタグがついていた場合は、リリースタグとみなして、jobを実行します。
create-release:
runs-on: windows-latest
needs: [build]
if: "contains( github.ref , 'v')"
steps:
...
Publishの実行
dotnet publish
で自己完結型、単一ファイルの指定をして、アプリケーションの発行を行います。x86とx64で2回行います。
発行完了後、ファイルをアップロードしておきます。
- name: dotnet publish x86
run: dotnet publish ${{ env.App_Project_Path }} -c Release -r win-x86 --self-contained true -p:PublishTrimmed=false -p:PublishSingleFile=true -p:PublishReadyToRun=true -o outputs\${{ env.app_x86_name }}
- name: dotnet publish x64
run: dotnet publish ${{ env.App_Project_Path }} -c Release -r win-x64 --self-contained true -p:PublishTrimmed=false -p:PublishSingleFile=true -p:PublishReadyToRun=true -o outputs\${{ env.app_x64_name }}
- name: Archive publish files
uses: actions/upload-artifact@v1
with:
name: FileRenamerDiff_apps
path: outputs
Releaseノートの下書き
Releaseノートの下書きを作ります。毎回デザイン修正とバグフィックスはあるので、最初から書いておきます。
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
with:
tag_name: v${{ env.version }}
release_name: Ver ${{ env.version }}
body: |
- Change design
- Bug fix
draft: true
prerelease: false
ReleaseノートへのAssetsの追加
さきほど作成したPublishファイルをZipに圧縮してリリースノートのAssetsへ追加します。
- name: Archive packages
shell: pwsh
run: |
Compress-Archive -Path outputs\${{ env.app_x86_name }} -DestinationPath ${{ env.app_x86_name }}.zip
Compress-Archive -Path outputs\${{ env.app_x64_name }} -DestinationPath ${{ env.app_x64_name }}.zip
- name: Upload Release Asset
uses: csexton/release-asset-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
release-url: ${{ steps.create_release.outputs.upload_url }}
files: |
${{ env.app_x86_name }}.zip
${{ env.app_x64_name }}.zip
全体
YAMLファイル全体は以下です。
name: .NET Build and Test
on:
push:
env:
App_Name: FileRenamerDiff
Solution_Directory: src
Solution_Path: src/FileRenamerDiff.sln
App_Project_Path: src/FileRenamerDiff/FileRenamerDiff.csproj
Test_Directory: UnitTests
CodeCov_Result: "lcov.xml"
jobs:
build:
strategy:
matrix:
configuration: [Debug, Release]
runs-on:
windows-latest
steps:
# Dump for debug workflow
- name: Dump Github Context
env:
GitHub_Context: ${{ toJson(github) }}
run: echo "${GitHub_Context}"
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
# Install the .NET Core workload
- name: Install .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
# Add MsBuild to the PATH: https://github.com/microsoft/setup-msbuild
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v1.0.2
# Restore before build and test
- name: Restore
run: dotnet restore ${{ env.Solution_Path }}
- name: Build with dotnet
run: dotnet build ${{ env.App_Project_Path }} --no-restore
env:
Configuration: ${{ matrix.configuration }}
# Execute all unit tests in the solution
- name: Execute unit tests
run: >
dotnet test ${{ env.Solution_Path }}
--verbosity normal --no-restore
--collect:"XPlat Code Coverage"
/p:CollectCoverage=true /p:CoverletOutputFormat=opencover
/p:ExcludeByFile="**.Designer.cs%2c**.xaml*%2c**.g.cs%2c**.xaml"
-p:coverletOutput=${{ env.CodeCov_Result }}
env:
Configuration: ${{ matrix.configuration }}
- name: Send coverage result to codecov
uses: codecov/codecov-action@v2.0.3
with:
files: ${{ env.Solution_Directory }}/${{ env.Test_Directory }}/${{ env.CodeCov_Result }}
create-release:
runs-on: windows-latest
needs: [build]
if: "contains( github.ref , 'v')"
steps:
- name: echos
shell: bash
run: |
echo $RELEASE_VERSION
echo version=${GITHUB_REF/refs\/tags\/v/}
echo "version=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
echo "app_x86_name=${{ env.App_Name }}_app_win-x86_ver${{ env.version }}" >> $GITHUB_ENV
echo "app_x64_name=${{ env.App_Name }}_app_win-x64_ver${{ env.version }}" >> $GITHUB_ENV
pwd
- name: confirm env value
shell: bash
run: |
echo "env.version=${{ env.version }}"
echo "app_x86_name=${{ env.app_x86_name }}"
echo "app_x64_name=${{ env.app_x64_name }}"
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: dotnet publish x86
run: dotnet publish ${{ env.App_Project_Path }} -c Release -r win-x86 --self-contained true -p:PublishTrimmed=false -p:PublishSingleFile=true -p:PublishReadyToRun=true -o outputs\${{ env.app_x86_name }}
- name: dotnet publish x64
run: dotnet publish ${{ env.App_Project_Path }} -c Release -r win-x64 --self-contained true -p:PublishTrimmed=false -p:PublishSingleFile=true -p:PublishReadyToRun=true -o outputs\${{ env.app_x64_name }}
- name: Archive publish files
uses: actions/upload-artifact@v1
with:
name: FileRenamerDiff_apps
path: outputs
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
with:
tag_name: v${{ env.version }}
release_name: Ver ${{ env.version }}
body: |
- Change design
- Bug fix
draft: true
prerelease: false
- name: Archive packages
shell: pwsh
run: |
Compress-Archive -Path outputs\${{ env.app_x86_name }} -DestinationPath ${{ env.app_x86_name }}.zip
Compress-Archive -Path outputs\${{ env.app_x64_name }} -DestinationPath ${{ env.app_x64_name }}.zip
- name: Upload Release Asset
uses: csexton/release-asset-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
release-url: ${{ steps.create_release.outputs.upload_url }}
files: |
${{ env.app_x86_name }}.zip
${{ env.app_x64_name }}.zip
実行結果
GithubActions進行状況
YAMLファイルを追加すると、GithubActionsが実行されます。
GitHubのActionsタブにいくと、進行状況が見れます。
build
jobはconfiguration
をDebugとReleaseの2つ指定したので、並列で2つのjobが走ります。それらが終わって、コミットタグにv~
が指定されていたら、create-release
jobが走ります。
完了すると、緑色の表示になります。
Releaseノート
Releaseノートにいくと、下書きができていますので、リリース内容を確認してPublishします。
テストカバレッジ表示
テストカバレッジの結果をアップロードしているので、カバレッジが経時的に変化しているかも見ることができます。
こうやってVisualizeされると退屈なテストを書くモチベーションも上がりますね!
デバッグのコツ
うまくワークフローが動くようになるとGithubActionsはとても便利ですが、最初はつまずくこともあります。
ここでは私が行ったいくつかの工夫を紹介します。
GithubActions練習用レポジトリの作成
GithubActionsの練習をアプリケーションのレポジトリでやってしまうとコミットツリーにノイズが増えすぎてしまいます。
またアプリケーションの規模大きくなると、ワークフローの実行時間も増えてきてしまいます。
最終的には本番レポジトリでも試行錯誤が必要ですが、GithubActions自体の練習は専用のレポジトリを作成したほうがよいでしょう。
Dumpを確認する
GithubActionsがうまく動かない原因は、だいたいレポジトリ名やファイルパスなどが間違っていることです。
Run結果の一番最初のDump Github Context
の中を確認して他の変数指定とあっているかを確認します。
ログを見やすく
GithubActionsのログは"個別のJobのRun結果>⚙アイコン>View raw logs"から表示できます。
ただしそのままではハイライトも無く見づらいのでダウンロードして.log
ファイルとしてVSCodeで見ると色がついて見やすくなります。
Re-run
ローカルでは成功するテストがGithubActions上では失敗することがあります。この場合、ワークフローを再実行することで成功することもあります。
"個別のJobのRun結果>Re-run all jobs"で再実行します。
ただ、そもそも環境によって結果が変わるテストは筋が悪いので、テストコードを修正したほうがよいでしょう。
注意点
プライベートレポジトリの場合はいくつかのstepで認証が必要になるかもしれません。そのあたりはGithubActionsのドキュメントか使用するActionのレポジトリを確認してください。
後書き
GitHubActionsは動作を確認するためにCommitが必要になるので、試行錯誤を繰り返すと自分のGithubマイページの芝がよく生えます。
なれると自動でGithub側が色々やってくれてスゴイ便利です。
参考
環境
VisualStudio 2019
C# 9
.NET 5
Microsoft.NET.Test.Sdk 16.9.4
xunit 2.4.1
xunit.runner.visualstudio 2.4.3
coverlet.collector 3.0.2
System.IO.Abstractions 13.2.38
System.IO.Abstractions.TestingHelpers 13.2.38