Extends
Looking at our pipeline, we can observe that the build wasn't even used to perform the tests we want. The earlier we catch errors, the better. We're wasting time with that build step before. How could we improve this? Let's create a stage before called check, but we could use the default .pre.
before_script, script and after_script​
The order of execution in a GitLab CI job is:
- before_script (global or job-specific)
- script
- after_script (if it exists)
After that, if the job succeeds, it saves the artifacts (if defined)
before_script and after_script can be defined, but they're not mandatory. If defined...
- In default: → affects all jobs
- Or individually per job
If before_script fails, the script doesn't run, but if the script fails, after_script still runs (for cleanup, logs, notifications, etc.).
Knowing this and with the information we have, we can already improve our pipeline. Let's apply the concept of extends and templates to streamline this.
default:
tags:
- general
# Jobs will now have this image by default, eliminating one line in each of them
# It's also a smaller image.
image: node:22-alpine
stages:
- check # New earlier stage
- build
- test # This is here but won't be used, just to show that I haven't blocked the pipeline if a stage is defined but not used
.check: # Template for check stage with only what will be standard for all
stage: check
before_script:
- npm ci
artifacts:
when: always
expire_in: "3 months"
unit-test:
extends: .check
script:
- npm test
artifacts: # Now we only put the reports and eliminate other things
reports:
junit: reports/junit.xml
lint-test:
extends: .check
script:
- npm run lint
artifacts:
reports:
codequality: gl-codequality.json
vulnerability-test:
extends: .check
script:
- npm audit --audit-level=high --json > vulnerability-report.json
artifacts:
paths:
- vulnerability-report.json
build:
stage: build
script:
- node --version
- npm --version
- npm ci
- npm run build
artifacts:
when: on_success
expire_in: "1 hour"
paths:
- build/
Before with the slim image we had this time and this sequence.

Now we have these times and this sequence. Extends and templates only improve code visibility, not performance. What improved the time was the smaller image.

An important detail. Extends is actually a list. We can extend more than one template as if it were a combination between them.
jobx:
extends: [.check, .other]
If .check and .other define the same thing, .other will prevail because it was the last to write. It's an ordered list.
Now it's time to define when this pipeline will be executed because it's being executed always. If a person is developing, the code isn't even finished properly and they need to push to their branch, we don't need the pipeline to be executed. Let's start this concept, but we'll understand better about this later.
In one of the jobs we can have a rule that will define when a job should or shouldn't be executed.
ex:
jobX:
...
script:
...
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
when: always
We'll introduce variables and rules soon. This CI_MERGE_REQUEST_TARGET_BRANCH_NAME variable is a GitLab variable and there are several. The rule above says that the pipeline should always be executed when it's a merge request directed to main. We could put this in all our jobs, but let's take advantage and do an extends of two templates at the same time.
default:
tags:
- general
image: node:22-alpine
stages:
- check
- build
.check: # Template for check stage
stage: check
before_script:
- npm ci
artifacts:
when: always
expire_in: "3 months"
.rules-only-main-mr: # Template for general rules
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
when: always
unit-test:
extends: # We can define it this way
- .check
- .rules-only-main-mr
script:
- npm test
artifacts:
reports:
junit: reports/junit.xml
lint-test:
extends: [.check,.rules-only-main-mr] # Or this way
script:
- npm run lint
artifacts:
reports:
codequality: gl-codequality.json
vulnerability-test:
extends: [.check,.rules-only-main-mr]
script:
- npm audit --audit-level=high --json > vulnerability-report.json
artifacts:
paths:
- vulnerability-report.json
build:
stage: build
extends: [.rules-only-main-mr] # Or this way or as we saw before in case of a single extends: .rules-only-main-mr
script:
- node --version
- npm --version
- npm ci
- npm run build
artifacts:
when: on_success
expire_in: "1 hour"
paths:
- build/
Now when uploading this code, the pipeline won't trigger. Test it.
However, when creating a merge request we'll have the pipeline triggered, and if you followed the recommendations, we can only merge to main if the pipeline passes.
The result...
