Azure DevOps で Public Project に対して、GitHub の PR に対して Multi-Agent 対応させる（１）

今回のブログは、残念ながら出来なかったことなのですが、技術の記録として書いておきます。

# 動機

オープンソースの某プロジェクトで、CIが無くて、自分がコントリビュートするときに、テスト実行時間が長すぎて泣いたので、CI作ろうぜという話をしました。既存のテストは、インテグレーションテストのみで、コントリビュータもあまりテストをしていない様子。インテグレーションテストのみだから、テスト実行時間もとても長くて、普通のパイプラインを作ると、タイムアウトして実行出来ません。

# 解決策

解決策としては、Azure DevOps には、最高にいい機能があって、GitHub のパブリックリポジトリに対して、Public プロジェクトを作ると、１０台のエージェントまで同時に使えるので、１０台並列実行するテストを実行すれば良いと思いました。各テストはシナリオテスト的で、テストケース自体を同時に実行したらうまく動きません。対策としては、エージェント毎に違うコネクションストリングを渡して、１０台それぞれ違うストレージキューや、ServiceBusで実行すれば並列で実行が可能です。

単純に、Multi-agent を設定すれば良いです。

![Screen Shot 2019-03-19 at 9.22.17 PM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/a7b468ab-f1a1-231e-517c-06a0ac3f4a86.png)

Multi-config という設定があり気になります。また後のポストで使ってみましょう。

## エージェント毎のコネクションストリング

エージェント毎にコネクションストリングや各種設定を変えるいい方法を思いつかなかったので、PowerShell を書いてみました。後で気づきましたが、Multi-confgration を使えばいけるかもですが、こんな感じで書いてみました。コネクションストリングを定義しておいて、エージェントの番号によって、app.config を上書きするという作戦です。最初は、インターネットに多くあるEnvironment Variable を強引に、PowerShell で設定する方法が出回っていましたが、今のAzure DevOps では動かないようです。

```Powershell
| $jobname = [Environment]::GetEnvironmentVariable("AGENT_JOBNAME") |
|:--|
| $jobid = $jobname.Substring($jobname.Length -1, 1) |
| $serviceBusEnvironmentVariables = "ServiceBusConnectionString$($jobid)" |
| $filePath = ".\test\Some.ServiceBus.Tests\app.config" |
| $appConfig = [xml](cat $filePath) |
| $serviceBusConnectionString = $null |
| switch ($jobid) { |
|   "1" {  $serviceBusConnectionString = "$(ServiceBusConnectionString1)" |
|             break} |
|   "2" {  $serviceBusConnectionString = "$(ServiceBusConnectionString2)" |
|             break}   |
|   "3" {  $serviceBusConnectionString = "$(ServiceBusConnectionString3)" |
|             break}   |
|   "4" {  $serviceBusConnectionString = "$(ServiceBusConnectionString4)" |
|             break}   |
|   "5" {  $serviceBusConnectionString = "$(ServiceBusConnectionString5)" |
|             break}   |
|   default  |
|      { Write-Host "$($jobid)"; break; } |
| } |
|  |
| $appConfig.configuration.appSettings.add | foreach { |
|     Write-Host $_.key  #.ServiceBusConnectionString= $servcieBusConnectionString  |
|     if ($_.key -eq "ServiceBusConnectionString") |
|     { |
|         $_.value = $serviceBusConnectionString |
|     } |
| } |
|  |
| $appConfig.Save($filePath) |
```

# シークレットを設定する

さて、コネクションストリングは、晒したくないと思うでしょう。そういう時に、コネクションストリングをマスクする Secret というチェックボックスが Variables の中にあります。

鍵マークをクリックすればシークレットになって、設定した人しか見れません。ただし、やろうと思ったら、不正なユーザがテストでその値をとって、どこかに流すとかやろうと思ったら出来るので、パブリックリポジトリでは要注意です。

![Screen Shot 2019-03-19 at 9.46.02 PM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/b329f3ba-b936-8ecf-6308-cd624d131385.png)

ちなみに、パブリックリポジトリの場合は、次のチェックボックスにチェックをしないと、シークレットは取得出来ません。

![Screen Shot 2019-03-19 at 9.53.53 PM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/fa231238-f61b-43e3-90bb-3ec1d6402b76.png)

不正なユーザーがやろうと思えば、シークレットを盗めるので、パブリックには十分ご注意ください。

# Storage Emulator 

ちなみに、ストレージエミュレータも使えます。簡単にこんな感じのコードを書くだけ。Command Line タスクを使います。

```
sqllocaldb create MSSQLLocalDB
sqllocaldb start MSSQLLocalDB
sqllocaldb info MSSQLLocalDB

"C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator\AzureStorageEmulator.exe" start
```

# テストの設定をする。

