GitLabでASP.NET Web SiteのプロジェクトをCIする

# 前置き
現在参画している現場でASP.NEST Web Siteのシステムを担当しています。
ソースコード管理のツールとしてGitLabが採用されています。
GitLabではCI/CDの機能が備わっているので、マージリクエストが承認された段階でコンパイルさせたり、テスト実行させたりステージングや本番環境へのデプロイなんかも出来るように提案したいと思い、本記事は実現方法について調査した結果になります。

え？Web Applicationでもなく、MVCでもなく（イマドキ）ASP.NEST Web Siteなの？
という声が聴こえてきそうですが、そうなのです。
SESで働いているとそういう事もあります。やっていきましょう。

# やりたいこと
1. GitLabからソースコードをcloneする
2. cloneしたソースコードをビルドし（プレコンパイル）結果をGitLab上で確認する
3. cloneしたソースコードからIIS Express上でサイトとして起動する
4. 起動したサイトに対してSeleniumでUIテスト実行し、結果をGitLab上で確認する

3はテスト環境などでデプロイするという手段もあると思いますが、IIS Expressをコマンドで起動できることがわかったので、今回はビルドサーバーでテストも実行するような形にしたいと思います。

# 準備
以下ツールをダウンロードしておきます。
GitLab Runnerの概要等については本記事では割愛させていただきます。

* GitLab Runner 
https://docs.gitlab.com/runner/install/windows.html

* MSBuild.exe（Microsoft Build Tools 2015）
https://www.microsoft.com/ja-JP/download/details.aspx?id=48159

* IIS Express
https://www.microsoft.com/ja-JP/download/details.aspx?id=48264

* vstest.console.exe
テストプロジェクトをコマンドラインから実行させるために必要
https://www.nuget.org/packages/Microsoft.TestPlatform/
からnugetでダウンロードします。
ダウンロードしたpackageのフォルダ内に配置されています。
「〜packages\Microsoft.TestPlatform.16.0.1\tools\net451\Common7\IDE\Extensions\TestPlatform\vstest.console.exe」
Visual Studioがインストーする済みなら以下です。
「C：¥Program Files（x86）¥Microsoft Visual Studio¥2017¥Enterprise¥Common7¥IDE¥CommonExtensions¥Microsoft¥TestWindow」

※Build ToolsとIIS Expressは、Visual StudioがインストールされているPCで実行するなら不要です。
※今回はVisual Studio Commnunity 2017がインストールされている環境となります。

# 手順
## 1. GitLab Runnerの設定
適用なディレクトリにダウンロードしたexeを配置してください。
公式ドキュメントだと「gitlab-runner.exe」にリネームするよう記載があります。

まずはregisterコマンドで設定を行います。
手順詳細は以下を参考に。
https://docs.gitlab.com/runner/register/index.html#windows

必須となるのはurlとtokenです。
urlとtokenの値はGitLabのプロジェクトページの「Settings > CI/CD > Runners」で確認できます。
SettingsはGitLabでの権限が「Developer」だと表示されません。
「Maintainer」「Owner」の権限が必要です。

設定が完了すると、exeが配置されているディレクトリにconfig.tomlという設定ファイルが作成されます。

で、現場で引っかかったポイントを記載しておきます。
GitLabが自社内構築などで、ネットワークの都合上GitLab上で表示されているリポジトリURLと実際のURLが異なる場合があるようです。

上記のよう場合はconfig.tomlにclone_urlを追記して設定しておきます。

```config.toml
concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "build"
  url = "https://project-url.com/"
  clone_url = "https://project-repo-url.com/" #必要であれば設定する
  token = "your token"
  executor = "shell"
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
```

registerが登録すると「Settings > CI/CD > Runners」の画面で「Runners activated for this project」として表示されているはずです。

## 2. GitLab Runnerの実行
以下のコマンドでサービスとして実行させます。

```
gitlab-runner install
gitlab-runner start
```

現場ではサービスの登録がWindowsアカウントの権限に引っかかって登録できませんでしたので、
取り急ぎgitlab-runner runコマンドで実行させました。
runだとコマンドプロンプト上からRunnerが実行されるイメージです。

参考：https://docs.gitlab.com/runner/commands/README.html

※Proxy経由の場合、以下のように環境変数にURLを設定する必要があるっぽいです。

```
SET HTTP_PROXY=http://Address.ofMyProxy.net:8079
SET HTTPS_PROXY=https://Address.ofMyProxy.net:8079
gitlab-runner run
```
参考：https://gitlab.com/gitlab-org/gitlab-runner/issues/3400

「Settings > CI/CD > Runners > Runners activated for this project」で、対象のRunnerがグリーンになっていれば疎通成功です。

## 3. .gitlab-ci.ymlファイルの設定

Runnerに実行してもらうscriptを記述する.gitlab-ci.ymlを設定します。
.gitlab-ci.ymlをリポジトリにaddします。
以降、.gitlab-ci.ymlが登録されたブランチに対してpush（※対象のブランチ、トリガータイミングも.gitlab-ci.ymlに記述可能）すると、GitLab上でジョブとして登録・実行されます。
今回の設定は以下のような形です。

```.gitlab-ci.yml
variables:
  MSBuildEXE: 'C:/Windows/Microsoft.NET\Framework/v4.0.30319/MSBuild.exe'
  VSTESTEXE:  'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe'

stages:
  - build
  - test

build_job:
  stage: build
  script:
    - chcp 65001
    - '"%MSBuildEXE%" TestGitLabCI.sln'
  only: 
    - develop
    - master

test_job:
  stage: test 
  script: 
    - chcp 65001
    - xcopy TestGitLabCI /S/R/Y/I C:\GitLab-Runner\source
    - '"%VSTESTEXE%" SeleniumUITests.dll'
```

