validation
GitLab CI/CD Pipeline Validation
Test and validate pipelines locally before pushing to prevent wasted CI minutes and broken pipelines.
Table of Contents
- Why Validate Locally
- gitlab-ci-local
- GitLab CI Lint API
- Pre-commit Hooks
- Schema Validation
- Testing Strategies
- Best Practices
Why Validate Locally
Cost of Pipeline Errors
Without validation:
Push Pipeline fails (syntax error) 5 CI minutes wasted
Fix Push Pipeline fails (wrong variable) 5 CI minutes wasted
Fix Push Pipeline fails (missing dependency) 5 CI minutes wasted
Fix Push Pipeline succeeds Total: 20 CI minutes wasted
With local validation:
Validate locally Fix errors Push Pipeline succeeds 5 CI minutes used
Savings: 15 CI minutes (75% reduction)
Additional Benefits
- Faster feedback: Seconds vs minutes
- Fewer failed pipelines: Better success rate metrics
- Less noise: Don't spam team with broken pipeline notifications
- Confidence: Know pipeline will work before pushing
gitlab-ci-local
What is gitlab-ci-local?
A tool to run GitLab CI/CD pipelines locally using Docker or shell executors.
Repository: https://github.com/firecow/gitlab-ci-local
Key features:
- Run entire pipelines or individual jobs
- Test with real Docker containers
- Support for variables, artifacts, cache
- Interactive debugging mode
- Fast iteration without pushing
Source: gitlab-ci-local GitHub
Installation
macOS:
brew install gitlab-ci-local
Debian/Ubuntu:
# Add repository curl -s https://gitlab-ci-local-ppa.firecow.dk/public.key | sudo apt-key add - echo "deb https://gitlab-ci-local-ppa.firecow.dk/debian stable main" | sudo tee /etc/apt/sources.list.d/gitlab-ci-local.list # Install sudo apt update sudo apt install gitlab-ci-local
npm (all platforms):
npm install -g gitlab-ci-local
Source: BrowserStack Guide
Basic Usage
Run entire pipeline:
gitlab-ci-local
List available jobs:
gitlab-ci-local --list
Output:
name description stage when allow_failure needs
lint Lint code test on_success false []
build Build app build on_success false [lint]
test Run tests test on_success false [build]
deploy Deploy deploy manual false [test]
Run specific job:
gitlab-ci-local build
Run job with dependencies:
gitlab-ci-local --needs test # Runs lint build test
Advanced Features
Interactive Mode (for debugging):
gitlab-ci-local --shell-isolation test
Pauses execution and drops into a shell where you can inspect state, run commands manually, and debug issues.
Dry Run (show what would run):
gitlab-ci-local --preview
Set Variables:
gitlab-ci-local --variable ENVIRONMENT=staging deploy
Mount Volumes:
gitlab-ci-local --volume /host/path:/container/path
CSV Output (for scripting):
gitlab-ci-local --list-csv
Source: gitlab-ci-local Documentation
Example Workflow
# 1. Make changes to .gitlab-ci.yml vim .gitlab-ci.yml # 2. Validate syntax gitlab-ci-local --preview # 3. Test specific job gitlab-ci-local build # 4. If passing, run full pipeline gitlab-ci-local # 5. Only push when local tests pass git add .gitlab-ci.yml git commit -m "ci: update build configuration" git push
Time saved: 5-10 iterations 5 minutes each = 25-50 CI minutes saved per pipeline change.
Limitations
- No GitLab-hosted features: Can't test GitLab-specific services (Auto DevOps, SAST, etc.)
- Environment differences: Local Docker vs GitLab runners may differ
- Secrets: Must provide secrets locally (don't use production secrets!)
- Resource limits: Local machine resources != runner resources
Recommendation: Use for quick validation, still verify on real runners for critical changes.
Source: Testing GitLab Pipeline Locally
GitLab CI Lint API
API-based Validation
GitLab provides a /api/v4/ci/lint endpoint to validate .gitlab-ci.yml syntax.
Endpoint: POST https://gitlab.com/api/v4/ci/lint
Request:
curl --header "Content-Type: application/json" \ --data '{"content":"stages:\n - build\n\nbuild-job:\n stage: build\n script:\n - echo 'Building'"}' \ https://gitlab.com/api/v4/ci/lint
Response (valid):
{ "valid": true, "merged_yaml": "...", "errors": [], "warnings": [] }
Response (invalid):
{ "valid": false, "errors": ["Invalid syntax at line 5: unexpected '}'"], "warnings": [] }
Using the Web UI
Location: Project CI/CD Editor Validate
- Paste your
.gitlab-ci.ymlcontent - Click "Validate"
- See errors/warnings immediately
Use case: Quick syntax checks without CLI tools.
CI Lint CLI Tool
Script (save as gitlab-ci-lint):
#!/bin/bash # Validate .gitlab-ci.yml using GitLab API if [ ! -f .gitlab-ci.yml ]; then echo "Error: .gitlab-ci.yml not found" exit 1 fi CONTENT=$(cat .gitlab-ci.yml | jq -sR '.') RESPONSE=$(curl -s --header "Content-Type: application/json" \ --data "{\"content\":$CONTENT}" \ https://gitlab.com/api/v4/ci/lint) VALID=$(echo "$RESPONSE" | jq -r '.valid') if [ "$VALID" = "true" ]; then echo " .gitlab-ci.yml is valid" exit 0 else echo " .gitlab-ci.yml is invalid:" echo "$RESPONSE" | jq -r '.errors[]' exit 1 fi
Usage:
chmod +x gitlab-ci-lint ./gitlab-ci-lint
Source: GitLab CI Lint Documentation
Pre-commit Hooks
What are Pre-commit Hooks?
Git hooks that run automatically before git commit executes, allowing validation to catch errors before code is committed.
Framework: https://pre-commit.com/
Installation
# Install pre-commit pip install pre-commit # Or via Homebrew brew install pre-commit
Configuration
Create .pre-commit-config.yaml:
repos: # Validate GitLab CI syntax - repo: https://github.com/emmeowzing/gitlabci-lint-pre-commit-hook rev: v1.3.0 hooks: - id: gitlabci-lint args: ['--server', 'https://gitlab.com'] # Alternative: validate-gitlab-ci - repo: https://github.com/dcs3spp/validate-gitlab-ci rev: v1.1.0 hooks: - id: validate-gitlab-ci args: ['--api-endpoint', 'https://gitlab.com/api/v4'] # Additional checks - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-yaml # Validate YAML syntax - id: end-of-file-fixer - id: trailing-whitespace
Install hooks:
pre-commit install
Sources:
Usage
Automatic (on every commit):
git add .gitlab-ci.yml git commit -m "ci: update pipeline" # Pre-commit hooks run automatically # If validation fails, commit is blocked
Manual (test before commit):
pre-commit run --all-files
Output (on success):
validate-gitlab-ci..................................................Passed
check-yaml..........................................................Passed
end-of-file-fixer...................................................Passed
trailing-whitespace.................................................Passed
Output (on failure):
validate-gitlab-ci..................................................Failed
- hook id: validate-gitlab-ci
- exit code: 1
Error: Invalid GitLab CI syntax at line 10: unexpected token
Cost savings: Block invalid configs before they reach GitLab runners = 100% CI minute savings for that error.
Project-Specific Configuration
For projects with custom GitLab instances:
repos: - repo: https://github.com/dcs3spp/validate-gitlab-ci rev: v1.1.0 hooks: - id: validate-gitlab-ci args: - '--api-endpoint' - 'https://gitlab.company.com/api/v4' - '--project-id' - '12345' # Your project ID
CI Integration
Run pre-commit in CI to enforce validation:
pre-commit: stage: validate image: python:3.11 before_script: - pip install pre-commit script: - pre-commit run --all-files
Source: Using pre-commit in GitLab Pipelines
Schema Validation
JSON Schema for .gitlab-ci.yml
GitLab provides a JSON schema for .gitlab-ci.yml that editors can use for validation and autocompletion.
Schema URL: https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json
VS Code Integration
Install Extension: GitLab Workflow (official)
Or configure manually in .vscode/settings.json:
{ "yaml.schemas": { "https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json": ".gitlab-ci.yml" } }
Features:
- Real-time syntax validation
- Autocompletion for keywords
- Hover documentation
- Error highlighting
Source: GitLab VS Code Extension
IntelliJ / PyCharm Integration
Install Plugin: GitLab Plugin
Configure:
- Settings Languages & Frameworks YAML
- Add schema mapping:
- URL:
https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json - File pattern:
.gitlab-ci.yml
- URL:
CLI Validation with yq
Install yq:
brew install yq # macOS
Validate YAML syntax:
yq eval '.gitlab-ci.yml' > /dev/null # Exit code 0 = valid YAML # Exit code 1 = syntax error
Check for required fields:
# Check if stages are defined yq eval '.stages' .gitlab-ci.yml # Check if job has script yq eval '.build.script' .gitlab-ci.yml
Testing Strategies
Staged Testing Approach
Level 1: Syntax Validation (instant)
yq eval .gitlab-ci.yml > /dev/null
Level 2: Schema Validation (seconds)
pre-commit run gitlabci-lint
Level 3: Local Execution (minutes)
gitlab-ci-local build
Level 4: CI Execution (minutes, on real runners)
git push
Recommendation: Run Levels 1-3 locally, only push when all pass.
Testing Pipeline Changes
1. Create a Test Branch:
git checkout -b test/pipeline-changes
2. Make Changes:
vim .gitlab-ci.yml
3. Validate Locally:
gitlab-ci-local --preview gitlab-ci-local
4. Push to Test Branch:
git push -u origin test/pipeline-changes
5. Verify on Real Runners:
- Check GitLab UI for pipeline status
- Review job logs for unexpected behavior
6. Merge to Main:
git checkout main git merge test/pipeline-changes git push
Testing Variables and Secrets
Local Testing (use dummy values):
# Create .gitlab-ci-local-variables.yml cat > .gitlab-ci-local-variables.yml <<EOF DEPLOY_TOKEN: dummy-token-for-testing DATABASE_URL: postgresql://localhost/test EOF # Run with local variables gitlab-ci-local
Security: Never use production secrets for local testing!
Testing with Different Conditions
Test MR pipeline:
gitlab-ci-local --variable CI_PIPELINE_SOURCE=merge_request_event
Test specific branch:
gitlab-ci-local --variable CI_COMMIT_BRANCH=main
Test draft MR behavior:
gitlab-ci-local --variable CI_MERGE_REQUEST_TITLE="Draft: WIP feature"
Best Practices
1. Always Validate Before Pushing
Workflow:
# 1. Edit pipeline vim .gitlab-ci.yml # 2. Syntax check yq eval .gitlab-ci.yml > /dev/null # 3. Lint gitlab-ci-lint # Custom script or pre-commit # 4. Test locally (if available) gitlab-ci-local build # 5. Push git add .gitlab-ci.yml git commit -m "ci: update build step" git push
2. Use Pre-commit Hooks
Setup once, benefit forever:
pip install pre-commit pre-commit install # Now every commit validates automatically
3. Test Incrementally
Don't test the entire pipeline at once. Test jobs individually:
# Test just the build job gitlab-ci-local build # If that works, test build + test gitlab-ci-local --needs test # Finally, test entire pipeline gitlab-ci-local
4. Maintain a Test Project
Create a sandbox project for experimenting with pipeline changes:
- Clone production project config
- Test changes in sandbox
- Apply to production only when validated
Cost savings: Test complex changes in sandbox before affecting production pipelines.
5. Document Pipeline Behavior
Add comments explaining complex logic:
# Only run on MRs, skip on draft test: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE !~ /^Draft:/ when: on_success - when: never script: npm test
6. Version Control for Pipeline Configs
Use branches for pipeline changes:
git checkout -b ci/add-caching # Make changes git commit -m "ci: add dependency caching" git push # Create MR, review pipeline results # Merge only if successful
7. Monitor Pipeline Changes
After pushing pipeline changes, actively monitor:
- Pipeline success rate
- Job duration changes
- CI minute consumption
- Error patterns
Use CI/CD Analytics: Analytics CI/CD Analytics
Common Validation Errors
Syntax Errors
Error:
Invalid syntax at line 15: unexpected token '}'
Fix: Check YAML syntax (indentation, colons, quotes)
Prevention: Use editor with YAML schema validation
Undefined Variables
Error:
undefined variable: $DEPLOY_TOKEN
Fix: Define in GitLab project settings or .gitlab-ci.yml
Local testing: Provide in .gitlab-ci-local-variables.yml
Missing Dependencies
Error:
needs: job 'build' not found
Fix: Ensure referenced job exists and is not conditionally skipped
Validation:
gitlab-ci-local --list # Check if 'build' is listed
Invalid Rules
Error:
rules: when: invalid value 'maybe'
Fix: Use valid values (on_success, manual, always, never, delayed)
Prevention: Use schema validation in editor
Troubleshooting
gitlab-ci-local Issues
Issue: "Docker not found"
Solution: Ensure Docker is installed and running:
docker ps
Issue: "Job failed with unknown error"
Solution: Add --debug flag for verbose output:
gitlab-ci-local --debug build
Issue: "Cache not working locally"
Solution: gitlab-ci-local cache behavior differs from GitLab. Use volumes for testing:
gitlab-ci-local --volume $PWD/node_modules:/app/node_modules
Pre-commit Hook Issues
Issue: "Pre-commit hook not running"
Solution: Verify installation:
pre-commit install # Re-run installation ls -la .git/hooks/pre-commit # Check hook file exists
Issue: "Hook failing on valid config"
Solution: Update hook version:
pre-commit autoupdate
API Validation Issues
Issue: "401 Unauthorized"
Solution: For private projects, provide API token:
curl --header "PRIVATE-TOKEN: your-token" \ --header "Content-Type: application/json" \ --data "{\"content\":...}" \ https://gitlab.com/api/v4/ci/lint
Summary Checklist
Essential Validation Steps
- Install
gitlab-ci-localfor local testing - Set up pre-commit hooks for automatic validation
- Configure editor with GitLab CI schema
- Create validation script for manual checks
- Document pipeline testing process for team
Before Every Push
- Validate YAML syntax (
yq eval) - Run pre-commit hooks (
pre-commit run) - Test locally if possible (
gitlab-ci-local) - Review pipeline logic for common pitfalls
- Commit with descriptive message
After Pushing
- Monitor pipeline execution
- Review job logs for warnings
- Check CI minute consumption
- Verify expected behavior (caching, artifacts, etc.)
Additional Resources
- Cost Optimization - Prevent wasted CI minutes
- Pipeline Efficiency - Optimize pipeline performance
- Caching Strategies - Speed up jobs
- gitlab-ci-local GitHub
- pre-commit.com
- GitLab CI Lint API
Last Updated: 2026-01-08 Priority: HIGH - Prevent wasted minutes from failed pipelines