【2021鐵人賽】Pipeline與Artifacts應用:覆寫C#專案屬性資訊

前面文章透過Pipeline上傳nuget package到Artifact feed的時候因為產生的版本已經在之前上傳過了,所以造成Pipeline最後執行失敗。為了要解決這個問題,希望能夠在Pipeline執行的時候自動覆寫版本的資訊,利用Pipeline的BuildId或BuildNumber來代表Package的版本,因此在前一篇文章中也從Azure DevOps Extensions Marketplace中安裝了「.Net Standard Project Property Reader and Writer」這個Extension,這篇文章就讓我們用先前文章中失敗的Yaml檔內容來繼續吧!

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- none

pool:
  vmImage: ubuntu-latest

steps:
- task: DotNetCoreCLI@2
  displayName: Build C# Project
  inputs:
    command: 'build'
    projects: '$(ProjectName)/*.csproj'
    arguments: '-o $(Build.BinariesDirectory)'
- task: ArchiveFiles@2
  displayName: Zip output files
  inputs:
    rootFolderOrFile: '$(Build.BinariesDirectory)'
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)/$(ProjectName)-$(Build.BuildId).zip'
    replaceExistingArchive: true
- task: PublishPipelineArtifact@1
  displayName: Publish files to pipeline artifacts
  inputs:
    targetPath: '$(Build.ArtifactStagingDirectory)'
    artifact: 'BuildOutputFiles'
    publishLocation: 'pipeline'
- task: DotNetCoreCLI@2
  displayName: Push Nuget package
  inputs:
    command: 'push'
    packagesToPush: '$(Build.BinariesDirectory)/*.nupkg'
    nuGetFeedType: 'internal'
    publishVstsFeed: '15523482-96ea-4d9f-83bf-a57fc10e79ce'

首先,我們先來看一下ModuleBase.csproj檔案的內容:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    <Version>1.0.1</Version>
  </PropertyGroup>

</Project>

從上面的XML內容中可以看到版本是設定在Version這個名稱的節點;另外,前面文章中設定Build專案時順便產生nupkg的設定則是在GeneratePackageOnBuild這個節點中設定為true。

接下來我們從Task清單中找到PowerShell task:

在Type的部份選擇Inline,接著在Script的內容部份輸入cat $(ProjectName)/$(ProjectName).csproj,將csproj檔案內容在Pipeline一開始執行的時候先輸出一份,所以這個task將會插入到steps下面作為第一個task:

這樣做的原因是可以在修改Version的前後看到檔案內容的差異,也算是一種log的方式。

接下來從Task清單中找到下面這個Project Property To Environment Variable task:

Project Var Reader task

第一個Path to csproj/vbproj file的屬性設定同樣設為$(ProjectName)/$(ProjectName).csproj,第二個Variable Prefix設為Proj,第三個則是選擇Version選項:

Project Var Reader settings

上面Task的設定會將csproj檔案中的Version內容讀取出來之後放在$(Proj.Version)裡面,以便後續使用。

讀取了Version資訊之後,再從Task清單中找到Write Project Property這個Task:

Project Var Writer

對照上面讀取Version內容的Task設定,唯一不同的地方是第二個屬性設為$(Proj.Version).$(Build.BuildId),也就是將剛才讀出來的Version資訊後面加上Pipeline的BuildId,這樣每次執行Pipeline所產生的檔案版本就會不同:

Project Var Writer settings

之後再將前面的PowerShell task複製一份插入在ProjectVarWriter task後面,這樣就可以查看寫入之後的內容是什麼。

另外,這個Extension的Task只能執行在Windows環境的Agent,所以還必須要將上面的vmImage設定改為windows-latest,最後完成的Yaml內容如下:

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- none

pool:
  vmImage: windows-latest

steps:
- task: PowerShell@2
  displayName: Print csproj content
  inputs:
    targetType: 'inline'
    script: 'cat $(ProjectName)/$(ProjectName).csproj'
- task: ProjectVarReader@0
  displayName: Read version value
  inputs:
    searchPattern: '$(ProjectName)/$(ProjectName).csproj'
    variablePrefix: 'Proj'
    propertyName: 'Version'
- task: ProjectVarWriter@0
  displayName: Writer version value
  inputs:
    searchPattern: '$(ProjectName)/$(ProjectName).csproj'
    value: '$(Proj.Version).$(Build.BuildId)'
    propertyName: 'Version'
- task: PowerShell@2
  displayName: Print csproj content
  inputs:
    targetType: 'inline'
    script: 'cat $(ProjectName)/$(ProjectName).csproj'
- task: DotNetCoreCLI@2
  displayName: Build C# Project
  inputs:
    command: 'build'
    projects: '$(ProjectName)/$(ProjectName).csproj'
    arguments: '-o $(Build.BinariesDirectory)'
- task: ArchiveFiles@2
  displayName: Zip output files
  inputs:
    rootFolderOrFile: '$(Build.BinariesDirectory)'
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)/$(ProjectName)-$(Build.BuildId).zip'
    replaceExistingArchive: true
- task: PublishPipelineArtifact@1
  displayName: Publish files to pipeline artifacts
  inputs:
    targetPath: '$(Build.ArtifactStagingDirectory)'
    artifact: 'BuildOutputFiles'
    publishLocation: 'pipeline'
- task: DotNetCoreCLI@2
  displayName: Push Nuget package
  inputs:
    command: 'push'
    packagesToPush: '$(Build.BinariesDirectory)/*.nupkg'
    nuGetFeedType: 'internal'
    publishVstsFeed: '15523482-96ea-4d9f-83bf-a57fc10e79ce'

