GitLab CI/CD – Script

In today’s blog post, we will learn .gitlab-ci.yml file script keyword. In particular, what is before_script, script, and after_script keywords in .gitlab-ci.yml, how to define before_script, script, and after_script with example, comparison between before_script, script, and after_script blocks.

The script block is used to define what action you want to perform in your GitLab CI/CD pipeline. In the script block you write the shell commands that GitLab Runner will execute to perform tasks like compiling code, running tests, deploying applications, or anything else you need to run.

The before_script block is used to execute some commands before the script block start execution (hence the name before_script). For example, you can setup the directory structure, download and install any missing packages that your script block will need.

The after_script block is used to execute some commands after the script block has finished execution (hence the name after_script). For example, you can run clean ups, commits, generate reports, upload artifacts etc. in the after_script block.

Below is a sample .gitla-ci.yml file with all the three blocks for your reference.

stages:
  - install

example:
  stage: install
  before_script:
    - echo "I got executed inside before_script"
  script:
    - echo "I got executed inside script"
  after_script:
    - echo "I got executed inside after_script"
GitLab Script
Sample output of script

The script Keyword: The Main Act

Every job in your .gitlab-ci.yml file must have a script section. This is a YAML sequence (list) of commands that are executed in the shell environment provided by your job’s Docker image (or runner’s environment).

Key characteristics of script:

  • Sequential Execution: Commands within the script block are executed sequentially, one after another.
  • Fail-Fast: If any command in the script returns a non-zero exit code (indicating failure), the job immediately fails, and the pipeline typically stops (unless allow_failure: true is set for the job).
  • Context: Commands run in the project’s root directory by default.

Example:

stages:
  - build
  - test

build_job:
  stage: build
  script:
    - echo "Starting the build process..."
    - npm install       # Install Node.js dependencies
    - npm run build     # Execute the build command
    - echo "Build finished successfully!"

test_job:
  stage: test
  script:
    - echo "Running unit tests..."
    - npm test          # Execute tests
    - echo "Tests passed!"

In build_job, npm install runs first, then npm run build. If npm install fails, npm run build will not execute, and the build_job will fail.

before_script: Setting the Stage

The before_script keyword allows you to define commands that run before the script of a job. This is incredibly useful for common setup tasks that apply to multiple jobs or a specific job.

Key characteristics of before_script:

  • Execution Order: Runs after fetching the repository and before the script block.
  • Scope: Can be defined globally (at the top level of .gitlab-ci.yml, applying to all jobs) or per-job. A job’s before_script overrides or extends the global before_script.
  • Fail-Fast: If a command in before_script fails, the job fails, and the script block will not execute.

When to use before_script:

  • Installing common dependencies: E.g., apt-get update && apt-get install -y <package>, bundle install, npm ci.
  • Logging in to external services: E.g., docker login.
  • Environment setup: Setting environment variables or configuring tools.

Example:

stages:
  - build
  - test

default:
  image: node:18 # Base image for all jobs
  before_script:
    - echo "Running global before_script..."
    - npm install --prefer-offline # Install dependencies for all jobs

build_job:
  stage: build
  script:
    - echo "Building application..."
    - npm run build

test_job:
  stage: test
  before_script: # This overrides the default before_script for test_job
    - echo "Running test_job specific before_script..."
    - npm ci # Use clean install for tests
  script:
    - echo "Running tests..."
    - npm test

In test_job, the npm ci command from its specific before_script will run instead of npm install --prefer-offline from the default before_script.

after_script: Cleaning Up or Reporting

The after_script keyword defines commands that run after the script of a job, regardless of whether the script succeeded or failed.

Key characteristics of after_script:

  • Execution Order: Runs after the script block has completed (either successfully or with a failure).
  • Scope: Can be defined globally or per-job. A job’s after_script overrides or extends the global after_script.
  • Independence from script success: Commands in after_script will always run, making it ideal for cleanup.
  • Failure: If a command in after_script fails, the job’s overall status will still be determined by the script‘s outcome. The after_script failure will be noted, but it would not retroactively fail a job that passed its script.

When to use after_script:

  • Cleanup: Removing temporary files, stopping services.
  • Reporting: Sending notifications, uploading logs, posting status updates to external systems.
  • Artifact archiving: If not handled by artifacts keyword directly.

Example:

stages:
  - build

build_and_cleanup:
  stage: build
  script:
    - echo "Building something..."
    - mkdir temp_output
    - echo "Build content" > temp_output/result.txt
  after_script:
    - echo "Cleaning up temporary files..."
    - rm -rf temp_output
    - echo "Reporting job status..."
    - curl -X POST -d "status=completed" https://example.com/api/report

Even if the script fails in build_and_cleanup, the rm -rf temp_output and curl commands will still execute.

Choosing the Right Script Block: before_script vs. script vs. after_script

