Erfolgreiche Softwareentwicklung

In diesem Beitrag versuche ich eine lose Auflistung von Punkten zu bieten, die eine Softwareentwicklung erfolgreich machen. Klar ist: Alles kann nichts muss. Also ist es weder so, dass man alles einsetzen muss, noch ist der Erfolg bei Einsatz garantiert.

Kommen wir also zu meinen Empfehlungen. Vermutlich ist kollidieren sogar einige meiner Empfehlungen. Daher gilt: Nehmt Euch raus, was Euch gefällt und setzt es für Euch richtig um. Denn wie so oft im Leben gibt es mehr als nur schwarz und weiß. Viel Spaß.

  • Einsatz eines Versionskontrollsystems (z.B. GIT)
  • Einsatz von Entwicklungszweigen im VCS (Versionskontrollsystems). Branches.
  • UnitTests: Für einzelne Klassen (Basisbausteine) bis hin zu Komponenten (Fertigbauteile) sollten UnitTests eingesetzt werden und bei CI/CD ausgeführt werden. UnitTests von Anfang an schreiben.
  • Für Komponentenübergreifende Teile sollten Modultests gemacht werden. Testszenarien. GUI-Tests, Replay-Tests und bei Testreleases und sowieso bei Releases ausgeführt werden.
  • Für die Gesamtanwendung sollte eine QA-Abteilung mit Menschen sich dran setzen. Die ganze Zeit und speziell zu Releases.
  • Einrichtung einer CI/CD-Pipeline . UnitTests sollten dort ausgeführt werden, besser: Statische Analysen + Code-Style. Als Ergebnis wird ein Installer/Paket oder ein Deployter Container o.ä. erwartet. Ein Tester kann also gleich ran an den Speck!
  • Release-Versionierung. Es kann für Regressionen wichtig sein, auf einen laufenden früheren Stand zurückzugehen. Also: „War das früher auch schon kaputt, oder ist das neu?“. Daher: Setups reproduzierbar machen (Installer, VMs, Container deployments etc.)
  • Release-Management. Es braucht einen Plan, wie man von Release zu Release kommt und wie ältere gepflegt werden und welche Merkmale „gemerged“ werden.
  • Ticketsystem einsetzen. Es ist unmöglich in einem Wust von Code und Information den Überblick zu behalten. Aufgaben müssen verwaltet werden. Tickets immer mit Commits im Versionskontrollsystem verknüpfen (wo sinnvoll).
  • Logging einsetzen. Erfindet das Rad nicht neu! Nutzt Logging-Frameworks. So kann auch auf externe Server geloggt werden etc.
  • Audit-Log. Je nach Anwendung frühzeitig einführen, denn später anflanschen ist doof. Es gibt immer wieder sicherheitsrelevante Dinge zu loggen -> Audit-Log
  • Baut die Anwendung in Schichten auf. Es hat sich bewährt.
  • ORM ist Pflicht. Die Datenschicht ist oft eine Relationale Datenbank. Vermeidet SQL-Zeug. Überbrückt die OO-ER-Lücke mit einem Object Relational Mapper (ORM) wie z.B. Entity Framework!
  • Scheut Euch nicht, auch mal andere Konzepte auszuprobieren. Sie könnten für das zu lösende Problem eine einfachere, zuverlässigere Lösung parat haben. Genannt sei das Aktor-Modell oder Reactive oder Prolog-artige Horn-Klauseln.
  • Baut Internationalisierung (i18n) von Anfang an ein. Das schärft gleich den Sinn, wann etwas lokalisiert dargestellt wird, und wann eine Darstellung kulturinvariant sein soll (bei Persistenz). Außerdem: Später hinzufügen ist wieder mal schlecht und teuer.
  • Baut Barrierefreiheit (accessiblity, a11y) von Beginn an ein. Es ist inzwischen in manchen Ländern oder Bereichen (öffentliche Hand) Pflicht. Aber: Großes Thema, nicht einfach. Screenreader sollten aber an den Text kommen können.
  • Setzt immer Unicode ein. Geht davon aus, dass die Anwender alle gültigen Zeichen der Welt einsetzen wollen und werden. Kodiert Dateien mit UTF-8-BOM.
  • Lernt bei Developer Falsehoods und dem gigantischen Git-Repo über Falsehoods, was so die typischen Fehlannahmen sind und vermeidet sie. Schon gewusst: Vor+ Nachname sind eine Besonderheit, die es hier gibt.
  • Bedenkt Sicherheit im Sinne von Security und setzt Verschlüsselung ein. Nutzt aber immer Bibliotheken und erfindet nichts selbst.
  • Paarprogrammierung. Setzt das XP-Merkmal der Paar-Programmierung ein. Ein Junior kann von einem Senior so viel lernen und Umgekehrt. Oder Wissen aus verschiedenen Programmbereichen verteilen. Vorteil: Es gibt nicht mehr einzelne Koryphäen, da sich Wissen dupliziert. Man lernt Programmiertechniken und Prozesse und die Entwickler sind konzentrierter dabei und machen weniger Fehler, was den „doppelten Aufwand“ mehr als Wett macht.
  • Nutze TDD – Test driven develoment. Nicht überall aber bei Kernkomponenten/Klassen empfohlen. Die dabei entstehenden UnitTests können gleich bleiben und in der CI-Pipeline verwendet werden.
  • Coding-Standard. Entwickelt einen Formatierungsstandard und forciert ihn mit Programmen wie StyleCop.
  • Code-Reviews. Macht z.b. alle 14 Tage ein öffentliches Review. Das ist ein unglaublich gutes Werkzeug, um Fehler zu finden und einander Einblick und Tricks zu vermitteln.
  • Check-In mit Pull-Requets und 4-Augen-Prinzip. Nutzt die Mechanismen, die moderne Entwicklungsplattformen bieten. Bei Git gibt es einen zweistufigen Commit mit Code-Review. Nutzt das und lasst einen Check-In immer von einer anderen Person reviewen. Es hilft immens, Fehler von Beginn an zu vermeiden.
  • Refaktorisieren. Mut zur Refaktorisierung. In der Regel kommt was besseres dabei raus. Schiefe Balken müssen gerade gerichtet werden. Nutzt Tools dazu.
  • Kommentiert, aber auch nicht zu viel. Dokumentation veraltet schnell, Kommentare veralten auch. Daher Pflegt zumindest diese. Keine Kommentare ist auch falsch. Mittelweg! Bewährt hat sich, öffentliche Methoden zu kommentieren mit (ohoh) XML-Doc und die Klasse an sich. Dies gefällt mir insbesondere bei fremdem Code, wenn wieder „die nächste Klasse“ auftaucht, und man wieder sich fragt : „warum ist diese Klasse jetzt nötig, was verdammt soll ihre Aufgabe jetzt genau sein?“. Wer mir diese Frage gleich oben beantwortet (und die sollte recht konstant bleiben), der hat bei mir einen Stein im Brett! Sparam im Quellcode zu kommentieren ist auch keine gute Idee. Ich vergesse recht schnell, welche kranken und doch genialen Ideen ich da hatte.
  • Nutzt schlaue Tools. Tools, die Euch das Leben einfacher machen und z.B. Code überprüfen, generieren oder automatisch umstrukturieren. Genannt sei hier z.B. Re-Sharper. Viele haben Angst vor der Automatik, aber sie ist deterministisch und wenn man es einmal gelernt hat, ist sie ein Segen. Denn sie denkt meist sogar an mehr, als man selbst. Dazu gehören auch Analysetools, wie z.B. der Nachfolger von FxCop oder LINTer. Sie analysieren Code auf typische Fehler und weisen Entwickler darauf hin.
  • Automatisiert, wo es geht. Das ist DevOps. Alle dummen, manuellen Schritte sollten wenn möglich automatisch getriggert und ausgeführt werden.