從Pipeline執行的log可以看到第一個PowerShell task log中的Version和第二個PowerShell task log中的Version不同,第二個印出的Version值是1.0.1.31:

從Artifact feed(Private nuget)中也可以看到1.0.1.31的版本:

從下面的下拉選單中可以看到GeneratePackageOnBuild也在選項中,所以也可以透過這個方式從Pipeline中設定GeneratePackageOnBuild=true,因為預設建立的C# Project設定中是沒有特別設定這個選項:

除此之外,還有一個Custom的選項,可以自行填入要設定的Property name,可以透過這個方式設定Authors、Description這些屬性,因為在nuget package的也會使用到這些屬性值,也可以將這些設為Variable讓執行Pipeline的使用者輸入這些內容。實際上的作法則是要看團隊的習慣,不過一般來說這些不太會變動的內容應該也是要寫在C#專案設定中才對。

上面的步驟在C# Project檔案中已經有設定了Version值的情況下並不會有什麼問題,但是一般新建立的專案沒有特別去設定版本號的話,其實csproj裡面是不會有Version的內容的,也就是若沒有設定Version值的時候在Build時候系統會預設為1.0.0,但是…

上面的Task在讀取Version內容的時候實際上因為Version並不在檔案內容中,所以在Proj.Version中將會是空的!

這下慘了…那寫入的Version不就變成「.31」這樣的內容?這是什麼奇怪的版本號碼格式…

為了避免這樣的問題,所以要設定另外一個預設值變數,在Yaml中的steps前面加入下面這段:

variables:
  version: '$(Proj.Version).$(Build.BuildId)'
  predefined_version: '1.0.0.$(Build.BuildId)'

增加了version、predefined_version兩個變數,並且將寫入Version值的那個Task設定中的value屬性從’$(Proj.Version).$(Build.BuildId)’改為’$(version)’。

接下來要做的是判斷ProjectVarReader讀取到的Version值是不是空的,也就是判斷Proj.Version的內容是不是空的,如果是空的,那麼就把predefined_version變數的內容設定到version變數中,這部份使用到的還是PowerShell task,yaml內容如下(插入在Writer之前):

- task: PowerShell@2
  displayName: Check version value
  inputs:
    targetType: 'inline'
    script: |
      echo "Proj.Version = $env:Proj_Version"
      if ([string]::IsNullOrWhiteSpace($env:Proj_Version))
      {
        echo '##vso[task.setvariable variable=version]$(predefined_version)'
      }

這邊要特別提的就是在Pipeline中根據不同情況設定variable內容的技巧,也就是透過PowerShell的echo輸出特別的字串格式「##vso[task.setvariable variable=變數名稱]設定值」,變數名稱就是在variables裡面設定的變數,設定值的部份則是可以使用「$(變數名稱)」再從別的變數中取得值,透過這個方式就可以將version這個變數內容更改為predefined_version所設定的內容。(註:在PowerShell中是透過環境變數來取得設定的變數值,並且點( . )會取代為底線( _ ),詳細資訊可參考官方文件。)

最後完成的Yaml檔內容:

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- none

pool:
  vmImage: windows-latest

variables:
  version: '$(Proj.Version).$(Build.BuildId)'
  predefined_version: '1.0.0.$(Build.BuildId)'

steps:
- task: PowerShell@2
  displayName: Print csproj content
  inputs:
    targetType: 'inline'
    script: 'cat $(ProjectName)/$(ProjectName).csproj'
- task: ProjectVarReader@0
  displayName: Read version value
  inputs:
    searchPattern: '$(ProjectName)/$(ProjectName).csproj'
    variablePrefix: 'Proj'
    propertyName: 'Version'
- task: PowerShell@2
  displayName: Check version value
  inputs:
    targetType: 'inline'
    script: |
      echo "Proj.Version = $env:Proj_Version"
      if ([string]::IsNullOrWhiteSpace($env:Proj_Version))
      {
        echo '##vso[task.setvariable variable=version]$(predefined_version)'
      }
- task: ProjectVarWriter@0
  displayName: Writer version value
  inputs:
    searchPattern: '$(ProjectName)/$(ProjectName).csproj'
    value: '$(version)'
    propertyName: 'Version'
- task: ProjectVarWriter@0
  displayName: Set GeneratePackageOnBuild = true
  inputs:
    searchPattern: '$(ProjectName)/$(ProjectName).csproj'
    value: 'true'
    propertyName: 'GeneratePackageOnBuild'
- task: PowerShell@2
  displayName: Print csproj content
  inputs:
    targetType: 'inline'
    script: 'cat $(ProjectName)/$(ProjectName).csproj'
- task: DotNetCoreCLI@2
  displayName: Build C# Project
  inputs:
    command: 'build'
    projects: '$(ProjectName)/$(ProjectName).csproj'
    arguments: '-o $(Build.BinariesDirectory)'
- task: ArchiveFiles@2
  displayName: Zip output files
  inputs:
    rootFolderOrFile: '$(Build.BinariesDirectory)'
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)/$(ProjectName)-$(Build.BuildId).zip'
    replaceExistingArchive: true
- task: PublishPipelineArtifact@1
  displayName: Publish files to pipeline artifacts
  inputs:
    targetPath: '$(Build.ArtifactStagingDirectory)'
    artifact: 'BuildOutputFiles'
    publishLocation: 'pipeline'
- task: DotNetCoreCLI@2
  displayName: Push Nuget package
  inputs:
    command: 'push'
    packagesToPush: '$(Build.BinariesDirectory)/*.nupkg'
    nuGetFeedType: 'internal'
    publishVstsFeed: '15523482-96ea-4d9f-83bf-a57fc10e79ce'

發佈留言

%d 位部落客按了讚: