Ein anpassbares und erweiterbares ALM-Tool zu haben, ist heutzutage für den Erfolg von wesentlicher Bedeutung. Die Möglichkeiten, Work-Item-Typen zu erweitern oder gar neue Entitäten zu erstellen, kennt vermutlich jeder. Dieser Artikel legt den Fokus auf die Anpassung der Weboberfläche sowie auf die Integration von eigenen Services in Visual Studio Team Services (VSTS) und Team Foundation Server (TFS). Abbildung 1 zeigt, welche Integrationspunkte zur Verfügung stehen.
Wie oft hatte man sich in der Vergangenheit gewünscht, an einer bestimmten Stelle einen zusätzlichen Button hinzuzufügen oder gar eigene Views einzublenden. Diese Möglichkeit ist mit der aktuellen Version nun gegeben. Die Erweiterungen in der Oberfläche enthalten Code in Form von JavaScript zur Interaktion mit dem System, die Visualisierung wird mittels HTML implementiert. Eigene Backend-Services können damit aber nicht integriert werden. Sobald Servicelogik erforderlich ist, kann dies nur als externer Service implementiert werden, also z. B. als eigenständige Webapplikation.
Diese Applikationen können über die REST-Schnittstelle mit dem VSTS bzw. TFS interagieren. Zur Benachrichtigung der externen Applikation stehen so genannte WebHooks zur Verfügung. Zu guter Letzt können dem TFS bzw. VSTS noch eigene Build-Tasks über die Extensibility-Schnittstelle zur Verfügung gestellt werden. Weitere Details zu den einzelnen Erweiterungspunkten sind auf der Extensibility-Webseite von VSTS zu finden. Neben einer detaillierten Auflistung werden entsprechende Beispiele zur Verfügung gestellt.
Dashboard-Widget über ein REST-API erstellen
In diesem Artikel wird die Extensibility-Schnittstelle anhand eines eigenen Dashboard-Widgets erklärt. Um die Komplexität klein zu halten, wollen wir ein einfaches Widget in Form einer Build-Ampel erstellen (Abb. 2). Die Ampel kommuniziert mit dem Build-System über ein REST-API, um den aktuellen Status periodisch zu erfragen. Da wir für verschiedene Build-Definitionen verschiedene Ampeln auf dem Dashboard positionieren möchten, benötigen wir ebenfalls eine Konfigurationsmöglichkeit für den Nutzer, in der er die Größe des Widgets sowie die gewünschte Build-Definition auswählen kann.
UI-Extensions bestehen primär aus HTML und JavaScript und den üblichen Webartefakten wie Bildern und CSS-Dateien. Damit VSTS respektive TFS weiß, was wie und wo intergiert werden muss, beinhaltet jede Extension ein Manifest. Das Manifest, das als JSON-Datei definiert wird, beinhaltet alle Informationen, Dateispezifikationen sowie Integrationspunkte. Dazu kommt die Definition der Scopes, also die Definition der Zugriffsrechte der Erweiterung. Vor der Installation wird der Nutzer darüber informiert, auf was die Erweiterung Zugriff wünscht, und muss dies entsprechend bestätigen. Ist eine Erweiterung einmal ausgerollt, kann der Scope nicht mehr verändert werden. So wird verhindert, dass durch ein Update plötzlich mehr Berechtigungen als einmal bestätigt eingefordert werden können.
Generell wird die Sicherheit großgeschrieben. Eine UI-Erweiterung läuft immer „sandboxed“ in einem dedizierten iFrame und erhält über ein SDK Zugriff auf den VSTS/TFS – samt Services und Events. Sorgen wegen der Authentifizierung muss man sich bei der Verwendung des SDK keine machen, da der aktuelle Nutzerkontext übernommen wird. Jeder API-Call wird gegenüber den definierten und durch den Nutzer bestätigten Scopes geprüft und bei einem Fehlverhalten blockiert. Damit alle Artefakte sowie das Manifest ausgeliefert und installiert werden können, erstellen wir als Artefakt ein Paket in Form einer VSIX-Datei. Diese kann manuell auf den TFS geladen oder über den VSTS Marketplace vertrieben werden.
Grundsätzlich können wir die Erweiterung mit jedem Texteditor erstellen, jedoch wollen wir auch in diesem kleinen Beispiel den Entwicklungsprozess gleich sauber aufgleisen und verwenden hierzu Visual Studio. Für die Libraries und Zusatztools verwenden wir den Node Package Manager (npm). Damit wir die Paketerstellung automatisieren können, kommt Grunt als Task-Runner zum Einsatz. Wer das alles nicht von Hand aufbauen möchte, kann die Visual-Studio-Erweiterung VSTS Extension Project Templates installieren. Hiermit verfügt Visual Studio über einen neuen Projekttyp, der das Erweiterungsprojekt mit allen Abhängigkeiten und Tools korrekt aufsetzt. Zudem möchten wir in unserem Beispiel nicht direkt JavaScript-Code schreiben, sondern TypeScript zur Implementierung verwenden. Abbildung 3 zeigt die Projektstruktur in Visual Studio für unsere Erweiterung.
Unsere Manifestdatei befüllen wir mit den üblichen Informationen zum Autor sowie der Beschreibung der Erweiterung. Von entscheidender Bedeutung sind der Scope für die Berechtigungen sowie die Contribution Points, in denen die eigentliche Integration beschrieben wird. Da wir auf Build-Informationen lesend zugreifen möchten, wählen wir den Scope vso.build_execute. Als Contribution Points definieren wir zwei UI-Elemente: ein Widget-UI sowie ein Konfigurations-UI. In Listing 1 sind die entsprechenden Stellen fett hervorgehoben.
{ "manifestVersion": 1, "id": "BuildTrafficLights", "version": "0.1.16", "name": "Build Traffic Lights", "scopes": [ "vso.build_execute" ], ... "contributions": [ { "id": "BuildTrafficLightsWidget", "type": "ms.vss-dashboards-web.widget", "targets": [ "ms.vss-dashboards-web.widget-catalog", "4tecture.BuildTrafficLights.BuildTrafficLightsWidget.Configuration" ], "properties": { "name": "Build Traffic Lights Widgets", "uri": "TrafficLightsWidget.html", ... "supportedScopes": [ "project_team" ]}}, { "id": "BuildTrafficLightsWidget.Configuration", "type": "ms.vss-dashboards-web.widget-configuration", "targets": [ "ms.vss-dashboards-web.widget-configuration" ], "properties": { "name": "Build Traffic Lights Widget Configuration", "description": "Configures Build Traffic Lights Widget", "uri": "TrafficLightsWidgetConfiguration.html" }}]}
Aufgrund des Manifests wird unsere Erweiterung richtig im Widgetkatalog aufgeführt, und aufgrund der Contribution Points werden die richtigen HTML-Dateien für die UI-Erweiterungen angezogen. Die Implementierung der Views ist nichts anderes als klassische Webentwicklung. Jedoch ist die Integration der Komponente über das zur Verfügung gestellte SDK von entscheidender Bedeutung.
Das SDK liegt uns in Form einer JavaScript-Datei (VSS.SDK.js) in unserem Projekt vor. Da wir mit TypeScript arbeiten, laden wir ebenso die dazugehörigen Typinformationen. Nach der Referenzierung der Skriptdatei in unserer HTML-Datei können wir nun über das VSS-Objekt die Erweiterung registrieren. Zudem geben wir VSTS/TFS bekannt, wie die Runtime unsere JavaScript-Module finden und laden kann. In unserem Fall ist der gesamte Code in TypeScript implementiert und wird als AMD-Modul zur Verfügung gestellt. Da wir unsere Build-Ampel dynamisch in unserem TypeScript-Code aufbauen werden, enthält die HTML-Datei nur das Grundgerüst des Widgets (Listing 2).
Listing 2: "TrafficLightsWidget.html" <script type="text/javascript"> // Initialize the VSS sdk VSS.init({ explicitNotifyLoaded: true, setupModuleLoader: true, moduleLoaderConfig: { paths: { "Scripts": "scripts" } }, usePlatformScripts: true, usePlatformStyles: true }); // Wait for the SDK to be initialized VSS.ready(function () { require(["Scripts/TrafficLightsWidget"], function (tlwidget) { }); }); </script>
Damit unser Widget und seine Controls nicht vom Look and Feel von VSTS/FS abweichen, gibt uns das SDK die Möglichkeit, über WidgetHelpers.IncludeWidgetStyles() die Styles von VSTS/TFS zu laden und in unserem Widget zu verwenden. Das erspart uns einiges an Designarbeit. Listing 3 zeigt zudem das Auslesen der Konfiguration sowie der Instanziierung unseres Ampel-Renderers TrafficLightsCollection.
Listing 3: "TrafficLightsWidget.ts /// /// import TrafficLights = require("scripts/TrafficLightsCollection"); function GetSettings(widgetSettings) {...} function RenderTrafficLights(WidgetHelpers, widgetSettings) { var numberOfBuilds = widgetSettings.size.columnSpan; var config = GetSettings(widgetSettings); if (config != null) { var trafficLights = new TrafficLights.TrafficLightsCollection( VSS.getWebContext().project.name, config.buildDefinition, numberOfBuilds, document.getElementById("content")); } else {...} } VSS.require("TFS/Dashboards/WidgetHelpers", function (WidgetHelpers) { WidgetHelpers.IncludeWidgetStyles(); VSS.register("BuildTrafficLightsWidget", function () { return { load: function (widgetSettings) { RenderTrafficLights(WidgetHelpers, widgetSettings); return WidgetHelpers.WidgetStatusHelper.Success(); }, reload: function (widgetSettings) {...} }; }); VSS.notifyLoadSucceeded(); });
Um nun mit dem REST-API von VSTS/TFS zu kommunizieren, können wir über das SDK einen entsprechenden Client instanziieren. Der Vorteil dieser Vorgehensweise liegt darin, dass wir uns nicht über die Authentifizierung kümmern müssen. Die REST-Aufrufe erfolgen automatisch im Sicherheitskontext des aktuellen Nutzers. In Listing 4 ist der entsprechende Auszug zu sehen.
Listing 4: "TrafficLightsCollection.ts" /// import Contracts = require("TFS/Build/Contracts"); import BuildRestClient = require("TFS/Build/RestClient"); export class TrafficLightsCollection { ... constructor(projectname: string, builddefinition: number, numberofbuilds: number, element: HTMLElement) { ... } public updateBuildState() { var buildClient = BuildRestClient.getClient(); buildClient.getBuilds(this.projectname, [this.buildDefinitionId, ..., this.numberOfBuilds).then((buildResults: Contracts.Build[]) => { this.builds = buildResults; this.renderLights(); }); }, err => { this.renderLights(); }); } }
Was jetzt noch fehlt, ist die Möglichkeit, unser Widget zu konfigurieren (Listing 5). Jedes Ampel-Widget benötigt eine entsprechende Konfiguration, in der die gewünschte Build-Definition ausgewählt und gespeichert wird. Die Grunddefinition der Konfigurationsansicht (HTML-Datei) verhält sich genauso wie die beim Widget. Im Code für die Konfigurationsansicht müssen zwei Lifecycle-Events überschrieben werden: load und onSave(). Hier können wir über das SDK unsere spezifischen Einstellungswerte als JSON-Definition laden und speichern. Damit die Livepreview im Konfigurationsmodus funktioniert, müssen entsprechende Änderungsevents publiziert werden. Die aktuellen Konfigurationswerte werden mittels TypeScript mit den UI-Controls synchron gehalten.
import BuildRestClient = require("TFS/Build/RestClient"); import Contracts = require("TFS/Build/Contracts"); export class TrafficLightsWidgetConfiguration { ... constructor(public WidgetHelpers) { } public load(widgetSettings, widgetConfigurationContext) { this.widgetConfigurationContext = widgetConfigurationContext; this.initializeOptions(widgetSettings); this.selectBuildDefinition.addEventListener( "change", () => { this.widgetConfigurationContext .notify(this.WidgetHelpers.WidgetEvent.ConfigurationChange, this.WidgetHelpers.WidgetEvent.Args(this.getCustomSettings())); }); ... return this.WidgetHelpers.WidgetStatusHelper.Success(); } public initializeOptions(widgetSettings) { ... var config = JSON.parse(widgetSettings.customSettings.data); if (config != null) { if (config.buildDefinition != null) { this.selectBuildDefinition.value = config.buildDefinition; } } } public onSave() { var customSettings = this.getCustomSettings(); return this.WidgetHelpers.WidgetConfigurationSave .Valid(customSettings); } } VSS.require(["TFS/Dashboards/WidgetHelpers"], (WidgetHelpers) => { WidgetHelpers.IncludeWidgetConfigurationStyles(); VSS.register("BuildTrafficLightsWidget.Configuration", () => { var configuration = new TrafficLightsWidgetConfiguration(WidgetHelpers); return configuration; }) VSS.notifyLoadSucceeded(); });
Ein komplettes Code-Listing wäre für diesen Artikel zu umfangreich. Entsprechend steht der gesamte Code dieser Beispiel-Extension auf GitHub zur Verfügung.
Entwicklungsworkflow von Extensions
Wer Extensions entwickelt, setzt sich primär mit klassischen Webentwicklungstechnologien auseinander. Doch wie bekommen wir nun die Extension in den TFS oder VSTS, und wie können wir debuggen? Als Erstes benötigen wir ein Extension Package in Form einer VSIX-2.0-Datei. Diese können wir anhand des Manifests und dem Kommandozeilentool tfx-cli erstellen. Das CLI wird als npm-Paket angeboten und entsprechend in unserem Projekt geladen. Wie bereits weiter oben erwähnt, möchten wir diesen Schritt als Grunt-Task in den Visual Studio Build integrieren. Wie in Abbildung 4 zu sehen ist, verknüpfen wir den Grunt-Task mit dem After-Build-Event von Visual Studio. Somit erhalten wir nach jedem Build das entsprechende Package.
Der nächste Schritt ist das Publizieren in TFS oder VSTS. Im Fall von TFS wird die VSIX-Datei über den Extension-Manager hochgeladen und in der gewünschten Team Project Collection installiert. Soll die Extension in VSTS getestet werden, ist das Publizieren im Marketplace notwendig. Keine Angst, auch hier können die Extensions zuerst getestet werden, bevor sie der Allgemeinheit zugänglich gemacht werden. Extensions können außerdem als private Erweiterungen markiert werden. Somit stehen sie nur ausgewählten VSTS-Accounts zur Verfügung.
Nun ist die Extension verfügbar und kann getestet werden. Hierzu wenden wir die Entwicklertools der gängigen Browser an. Die Extension läuft in einem iFrame; entsprechend kann die JavaScript-Konsole auf den gewünschten iFrame gefiltert, Elemente inspiziert und der TypeScript- bzw. JavaScript-Code mit dem Debugger analysiert werden.
Einbindung von externen Services mit REST-API und WebHooks
Die aktuelle Erweiterungsschnittstelle von VSTS/TFS erlaubt nur Frontend-Erweiterungen. Eigene Services oder Backend-Logik können nicht integriert werden. Entsprechend muss diese Art von Erweiterung extern als Windows-Service oder Webapplikation betrieben bzw. gehostet werden. VSTS und TFS stellen ein REST-API zur Verfügung, mit dem fast alle Aktionen getätigt werden können. Entwickelt man mit .NET, so stehen entsprechende NuGet-Pakete zur Verfügung.
Oft bedingen eigene Logikerweiterungen aber, dass man auf Ereignisse in VSTS/TFS reagieren kann. Ein Polling über das API wäre ein schlechter Ansatz. Entsprechend wurde in VSTS/TFS das Konzept von WebHooks implementiert. Über die Einstellungen unter SERVICE HOOKS können eigene WebHook-Empfänger registriert werden. Jede WebHook-Registrierung ist an ein bestimmtes Event in VSTS/TFS gebunden und wird bei dessen Eintreten ausgelöst.
Der WebHook-Empfänger ist an und für sich relativ einfach zu implementieren. VSTS/TFS führt einen POST-Request an den definierten Empfänger durch, und der Body beinhaltet alle Eventdaten, wie zum Beispiel die Work-Item-Daten des geänderten Work Items bei einem Work-Item-Update-Event. Einen POST Request abzuarbeiten, ist nicht weiter schwierig, jedoch ist es mit einem gewissen Zeitaufwand verbunden, die Routen zu konfigurieren und den Payload richtig zu parsen. Zum Glück greift uns Microsoft ein wenig unter die Arme. Für ASP.NET gibt es ein WebHook-Framework, das eine Basisimplementierung für das Senden und Empfangen von WebHooks bereitstellt. Zudem ist ebenfalls eine Implementierung für VSTS-WebHook-Events verfügbar, was einem das gesamte Parsen des Payloads abnimmt. Nach dem Erstellen eines Web-API-Projekts muss lediglich das NuGet-Paket Microsoft.AspNet.WebHooks.Receivers.VSTS hinzugefügt werden. Zu beachten ist hierbei, dass das Framework sowie die Pakete noch im RC-Status sind und entsprechend über NuGet als Pre-Release geladen werden müssen.
Um auf VSTS/TFS-Events reagieren zu können, muss von der entsprechende Basisklasse abgeleitet und die gewünschte Methode überschrieben werden (Listing 6).
public class VstsWebHookHandler : VstsWebHookHandlerBase { public override Task ExecuteAsync(WebHookHandlerContext context, WorkItemUpdatedPayload payload) { // sample check if (payload.Resource.Fields.SystemState.OldValue == "New" && payload.Resource.Fields.SystemState.NewValue == "Approved") { // your logic goes here... } return Task.FromResult(true); } }
Das WebHooks-Framework generiert über die Konfiguration entsprechende Web-API-Routen. Damit nicht jeder einen Post auf den WebHook-Receiver absetzen kann, wird ein Sender anhand einer ID Identifiziert, die in der Web.config-Datei hinterlegt wird (Abb. 5). Der entsprechende URL muss dann noch im VSTS/TFS hinterlegt werden.
Fazit
Die hier gezeigte UI-Integration bietet weitere Möglichkeiten, den eigenen Prozess bis in das Standard-Tooling zu integrieren. Somit sind nahezu grenzenlose Möglichkeiten für eine optimale Usability und Unterstützung der Nutzer gegeben. Wer sich mit HTML, CSS und JavaScript/TypeScript auskennt, dem dürfte das Erstellen eigener Extensions nicht allzu schwer fallen. Für Nutzer können die neuen Funktionen nur Vorteile haben, zumal die Werbefläche von VSTS/TFS fast nicht mehr verlassen werden muss.