Erleichterung im Entwickleralltag

Automatisierte Pull Request Checks mit Azure Function Apps
Keine Kommentare

Jeder Entwickler kennt sie: Pull Request Reviews, ein notwendiges Übel in jedem kollaborativen Software-Projekt. Wäre es nicht traumhaft, wenn man so viel wie möglich automatisieren könnte? Natürlich gibt es einige Tools wie SonarQube, Unit Tests und Linting, die Entwicklern das Leben erleichtern. Jedes Projekt bringt jedoch auch spezifische prozessbezogene Anforderungen mit, seien es Namenskonventionen, gewisse Ticket-Stati oder andere Richtlinien, die eine Automatisierung erschweren.

Für diese speziellen, prozessspezifischen Anforderungen können recht einfach Cloud-Funktionen herangezogen werden. Anhand eines Beispiels möchte ich zeigen, wie leicht es ist, in Azure DevOps eine sehr individuelle Status Policy und Pull Request Validation mit Hilfe von Azure Function Apps umzusetzen. Ein kurzer Begriffsexkurs: Bei Azure DevOps handelt es sich um die Lösung von Microsoft für Projektplanung, Code Management, CI/CD Tooling und Testing. Für das folgende Beispiel werden ausschließlich die Cloud-Version und Features, die sich nicht im Public Preview befinden, genutzt. Azure Function Apps ist das Azure Cloud-Äquivalent zu AWS Lambda Functions oder Google Cloud Functions, kurz Function as a Service (FaaS).

In dem Beispielprojekt benutze ich einen bekannten Commit Message Standard: Conventional Commits. Er definiert die Struktur von Commit Messages. Damit will ich sicherstellen, dass der Pull Request Titel einem von mir spezifizierten Format entspricht. Ausgangspunkt für das Experiment ist ein Standard-Projekt-Setup in Azure DevOps. Der gewählte Projektprozess ist an dieser Stelle egal. Das Repository im Projekt hat einen Master Branch als default, gegen den Pull Requests erstellt werden müssen.

Der Service Hook

Azure DevOps bietet die Möglichkeit, Service Hooks einzurichten. Ich werde diese dazu nutzen, ein Update Event des Pull Requests an den zu erstellenden Webservice zu schicken. Die Option, einen Service Hook einzurichten, findet man in den Projektoptionen unter dem Punkt “General > Service Hooks”. Es gilt, “Web Hooks” aus der Liste auszuwählen.

Service Hooks in Azure DevOps

Wie bereits erwähnt, werde ich als Trigger “Pull Request updated” verwenden. Dieses Event hat den Vorteil, dass der Titel nach jeder Änderung des Pull Requests erneut validiert wird, da eine Änderung des Titels leider kein explizites Event erzeugt. Alternativ ist es möglich, das Event “Pull Request merge attempted” zu wählen.

Als URL verwende ich vorerst einen Platzhalter, da der Service noch nicht deployed wurde. Es sollte sich jedoch um eine gültige URL handeln.

Die Optionen „Resource details to send“ sollte auf All gestellt werden. „Messages to send und Detailed messages to send“ kann mit None deaktiviert werden, da die Daten nicht verarbeitet werden müssen.

Man speichert nun den Service Hook ab und erstellt einen Pull Request, um die Funktionalität zu testen und einen exemplarischen Payload zu erzeugen, der sich in der folgenden Entwicklung über ein Tool wie Postman an die lokale Entwicklungsumgebung senden lässt.

Nachdem der Pull Request erstellt ist, sollte man über das Kontext-Menü im “Service Hook”-Bereich die “History” öffnen können und einen Eintrag zu dem Pull Request vorfinden. Im Reiter “Request” vom zugehörigen Eintrag findet man den Payload, der den Service Hook an den Service schicken wird.

Azure Function als Validierungsservice

Los geht es mit der Validierungsfunktion: Ich möchte zuerst alle nötigen Metadaten des Pull Requests laden, prüfen ob der Titel dem Conventional Commits Model folgt und zuletzt den Status im Pull Request setzen. Hierzu werde ich die oben bereits erwähnte Azure Function einsetzen. Eine laufende Azure Subscription ist für die experimentelle, lokale Nutzung nicht erforderlich. Für alle VS Code User sind die Azure Plugins sehr zu empfehlen. Ich werde eine Azure Function mit Typescript und einem HTTP-Trigger verwenden. Detaillierte Anleitungen zur Einrichtung von Azure-Function-Projekten finden sich in der Dokumentation von Microsoft.

Nachdem man eine Standard Azure Function mit HTTP-Trigger eingerichtet hat, kann man sich endlich dem Code widmen.

Microsoft bietet das NPM Package azure-devops-node-api an, welches die Interaktionen mit der API deutlich vereinfacht. Der erste Schritt ist eine Authentifizierung:

import * as azdev from "azure-devops-node-api";

// Die URL der DevOps Instanz
let orgUrl = "https://dev.azure.com/yourorgname";

// Azure Devops Personal Access Token ;
let token: string = process.env.AZURE_PERSONAL_ACCESS_TOKEN;  

let authHandler = azdev.getPersonalAccessTokenHandler(token); 
let connection = new azdev.WebApi(orgUrl, authHandler);

