The orchestration of continuous integration and continuous delivery (CI/CD) typically relies on remote runners and cloud-based orchestrators. However, the necessity for rapid iteration, cost reduction, and the elimination of "commit-and-pray" workflows has led to the rise of local GitLab CI execution. By shifting the pipeline execution from a remote GitLab Runner to a local environment, developers can validate their .gitlab-ci.yml configurations, test complex script logic, and ensure environment parity without consuming remote runner minutes or polluting the project's history with "test" commits. This process involves a combination of local emulation tools, such as gitlab-ci-local, and the manual configuration of self-hosted runners for those operating a full local GitLab instance.
The fundamental goal of local CI is feature parity. The objective is to maintain a state where the .gitlab-ci.yml file requires minimal to no modification to function both on a production GitLab Runner and on a developer's local machine. This parity is achieved by simulating the containerized environment—typically via Docker—and providing the necessary environment variables that the GitLab server would otherwise inject into the runtime.
The Architecture of GitLab CI Local
The tool gitlab-ci-local serves as a bridge between the developer's local file system and the GitLab CI specification. Instead of pushing code to a remote repository to trigger a pipeline, the tool parses the YAML configuration and executes the defined jobs within local Docker containers.
Command Line Interface and Job Discovery
The tool provides specific flags to allow developers to inspect their pipeline structure before execution. This is critical for complex pipelines where jobs may be conditional or dependent on specific triggers.
The command gitlab-ci-local --list is used to return a formatted output of available jobs. A key functional aspect of this command is its filtering capability: it automatically filters out all jobs that are explicitly set to when: never. This prevents the developer from attempting to run jobs that are intended to be dormant or manually triggered under specific conditions.
When utilizing the list command, the output provides a detailed matrix of the pipeline:
| Column | Description |
|---|---|
| name | The unique identifier of the job |
| description | A human-readable explanation (empty if not specified) |
| stage | The pipeline stage the job belongs to |
| when | The trigger condition (e.g., on_success) |
| allow_failure | Boolean or specific exit codes allowed to fail |
| needs | Dependencies required before the job can start |
Advanced Job Logic and Dependencies
The allow_failure attribute is particularly nuanced in local execution. It can be set to true or false, but it also supports an array of specific exit codes, such as [42, 137]. This allows the pipeline to continue even if a job fails with a known, non-critical error code.
The needs attribute dictates the execution order. If needs is omitted, the job strictly follows the sequential order of the defined stages. If it is explicitly set to [] (an empty array), the job is treated as having no dependencies and can start immediately, regardless of the stage sequence.
Installation Vectors Across Platforms
gitlab-ci-local is designed for cross-platform compatibility, ensuring that developers on Linux, macOS, and Windows can maintain a consistent workflow.
Linux Distribution Setup
For Ubuntu users, specifically those using Ubuntu Focal, the installation requires the addition of a specific PPA to ensure the binary is authenticated and updated.
The installation sequence involves:
1. Defining the key path: PPA_KEY_PATH=/etc/apt/sources.list.d/gitlab-ci-local-ppa.asc
2. Fetching the GPG key: curl -s "https://gitlab-ci-local-ppa.firecow.dk/pubkey.gpg" | sudo tee "${PPA_KEY_PATH}"
3. Adding the repository: echo "deb [ signed-by=${PPA_KEY_PATH} ] https://gitlab-ci-local-ppa.firecow.dk ./" | sudo tee /etc/apt/sources.list.d/gitlab-ci-local.list
4. Updating and installing: sudo apt-get update followed by sudo apt-get install gitlab-ci-local
It is critical to note that the path /etc/apt/sources.list.d/gitlab-ci-local.list is used within the list file itself. Any modification to this path during the tee command must be mirrored within the content of the file to avoid repository synchronization errors.
For Arch Linux users, the tool is available via the AUR (Arch User Repository), allowing installation through helpers such as paru:
paru -S gitlab-ci-local
macOS and Windows Deployment
On macOS, the tool is distributed via Homebrew:
brew install gitlab-ci-local
For Windows environments, the binary must be placed within the Git Bash ecosystem to operate correctly. The recommended path is C:\Program Files\Git\mingw64\bin. This can be achieved via a one-line curl and unzip command:
curl -L https://github.com/firecow/gitlab-ci-local/releases/latest/download/gitlab-ci-local-windows-amd64.zip -o gcl.zip && unzip -o gcl.zip -d /c/Program\ Files/Git/mingw64/bin && rm gcl.zip
A specific requirement for Windows users is the use of the --variable MSYS_NO_PATHCONV=1 flag. This prevents MSYS from converting Unix-style paths to Windows paths, which often breaks the mapping between the host machine and the Docker container.
Alternative Package Managers
The tool is also available via Node-based managers:
- NPM: npm install -g gitlab-ci-local
- Bun: bun install -g gitlab-ci-local
For these installations, it is mandatory that the system bash version is 4.x.x or higher to ensure script compatibility.
Configuration and Variable Management
Local CI execution lacks the built-in "CI/CD Variables" UI found in the GitLab web interface. To emulate this, gitlab-ci-local employs a tiered variable system.
Variable Definition Files
To handle variables that would normally be secret or environment-specific, developers can use two primary methods:
Local Project Variables: A file named
.gitlab-ci-local-variables.ymlplaced in the root of the project. For example, if a job requires a variableSOME_TEXT, the file would contain:
SOME_TEXT: "hello from the other side!"Global User Variables: A file located at
~/.gitlab-ci-local/variables.yml. This is used for variables common across all projects. If the user is running Docker as root, this file must be placed in/root/.gitlab-ci-local/variables.yml.
A common example of a global variable is the default branch definition:
global:
CI_DEFAULT_BRANCH: "master"
Environment and CLI Defaults
Developers can avoid repetitive typing by setting environment variables that act as defaults for CLI flags. These can be placed in .gitlab-ci-local-env (current directory) or $HOME/.gitlab-ci-local/.env.
The following mappings apply:
- GCL_NEEDS=true replaces the --needs flag.
- GCL_FILE='.gitlab-ci-local.yml' replaces the --file flag.
- GCL_TIMESTAMPS=true enables timestamps in the logs.
- GCL_MAX_JOB_NAME_PADDING=30 limits the padding around job names for better readability.
- GCL_QUIET=true suppresses all job output.
To optimize the workflow further, users are encouraged to add an alias and completion script to their .bashrc:
echo "alias gcl='gitlab-ci-local'" >> ~/.bashrc
gitlab-ci-local --completion >> ~/.bashrc
Local GitLab Server and Self-Hosted Runners
While gitlab-ci-local emulates the runner, some users run a full GitLab instance on localhost using Docker. In this scenario, the GitLab server (the "brain") is present, but the pipeline remains in a "pending" state because there are no "hands" (Runners) to execute the code.
Setting Up a Local Repository
To test a self-hosted CI/CD pipeline, a repository must first be initialized and pushed to the local server:
cd project_folder
git init --initial-branch=main
git remote add origin http://localhost:8080/root/ci-test.git
git add .
git commit -m "add: test CI"
git push --set-upstream origin main
The Pipeline Configuration
A basic .gitlab-ci.yml file is required to trigger the runner. A minimal configuration consists of a stage definition and a job script:
stages:
- test
dummy_test_job:
stage: test
script:
- echo "Running dummy test..."
- exit 0
Without a registered GitLab Runner, this pipeline will stay pending. The user must install and register a GitLab Runner on the host machine or as a separate Docker container and link it to the local GitLab instance using the registration token provided in the project settings.
Case Study: The ns-3 Project Integration
The ns-3 project provides a practical example of high-scale local CI usage. Their repository is hosted in GitLab and utilizes CI to automate building, testing, packaging, and distribution.
CI File Structure in ns-3
The ns-3 project organizes its CI logic within the ns-3-dev/utils/tests/ directory. The primary configuration is handled by the gitlab-ci.yml file. This specific implementation uses CI to verify compatibility across a wide array of compilers and package versions.
The workflow typically follows a pattern where a "build" job is immediately followed by a "test" run. This ensures that any incompatibility in the compiler toolchain is caught before the software is packaged. Additionally, specific jobs are dedicated to generating and validating documentation, issuing warnings if the documentation build fails.
Pipeline Batching and Parallelism
The ns-3 project utilizes pipelines composed of a sequence of job batches. In this architecture, jobs within the same batch are executed in parallel. This significantly reduces the total time required to validate the software across multiple environments, as the "build" phase for different compilers can occur simultaneously.
Detailed Execution Flow and Troubleshooting
When executing a job locally using gitlab-ci-local, the tool performs the following sequence:
1. It reads the .gitlab-ci.yml file to identify the image (e.g., image: debian:latest).
2. It pulls the specified Docker image.
3. It injects variables from .gitlab-ci-local-variables.yml and the system environment.
4. It executes the script block within the container.
5. It handles the exit code to determine if the job passed or failed based on the allow_failure settings.
For a simple "Hello World" test, a configuration using image: debian:latest and a script echo "Hello, world" can be executed directly via:
gitlab-ci-local some-job
This allows for immediate feedback on whether the container image is correct and the script is compatible with the shell provided by the image.
Conclusion
The transition from remote to local GitLab CI execution represents a significant shift in developer productivity. By utilizing gitlab-ci-local, developers can transform their local machine into a pre-production validation environment, effectively eliminating the latency associated with pushing code to a remote server just to test a YAML syntax change or a script logic error.
The integration of variable files like .gitlab-ci-local-variables.yml allows for a sophisticated emulation of the GitLab environment, while the ability to filter jobs via --list and manage dependencies through the needs attribute ensures that complex, multi-stage pipelines can be debugged with precision. Whether it is used for small-scale personal projects or large-scale academic projects like ns-3, local CI provides the essential infrastructure for ensuring that code is "green" before it ever touches the central repository. The synergy between Docker, local binaries, and structured YAML configuration creates a robust ecosystem that bridges the gap between local development and remote deployment.