GitHub Projekte automatisch bauen und verteilen mit Azure DevOps

Mein letzter Blogeintrag liegt mehr als ein Jahr zurück. Ich hab es aus Zeitgründen einfach nicht geschafft im letzten Jahr mal etwas zu schreiben. Dies soll sich nun aber wieder ändern.

Ganz untätig war ich im letzten Jahr nicht, was das Thema Systems Engineering angeht. Dazu werden sicher noch ein paar Beiträge folgen. Seit Februar arbeite ich nun auch wieder als Systemingenieur bei Karl Mayer und daher steht das Thema bei mir täglich wieder mehr im Fokus, als dies die letzten zwei Jahre der Fall war.

Heute möchte ich jedoch darüber berichten, wie ich mein FMC4SE Enterprise Architect Plugin nun automatisch aus den Quellcodedateien in Github übersetzen (Contineous Integration, CI) und auch freigeben (Contineous Delivery, CD) kann.

Anforderungen

Was waren die Anforderungen an den CI/CD-Prozess?

Bisher hatte ich auf meinem Rechner zuhause einen Jenkins-Build-Server eingerichtet, der die Quelldateien aus GitHub bei Bedarf übersetzt und ein MSI-Setup daraus generiert hat. Dieses habe ich dann manuell wieder in GitHub hochgeladen.

Ziel war es nun, da die Quelldateien sowieso in GitHub öffentlich zugänglich sind auch den Build-Prozess auf einem Server in der Cloud durchzuführen. Ein automatisches Bereitstellen einer Softwarefreigebe in GitHub wäre natürlich auch super.

Zusätzlich sollte folgendes beachtet werden:

  • Die Versionsnummer des Builds soll sich aus 4 Stellen zusammen A.B.C.D, wobei A.B.C aus einer Datei kommt (version.txt), die man manuell im Projekt anpasst und eincheckt. Angewendet wird hierbei das Semantic Versioning. Die Stelle D soll ein Zähler sein, der bei jedem neuen gestarteten Build automatisch hochgezählt wird (1, 2, 3, ….)
  • Da die Applikation eine .NET Applikation ist, spielen hier die Versionsnummern der erzeugten Softwarekomponenten (DLLs) eine wichtige Rolle. Diese sind bei C#-Projekten über die Datei AssemblyInfo.cs gesteuert. Vor dem Übersetzten sollte also die aktuelle Version hier gesetzt werden.

Realisierung

Nach einer kurzen Recherche bei GitHub bin ich auf Azure DevOps von Microsoft gestoßen. Dieser Dienst ist für Open Source Projekte kostenlos und bietet die Möglichkeiten die oben genannten Anforderungen zu erfüllen. Genau genommen braucht man die Azure DevOps Pipelines.

Build-Schritte mit YML beschreiben

Mit Hilfe von Skripten in der Sprache YML (YAML) kann man die Schritte beschreiben, um einen automatisierten Build-Prozess aufzusetzen. Leider ist die Dokumentation noch etwas verbesserungswürdig. Das Ganze Portal scheint auch aktuell noch stark erweitert zu werden. Daher hat es einige versuche bedurft, bis alles so funktioniert hat wie ich mir das gedacht hatte. Dieser Blog-Artikel soll daher auch ein bisschen helfen, wenn Sie vielleicht ähnliches vorhaben.

Wenn man eine neue Build-Pipeline anlegt, wird automatisch eine azure-pipelines.yml Datei angelegt, die die Prozessschritte beschriebt. Diese wird dann auch im GitHub Projekt eingecheckt. Das ganze hat auch den Vorteil, dass der Quellcode und die Vorschrift wie daraus ausführbare Software entstehen soll unter der gleichen Versionskontrolle steht.

Prozesschritte

Um das EA-Plugin zu erzeugen sind folgende Schritte notwendig:

  1. Holen der manuellen Versionsnummer aus der Datei version.txt und Zwischenspeicherung in einer Variablen
  2. Setzen der Build-Nummer aus der manuellen Versionsnummer und dem Build-Zähler
  3. Herstellung der benötigten Nuget-Pakete (referenzierte externe Bibliotheken)
  4. Update der Versionsnummer in den AssemblyInfo.cs Dateien
  5. Übersetzen der Software. Hierbei entsteht als Ergebnis eine Windows-Installationsdatei (.msi)
  6. Umbenennen der msi-Datei, damit auch die Versionsnummer im Dateinamen erscheint
  7. Publizierung der erzeugten msi-Datei, damit man diese nach Ende des Buid-Vorgangs auch manuell im Azure DevOps Portal herunter laden kann

