Comparing CI pipelines on different platforms

Tomomi Sako
5 min readJun 11, 2022
Photo by Quinten de Graaf on Unsplash

Nowadays, we have a lot of ways to manage our code and I believe most people are using Github, Azure DevOps, or Gitlab. From the perspective of code management, there are not many differences between them. However, they do diverse more in their pipeline offerings.

Unfortunately, we have different formats/syntaxes between those three DevOps platforms. So sometimes it will be a challenge for a Software engineer to set up a pipeline in different DevOps platforms.

In this article, I will compare those three different DevOps platforms using a Golang project as a sample code. If you want to see the complete pipeline code first, please scroll all the way to the bottom.

Pipeline contents

Although the format/syntax varies between pipelines, the content of pipelines should be the same. So I will quickly explain what the pipeline in the sample code looks like.

Test/ Build stage

1. Set up Golang environment
2. Run unit tests
3. Run code coverage
4. Run linting
5. Build a project
6. Publish an artifact

Deploy stage

1. Download the artifact
2. Deploy the artifact to Azure Function

Set up Golang environment

The pipeline needs to have a Golang environment configured to execute Golang commands for testing and linting, etc.

GitHub Actions

Setting up the Golang environment in Github Actions is actually pretty easy. We can simply use setup-go.

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17.6

And after that, we can use the go command in the same stage. This sample uses the make command to execute the GOOS=linux go build -o app cmd/main.go command.

- name: Build
run: make build_linux

Azure DevOps

Azure DevOps has a Go task. However, we can not use it for a project using a make file. The sample code uses a make file to execute the go command so we need to set up the Golang environment in the pipeline.

- task: GoTool@0
inputs:
version: '1.17.6'
displayName: Install Go
- script: |
echo '##vso[task.prependpath]$(GOBIN)'
echo '##vso[task.prependpath]$(GOROOT)/bin'
displayName: Add GOPATH

We can still use Go task to install Golang in the pipeline. Then we need to export the GOPATH using echo '##vso[task.prependpath]$(PATH)'.

After that, we can use go command in the same stage. Since we already set up the Golang environment, we can just use the bash task to run the make command.

- script: make build_linux
displayName: 'Build'

Gitlab

To set up the Golang environment in the pipeline, I referred to this blog in Gitlab. We can use golang base docker image and set up GOPATH in the before_script section.

image: golang:1.17.6before_script:
- mkdir -p $GOPATH/src/$(dirname $REPO_NAME)
- ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME
- cd $GOPATH/src/$REPO_NAME

After that, we can use go command in the pipeline.

build:
stage: build
script:
- make build_linux

Publish/Download an artifact

After building the project, we need to publish the compiled binary file as an artifact. And then we can download it at a different stage.

GitHub Actions

We can use upload-artifact and specify the name and path.

- name: Upload
uses: actions/upload-artifact@v2
with:
name: app
path: ./app

Then we can download it in a different stage using download-artifact.

- name: Download
uses: actions/download-artifact@v2
with:
name: app

Azure DevOps

As with Github Actions, Azure DevOps also has the Publish Pipeline Artifacts task. Set the path to the binary file in the targetPath.

- task: PublishPipelineArtifact@1
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/app
artifactName: gobinary
displayName: 'Publish binary for later use'

For downloading the artifact, we can use the Download Pipeline Artifacts task.

- task: DownloadPipelineArtifact@2
inputs:
artifact: gobinary
path: $(Build.SourcesDirectory)
displayName: 'Download binary'

Gitlab

Gitlab also has a task for creating an artifact: Job artifacts. This job is used with another script so we can use it with the build command.

build:
stage: build
artifacts:
untracked: true
script:
- make build_linux

To fetch the artifact, we can use dependencies. This job also needs to use another script so we can add it to the deploy script that I will explain in the next step.

deploy:
stage: deploy
script:
# - Will explain later.
dependencies:
- build

Deploy the artifact to Azure Function

Now we are ready to deploy the artifact somewhere. In this sample, I will show you how to deploy the artifact to Azure Function. Azure Function is a serverless solution in Azure and the sample code uses the custom handler.

GitHub Actions

Continuous delivery by using GitHub Action shows how to deploy Azure Function by GitHub Actions. But unfortunately, using the “publish profile” way doesn’t support the custom handler option yet so we need to use the Azure Login task.

- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: 'Run Azure Functions Action'
uses: Azure/functions-action@v1
with:
app-name: ${{ secrets.FUNCTION_APP }}
package: ${{ github.workspace }}

Also if we want to set up the condition to “run the deploy stage only for the main branch”, we can add an if condition. The deploy stage has a dependency on the build stage so it has a needs condition as well.

deploy:
name: Deploy
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'

Azure DevOps

Azure DevOps has the Azure Function App task so we can just use that task as well as the Archive Files task to create a zip file from a source folder.

- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(Build.Repository.LocalPath)'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
replaceExistingArchive: true
displayName: 'Archiving binaries'
- task: AzureFunctionApp@1
inputs:
azureSubscription: $(AzureSubscription)
appType: 'functionAppLinux'
appName: $(FunctionApp)
package: '$(System.ArtifactsDirectory)/**/*.zip'
displayName: 'Publishing build artifacts'

To set up the “Run the deploy stage only for the main branch” condition, we can use a custom condition.

- stage: Release
dependsOn: build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))

Gitlab

Unfortunately, Gitlab doesn’t have a specific job that can easily deploy Azure Function so we need to use a bash script to deploy it.

First, we need to download the az command to the pipeline. And then we can run az login to log in to your Azure Subscription using a service principal.
We also need to download the Azure Functions Core Tools to deploy the Azure Function using the func azure functionapp publishcommand.

only can specify which branch would like to be triggered. So it specifies the main branch.

deploy:
stage: deploy
before_script:
- export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
script:
- curl -sL https://aka.ms/InstallAzureCLIDeb | bash
- wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.asc.gpg
- mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/
- wget -q https://packages.microsoft.com/config/debian/11/prod.list
- mv prod.list /etc/apt/sources.list.d/microsoft-prod.list
- chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg
- chown root:root /etc/apt/sources.list.d/microsoft-prod.list
- apt-get update && apt-get -y install --no-install-recommends azure-functions-core-tools-3
- az login --service-principal -u $APPLICATION_ID -p $APPLICATION_SECRET --tenant $TENANT_ID
- func azure functionapp publish $FUNCTION_APP --custom
dependencies:
- build
only:
- main

Complete pipelines

This Github repository contains the Golang sample code and the definition for all three pipelines in the pipelines folder.

--

--