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

Last week I published a [blog post](https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version) 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` =&gt; `{NameOfRepository}` =&gt; `Security` =&gt; `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](https://cdn.hashnode.com/res/hashnode/image/upload/v1655748596790/6_W-LOIiu.png align="left")

### 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](#heading-the-complete-file) and use it.

#### Step 1: Checkout

```yaml
- 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.

```yaml
- 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

```yaml
- 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.

```yaml
- 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`

```yaml
- 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](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/compatibility)

#### Step 6: install pnpm (OPTIONAL)

This step is optional, but if you use `pnpm` instead of `npm`, you'll need to install pnpm first.

```yaml
- 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`

```yaml
- 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`

```yaml
- 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.

```yaml
- 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](https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version) is used to increment the version. Again, only if the pipeline variable `CI_BUMP_VERSION` has been set to `TRUE`.

```yaml
- 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](#heading-step-3-powershell-to-set-the-output-variables) 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](https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version)). 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.

```yaml
- 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.

```yaml
- 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.

```yaml
- 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.

```yaml
- 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: https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#skipping-ci-for-individual-commits).

#### 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`.

```yaml
- 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.

```yaml
- 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.

```yaml
- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact: drop'
  enabled: true
```

That was it (almost)

### The complete file

Here is the complete file:

```yaml
# 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](https://cdn.hashnode.com/res/hashnode/image/upload/v1655751130129/ZlEshA19g.png align="left")

## Result

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

![Commit message](https://cdn.hashnode.com/res/hashnode/image/upload/v1655751167457/_lKwuIQT3.png align="left")

And the tags were created:

![Git tags](https://cdn.hashnode.com/res/hashnode/image/upload/v1655751194163/rM9UXozoQ.png align="left")

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

Happy Coding ;-)
