What are GitLab CI/CD jobs and how do you define them in .gitlab-ci.yml?
Quick Answer
Jobs are the fundamental building blocks of a GitLab pipeline. Each job is an independent unit of work that runs a series of shell commands (scripts) on a GitLab Runner. Jobs are defined as top-level keys in .gitlab-ci.yml with properties like stage, image, script, artifacts, and rules that control their behavior.
Detailed Answer
Think of jobs like individual tasks on a project manager's task board. Each task has a clear name (build-frontend, run-unit-tests), is assigned to a specific phase of the project (stage), has step-by-step instructions (script), and might produce deliverables (artifacts). A team member (GitLab Runner) picks up a task, follows the instructions, and reports back whether it passed or failed. Multiple team members can work on different tasks simultaneously if they are in the same project phase.
A job in GitLab CI/CD is defined as a top-level YAML key in .gitlab-ci.yml. Any top-level key that is not a reserved keyword (like stages, variables, include, workflow, or default) is treated as a job definition. Each job must contain at least a script keyword, which is a list of shell commands to execute. Beyond the script, jobs support many configuration options: stage assigns the job to a pipeline phase, image specifies the Docker image to run the job in, tags select which Runners can execute the job, variables set environment variables, before_script and after_script run commands before and after the main script, artifacts define files to preserve after the job finishes, cache specifies directories to cache between pipeline runs for faster execution, and rules or only/except control when the job should be included in the pipeline.
When a Runner picks up a job, it executes a well-defined sequence. First, it prepares the execution environment by pulling the specified Docker image or setting up the shell. Then it clones the repository at the exact commit SHA that triggered the pipeline. It restores any cached files from previous runs. It runs the before_script commands, which are commonly used for setup tasks like installing dependencies or logging into registries. Then it executes the main script commands sequentially; if any command returns a non-zero exit code, the job fails immediately. After the main script (regardless of success or failure), it runs after_script commands, which are useful for cleanup tasks. Finally, it uploads any files matching the artifacts paths to the GitLab server and reports the job status. The entire execution log is streamed to the GitLab web interface in real time, where developers can watch the output and debug failures.
In production, teams design jobs to be focused and single-purpose. Rather than one monolithic test job, a mature pipeline for an application like notification-service might have separate jobs: lint-python for code style, type-check for mypy validation, unit-tests with pytest, integration-tests against a database service, build-docker to create the container image, and deploy-k8s to roll it out. Each job runs in its own isolated container, ensuring clean environments and reproducible results. Jobs use the services keyword to spin up dependent containers like PostgreSQL or Redis during testing. The allow_failure keyword lets non-critical jobs (like experimental linters) fail without blocking the pipeline. The retry keyword automatically retries jobs that fail due to transient issues like network timeouts.
A common beginner gotcha is using reserved keywords as job names. Names like image, services, stages, variables, cache, before_script, and after_script are reserved by GitLab and cannot be used as job names. If you accidentally use one, GitLab will either throw a parsing error or silently misinterpret your configuration. Another mistake is forgetting that each job runs in a fresh environment; files created in one job are not available in another unless you explicitly pass them using artifacts or the cache. This isolation is a feature, not a bug, but it surprises developers who expect state to persist between jobs.