GitHub Actions ermöglicht die Realisierung einer vollständigen Continuous Integration / Continuous Delivery Pipeline direkt in unserem GitHub Repository. In diesem Workshop erstellen wir eine komplette Build- und Release-Pipeline für ein bestehendes Node.js-Projekt und lernen dabei die Funktionsweise von CI/CD sowie die Kernkonzepte von GitHub Actions in der Praxis kennen.
Benötigte Software
GitHub Actions kann in jedem beliebigen GitHub Repository genutzt werden. Zum Ausführen der im Workshop erstellten Prozessschritte ist daher lediglich ein GitHub-Account sowie die Installation eines lokalen Git-Clients erforderlich.
Um die einzelnen Schritte des Workshops auch lokal auf ihrem System ausführen und testen zu können, empfiehlt sich die Installaton der JavaScript-Runtime Node.js. Für den letzten Pipeline-Step „Dockerize“ ist zudem eine Installation von Docker erforderlich.
Folgen Sie den Installationsanweisungen unter Node.js und Docker.
CI/CD in ein bestehendes Projekt einführen
Unser vorgegebenes Projekt verwendet einen Node.js-Tech-Stack und definiert ein minimales REST-API, das mit der Node-Bibliothek Express.js entwickelt wurde. Für die folgenden Szenarien sind bereits npm-Skripte im Projekt definiert:
- Ausführen der Anwendung
- Bauen und Paketieren der Anwendung
- Starten der Unit-Tests
- Durchführen einer statischen Code-Analyse
- Generieren der Sourcecode-Dokumentation
Die Ausführung dieser Schritte wollen wir nun automatisieren, indem wir sie kapitelweise in eine neue CI/CD-Pipeline überführen.
Das Projekt forken und klonen
Das GitHub Repository mit dem vorgegebenen Node.js-Projekt findet man auf GitHubActionsWorkshopStart.
Nutzen Sie auf der Webseite dieses Repositories den Button „Fork“, um eine Kopie des Projekts in ein neues Repository ihres eigenen GitHub Accounts zu überführen:
Das neu erstellte Repository steht Ihnen danach zum Klonen auf ihrem lokalen System zur Verfügung. Erweiterungen am Projekt können Sie jederzeit pushen.
Node.js Dependencies installieren
Die Node.js-Projektdatei package.json
definiert alle Bibliotheken, die für den Betrieb des Projekts erforderlich sind. Die Installation der Bibliotheken erfolgt über den Node Package Manager:
% npm install
npm-Skript zum Starten der Anwendung
Der Anwendungscode befindet sich in der Skriptdatei src/server.js
. Mithilfe der Bibliothek Express.js wird hier ein Webserver mit zwei simplen GET-Routen erstellt und auf Port 8181
des lokalen Systems gestartet.
Im Skript-Bereich unserer package.json
ist bereits das npm-Skript start
zum Starten der Anwendung definiert:
"scripts": { "start": "node src/server.js", ... }
Wir führen das Skript start
mit dem Node Package Manager (npm start
) aus und starten so unsere Anwendung:
Sofern der im Skript allokierte Port 8181
auf unserem lokalen System noch nicht belegt ist, sollte nun die folgende Ausgabe zu lesen sein:
Start Express.js server on http://0.0.0.0:8181
Unser Webserver ist nun auf 0.0.0.0:8181
etabliert und wartet auf eingehende Requests. Zum Testen der Funktionalität können wir mit Hilfe des Programms cURL einen HTTP-Request auf die definierte GET-Route /user
abschicken. Die vom Server erhaltene Response wird auf der Konsole ausgegeben.
Der cURL sollte nun die empfangenen HTTP-Header und den Response-Body mit drei konstanten User-Objekten im JSON-Format zurückgeben:
% curl -i 0.0.0.0:8181/user HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 135 ETag: W/"87-gHZz+1+n7pHvd2ovXSzGNYJ6RNM" Date: Thu, 14 Oct 2021 07:07:32 GMT Connection: keep-alive Keep-Alive: timeout=5 [{"name":"John Smith","username":"jsmith"},{"name":"Jane Williams","username":"jwilliams"},{"name":"Robert Brown","username":"rbrown"}]
Unser erster CI-Job: npm-start – Die Node.js-Anwendung ausführen und requesten
Die letzten beiden Shell-Befehle zum Starten und Requesten der Anwendung lassen wir nun im ersten Schritt unserer CI/CD-Pipeline automatisiert ausführen. Eine Pipeline wird durch einen GitHub Workflow repräsentiert und innerhalb eines GitHub-Repositories im Unterordner .github/workflows/
mittels einer yaml-Datei definiert. Dabei kann der Dateiname frei vergeben werden.
Wir definieren unsere CI/CD-Pipeline in der Workflow-Datei .github/workflows/node.js.yml
und legen dort unseren ersten Job npm-start
an:
name: CI/CD for Node.js app on: push: branches: [ '**' ] pull_request: branches: [ main ] jobs: npm-start: name: Start Node.js App runs-on: ubuntu-latest steps: - name: Checkout GitHub Repository uses: actions/checkout@v2 - name: Use Node.js 14.18 uses: actions/setup-node@v2 with: node-version: 14.18 cache: 'npm' - name: Install npm packages run: npm install - name: Start app run: npm start & - name: Send cURL Request to Node.js app run: curl -i 0.0.0.0:8181/user
Starten der Pipeline
Pushen wir die Workflow-Datei nun in einen beliebigen Branch unseres GitHub Repositories, so wird unsere neue Pipeline serverseitig ausgeführt. Jeder CI/CD-Durchlauf wird auf GitHub im Register „Actions“ des zugehörigen Repositories festgehalten und kann dort genauer inspiziert werden. Unter anderem können hier die einzelnen Durchläufe der Jobs und deren Steps eingesehen werden:
Aufbau der Workflow Datei
name:
Das Feld name
beschreibt diesen Workflow; der Name kann frei vergeben werden.
on:
Im on
-Bereich werden alle GitHub Events definiert, die einen Start dieses Workflows triggern. In unserem Fall wird der Workflow ausgelöst, wenn ein Git Push auf einen beliebigen Branch erfolgt oder wenn ein Pull Request in den main
-Branch gemerged wird.
jobs:
Der Bereich jobs
spezifiziert alle Jobs, aus denen dieser Workflow besteht. Ein Job stellt dabei einen in sich abgeschlossenen Prozessschritt der CI/CD-Pipeline dar, der in einer separaten virtuellen Umgebung ausgeführt wird. Daher muss jeder Job das gewünschte Betriebssystem auf dem er läuft explizit definieren. Unser erster Job npm-start
gibt hierfür über das Feld runs-on
mit ubuntu-latest
die aktuellste Version des Betriebssystems Ubuntu an.
steps:
Der steps
-Bereich definiert alle einzelnen Schritte, die beim Ausführen des zugehörigen Jobs sequentiell abgearbeitet werden. Ein Step kann dabei ein beliebiger Shell-Befehl oder eine vordefinierte Action sein. Zahlreiche Actions für die häufigsten Use Cases stehen im GitHub Marketplace zur Verfügung.
Steps im Job npm-start
Die folgenden fünf Schritte sind im steps
-Bereich unseres ersten Jobs npm-start
definiert und werden von der Pipeline sequentiell ausgeführt:
ID | Name | Beschreibung |
1 | Checkout GitHub Repository | Das GitHub Repository wird im aktuellen Arbeitsverzeichnis der CI/CD-Maschine geklont, sodass alle Resourcen dort zur Verfügung stehen. |
2 | Use Node.js 14.18 | Für die virtuelle Maschine wird eine Node.js-Umgebung auf v.14.18 initialisiert. Danach stehen alle gewohnten Node-Befehle zur Verfügung. |
3 | Install npm packages | Es erfolgt ein Aufruf von npm install durch den alle in der package.json definierten Node.js-Bibliotheken auf der CI/CD-Maschine installiert werden. |
4 | Start app | Das npm-Skript start wird über den Aufruf npm start ausgeführt und startet die Applikation auf der CI/CD-Maschine. Durch die Angabe des Ampersands wird dieser Prozess in den Hintergrund verlagert sodass die Ausführung der Pipeline an dieser Stelle nicht blockiert. |
5 | Send cURL Request to Node.js app | Es wird ein cURL-Request auf 0.0.0.0:8181/user ausgeführt und die empfangene Response auf der Konsole ausgegeben. |
Brechen der Pipeline
Unsere Pipeline weist uns bereits jetzt auf grundsätzliche Probleme in unserer Anwendung hin. Kann die Anwendung nicht erfolgreich gestartet und requested werden, so wird der Build als fehlgeschlagen quittiert. Das Abstürzen der Pipeline kann folgendermaßen getestet werden:
- Einbau eines Compiler-Fehlers in den JavaScript-Code der Anwendung, bevor der Express.js-Server gestartet wird.
Bauen und Paketieren der App
Nun ist unsere CI/CD-Pipeline erstellt und kann um neue Prozessschritte erweitert werden. Im zweiten Schritt automatisieren wir das Bauen und Paketieren der Anwendung mit einem JavaScript Module Bundler.
Im Projekt wird Webpack als Module Bundler eingesetzt. Beim Ausführen von Webpack wird das Anwendungsskript src/server.js
mit allen erforderlichen Node.js-Bibliotheken zusammengefasst und in die eigenständige JavaScript-Datei ./public/app-bundle.js
gebündelt.
npm-Skript zum Bauen via Webpack
Im Skript-Bereich der package.json
ist das npm-Skript webpack-production
für den Aufruf des Tools Webpack definiert:
"scripts": { ... "webpack-production": "webpack –mode=production", ... }
Das Skript können wir über den Node Package Manager mit npm run webpack-production
aufrufen:
Nach dem Durchlauf von Webpack wurde die Datei public/app-bundle.js
generiert. Da im Skript die Option mode=production
angegeben ist, hat Webpack die generierte JavaScript-Datei zudem minifiziert. Die erzeugte Datei beinhaltet nun den Anwendungscode und alle darin verwendeten Node-Bibliotheken. Somit kann unser Programm nun auch außerhalb unseres Projektordners mittels des node
-Befehls ausgeführt werden:
% node public/app-bundle.js Start Express.js server on http://0.0.0.0:8181
2.2. Unser zweiter CI-Job: build-production – Die Anwendung mit Webpack bauen
Wir automatisieren nun das Bauen unserer Anwendung mittels Webpack. Dafür erstellen wir unseren zweiten Job build-production
und rufen dort das npm-Script webpack-production
auf. Anschließend wird die generierte Datei public/app-bundle.js
als Build-Artefakt exportiert.
... build-production: name: Build Production App runs-on: ubuntu-latest steps: - name: Checkout GitHub Repository uses: actions/checkout@v2 - name: Use Node.js 14.18 uses: actions/setup-node@v2 with: node-version: 14.18 cache: 'npm' - name: Install npm packages run: npm install - name: Build Production App Bundle run: npm run webpack-production - name: Upload Production App Bundle to Build Artefacts uses: actions/upload-artifact@v1 with: name: app-bundle path: public/app-bundle.js
Parallele Ausführung
Da an das Starten des neuen Jobs build-production
keine Bedingungen geknüpft sind, wird er in der CI/CD-Pipeline zeitgleich mit unserem ersten Job npm-run
ausgeführt.
Steps im Job build-production
Auch in unserem zweiten Job beziehen sich die ersten drei Steps lediglich auf die Initialisierung der Node.js-Umgebung. Sie sind daher identisch mit denen des vorhergehenden Jobs npm-start
.
ID | Name | Beschreibung |
1 | Checkout GitHub Repository | Identisch zum ersten Job npm-start |
2 | Use Node.js 14.18 | |
3 | Install npm packages | |
4 | Build Production App Bundle | Das npm-Skript webpack-production wird auf der CI/CD-Maschine gestartet. Dies generiert die alleinstehende und minifizierten JavaScript-Datei public/app-bundle.js . |
5 | Upload Production App Bundle to Build Artefacts | Hier wird erstmals eine vorgegebene Action aufgerufen. Die Action actions/upload-artifact lädt die gebaute Anwendung public/app-bundle.js zu den Build Artefakten hoch. |
Build-Artefakte stehen nach dem Durchlauf der Pipeline zum Download zur Verfügung. Auf der Übersichtsseite des Pipeline-Laufs werden sie nach dem Durchlauf der Pipeline unter Artifacts aufgelistet:
Build-Artefakte kommen oft auch bei der Bereitstellen von Log-Dateien für bestimmte Tools oder Prozesse zum Einsatz. Darüberhinaus können Build-Artefakte auch von nachfolgenden CI/CD-Jobs heruntergeladen und weiterverarbeitet werden.
Brechen der Pipeline
Scheitert der Build mit Webpack, so weist uns die Pipeline nun darauf hin. Dieses Szenario kann folgendermaßen getestet werden:
- Einbau eines Compiler Fehlers in den JavaScript-Code der Anwendung.
- Einbau eines Fehlers in die Webpack-Konfigurationsdatei
webpack.config.js
.
Unit Tests
Im Projekt wird das Test-Framework Jest eingesetzt – es ermöglicht die Abdeckung unseres Produktionscodes mit Unit Tests und das Sammeln einer Test Code Coverage. Zudem können damit zukünftige Erweiterungen an unserer Applikation testgetrieben entwickelt werden.
In der JavaScript-Datei test/server.test.js
ist eine Test Suite mit drei Test Cases formuliert. In jedem Test Case wird die Anwendung neu gestartet und eine bestimmte Route via HTTP angefordert.
npm-Skript zum Ausführen der Unit Tests
Im Script-Bereich unserer package.json
ist der Aufruf von Jest als npm-Skript festgehalten:
"scripts": { ... "test": "jest –ci", ... }
Damit können wir die Tests über den Node Package Manager mit npm test
ausführen.
Test- und Coverage-Reports
Neben den Ausgaben des Testlaufs auf der Kommandozeile werden von Jest zwei Test-Reports generiert:
- Ein Test-Report im JUnit XML Format mit den Ergebnissen des Testlaufs in der Datei
public/test-results.xml
. - Die Ergebnisse der Code Coverage im Clover XML Format in der Datei
public/coverage/clover.xml
.
CI-Job Nummer Drei: run-unit-tests – Unit Tests und Code Coverage ausführen
Das Ausführen automatisierter Tests ist wohl einer der gebräuchlichsten Anwendungsfälle einer Continuous-Integreation-Pipeline und stellt einen wichtigen Faktor zur Sicherstellung einer hohen Code Qualität dar.
Wir ergänzen unseren Workflow um einen weiteren Job, indem wir das npm-Skript test
automatisiert aufrufen. Die generierten Test- und Coverage-Reports werden nach Abschluss der Tests als Build-Artefakte exportiert.
... run-unit-tests: name: Unit Tests needs: build-production runs-on: ${{ matrix.os }} strategy: matrix: node-version: [14.x, 16.x] # See supported Node.js release schedule at # https://nodejs.org/en/about/releases/ os: [ubuntu-latest, ubuntu-18.04] steps: - name: Checkout GitHub Repository uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm install - run: echo Running Jest Unit tests - run: npm test - name: Upload Test Report Artefact uses: actions/upload-artifact@v1 with: name: test-results-${{ matrix.os }}-${{ matrix.node-version }} path: public/test-results.xml - name: Upload Test Coverage Report Artefact uses: actions/upload-artifact@v1 with: name: test-coverage-${{ matrix.os }}-${{ matrix.node-version }} path: public/coverage/clover.xml
Bedingter Start des Jobs
needs: build-production
Der Job wird nur dann gestartet, wenn der zuvor definierte Job build-production
erfolgreich abgeschlossen wurde. Hierdurch wird auf einen unnötige Start der Tests verzichtet, falls die Anwendung gar nicht korrekt kompiliert oder gebaut werden konnte.
In der grafischen Übersicht des Workflows wird diese Abhängigkeit und die damit festgelegte sequentielle Ausführung der beiden Jobs run-unit-tests
und build-production
ersichtlich:
Einsatz der Build Matrix
strategy: matrix:
GitHub Actions ermöglicht das Ausführen eines Jobs auch unter Verwendung einer Build Matrix. Hierdurch werden Jobs wiederholt mit allen möglichen Kombinationen der spezifizierten Matrix durchgeführt. In unserem Fall für alle Kombinationen der spezifizierten Betriebssysteme und Node.js-Umgebungen. Die eingesetzten Werte können innerhalb des Jobs über die beiden Umgebungsvariablen ${{ matrix.node-version }}
und ${{ matrix.os }}
verwendet werden.
In unserem Fall wird nach jedem Aufruf der Tests der Dateiname der beiden Build-Artefakte um diese beiden Variablen erweitert. So bleibt nachvollziehbar, unter welchen Umständen diese Reports generiert wurden.
Brechen der Pipeline
Scheitert der Testlauf, so kennzeichnet unsere Pipeline den Workflow als fehlgeschlagen. Das kann folgendermaßen getestet werden:
- Entfernen der im Livecode definierten Route
/
. - Verändern der in den Tests erwarteten JSON-Objekte für die Route
/user
. - Unterschreiten des in der Jest-Config
jest.config.js
definierten Code-Coverage-Schwellwerts von 80 Prozent, beispielsweise durch das Entfernen des Test Cases für die Route/user
.
Statische Code Analyse
Der Einsatz eines Code Style Linters ermöglicht die Sicherstellung eines einheitlichen Code Styles für unseren JavaScript-Quellcode. Wird beim Aufruf des Linters im Projektcode eine Abweichung vom vordefinierten JavaScript-Code-Style gefunden, so meldet der Linter einen Fehler.
Im Projekt wird der Linter ESLint eingesetzt und der sicherzustellende Code Style in der Datei .eslintrc.js
festgehalten. Hier sind ein paar gebräuchliche Code-Style-Regeln definiert – unter anderem ist eine Einrückungstiefe von vier Spaces und eine maximale Zeilenlänge von 120 Zeichen festgelegt.
npm-Skripte für ESLint
Analog zu den Tests kann auch für ESLint die Generierung eines Reports im JUnit XML-Format angegeben werden. In diesem Fall erzeugt ESLint leider keine Ausgabe etwaige Linter-Fehler auf der Konsole, was die manuelle Sichtung der Resultate erschwert. Als Workaround wurden im Projekt einfach zwei npm-Skripte für den Einsatz von ESLint in der package.json
festgehalten:
"scripts": { ... "eslint-scan": "eslint \"{src,test}/**/*.{js,jsx}\"", "eslint-report": "eslint --format junit -o public/linter-results.xml \"{src,test}/**/*.{js,jsx}\"", ... }
ESLint ausführen
Lokal können wir ESLint nun mit dem Skript eslint-scan
(npm run eslint-scan
ausführen. Auftretende Linter-Fehler werden dabei auf der Konsole ausgegeben.
Linter-Report erstellen
Auch das zweite ESLint-Skript eslint-report
führt den Linter mit npm run eslint-report
lokal aus. Dabei werden eventuelle Linter-Fehler aber nicht auf der Konsole ausgegeben, sondern ausschließlich in der generierten Report-Datei public/linter-results.xml
festgehalten.
Unser vierter CI-Job: run-code-inspection – Statische Code Analyse durchführen
Nun automatisieren wir den Durchlauf unseres Code Style Linters und schaffen damit eine Continuous Inspection in unserer CI/CD-Pipeline. Der neuen Job run-code-inspection
ruft beide npm-Skripte für ESLint hintereinander auf – so werden eventuelle Linter-Fehler im Log der Pipeline deutlich festgehalten und zudem die Report-Datei public/linter-results.xml
generiert. Letztere wird anschließend als Build-Artefakt exportiert.
... run-code-inspection: name: Code Inspection needs: build-production runs-on: ubuntu-latest steps: - name: Checkout GitHub Repository uses: actions/checkout@v2 - name: Use Node.js 14.18 uses: actions/setup-node@v2 with: node-version: 14.18 cache: 'npm' - run: npm install - name: Run ESLint Scan on app sourcecode run: npm run eslint-scan - name: Generate ESLint Report from app sourcecode scan run: npm run eslint-report - name: Upload Linter Report Artefact uses: actions/upload-artifact@v1 with: name: linter-results path: public/linter-results.xml
Bedingter Start des Jobs
needs: build-production
Auch dieser Job hat als Startbedingung den erfolgreichen Abschluss des Jobs build-production
angegeben. Somit wird er nach dessen erfolgreichem Abschluss parallel zu dem Job run-unit-tests
ausgeführt. Dies wird in der Übersicht unserer Pipeline ersichtlich:
Brechen der Pipeline
Ein Scheitern des Linters führt zu einem fehlgeschlagenen Durchlauf der Pipeline. Das kann beispielsweise durch eine der folgenden Änderungen am JavaScript-Quellcode der Anwendung oder der Tests reproduziert werden:
- Einfügen eines unerwarteten Whitespaces.
- Überschreiten der maximal erlaubten Zeilenlänge von 120 Zeichen.
- Falsche Einrückung einer beliebigen Zeile.
Generieren einer Sourcode Dokumentation
Im Projekt wird JSDoc zur Generierung einer Sourcode-Dokumentation im HTML-Format eingesetzt. Die Dokumentation wird dabei für den JavaScript-Projektcode in src/server.js
erstellt.
npm-Skript zum Ausführen von JSDoc
Der Aufruf des Tools JSDoc ist bereits in einem npm-Skript festgehalten. Als Zielordner für die zu generierende HTML-Dokumentation ist hier der Ordner public/jsdoc
angegeben. Als Eingangsdateien wird lediglich unser Programmskript src/server.js
angegeben – für unsere Tests in test/server.test.js
wird also per Design keine Sourcecode-Dokumentation erstellt.
"scripts": { ... "jsdoc": "jsdoc --destination public/jsdoc src/server.js", ... }
Auch dieses Skript können wir über den Node Package Manager mit Hilfe von npm run jsdoc
starten.
Generierte Sourcecode-Dokumentation
Nach dem Durchlauf von JSDoc wurde die HTML-Dokumentation im Verzeichnis public/jsdoc
erstellt. Zur Einsicht der Dokumentation können Sie die Indexseite public/jsdoc/index.html
in Ihrem Browser öffnen.
CI-Job Nummer Fünf: generate-jsdoc – Sourcode Dokumentation generieren
Wir rufen das npm-Skript jsdoc
im neuen Job generate-jsdoc
unseres Workflows auf. Nach dem Durchlauf von JSDoc wird das gesamte Verzeichnis public/jsdoc
als Build-Artefakt exportiert.
... generate-jsdoc: name: Generate Documentation needs: [run-unit-tests, run-code-inspection] runs-on: ubuntu-latest steps: - name: Checkout GitHub Repository uses: actions/checkout@v2 - name: Use Node.js 14.18 uses: actions/setup-node@v2 with: node-version: 14.18 cache: 'npm' - run: npm install - run: npm run jsdoc - name: Upload JSDoc documentation uses: actions/upload-artifact@v1 with: name: jsdoc-sourcecode-documentation path: public/jsdoc
Bedingter Start des Jobs
needs: [run-unit-tests, run-code-inspection]
Die Sourcecode-Dokumentation soll erst erstellt werden, nachdem die beiden vorhergehenden Jobs run-unit-tests
und run-code-inspection
erfolgreich abgeschlossen wurden.
Brechen der Pipeline
Sollte die Generierung der Sourcecode-Dokumentation scheitern, kennzeichnet die Pipeline den Build als fehlgeschlagen. Dieser Fall ist eher unwahrscheinlich und kann beispielsweise durch eine falsche Konfiguration folgendermaßen nachgestellt werden:
- Umbenennen der Datei
src/server.js
, sodass JSDoc die im npm-Skript definierte Eingabedatei nicht mehr findet.
Erstellen eines GitHub Release Items
Ein GitHub Release repräsentiert eine veröffentlichte Version unseres Programms und wird anhand einer getaggten Git Revision erstellt. Einem GitHub Release Item kann zudem auch ein oder mehrere ausgewählte Build Artefakte beigefügt werden.
GitHub Releases werden ausschließlich auf dem GitHub-Server angelegt und können nicht lokal realisiert oder getestet werden – daher existiert hierfür auch kein npm-Skript.
CI-Job Nummer Sechs: create-github-release – Erstellen eines GitHub Releases beim Pushen eines Tags mit semantischer Versionsnummer
Wir gestalten das Release-Management für unser Node.js-Projekt nun Git Aware und lassen unsere CI/CD-Pipeline automatisch ein GitHub Release Item erstellen, sofern ein Git Tag mit einer semantischen Versionsnummer ins Repository gepusht wird.
Unser Workflow startet aktuell noch nicht, wenn ein Git Tag gepusht wird. Daher fügen wir dieses neue Event zum on
-Bereich unseres Workflows hinzu. Als erwarteter Tagname wird das RegEx-Pattern *.*.*
angegeben.
name: CI/CD for Node.js app on: push: branches: [ '**' ] tags: [ '*\.*\.*' ] pull_request: branches: [ main ] jobs: ...
Nun fügen wir unseren neuen Job create-github-release
zu unserem Workflow hinzu. Der Release-Build unserer Applikation wird dem Benutzer so standardisiert zur Verfügung gestellt. Dieser Prozessschritt fällt in einer CI/CD-Pipeline unter den Begriff Continuous Delivery.
... create-github-release: name: Create GitHub Release if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') needs: generate-jsdoc runs-on: ubuntu-latest steps: - name: Parse release version from git tag run: echo "release-version=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Print out parsed release version run: echo Parsed release version is:${{ env.release-version }} - name: Download Production Build Artefact uses: actions/download-artifact@v1 with: name: app-bundle path: public - name: Delete Production Build Artefact uses: geekyeggo/delete-artifact@v1 with: name: app-bundle - name: Rename Production Build Artefact by inserting release version run: mv "public/app-bundle.js" "public/app-bundle-v${{ env.release-version }}.js" - name: Upload Renamed Production Build Artefact uses: actions/upload-artifact@v1 with: name: app-bundle-v${{ env.release-version }} path: public/app-bundle-v${{ env.release-version }}.js - name: Create GitHub Release uses: ncipollo/release-action@v1 with: name: Release v${{ env.release-version }} token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ env.release-version }} artifacts: 'public/app-bundle-v${{ env.release-version }}.js' allowUpdates: true omitBody: true removeArtifacts: true
Bedingte Job-Ausführung
needs: generate-jsdoc
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
Durch die kombinierte Angabe der beiden Felder needs
und if
wird unser neu geschaffener Job create-github-release
nur dann ausgeführt, wenn der Job generate-jsdoc
erfolgreich abgeschlossen wurde und wenn ein Git Tag gepusht wurde.
Steps im Job create-github-release
Im Gegensatz zu allen bisherigen Jobs muss diesmal kein npm-Skript ausgeführt werden. Somit muss für diesen Job auch keine Node.js-Umgebung initialisiert oder installiert werden.
ID | Name | Beschreibung |
1 | Parse release version from git tag | Der Job parst den Namen des gepushten Tags und speichert ihn in der Umgebungsvariable ${{ env.release-version }} . |
2 | Print out parsed release version | Die in der Umgebungsvariablen ${{ env.release-version }} gespeicherte Release-Versionsnummer wird auf der Konsole zu Debugzwecken ausgegeben. |
3 | Download Production Build Artefact | Das vom Job build-production hochgeladene Build-Artefakt mit der paketierten Applikation wird heruntergeladen und auf dem CI/CD-Server unter public/app-bundle.js gespeichert. Hierfür wird die vorgegebene Action actions/download-artifact verwendet. |
4 | Delete Production Build Artefact | Das vom Job build-production hochgeladene Build-Artefakt wird aus den Build-Artefakten gelöscht. Hierfür wird die vorgegebene Action actions/delete-artifact verwendet. |
5 | Rename Production Build Artefact by inserting release version | Die heruntergeladene Datei public/app-bundle.js wird auf dem CI/CD-Server umbenannt indem der Dateiname um die Release-Versionsnummer erweitert wird. |
6 | Upload Renamed Production Build Artefact | Die umbenannte Datei mit der paketierten Anwendung wird als ein neues Build-Artefakt hochgeladen. |
7 | Create GitHub Release | Die Erstellung des GitHub Releases wird abschließend mit der vorgegebenen GitHub Action ncipollo/release-action realisiert. Dabei wird das Build Artefakt mit der paketierten Applikation dem Release hinzugefügt. |
Testen des Jobs
Zum Testen unseres neuen Jobs create-github-release
committen wir die Erweiterung unserer Workflow-Datei und fügen ein Git Tag mit einer Versionsnummer zu dieser Revision hinzu. Pushen wir anschließend dieses Tag, so können wir den Durchlauf unseres neuen Jobs und die Erstellung eines neuen Release Items auf GitHub beobachten.
% git tag 1.0.0 HEAD % git push origin 1.0.0
Alle Release-Items eines GitHub Repositories werden auf der Webseite im Tab Code unter Releases angezeigt.
Deployment und Betrieb der Anwendung in einem Docker Container
Wir können unsere Anwendung in einer gleichbleibenden Umgebung betreiben, testen und weitergeben, indem wir sie in einen Docker-Container verpacken. Hierfür wurde in der Root unseres Projekts bereits ein einfaches Dockerfile
erstellt:
# base image for this container FROM node:14 # make container's port 8181 accessible to the outside EXPOSE 8181 # copy entire 'public' directory into the container COPY public/* public/ # run the app bundle with node CMD ["node", "./public/app-bundle.js"]
Als Basis-Image wird hier die Dockerumgebung Node.js v.14 verwendet. Der von unserer Anwendung verwendete Port 8181
wird innerhalb des Docker-Containers nach außen zugänglich gemacht. Anschließend wird unser lokaler Pfad public
mitsamt dessen Inhalt in das Arbeitsverzeichnis des Docker-Containers kopiert und die dadurch im Container verfügbare Anwendung in der Datei ./public/app-bundle.js
unter Verwendung des node
-Befehls gestartet.
Erstellen des Docker Images
Bevor ein neuer Docker-Container instanziiert werden kann, muss aus dem Dockerfile ein neues Docker-Image erstellt werden. Das erfolgt mit Hilfe des Befehls docker build
in unserer Projekt Root und weist dabei dem neu erstellten Docker-Image den Namen express-js-app:14.18
zu.
% docker build . --tag express-js-app:14.18
Das neu erstellte Docker-Image sollte danach mit docker images
in der Liste der lokal verfügbaren Docker Images zu finden sein.
Bauen und Starten des Docker Containers
Mit dem Befehl docker run
kann aus dem Docker Image ein neuer Docker-Container erstellt und gestartet werden:
% docker run --name express-js-app-container --detach --publish 45678:8181 --tty express-js-app:14.18
Für den neuen Container wird der Name express-js-app-container
vergeben. Die Angabe der Option --detach
startet den Container im Hintergrund und löst diesen somit vom aufrufenden Prozess los. Die Option --publish
mappt den exponierten Port 8181
innerhalb des Containers auf den Port 45678
unseres Host Systems.
Der gestartete Docker Container sollte nach einem Aufruf von docker container ls
mit dem Status „Up“ gelistet werden
Requesten unserer Anwendung im Docker Container
Wir können die Anwendung im laufenden Docker Container nun ansprechen, indem wir mit curl -i 0.0.0.0:45678/user
einen cURL auf den gemappten lokalen Port 45678
durchführen:
Es sollte nun eine erfolgreiche Response der Route /user
aus der im Docker-Container laufenden Anwendung empfangen worden sein:
HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 135 ETag: W/"87-gHZz+1+n7pHvd2ovXSzGNYJ6RNM" Date: Fri, 15 Oct 2021 08:43:21 GMT Connection: keep-alive Keep-Alive: timeout=5 [{"name":"John Smith","username":"jsmith"},{"name":"Jane Williams","username":"jwilliams"},{"name":"Robert Brown","username":"rbrown"}]
Stoppen des laufenden Docker-Containers
Mit dem folgenden Befehl können wir den laufender Docker-Container auch wieder stoppen:
% docker stop express-js-app-container
CI-Job Nummer Sieben: dockerize – Dockerisierung unserer Anwendung
Wir fügen nun unseren letzten Job dockerize
zu unserem Workflow hinzu. Dabei können wir die beiden zuletzt verwendeten Docker-Kommandos eins zu eins übernehmen:
... dockerize: runs-on: ubuntu-latest needs: build-production name: Dockerize Production App steps: - name: Checkout GitHub Repository uses: actions/checkout@v2 - name: Download Production Build Artefact uses: actions/download-artifact@v1 with: name: app-bundle path: public - name: Build Docker Image from Dockerfile run: docker build . --tag express-js-app:14.18 - name: Create and run Docker Container from Docker Image run: docker run --name express-js-app-container --detach --publish 45678:8181 --tty express-js-app:14.18 - name: Sleep 5 seconds (wait till Express.js app is running) run: sleep 5s - name: Send a cURL request to the Docker container run: curl -i 0.0.0.0:45678/user - name: Stop the Docker Container run: docker stop express-js-app-container - name: Wait until Docker Container is down run: docker wait express-js-app-container
Auch dieser Job kommt ohne die Initialisierung einer Node.js-Umgebung aus. Vor dem Erstellen des Docker-Containers wird das Build-Artefakt app-bundle
ins Verzeichnis public
heruntergeladen, damit es in den Docker-Container deployed werden kann. Da der Container im Hintergrund gestartet wird, erfolgt anschließend eine Verzögerung von 5 Sekunden bevor der cURL-Request an die Anwendung im Container gesendet wird. Anschließend wird der Docker Container wieder gestoppt.
Der GitHub Marketplace bietet zudem Actions, mit denen ein Docker-Image unter Angabe der entsprechenden Authentifizierung direkt ins Docker Hub gepusht oder in ein Orchestrierungssystem wie Kubernetes deployed werden kann. In unserer CI/CD-Pipeline wird dieser Schritt auch als Continuous Deployment bezeichnet.
Pipeline Tutto Completto
Unser GitHub-Actions-Workflow ist nun komplett. In der schematischen Darstellung ist jetzt auf einen Blick ersichtlich, welche Jobs unserer Build- und Release-Pipeline parallel und welche sequentiell ausgeführt wurden:
CI/CD im Handumdrehen
GitHub Actions ermöglicht eine schnelle und leichtgewichtige Realisierung vollständiger CI/CD-Pipelines. Dabei sind keine externen Ressourcen wie Build-Server oder CI/CD-Services erforderlich. Unterstützt werden auch Multi-Container-Setups und Testing via Docker. Darüber hinaus bietet Github zahlreiche vorgegebene CI-Templates für diverse Umgebungen und Tech Stacks.
Entwicklern ermöglicht das einen sehr schnellen Start in das Thema CI/CD. Auch die große Community und die große Anzahl an Actions die im GitHub Marketplace zur Verfügung stehen und mit der die häufigsten Use Cases schnell umgesetzt werden können, zahlen auf die hohe Developer Experience ein.
Ich freue mich wenn ich Ihnen einen schnellen Einstieg in die Kernkonzepte von GitHub Actions und in die praktische Arbeit mit CI/CD-Pipelines geben konnte. Für Feedback oder Rückfragen können Sie mich gerne via christopher.stock@mayflower.de erreichen.
Bild von fanjianhua auf Freepik
Schreibe einen Kommentar