Masterain

Masterain

Build CI/CD pipelines for C# .NET MSIX applications using Azure Pipelines

Preface#

At the end of last year, the Snap Genshin project was archived and replaced by Snap Hutao. In the new generation of applications, the project utilized updated technologies, replacing the original WPF with WinUI3, and was ultimately packaged as an MSIX installation package, making it available in the Microsoft Store. Due to the store's review mechanism, the application can no longer push updates at any time like in the past self-distribution model, making the CI/CD channel version built for developer testing more important. To this end, I created a CI/CD pipeline through Azure DevOps Pipelines by integrating it with GitHub.

Creating a DevOps Account#

  • Go to the Microsoft Azure Pipelines homepage, click Start free with GitHub to log in with your Microsoft account and access the Azure DevOps panel while linking your GitHub account.

  • If you are creating this environment for a team or company, it is recommended to use the team Microsoft account and authorize Azure DevOps to use team repository data during the GitHub linking process.

    • At the same time, you need to create a DevOps team name for your team.
    • For open-source projects that require free quotas, you need to add this form or send an email to [email protected] to apply for a free quota.
  • You will need to wait 1-2 days for Microsoft to add a free quota for your organization to run the CI/CD pipeline. In the meantime, you can continue building your CI/CD pipeline, but they will not run due to insufficient quota.

Creating a Project#

  • Enter the DevOps panel, go to the organization page, and click New Project on the right to create a new project.

  • After successful creation, select GitHub as your repository.

    GitHub as repo

  • When selecting the repository, choose All repositories in the filter, find the project for which you need to create the CI pipeline, and confirm.
    Select GitHub Repository

  • The page will then redirect to the GitHub authorization page, where you agree to allow Azure DevOps to edit the repository.

  • After completing the authorization, you will be redirected to a YAML file editor, which is the configuration file for Azure Pipelines.

Building the Pipeline#

[tip type="info" title="Compatibility Issues"]
In the official Microsoft documentation, they recommend the Azure Pipelines plugin maintained by Microsoft, but this plugin has compatibility issues with VS 2022 at the time of writing this article. Therefore, when building the Snap Hutao project, I packaged the application via PowerShell in command line mode. Thus, you can also learn about the details of the MSIX packaging process through this article.
[/tip]

Preparation#

Before everything starts, we first set some environment variables for the pipeline environment.

  • build_date returns a date format like 2023.2.28 by calling PowerShell in the environment, and we will use such a date as the version number of the software in the CI/CD testing channel.
variables:
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
  solution: '$(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao.sln'
  project: '$(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj'
  buildPlatform: 'x64'
  buildConfiguration: 'Release'
  build_date: $[ format('{0:yyyy}.{0:M}.{0:d}', pipeline.startTime) ]

We set the CI/CD environment to Windows Server 2022, which already includes Visual Studio 2022 Enterprise Edition.

pool:
  vmImage: 'windows-2022'

Compiling the Binary Files#

  • Next, we create a packaging environment for the .NET Framework program.
 - task: UseDotNet@2
  displayName: Install dotNet
  inputs:
    packageType: 'sdk'
    version: '7.x'
    includePreviewVersions: true

 - task: NuGetToolInstaller@1
  name: 'NuGetToolInstaller'
  displayName: 'NuGet Installer'

 - task: NuGetCommand@2
  displayName: NuGet restore
  inputs:
    command: 'restore'
    restoreSolution: '$(solution)'
    feedsToUse: 'select'

Then, we can use MSBuild included in Visual Studio 2022 to compile the code.

  • At this step, in most cases, we only need to set msbuildLocationMethod to version and set msbuildVersion to latest or another specified version. However, due to compatibility issues with VS2022, we have to use location and the path of MSBuild.exe as packaging parameters.
 - task: MsixPackaging@1
  displayName: Build binary package
  inputs:
    outputPath: '$(Build.ArtifactStagingDirectory)/'
    solution: '$(solution)'
    clean: false
    generateBundle: false
    buildConfiguration: 'Release'
    buildPlatform: 'x64'
    updateAppVersion: false
    appPackageDistributionMode: 'SideloadOnly'
    msbuildLocationMethod: 'location'
    msbuildLocation: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Msbuild\Current\Bin\MSBuild.exe'

MSIX Packaging#

Before packaging, we need to prepare the version number of the packaged software. In the compiled binary program, the software version number follows the settings in the code, but we cannot modify it for a temporary CI/CD version, so we need the CI/CD pipeline to prepare this temporary version number for us. When the CI/CD pipeline initially runs, we have already obtained a date format like 2023.2.28 through environment variables, but it lacks the fourth revision number. Coincidentally, Azure Pipelines creates a version number for each CI/CD task, formatted like 20230228.1, where the number after the decimal point represents the nth task on that date, making it very suitable for the revision number, as it ensures that the version number is always incrementally ordered.

In the Azure DevOps Market, we can add a plugin called GetRevision. By using it, we can add the following script to set the rev_number variable.

 - task: GetRevision@1
  displayName: get Pipelines revision number
  inputs:
    VariableName: 'rev_number'

Unless specified otherwise, the compiled program will be stored in the \bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\ directory under the project directory.

