GitLab CI/CD: Understanding before_script & after_script in Jobs
1. Overview and Core Premise
In GitLab CI/CD pipelines, jobs are the fundamental building blocks where execution happens. While the core script keyword handles the primary task execution, complex production workflows require cleaner organization, pre-checks, environment setup, and cleanup routines.
The before_script and after_script keywords allow developers to inject shell commands immediately before and after the main script block executes within the exact same shell environment. This chapter explores how to structure your pipeline jobs systematically using these execution hooks, improving code maintainability and setting the stage for advanced template reuse.
2. Why Do We Need Job Lifecycle Hooks?
When writing CI/CD configurations (.gitlab-ci.yml), developers often face scenarios where certain commands must always run prior to the main logic (such as dependency installations, environment validation, or authentication checks) or immediately following execution (such as cleanup, test report archiving, or notification logging).
The Naive Approach vs. The Structured Approach
- Naive Approach (Monolithic
scriptblock): Putting installation steps, pre-checks, main build commands, and cleanup routines into a single massivescriptlist. This makes the YAML file messy, hard to read, and difficult to maintain or refactor when templates are shared across multiple projects. - Better Approach (Lifecycle Hooks): Separating concerns by delegating environment setup to
before_script, core business logic toscript, and teardown/cleanup actions toafter_script.
3. Core Mental Model: The Job Execution Lifecycle
Think of a GitLab CI/CD job as a single continuous shell session spawned on a runner runner container or virtual machine. Within this single session, commands execute sequentially in three distinct phases:
Note: All three blocks (
before_script,script, andafter_script) share the same working directory and shell environment variables, but they execute in a strict, predictable sequence.
4. Syntax and Basic Configuration Structure
A job in GitLab CI/CD can incorporate any combination of before_script, script, and after_script. Below is a comprehensive example demonstrating how these components are declared within .gitlab-ci.yml:
stages:
- build
- test
- package
compile_job:
stage: build
before_script:
- echo "Running pre-check: Verifying environment variables and tooling..."
- node -v
script:
- echo "Executing main compilation script..."
- npm install
- npm run build
after_script:
- echo "Job completed. Cleaning up temporary cache files..."
5. Flexible Job Structures: Partial Hooks
You are not required to use all three components in every job. Depending on the specific job requirements, you can mix and match hooks:
- Jobs with
before_script+script: Ideal for setup-heavy tasks (like installing tools or pulling credentials) followed by execution, without needing post-processing. - Jobs with
script+after_script: Ideal for build/test jobs that must archive artifacts or clean up directories regardless of whether the main script succeeds or fails.
Code Demonstration: Selective Hook Usage
test_job_setup_only:
stage: test
before_script:
- echo "Running environment setup..."
- whoami
script:
- echo "Running main test suite..."
- npm test
package_job_cleanup_only:
stage: package
script:
- echo "Packaging application artifacts..."
- tar -czf release.tar.gz ./dist
after_script:
- echo "Printing working directory state post-packaging..."
- pwd
6. Execution Walkthrough: Real Pipeline Logs
When you commit these changes and trigger a pipeline (git push to trigger GitLab CI/CD), the runner processes the steps sequentially. Observing the raw job logs reveals the clear boundaries between execution phases:
Pipeline Execution Flow Analysis
-
compile_jobExecution:- Before Script Output: Displays the custom message
My command which runs before scriptalong with tool versions. - Main Script Output: Executes core compilation commands (e.g.,
npm run build). - After Script Output: Prints
Running after scriptand finishes execution.
- Before Script Output: Displays the custom message
-
test_job_setup_onlyExecution:- Before Script Output: Executes
whoamiwhich printsroot(showing the runner execution context). - Main Script Output: Runs the test commands.
- Before Script Output: Executes
-
package_job_cleanup_onlyExecution:- Main Script Output: Bundles files into an archive.
- After Script Output: Executes
pwd(Present Working Directory) to verify the build location before job termination.
7. Why This Matters for Advanced Pipelines
While using before_script and after_script looks straightforward in simple pipelines, their true power emerges in enterprise-grade workflows:
- Template Inheritance & Reuse: When incorporating pre-built CI/CD templates (e.g., standard security scanning or Docker build templates provided by GitLab or internal platform teams), you often cannot (or should not) modify the core
scriptsection. - Layered Customization: By leveraging
before_scriptandafter_script, you can inject custom pre-processing or post-processing layers on top of standard, pre-existing pipeline templates without breaking their underlying logic.
8. Summary and Key Takeaways
- Lifecycle Ordering: Commands always execute in the strict sequence of
before_scriptscriptafter_script. - Modular Code Organization: Breaking monolithic scripts into lifecycle hooks separates setup, execution, and cleanup, making pipelines cleaner and easier to read.
- Shared Environment: All three blocks operate within the same shell session and share environment variables and working directories.
- Optional Utilization: Jobs can use
before_scriptorafter_scriptindependently depending on whether pre-checks or teardowns are required. - Production Readability: Using hooks prevents YAML files from becoming bloated and unmaintainable as pipelines grow.
- Advanced Extensibility:
before_scriptandafter_scriptplay a crucial role when layering custom behavior on top of reusable enterprise pipeline templates.