Automatisierte Builds in Azure DevOps/VSTS

Ein essentieller Teil einer erfolgreichen Softwareenwicklung in einem Unternehmen ist Continuous Integration, ein ständig verfügbarer kontinuierlicher Build. Und daneben gehört natürlich Continuus Deployment hinein. So werden fertige Builds auch gleich auf Herz und Nieren geprüft.

In diesem Artikel geht es darum, wie man aus einem .Net 4.7-Projekt mit mehreren Setups und exe-Dateien (GUI und CUI) einen continuus Build hinbekommt. Dabei sollen die Versionsnummern immer stetig hochgezählt werden und sowohl in jeder Assembly als auch in jedem Setup landen. Zusätzlich gibt es einige GUI-Komponenten, die das unsägliche .Net-licensing verwenden. Auch dafür gibt es eine Lösung.

Der Build wird eingerichtet auf Azure DevOps. Modernere Versionen sollten Ähnlich oder einfacher einzurichten sein.

Ziel

Folgende Probleme sollen adressiert und gelöst werden:

  • .Net Licensing von GUI-Komponenten (z.b. PerpetuumSoft)
  • Versionsnummern automatisch vergeben
  • Basis der Versionsnummer konfigurierbar (beim Build)
  • Versionsnummer in jeder Assembly (Asm. Ver + File Ver.)
  • Versionsnummer in jedem MSI, „Drüberinstallieren“ soll gehen.

