Limit deployments to Platform.sh only when Git tagged: part two
In part one of this series, we covered how you could limit deployments to Platform.sh only when a tag is pushed/created, focusing primarily on using GitHub and the GitHub Actions platform to accomplish this goal. But we’re a polyglot PaaS and strive to be agnostic in our users’ source code management terms of the service. With that in mind, let’s look at how we can accomplish the same goal using GitLab and your CI/CD system.
Just like last time, there are some assumptions to consider:
The assumptions
For the purpose of this article and the steps detailed, I will assume that you have:
- A GitLab account and a repository with working code
- Administrative rights on the GitLab repository (so you can add CI/CD variables)
- A Platform.sh account and a project for your code base with working code
- A default branch in GitLab which is the same branch as your production branch in Platform.sh
- A default branch in Platform.sh which is also your production branch
- You do not have a source code integration created between Platform.sh and GitLab.
The .gitlab-ci.yaml
file
While GitHub has a `.github/workflows
directory where any *.yaml
file can be a workflow definition, GitLab consolidates the CI/CD configuration into a single file named .gitlab-ci.yml
. This file must be in the root of your repository and contain everything needed to run your GitLab pipeline.
The event
Unlike GitHub workflows, GitLab pipelines are triggered1 when any commit is pushed to your repository. In our case, we only want the pipeline to run when a tag is pushed or created. Instead of defining the specific event we want to trigger our pipeline, we'll build a set of rules to define which type of push should trigger our pipeline. To build these rules we'll utilize the workflow
property which controls when pipelines are run.
workflow:
name: Deploy to Platform.sh on tag
rules:
- if: $CI_COMMIT_TAG && $CI_PIPELINE_SOURCE == "push"
The predefined CI/CD variable $CI_COMMIT_TAG
contains the commit tag name and, most importantly, is only set in tag pipelines. The $CI_PIPELINE_SOURCE
variable contains how the pipeline was triggered. Since we want a tag push, we want to only run this pipeline if the trigger is a push
event. I've also added a name
property so this pipeline is easier to identify in the repository's pipelines area.
The stages
Like with GitHub, we'll need to define a series of jobs that perform the tasks. And also just like with GitHub, jobs run in parallel by default. However, unlike GitHub Actions, GitLab gives us a way to control the order in which jobs run with stages. The stages are executed in order2, and later stages can only start at earlier stages that have been completed.
Before we define our jobs, we'll define the stages that should occur, and then we'll attach jobs3 to their relevant stage.
stages:
- setup
- perform
I've defined a setup
stage, where I'll add the jobs that check to see if the tag that was pushed is one we want to deploy to Platform.sh, and a perform
stage where I'll add the jobs to perform the push.
Caching
You might be wondering, "Why is there a section on caching when we just want to see if we need to push, and why is it before we've defined any jobs?" Unlike GitHub and its outputs, GitLab does not provide a straightforward way to share small bits of data between jobs and stages. To facilitate data sharing they instead utilize artifacts and cache.
In either case, we have to use a file to store the data we want to share. Since job artifacts were specifically designed for sharing intermediate build results between stages, in the case of just needing to pass a small bit of data to the next stage, it's overkill. Instead, for this pipeline, we'll set up caching and cache a single file to share data between our stages.
Caching needs to be set up on a per-job basis but since we want both stages and their jobs to access the same cache, we'll define our cache instead in the default area of our pipeline.
default:
cache: &global_cache
key: push-to-psh
paths:
- "push.txt"
when: always
Here I'm using a yaml anchor ( &global_cache
) to set up a reusable cache definition at the global level that I then reference in each job, but with the ability to override any properties as needed. A cache is required to have a unique key
, and jobs that use the same cache key will use the same cache. Perfect for what we need!
We then define the file we want to cache (push.txt
) using the paths
property and instruct GitLab to always
save the cache with when
.
The jobs
Now I'll define my jobs and attach them to the stage where they should run. To make things easy to read, I'll define the job in the setup
stage first and attach it to setup
using the stage
property:
should_we_push:
stage: setup
By default, GitLab checks out a shallow clone of your repository based on the commit that was pushed. In our case, we want to see if the tag that was pushed is the closest tag to the latest commit on our default branch so we'll instruct GitLab instead to checkout the default branch.
We can do so by using the before_script
property. Any steps defined in before_script
are executed before the job is started which allows us to alter the default behavior:
should_we_push:
stage: setup
before_script:
- git fetch origin "${CI_DEFAULT_BRANCH}"
- git fetch --tags
- git checkout "${CI_DEFAULT_BRANCH}"
Now before the should_we_push
job begins, we'll fetch any changes from GitLab, then fetch all the tags, and finally checkout the default branch. This is fairly similar to what we accomplished in the GitHub Actions article when we used the actions/checkout action to checkout our repository using the default branch with a depth of 0. The script
section seen below contains the commands we want our runner to execute.
should_we_push:
stage: setup
before_script:
<snip>
script: |
pushToPSH="no"
if [ "${CI_COMMIT_TAG}" == "$(git describe --abbrev=0 --tags)" ]; then
pushToPSH="yes"
fi
echo "${pushToPSH}" >> push.txt
Now, we check to see if the pushed tag is the most recent from the latest commit in our default branch (git describe
), exactly the same as we did in the GitHub workflow. If so, we set the value of pushToPSH
to yes
and save that value in the push.txt that we'll save in our cache.
Last we need to set up the cache
configuration for the job:
should_we_push:
stage: setup
before_script:
<snip>
script: |
<snip>
cache:
<<: *global_cache
policy: push
Here I'm referencing the YAML block I set up earlier for cache ( *global_cache
) which tells the YAML parser to grab that named section and reuse it. The <<:
informs the YAML parser that I'm going to add value or override existing ones to the section.
Since we never want this job to reuse the cache from a previous run, I'll override the default policy
of pull-push
to only push
for this job (i.e. only upload a cache when the job is complete, never download cache when the job starts).
I'll next define our second job and attach it to the perform stage:
we_should_push:
stage: perform
Since this job will utilize the data stored in the cache, it’s time to go ahead and set up the cache configuration4:
we_should_push:
stage: perform
cache:
<<: *global_cache
policy: pull
Similar to the should_we_push
job, I'm reusing the cache configuration I created in the default section (*global_cache
), but this time I'm changing the policy to pull
. This ensures the cache will always be downloaded before the job begins, but never uploads the cache when the job completes.
Before we can build out the rest of the steps, just like we did with GitHub, we'll need to create a couple of repository variables that we can access in our steps. Make sure you've generated a Platform.sh API token, and have the Platform.sh ID of your project.
To locate your project's ID, if you are logged into the console, you can locate your project's ID under the title of your project, to the right of the project's region when viewing the project's page:
Or to locate your project’s ID from the command line, when in the local copy of your project, you can run the following command:
❯ platform project:info id
ropxcgtns2wgy
From there, follow the GitLab directions for adding a CI/CD variable in the UI, adding one for PLATFORM_CLI_TOKEN
5 and one for PROJID
, setting the values as appropriate.
And finally, we’ve arrived at the steps for pushing our new code to Platform.sh—if the should_we_push
job determined this was a tag we want to deploy to Platform.sh, that is.
we_should_push:
<snip>
script: |
if [ $(cat push.txt | grep yes) ]; then
echo "setting up the platform.sh cli"
curl -fsSL https://raw.githubusercontent.com/platformsh/cli/main/installer.sh | bash
platform project:set-remote "${PROJID}"
echo "set up a new ssh cert"
platform ssh-cert:load --new --no-interaction
pshWholeGitAddress=$(git remote get-url platform --push)
pshGitAddress=$(TMP=${pshWholeGitAddress#*@};echo ${TMP%:*})
echo "Adding psh git address ${pshGitAddress} to known hosts"
ssh-keyscan -t rsa "${pshGitAddress}" >> ~/.ssh/known_hosts
echo "Pushing tag ${CI_COMMIT_TAG} to Platform.sh..."
pshDefaultBranch=$(platform project:info default_branch)
git push platform "refs/tags/${CI_COMMIT_TAG}^{commit}:refs/heads/${pshDefaultBranch}"
else
echo "no changes to push. exiting"
fi
Beyond checking to make sure "yes
" is contained in our cached push.txt
file, the rest of the steps are identical to the steps we used in the GitHub Actions article for the we_should_push
job.
Your .gitlab-ci.yml
file is now ready to commit to your repository and push it to GitLab. You'll need to follow whatever workflow path you use to get the file into your production branch, be it pushing directly, if allowed, or through a pull request process.
You’re all set
Now that the .gitlab-ci.yml
file is committed into your default branch, any future tags that are pushed to GitLab, or created on GitLab when creating a release will trigger the pipeline. If the tag is newer than any other tags (closest to the current commit), our we_shou
job will run and push that tag to Platform.sh and deploy your new code base!
Take a look at the complete GitLab pipeline file.