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
- Rules are evaluated in order, the first one that matches defines the job's behavior.
- Super customizable with expressions, variables, tags, branch, MR, etc.
A quick table to understand the difference.
| Aspect | only / except | rules |
|---|---|---|
| Official support | Deprecated | Current and recommended |
| Flexibility | Low | High |
| Conditional (if) | No | Yes |
| Evaluation order | Limited | Sequential and logical |
| MR detection | Limited | Advanced |
| Expressiveness | Low | High |
Only and except are only present for backwards compatibility and nothing more.
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>
| Field | Required? | Description |
|---|---|---|
| if | ❌ | Boolean expression using GitLab variables ($CI_*, etc). |
| when | ❌ | Defines when the job will be executed (on_success, manual, etc). |
| start_in | ❌ | Used with when: delayed (ex: "5 minutes"). |
| allow_failure | ❌ | If true, job failure doesn't break the pipeline. |
| exists | ❌ | Runs the job if the file(s) exist. |
| changes | ❌ | Runs the job if the listed files were modified. |
| variables | ❌ | List 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 Value | Behavior |
|---|---|
| on_success | Executes the job only if no previous job fails. (Default) |
| on_failure | Executes the job only if some previous job fails. |
| never | Never executes the job, regardless of any condition. Can only be used in rules, since it wouldn't make sense otherwise |
| always | Executes the job always, even if previous jobs fail. |
| manual | The job waits for manual execution (button appears in GitLab UI). |
| delayed | The 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"'