Automating Gitlab Releases

Automating Gitlab Releases

My previous post talked about versioning. I thought I would add to that discussion by talking about how I do releases and how I setup auto-updating in my apps, using the GitLab Package Repository. This a new feature I’ve started adding lately, so it’s still a work in progress, but I am currently using it on a project and it is working very well. Also I owe a huge deal of thanks to Felipe Pinheiro Silva. His blog posts have inspired a lot of this and helped me put some of the pieces together. This post just covers automating the release process. I’ll cover auto-updating in a future post.

Versioning

Before attempting this, check out my previous post about versioning. You’ll want to have some sort of auto-versioning system set up. I do the auto-versioning inside my build VIs that I call from G-CLI. It is based on the Git history.

This VI automatically calculates the version based on the Git history.
This VI automatically calculates the version based on the Git history.

What is a GitLab Release?

A GitLab Release is a way to signify that your code is officially ready for release. It is associated with a tag in your repository. The release grabs a copy of your source code and some evidence (it’s for traceability). It also gives you a spot to link to a package (under other) and a spot to put your release notes.

In GitLab under the Deployments menu, you can access a list of all releases for a project.

Automatically creating GitLab Releases

Automating releases in GitLab is a multistep process. This assumes that you are already somewhat familiar with GitLab CI. If not, try starting here. There are a couple steps, each of which are jobs or stages in your CI pipeline.

Triggering

The first step is triggering a release. Since good practice is to tag every release (so that you can then get the source code that went into a particular release easily), we’ll use the presence of a tag to trigger the release process. So I make my changes and commit everything. Then to start the process, I create a tag using git tag -a -m"Release x.x.x" x.x.x I like to use annotated tags for this so that it is clear who created the tag and when.

NOTE: I only use the first 3 version numbers. See my previous post on versioning.

Then I do a git push --follow-tags This will push only the annotated tags. The presence of an x.x.x tag is what will trigger the jobs associated with releasing code in addition to the normal CI jobs.

Release Notes

I should mention release notes here. You could add the release notes in the annotated tag. However the way I do it, is that the last commit (the one that gets tagged and released) is simply updating a release-notes.txt file that is stored in the repository. Then my CI jobs can use that to populate the release notes anywhere that requires them.

Build Job

In order to make your release, you need to have a build job. This can just be the normal build job . For me, this runs regardless of the tag. I want to make sure my changes haven’t broken the build, regardless of whether it is getting released or not. This job is needed for the release though, because we need the built executable, installer, or package to create the release. The build job must occur before the upload and release jobs. Any files created in the build job will automatically be available in subsequent stages. No need to create artifacts, although you probably want to anyway for debugging purposes.

Upload Job

A release allows us add a link to download our package. However we need a place to host that package. You could certainly setup your own webserver, but GitLab has a built-in package manager, so why not use that?

All you need to do the upload job is runner with curl installed. You can easily use a curl docker image. I use a custom one I created that also includes zip in case I want to zip some things up. You just use curl and the GitLab API to upload the file(s) you want to release to GitLab’s built in package repository. In my case it is usually a single package (either an NI Package or a VIPM package).

When you upload a package using curl, it shows up in the GitLab package registry for your project.

Under the package registry, you can get a list of all the packages.
Here is the information contained in an individual package. In this example, the package includes a link to an NI package. It is not limited to one file. It could include multiple files.

Release Job

The release job simply creates a release for the tag. It gives you place to store a link to the package and some release notes. The package is uploaded in the upload step and I get the release notes simply by catting the release-notes.txt file. GitLab maintains a special docker image just for this and has some special syntax.

Technically the upload and release step can occur in parallel (since we already know the link ahead of time – it is a predetermined format), but I’ve had some weird issues where the upload fails for some reason and then I have a release with a dead link. To avoid that I do these serially so that if the upload fails, the release never happens.

Notification

Once you make a release, you need to let people know it is available. You could have a separate notification job that sends an e-mail, but the easy way is to let GitLab do it for you. If the person you want to notify is a member of the project in GitLab, they can subscribe within GitLab’s notification settings to get an e-mail from GitLab when there is a new release.

These are the notifications you can set up in GitLab. You can set these globally or for each project. If your customers have access to your project, they can subscribe to be notified of new releases. It requires no effort on your part, GitLab will take care of notifying them.

Something else I also do is use the GitLab API from my program to check for new releases. I’ll make a new post about that shortly.

Sample YAML File

Below is a sample .yaml file showing how to set it all up.

stages:
  - build
  - upload
  - release


