GitLab CI Local Execution and Self-Hosted Runner Orchestration

The process of developing and validating Continuous Integration and Continuous Deployment (CI/CD) pipelines typically involves a repetitive and time-consuming cycle of committing code, pushing to a remote repository, and waiting for a server to trigger a runner. This "push-and-pray" methodology often leads to cluttered commit histories filled with "test" or "fix yaml" messages. To mitigate this, the ecosystem has evolved to support local execution of .gitlab-ci.yml jobs, allowing developers to simulate the pipeline environment on their own workstations. By leveraging tools like gitlab-ci-local (also known as gcil), engineers can execute jobs within specific Docker images, utilizing inplace project volume mounts and adaptive user selections. This shift from remote dependency to local validation ensures that the .gitlab-ci.yml specification remains the unique interface between the local environment and the actual GitLab CI server, effectively eliminating the need for redundant Makefiles or separate shell scripts designed solely for local testing.

The Architectural Divide Between GitLab Server and GitLab Runner

In a self-hosted GitLab environment, such as one deployed via Docker on a local machine, there is a critical architectural distinction between the "brain" and the "hands" of the operation. The GitLab server acts as the brain; it stores the repository, parses the .gitlab-ci.yml file, and manages the pipeline logic. However, the server itself does not execute the code. For a pipeline to move from a "pending" status to "running," it requires a GitLab Runner.

A GitLab Runner is a separate application specifically designed to execute the jobs defined in the CI/CD configuration. While GitLab.com provides a shared fleet of managed runners, a private or self-hosted installation requires the manual deployment and registration of a runner. Without this component, the GitLab server has no mechanism to delegate tasks, leaving the pipeline stuck. The runner operates by polling the GitLab server, asking if there are any available jobs, and then executing those jobs based on the specified executor (such as Docker or Shell).

GitLab CI Local and GCIL Tooling

gitlab-ci-local, often invoked via the shortcut alias gcil, is a specialized tool designed to launch .gitlab-ci.yml jobs locally. This tool is engineered to wrap jobs inside specific images and provide a seamless local context for builds, tests, and releases. The primary objective of gcil is to enhance the reliability of the CI/CD process by providing an interactive and automated terminal tool, thereby avoiding the duplication of logic across various scripts.

Command Line Interface and Execution Modes

The gcil tool provides a variety of flags to control how jobs are selected and executed. These commands allow for granular control over the pipeline flow:

  • gcil: Launches an interactive menu for job selection.
  • gcil -p: Automatically launches the jobs as a pipeline.
  • gcil -l: Triggers the job selection interactive menu.
  • gcil 'Dev': Launches only those jobs whose names contain the specified string (e.g., "Dev").
  • gcil --debug 'Job 1': Holds a finishing specific job open for debugging purposes.
  • gcil --bash 'Job 1': Prepares a bash environment for a specific job, allowing for manual inspection.

Detailed Usage and Flag Specifications

The full usage of the gcil command encompasses a wide range of configuration options to mimic the remote environment.

usage: gcil [-h] [--version] [--no-color] [--update-check] [--settings] [--set GROUP KEY VAL] [-p] [-q] [-c CONFIGURATION] [-B] [-A] [-C COMMANDS] [-n NETWORK] [-e ENV] [-E ENGINE] [-H] [--notify] [--privileged [BOOL]] [--random-paths] [-r] [-S] [--ssh [SSH_USER]] [-v VOLUME] [-w WORKDIR] [--bash | --debug] [--display] [--shell SHELL] [--all] [--configure] [--input KEY=VAL] [-f] [-i] [-m] [--no-console] [--no-git-safeties] [--no-script-fail] [-R]

Managing Jobs and Pipeline Visibility

The gitlab-ci-local tool provides specific mechanisms to visualize and filter jobs defined in the configuration. By using the --list flag, the tool generates a formatted output of the available jobs.

gitlab-ci-local --list

This command returns a list of jobs including their description, stage, "when" condition, "allow_failure" status, and "needs" dependencies. By default, this list filters out any jobs set to when: never. To see all jobs, including those explicitly or implicitly set to when: never, the user can utilize the same command structure but with different internal logic or flags as specified in the tool's documentation.

Job Attribute Analysis

The output of the job list provides critical data regarding the pipeline structure:

Attribute Description Possible Values / Behavior
name The identifier of the job e.g., test-job, build-job
description Descriptive text added via @Description Always shown; empty if not set
stage The pipeline stage the job belongs to e.g., test, build, deploy
when The condition for job execution on_success, never
allow_failure Determines if the pipeline continues on job failure true, false, or specific exit codes like [42,137]
needs Defines dependencies for the job Omitted for stage ordering, [] for immediate start

Advanced Configuration and Variable Management

Handling secrets and environment variables is a primary challenge in local CI/CD simulation. gitlab-ci-local supports multiple ways to inject these variables into the executor.

Variable Files and Remote Integration

Users can specify a variables file using the --variables-file flag. By default, the tool looks for a file named .gitlab-ci-local-variables.yml in the current working directory ($CWD). If multiple variable files are required, the --remote-variables flag can be repeated.

Example of remote variable integration:
gitlab-ci-local --remote-variables [email protected]:firecow/example.git=gitlab-variables.yml=master

Variable Formats and Scoping

Variables can be defined in both YAML and plain text formats, each with different behaviors regarding file paths and environment scoping.

In YAML format, variables can be scoped to specific environments:

yaml EXAMPLE: values: "*": "I am only available in all jobs" staging: "I am only available in jobs with `environment: staging`" production: "I am only available in jobs with `environment: production`"