最後にテストの設定ですが、テストの並列実行の仕組みは、[vstest](https://github.com/Microsoft/vstest)という方法が使われています。このGitHubのドキュメントを読むと、そのアーキテクチャが理解できると思います。
　あとは、エージェントにどうやってテストを分散させるかですが、アルゴリズムが定義されています。私が好きなのは、テスト実行時間を記録しておいて、その統計情報を見て、勝手にリバランスしてくれるオプションがありますので、これが一番良いでしょう。

![Screen Shot 2019-03-19 at 9.49.49 PM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/5280f619-504f-8db3-c21e-36d9828670d2.png)

この結果として、最初は、全部テストを流したら、６０分以上かかって、タイムアウトするところが、１０エージェントで６分で終わるようになりました。これにはびっくり。

# GitHub のPRには対応せず

ところが、実際に、パブリックのPRを受け付けると、次のエラーが発生して、テストが実行されません。権限がある人が手動トリガーすると、完全に動きます。どう見ても、PR を送ってくる、annonymous user に対して、manage test configuration の権限がないから発生しています。ところが、どう探しても、annonymous user に対して、設定できるところが見つかりませんでした。何日かたつと、知らない間にプロダクトチームの人が意味不明な場所でメンションをしてくれて、現在開発中だというコメントをもらいました。多分ハマる人もいると思ったので、issue をあげておきました。

```
Unhandled exception ocurred: System.AggregateException: One or more errors occurred. ---> Microsoft.TeamFoundation.Test.WebApi.TestExecutionAccessDeniedException: You do not have the appropriate permissions to manage test configurations in this team project.
```

* [Multi agent Test fails with Public repo with GitHub PR Trigger #9879](https://github.com/Microsoft/azure-pipelines-tasks/issues/9879)

この動向を見て、実装されたらまた試して見ましょう。残念ながらそれまでは、リポジトリに、PR を送る人を招待せよというとてもかなしいインストラクションがDocには書かれています。意味ないやんw

* [Default roles & access for public projects](https://docs.microsoft.com/en-us/azure/devops/organizations/public/default-roles-access-public?view=azure-devops)

> To contribute to a public project, you must be added as a member of that project and assigned either Stakeholder or Basic access. The access level determines the user interfaces you can access. The security group you're assigned to determines the features you can exercise. For details, see About access levels.

# .Net Core のプロジェクト追加で、テストが検出されず

他の問題としては、そのプロジェクトは、.netstandard2.0 と net451 の両方にビルドできるプロジェクトなのですが、私が、そのソリューションに.netcore2.1 のプロジェクトを追加したら、テストが突然認識されなくなりました。結局、.netcore から、net451 に書き換えたら認識するようになりました。エラーは次の通り。

```
Test run is aborted. Logging details of the run logs.

New test run created.

Test discovery started.

Test Run Discovery Aborted . Test run id : 1000120

Unexpected error occurred during test execution. Try again.

Error : No test is available in d:\a\1\s\Test\DurableTask.AzureStorage.Tests\bin\Debug\net451\DurableTask.AzureStorage.Tests.dll d:\a\1\s\Test\DurableTask.AzureStorage.Tests\bin\Debug\net451\DurableTask.Core.Tests.dll d:\a\1\s\Test\DurableTask.Core.Tests\bin\Debug\net451\DurableTask.Core.Tests.dll d:\a\1\s\Test\DurableTask.Emulator.Tests\bin\Debug\net451\DurableTask.Emulator.Tests.dll d:\a\1\s\Test\DurableTask.ServiceBus.Tests\bin\Debug\net451\DurableTask.Core.Tests.dll d:\a\1\s\Test\DurableTask.ServiceBus.Tests\bin\Debug\net451\DurableTask.ServiceBus.Tests.dll. Make sure that test discoverer & executors are registered and platform & framework version settings are appropriate and try again.

Test run aborted. Test run id: 1000120

System.Exception: The test run was aborted, failing the task.
```

残念ながら、どこにあったかわからなくなったのですが、Issueを調べていると、.netcore の時に、Visual Studio Test が、テストを見つけられない問題があり、その後解決したみたいなものがありました。きっと、テストの検出のアルゴリズムで、.Net Core がきたら何か条件分岐して、新しいバージョンのテストアダプター落としてきて、そいつが、net451 のプロジェクトに対応してないとかそんな感じじゃないでしょうか。Issue をあげておきました。

* [Visual Studio Test can't find tests when I add .Net Core project #9880](https://github.com/Microsoft/azure-pipelines-tasks/issues/9880)

# まとめ

というわけで、「なんの成果もありませんでした！」という感じなのですが、なんとか、Issue を回避して、オープンソースのプロジェクトのPRバリデーションを実施する方法を検討して見たいと思います。（手動トリガー含め）また、シークレットの扱いが肝なので、ディスカッションをして、方法を考えて見たいと思います。あと、Multi-Configuration と、Configuration Group は試したいと思います。特に KeyVault と連携しているのは渋そうです。

# 追記

私の師匠のかずきさんからいいコメントをいただきました。

シークレットは、毎回リソースを作ってしまうというのがいいかも。で、終わったら消す。もしくは、リソース側の、コネクションストリングを再作成する作戦がいいかも。そっちの方が早そうだし。その方法の課題としては、同時にテストが走ると死ぬと。確かに、、、

他に、現時点で、Public の Multi Agent Test をやるためにはPRがかかったら、そこから、RESTAPIで、私の権限を使って、他のビルドをキューして、その結果を取得する的な方法もあるかも、相当Hackey だけど、作戦としてはストックしておこう。