build_exe_pkg:
  variables:
    GIT_DEPTH: 0 # this is needed so build numbers calculate correctly. It is based on number of commits in entire repository.
  stage: build 
  tags:
    - LV21x64
  script:
    - g-cli --kill LVBuildSpec -- -av -p "Solid-State Electron Emitter.lvproj" -b "SSEE EXE"
    - g-cli --kill LVBuildSpec -- -av -p "Solid-State Electron Emitter.lvproj" -b "SSEE Package"
  artifacts:
    paths:
      - builds/exe
      - builds/package/$(ls -t builds/package | head -1)
    when: always

     
release_job:
  stage: release
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  rules:
    - if: $CI_COMMIT_TAG =~ /[0-9]*.[0-9]*.[0-9]/                 # Run this job when a tag is created manually
  script:
    - 'echo "creating release"'
 
  release:
    name: 'Release $CI_COMMIT_TAG'
    description: '$(cat release-notes.txt)' 
    tag_name: '$CI_COMMIT_TAG'             
    ref: '$CI_COMMIT_TAG'   
    assets:
      links:
        - name: '$(ls -t builds/package | head -1)'
          url: '${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/SEEE/${CI_COMMIT_TAG}/$(ls -t builds/package | head -1)'
      

upload_job:
  stage: upload
  image: registry.gitlab.com/sas_public/curl-jq
  rules:
    - if: $CI_COMMIT_TAG  =~ /[0-9]*.[0-9]*.[0-9]/                  # Run this job when a tag is created manually
  script:
    - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file "builds/package/$(ls -t builds/package | head -1)"  "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/SEEE/${CI_COMMIT_TAG}/$(ls -t builds/package | head -1)"'

Let’s walk through it quickly:

  • lines 1-4 setup the order of the stages. First we build. Then we upload. Then we do the release. As I mentioned above, in theory you could do the upload and release jobs in parallel in the same stage, but I prefer using seperate stages so they execute in series. That avoids ending up with a release where the link is dead.
  • lines 7-20 define the job that builds the executable and then the package.
    • lines 8 and 9 tell GitLab to do a full clone for this job so our version number comes out correctly
    • line 10 makes sure this job executes during the build stage
    • line 11 and 12 ensure this job gets executed on a machine running LabVIEW 2021 64bit
    • lines 13-15 are the g-cli commands to build first the executable and then the pacakage. LVBuildSpec is the name of a custom G-CLI command that I built for building LV BuildSpecs.
      It has some parameters:
      • -av specificies automatic versioning, which means it will automatically calculate the version number based on the Git history
      • -p is the project file
      • -b is the specific build spec within the project to build.
    • lines 16-20 tell GitLab to grab the build output and store it on the GitLab server as an artifact. When always is useful so if the build fails, it still grabs the build log.
  • lines 23-39 define the release job.
    • line 25 tells GitLab to run the job on a Docker image. This image is provided by GitLab and has all the tools preinstalled to create releases
    • line 26-27 cause the release job to only run when the commit tag is of the correct form: ie x.x.x
    • lines 31-39 define the release and what goes into it. In line 30 I take a text file with the release notes and use that as the description. In lines 36-39 I add a link to the package that got uploaded in the upload step. $(ls -t builds/package | head -1) simply refers to the most recent file in the builds/package directory. I reference it this way because then I don’t care what the exact filename is.
  • lines 42-48 define the upload job.
    • It runs in a Docker Image. You really just need an image with curl. In this case I am using a custom image I created which also includes zip, in case I need to zip anything up
    • line 48 is the key to this job and it is a simple curl command. Note the authentication is provided by the $CI_JOB_TOKEN environment variable. This is unique to each job and set by GitLab. Also note the format for the url.

Finding a System that works for you

What I’ve just shown you is what a typical CI Release Pipeline looks like for me. I’m not claiming it is the best way, just what works for me. I am also always experimenting and finding new and better ways of doing things. If you have suggestions on how to improve it, please let me know.

Automating the release is the first step to setting up an auto-updating system. This can make it very quick and easy to get new code to your customers. Once it is all working, making a release is very simple:

  • make your changes
  • update your release notes
  • tag the commit
  • push the tag
  • CI works its magic and your customer get’s notified that there is a new release and it’s available for them to download and install.

This makes it very easy to do agile development because it makes it very simple to get working code into your customer’s hands quickly.

If simplifying your release process like that sounds like a breath of fresh air, let’s talk. There are a lot of little gotchas that can trip you up. We’ve already worked a lot of them out so we can help you get up and running quickly. Use the button below to schedule a call.