buildステージで、cloneされたソースコードのslnファイルに対して、MSBuild.exeでコンパイルを実行させます。
続いてtestステージではvstest.console.exeからSelniumでUIテストを記述したテストプロジェクトのdllを実行させます。
テストの前にxcopyを実行しているのは、vstest.console.exeで対象のソースを指定する場合絶対パスのみでしか指定ができなく、Runnerでcloneしたソースのパスは変わってしまう場合もあるのではと懸念したためです。工夫次第では不要だと思います。
そしてここでは以下3つの箇所でつまずきました。

### つまづき-1. IIS Expressが自動でプロセスを落とせない

IIS Expressはコマンドから起動できるのですが、なんとコマンドプロンプト上でキー（Q）を叩くしか止める手段が無い模様です。
今回のように自動起動>終了させる場合は非常にやっかいな仕様です。

やっぱりこんな時はstackoverflow。

https://stackoverflow.com/a/24665612

テストコード内でIIS Expressのプロセスをstart/killしてしまえば良いというアイデアを発見。
ざっくりなコードですが以下のような形になりました。
パスの指定とかはconfigとかにしておきたいところですが取り急ぎ。

```uitest.cs
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

namespace SeleniumUITests
{
    [TestClass]
    public class UITests
    {
        [ClassInitialize()]
        public static void MyClassInitialize(TestContext testContext)
        {
            iis = new IisExpressAgent();
            iis.Start(@"/path:C:\GitLab-Runner\source /port:8080");
        }

        static IisExpressAgent iis;

        [ClassCleanup()]
        public static void MyClassCleanup()
        {
            iis.Stop();
            
        }

        [TestMethod]
        public void TestMethod()
        {
            IWebDriver driver = new ChromeDriver();
            driver.Url = "http://localhost:8080";

            IWebElement element = driver.FindElement(By.ClassName("jumbotron"));
            Assert.IsNotNull(element);

            driver.Quit();
        }
    }
}
```

```IisExpressAgent.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SeleniumUITests
{
    class IisExpressAgent
    {
        public void Start(string arguments)
        {
            ProcessStartInfo info = new ProcessStartInfo(@"C:\Program Files (x86)\IIS Express\iisexpress.exe", arguments)
            {
                // WindowStyle= ProcessWindowStyle.Minimized,
            };

            process = Process.Start(info);
        }

        Process process;

        public void Stop()
        {
            process.Kill();
        }
    }
}
```

### つまづき-2. テスト成功後、Testジョブが終了されない。

GitLab上でジョブがトリガーされた後、テストの結果が成功するところまで確認できたのですが、なぜか実行は続いておりTestジョブが終了されない状態になってしまいました。

原因は、前述のSeleniumのテストコード内でWebDriverを終了させるのを忘れていたためでした。
WebDriverのプロセスが残り続けていたことがジョブの終了を邪魔していたようです。
driver.Quit()を追記することで解消しました。

### つまづき-3. Binのdllが存在しない

buildステージでdllが存在しないといったようなエラーでコンパイルが失敗しました。
これは.gitignoreによってBin(or bin）フォルダをignoreしていたためでした。

ASP.NET WebSiteでは、そのプロジェクトが参照しているdllはBinフォルダ内のdllとなります。
WinFormアプリやWeb Applicationのようのプロジェクトファイルにパスを記述するといった形式は取れません。

https://stackoverflow.com/questions/4089165/what-is-a-dll-refresh-file-in-asp-net
のstackoverflowの投稿を見ると、ASP.NET Web SiteではBinフォルダ内はdllの参照パスを記述している.dll.refreshだけ管理しておけばよいとは思うのですが、
今回当方AWSのWorkspacesで実行しているため、ユーザー権限の事情でdllのファイルコピーが失敗してしまうようでうまくできませんでした。
よって、今回はBin内のdllもGit管理としているのが前提となります。
本来であればコンパイル前にnugetの実行等も必要になるかもです。

その場合はnuget.exeもダウンロードして、ymlでコンパイルの前に以下のように記述しておけば良いと思われます。

```
'"C:/nuget/nuget.exe" restore TestGitLabCI/packages.config -PackagesDirectory .\packages'
```

## 4. 結果確認
各種設定が完了したら、何らかのcommit>pushを行うことでjobがトリガーされます。
gitlab-runner.exeのパスにbuildsというフォルダが作成されていて、そこにソースがcloneされていることが確認できます。
ジョブの実行結果はCI/CD > Jobsの画面から確認できます。

↓buildステージの実行結果
<img width="1274" alt="スクリーンショット 2019-04-20 23.45.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23789/3bd138bd-ea3f-c0f6-7962-0e6a531caebb.png">

↓testステージの実行結果
<img width="1271" alt="スクリーンショット 2019-04-20 23.31.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23789/9c9afd0f-b7d3-0436-95d0-65906cfba923.png">

良い感じですね。
よかったですね。

今回はデプロイについては記載しておりませんが、ここまで出来ればあとはFTPなどを実行するdeployステージのscriptを組めば良いと思います。

# 補足
IIS ExpressとSeleniumから実行されたChromeは、
Runnerをサービスで起動しているとウィンドウは表示されずChromeは所謂headlessモードで実行。
Runnerをrunコマンドで実行するとウィンドウが表示されて実行されました。