Lizensierung

Mit .Net Licensing hat Microsoft ehemals ein Standardverfahren etabliert, mit dem Komponenten (v.a. Windows Forms) lizensiert werden können. Aufgrund des Alters und seiner Art, ein eher unsägliches Ding. Aber man kommt damit klar.

Die primäre Idee von .Net Licensing ist es, für GUI-Komponenten Entwicklerlizenzen während der Entwicklung durchzusetzen. Das sieht dann etwa so aus: Der Entwickler bindet eine GUI-Komponente ein und bei jeder Verwendung prüft diese, ob auf dem Entwicklerrechner eine Lizenz vorhanden ist. Ist das nicht der Fall, kann etwa ein Nag-Screen aufpoppen. Dazu gehört noch der Lizenzcompiler LC.Exe, der aus *.licx-Dateien *.licenses-Dateien baut. Das erledigt jeweils die Komponente, die auf ihre Weise checkt, ob sie auf diesem Entwicklersystem lizenziert ist. Deshalb steht auch in der *.licx-Datei eine Liste von Komponenten, die der LC.EXE aufrufen soll (per Reflection). Im Ergebnis entsteht eine *.licenses-Datei, die als Eingebettete Ressource in die Assembly eincompiliert wird. Danach ist die Komponente dauerhaft auch auf Kundensystemen lizensiert.

Das Problem ist nun: So lange alles nur auf Entwicklermaschinen lief, ging alles gut. Der CI-Build läuft aber auf einer frisch instantiierten VM in der Cloud und diese hat keine Entwicklerlizenz installiert. Ergo kann es passieren, dass ein Nag-Screen aufgeht und keiner ihn kann klicken. Der Build wird nie fertig.

Info dazu auf Stackoverflow

Lösung: Man macht einmal einen ordentlichen Build auf einem Entwicklersystem und sammelt dort die *.licenses-Dateien ein (Bin-VZ). Dann setzt man den Dateityp in der SLN von *.licx auf „None„. Sodann checkt man die *.licenses Dateien pro Assembly ein und bindet sie als „Embedded Resource“ zusätzlich in die SLN ein.

Ab da läuft der Build ohne Nag-Screen durch. Nur beim Aktualisieren der Komponenten muss man diesen Vorgang wiederholen. Man macht somit LC.EXE arbeitslos und verhindert das Aufrufen des Lizensierungs-Codes.

Versionierung