In plain text format, the mapping is direct:

text AUTHORIZATION_PASSWORD=djwqiod910321 DOCKER_LOGIN_PASSWORD=dij3213n123n12in3 KNOWN_HOSTS='~/.ssh/known_hosts'

A critical distinction exists for file-type variables. In YAML format, a value like ~/.ssh/known_hosts is treated as a file path. In plain text format, the value is treated as a literal string, which may differ from the expected behavior of the GitLab CI executor.

Custom Annotations and Behavioral Overrides

To enhance the local execution experience, gitlab-ci-local supports special annotations within the .gitlab-ci.yml file. These annotations allow developers to modify how the tool interacts with specific jobs.

  • @Description: Adds descriptive text that appears when running gitlab-ci-local --list.
  • @Interactive: Marks a job for interactive execution, often used with rules that check if $GITLAB_CI == 'false'.
  • @InjectSSHAgent: Instructs the tool to inject the SSH agent into the job container, facilitating secure access to remote resources.
  • @NoArtifactsToSource: Prevents the tool from copying artifacts back into the local source folder, which is useful for jobs that produce large amounts of data not needed on the host machine.

Example of an annotated job:

```yaml

@Description Install npm packages

npm-install:
image: node
artifacts:
paths:
- node_modules/
script:
- npm install --no-audit
```

Installation Procedures for Debian-Based Systems

For users on Debian-based distributions, there are two primary methods of installation. The preferred method is the Deb822 format, which provides a more modern approach to package management.

Preferred Deb822 Installation

The following commands set up the repository and install the tool:

sudo wget -O /etc/apt/sources.list.d/gitlab-ci-local.sources https://gitlab-ci-local-ppa.firecow.dk/gitlab-ci-local.sources
sudo apt-get update
sudo apt-get install gitlab-ci-local

Legacy Installation Method

If the distribution does not support the Deb822 format, the traditional GPG key and list file method can be used:

curl -s "https://gitlab-ci-local-ppa.firecow.dk/pubkey.gpg" | sudo apt-key add -
echo "deb https://gitlab-ci-local-ppa.firecow.dk ./" | sudo tee /etc/apt/sources.list.d/gitlab-ci-local.list

Setting Up a Self-Hosted GitLab Runner via Docker

When transitioning from local tool simulation (gcil) to a full self-hosted server environment, the installation of a Runner is mandatory. This process involves setting up the GitLab server first and then connecting the Runner.

Initial Project Setup

Before configuring the runner, a project must be established. This involves initializing a local repository and pushing it to the self-hosted 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

Once the project is pushed, a .gitlab-ci.yml file must be created in the root folder to define the pipeline. For example:

```yaml
stages:
- test

dummytestjob:
stage: test
script:
- echo "Running dummy test..."
- exit 0
```

The Role of the Runner in Localhost Environments

In a localhost Docker setup, the GitLab server is successfully installed, but the pipeline will remain "pending" because there is no available executor. The GitLab Runner must be installed and registered to the server. The Runner acts as the agent that polls the server for new jobs and executes the scripts defined in the YAML file. This creates a complete loop where the server manages the state and the runner performs the actual computation.

Comparative Analysis: gitlab-ci-local vs. gitlab-runner exec

A common point of confusion for developers is the choice between gitlab-ci-local (gcil) and the built-in gitlab-runner exec command. These two approaches have fundamentally different behaviors regarding state and artifacts.

Artifact and Cache Persistence

gitlab-runner exec is designed to run a single job locally. However, it suffers from a significant limitation: it does not natively support the passing of artifacts between jobs in a local sequence. If a "build" job produces an artifact and a subsequent "test" job depends on that artifact, running them sequentially via gitlab-runner exec will result in failure. This occurs because each exec call starts from a fresh local repository checkout, wiping the state between jobs.

In contrast, gitlab-ci-local is designed to provide a more holistic pipeline simulation. It handles volume mounts and project context more effectively, allowing for a more realistic reproduction of the pipeline flow without the "fresh checkout" issue that plagues the standard runner's exec command.

Debugging Strategies

When gitlab-ci-local fails to install on certain versions of Ubuntu (such as 20.04), or when developers encounter issues with gitlab-runner exec, the fallback strategy is often manual verification. This involves manually launching a Bash shell on the Docker image specified in the .gitlab-ci.yml file and stepping through the commands one by one. While this is tedious and lacks automation, it is the only way to guarantee a controlled environment when high-level automation tools fail.

Conclusion

The transition from remote CI/CD validation to local execution represents a significant increase in developer productivity. By utilizing gitlab-ci-local (gcil), developers can move away from the inefficient cycle of pushing commits just to test YAML syntax or script logic. The tool's ability to simulate Docker executors, manage environment variables through specialized files, and provide interactive debugging shells makes it an essential part of the modern DevOps toolkit.

However, the distinction between a local simulation tool and a full self-hosted runner remains critical. While gcil is ideal for rapid development and iterative testing of job scripts, the actual deployment of a self-hosted GitLab Runner is necessary for those building a full local mirror of the GitLab ecosystem. The architectural requirement of having a Runner to "poll" the server ensures that the separation of concerns between orchestration (the server) and execution (the runner) is maintained. Ultimately, the choice between these tools depends on whether the goal is rapid job-level debugging or full-scale pipeline orchestration on a private infrastructure.

Sources

  1. GitHub - firecow/gitlab-ci-local
  2. Dev.to - CICD on Local GitLab Server Setup
  3. PyPI - gitlabci-local
  4. GitLab Forum - Testing entire pipeline locally

Related Posts