Nahaufnahme einer Person, die mit einem Tablet auf einer Decke sitzt.

TechWiese Blog

Einfach Azure DevOps Extensions in einem VS Code Dev Container entwickeln und debuggen

28. Juni 2021

Portrait Bild von Tobias Fenster

Tobias Fenster

Dieser Blogbeitrag ist ein Repost und stammt im Original aus dem Know-how-Bereich von TechWiese, dessen Artikel in diesem Blog aufgegangen sind.

Seit etwa 2013 nutze ich Azure DevOps bzw. damals noch Team Foundation Server. In den ersten Tagen war ich kein großer Fan, aber das Produkt hat sich kontinuierlich verbessert und ist jetzt an einem Punkt, an dem es meiner Meinung nach eine sehr gute Allround-Lösung für alle wichtigen Aspekte der Softwareentwicklung ist, aber auch für andere Szenarien genutzt werden kann. Während GitHub als führende Source Control / CI/CD Plattform von Microsoft an Wichtigkeit gewinnt, ist vor allem der Boards-Teil für die Organisation und Abwicklung von Projekten in Azure DevOps weitaus besser. Er bringt bereits sehr viel Funktionalität mit und ist stark anpassbar, hat aber auch ein tolles Erweiterungsmodell. Allerdings kann der Einstieg in die Erweiterung von Azure Boards ein wenig mühsam sein, da die Dokumentation recht verstreut ist und man viele veraltete Informationen findet. Hinzu kommt, dass die Toolchain ziemlich heterogen ist, was auch nicht gerade hilfreich ist. Als ich vor kurzem eine Erweiterung als Proof of Concept erstellen musste (für die Zeiterfassung in Azure DevOps, die Daten direkt an unser ERP sendet), hatte ich wieder einmal damit zu kämpfen alle Teile zusammenzubringen, obwohl ich es schon ein paar Mal gemacht hatte. Aus diesem Grund habe ich mich entschlossen, ein sehr einfaches Paket zu schnüren, das es ermöglicht, eine funktionierende Azure DevOps-Web-Extension mit nur sehr wenigen Setup-Schritten zu erhalten, die auf den Boards-Teil ausgerichtet ist und die - meiner Erfahrung nach - relevantesten Szenarien abdeckt.

Das TL;DR

Mein Beispiel hat ein Backend, welches nur Daten zurückgibt, aber hilft zu verstehen, wie man auf ein Backend zugreifen würde, inklusive Autorisierung. Es hat natürlich auch ein Frontend, das dem Endbenutzer angezeigt wird und in React implementiert ist. Es zeigt, wie man auf Änderungen in einem WorkItem hört, sich mit dem Backend verbindet und Änderungen am WorkItem vornimmt. Ich wollte vermeiden, mich auf lokal installierte SDKs und Versionen zu verlassen, also habe ich alles in einen VS Code Dev Container gepackt. Angenommen, du hast bereits WSL2 und die VS Code remote container extension installiert, dann musst du folgendes tun:

  1. Verwende VS Code, um mein Repo in einen Container zu klonen, indem du den Befehl "Remote-containers: Clone repository in Container volume" ausführt. Es wird dich fragen, ob du den Workspace in diesem Ordner öffnen willst, was du tun solltest.
  2. Ersetze <put-your-publisher-here> an zwei Stellen in frontend/vss-extension.json mit deiner Marketplace Publisher ID. Wenn du keine hast, erstelle eine, indem du der Dokumentation folgst.
  3. Führe npm i im Frontend-Ordner aus, um alle Node-Module zu installieren.
  4. Führe npm run publish im Frontend-Ordner aus. Dies wird die Erweiterung auf dem Visual Studio Marktplatz veröffentlichen, der auch von Azure DevOps genutzt wird, so dass wir sie nutzen können. Du musst ein Personal Access Token mit den richtigen Berechtigungen eingeben. Wenn du dir nicht sicher bist, was das ist oder was die richtigen Berechtigungen sind, folge wieder der Dokumentation
  5. Gehe zum Visual Studio Marketplace management portal und teile deine Erweiterung mit deiner Organisation, wie hier erklärt.
  6. Wenn wir schon hier sind, lade auch das Zertifikat aus dem gleichen Dropdown herunter und trage den Wert in backend -> .vscode -> launch.json ein, ersetze <put-your-key-here>
  7. Gehe zu den Organisationseinstellungen der Organisation, die du im vorherigen Schritt verwendet hast und installiere die Erweiterung
  8. Gehe zurück zu VS Code und starte die "Start backend" Debug-Konfiguration, um das Backend zu starten, mache dasselbe mit "Start frontend", um das Frontend zu starten und auch mit "Start firefox", um Firefox zu starten (weil Chrome / Edge keine iframes debuggen kann). Wenn du eine Fehlermeldung wie "can't find task 'build'" siehst, dann musst du eventuell das VS Code Fenster einmal neu laden und es erneut versuchen.
  9. In Firefox wirst du eine Fehlermeldung sehen, dass die Seite nicht vertrauenswürdig ist. Das liegt daran, dass wir ein selbstsigniertes Zertifikat verwenden. Du musst "Erweitert" -> "Risiko akzeptieren und fortfahren" verwenden, um das Zertifikat für dieses Dev-Szenario zu akzeptieren. Gehe danach zu einem WorkItem in der Organisation, in der du die Erweiterung installiert hast. Es wird einen Reiter "Example" haben, gehe dorthin.
  10. Setze irgendwo in frontend -> src -> example -> example.tsx einen Breakpoint, z.B. in Zeile 97, um den Backend-Aufruf zu verfolgen. Setze auch einen Breakpoint in Backend -> Controllers -> ExampleController.cs, z.B. in Zeile 24, um die Anfrage zu verfolgen.
  11. Klicke in Azure DevOps auf "Ask the backend for a greeting". Dies sollte dir zunächst erlauben, das Frontend zu debuggen, wenn der Breakpoint erreicht wird und dann im nächsten Schritt das Backend
  12. Als Bonus kannst du eine Änderung im Frontend vornehmen, z.B. in der Buttonbeschriftung in Zeile 86 in example.tsx und sehen, wie die Extension automatisch mit deiner Änderung neu lädt.

