AutoIncrementing Build with Core and DevOps Pipeline - msbuild

I'm trying to setup an auto-incrementing version number for my ASP.NET Core 3.1 web app built using Azure Pipelines. I've tried various snippets and pipeline tasks and got as far as generating the version number but my build fails with the message
The specified version string does not conform to the required format - major[.minor[.build[.revision]
I'm using the below snippet of yml to generate the version number:
variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
version.MajorMinor: '1' # Manually adjust the version number as needed for semantic versioning. Revision is auto-incremented.
version.Revision: $[counter(variables['version.MajorMinor'], 0)]
versionNumber: '$(version.MajorMinor).$(version.Revision)'
steps:
- task: PowerShell#2
displayName: Set the name of the build (i.e. the Build.BuildNumber)
inputs:
targetType: 'inline'
script: |
$doy = (Get-Date).DayofYear
Write-Host "##vso[task.setvariable variable=DayOfYear]$doy"
$rev = $env:BUILD_BUILDNUMBER.Split('.')[-1]
Write-Host "##vso[task.setvariable variable=Revision]$rev
Write-Host "set Revision to $rev"
[string] $buildName = "$(versionNumber).$doy.$rev"
Write-Host "Setting the name of the build to '$buildName'."
Write-Host "##vso[build.updatebuildnumber]$buildName"
This does generate a version number (1.26.199.50), however when I get the the MSBuild task:
task: VSBuild#1
inputs:
solution: '$(solution)'
msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site" /p:Version=$(buildName)'
I get the version error. I've even tried replacing the version with 1.0.199.50 but I get the same error. I understand that each segment has to be less than 65534 as that's the max value for a UInt so I don't understand why it's failing.

After much frustration, I did this with a Powershell script in the release pipeline instead. I've got a Powershell task containing the below which is before the File Transform task. Steps to get it working.
Add a release pipeline variable called "MajorVersion". This is the first part of the version string
Add a property to appsettings.json called AppConfig.Version, set it to whetever for local testing.
Add a powershell task. The order should be Powershell Task->File Transform->App Service Deploy:
$doy = $(Get-Date).DayofYear
[string] $buildName = "$Env:MajorVersion.$doy.$Env:BUILD_BUILDNUMBER"
Write-Host "##vso[task.setvariable variable=AppConfig.Version;]$buildName"
Access the variable from your app settings using your preferred method. In core, probably via Configuration.GetSection("blah")
The assembly isn't stamped with the version, but you can get to it to display on the UI. I feel like this needs to be simplified by MS as auto-incrementing a version shouldn't be so difficult.

Related

Cobertura - Coverlet.runsettings.xml values ignored by dotnetcore CLI test command

I am trying to generate code coverage for one of my solutions in an Azure DevOps build pipeline. Whilst I have the results of my tests appearing upon build completion along with the Cobertura code coverage report, there are a lot of files and namespaces I would like to exclude from the code coverage report as they are not testable units of code (e.g. models or database migrations).
I understand that I can utilise the coverlet.runsettings.xml file to do namespace exclusions but this does not appear to be working.
My runsettings file is setup inside one of the test projects that is run and is setup as follows:
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<Exclude>[MyDatabase.Migrations.*]*,[*]MyDatabase.Migrations*</Exclude>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
I'm trying to exclude all files that sit under the MyDatabase.Migrations namespace but the syntax I have used above doesn't seem to have any effect and I still see the files beneath this namespace shown in final code coverage report in Devops.
My pipeline is setup as follows:
steps:
- task: DotNetCoreCLI#2
displayName: 'Build'
inputs:
command: 'build'
projects: '$(solution)'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI#2
displayName: 'Tests'
inputs:
command: 'test'
projects: |
**\MyDatabase.Tests
**\MySearch.Common.Tests
**\MySearchService.Tests
**\MyConfiguration.Tests
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"'
publishTestResults: true
# ReportGenerator extension to combine code coverage outputs into one
- task: reportgenerator#5
inputs:
reports: '$(Agent.WorkFolder)/**/coverage.cobertura.xml'
targetdir: '$(Build.SourcesDirectory)/CoverageResults'
- task: PublishCodeCoverageResults#1
displayName: 'Publish code coverage report'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Build.SourcesDirectory)/CoverageResults/Cobertura.xml'
reportDirectory: '$(Build.SourcesDirectory)/CoverageResults'
I am aware I can use the [ExcludeFromCodeCoverage] tag but with things that are auto-generated like migrations this could get pretty messy having to go in and modify them every time a new one is generated.
I would like the migrations folder completely omitted from even appearing in the generated report.
Any help with this issue would be greatly appreciated.
The VSTestIntegration documentation indicates you will need to include the custom runsettings file as an agrument:
This runsettings file can easily be provided using command line option as given : dotnet test --collect:"XPlat Code Coverage" --settings coverlet.runsettings
Try the following:
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage" --settings [path to tests project]\coverlet.runsettings.xml'

Azure DevOps build pipeline fails for solution with .NET Framework and .NET 5 projects

I have a .NET solution that used to contain just .NET 5 applications. This compiled just fine using a YAML file on Azure DevOps.
However, since adding some .NET Framework 4.8 projects this now fails. I have read some articles that seem to explain the problem (to some extent) but the solution isn't clear to me. Perhaps someone could spell it out for me here?
In more detail...
Projects
I have the following mixture of projects:
5 x SDK projects, net5.0 (Test Suite)
1 x SDK project, netstandard 2.0 (library)
1 x SDK project, net5.0 (Web API)
1 x SDK project, net4.8 (Test)
1 x non-SDK project, net4.8 (Web API)
The library is common to all. The .NET 4.8 WebAPI is referenced by the .NET 4.8 test suite, and the other .NET 5 test suites reference the .NET 5 WebAPI.
I have the following YAML file:
trigger:
- master
pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildConfiguration: 'Release'
buildPlatform: 'Any CPU'
platform: x64
steps:
- task: NuGetCommand#2
displayName: 'Restore for all'
inputs:
restoreSolution: '$(solution)'
feedsToUse: 'select'
vstsFeed: '{Guid # 1}/{Guid # 2}'
- task: VSBuild#1
displayName: 'Build all'
inputs:
solution: '$(solution)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
maximumCpuCount: true
- task: DotNetCoreCLI#2
displayName: 'Run Tests'
inputs:
command: 'test'
projects: '**/*Test.csproj'
arguments: '--configuration $(buildConfiguration) --collect "Code coverage"'
We have a private NuGet gallery in the Azure Artifacts and the two Guids ({Guid # 1}/{Guid # 2}) allow us to access that.
When this now executes, the following steps appear to succeed: Restore for all and Build all. However, the step that appears to fail is the Run Tests.
The error message is:
##[error]Dotnet command failed with non-zero exit code on the following projects : D:\a\1\s\path\MySdkNet48.Test.csproj
This is the Test project that is .NET 4.8 and uses a SDK project file format.
Digging into the more verbose details in the Run Tests step I see:
D:\a\1\s\path\MyNonSdkNet48WebApi.csproj(328,3): error MSB4019: The imported project "C:\Program Files\dotnet\sdk\5.0.401\Microsoft\VisualStudio\v16.0\WebApplications\Microsoft.WebApplication.targets" was not found. Confirm that the expression in the Import declaration "C:\Program Files\dotnet\sdk\5.0.401\Microsoft\VisualStudio\v16.0\WebApplications\Microsoft.WebApplication.targets" is correct, and that the file exists on disk.
##[error]Error: The process 'C:\Program Files\dotnet\dotnet.exe' failed with exit code 1
and
##[error]Dotnet command failed with non-zero exit code on the following projects : D:\a\1\s\path\MySdkNet48Test.csproj
As mentioned above, I'm not clear on how to fix this...
Since the 4.8 project isn't a Dotnet Core/5.0 project, it'll need to run using the Visual Studio Test task.
Split the test step in 2 separate steps, one configured to run the 4.8 projects, one to run the dotnet core/5.0 projects.
- task: DotNetCoreCLI#2
displayName: 'Run Tests'
inputs:
command: 'test'
projects: '**/*Test.csproj;!**/*48.Test.csproj'
arguments: '--configuration $(buildConfiguration) --collect "Code coverage"'
- task: VSTest#2
displayName: 'VsTest - testAssemblies'
inputs:
testAssemblyVer2: |
**\*48.Test.dll

When deploying my build to azure (i use devops pipeline but i also tried to deploy via visual studio directly) i keep getting resource not found 404

When I try to publish my .net core 2.2 webapi to my azure app service (via azure devops using Azure App Service deploy or via visual studio publish method) I keep getting:
"The resource you are looking for has been removed, had its name changed, or is temporarily unavailable."
But when I publish it directly with visual studio ( without package or zip configuration) it works. (but we can't keep doing this because we need it working on the azure devops pipeline).
I have tried to build the project differently using dotnet build/publish and vsbuild. I Also tried using different methods of publish (zip,package, web deploy).
I went thru the generated xml files (deploy.cmd,deploy-readme,parameters,setparametersa and sourcemanifest) to check if there is some wrong naming issue or if the folder structure is incorrect but everything matches correctly. The zip file where my project is in, is also in the same location as all the generated xml files(root).
The strange thing is that it was working before and we are using terraform to generate the azure resources. So we threw away the resources which we made manually and remade them using terraform and then we get the error. We did not change anything on the pipeline.
yaml build code from the pipeline:
trigger:
- master
pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
steps:
- task: NuGetToolInstaller#1
- task: NuGetCommand#2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild#1
inputs:
solution: '$(solution)'
msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:CreatePackageOnPublish=true /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\{redactedprojectname}.zip" /p:DeployIisAppPath="Default Web Site"'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: PublishBuildArtifacts#1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
Terraform script for the app service part :
# Create App Service Plan
resource "azurerm_app_service_plan" "asp" {
name = "${local.aspName}"
location = "${azurerm_resource_group.rg.location}"
resource_group_name = "${azurerm_resource_group.rg.name}"
tags = "${var.tags}"
sku {
tier = "${var.aspSku.tier}"
size = "${var.aspSku.size}"
}
}
# Create App Service
resource "azurerm_app_service" "as" {
name = "${local.apiName}"
location = "${azurerm_resource_group.rg.location}"
resource_group_name = "${azurerm_resource_group.rg.name}"
app_service_plan_id = "${azurerm_app_service_plan.asp.id}"
tags = "${var.tags}"
https_only = true
site_config {
dotnet_framework_version = "v4.0"
always_on = true
}
app_settings = {
"APPINSIGHTS_INSTRUMENTATIONKEY" = "${azurerm_application_insights.ai.instrumentation_key}"
"ASPNETCORE_ENVIRONMENT" = "${var.environment}"
}
}
# --- Output section --
output "appServiceName" {
value = azurerm_app_service.as.name
}
Terraform steps in azure devops:
use terraform 0.12.11
Terraform init
Terraform plan
Terraform apply
Terraform output get appservice name
after this we do Azure App Service deploy
yaml code app service deploy:
variables:
environment: 'prd'
steps:
- task: AzureRmWebAppDeployment#4
displayName: 'Azure App Service Deploy: $(appServiceName)'
inputs:
azureSubscription: '{redactedprojectname}'
WebAppName: '$(appServiceName)'
packageForLinux: '$(System.DefaultWorkingDirectory)/{redactedprojectname}/drop'
AppSettings: ASPNETCORE_ENVIRONMENT "$(environment)"'
enableCustomDeployment: true
DeploymentType: zipDeploy
TakeAppOfflineFlag: false
I would expect that after having a working solution before, that deleting the resources and redeploying to let terraform recreate the resources from scratch without modifying the pipeline, would just provide a working solution
We ended up solving the build issue by using msbuild with filesystem instead of package
Here is the yaml code for anyone who encounters the same issue:
trigger:
- master
pool:
vmImage: 'windows-latest'
variables:
solution: '**/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
steps:
- task: NuGetToolInstaller#1
- task: NuGetCommand#2
inputs:
restoreSolution: '$(solution)'
- task: VSBuild#1
inputs:
solution: '$(solution)'
msbuildArgs: '/p:DeployOnBuild=true /p:Configuration=Release /p:WebPublishMethod=FileSystem /p:SkipInvalidConfigurations=true /p:DeployDefaultTarget=WebPublish /p:DeleteExistingFiles=True /p:publishUrl="$(build.artifactStagingDirectory)\YourProjectNameFolder" /p:DeployIisAppPath="Default Web Site"'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: PublishBuildArtifacts#1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
All you need to change is the msbuildArgs property.
The msbuild arguments for the filesystem are based on https://www.joe-stevens.com/2018/12/14/publish-to-file-system-with-msbuild/

Invalid results file Warning when running Azure DevOps Test Task

I am using Azure DevOps to run some XUnit tests of an Asp.Net Core application:
- task: DotNetCoreCLI#2
displayName: 'Test'
inputs:
command: test
projects: '**/*[Tt]est/*.csproj'
arguments: '--configuration $(buildConfiguration)'
The task succeeds but I get two warnings:
[warning] Invalid results file. Make sure the result format of the file '/home/vsts/work/_temp/_fv-az592_2019-04-09_21_14_05.trx' matches 'VSTest' test results format.
[warning] Invalid results file. Make sure the result format of the file '/home/vsts/work/_temp/_fv-az592_2019-04-09_21_14_10.trx' matches 'VSTest' test results format.
What am I missing?
From github:
warning is as per our design of task since we wants to warn customer
if he is trying to use PTR task without any results, but i agree with
you about more apt warning message and we will correct it in upcoming
release.
Also try yo use VSTest task instead

Azure Pipeline - Increment build number and display in web app

I have the following simple build pipeline working in Azure DevOps with releases deployed to a staging slot.
I would like to have a build revision/version string and that is auto-incremented. I then want to display this in my web app so I can tell which version of the software is in production.
Currently I display the version string from the .csproj file. Something like this in a
<Version>1.1.4.7</Version>
And then displayed on a web page using the following code:
Version: #typeof(Startup).Assembly.GetName().Version.ToString()
If I can update the existing version string that would be great but I'm open to changing to whatever's easiest to integrate in the CI process.
Versioning has been simplified in the .Net Core world.
Edit your csproj and modify it as follows:
<PropertyGroup>
<Version Condition=" '$(BUILD_BUILDNUMBER)' == '' ">1.0.0.0</Version>
<Version Condition=" '$(BUILD_BUILDNUMBER)' != '' ">$(BUILD_BUILDNUMBER)</Version>
</PropertyGroup>
If your file doesn’t have a version node, add the above.
The above setup will mean debugging locally will give you a version of 1.0.0.0, and in the event, you build in a non-Azure DevOps environment you will also end up with a 1.0.0.0 version. $(BUILD_BUILDNUMBER) is an environment variable set by Team Build and which will be updated at build time by VSTS or TFS.
The .Net version needs to be in the format [major].[minor].[build].[revision] with each segment being a number between 0 and 65000. You can configure the build number format in the Options tab, see here more info about the formatting. See here for helpful steps on configuring the build.
I am using ADO pipeline variables and the counter function like this
The patch variable is $[counter(format('{0}.{1}',variables['Major'], variables['Minor']),0)]
Then I put all of these values directly into a variable as a powershell task.
- task: InlinePowershell#1
displayName: Create Version Number
inputs:
Script: |
param($major, $minor, $patch)
$bv = "$major.$minor.$patch"
Write-Host "##vso[task.setvariable variable=buildVersion]$bv"
Write-Host "Version of net App : $bv"
ScriptArguments: '-major $(Major) -minor $(Minor) -patch $(Patch)'
Now when I do the .net publish I just pass the version
- task: DotNetCoreCLI#2
displayName: Publish Application
inputs:
command: 'publish'
publishWebProjects: false
projects: '**/MyAPIProject.csproj'
arguments: '-r $(buildPlatform) -o $(Build.BinariesDirectory) -c $(buildConfiguration) --self-contained true /p:Version=$(buildVersion)'
zipAfterPublish: false
modifyOutputPath: false
What is really nice about this, is the patch gets seeded at zero, then every follow up build will auto-increment.
You should Use Azure DevOps Pipelines Release (a.k.a. Azure Pipelines release) instead of Azure DevOps Pipelines build (a.k.a. Azure Pipelines build). Azure Pipelines release by default will auto-increment your releases.
Azure Pipelines build has no automatic version numbering by default. Because increasing version after it should be done at release stage, because build should only concerns the continuous integrations, not to be used as versioning a build to be released.
This is the official docs of Azure Pipelines release on auto-increment your release:
https://learn.microsoft.com/en-us/azure/devops/pipelines/release/?view=vsts#numbering
In a .NET Core (.NET 6.0 in particular) app I have set the following code in .csproj:
<PropertyGroup>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
</PropertyGroup>
Then in Azure DevOps I have added the following powershell script task to set version on my build.
Note that I am using the below convention:
Major -> Phase/Product cycle. I am using a variable/parameter to pass this value in the pipeline.
Minor -> Sprint number. I am using a variable/parameter to pass this value in the pipeline.
Revision -> Max limit for this number is 65000 and therefore cannot use Build.BuildNumber. I am using (Date:YY)(DayOfYear).
Build -> Taking the second part of the Build.BuildNumber (increment of the day)
- task: PowerShell#2
displayName: 'Edit AssemblyInfo'
inputs:
targetType: 'inline'
script: |
$Major = "$(Major)"
Write-Host "Major: $Major"
$Minor = "$(Minor)"
Write-Host "Minor: $Minor"
$DayOfYear = (Get-Date).DayOfYear
$Year = (Get-Date –format yy)
$Revision = '' + $Year + $DayOfYear
Write-Host "Revision: $Revision"
$BuildNumber = "$(Build.BuildNumber)"
Write-Host "BuildNumber: $BuildNumber"
$Build = $BuildNumber.Split('.')[1]
Write-Host "Build: $Build"
$pattern = '<AssemblyVersion>(.*)</AssemblyVersion>'
Write-Host "Will search in paths.."
$csProjFiles = Get-ChildItem .\*.csproj -Recurse
Write-Host "CsProjFiles found are: "
Write-Host "$csProjFiles"
foreach ($file in $csProjFiles)
{
(Get-Content $file.PSPath) | ForEach-Object{
if($_ -match $pattern){
# We have found the matching line
# Edit the version number and put back.
Write-Host "Matched $file"
$fileVersion = [version]$matches[1]
$newVersion = "{0}.{1}.{2}.{3}" -f $Major, $Minor, $Revision, $Build
'<AssemblyVersion>{0}</AssemblyVersion>' -f $newVersion
} else {
# Output line as is
$_
}
} | Set-Content $file.PSPath
}