huserben / TfsExtensions

Extensions for TFS 2015+ such as custom Widgets (require TFS 2017) and Build Tasks
MIT License
45 stars 22 forks source link

Link Build to Release #198

Closed Ni2Be closed 2 years ago

Ni2Be commented 2 years ago

Hi, we are using "Trigger Build" in our release pipeline to build multiple micro services in one stage, that get packed into a setup in a second stage.

When I create a release that builds the setup with Build Artifacts (instead of using "Trigger Build") I have a link between the two. In Release: image

In Build: image

This is very convenient if we want to go from a release to a specific commit of a build.

Is there some way to achieve this (or something similar) with builds triggered by "Build Task"?

huserben commented 2 years ago

Hi @Ni2Be

could you please clarify whether I understood it correctly: Ideally you would like to see on the triggered build which other build was triggerring it (and potentially vice versa)?

If that's the case, I'm not sure if it's possible. Maybe a custom extension could be written that appears as "tab" like the "Releases" tab in your second screenshot. However that is probably quite some work. Other than that I can not think of something - ideally would be some existing functionality that could be hooked in to, as it would limit the effort. But I am not aware of anything like that, sorry.

Ni2Be commented 2 years ago

Hi @huserben

Almost, the Build is not triggered by another build but by a release-pipeline. So, for all builds I want to go into a build and see which release it belongs to and for a release I want to see which builds it did trigger.

I was thinking of adding the artifact the build created as an artifact of the release-pipeline with the api, but I think it is not possible to add them after the release was created. (these are the artifacts I’m talking about: https://docs.microsoft.com/en-us/azure/devops/pipelines/release/artifacts?view=azure-devops)

We want to give our release process a complete overhaul so maybe writing our own plugin would be an option, I’m just collection options at the moment. Is getting into “writing Azure DevOps extensions” something for a week + a sample project and then you are good to go or more like learning a new programming language + all frameworks? 😅

huserben commented 2 years ago

Almost, the Build is not triggered by another build but by a release-pipeline. So, for all builds I want to go into a build and see which release it belongs to and for a release I want to see which builds it did trigger.

Ok got it. I think what could be done is on the release side to just collect all builds and put it in some (text) file and upload it as artifact to that release. The task has the option to store all build ids of triggered builds, this could then be read and stored in a file (e.g. via some powershell script or so). On the side of the triggered build, I think it would be trickier. What could be done is to add a variable with the triggering build that you then pass - however that means you would need to adjust all the builds to have such a variable that you then can set while triggering. Otherwise there is no way to know what triggered it, as it's a simple REST call that is made in the back, like if you would queue it up manually...

We want to give our release process a complete overhaul so maybe writing our own plugin would be an option, I’m just collection options at the moment.

Maybe you can also think about ditching the "Releases" and instead look at yaml pipelines, they offer more functionality and also have support for triggering "upstream" builds (see docs on Pipeline Triggers). We moved to yaml builds for most of our pipelines and make use of pipeline triggers and also of multiple stages within one pipeline - that removed the need for this task in fact :-)

Is getting into “writing Azure DevOps extensions” something for a week + a sample project and then you are good to go or more like learning a new programming language + all frameworks?

As usual it depends. A pipeline task is easy to start if you have some basic knowledge of typescript/node.js. Of course if you want to do it more properly you should also think about how you (unit) test your task, and build & deploy it etc., that might take a bit more time. But you get started quickly if you want to try something out to even see if it's possible what you want to achieve. I can recommend having a look at the tutorial.

If we talk about an extension that adds some graphical things to Azure, you will need to know some things about frontend development (Reactjs is supported, not sure if other frameworks work too). This is what is called a web extension. I'm not that familiar with it, I just developed some extension for the Pipelines once, see:

Given that I was not really aware what I'm doing (due to the lack of frontend development know how) the code is probably not very optimally done. But maybe it can serve as inspiration what's possible.

So all in all it depends on what exactly you want to achieve and how good looking it should be :-)

I hope that helps you a bit.

Ni2Be commented 2 years ago

Maybe a file wouldn’t be too bad, I could probably generate the urls so navigating to a build would be easy.

On the side of the triggered build, I think it would be trickier. What could be done is to add a variable with the triggering build that you then pass - however that means you would need to adjust all the builds to have such a variable that you then can set while triggering. Otherwise there is no way to know what triggered it, as it's a simple REST call that is made in the back, like if you would queue it up manually...

The other way around is even trickier. :D Before a build is triggered we check if the micro-service needs to be build – so if there are no changes there is no build. We just use the artifact that was created last time the build ran. But we would like to link those “old” builds too.

Maybe you can also think about ditching the "Releases" and instead look at yaml pipelines, they offer more functionality and also have support for triggering "upstream" builds (see docs on Pipeline Triggers). We moved to yaml builds for most of our pipelines and make use of pipeline triggers and also of multiple stages within one pipeline - that removed the need for this task in fact :-)

I already forgot why I moved to a release-pipeline in the first place and we are already using yaml builds, so it’s probably worth a try.

The web extension thing also doesn’t sound too bad. We have some capable React developers and maybe this would be a very cool new toolkit to have :D

Big thanks for you input!

huserben commented 2 years ago

No problem, happy that I could be of (some) help.

If you have any other questions, face some issues with the task or have a request for a missing feature please don't hesitate to open up another issue.

Ni2Be commented 2 years ago

For future readers: We went with an approach to move back into a yaml-build-pipeline.

I wrote a template that does this:

Output: build if changed

Can be used like this:

