使用 Azure Pipelines 为 C# .NET MSIX 应用构建 CI/CD 管道

前言

去年年底，Snap Genshin 项目被归档并被 Snap Hutao 替代。在新一代的应用中，项目使用了更新式的技术，使用 winui3 替代了原始的 WPF，并最终打包为 msix 安装包，使其上架到了微软商店。由于商店的审核机制，应用不能再像过去自分发模式一样随时推送更新，因此构建为开发人员测试的 CI/CD 通道版本变得更加重要了。为此，我使用 Azure DevOps 中的 Pipelines 通过其和 GitHub 的集成创建了 CI/CD 管道。

创建 DevOps 帐号

来到微软 Azure Pipelines 主页，点击 Start free with GitHub 来通过登录你的微软帐号来进入 Azure DevOps 面板并绑定 GitHub 帐号。

如果你是为团队或公司创建这个环境，建议使用团队微软帐号并在绑定 GitHub 过程中授权 Azure DevOps 使用团队库数据

同时，你需要为你的团队创建一个 DevOps 团队名称

对于需要免费额度的开源项目而言，需要添加这个表格或发送邮件给azpipelines-freetier@microsoft.com来申请免费额度

你需要等待 1-2 天等待微软官方为你的组织添加免费额度以运行 CI/CD 管道。在此之前，你仍然可以继续构建你的 CI/CD 管道，只是它们会因为额度不足而无法实际运行

创建项目

进入 DevOps 面板，进入组织页面，点击右侧的 New Project 来创建一个新的项目

创建成功后选择 GitHub 作为你的代码库

在选择代码库时，在筛选中选择 All repositories 后找到你需要创建 CI 管道的项目并确认

之后页面会跳转到 GitHub 授权页面，在这里同意 Azure DevOps 编辑该库

完成授权后，你会被跳转到一个 YAML 文件编辑器上，这个就是 Azure Pipelines 的配置文件

构建管道

[tip type="info" title="兼容性问题"]
在微软官方文档中，他们推荐了由微软官方维护的 Azure Pipelines 插件，但该插件在写本文时对 VS 2022 存在兼容性问题。所以在构建 Snap Hutao 项目时，我通过 PowerShell 以命令行的方式打包了应用程序。因此，你也可以通过本文了解 MSIX 打包的细节流程。
[/tip]

准备工作

在一切开始之前，我们先为管道环境设置好一些环境变量

build_date 通过调用环境中的 PowerShell 返回了一个形如 2023.2.28的时间格式，我们将这样的日期作为 CI/CD 测试通道中软件的版本号

Copy
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) ]

我们将 CI/CD 环境设置为 Windows Server 2022，该环境下已包含 Visual Studio 2022 企业版

Copy
pool:
vmImage: 'windows-2022'

编译二进制文件

接下来我们为 .NET Framework 程序创建打包环境

Copy
- 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'

随后，我们就可以使用 Visual Studio 2022 中包含的 MSBuild 来编译代码了

在这一步，大多数情况下我们都只需要msbuildLocationMethod设置为version并将msbuildVersion设置为lastest或其它指定版本。但是，由于对 VS2022 的兼容性问题，我们不得不使用 location 和 MSBuild.exe 的路径作为打包参数

Copy
- 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 打包

在打包之前，我们要准备好打包后的软件包版本号。在已编译的二进制程序中，软件版本号是跟随代码中的设置的，但我们是不可能会为了一个 CI/CD 的临时版本而修改它的，所以我们需要让 CI/CD 管道为我们准备这个临时的版本号。在 CI/CD 管道最初运行时，我们已经通过环境变量获得了一个格式形为2023.2.28这样的日期，但缺少第四位的 revision number。非常巧合的是， Azure Pipelines 会为每一个 CI/CD 任务创建一个版本号，其格式形如20230228.1，其实小数点后的数字代表该日期下的第几次任务，这个数字就非常适合用于 revision number，因为它可以保证版本号一定是有序递增的。

在 Azure DevOps Market 中我们可以添加一款名为 GetRevision 的插件。使用它，我们就可以添加下面这段脚本将 revision number 设置到 rev_number 变量上。

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

除非额外指定参数，编译后的程序会被储存在项目目录下的\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\目录中

Windows 系统会通过读取 MSIX 应用的 Identity Name 来作为应用的唯一标识，因此我们需要更改一些信息以让系统和我们自己来区分 CI/CD 版本应用和正式版应用。在这里，我们使用 MagicChunks来修改管理这些属性的AppxManifest.xml文件

transformations中包含了我们覆盖的属性数值

Package/Identity/@Name就是系统用于分辨软件包唯一标识的名称

Package/Identity/@Publisher是关键的开发者信息，它的值必须和你 MSIX 应用包签名完全一致，否则会产生签名错误

如果你的代码签名证书是购买来的，有时候它会包含多重信息，比如公司名称、组织名称以及邮箱。这种情况下，一些标点符号就会和 xml 格式发生冲突。因此，我们需要对这些信息中有冲突的符号进行转义，并且需要包含这些全部信息，而不仅仅是CN。

"Package/Identity/@Version": "$(build_date).$(rev_number)"这一行将我们之前准备好的应用包版本写入属性

Copy
- 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": "胡桃 Alpha",
"Package/Properties/PublisherDisplayName":"DGP Studio CI",
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
}

接下来，我们将应用程序中使用的静态资源也加入二进制程序目录下。这些资源在代码中是一种外部调用，所以在二进制编译过程中，它们是不会被自动添加到编译后的目录中的。

Copy
- 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'

一切就绪，我们现在可以通过makeappx将二进制文件打包为 MSIX 应用包了

其中，$(Build.ArtifactStagingDirectory)是 Azure Pipelines 默认的导出资源目录

Copy
- 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'

签名 MSIX 应用包

MSIX 应用包要求所有应用都需要代码签名，否则应用无法安装。所以接下来，我们将要为已打包的 MSIX 包签名。

点击 Pipelines 菜单中的 Library，进入 Secure files，点击按钮上传设有密码的 pfx 证书文件

回到 Pipelins YAML 文件编辑器，我们需要将 pfx 证书的密码作为变量储存在这个管道任务中。点击右上角的 Variables，点击添加。在添加变量的窗口中，为变量设置一个名称，并在Value中填写密码，最后勾选 Keep this value secret

现在我们就可以使用微软官方的MsixSigning插件来签名 Msix 应用包

其中certificate为你在 Secure Files中上传的证书文件名

passwordVariable为你储存 pfx 证书密码的变量名称

Copy
- 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'

如果你希望使用 PowerShell 或 CMD 来签名可以参考以下的命令

Copy
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

发布 CI/CD 应用

在 Snap Hutao 项目中，我决定将侧载包和 CI/CD 侧载证书一起以 pre-release 的方式在 GitHub 主项目库中发布。其中，

Download Root CA任务将储存在 Secure Files中的侧载证书下载到 CI/CD 环境中，并将该文件以cerFile名称储存在环境变量中。在 GitHub Release 发布流程中，我们可以通过$(cerFile.secureFilePath)来引用该文件

gitHubConnection是集成的 GitHub 帐号

Copy
- 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: |
## 普通用户请勿下载
该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本，**仅供开发者测试使用**

普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本

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

成果

点击 YAML 编辑器右上角的保存，你的 CI/CD 配置文件就会被添加到 GitHub 代码库中，并立刻执行一次。在 Azure DevOps 中你可以看到 Pipelines 的全部任务记录。

在 GitHub Release 中你可以看到已发布的 CI/CD 版本

在对应的 Commit 中，你也可以看到对应 CI/CD 任务的信息