読者です 読者をやめる 読者になる 読者になる

銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

ASP.NET WebHooks で VSTS のService Web Hook を受け取る

Visutal Studio Team Services(VSTS)でビルド結果を通知するにはService Hooksを使います*1。Integrated されているサービスもあるのですが、これ以外のサービスと連携するには自前でWeb Hookを作る必要があります。

Integrate with service hooks | Visual Studio Team Services

Web Hookを作るライブラリやサービスは多数ありますが、今回は ASP.NET Web Hooksを使うことにしました。Web Hooksの概要はこちらのブログがわかりやすいです。

kiyokura.hateblo.jp

Microsoftが出しているライブラリなんだから、MicrosoftのDevOpsのサービスの切り札でもあるVSTSのWebHookのIntegrationが存在しないはずはない!!!11といいたいところなんですが、ないです。しょうがないのでCustomで作成することにします。ASP.NET WebHooks はWebHookを送信する側と受信する側の両方に対応したライブラリとツールがあるんですが、今回は受信側だけを使います。受信側はReceiverと言い、Microsoft.AspNet.WebHooks.Receivers.Customがカスタム用のレシーバーライブラリです。また、プロジェクトテンプレートですが、ASP.NET 5 Web APIを使うのが楽でした。余計なものが追加されるので、それを避けたい場合はEmptyがいいでしょう。

まず、Nugetからライブラリを追加します。

PM> Install-Package -IncludePrerelease Microsoft.AspNet.WebHooks.Receivers.Custom

次にStartUpメソッドにCustomWebHooksの初期化処理を追加します。

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        GlobalConfiguration.Configure(WebApiConfig.Register);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        GlobalConfiguration.Configuration.InitializeReceiveCustomWebHooks(); // この1行を追加
    }
}

そして、VSTSからのWebHookを受け取るVSTSWebHookReceiverクラスを作成します、IWebHookReceiverインターフェースを実装すればいいのですが、便利メソッドがあるので、WebHookReceiverクラスを継承しておくのがいいでしょう。Nameプロパティをオーバーライドして、そのプロジェクトで一意な名前を付けます。これはWebHooksを受け取るURLにも使われます。実際に受け取る処理はReceiveAsyncをオーバーライドします。実際にこのクラスで受け取るWebHookのURLは

https://<ホスト名>/api/webhooks/incoming/vsts/id?key=value

といったURLになります。idの部分はReceiveAsyncの第一引数に渡されますが空でも問題ありません、クエリ文字列も任意です。

さて、このようなURLのクエリパラメーターも取得して、Bodyで送信されてきたJSONをパースする処理はこんな風に書けます。

public class VSTSWebHookReceiver : WebHookReceiver
{
    public override string Name => "vsts";

    public override async Task<HttpResponseMessage> ReceiveAsync(string id, HttpRequestContext context, HttpRequestMessage request)
    {
        var query = request.GetQueryNameValuePairs().FirstOrDefault(e => e.Key == "flg");
        var json = await ReadAsJsonAsync(request);
        var message = json["detailedMessage"]["text"];
        return new HttpResponseMessage(HttpStatusCode.Accepted);
    }
        
}

あとは、このプロジェクトをAzure Web Appsなどにデプロイすれば完了です。

実際にVSTSから送信してみましょう。Service Hooksの画面からWeb Hooksを選択します。 f:id:tanaka733:20151208023042p:plain

Build Completedを選びビルド完了後のメッセージを選択します。 f:id:tanaka733:20151208023049p:plain

ActionのURLにReceiverで受け取るURLを指定します*2f:id:tanaka733:20151208023058p:plain

入力したらTestボタンを押してテストでWeb Hookを送ってみます。リモートデバッグしてみると実際に送信されてきていることが確認できます*3f:id:tanaka733:20151208023103p:plain f:id:tanaka733:20151208023109p:plain

さて、このままだとインターネット上に裸のまま公開されているので、もしこのURLが見つかるとWebHooksに投げられまくりの状態になります。そこで簡易的な認証などを行いたいわけですが、CustomWebHookReceiverというクラスを継承すると、送信データの正当性検証などを行うコードが書かれています。これをうまく使えばいいのですが、VSTSについては送信側でこれに対応することができません*4。ですので今回は割愛しますが、必要な場合はコードを見てみると参考になると思います。なお、これに関するドキュメントは今のところ見つけられていないので、「コード見ろ」状態です。

*1:Service Hooksはビルド結果以外のイベントでも送信可能

*2:このURL欄、なぜか https://example.com/hoge/?k=v みたいな/の直後に?が来るURLだとフォームの入力値検証はOKなのにFinishで保存しようとすると不正なURLで保存できなくなります

*3:WebHookは基本的にはインターネット上にアプリを公開しないと本番データを受信できないですが、Azure Web Appsだとリモートデバッグで手元で確認できるのが便利です

*4:最初に書いた通り、送信するデータのBody部分は編集できないし、Headerに指定できるのは固定文字列のみであるため