Um dir eine Vorstellung zu geben, so sollte das Ergebnis in Azure DevOps aussehen, nachdem du die Extension geladen und einmal auf den vorgesehenen Button geklickt hast:

Screenshot der Erweiterung in Azure DevOps

Damit hast du eine Umgebung zur Verfügung, mit der du eine Azure DevOps Extension mit einfachem Debugging und Hot Reload entwickeln kannst. Bitte beachte, dass dies nur für Dev- und vielleicht PoC-Szenarien gedacht ist, nicht für die Produktion. Aber wenn du nur einen schnellen und einfachen Startpunkt zum Experimentieren willst, sollte das gut funktionieren.

Die Details: Der Dev-Container

Da das Backend meines Beispiels in C# (.NET) geschrieben ist und das Frontend React / TypeScript ist, brauche ich C# und Node.js in meinem Dev Container. Der Standard C# (.NET) Container erlaubt es bereits, das Hinzufügen von Node.js über eine Konfigurationseinstellung zu aktivieren (s. Dokumentation), also gab es nicht viel, was ich tun musste. Ich fügte GitLens (weil es fantastisch ist) und den Firefox Debugger als zusätzliche Extensions und ein paar npm packages hinzu. Auch hier habe ich, um die Benutzung zu vereinfachen, Frontend und Backend in denselben Workspace und denselben Container gelegt. Wenn du ihn öffnest, wirst du drei "Ordner" im Workspace finden: Einen für das C#-Backend, einen für das React-Frontend und einen für die Devcontainer-Dateien.

Screenshot des Workspace in VS Code

Der eigentliche Root-Ordner des Repo enthält nur die .gitignore-Datei, die Workspace-Definitionsdatei und die obligatorische README.md

Die Details: Das Frontend

Das Frontend ist mit React gebaut und wenn du das noch nie benutzt hast, könnte es ein wenig verwirrend sein. In diesem Fall würde ich das großartige offizielle Anfänger-Tutorial empfehlen. Die interessanten Teile aus Sicht einer Azure DevOps Erweiterung sind:

  • Der Zugriff auf das SDK, z.B. um Event-Listener zu registrieren: Dies geschieht über SDK.init(), am besten in componentDidMount() deiner React-Komponente. Sobald du sie initialisiert hast, kannst du deine Listener registrieren. Ich zeige hier nur zwei (onFieldChanged und onLoaded), aber du kannst die komplette Registrierung hier finden.
