Skip to main content

Rules

Before we start talking about rules, know that it is an evolution of something that is still used but is deprecated called only.

Two things used to be used together (only and except) that today we solve with just rules. Use rules whenever possible. only is on its way out.

Just to understand the context of what we had before...

job:
script: echo "Running job"
only:
- main # Only runs on main branch
- tags

only: runs the job only when the commit comes from certain conditions (branch, tag, etc).

Or except...

job:
script: echo "Running job"
except:
- develop

except: doesn't run the job if it falls into any of these conditions.

Limitations:

  • We don't have if or conditional logic using only and except.
  • We can't make combined logic using variables.

So forget that and let's go straight to what gives us more power, rules.

job:
script:
...
rules: [] # It's a list
  1. Rules are evaluated in order, the first one that matches defines the job's behavior.
  2. Super customizable with expressions, variables, tags, branch, MR, etc.

A quick table to understand the difference.

Aspectonly / exceptrules
Official supportDeprecatedCurrent and recommended
FlexibilityLowHigh
Conditional (if)NoYes
Evaluation orderLimitedSequential and logical
MR detectionLimitedAdvanced
ExpressivenessLowHigh

Only and except are only present for backwards compatibility and nothing more.

Official Rules Documentation

Official Only and Except Documentation

The structure of a rules block is as follows.

job:
script:
...
rules:
- if: <expression>
when: [on_success | on_failure | manual | always | never | delayed]
start_in: <time>
allow_failure: true/false
exists:
- <path/to/file>
changes:
- <file or folder>
variables:
- <variable>
FieldRequired?Description
ifBoolean expression using GitLab variables ($CI_*, etc).
whenDefines when the job will be executed (on_success, manual, etc).
start_inUsed with when: delayed (ex: "5 minutes").
allow_failureIf true, job failure doesn't break the pipeline.
existsRuns the job if the file(s) exist.
changesRuns the job if the listed files were modified.
variablesList of required variables for this rule to be applied.

when

We have an important item there that besides being inside rules can also be declared outside (directly in the job) which is when. First let's understand what it does and then understand the difference between using it in rules and outside it.

when ValueBehavior
on_successExecutes the job only if no previous job fails. (Default)
on_failureExecutes the job only if some previous job fails.
neverNever executes the job, regardless of any condition. Can only be used in rules, since it wouldn't make sense otherwise
alwaysExecutes the job always, even if previous jobs fail.
manualThe job waits for manual execution (button appears in GitLab UI).
delayedThe job is executed with a delay, according to the time defined in start_in:.

An example of when: manual would be that someone needs to approve the deploy to go to production.

job_manual:
script: echo "Running the deploy..."
when: manual

Example delayed + start_in would be to give time between staging deploy and production deploy so there's time to test and if someone notices an error there's time to cancel the job before it runs. If using delayed, start_in is mandatory.

job_delayed:
script: echo "Running the deploy..."
when: delayed
start_in: 10 minutes

An example of always could be sending a notification at the end of the pipeline even if it fails, for example sending an email and a slack notification. If you want it to only send if it fails then it could be on_failure.

When we declare when outside of rules what we're actually doing is putting the same when for all rules. This serves to make rules cleaner.

job:
script: echo "Running"
when: manual
rules:
- if: Condition 1
- if: Condition 2

In other words, it's the same thing as doing this.

job:
script: echo "Running"
rules:
- if: Condition 1
when: manual
- if: Condition 2
when: manual

But we can override it for a specific rule. In this case the job is always manual, but if it's on the develop branch then it runs directly without intervention.

job:
script: echo "Deploying"
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_BRANCH == "staging"'
- if: '$CI_COMMIT_BRANCH == "develop"'
when: on_success

allow_failure

The same applies to allow_failure, which can be defined directly at the job level or individually within each rule in rules. When defined at the job level, the value will be applied as default for all rules.

By default, allow_failure is false, meaning: if the job fails, the pipeline will also be considered failed. When set to true, the job can fail without breaking the pipeline, allowing the next jobs to continue normally.

This is useful, for example, for experimental tests, static analyses or any non-critical step that you don't want to block the rest of the execution.

exists, changes and variables

Often code undergoes changes that don't change anything in the environment, a great example of this is making a change in README.md. Why would we deploy?

In the rules block, besides if, you can use exists and changes, which are ways to activate a job based on the presence or modification of files.

job:
script: "Creating an image"
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
exists:
- Dockerfile
- .dockerignore
job:
script: "Creating an image"
when: manual
rules: # Notice that the IF can exist. both changes and exists.
- changes: # If any of these files change
- src/**
- config/*.yaml

As for the variables block, as we saw before, it serves to define custom variables.

deploy:
stage: deploy
image: alpine
script:
- echo "Deploy starting with TOKEN=$TOKEN"
environment:
name: $DEPLOY_ENV
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
variables:
DEPLOY_ENV: develop
- if: '$CI_COMMIT_BRANCH == "main"'