Kommen wir zum Thema Versionsnummern. Die wichtigste Zutat für einen automatiserten Build sind korrekte und konfigurierbare Versionsnummern. Nur so kann man feststellen, ob ein Artefakt neuer oder älter ist und zu welchem Versionsstand er gehört. .Net bringt uns 3 verschiedene Versionsnummern, die je Assembly gesetzt werden können. Eine Erklärung dazu und zu „Semantischer Versionierung“ findet man hier:

Umsetzung

Um eine automatische Versionierung hinzubekommen braucht man zwei Zutaten:

  • Eine Pipeline-Variable für die Assemblyversion
  • Ein Skript, um die Versionsvariable zu verrechnen
  • Ein Build-Target für MSBUILD
Die Variable BuildVersionOfAssembly wird angelegt mit dem aktuellen Standardwert

Der Plan ist es, die Versionierung so zu machen, wie es in semantic versioning empfohlen wird. Also die verschiedenen Typen von Assembly-Versionsattributen korrekt zu setzen. Sprich: AssemblyVersion und AssemblyInformationalVersion ist die ‚fixe‘ Version. Darüber referenzieren und finden sich die abhängigen Assemblies. Die letzte Stelle ist leer. Derweil ist AssembyFileVersion quasi dieselbe Nummer aber mit gesetzter Build-Nummer, die irgendwie ausgerechnet wird.

Konkret kommt über die Build-Pipeline eine Variable AssemblyVersion hinein in Form einer 3-Stelligen Version. Z.B. „11.22.33“

Dies resultiert in:
AssemblyVersion = „11.22.33“
AssemblyInformationalVersion = „11.22.33“
AssembyFileVersion = „11.22.33.1928“
Das Versions-Rechen-Skript setzt die neue Variable BuildVersionOfAssembly auf den Wert „11.22.33.1928“ und exportiert sie als DevOps-Variable auf die Konsole. Die Möglichkeiten dabei sind grenzenlos. Üblicherweise sollte man die BuildID nutzen, welche eine stetig steigene Nummer je getriggerten Build im CI-System ist. Problem: Die Zahl wird irgendwann größer als die erlaubten 16-bit an dieser Stelle. Lösung: Man macht Modulo 2^16 und hofft, dass dies nur alle 65k Builds zu Problemen führt und inkrementiert dann schnell die Patch-Version (Stelle 3). Wahlweise kann man auch die oberen Bits auf Patch-Version geben; dann aber nur zwei Versionsstellen vorgeben. (alles hier nicht gemacht).

Skript

Version Script (PowerShell inline):

# This script computes the $(BuildVersionOfAssembly) variable off of the User set $(VersionOfAssembly) build var.
# adds the buid pipeline ID (ordered number) as the last figure of the version

# VersionOfAssembly is expected to be "x.y.z" BuildVersionOfAssembly becomes "x.y.z.buildID"
Write-Host  "Looking for VersionOfAssembly. Found : $Env:VersionOfAssembly"

if( -not ("$Env:VersionOfAssembly" -match "^\d+\.\d+\.\d+$" ))
{
   Write-Host  "##vso[task.LogIssue type=error;]Error: $Env:VersionOfAssembly does not match X.Y.Z format."
   exit 1
}

$BuildVersionOfAssembly = $Env:VersionOfAssembly + '.' + $Env:Build_BuildId
Write-Host  "BuildVersionOfAssembly= $BuildVersionOfAssembly"

#To set a variable in DevOps pipeline, use Write-Host
Write-Host "##vso[task.setvariable variable=BuildVersionOfAssembly]$BuildVersionOfAssembly"
Version Script in DevOps – Variablen Sektionen leer lassen

Version in Assemblies schreiben

Versionen sind als Varialben da, doch wie bekommen wir sie als Assembly-Attribute in die Assemblies hinein. MSBUILD hat das etwas unbekannte Merkmal ‚directory target‘. Dieses sind Kompile-Schritte, die MSBUILD pro Projekt durchführt. Setzt man sie richtig ein, werden diese Aufgaben für jedes Projekt im selben oder tieferen Verzeichnis ausgeführt. Siehe .