public componentDidMount() {
    SDK.init().then(() => {
        SDK.register(SDK.getContributionId(), () => {
            return {
                // Wird aufgerufen, wenn das aktive Workitem geändert wird
                onFieldChanged: (args: IWorkItemFieldChangedArgs) => {
                    this.setState({
                        eventContent: `onFieldChanged - ${JSON.stringify(args)}`
                    });
                },

                // Wird aufgerufen, wenn ein neues Workitem in der UI geladen wird
                onLoaded: (args: IWorkItemLoadedArgs) => {
                    this.setState({
                        eventContent: `onLoaded - ${JSON.stringify(args)}`
                    });
                },
...
  • Um den Backend-Aufruf zu machen, verwende ich das beliebte axios npm-Paket auf eine sehr standardmäßige Weise, aber die Autorisierung ist interessant: Ein Aufruf von SDK.getAppToken() gibt den richtigen Token zurück, der dann in den Headern des axios-Aufrufs zur Autorisierung verwendet wird. Der Backend-Teil wird unten in den Backend-Details erklärt.
const appToken = await SDK.getAppToken();
const response = await axios.get(`http://localhost:5000/Example?name=${this.state.name}`, {
    headers: {
        `Authorization': `Bearer ${appToken}`
    }
});
  • Das letzte Thema, das spezifisch für Azure DevOps Erweiterungen ist, ist der Zugriff auf SDK Dienste, z.B. einer um mit WorkItems zu interagieren: Ein Aufruf von SDK.getService<IWorkItemFormService>(WorkItemTrackingServiceIds.WorkItemFormService) gibt uns den richtigen Dienst, der dann verwendet wird, um den Wert eines Feldes im WorkItem zu setzen. Die vollständige Dokumentation für die Azure DevOps Extension API und SDK Pakete ist hier
const workItemFormService = await SDK.getService<IWorkItemFormService>(
    WorkItemTrackingServiceIds.WorkItemFormService
);
workItemFormService.setFieldValue(
    "System.Title",
    `"${response.data}" set by extension`
);

Der Rest ist Standard React und Typescript Code. Ich habe auch versucht, das Paket azure-devops-ui zu verwenden, aber wie schon bei früheren Gelegenheiten bin ich daran gescheitert, dass es die richtigen Styles zieht. Wenn du das erfolgreich nutzen konntest, lass mich bitte wissen, wie...

Die Details: Das Backend

Das Backend ist ziemlich trivial, da es nur einen Controller hat, der nur eine Get-Methode hat, implementiert in einer Zeile

public string Get(string name) => string.IsNullOrEmpty(name) ? "Hello, please let me know your name" : $"Hello {name}!";

Einfacher geht's nicht mehr... Der Autorisierungsteil ist allerdings etwas interessanter. Du benutzt das SDK im Frontend, um ein Access Token zu erhalten (siehe die Frontend-Details oben), welches automatisch mit einem Schlüssel signiert wird, der spezifisch für deine Extension ist. Um diesen Schlüssel zu validieren, musst du das Zertifikat aus dem Extension Management Portal verwenden (siehe das TL;DR oben). Auf diese Weise kannst du in deinem Backend sicherstellen, dass ein Aufruf von deiner Extension im Frontend kommt. Der Quellcode ist mehr oder weniger identisch mit der offiziellen Dokumentation zum Thema, daher werde ich nicht ins Detail gehen, aber du kannst ihn hier finden. Die einzige besondere Zeile ist die Verwendung des richtigen IssuerSigningKey bei der Validierung des Tokens:

TokenValidationParameters validationParameters = new TokenValidationParameters
{
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(EXTENSION_SIGNING_KEY)),
    ...
};

Die Details: Quellen

Wie ich zu Beginn geschrieben habe, ist die gesamte Dokumentation rund um das Thema Azure DevOps Extensions etwas verstreut, aber das sind die Quellen, die ich für mein "all-in-one" Beispiel verwendet und zusammengestellt habe:

Ich hoffe, das hilft dir bei deinen ersten Schritten zu deiner eigenen Azure DevOps Web Extension!

Hinweis: Dieser Post ist ursprünglich auf Englisch auf dem Blog des Authors erschienen.