前面文章透過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:
第一個Path to csproj/vbproj file的屬性設定同樣設為$(ProjectName)/$(ProjectName).csproj,第二個Variable Prefix設為Proj,第三個則是選擇Version選項:
上面Task的設定會將csproj檔案中的Version內容讀取出來之後放在$(Proj.Version)裡面,以便後續使用。
讀取了Version資訊之後,再從Task清單中找到Write Project Property這個Task:
對照上面讀取Version內容的Task設定,唯一不同的地方是第二個屬性設為$(Proj.Version).$(Build.BuildId),也就是將剛才讀出來的Version資訊後面加上Pipeline的BuildId,這樣每次執行Pipeline所產生的檔案版本就會不同:
之後再將前面的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'
PS. 這個Extension有個限制是對於沒有設定在csproj檔案內容中的Property並不會增加並寫入值,也就是說如果csproj裡面沒有設定Version,也就是Version節點不存在,那麼便不會寫入值。(可以是<Version></Version>
,但是不能沒有)