Den Personal Access Token (PAT) kann man in Azure Devops anlegen. Ein Hinweis an dieser Stelle: Der PAT für Services in produktiver Nutzung sollte unbedingt über einen Service-Account erstellt werden. So ist der Service nicht mit dem persönlichen Account verknüpft. Die benötigten Scopes für das exemplarische Szenario sind:

  • Code (Status)

Darauf folgend verwende ich die GitAPI.

const gitApi = await connection.getGitApi()

Den für diesen Zwecke relevanten Payload des Web-Hook-Aufrufs kann man ebenfalls in einer const vorhalten.

const pr = req.body.resource as GitPullRequest
Da ein Pull Request neue, sogenannte Iterationen erstellt, sobald ein neuer Commit erzeugt wurde, ist es erforderlich, die aktuelle Iteration zu identifizieren, damit man diese mit dem korrekten Status versehen kann.

const currentIteration = (await gitAPI.getPullRequestIterations(pr.repository.id, pr.pullRequestId)).pop()

Nun ist es möglich, den ersten Status des Service im Pull Request anzuzeigen.

let status: GitPullRequestStatus = {
    context: {
        genre: 'my-validator',
        name: 'title-checker'
    },
    iterationId: currentIteration.id,
    state: 1 as GitStatusState 
}

await gitAPI.createPullRequestStatus(status, pr.repository.id, pr.pullRequestId)

Der Kontext kann frei definiert werden, sollte jedoch einzigartig im Projektkontext sein.
Mögliche state Werte sind:

  • Not Set = 0
  • Pending = 1,
  • Succeeded = 2,
  • Failed = 3,
  • Error = 4,
  • NotApplicable = 5, Dies entfernt ebenfalls die Statusmeldung vom Pull Request

Erfahrungsgemäß sollte der Status 1 möglichst früh gesetzt werden, um in der UI zu visualisieren, dass die Policy bearbeitet wird. Navigiert man nun auf den Pull Request im DevOps UI, sollte man im Abschnitt “Status” die Policy vorfinden, die mit einem Icon den Status anzeigt. Wer will, kann hier alle Status ausprobieren, um zu sehen, welche Icons welchen Status repräsentieren.

Damit hat man alles, was notwendig ist, um mit dem Pull Request über den Service zu interagieren. Man kann nun damit beginnen, den Pull-Request-Titel anhand des Commitizen Standards zu validieren. Auch hier verwende ich ein NPM Package: @commitlint/lint. Die Regeln lassen sich sehr flexibel an ziemlich jedes Bedürfnis anpassen. Für das Beispiel folge ich einem vereinfachten Regelwerk.

const RULES: LintRuleConfig = {
    'body-leading-blank': [1, 'always'],
    'footer-leading-blank': [1, 'always'],
    'header-max-length': [2, 'always', 80],
    'scope-enum': [
        2,
        'always',
        []
    ],
    'scope-empty': [
        2,
        'never'
    ],
    'subject-empty': [2, 'never'],
    'type-empty': [2, 'never'],
    'type-enum': [
        2,
        'always',
        [
            'build',
            'chore',
            'ci',
            'docs',
            'feat',
            'fix',
            'perf',
            'refactor',
            'revert',
            'style',
            'test'
        ]
    ]
}

Für die finale Überprüfung des Pull-Request-Titels ist nur noch ein einfacher Funktionsaufruf erforderlich.

import lint from '@commitlint/lint'
// ...
const report: LintOutcome = await lint(pr.title, RULES)

Je nach Resultat setzt man den Status in den Pull Request.

let status: GitPullRequestStatus = {
    context: {
        genre: 'my-validator',
        name: 'title-checker'
    },
    iterationId: currentIteration.id,
    state: report.valid ? 2 : 3 as GitStatusState
}

await gitAPI.createPullRequestStatus(status, pr.repository.id, pr.pullRequestId)

Der Service ist nun in seiner einfachen Form einsetzbar. Es sollte erwähnt werden, dass in dem beschriebenen Beispiel einfachheitshalber auf ein explizites Error Handling verzichtet wurde.

Nachdem man die Funktion in einer Azure Subscription deployed hat, lassen sich die Service Hooks auf die URL https://<APP_NAME>.azurewebsites.net/api/<FUNCTION_NAME> anpassen. Falls der Authorization Level auf function gesetzt wurde, antwortet die App jedoch mit einem 401 Fehler, wenn nicht ein Header Entry mit dem Function Key übermittelt wurde. Das HTTP-Header-Feld sollte mit folgendem Wert gefüllt werden:

x-functions-key:
Um die Policy im Pull Request Kontext als “required” zu definieren, kann diese nun in den Branch Policies im Abschnitt “Status Checks” ausgewählt werden.

Fazit

Wie das Beispiel zeigt, ist es mit nur wenigen Zeilen Code möglich, zusätzliche Status Policies für Pull Requests zu erstellen. Weitere API-Schnittstellen für beispielsweise Work Items, Builds, Releases und Wiki in Kombination mit weiteren Event Hooks eröffnen viele Möglichkeiten, auch abseits von Pull-Request-Validierungen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Abonnieren
Benachrichtige mich bei
guest
0 Comments
Inline Feedbacks
View all comments
X
- Gib Deinen Standort ein -
- or -