Featurescriptbefore_scriptafter_script
PurposeCore job executionPre-execution setupPost-execution cleanup/reporting
Defined In.gitlab-ci.yml (Job-level).gitlab-ci.yml (Global or Job-level).gitlab-ci.yml (Global or Job-level)
Execution OrderAfter before_scriptBefore scriptAfter script
Failure BehaviorJob fails if command failsJob fails if command failsJob status based on script; after_script failure noted but does not change script outcome
Access to Project CodeYesYesYes
Use CasesBuild, test, deployInstall dependencies, loginCleanup, notifications, logs

FAQs – before_script, script, after_script


What is the before_script keyword in GitLab CI/CD?
The before_script keyword defines a list of shell commands that run before the main script of a job. It is often used to perform setup tasks such as installing dependencies, configuring environment variables, or preparing the build environment.


How do I use before_script in a GitLab job?
You can use before_script inside a job to run pre-commands before the job’s main script:

test-job:
  before_script:
    - echo "Setting up test environment"
    - npm install
  script:
    - npm test

In this example, the echo and npm install commands run before npm test.


Can I define a global before_script for all jobs?
Yes. You can define a global before_script at the top level of your .gitlab-ci.yml file. It applies to all jobs, unless a job overrides it:

before_script:
  - echo "Global setup step"
  - apt-get update

job1:
  script:
    - echo "Running job1"

job2:
  script:
    - echo "Running job2"

Both job1 and job2 will run the global before_script commands first.


How do I override the global before_script for a specific job?
To override the global before_script, you simply define a new one in your job. This replaces the global version:

before_script:
  - echo "Global setup"

custom-job:
  before_script:
    - echo "Custom setup for this job only"
  script:
    - echo "Running custom job"

In this case, custom-job only runs its own before_script.


Can I skip the global before_script for a job?
Yes. To skip the global before_script, define an empty list in your job:

no-setup-job:
  before_script: []
  script:
    - echo "Running without global before_script"

This completely disables the global before_script for no-setup-job.


What is the difference between before_script and script in GitLab CI/CD?

  • before_script: Runs before the main job logic. Commonly used for setup steps.
  • script: Contains the main commands that the job is supposed to execute.

The commands in before_script and script are both executed in the same shell session, so environment variables or changes made in before_script persist into script.


Can I use variables inside before_script?
Yes. You can use environment variables, including custom or predefined ones:

variables:
  SETUP_DIR: "/opt/app"

prepare:
  before_script:
    - echo "Setting up in $SETUP_DIR"
    - cd $SETUP_DIR
  script:
    - ls

This makes your configuration dynamic and reusable.


Can I define before_script using anchors for reuse?
Yes. You can define a before_script block as a YAML anchor and reuse it in multiple jobs:

.default-before: &setup_steps
  - echo "Shared setup"
  - setup.sh

job1:
  before_script: *setup_steps
  script:
    - echo "Job 1"

job2:
  before_script: *setup_steps
  script:
    - echo "Job 2"

This avoids repeating the same setup logic in every job.


Are before_script commands executed even if the job fails early?
Yes, before_script commands are always executed first, and if any of them fail, the job stops immediately and is marked as failed. They must all succeed for the main script to run.


Can I use before_script in combination with after_script?
Absolutely. Use before_script for pre-job setup and after_script for post-job cleanup:

build:
  before_script:
    - echo "Setup"
  script:
    - make build
  after_script:
    - echo "Cleanup"

This structure provides a clean separation between setup, execution, and teardown.


What is the script keyword in GitLab CI/CD?
The script keyword defines the core commands that a job will execute in GitLab CI/CD. It is a required keyword in every job and contains the actual tasks that GitLab Runner performs, such as compiling code, running tests, or deploying applications.


How do I use the script keyword in a job?
You use the script keyword by specifying a list of shell commands under any job:

build:
  script:
    - echo "Building the app..."
    - make build

The commands listed under script will be executed in sequence inside the GitLab Runner shell.


Is script mandatory for every job in GitLab CI/CD?
Yes. The script keyword is required in every job unless the job uses a predefined GitLab template that includes a script internally. Without script, the job will fail to run or will be ignored during pipeline execution.


Can I define multiple commands under the script keyword?
Yes. The script keyword accepts a list of shell commands that are run one after another:

test:
  script:
    - echo "Starting tests"
    - npm install
    - npm test

Each command runs in the same shell session, so variables and directory changes persist between commands.


What shell environment does the script run in?
By default, GitLab CI/CD jobs execute the script commands in a Bash shell, unless you explicitly define another shell in the GitLab Runner configuration. The shell is typically a Unix-like shell (or PowerShell on Windows runners).


Can I use inline shell operators like && or || in script?
Yes. You can combine commands on the same line using operators:

build:
  script:
    - make clean && make all

However, it is generally better to list each command on a separate line to improve readability and error tracking.


Can I use environment variables inside script?
Yes, both custom-defined and GitLab predefined environment variables can be used in script commands:

variables:
  BUILD_DIR: build