- stage: BuildMyCoolService
  displayName: CoolServiceName
  pool:
    name: 'PoolName'
    demands: 'CanBuildCoolStuff'
  jobs:
  - template: BuildIfChanged.yaml
    parameters:
      AzureServerAndCollection: 'https://myBuildServer.com/CollectionName/'
      AzureProject: 'AwesomeProjectName'
      Branch_Name: 'dev/justSomeBranchNameNothingCoolGoingOnHere'
      Build_Name:  'AzureBuildName'

Template (Maybe there is a simpler way to get the needed information using Azures Api but I didn’t find one ;D):

# BuildIfChanged.yaml

###############################################################################################################
# 
# Builds the provided Build only if the provided branch contains changes (since last build of that branch).
# If the Branch has not changed it will show a link to the last Build of it.
# 
###############################################################################################################

parameters:
- name: AzureServerAndCollection # like "https://myBuildServer.com/CollectionName/"
  type: string
- name: AzureProject # project the repo and build are part of
  type: string
- name: Branch_Name  # branch to be build
  type: string
- name: Build_Name   # build to be used
  type: string

jobs:
  - job: 'BuildIfChanged'
    displayName: 'Build if changed'

    # Workspace settings
    workspace:
      clean: all

    variables:
      AzureServerAndCollection: ${{ parameters.AzureServerAndCollection }}
      AzureProject: ${{ parameters.AzureProject }}
      Build_Name: ${{ parameters.Build_Name }}
      Branch_Name: ${{ parameters.Branch_Name }}
      Is_UpToDate: false

    steps:
    - task: PowerShell@2
      displayName: 'Check if up to date'
      inputs:
        targetType: 'inline'
        script: |
          # uncomment for local development
          # $pat = "1234swy1234b1234fx54y1234xayuoz1234l7hdoeuz7mag1234"
          # $token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($pat)"))
          # $header = @{authorization = "Basic $token"}

          #$definition=""
          #$branchName="develop"
          #$upToDateAzureVarName = "$env:UPTODATE"

          $header = @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" }

          $azureServerAndCollection = "$(AzureServerAndCollection)"
          $azureProject ="$(AzureProject)"
          $definition="$(Build_Name)"
          $branchName="$(Branch_Name)"
          $upToDateAzureVarName = "$env:UPTODATE"

          Write-Host get latest build of branch
          $uri = $azureServerAndCollection+$azureProject+"/_apis/build/latest/"+$definition+"?branchName="+$branchName+"&api-version=6.0-preview.1"
          try { 
              $result = Invoke-WebRequest -Uri $uri -Method GET -Headers $header -UseBasicParsing
          } catch [System.Net.WebException] {
              Write-Host (ConvertFrom-Json $_).message
              Write-Host Needs to be build`n
              Write-Host "##vso[task.setvariable variable=$upToDateAzureVarName;]False"
              exit 1
          }
          $result = ConvertFrom-Json $([String]::new($result.Content))
          $buildId = $result.id
          $repositoryId = $result.repository.id
          Write-Host Latest buildId for branch $brancdirhName : $buildId`n
          Write-Host Latest repositoryId for branch $branchName : $repositoryId`n

          Write-Host get latest commit id in branch
          $uri = $azureServerAndCollection+$azureProject+"/_apis/git/repositories/"+$repositoryId+"/stats/branches?name="+$branchName+"&api-version=6.0"
          $result = Invoke-WebRequest -Uri $uri -Method GET -Headers $header -UseBasicParsing
          $result = ConvertFrom-Json $([String]::new($result.Content))
          $commitId = $result.commit.commitId 
          Write-Host latest commit in branch $branchName : $commitId`n

          Write-Host get latest build of that branch
          $uri = $azureServerAndCollection+$azureProject+"/_apis/git/repositories/"+$repositoryId+"/commits/"+$commitId+"/statuses?api-version=5.1"
          $result = Invoke-RestMethod -Uri $uri -Method GET -Headers $header -UseBasicParsing
          $latestBuildOfBranch = $result.value[0].targetUrl.Split('/')[-1]
          $state = $result.value[0].state
          Write-Host lastest build was: $latestBuildOfBranch
          Write-Host state: $state`n

          if($state -eq "succeeded" -and $latestBuildOfBranch -eq $buildId){
              Write-Host Is up to date, no need to build`n
              Write-Host "##vso[task.setvariable variable=$upToDateAzureVarName;]True"

              Write-Host related build:
              $uri = $azureServerAndCollection+$azureProject+"/_apis/build/builds/"+$buildId+"?api-version=6.0"
              $result = Invoke-RestMethod -Uri $uri -Method GET -Headers $header -UseBasicParsing
              Write-Host $result._links.web.href

          } else {
              Write-Host Needs to be build`n
              Write-Host "##vso[task.setvariable variable=$upToDateAzureVarName;]False"
              exit 1
          }
      env:
        UPTODATE: Is_UpToDate
        SYSTEM_ACCESSTOKEN: $(System.AccessToken)
      continueOnError: true

    - task: TriggerBuild@4
      displayName: 'Trigger a new build'
      inputs:
          definitionIsInCurrentTeamProject: false
          tfsServer: '$(AzureServerAndCollection)'
          teamProject: '$(AzureProject)'
          buildDefinition: '$(Build_Name)'
          useSameBranch: false
          branchToUse: '$(Branch_Name)'
          waitForQueuedBuildsToFinish: true
          waitForQueuedBuildsToFinishRefreshTime: 15
          storeInEnvironmentVariable: true
          authenticationMethod: 'OAuth Token'
      condition: and(succeeded(), eq(variables['Is_UpToDate'], False))

Note: To get visual feedback we set the build to "succeeded with issues" if the build is triggered. If this behavior in not wanted delete lines: exit 1 and continueOnError: true