Das Skript muss also irgendwo ‚oben‘ im Projekt angelegt werden und wir nennen es ‚Directory.Build.targets‘. Innerhalb des Skripts kann sogar inline-C# verwendet werden. Dabei nutzen wir Reguläre Ausdrücke um die AssemblyInfo.cs-Dateien zu patchen (alle 3 Versionsattribute). Dabei wird aber nicht die eigentliche Datei gepatcht, sondern eine Kopie. Die Originale (im VCS eingecheckt) werden als ‚targets‘ ausgeschlossen und dafür die gepatchten hinzugefügt. Das ist besser als checkout+checkin auf Build- und Entwicklermaschinen. Denn diese Targets werden ja immer mit ausgeführt. Wenn aber BuildVersionOfAssembly leer ist, macht dieses ‚Target‘ nichts. Das Ganze sollte mit *.vb genauso funktionieren.

Script: \Directory.Build.targets

<Project DefaultTargets="Build" InitialTargets="UpdateAssemblyVersion" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- from:
    http://www.lionhack.com/2014/02/13/msbuild-override-assembly-version/
    Creates modified version of AssemblyInfo.cs, replaces
    [AssemblyVersion] attribute with the one specifying actual build version
    (from MSBuild properties), and includes that file instead of the
    original AssemblyInfo.cs in the compilation.

    Works with both, .cs and .vb version of the AssemblyInfo file, meaning
    it supports C# and VB.Net projects simultaneously.

    see:
    Global .NET Versioning Strategy – AssemblyInformationalVersion
    or https://intovsts.net/2015/08/24/tfs-build-2015-and-versioning/ for
    further information

    Note: C++ projects have a CommonCpp.targets
