Maven / GitLab CI Basics Interview Questions
1. What is GitLab CI/CD and what problem does it solve?
GitLab CI/CD is a built-in automation system within GitLab that automatically builds, tests, and deploys code every time a developer pushes changes to a repository. It eliminates the need for separate CI tools like Jenkins by providing pipelines as a native GitLab feature.
The problems it solves:
- Manual errors - human steps in build/deploy processes are replaced with scripted automation
- Slow feedback - developers learn about broken code minutes after pushing, not hours later
- Integration debt - frequent small merges catch conflicts early instead of painful big-bang integrations
- Inconsistency - every pipeline run uses the same defined environment and steps
| Term | Full name | What it does |
|---|---|---|
| CI | Continuous Integration | Auto-build and test every code change |
| CD | Continuous Delivery | Auto-prepare release-ready artifacts after CI passes |
| CD | Continuous Deployment | Auto-deploy all the way to production after CI passes |
Everything is configured in a single YAML file (.gitlab-ci.yml) committed alongside the code, making pipeline configuration version-controlled and auditable.
2. What is the.gitlab-ci.yml file and where must it be placed?
The .gitlab-ci.yml file is the pipeline configuration file for GitLab CI/CD. It uses YAML syntax to define stages, jobs, scripts, rules, and all other pipeline behaviour. GitLab automatically detects and runs the pipeline whenever this file is present and a triggering event occurs.
Key facts:
- Must be placed at the root of the repository by default
- The filename is case-sensitive - it must be exactly
.gitlab-ci.yml - You can configure a different path/filename in Settings > CI/CD > General pipelines > CI/CD configuration file
- It is a regular file tracked by Git, so all changes are version-controlled and can be reviewed in merge requests
# Minimal .gitlab-ci.yml stages: - build - test - deploy build-job: stage: build script: - echo "Compiling the code..." - echo "Compile complete." test-job: stage: test script: - echo "Running tests..." deploy-job: stage: deploy script: - echo "Deploying to production..."
The CI Lint tool (available in the GitLab UI under CI/CD > Pipelines > CI Lint) validates your .gitlab-ci.yml syntax before you commit, helping you catch configuration errors early.
3. What is a GitLab CI/CD pipeline?
A pipeline is the top-level component of GitLab CI/CD. It is a complete automated workflow that runs when triggered by an event (such as a git push). A pipeline consists of one or more stages, each containing one or more jobs.
| Level | Component | Description |
|---|---|---|
| 1 (top) | Pipeline | The complete automated workflow triggered by an event |
| 2 | Stage | A group of jobs that run in parallel; stages run sequentially |
| 3 (bottom) | Job | The individual unit of work - runs scripts on a runner |
Pipeline lifecycle:
- A pipeline is created when a triggering event occurs (push, merge request, schedule, API call, etc.)
- Jobs are assigned to available runners which execute them
- The pipeline passes if all jobs succeed, fails if any job fails
- Results and logs are visible under Build > Pipelines in the GitLab UI
# A pipeline with 3 stages: # Stage 1 (build): runs first # Stage 2 (test): runs after build passes # Stage 3 (deploy): runs after test passes stages: - build - test - deploy build-app: stage: build script: make build unit-tests: stage: test script: make test deploy-staging: stage: deploy script: make deploy
4. What are stages in GitLab CI/CD and how do you define them?
Stages control the order of execution in a pipeline. Jobs within the same stage run in parallel; stages themselves run sequentially. If any job in a stage fails, subsequent stages are not run (unless configured otherwise).
# Define stages at the top level of .gitlab-ci.yml stages: - build - test - deploy # Jobs reference their stage with the "stage" keyword build-job: stage: build script: - echo "Building..." test-unit: stage: test script: - echo "Running unit tests..." test-integration: stage: test # also in "test" stage - runs PARALLEL to test-unit script: - echo "Running integration tests..." deploy-job: stage: deploy script: - echo "Deploying..."
Default stages: GitLab provides default stages if you do not define a stages block. The built-in defaults are: .pre, build, test, deploy, .post. The special .pre stage always runs first (before any other stages) and .post always runs last.
| Stage | Behaviour |
|---|---|
| .pre | Always runs before all other stages - regardless of stages list order |
| .post | Always runs after all other stages |
5. What is a CI/CD job in GitLab and what are the required keywords?
A job is the fundamental unit of work in a GitLab pipeline. It defines a set of shell commands to run, where to run them, and under what conditions. Jobs run independently on runners and produce a job log accessible in the GitLab UI.
# Minimum viable job (only "script" is required): hello-world: script: - echo "Hello, World!" # Full job with common keywords: build-app: stage: build image: node:20-alpine before_script: - npm ci script: - npm run build after_script: - echo "Build complete" artifacts: paths: - dist/ rules: - if: $CI_COMMIT_BRANCH == "main"
| Keyword | Purpose | Required? |
|---|---|---|
| script | Commands to execute - the job's main work | Yes |
| stage | Which stage this job belongs to | No (defaults to 'test') |
| image | Docker image to use as the job environment | No |
| before_script | Commands that run before script (setup) | No |
| after_script | Commands that run after script (cleanup, always runs) | No |
| artifacts | Files/dirs to save and pass to later jobs | No |
| rules | Conditions that determine when the job runs | No |
| tags | Specify which runner to use by tag | No |
Jobs must be defined at the top level of the YAML file. Any YAML key not matching a reserved keyword is treated as a job name. Job names starting with a dot (e.g. .setup) are hidden jobs - they don't run but can be used as templates.
6. What is a GitLab Runner and what types are available?
A GitLab Runner is an agent (a program) that picks up CI/CD jobs from GitLab and executes them. Runners are separate from the GitLab server - they can run on any machine including physical servers, VMs, containers, or cloud instances.
| Type | Scope | Configured at |
|---|---|---|
| Instance runners (shared) | Available to all projects on a GitLab instance | Admin area (self-managed) or GitLab.com |
| Group runners | Available to all projects in a group and its subgroups | Group > Settings > CI/CD > Runners |
| Project runners (specific) | Available to one specific project | Project > Settings > CI/CD > Runners |
GitLab.com users: GitLab.com provides instance runners that you can use immediately without any setup. For self-managed GitLab, administrators must install and register runners separately.
Runner executors: how the runner actually runs jobs depends on its executor type:
| Executor | Runs jobs in | Use case |
|---|---|---|
| Shell | Direct shell on the runner machine | Simple setups; persistent environment |
| Docker | A fresh Docker container per job | Isolated, reproducible environments |
| Docker+Machine | Auto-scaled Docker VMs | High concurrency, auto-scaling |
| Kubernetes | A Kubernetes pod per job | Cloud-native, auto-scaling |
| Virtual Machine | A VM snapshot | Full VM isolation |
You specify which runner to use by adding tags to a job. The runner must have the same tag registered to pick up that job.
7. What is the 'script', 'before_script', and 'after_script' keywords and how do they differ?
These three keywords define the shell commands that run during a job. They execute in sequence but have different scopes and failure behaviour.
| Keyword | When it runs | Fails the job if it fails? | Scope |
|---|---|---|---|
| before_script | Before the main script | Yes | Can be set globally or per-job |
| script | The job's main work | Yes | Per-job only (required) |
| after_script | After script (and before_script) | No (always runs) | Can be set globally or per-job |
# Global before_script runs before every job's script default: before_script: - echo "Global setup" build-job: stage: build before_script: - echo "Job-specific setup (overrides global)" # overrides the default script: - npm ci - npm run build after_script: - echo "Cleanup - always runs even if script fails"
Key behaviours:
before_scriptdefined in a job overrides (not appends to) the globalbefore_scriptafter_scriptruns in a separate shell context fromscript- environment variable changes made inscriptare not visible inafter_scriptafter_scriptruns even when the job fails or times out, making it ideal for cleanup tasks
8. How do you use the 'image' keyword to specify a Docker environment for a job?
The image keyword specifies the Docker image that the runner uses as the execution environment for the job. This ensures each job runs in a consistent, isolated environment with all required tools pre-installed.
# Set a default image for all jobs default: image: ubuntu:24.04 # Override image for a specific job build-node: stage: build image: node:20-alpine # uses Node.js 20 script: - node --version - npm ci - npm run build build-python: stage: build image: python:3.12-slim # different image for Python job script: - pip install -r requirements.txt - python -m pytest # Image with a registry prefix deploy-job: image: registry.gitlab.com/myorg/myimage:latest script: - ./deploy.sh # Image with a specific SHA (for reproducibility) test-job: image: name: node:20-alpine entrypoint: [""] # override the entrypoint if needed script: - npm test
Without a Docker executor: the image keyword is only used by runners with a Docker, Kubernetes, or similar container-based executor. Runners using the Shell executor ignore the image keyword and run jobs directly on the host machine.
Default image: if you don't specify an image, GitLab.com runners use a default image (often a Ruby image). Always specify an explicit image for consistent, predictable environments.
9. What are GitLab CI/CD artifacts and how do you use them?
Artifacts are files and directories that a job produces and saves after it finishes. They serve two purposes: downloading results from the GitLab UI, and passing files between jobs in subsequent stages.
build-job: stage: build script: - npm ci - npm run build # creates dist/ directory artifacts: paths: - dist/ # save entire dist directory - coverage/report.html # save a specific file exclude: - dist/**/*.map # exclude sourcemap files expire_in: 1 week # auto-delete after 1 week (default: 30 days) when: always # save artifacts even if job fails test-job: stage: test script: - ls dist/ # artifacts from build-job are available here! - npm test dependencies: - build-job # explicitly specify which artifacts to download # JUnit test reports (display inline in MR UI) test-with-report: stage: test script: - pytest --junitxml=report.xml artifacts: reports: junit: report.xml
| when value | Behaviour |
|---|---|
| on_success (default) | Save artifacts only when job succeeds |
| on_failure | Save artifacts only when job fails (useful for debug files) |
| always | Save artifacts regardless of job result |
Automatic download: by default, jobs automatically download artifacts from all jobs in preceding stages. Use dependencies: [] to opt out, or list specific jobs to download from.
10. What is the difference between cache and artifacts in GitLab CI?
Cache and artifacts are both mechanisms for persisting files across jobs, but they serve very different purposes and have different behaviours.
| Aspect | Cache | Artifacts |
|---|---|---|
| Purpose | Speed up jobs by reusing downloaded dependencies | Pass build outputs between stages/jobs |
| Storage | Runner's local disk or shared S3/GCS | GitLab server |
| Scope | Same runner, same branch (by cache key) | Any runner, same pipeline |
| Typical content | node_modules/, .m2/, pip cache | dist/, binaries, test reports |
| Reliability | Not guaranteed (may not exist) | Guaranteed (always transferred) |
| Downloaded by | Only the same runner (by default) | All jobs in later stages |
| Expiry | Configurable; can be cleared manually | expire_in (default: 30 days) |
# Cache example: reuse npm packages across pipeline runs cache: key: "$CI_COMMIT_REF_SLUG" # different cache per branch paths: - node_modules/ policy: pull-push # default: download at start, upload at end # Artifacts example: pass build output to next stage build: script: npm run build artifacts: paths: - dist/ # Best practice: use both together build-job: cache: paths: - node_modules/ # cache deps for speed script: - npm ci - npm run build artifacts: paths: - dist/ # pass output to deploy stage
Key insight: cache is not reliable for passing required build outputs between jobs - it is a performance optimisation only. For guaranteed file transfer between jobs, always use artifacts.
11. What are GitLab CI/CD variables and what types are available?
CI/CD variables are key-value pairs that make values available to jobs. They are used to configure jobs without hardcoding values, store secrets safely, and pass information between pipeline components.
| Type | Where defined | Use case |
|---|---|---|
| Predefined variables | Set automatically by GitLab | Pipeline metadata: $CI_COMMIT_SHA, $CI_COMMIT_BRANCH |
| Project variables | Settings > CI/CD > Variables | Project-specific secrets: API keys, passwords |
| Group variables | Group > Settings > CI/CD > Variables | Shared secrets across projects in a group |
| Instance variables | Admin area > CI/CD > Variables | Shared across the entire GitLab instance |
| .gitlab-ci.yml variables | variables: keyword in YAML | Non-secret config values |
# Defining variables in .gitlab-ci.yml variables: DATABASE_URL: "postgres://localhost/mydb" # available to all jobs DEPLOY_ENV: "staging" build-job: variables: NODE_ENV: "production" # job-level variable (overrides global) script: - echo "Branch: $CI_COMMIT_BRANCH" - echo "Commit: $CI_COMMIT_SHA" - echo "DB: $DATABASE_URL" - echo "Node env: $NODE_ENV"
Security settings for project/group variables:
- Masked - value is hidden in job logs (must be a single line, no spaces)
- Protected - variable only available to jobs running on protected branches or tags
- Expanded - controls whether
$VARreferences within the value are expanded
12. What are the most important predefined CI/CD variables in GitLab?
GitLab automatically sets dozens of predefined variables for every pipeline run. These provide jobs with information about the current commit, branch, pipeline, runner, and environment without any configuration.
| Variable | Value | Common use |
|---|---|---|
| $CI_COMMIT_SHA | Full commit SHA (40 chars) | Tagging Docker images |
| $CI_COMMIT_SHORT_SHA | First 8 chars of SHA | Short image tags, logs |
| $CI_COMMIT_BRANCH | Branch name | Conditional logic (if main...) |
| $CI_COMMIT_REF_NAME | Branch or tag name | Generic ref reference |
| $CI_COMMIT_REF_SLUG | Branch name, URL-safe (/ replaced with -) | Docker tags, URLs |
| $CI_PIPELINE_ID | Unique pipeline ID | Linking artifacts to pipelines |
| $CI_JOB_ID | Unique job ID | Unique artifact names |
| $CI_PROJECT_NAME | Repository name | Build labels |
| $CI_PROJECT_PATH | namespace/project | Registry image paths |
| $CI_REGISTRY | GitLab Container Registry URL | Docker login |
| $CI_REGISTRY_IMAGE | Full image path in registry | docker build/push |
| $GITLAB_USER_LOGIN | Username of who triggered pipeline | Audit logs |
| $CI_PIPELINE_SOURCE | What triggered the pipeline | Conditional job rules |
deploy: script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA - echo "Deployed by $GITLAB_USER_LOGIN from $CI_COMMIT_BRANCH"
13. What are 'rules' in GitLab CI/CD and how do they control when jobs run?
The rules keyword lets you add conditions that determine whether a job is included in a pipeline, skipped, or made manual. Rules are evaluated in order - the first matching rule applies and the rest are ignored.
deploy-prod: stage: deploy script: - ./deploy.sh production rules: - if: $CI_COMMIT_BRANCH == "main" # only run on main branch when: on_success # (default) run if prior stages passed - if: $CI_PIPELINE_SOURCE == "schedule" when: on_success - when: never # never run for any other condition # Common rules patterns: job-on-mr: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" job-on-tag: rules: - if: $CI_COMMIT_TAG # runs when a git tag is pushed job-manual-on-prod: rules: - if: $CI_COMMIT_BRANCH == "main" when: manual # must be manually triggered in UI job-with-changes: rules: - changes: # only run if these files changed - src/**/* - package.json job-allow-fail: rules: - if: $CI_COMMIT_BRANCH == "main" allow_failure: true # pipeline passes even if this fails
| Value | Behaviour |
|---|---|
| on_success (default) | Run if all prior jobs in pipeline passed |
| on_failure | Run only if a prior job failed |
| always | Run regardless of prior job status |
| never | Never add this job to the pipeline |
| manual | Add job but require manual trigger to start |
| delayed | Run after a specified delay |
14. What is the difference between 'only/except' and 'rules' in GitLab CI?
only and except are the original (legacy) keywords for controlling when jobs run. rules is the modern replacement introduced in GitLab 12.3 that provides more flexibility and clearer logic.
| Aspect | only/except | rules |
|---|---|---|
| Introduction | GitLab initial release | GitLab 12.3 |
| Status | Still supported; not deprecated but not recommended for new pipelines | Recommended for new configurations |
| Conflict | Can be used together (but only/except wins if both present on same job) | Cannot be combined with only/except on the same job |
| Logic | Separate allow/deny lists | Sequential rule evaluation - first match wins |
| Flexibility | Limited - only ref, variables, changes, kubernetes | Full expressions, allow_failure, when, start_in |
# Legacy only/except (works but not recommended for new pipelines): deploy-old: script: ./deploy.sh only: - main - tags except: - schedules # Modern rules equivalent (preferred): deploy-new: script: ./deploy.sh rules: - if: $CI_COMMIT_TAG # matches any tag push - if: $CI_COMMIT_BRANCH == "main" when: on_success - when: never
Recommendation: use rules for all new pipeline configurations. It supports complex conditional logic with if, changes, exists, and when in a single keyword, replacing the fragmented only/except approach.
15. What is the 'needs' keyword in GitLab CI/CD and what problem does it solve?
The needs keyword creates a directed acyclic graph (DAG) of job dependencies, allowing jobs to start as soon as their specific dependencies are complete - rather than waiting for the entire previous stage to finish. This reduces total pipeline time significantly.
stages: - build - test - deploy build-frontend: stage: build script: npm run build artifacts: paths: [dist/] build-backend: stage: build script: go build -o api ./cmd artifacts: paths: [api] # Without needs: test-frontend must wait for BOTH build jobs to finish # With needs: test-frontend starts as soon as build-frontend is done test-frontend: stage: test needs: - build-frontend # only waits for this specific job script: npm test test-backend: stage: test needs: - build-backend # only waits for this specific job script: go test ./... deploy: stage: deploy needs: - test-frontend - test-backend script: ./deploy.sh
Key behaviours with needs:
- Jobs with
needscan run out of stage order -needsoverrides stage sequencing - A job with
needs: [](empty list) starts immediately when the pipeline creates - By default, a job with
needsdownloads artifacts from the listed jobs - Set
artifacts: falsein needs to skip downloading artifacts
16. What is the 'workflow' keyword in GitLab CI and how does it control pipeline creation?
The workflow keyword controls whether an entire pipeline is created at all, based on conditions. While rules on individual jobs control whether a specific job runs, workflow: rules controls whether the entire pipeline is created for a given event.
# Only create pipelines for main branch, MRs, and tags workflow: rules: - if: $CI_COMMIT_BRANCH == "main" - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_TAG - when: never # suppress all other pipeline triggers # Name the pipeline dynamically workflow: name: "Pipeline for $CI_COMMIT_REF_NAME" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" name: "MR Pipeline: $CI_MERGE_REQUEST_TITLE" - if: $CI_COMMIT_BRANCH # Prevent duplicate pipelines (MR + branch pipelines) # This is a very common pattern: workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never # skip branch pipeline when MR pipeline exists - if: $CI_COMMIT_BRANCH
Preventing duplicate pipelines: by default, pushing to a branch that has an open MR creates two pipelines - a branch pipeline and an MR pipeline. The pattern above suppresses branch pipelines when an MR is open, avoiding redundant CI runs that waste runner minutes.
17. What is the 'extends' keyword and how does it enable job templates?
The extends keyword lets one job inherit configuration from another job (or a hidden job used as a template). This reduces duplication and keeps the pipeline DRY (Don't Repeat Yourself).
# Define a hidden job template (starts with .) .base-deploy: image: alpine:latest before_script: - apk add --no-cache curl - echo "Logging into registry..." after_script: - echo "Deployment complete" rules: - if: $CI_COMMIT_BRANCH == "main" # Jobs extend the template deploy-staging: extends: .base-deploy script: - ./deploy.sh staging environment: name: staging deploy-production: extends: .base-deploy script: - ./deploy.sh production environment: name: production rules: # overrides the template rules - if: $CI_COMMIT_TAG when: manual # extends with multiple parents (array form) .logging: after_script: - send-logs.sh my-job: extends: - .base-deploy - .logging # merges config from both (later parent wins on conflict)
Merge behaviour: when a job extends a template, YAML hashes are merged deeply. Arrays (like script, rules) in the child job replace (not append to) the parent's arrays. A job can extend multiple templates by providing an array - conflicts are resolved with the last-listed parent winning, then the child overriding all.
18. What is the 'include' keyword and how do you share CI configuration across projects?
The include keyword lets you split pipeline configuration across multiple files or pull shared configuration from external sources. This is the foundation for sharing CI standards across many projects in an organisation.
| Type | Syntax | Use case |
|---|---|---|
| local | include: local: '/templates/jobs.yml' | Another file in the same repository |
| project | include: project: 'group/repo', file: '/ci/template.yml' | File in another GitLab project |
| remote | include: remote: 'https://example.com/ci.yml' | Any publicly accessible URL |
| template | include: template: 'Auto-DevOps.gitlab-ci.yml' | GitLab built-in templates |
# Include multiple sources include: - local: '/ci/build.yml' # file in same repo - project: 'myorg/ci-templates' # file from another project ref: main file: '/templates/deploy.yml' - template: 'SAST.gitlab-ci.yml' # GitLab built-in security template - remote: 'https://cdn.example.com/ci/lint.yml' # external URL # Using included configuration # The included files can define jobs, stages, variables, etc. # They are merged into the main .gitlab-ci.yml stages: - build - test - security - deploy # Your local jobs here, plus everything from the included files
Security note: when including files from another project with include: project, the user triggering the pipeline must have at least Reporter access to that project. Using a specific SHA hash for ref (rather than a branch name) is recommended for stability and security - branch-based refs can change.
19. What are GitLab environments and how do you define them in CI/CD?
Environments in GitLab CI/CD represent deployment targets - staging, production, review apps, etc. Defining an environment in a job links that deployment to the environment, enabling GitLab to track which version is deployed where and provide rollback capabilities.
# Basic environment definition deploy-staging: stage: deploy script: - ./deploy.sh staging environment: name: staging url: https://staging.example.com rules: - if: $CI_COMMIT_BRANCH == "main" deploy-production: stage: deploy script: - ./deploy.sh production environment: name: production url: https://example.com rules: - if: $CI_COMMIT_TAG when: manual # requires manual approval # Dynamic review environments per merge request review-app: stage: deploy script: - deploy-to-review.sh $CI_COMMIT_REF_SLUG environment: name: review/$CI_COMMIT_REF_SLUG url: https://$CI_COMMIT_REF_SLUG.review.example.com on_stop: stop-review rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" stop-review: stage: deploy script: - teardown-review.sh $CI_COMMIT_REF_SLUG environment: name: review/$CI_COMMIT_REF_SLUG action: stop rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: manual
Environments are visible under Deploy > Environments in the GitLab UI, showing the current deployed version, who deployed it, and when. The on_stop option links a cleanup job that tears down ephemeral environments (like review apps) when no longer needed.
20. What are manual jobs in GitLab CI/CD and how do you configure them?
A manual job is a job that is added to the pipeline but does not run automatically - it requires someone to click a button in the GitLab UI (or API call) to trigger it. Manual jobs are commonly used for production deployments that need human approval.
# Method 1: using rules deploy-production: stage: deploy script: - ./deploy.sh production environment: production rules: - if: $CI_COMMIT_BRANCH == "main" when: manual # must be clicked to run allow_failure: false # blocks pipeline completion until clicked # Method 2: legacy "when" keyword deploy-prod-legacy: stage: deploy script: ./deploy.sh production when: manual only: - main # Blocking vs non-blocking manual jobs: # Non-blocking (default): pipeline is marked as passed without clicking # Blocking: pipeline stays in "blocked" status until job is triggered # To make it blocking (requires allow_failure: false): critical-approval: stage: approve script: echo "Approval recorded" when: manual allow_failure: false # blocks the pipeline # Protected manual jobs (only certain roles can trigger): # Set in project Settings > CI/CD > Protected environments
allow_failure with manual jobs: when when: manual is set without allow_failure: false, the job is non-blocking - the pipeline proceeds (and can pass) without the manual job being triggered. With allow_failure: false, the pipeline stays in a blocked state until someone triggers the manual job.
21. What is the 'allow_failure' keyword and how does it affect pipeline status?
The allow_failure keyword controls whether a failing job causes the pipeline to fail. When set to true, the job can fail without marking the overall pipeline as failed - GitLab shows a warning icon on the job instead of a failure.
# allow_failure: true - job failure does not fail the pipeline linting: stage: test script: - eslint src/ --max-warnings 0 allow_failure: true # linting warnings won't block deployment # allow_failure with rules (preferred modern approach) code-quality: stage: test script: - run-quality-check.sh rules: - if: $CI_COMMIT_BRANCH allow_failure: true - if: $CI_COMMIT_TAG allow_failure: false # strict on releases # allow_failure with exit codes # Only allow specific non-zero exit codes: flaky-test: script: - run-flaky-tests.sh allow_failure: exit_codes: 137 # allow OOM kills (exit code 137) # allow_failure: true with manual jobs # makes the manual job non-blocking optional-deploy: when: manual allow_failure: true # pipeline passes without triggering this job
| allow_failure | Job fails | Pipeline result |
|---|---|---|
| false (default) | Yes | Pipeline fails |
| true | Yes | Pipeline passes with warning icon |
| exit_codes: [N] | Exit code N | Pipeline passes; other codes still fail pipeline |
22. How does parallel job execution work in GitLab CI/CD?
GitLab CI supports running multiple instances of the same job in parallel using the parallel keyword. This is useful for splitting test suites across multiple runners to reduce total execution time.
# Run the same job N times in parallel test: stage: test image: ruby:3.2 script: - bundle exec rspec parallel: 5 # GitLab creates 5 jobs: test 1/5, test 2/5, ..., test 5/5 # Each job gets: CI_NODE_INDEX (1-5) and CI_NODE_TOTAL (5) # Your test framework uses these to split the test suite # Parallel matrix - run job with different variable combinations build: stage: build script: - echo "Node: $NODE_VERSION on $OS" parallel: matrix: - NODE_VERSION: ["18", "20", "22"] OS: ["ubuntu", "alpine"] # Creates 6 jobs: all combinations of NODE_VERSION x OS # Access which shard you are in: test-shard: script: - echo "Running shard $CI_NODE_INDEX of $CI_NODE_TOTAL" - run-tests --split $CI_NODE_TOTAL --shard $CI_NODE_INDEX
| Variable | Value |
|---|---|
| $CI_NODE_INDEX | The index of this parallel job (1-based) |
| $CI_NODE_TOTAL | Total number of parallel jobs |
| $MATRIX_VAR | Matrix variables become env vars in each job |
Parallel jobs all run in the same stage - they start at the same time (assuming runners are available) and the stage completes when all parallel jobs finish. All jobs must pass for the stage to pass.
23. What are GitLab CI/CD scheduled pipelines and how do you set them up?
Scheduled pipelines run on a defined timetable (like a cron job) without requiring a code push. They are useful for nightly builds, weekly security scans, database backups, and performance benchmarks.
# In .gitlab-ci.yml - detect schedule trigger: nightly-test: stage: test script: - run-full-test-suite.sh rules: - if: $CI_PIPELINE_SOURCE == "schedule" # only runs when scheduled regular-test: stage: test script: - run-quick-tests.sh rules: - if: $CI_PIPELINE_SOURCE != "schedule" # skip during scheduled runs # Variables defined in the schedule are available as CI/CD variables: nightly-deploy: script: - echo "Running type: $PIPELINE_TYPE" # set in schedule config - echo "Env: $DEPLOY_ENV"
Setting up a schedule in the UI:
- Go to Build > Pipeline schedules > New schedule
- Enter a cron expression (e.g.
0 2 * * *for 2am daily) - Select the target branch or tag
- Optionally add custom CI/CD variables for this schedule
- Enable/disable the schedule without deleting it
The $CI_PIPELINE_SOURCE predefined variable equals 'schedule' for scheduled pipelines, allowing you to run different jobs for scheduled vs push-triggered pipelines.
24. What are pipeline triggers and how can a pipeline be started without a git push?
GitLab pipelines can be triggered by multiple mechanisms beyond a git push. Understanding all trigger sources allows you to design flexible CI/CD workflows.
| Source | Value | How triggered |
|---|---|---|
| Git push | push | Developer pushes commits to any branch |
| Merge request | merge_request_event | MR is created or updated |
| Web UI | web | 'Run pipeline' button in GitLab UI |
| API | api | POST to /api/v4/projects/:id/pipeline |
| Trigger token | trigger | POST with a pipeline trigger token |
| Schedule | schedule | Scheduled pipeline ran |
| Parent pipeline | parent_pipeline | Upstream pipeline triggered this child |
| Chat | chat | GitLab ChatOps slash command |
# Trigger a pipeline via API: curl --request POST \ --form "token=<trigger-token>" \ --form "ref=main" \ --form "variables[DEPLOY_ENV]=staging" \ "https://gitlab.example.com/api/v4/projects/123/trigger/pipeline" # Trigger a downstream pipeline from a job (bridge job): trigger-deploy: stage: deploy trigger: project: myorg/deploy-repo branch: main strategy: depend # wait for downstream pipeline to complete # Check trigger source in rules: job-for-mr-only: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"
Pipeline triggers can pass custom variables, enabling flexible parameterised pipelines. For example, you can trigger a deployment pipeline from another system, passing the environment name and image tag as variables.
25. What is the 'retry' keyword and how do you handle flaky tests in GitLab CI?
The retry keyword automatically reruns a job when it fails, up to the specified number of times. This is useful for handling transient failures - network timeouts, flaky tests, or race conditions - without manual intervention.
# Simple retry: retry up to 2 times on any failure flaky-test: script: - run-tests.sh retry: 2 # total: 3 attempts (1 original + 2 retries) # Retry only on specific failure types: network-test: script: - curl https://api.example.com/data retry: max: 2 when: - runner_system_failure # runner itself crashed - stuck_or_timeout_failure # job timed out - api_failure # GitLab API error during job # NOT for script failures (that would mask real test failures) # All retry "when" values: # always, unknown_failure, script_failure, api_failure, # runner_system_failure, missing_dependency_failure, # stuck_or_timeout_failure, scheduler_failure, data_integrity_failure
| Value | Triggers retry when |
|---|---|
| always | Any failure - use with caution |
| runner_system_failure | The runner system failed (not script failure) |
| stuck_or_timeout_failure | Job timed out or got stuck |
| api_failure | GitLab API error during job execution |
| script_failure | The script returned non-zero exit code |
Best practice: avoid retry: always or retrying on script_failure - these can hide real failures. Only retry on infrastructure failures (runner_system_failure, stuck_or_timeout_failure) where the failure is clearly not the code's fault.
26. What is the 'timeout' keyword in GitLab CI and what are the defaults?
The timeout keyword sets the maximum duration a job is allowed to run before GitLab forcefully stops it. Timeouts prevent hanging jobs from blocking runners and consuming resources indefinitely.
# Job-level timeout long-running-test: script: - run-full-integration-tests.sh timeout: 2 hours # stop after 2 hours build-job: script: - make build timeout: 30 minutes # stop after 30 minutes # Timeout format examples: # timeout: 3600 (seconds) # timeout: 60 minutes # timeout: 2 hours # timeout: 1h 30m # timeout: 1 day # Default timeout hierarchy: # 1. Job-level timeout (if set) # 2. Project-level timeout (Settings > CI/CD > General pipelines) # 3. Runner timeout (set when runner is registered) # The smallest of project timeout and runner timeout applies, # then job-level timeout can only be lower than that
| Level | Where set | Default |
|---|---|---|
| Job | timeout: keyword in job | No default (uses project or runner) |
| Project | Settings > CI/CD > General pipelines | 60 minutes |
| Runner | Registered runner config | No limit (unlimited) |
Key rule: a job timeout cannot exceed the project-level timeout or the runner's maximum timeout. If you set a job timeout longer than the project timeout, the project timeout wins.
27. What is the GitLab Container Registry and how do you use it in CI/CD pipelines?
The GitLab Container Registry is a built-in Docker image registry integrated with every GitLab project. It provides a private place to store and manage Docker images, directly accessible from CI/CD pipelines using predefined variables.
# Build and push to the GitLab Container Registry: build-image: stage: build image: docker:27 services: - docker:27-dind variables: DOCKER_TLS_CERTDIR: "/certs" before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA . - docker build -t $CI_REGISTRY_IMAGE:latest . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA - docker push $CI_REGISTRY_IMAGE:latest # Pull from registry in a later job: deploy-job: stage: deploy script: - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA - docker run -d $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
| Variable | Value |
|---|---|
| $CI_REGISTRY | registry.gitlab.com (or your instance's registry URL) |
| $CI_REGISTRY_IMAGE | Full path: registry.gitlab.com/group/project |
| $CI_REGISTRY_USER | GitLab CI token username (gitlab-ci-token) |
| $CI_REGISTRY_PASSWORD | GitLab CI token (auto-generated job token) |
The docker:dind (Docker-in-Docker) service is required when building Docker images inside a Docker-based runner. The DOCKER_TLS_CERTDIR variable enables TLS for Docker-in-Docker communication, which is required in modern versions of Docker.
28. What are 'services' in GitLab CI/CD and how do you use them?
Services are Docker containers that run alongside the main job container, providing supporting infrastructure like databases, message brokers, or a Docker daemon. They share a network namespace with the job container.
# Run tests with a PostgreSQL database service: test-with-db: stage: test image: python:3.12 services: - postgres:16-alpine # starts a Postgres container variables: POSTGRES_DB: testdb POSTGRES_USER: postgres POSTGRES_PASSWORD: secret DATABASE_URL: "postgresql://postgres:secret@postgres/testdb" script: - pip install -r requirements.txt - python -m pytest # Multiple services: integration-test: image: node:20 services: - redis:7-alpine - postgres:16-alpine - name: rabbitmq:3-management alias: rabbit # access as "rabbit" hostname script: - npm test # Service with alias and entrypoint override: custom-service: services: - name: my-registry/my-service:latest alias: api-mock entrypoint: ["/usr/bin/api-mock"] command: ["--port", "8080"]
Services are accessible from the main job container using the service's image name (or alias) as the hostname. For example, a postgres:16 service is reached at hostname postgres. Set an alias when you need a custom hostname.
29. What is the 'default' keyword in GitLab CI and how does it reduce duplication?
The default keyword defines configuration that applies to all jobs in the pipeline unless overridden at the job level. It acts as a global default for common settings, reducing repetition across many jobs.
# Set defaults for all jobs default: image: ubuntu:24.04 before_script: - apt-get update -qq - echo "Setting up environment..." after_script: - echo "Job complete" retry: max: 1 when: runner_system_failure timeout: 1 hour interruptible: true # allow newer pipelines to cancel this job # Jobs inherit defaults but can override individual settings: build-job: image: node:20-alpine # overrides the default ubuntu image script: - npm ci && npm run build # before_script from default still runs python-test: # uses all defaults: ubuntu image, before_script, retry, timeout script: - pip install -r requirements.txt - pytest before_script: - pip install --upgrade pip # overrides global before_script entirely
| Keyword | Effect when set globally |
|---|---|
| image | Default Docker image for all jobs |
| before_script | Setup commands prepended to every job |
| after_script | Cleanup commands appended to every job |
| retry | Default retry behaviour for all jobs |
| timeout | Default job timeout |
| interruptible | Whether newer pipelines can cancel pending jobs |
| artifacts | Default artifact paths |
| cache | Default cache configuration |
30. What are YAML anchors and aliases in GitLab CI and how do they reduce duplication?
YAML anchors (&name) and aliases (*name) are standard YAML features that let you define a block once and reuse it multiple times. In GitLab CI, they reduce duplication for shared configuration like rules, before_script, or variables - without requiring the extends keyword.
# Define a reusable block with an anchor .common_rules: &common_rules rules: - if: $CI_COMMIT_BRANCH == "main" - if: $CI_PIPELINE_SOURCE == "merge_request_event" .common_setup: &common_setup image: node:20-alpine before_script: - npm ci # Reuse with alias (*) or merge key (<<: *) build-job: <<: *common_setup # merge the anchor content <<: *common_rules # YAML 1.1 syntax script: - npm run build test-job: <<: *common_setup <<: *common_rules script: - npm test # Anchors for variables .env_vars: &prod_vars variables: ENVIRONMENT: production LOG_LEVEL: error deploy-prod: <<: *prod_vars script: ./deploy.sh production
Anchors vs extends:
- YAML anchors are processed before GitLab reads the YAML - they are pure YAML features and work at the text level
- extends is a GitLab-specific feature processed after YAML parsing, offering deep merging, multiple parents, and better tooling visibility
- GitLab recommends using
extendsover YAML anchors for job configuration since it is more explicit and readable
31. What is a merge request pipeline in GitLab CI and how does it differ from a branch pipeline?
GitLab supports two types of pipelines related to merge requests: branch pipelines (triggered by a push to a branch) and merge request pipelines (triggered specifically by merge request events). Understanding the difference is essential for designing efficient review workflows.
| Aspect | Branch Pipeline | Merge Request Pipeline |
|---|---|---|
| Trigger | Push to a branch | MR created, updated, or rebased |
| Source variable | push | merge_request_event |
| Runs on | The feature branch's HEAD | Merge result (branch + target merged) |
| MR status checks | Not shown in MR (unless configured) | Shown in MR as status checks |
| MR variables | $CI_MERGE_REQUEST_* not set | $CI_MERGE_REQUEST_* fully populated |
| Merged result | Tests the branch as-is | Tests what the code will look like after merge |
# Jobs that run only in MR pipelines: code-review: script: - run-code-quality.sh rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" # Useful MR-specific variables: check-mr: script: - echo "MR ID: $CI_MERGE_REQUEST_ID" - echo "Target branch: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME" - echo "Source branch: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" - echo "MR title: $CI_MERGE_REQUEST_TITLE" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" # Merged results pipeline (tests the post-merge state): # Enable in Settings > Merge requests > Merged results pipelines # Runs on the merge commit that would result from accepting the MR
Merged results pipelines (enabled in project settings) run the CI on the simulated merge result - catching conflicts between the MR branch and the target branch that would only appear after merging. This is safer than testing just the source branch.
32. What is pipeline caching and how do you configure cache keys?
Cache in GitLab CI speeds up jobs by storing files (typically dependencies like node_modules/) between pipeline runs. The cache key determines which jobs share a cache and when the cache is invalidated.
# Basic cache with branch-scoped key: cache: key: "$CI_COMMIT_REF_SLUG" # different cache per branch paths: - .npm/ - node_modules/ # Cache keyed to dependency file (invalidates when file changes): cache: key: files: - package-lock.json # cache key changes when lockfile changes prefix: "$CI_COMMIT_REF_SLUG" paths: - node_modules/ # Cache policies (pull-push is default): job-with-readonly-cache: cache: key: $CI_COMMIT_REF_SLUG paths: - node_modules/ policy: pull # only download cache, never update it job-that-updates-cache: cache: policy: push # only upload, skip downloading # Per-job cache: build: cache: key: "build-$CI_COMMIT_REF_SLUG" paths: [dist/] test: cache: key: "deps-$CI_COMMIT_REF_SLUG" paths: [node_modules/]
| Policy | Behaviour |
|---|---|
| pull-push (default) | Download cache at start, upload updated cache at end |
| pull | Download cache only - never update (read-only) |
| push | Upload cache only - never download (write-only) |
Use policy: pull for most jobs that just use the cache, and one dedicated job with policy: push or pull-push to update it. This prevents multiple parallel jobs from overwriting each other's cache updates.
33. What is the GitLab CI Lint tool and how do you validate a.gitlab-ci.yml file?
Before committing a .gitlab-ci.yml change and waiting for a pipeline to run, you can validate the configuration using GitLab's built-in CI Lint tool. This catches syntax errors, invalid keywords, and configuration issues immediately.
# Method 1: GitLab UI # Navigate to: Build > Pipelines > then click "CI Lint" # or: Project > CI/CD > Lint (path varies by GitLab version) # Paste your YAML and click "Validate" # Method 2: Pipeline Editor # Navigate to: Build > Pipeline editor # The editor validates in real-time as you type # Method 3: GitLab API curl --header "PRIVATE-TOKEN: <your-token>" \ --header "Content-Type: application/json" \ --data '{"content": "include:\n - local: /.gitlab-ci.yml"}' \ "https://gitlab.example.com/api/v4/projects/:id/ci/lint" # Method 4: gitlab-ci-local (third-party tool) # Run pipelines locally for development: pip install gitlab-ci-local # or: brew install gitlab-ci-local gitlab-ci-local # simulates pipeline execution locally # The CI Lint tool checks: # - Valid YAML syntax # - Valid GitLab CI keywords # - Correct job structure # - Variable expansion # - Stage references are defined
Pipeline editor: GitLab's built-in pipeline editor (Build > Pipeline editor) provides real-time validation, syntax highlighting, a visual pipeline diagram, and the CI Lint tool in one interface. It is the recommended way to edit .gitlab-ci.yml files in the browser.
34. What are GitLab CI/CD components and how do they differ from includes?
CI/CD components (introduced in GitLab 16.0) are a more structured, versioned, and discoverable way to share pipeline configuration across projects compared to simple include: project. They are published to the CI/CD catalog.
# Using a CI/CD component in .gitlab-ci.yml: include: - component: $CI_SERVER_FQDN/myorg/my-components/deploy@1.0.0 inputs: environment: production image_tag: $CI_COMMIT_SHORT_SHA # Use a specific version (tag), branch, or SHA: - component: gitlab.com/gitlab-org/ci-components/secret-detection@main # Creating a component (in its own repository): # File: templates/deploy.yml --- spec: inputs: environment: description: "Deployment environment (staging or production)" image_tag: description: "Docker image tag to deploy" default: "latest" --- deploy-$[[ inputs.environment ]]: stage: deploy script: - ./deploy.sh $[[ inputs.environment ]] $[[ inputs.image_tag ]]
| Aspect | include: project | CI/CD components |
|---|---|---|
| Versioning | Via ref (branch/tag/SHA) | Semantic versioning + catalog |
| Inputs | No input mechanism | Typed, documented inputs (spec: inputs:) |
| Discovery | Manual (know the path) | Searchable CI/CD catalog |
| Validation | None | Input type validation |
| GitLab version | All versions | 16.0+ |
| Recommended for | Internal file reuse | Shareable, published templates |
CI/CD components use the $[[ inputs.variable_name ]] syntax (double square brackets) to reference inputs, which is different from regular CI/CD variable syntax $VARIABLE.
35. What are parent-child pipelines in GitLab CI/CD?
Parent-child pipelines (also called dynamic pipelines) allow a job in one pipeline (the parent) to dynamically generate and trigger another pipeline (the child). This enables modular, scalable pipelines for monorepos and complex projects.
# Parent pipeline (.gitlab-ci.yml): stages: - generate - trigger generate-child-config: stage: generate script: - generate-pipeline-config.sh > child-pipeline.yml # dynamic YAML artifacts: paths: - child-pipeline.yml trigger-child: stage: trigger trigger: include: - artifact: child-pipeline.yml job: generate-child-config strategy: depend # wait for child to complete # Or trigger from a static file: trigger-static-child: trigger: include: - local: ci/child-pipeline.yml strategy: depend # Child pipeline (ci/child-pipeline.yml): stages: - build - test build-child: stage: build script: echo "Child pipeline building..."
| Feature | Detail |
|---|---|
| strategy: depend | Parent pipeline status reflects child pipeline result |
| Dynamic config | Child YAML can be generated at runtime by the parent |
| Isolation | Child pipeline has its own stages, jobs, and variables |
| Artifacts | Child can receive artifacts from the parent job that triggered it |
| Depth limit | GitLab limits nesting to a certain depth (check current docs) |
Common monorepo use case: a parent pipeline detects which subdirectories changed, then triggers separate child pipelines for only those services - avoiding running every service's tests on every push.
36. What is the 'resource_group' keyword and how does it prevent concurrent deployments?
The resource_group keyword ensures that only one job with the same resource group name runs at a time across all pipelines. This prevents concurrent deployments to the same environment, which can cause race conditions or configuration conflicts.
# Prevent concurrent deployments to production: deploy-production: stage: deploy script: - ./deploy.sh production resource_group: production # only one "production" deploy at a time environment: production # Multiple environments, each serialized separately: deploy-staging: script: ./deploy.sh staging resource_group: staging # serializes staging deploys deploy-production: script: ./deploy.sh production resource_group: production # serializes production deploys separately # Process mode (controls job ordering within the group): deploy-job: resource_group: my-group process_mode: oldest_first # FIFO - deploy in the order triggered # Options: # oldest_first (default): FIFO queue # newest_first: LIFO - skip old deploys, do most recent # unordered: no ordering guarantee
| Mode | Behaviour | Best for |
|---|---|---|
| oldest_first (default) | Queue in order - FIFO, every deploy runs | Compliance-critical environments |
| newest_first | LIFO - newer jobs skip older ones waiting in queue | Fast-moving dev environments |
| unordered | No guaranteed order | Non-deployment resource exclusion |
Why this matters: without resource groups, if two pipelines both have a deploy-to-production job and run at the same time, both deployment scripts could run concurrently, overwriting each other's changes or causing partial deployments.
37. What are GitLab's built-in security scanning templates and how do you enable them?
GitLab provides built-in CI/CD templates for security scanning that can be included in any pipeline with a single line. These templates add jobs to your pipeline that scan for vulnerabilities without requiring manual configuration.
# Include GitLab security templates: include: - template: Security/SAST.gitlab-ci.yml # Static Application Security Testing - template: Security/Secret-Detection.gitlab-ci.yml # Detect secrets/tokens in code - template: Security/Dependency-Scanning.gitlab-ci.yml - template: Security/DAST.gitlab-ci.yml # Dynamic (requires running app) - template: Security/Container-Scanning.gitlab-ci.yml stages: - build - test - security # security templates add jobs to appropriate stages - deploy # These templates are maintained by GitLab and auto-update # Results appear in: # Secure > Vulnerability Report (GitLab Ultimate) # As MR security widgets (GitLab Ultimate) # SAST customisation: variables: SAST_EXCLUDED_PATHS: "spec,test,tests,tmp" # skip test directories SAST_EXCLUDED_ANALYZERS: "bandit" # skip specific analyzer # Secret Detection customisation: SECRET_DETECTION_EXCLUDED_PATHS: "tests/"
| Template | Scans for |
|---|---|
| SAST | Code vulnerabilities using static analysis |
| Secret Detection | Hardcoded secrets, API keys, passwords in code |
| Dependency Scanning | Known CVEs in project dependencies |
| DAST | Runtime vulnerabilities by attacking a live app |
| Container Scanning | CVEs in Docker image layers |
Security scanning results are visible in merge requests and the Vulnerability Report for GitLab Ultimate. For lower tiers, results appear in the job artifacts as JSON reports that can be parsed by other tools.
38. What is the 'GIT_STRATEGY' variable in GitLab CI and what are the options?
The GIT_STRATEGY variable controls how GitLab CI fetches your repository's code at the start of each job. Choosing the right strategy balances freshness of code against job startup speed.
| Value | Behaviour | Speed | Use case |
|---|---|---|---|
| fetch (default) | Fetches only new commits; reuses existing clone | Fast | Most jobs |
| clone | Full fresh clone every time | Slow | When clean state is required |
| none | Skips Git entirely; no repo code | Fastest | Jobs that don't need repo code (deploy from artifact) |
# Set globally for all jobs: variables: GIT_STRATEGY: fetch # default - fast incremental fetch # Override for a specific job: clean-build: variables: GIT_STRATEGY: clone # ensure completely fresh checkout script: - make clean && make build # Skip checkout for deploy-only jobs: deploy-from-artifact: variables: GIT_STRATEGY: none # no git checkout needed script: - echo "Using artifact from build job..." - ls dist/ # artifact already downloaded - ./deploy.sh # Related: control submodule fetching variables: GIT_SUBMODULE_STRATEGY: normal # clone/fetch submodules too
GIT_DEPTH: the related variable GIT_DEPTH controls shallow clone depth. Setting GIT_DEPTH: "1" fetches only the latest commit (no history), which is significantly faster for large repositories. Setting it to 0 disables shallow cloning and fetches full history.
39. What are protected branches and protected variables in GitLab CI?
Protected branches and protected variables are security features that control who can push to critical branches and which jobs can access sensitive CI/CD variables.
# Protected variables are set in: # Settings > CI/CD > Variables > Add variable # Tick "Protected variable" checkbox # A protected variable is ONLY available in jobs running on: # 1. Protected branches (e.g. main, production) # 2. Protected tags # Example: PROD_API_KEY is protected: deploy-production: stage: deploy script: - echo "Using: $PROD_API_KEY" # only available because main is protected rules: - if: $CI_COMMIT_BRANCH == "main" # main is a protected branch # feature branches (unprotected) CANNOT access $PROD_API_KEY: test-on-feature: script: - echo "$PROD_API_KEY" # will be empty - variable not available here rules: - if: $CI_COMMIT_BRANCH =~ /^feature/ # Protected branches in GitLab: # Settings > Repository > Protected branches # Can restrict push access to Maintainers, Developers, No one # Can require merge request approval before merging # Can require status checks (pipeline must pass)
| Setting | Controls |
|---|---|
| Allowed to merge | Minimum role required to merge into this branch |
| Allowed to push and merge | Minimum role required to push directly (bypass MR) |
| Allowed to force push | Whether force push is allowed (usually disabled) |
| Code owner approval | Whether CODEOWNERS must approve changes |
Security design: this combination means developers on feature branches cannot access production secrets even if they have access to the project. Only pipelines running on protected branches (typically main or release tags) can use protected variables like production API keys.
40. What are common reasons a GitLab CI pipeline fails and how do you troubleshoot them?
When a pipeline fails, GitLab provides job logs, exit codes, and visual indicators to help diagnose the issue. A systematic approach to troubleshooting resolves most failures quickly.
| Failure | Likely cause | Fix |
|---|---|---|
| Job stuck in 'pending' | No runner available with matching tags | Check runner availability; verify job tags match a runner |
| 'script: command not found' | Tool not installed in the Docker image | Change image to one that includes the tool, or install it in before_script |
| Timeout | Job exceeds project or job timeout | Increase timeout or optimise the slow step |
| Artifact not found | Dependency job failed or wrong path | Check dependency job passed; verify artifact path |
| Permission denied (push) | CI_JOB_TOKEN lacks write access | Enable token access in Settings > CI/CD > Token access |
| Cache not working | Cache key mismatch or runner has no cache storage | Check cache key; verify runner has cache storage configured |
| YAML syntax error | Invalid .gitlab-ci.yml | Use CI Lint tool to validate before committing |
# Debugging tips: # 1. Check the full job log - error is usually at the bottom # Build > Pipelines > Click the failed job > View log # 2. Add debug output to the script: job-debug: script: - env # print all environment variables - ls -la # show workspace contents - which node && node -v # verify tools are installed - cat /etc/os-release # check OS in the Docker image # 3. Enable CI/CD debug logging: variables: CI_DEBUG_TRACE: "true" # very verbose - shows all shell commands # 4. Re-run a single failed job: # Click the failed job > Retry # Or retry from the pipeline view # 5. Use CI Lint to validate YAML before pushing: # Build > Pipelines > CI Lint
Most common beginner mistake: setting GIT_STRATEGY: none in a job that actually needs repository code, or forgetting that artifacts from a failed job are not available by default (add artifacts: when: on_failure to capture debug files from failing builds).