Herausgekommen ist folgende YML-Datei, die man natürlich auch in GitHub ansehen kann.

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- master

pool:
  vmImage: 'VS2017-Win2016'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  versionToBuild: ''
  
# Set name initialy to the BuildID (This is a counter, every time incremented when a build is started)
name: $(BuildID)

steps:

# read version from version.txt file and store it in a variable (versionToBuild)
- script: |
    set /p VER=<src/Plugin/version.txt @echo ##vso[task.setvariable variable=versionToBuild]%VER% # Print out the version to build and update the buildNumber - script: | echo The version from src/Plugin/version.txt is $(versionToBuild) @echo ##vso[build.updatebuildnumber]$(versionToBuild).$(build.buildnumber) # install nuget toolset - task: NuGetToolInstaller@0 # get the referenced nuget packages from nuget.org - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' # update assembly version in AssemblyInfo.cs with the buildNumber value - powershell: | function Update-SourceVersion { Param ([string]$Version) $NewVersion = 'AssemblyVersion("' + $Version + '")'; Write-output $NewVersion $NewFileVersion = 'AssemblyFileVersion("' + $Version + '")'; foreach ($o in $input) { Write-output $o.FullName $TmpFile = $o.FullName + ".tmp" get-content $o.FullName | %{$_ -replace 'AssemblyVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', $NewVersion } | %{$_ -replace 'AssemblyFileVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', $NewFileVersion } > $TmpFile

            move-item $TmpFile $o.FullName -force
        }
    }
    Write-output 'Modifiing AssemblyInfos.'
    foreach ($file in "AssemblyInfo.cs", "AssemblyInfo.vb" ) 
    {
        get-childitem -recurse |? {$_.Name -eq $file} | Update-SourceVersion $(build.buildnumber) ;
    }
   
# build software
- task: VSBuild@1
  inputs:
    solution: '$(solution)' 
    msbuildArgs: '/p:installerFileVersion=$(build.buildnumber)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    
# rename .msi file
- script: |
    cd src/Plugin/Setup/bin/Release
    ren FMC4SE-Plugin-Setup.msi FMC4SE-Plugin-Setup_$(build.buildnumber).msi
    cd ..
    cd ..
    cd ..
    cd ..
    cd ..

# copy build results
- task: CopyFiles@2
  inputs:
    contents: 'src/Plugin/Setup/bin/Release/*.msi'
    targetFolder: $(Build.ArtifactStagingDirectory)
    flattenFolders: true

# publish build results
- task: PublishBuildArtifacts@1
  inputs:
    pathtoPublish: '$(Build.ArtifactStagingDirectory)'
    artifactName: Output

 

 

Fallstricke

Folgende Dinge musste ich erst experimentell herausfinden, da mir hier die Dokumentation nicht so gut geholfen hat:

  • Der Zähler, der jedes mal wenn ein Build automatisch oder manuell angestossen inkrementiert wird, ist $(BuildID)
  • Die Build-Nummer kann man während des Buildvorgangs durch Setzen der Variablen build.updatebuildnumber setzen
  • Mir ist es nicht gelungen die Version in AssemblyInfo.cs mit einem speziellen task zu setzen. Daher das enthaltene PowerShell Skript. Vielleicht hat hier jemand eine bessere Lösung.
  • Es gibt einen task VSBuild für Visual Studio Build und einen für MSBuild, der steht für das Microsoft Build-Kommando. Bei beiden gibt es die Möglichkeit Parameter an MSBuild zu übergeben. Grosse Falle: Beim VSBuild-Task heißt der Eingabeparemeter msbuildArgs beim MSBuils-Task msbuildArguments.

Fazit

Am Ende funktioniert der automatische Build aber sehr gut. Damit ist das Thema CI erledigt. Wie man auch noch eine Softwarefreigabe per Knopfdruck auf GitHub in den Release-Bereich bekommt, beschreibe ich in einem Folgebeitrag demnächst.