-->

  <Target Name="UpdateAssemblyVersion" BeforeTargets="Compile"
       Condition="'$(VersionOfAssembly)' != '' or '$(BuildVersionOfAssembly)' != ''">
    <!-- Find AssemblyInfo.cs or AssemblyInfo.vb in the "Compile"
        Items. Remove it from "Compile" Items because we will use a modified
        version instead. -->
    <ItemGroup>
      <OriginalAssemblyInfo Include="@(Compile)" Condition="%(Filename) == 'AssemblyInfo' And (%(Extension) == '.vb' Or %(Extension) == '.cs')" />
      <Compile Remove="**/AssemblyInfo.vb" />
      <Compile Remove="**/AssemblyInfo.cs" />
    </ItemGroup>
    <!--  Copy the original AssemblyInfo.cs/.vb to obj\ folder, i.e.
          $(IntermediateOutputPath). The copied filepath is saved into
          @(ModifiedAssemblyInfo) Item. -->
    <Copy SourceFiles="@(OriginalAssemblyInfo)"
          DestinationFiles="@(OriginalAssemblyInfo->'$(IntermediateOutputPath)%(Identity)_patched')">
      <Output TaskParameter="DestinationFiles" ItemName="ModifiedAssemblyInfo"/>
    </Copy>

    <!-- DON'T Use VersionAssembly if InfoVersionAssembly is empty -->
    <!-- <PropertyGroup> -->
    <!-- <InfoVersionAssembly Condition="'$(BuildVersionOfAssembly)'== ''">$(VersionOfAssembly)</InfoVersionAssembly> -->
    <!-- </PropertyGroup> -->

    <Message  Text="-------------------------------------------------------------------------------" Importance="high" />
    <Message Text="Setting AssemblyVersionAttribute to $(VersionOfAssembly) " Importance="high" />
    <Message Text="Setting AssemblyFileVersionAttribute to $(BuildVersionOfAssembly) " Importance="high" />
    <Message Text="Setting InfoVersionAssemblyAttribute to $(VersionOfAssembly) " Importance="high" />
    <Message Text="Temp file is %(ModifiedAssemblyInfo.FullPath) " Importance="high" />
    <Message Text="--------------------------------------------------------------------------------" Importance="high" />

    <!-- Replace the version bit (in AssemblyVersion and
        AssemblyFileVersion attributes) using regular expression. Use the
        defined property: $(VersionOfAssembly). -->

    <!-- TODO: For Relseases, AssemblyVersion should be set to InfoVersionAssembly -->
    <Message Text="Setting AssemblyVersion to $(VersionOfAssembly)" />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)" 
                     Condition="'$(VersionOfAssembly)' != ''"
                     Regex="AssemblyVersion(Attribute)?\("(\d+)\.(\d+)\..*"\)"
                     ReplacementText="AssemblyVersion("$(VersionOfAssembly)")" 
                     />
    <Message Text="Setting AssemblyFileVersion to $(BuildVersionOfAssembly)" />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)"
                     Condition="'$(VersionBuildVersionOfAssemblyOfAssembly)' != ''"
                     Regex="AssemblyFileVersion(Attribute)?\("(\d+)\.(\d+)\..*"\)"
                     ReplacementText="AssemblyFileVersion("$(BuildVersionOfAssembly)")"
                     />
    <Message Text="Setting InfoVersionAssembly to $(VersionOfAssembly)" />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)"
                     Condition="'$(VersionOfAssembly)' != ''"
                     Regex="AssemblyInformationalVersion(Attribute)?\("(\d+)\.(\d+)\..*"\)"
                     ReplacementText="AssemblyInformationalVersion("$(VersionOfAssembly)")"
                     />
    <!-- Include the modified AssemblyInfo.cs/.vb file in "Compile" items (instead of the original). -->
    <ItemGroup>
      <Compile Include="@(ModifiedAssemblyInfo)" />
    </ItemGroup>
  </Target>

  <UsingTask TaskName="RegexUpdateFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Regex ParameterType="System.String" Required="true" />
      <ReplacementText ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Core" />
      <Using Namespace="System" />
      <Using Namespace="System.IO" />
      <Using Namespace="System.Text.RegularExpressions" />
      <Using Namespace="Microsoft.Build.Framework" />
      <Using Namespace="Microsoft.Build.Utilities" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
try {
var rx = new
System.Text.RegularExpressions.Regex(this.Regex);
for (int i = 0; i < Files.Length; ++i)
{
var path = Files[i].GetMetadata("FullPath");
if (!File.Exists(path)) continue;

var txt = File.ReadAllText(path);
txt = "// <auto-generated />\r\n" +
rx.Replace(txt, this.ReplacementText);
File.WriteAllText(path, txt);
}
return true;
}
catch (Exception ex) {
Log.LogErrorFromException(ex);
return false;
}
]]>
      </Code>
    </Task>
  </UsingTask>

  <!-- ConsoleToMsBuild="True" IgnoreStandardErrorWarningFormat="true" IgnoreExitCode="True" -->
  <!-- MsBuild is probably the only build tool in the world that can't copy dependencies without help.-->
  <!-- Therefore, I can't recommend msbuild for professional use. -->
  <!-- Add this lines to your project file:
  <PropertyGroup
  Condition="Exists('$(SolutionDir)CommonSettings.targets')">
  <BuildDependsOn>
  $(BuildDependsOn);
  CopyLocalAgain;
  </BuildDependsOn>
  </PropertyGroup>
  -->
  <Target Name="CopyLocalAgain">
    <!-- Bug with incremental clean should be fixed. -->
    <!-- <CreateItem Include="%(Reference.HintPath)" -->
    <!-- Condition="'%(Reference.Private)'=='true'"> -->
    <!-- ItemName defines the name of the item list. Who came up with this syntax? -->
    <!-- <Output TaskParameter="Include" -->
    <!-- ItemName="_CopyLocalReferencesAgain"/> -->
    <!-- </CreateItem> -->
    <!-- <Message Text="Copy CopyLocal references again: @(_CopyLocalReferencesAgain)" /> -->
    <!-- <Copy SourceFiles="@(_CopyLocalReferencesAgain)" -->
    <!-- DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true"> -->
    <!-- </Copy> -->
  </Target>