The Windows system identifies the application uniquely by reading the Identity Name of the MSIX application, so we need to change some information to distinguish between the CI/CD version application and the official version application. Here, we use MagicChunks to modify the AppxManifest.xml file that manages these properties.

  • transformations contains the property values we are overriding.
  • Package/Identity/@Name is the name used by the system to identify the unique identifier of the package.
  • Package/Identity/@Publisher is the key developer information, and its value must match exactly with your MSIX application package signature, otherwise, a signature error will occur.
    • If your code signing certificate is purchased, it may sometimes contain multiple pieces of information, such as company name, organization name, and email. In this case, some punctuation may conflict with the xml format. Therefore, we need to escape any conflicting symbols in this information and include all of it, not just CN.
  • "Package/Identity/@Version": "$(build_date).$(rev_number)" writes the application package version we prepared earlier into the property.
 - task: MagicChunks@2
  inputs:
    sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\AppxManifest.xml'
    fileType: 'Xml'
    targetPathType: 'source'
    transformationType: 'json'
    transformations: |
      {
        "Package/Identity/@Name": "7f0db578-026f-4e0b-a75b-d5d06bb0a74c",
        "Package/Identity/@Publisher": "CN=DGP Studio CI",
        "Package/Identity/@Version": "$(build_date).$(rev_number)",
        "Package/Properties/DisplayName": "Hutao Alpha",
        "Package/Properties/PublisherDisplayName":"DGP Studio CI",
        "Package/Applications/Application/uap:VisualElements/@DisplayName": "Hutao Alpha"
      }

Next, we will also add the static resources used in the application to the binary program directory. These resources are called externally in the code, so they will not be automatically added to the compiled directory during the binary compilation process.

 - task: CmdLine@2
  displayName: Create resources folder
  inputs:
    script: |
      mkdir Assets
      
      mkdir Resource
    workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64'
      

 - task: CopyFiles@2
  displayName: Copy Assets Folder
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Assets'
    Contents: '**'
    TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\Assets'

 - task: CopyFiles@2
  displayName: Copy Resource Folder
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Resource'
    Contents: '**'
    TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\Resource'

Everything is ready, and we can now use makeappx to package the binary files into an MSIX application package.

  • Among them, $(Build.ArtifactStagingDirectory) is the default export resource directory of Azure Pipelines.
 - task: CmdLine@2
  displayName: Build MSIX
  inputs:
    script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'

Signing the MSIX Application Package#

MSIX application packages require all applications to be code-signed; otherwise, the application cannot be installed. Therefore, we will sign the packaged MSIX package next.

Click on the Library in the Pipelines menu, go to Secure files, and click the button to upload the password-protected pfx certificate file.
Upload secure files to library

Back in the Pipelines YAML file editor, we need to store the password of the pfx certificate as a variable in this pipeline task. Click on Variables in the upper right corner and click to add. In the variable addition window, set a name for the variable and fill in the password in Value, and finally check Keep this value secret.
pfx password variable

Now we can use Microsoft's official MsixSigning plugin to sign the Msix application package.

  • Here, certificate is the name of the certificate file you uploaded in Secure Files.
  • passwordVariable is the name of the variable where you stored the pfx certificate password.
- task: MsixSigning@1
  name: signMsix
  displayName: Sign MSIX package
  inputs:
    package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
    certificate: 'DGP_Studio_CI.pfx'
    passwordVariable: 'pw'

If you wish to use PowerShell or CMD to sign, you can refer to the following command:

SignTool sign /fd SHA256 /a /f C:\Users\i\Documents\GitHub\Snap.Hutao.Output\Snap.Hutao_TemporaryKey.pfx /p defaultpassword C:\Users\i\Documents\GitHub\Snap.Hutao.Output\Snap.Hutao.signed.msix

Publishing the CI/CD Application#

In the Snap Hutao project, I decided to publish the sideload package and the CI/CD sideload certificate as a pre-release in the main GitHub repository. Among them,

  • The Download Root CA task downloads the sideload certificate stored in Secure Files to the CI/CD environment and stores that file as cerFile in the environment variable. In the GitHub Release publishing process, we can reference that file using $(cerFile.secureFilePath).
  • gitHubConnection is the integrated GitHub account.
- task: DownloadSecureFile@1
  name: cerFile
  displayName: Download Root CA
  inputs:
    secureFile: 'Snap.Hutao.CI.cer'

- task: GitHubRelease@1
  inputs:
    gitHubConnection: 'github.com_Masterain'
    repositoryName: 'DGP-Studio/Snap.Hutao'
    action: 'create'
    target: '$(Build.SourceVersion)'
    tagSource: 'userSpecifiedTag'
    tag: '$(build_date).$(rev_number)'
    title: '$(build_date).$(rev_number)'
    releaseNotesSource: 'inline'
    releaseNotesInline: |
      ## Ordinary users please do not download
      This version is an `Alpha` test version automatically packaged by the CI program, **for developer testing use only**.

      Ordinary users please [click here](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) to download the latest stable version.

    assets: |
      $(Build.ArtifactStagingDirectory)/*
      $(cerFile.secureFilePath)
    isPreRelease: true
    changeLogCompareToRelease: 'lastFullRelease'
    changeLogType: 'commitBased'

Results#

Click the save button in the upper right corner of the YAML editor, and your CI/CD configuration file will be added to the GitHub repository and executed immediately. In Azure DevOps, you can see all task records of the Pipelines.
Azure Pipelines history

In the GitHub Release, you can see the published CI/CD version.
GitHub release

In the corresponding Commit, you can also see the information of the corresponding CI/CD task.
Commit information

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.