SPFx Azure DevOps Pipeline: Increment version, push to repository and publish package

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

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.

Userpermission settings in Azure DevOps

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.

Add new Variables

Result

When the pipeline has gone through, it now looks like this:

Commit message

And the tags were created:

Git tags

Again, as a hint: Through the commit message you can define whether major, minor or patch version should be counted up.

Happy Coding ;-)

Did you find this article valuable?

Support $€®¥09@ by becoming a sponsor. Any amount is appreciated!