SPFx Azure DevOps Pipeline: Increment version, push to repository and publish package
Create an Azure DevOps Pipeline to increment the version, create a tag for that version and check it into the repository and then package the solution
Table of contents
- The idea is quite simple
- Here we go
- The yaml file
- Step 1: Checkout
- Step 2: Check if gitconfig exists
- Step 3: Set Git user
- Step 4: PowerShell to set the output variables
- Step 5: Use Node.js
- Step 6: install pnpm (OPTIONAL)
- Step 7: npm install
- Step 8: gulp clean
- Step 9: gulp build (OPTIONAL)
- Step 10 gulp bump-version
- Step 11: gulp bundle --ship
- Step 12: gulp package-solution --ship
- Step 13: Read updated version from package.json
- Step 14: push back to Repository
- Step 15: Create Git tag
- Step 16: Copy package file(s)
- Step 17: Publish Artifacts
- The complete file
- Result
Last week I published a blog post describing how to increment the version of the SPFx solution using a gulp task
. After that, people asked in the comments how to map the whole thing in an Azure DevOps pipeline so that the pipeline automatically increments the version and writes this updated version back to the repository. A very good question, I think, which I actually wanted to answer later (for myself). So far I only had the "normal" pipeline that publishes the package. Now the challenge came a bit earlier than I thought.
The idea is quite simple
When pushing to main/master branch, the Azure DevOps pipeline should automatically check out the branch, increment the version, build the package, check the changes back into the repository and (optionally) create a Git tag with the version number to make it clearer. You can turn off tagging or version incrementing in the pipeline. In addition, you can use the commit message to determine which part of the version (major, minor or patch) should be updated.
Here we go
Before you start with the pipeline and the .yaml
file, you have to configure something. In the project settings under Repository
=> {NameOfRepository}
=> Security
=> USER: {NameOfRepository} Build Service
. This user must be given the permissions Read
, Contribute
, Create tag
, Create branch
and Contribute to pull requests
.
The yaml
file
NOTE: This post and the yaml file were updated on January 25, 2024.
I won't show you how to create a pipeline but will go into the individual steps in the yaml
file. If that doesn't interest you, you can jump straight to the final file and use it.
Step 1: Checkout
- checkout: self
clean: true
persistCredentials: true
This step determines how the pipeline should check out the source code.
Step 2: Check if gitconfig exists
Sometimes, the pipeline might fail at step 3 if the .gitconfig file already exists. So, in this step, we check if it's there. I've only made it show whether the file exists or not. You can change the script to delete the file or create a variable to use as a "condition" in step 3.
- powershell: |
$exists = Test-Path -Path $HOME/.gitconfig
WRITE-HOST ".gitconfig exist:" $exists
workingDirectory: $(System.DefaultWorkingDirectory)
displayName: Check whether gitconfig exists
Step 3: Set Git user
- script: |
git config --global user.email devops@spfx-app.dev & git config --global user.name "spfx-app.dev".
workingDirectory: $(System.DefaultWorkingDirectory)
This sets the user to be displayed at (code) checkin/push. The user does not have to actually exist. It will only be displayed in Azure DevOps as specified.
Step 4: PowerShell to set the output variables
As mentioned at the beginning, the version should only be incremented if it is desired (pipeline variable CI_BUMP_VERSION
is set to TRUE
). In addition, the commit message Build.SourceVersionMessage
can be used to determine which part of the version (major
, minor
, or patch
) should be incremented. By default, the patch version is always incremented.
- powershell: |
$commitMsg = "$(Build.SourceVersionMessage)"
Write-Host $commitMsg
Write-Host "Version bump is ENABLED"
$bumpVersionArgs = "--no-patch"
Write-Host "##vso[task.setvariable variable=BUMP_MAJOR_VERSION;]$false"
Write-Host "##vso[task.setvariable variable=BUMP_MINOR_VERSION;]$false"
Write-Host "##vso[task.setvariable variable=BUMP_PATCH_VERSION;]$false"
if($commitMsg -match "(major):.*") {
Write-Host "Bump Major Version"
Write-Host "##vso[task.setvariable variable=BUMP_MAJOR_VERSION;]$true"
$bumpVersionArgs = "--major"
}
elseif($commitMsg -match "(minor):.*") {
Write-Host "Bump Minor Version"
Write-Host "##vso[task.setvariable variable=BUMP_MINOR_VERSION;]$true"
$bumpVersionArgs = "--minor"
}
else {
Write-Host "Bump Patch Version"
Write-Host "##vso[task.setvariable variable=BUMP_PATCH_VERSION;]$true"
$bumpVersionArgs = "--patch"
}
Write-Host "##vso[task.setvariable variable=BUMP_VERSION_ARGS;]$bumpVersionArgs"
displayName: Get the commit message
workingDirectory: $(System.DefaultWorkingDirectory)
condition: eq(variables['CI_BUMP_VERSION'], 'TRUE')
If the commit message contains a minor:
the minor version is incremented. If the commit message contains a major:
the major version is incremented. It is not necessary to specify patch:
as it will increment the patch version by default, but this message would increment the patch version. Depending on which version part is updated, the argument for the later gulp bump
command is stored in the variable BUMP_VERSION_ARGS
to be passed in step 10.
Note
: for a pull request, the "Title" column of the form in the UI corresponds to the variable Build.SourceVersionMessage
.
Step 5: Use Node.js
- task: NodeTool@0
displayName: 'Use Node 16.x'
inputs:
versionSpec: 16.x
checkLatest: true
I think it is clear what this step does. Node version 16.x will be installed.
Note
: Of course you have to adapt the version of Node to your SPFx version. A list of supported Node versions can be found here
Step 6: install pnpm (OPTIONAL)
This step is optional, but if you use pnpm
instead of npm
, you'll need to install pnpm first.
- task: Npm@1
displayName: 'npm install -g pnpm'
inputs:
workingDir: ' $(Build.Repository.LocalPath)'
command: 'custom'
customCommand: 'install -g pnpm'
verbose: true
enabled: false
If you use pnpm
, please remember to enable this step.
Step 7: npm install
- task: CmdLine@2
displayName: 'npm install'
inputs:
script: 'npm i'
workingDirectory: ' $(Build.Repository.LocalPath)'
enabled: true
All npm
packages will now be installed.
Step 8: gulp clean
- task: gulp@0
displayName: 'gulp clean'
inputs:
targets: clean
The gulp
task gulp clean
is executed.
Step 9: gulp build (OPTIONAL)
This step is not necessary, but if you'd like, you can enable it and run the gulp build --ship
command.
- task: gulp@0
displayName: 'gulp build --ship'
inputs:
targets: build
arguments: '--ship'
enabled: false
Step 10 gulp bump-version
Now the task I described in the last blog post is used to increment the version. Again, only if the pipeline variable CI_BUMP_VERSION
has been set to TRUE
.
- task: gulp@0
displayName: 'gulp bump-version'
inputs:
targets: 'bump-version'
arguments: $(BUMP_VERSION_ARGS)
continueOnError: true
condition: eq(variables['CI_BUMP_VERSION'], 'TRUE')
enabled: true
The stored value from the variable BUMP_VERSION_ARGS
from step 3 is passed as an argument (--major
, --minor
or --patch
).
Step 11: gulp bundle --ship
The gulp
task bump-version
is normally executed automatically before the gulp bundle --ship
task (see my blog post). But as you can tell the pipeline whether the version should be incremented or not, I had to split this task and pass the additional argument --no-ship
to the gulp bundle --ship
so that the bump-version
task is not executed again.
- task: gulp@0
displayName: 'gulp bundle --ship --no-patch'
inputs:
targets: bundle
arguments: '--ship --no-patch'
continueOnError: true
enabled: true
Step 12: gulp package-solution --ship
I think this step is self-explanatory. The solution is packed.
- task: gulp@0
displayName: 'gulp package-solution --ship'
inputs:
targets: 'package-solution'
arguments: '--ship'
enabled: true
Step 13: Read updated version from package.json
If the version was updated, it must of course be read again (for the later Git tag and also the commit message). I did this with a PowerShell script.
- powershell: |
$newVersion = (Get-content ./package.json| out-string | ConvertFrom-Json).version
Write-Host "##vso[task.setvariable variable=NEW_VERSION;]$newVersion"
Write-Host $newVersion
displayName: Get Version from package.json and set in variable
condition: eq(variables['CI_BUMP_VERSION'], 'TRUE')
The package.json
file is read and the version number is stored in the variable NEW_VERSION
.
Step 14: push back to Repository
Now the version is restored to the repository if the version has been updated.
- script: |
echo $(NEW_VERSION)
git add .
git commit -m "[skip ci] Version updated to $(NEW_VERSION)"
git push origin HEAD:$(Build.SourceBranch)
displayName: Push changes to source branch
workingDirectory: $(System.DefaultWorkingDirectory)
condition: eq(variables['CI_BUMP_VERSION'], 'TRUE')
enabled: true
All changes are checked in with the message [skip ci] version updated to $(NEW_VERSION)
. The [skip ci]
is very important. In case you have configured an automatic execution of the pipeline on the (main/master) branch, it would execute the pipeline again (continuous loop). This can be prevented with this text in the commit message (see: docs.microsoft.com/en-us/azure/devops/pipel..).
Step 15: Create Git tag
I thought it was helpful that a Git tag is created at the same time as the version update and after the check in. The name of the tag is then the version number. This is only done if the variables CI_BUMP_VERSION
and CI_CREATE_GIT_TAG
are both set to TRUE
.
- script: |
git tag $(NEW_VERSION) HEAD
git push origin --tags
displayName: Create Tag for last commit version
workingDirectory: $(System.DefaultWorkingDirectory)
condition: and(eq(variables['CI_BUMP_VERSION'], 'TRUE'), eq(variables['CI_CREATE_GIT_TAG'], 'TRUE'))
enabled: true
Step 16: Copy package file(s)
Now the .sppkg
files are copied into the temporary drop
folder.
- task: CopyFiles@2
displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)/drop'
inputs:
Contents: '**/*.sppkg'
TargetFolder: '$(Build.ArtifactStagingDirectory)/drop'
enabled: true
Step 17: Publish Artifacts
And finally, the artifacts have to be published so that the .sppkg
files can be downloaded.
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'
enabled: true
That was it (almost)
The complete file
Here is the complete file:
# SPFx Build Pipeline
# Author: seryoga@spfx-app.dev
# Version: 1.0
# Steps:
# Step 1: checkout the branch
# Step 2: Check if gitconfig exists
# Step 3: Set git user
# Step 4: Check commit message and determine version bump option
# Step 5: Use Node Version 16.x
# Step 6: OPTIONAL (Disbaled): install pnpm (for pnpm projects only)
# Step 7: Execute "(p)npm Install"-Command
# Step 8: Execute "gulp clean"-Command
# Step 9: (Disabled): Execute "gulp build --ship"-Command (not mandatory, you can disable this command when you want by adding to the task `enabled: false` after the `input`-section)
# Step 10: Execute "gulp bump-version"-Command
# Step 11: Execute "gulp bundle --ship --no-patch"-Command
# Step 12: Execute "gulp package-solution --ship"-Command
# Step 13: Read (new) version from package.json
# Step 14: Push the changes back to repository
# Step 15: Create Git Tag with the new version
# Step 16: Copy Files to: $(Build.ArtifactStagingDirectory)/drop
# Step 17: Publish Artifacts
trigger:
- development
pool:
name: Azure Pipelines
vmImage: 'ubuntu-latest'
steps:
- checkout: self
clean: true
persistCredentials: true
- powershell: |
$exists = Test-Path -Path $HOME/.gitconfig
WRITE-HOST ".gitconfig exist:" $exists
workingDirectory: $(System.DefaultWorkingDirectory)
displayName: Check whether gitconfig exists
- script: |
git config --global user.email devops@spfx-app.dev & git config --global user.name "spfx-app.dev"
workingDirectory: $(System.DefaultWorkingDirectory)
- powershell: |
$commitMsg = "$(Build.SourceVersionMessage)"
Write-Host $commitMsg
Write-Host "Version bump is ENABLED"
$bumpVersionArgs = "--no-patch"
Write-Host "##vso[task.setvariable variable=BUMP_MAJOR_VERSION;]$false"
Write-Host "##vso[task.setvariable variable=BUMP_MINOR_VERSION;]$false"
Write-Host "##vso[task.setvariable variable=BUMP_PATCH_VERSION;]$false"
if($commitMsg -match "(major):.*") {
Write-Host "Bump Major Version"
Write-Host "##vso[task.setvariable variable=BUMP_MAJOR_VERSION;]$true"
$bumpVersionArgs = "--major"
}
elseif($commitMsg -match "(minor):.*") {
Write-Host "Bump Minor Version"
Write-Host "##vso[task.setvariable variable=BUMP_MINOR_VERSION;]$true"
$bumpVersionArgs = "--minor"
}
else {
Write-Host "Bump Patch Version"
Write-Host "##vso[task.setvariable variable=BUMP_PATCH_VERSION;]$true"
$bumpVersionArgs = "--patch"
}
Write-Host "##vso[task.setvariable variable=BUMP_VERSION_ARGS;]$bumpVersionArgs"
displayName: Get the commit message
workingDirectory: $(System.DefaultWorkingDirectory)
condition: eq(variables['CI_BUMP_VERSION'], 'TRUE')
- task: NodeTool@0
displayName: 'Use Node 16.x'
inputs:
versionSpec: 16.x
checkLatest: true
enabled: true
- task: Npm@1
displayName: 'npm install -g pnpm'
inputs:
workingDir: ' $(Build.Repository.LocalPath)'
command: 'custom'
customCommand: 'install -g pnpm'
verbose: true
enabled: false
- task: CmdLine@2
displayName: 'npm install'
inputs:
script: 'npm i'
workingDirectory: ' $(Build.Repository.LocalPath)'
enabled: true
- task: gulp@0
displayName: 'gulp clean'
inputs:
targets: clean
enabled: true
- task: gulp@0
displayName: 'gulp build --ship'
inputs:
targets: build
arguments: '--ship'
enabled: false
- task: gulp@0
displayName: 'gulp bump-version'
inputs:
targets: 'bump-version'
arguments: $(BUMP_VERSION_ARGS)
continueOnError: true
condition: eq(variables['CI_BUMP_VERSION'], 'TRUE')
enabled: true
- task: gulp@0
displayName: 'gulp bundle --ship --no-patch'
inputs:
targets: bundle
arguments: '--ship --no-patch'
continueOnError: true
enabled: true
- task: gulp@0
displayName: 'gulp package-solution --ship'
inputs:
targets: 'package-solution'
arguments: '--ship'
enabled: true
- powershell: |
$newVersion = (Get-content ./package.json| out-string | ConvertFrom-Json).version
Write-Host "##vso[task.setvariable variable=NEW_VERSION;]$newVersion"
Write-Host $newVersion
displayName: Get Version from package.json and set in variable
condition: eq(variables['CI_BUMP_VERSION'], 'TRUE')
- script: |
echo $(NEW_VERSION)
git add .
git commit -m "[skip ci] Version updated to $(NEW_VERSION)"
git push origin HEAD:$(Build.SourceBranch)
displayName: Push changes to source branch
workingDirectory: $(System.DefaultWorkingDirectory)
condition: eq(variables['CI_BUMP_VERSION'], 'TRUE')
enabled: true
- script: |
git tag $(NEW_VERSION) HEAD
git push origin --tags
displayName: Create Tag for last commit version
workingDirectory: $(System.DefaultWorkingDirectory)
condition: and(eq(variables['CI_BUMP_VERSION'], 'TRUE'), eq(variables['CI_CREATE_GIT_TAG'], 'TRUE'))
enabled: true
- task: CopyFiles@2
displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)/drop'
inputs:
Contents: '**/*.sppkg'
TargetFolder: '$(Build.ArtifactStagingDirectory)/drop'
enabled: true
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'
enabled: true
Creating the variables
Before you save the pipeline, you must create variables. To do this, click on the "Variables" button at the top right of the editor and then on "+". Enter the variable name CI_BUMP_VERSION
and the value TRUE
. If you want to change these values when running manually, you have to select "Let users override this value when running this pipeline". Save the variable and do the same with the variable CI_CREATE_GIT_TAG
. Save the settings and then save the pipeline. Now it is ready to run.
Result
When the pipeline has gone through, it now looks like this:
And the tags were created:
Again, as a hint: Through the commit message you can define whether major, minor or patch version should be counted up.
Happy Coding ;-)