</Project>

MSI und Bootstrapper Setups

Die MSI Setups (WIX) und WIX-Bootstrapper-Setups funktionieren ein wenig anders um ihre Versionsnummer eingeimpft zu bekommen. Da es aber MSBUILD-Projekte sind, kann man ähnlich vorgehen. Quelle der Inspiration: Dieser Stackoverflow Artiel.

  1. In der Projektdatei *.wxiproj muss dieser Teil relativ an den Anfang. Hier wird die Umgebungsvariable als ‚Version‘ genommen. Bei fehlender Variable gibt es einen Fallback. Auf diese Weise gibt es immer eine gesetzte Version. Ansonsten scheitert der WIX-Build.
    <!-- define version constant if CI-Build not running  use a default -->
    <Version Condition=" '$(BuildVersionOfAssembly)' == '' ">5.99.99</Version>
    <Version Condition=" '$(BuildVersionOfAssembly)' != '' ">$(BuildVersionOfAssembly)</Version>
    <DefineConstants>Version=$(Version)</DefineConstants>
  1. In die Dateien *.wxs nutzt man die soeben gesetzte Variable (Verfpgbar als $(ver.Version)).

Beispielausschnitt einer *.wxs:

  <Product Id="CDE4C223-7A78-45FF-A984-675F414BD516" Name="!(loc.PRODUCT)" Language="1033" Version="$(var.Version)" Manufacturer="!(loc.MANUFACTURER)" UpgradeCode="cae20b70-d924-4b23-b90e-9ea1b5f5026b">
    <Package InstallerVersion="200" Manufacturer="!(loc.MANUFACTURER)" Compressed="yes" Description="!(loc.PRODUCT) $(var.Version)" />
    <Property Id="PREVIOUSVERSIONSINSTALLED" Secure="yes" />
    <Property Id="TAKEBACKUP" Secure="yes" />
    <Upgrade Id="cae20b70-d924-4b23-b90e-9ea1b5f5026b">
      <UpgradeVersion Minimum="$(var.Version)" IncludeMinimum="no" OnlyDetect="yes" Language="1033" Property="NEWERPRODUCTFOUND" />
      <UpgradeVersion Minimum="0.79.0" IncludeMinimum="yes" IncludeMaximum="no" Maximum="$(var.Version)" Language="1033" Property="UPGRADEFOUND" />
    </Upgrade>

Release

Zu einer Build-Pipeline gehört immer auch eine Release-Pipeline. Die soll aber nicht das Thema dieses Betrags werden. Hier ging es vornehmlich um das korrekte Errechnen und setzen von Assembly-Versionen und MSI-Versionen.

Als Bemerkunt oder Tip sei hier nur gesagt: Eine Build-Pipeline erzeugt Artefakte (irgendwo in Azure in einem Storage). Diese kann man im DevOps zwar herunterladen und damit arbeiten. Aber die verschwinden irgendwann. Daher benötigt man eine Release-Pipeline. Damit macht man eigentlich eher ‚Deployments‘ und Tests von Software. Man kann aber auch einfach eine leere Pipeline anlegen und einfach nur einen Copy-Task anlegen. Wenn der passende Agent ‚inhouse‘ läuft, kann man dann die Buildartefakte auf einen netzlokalen SMB-Share kopieren.

Fazit

Wir haben hier gesehen, wie man dynamisch beim Build eine jeweils neue Version errechnet und damit eine DevOps-Pipeline-Variable setzt und diese dann erfolgreich in die Buildartefakte (Assemblies und MSIs) hineinbekommt. Dies soll eine Inspiration sein, um jederzeit korrekt versionierte Builds zu haben und MSI-Installer, die automatische Upgrades machen können.