job:
  script:
    - mkdir -p $BUILD_DIR
    - cd $BUILD_DIR
    - echo "Build started in $BUILD_DIR"

GitLab provides many built-in variables like $CI_COMMIT_BRANCH, $CI_JOB_NAME, etc.


What happens if a command in the script fails?
If any command fails (returns a non-zero exit code), the job is marked as failed, and no subsequent commands in the script are executed unless you handle errors manually:

job:
  script:
    - some_command || echo "Handled failure"  # Command failure would not stop the job

You can also use set -e to explicitly stop execution on the first error (though it is on by default in most runners).


Can I use a shell script file instead of inline script commands?
Yes. You can execute a standalone shell script by calling it from the script section:

job:
  script:
    - ./scripts/deploy.sh

This is useful for complex logic that would clutter the .gitlab-ci.yml file.


Can I override the script keyword in extended jobs?
Yes. When using the extends keyword or YAML anchors, you can override or modify the script section:

.default-job: &defaults
  script:
    - echo "Default"

custom-job:
  <<: *defaults
  script:
    - echo "Custom script overrides default"

The custom-job will use its own script and not the one in defaults.


Is the script executed inside a Docker container if one is specified?
Yes. If your job specifies a image: (Docker container), then the script runs inside that container:

job:
  image: node:20
  script:
    - node --version

This allows you to run commands in specific runtime environments without installing dependencies globally on the runner.


Where can I see the output of the script execution in GitLab?
The output of the script section is visible in the Job Logs under the CI/CD → Pipelines section of your GitLab project. Each command’s output and errors are logged in real-time.


What is the after_script keyword in GitLab CI/CD?
The after_script keyword in GitLab CI/CD specifies commands that run after the main job script, regardless of whether the job succeeds or fails. It is commonly used for cleanup tasks, such as deleting temporary files, stopping services, or uploading logs.


How do I use the after_script keyword in a job?
You can define after_script inside a job to run cleanup or post-processing commands:

cleanup-job:
  script:
    - ./deploy.sh
  after_script:
    - echo "Cleaning up..."
    - rm -rf temp/

In this example, rm -rf temp/ will execute after the ./deploy.sh command, even if the deployment fails.


Can I define a global after_script for all jobs?
Yes. You can define after_script at the top level of the .gitlab-ci.yml file. It applies to every job, unless explicitly overridden by a job-level after_script.

after_script:
  - echo "Global cleanup"

job1:
  script:
    - echo "Doing work in job1"

job2:
  script:
    - echo "Doing work in job2"

Both job1 and job2 will run the global after_script at the end.


How do I override a global after_script for a specific job?
You can override a global after_script by specifying a new one inside a job:

after_script:
  - echo "Global after_script"

custom-job:
  after_script:
    - echo "Custom after_script for this job"
  script:
    - echo "Main job logic"

Here, custom-job will run only its own after_script and not the global one.


Can I completely disable the global after_script in a job?
Yes. You can skip the global after_script for a specific job by setting an empty array:

no-cleanup-job:
  after_script: []
  script:
    - echo "Run without global after_script"

This job will not execute the global after_script.


When does after_script run during job execution?
The order of job execution is as follows:

  1. before_script (if defined)
  2. script (main job logic)
  3. after_script (always runs, even if script fails)

This ensures that cleanup or reporting tasks are always executed.


Can I use environment variables in after_script?
Yes. You can use both predefined and custom environment variables:

variables:
  TEMP_DIR: temp_data

cleanup:
  script:
    - echo "Running job..."
  after_script:
    - echo "Removing $TEMP_DIR"
    - rm -rf $TEMP_DIR

This makes your post-job logic flexible and dynamic.


What happens if an after_script command fails?
The job is marked as passed or failed based on the outcome of the script, not the after_script. If an after_script command fails, the error is shown in the job log, but the job status will not change.


Can I use after_script to upload logs or reports?
Yes. A common use case is uploading test logs, coverage files, or build artifacts:

test:
  script:
    - npm test > output.log
  after_script:
    - curl --upload-file output.log https://logs.example.com/upload

You can also use it to notify external systems or clean up cloud infrastructure.


Is after_script suitable for deployment rollback or alerts?
Yes. You can use after_script to trigger rollback scripts or send notifications if the deployment job fails:

deploy:
  script:
    - ./deploy.sh || exit 1
  after_script:
    - if [ $? -ne 0 ]; then ./rollback.sh; fi

However, for complex rollback logic, it is better to separate it into a dedicated job using rules or when: on_failure.


Can I reuse after_script using YAML anchors in multiple jobs?
Yes. You can define a shared block using YAML anchors and reuse it:

.shared-cleanup: &clean_steps
  - echo "Shared cleanup"
  - rm -rf logs/

job1:
  script: echo "Job 1"
  after_script: *clean_steps

job2:
  script: echo "Job 2"
  after_script: *clean_steps

This makes your configuration cleaner and avoids duplication.


Author

Debjeet Bhowmik

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com

Leave a Comment