gitlab-ci — Cursor rule: what it does, how to install
This rule guides the structure and maintenance of GitLab CI/CD pipelines. Use it when reviewing .gitlab-ci.yml or splitting a growing pipeline into manageable parts.
.cursor/rules/gitlab-ci.mdcWhat gitlab-ci does
The rule treats .gitlab-ci.yml as code. It recommends keeping the root file as an orchestrator, moving stages and jobs into focused files with include, and sharing repeated job settings through extends and YAML anchors. It also sets a consistent two-space indentation style.
For job control, the guide favors rules over the deprecated only/except syntax. It uses needs to express direct build-to-test dependencies, so a test job can wait for its own build rather than every job in an earlier stage. Clear names for stages and jobs are part of the recommended layout.
Install the rule at .cursor/rules/gitlab-ci.mdc; its frontmatter applies to **/*. It targets Cursor and GitLab CI/CD. Mixed indentation or tabs can cause YAML parsing failures, so that detail matters before editing the pipeline.
What it helps with
- split a large
.gitlab-ci.ymlwithinclude - reuse job settings through
extendsand YAML anchors - replace deprecated
only/exceptconditions withrules - declare build-to-test links with
needs - fix YAML parsing problems caused by indentation
Example prompts for the agent
- Review
.gitlab-ci.ymland split its stages and jobs into focused files withinclude. - Replace the deploy conditions in
.gitlab-ci.ymlwithrulesinstead ofonly/except. - Add
needsso each frontend and backend test waits for its matching build job.
How to install gitlab-ci
Manually: put the files in place
.cursor/rules/gitlab-ci.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/gitlab-ci.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/gitlab-ci.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/gitlab-ci.mdManually: put the files in place
.clinerules/gitlab-ci.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about gitlab-ci
How do I install gitlab-ci in Cursor?
Save the rule as .cursor/rules/gitlab-ci.mdc. Cursor will apply it to files matched by **/*.
What does gitlab-ci do?
It gives Cursor guidance for organizing GitLab CI/CD files, reusing YAML configuration, controlling jobs with rules, and declaring dependencies with needs. It also enforces consistent indentation and naming practices.
Which files does the gitlab-ci rule cover?
Its frontmatter uses the **/* glob, so the rule applies to every file matched by that pattern.
Source text
gitlab-ci.mdc · CC0-1.0
---
description: This guide provides opinionated, actionable best practices for structuring, optimizing, and securing GitLab CI/CD pipelines, focusing on modern patterns and avoiding common pitfalls.
globs: **/*
---
# gitlab-ci Best Practices
GitLab CI/CD is the backbone of modern DevOps. This guide ensures your `.gitlab-ci.yml` files are robust, maintainable, performant, and secure. Follow these rules rigorously.
## 1. Code Organization and Structure
Your `.gitlab-ci.yml` is code. Treat it with the same discipline as application code.
### 1.1. Modularize with `include`
Break down large pipelines into smaller, focused YAML files. This improves readability, reusability, and reduces merge conflicts. Keep the root `.gitlab-ci.yml` as an orchestrator.
❌ **BAD: Monolithic `.gitlab-ci.yml`**
```yaml
# .gitlab-ci.yml (too long, hard to navigate)
stages:
- build
- test
- deploy
build_frontend:
stage: build
script:
- npm install
- npm run build
test_frontend:
stage: test
script:
- npm run test:unit
build_backend:
stage: build
script:
- pip install -r requirements.txt
- python setup.py build
test_backend:
stage: test
script:
- pytest
deploy_staging:
stage: deploy
script:
- deploy-script --env staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
```
✅ **GOOD: Modularized with `include`**
```yaml
# .gitlab-ci.yml (root orchestrator)
include:
- local: '.gitlab/ci/stages.yml'
- local: '.gitlab/ci/frontend.yml'
- local: '.gitlab/ci/backend.yml'
- local: '.gitlab/ci/deploy.yml'
# .gitlab/ci/stages.yml
stages:
- build
- test
- security
- deploy
# .gitlab/ci/frontend.yml
.frontend_template: &frontend_template
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}-node-modules
paths:
- frontend/node_modules/
policy: pull-push
before_script:
- cd frontend
build_frontend:
<<: *frontend_template
stage: build
script:
- npm install
- npm run build
artifacts:
paths:
- frontend/dist/
expire_in: 1 day
test_frontend:
<<: *frontend_template
stage: test
script:
- npm run test:unit
# .gitlab/ci/backend.yml
.backend_template: &backend_template
image: python:3.11-slim-buster
cache:
key: ${CI_COMMIT_REF_SLUG}-pip-cache
paths:
- backend/.venv/
policy: pull-push
before_script:
- cd backend
- python -m venv .venv
- source .venv/bin/activate
build_backend:
<<: *backend_template
stage: build
script:
- pip install -r requirements.txt
- python setup.py build
artifacts:
paths:
- backend/dist/
expire_in: 1 day
test_backend:
<<: *backend_template
stage: test
script:
- pytest
# .gitlab/ci/deploy.yml
deploy_staging:
stage: deploy
image: alpine/git:latest
script:
- echo "Deploying to staging..."
- ./scripts/deploy.sh staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
environment:
name: staging
url: https://staging.example.com
```
### 1.2. Use `extends` and YAML Anchors for Reusability
Avoid repetition. Define common configurations once and reuse them across jobs. `extends` is preferred for job-level inheritance, while YAML anchors (`&` and `*`) are excellent for smaller, inline reusable blocks.
❌ **BAD: Repetitive job definitions**
```yaml
build_job_a:
image: node:20-alpine
script:
- npm install
- npm run build
cache:
key: ${CI_COMMIT_REF_SLUG}-node-modules
paths:
- node_modules/
build_job_b:
image: node:20-alpine
script:
- npm install
- npm run build:prod
cache:
key: ${CI_COMMIT_REF_SLUG}-node-modules
paths:
- node_modules/
```
✅ **GOOD: Reusable with `extends` and anchors**
```yaml
.node_base: &node_base_config
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}-node-modules
paths:
- node_modules/
policy: pull-push
.build_template:
extends: .node_base
stage: build
script:
- npm install
- npm run build
build_job_a:
extends: .build_template
build_job_b:
extends: .build_template
script:
- npm install # Overrides script from .build_template
- npm run build:prod
```
### 1.3. Consistent Indentation
Always use **2 spaces** for indentation. Never use tabs. Inconsistent indentation is a leading cause of YAML parsing errors.
❌ **BAD: Mixed indentation or incorrect spacing**
```yaml
job_name:
stage: build
script: # 4 spaces, inconsistent
- echo "Hello"
```
✅ **GOOD: Consistent 2-space indentation**
```yaml
job_name:
stage: build
script:
- echo "Hello"
```
## 2. Common Patterns and Anti-patterns
Adopt modern GitLab CI/CD features for robust and efficient pipelines.
### 2.1. Use `rules` for Conditional Job Execution
The `rules` keyword is the definitive way to control when jobs run. It replaces the deprecated `only/except` syntax, offering far greater flexibility and clarity.
❌ **BAD: Deprecated `only/except`**
```yaml
deploy_prod:
stage: deploy
script:
- deploy-to-prod.sh
only:
- main
except:
- branches@feature/*
```
✅ **GOOD: Modern `rules` syntax**
```yaml
deploy_prod:
stage: deploy
script:
- deploy-to-prod.sh
rules:
- if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
when: manual # Requires manual approval for production pushes
- if: $CI_COMMIT_TAG # Run on tags automatically
when: on_success
- when: never # Do not run otherwise
```
### 2.2. Leverage `needs` for Explicit Job Dependencies
Use `needs` to define explicit dependencies between jobs. This allows jobs to run in parallel across stages, significantly speeding up pipelines by avoiding strict stage-based ordering.
❌ **BAD: Implicit stage-based dependencies**
```yaml
stages:
- build
- test
- deploy
build_app:
stage: build
script:
- make build
artifacts:
paths:
- build/
test_app: # Waits for all 'build' jobs to complete
stage: test
script:
- make test
```
✅ **GOOD: Explicit `needs` for parallel execution**
```yaml
stages:
- build
- test
- deploy
build_frontend:
stage: build
script:
- npm run build
artifacts:
paths:
- frontend/dist/
build_backend:
stage: build
script:
- make build
artifacts:
paths:
- backend/dist/
test_frontend:
stage: test
needs: ["build_frontend"] # Only waits for build_frontend
script:
- npm run test:unit
test_backend:
stage: test
needs: ["build_backend"] # Only waits for build_backend
script:
- make test
```
In this example, `test_frontend` and `test_backend` can run as soon as their respective build jobs are done, potentially in parallel, even if they are in different stages according to the `stages` definition.
### 2.3. Name Jobs and Stages Clearly
Use descriptive, consistent naming conventions. This makes pipelines easier to understand and debug. A good pattern is `stage_name:job_description`.
❌ **BAD: Generic or unclear names**
```yaml
stages:
- first
- second
- third
job1:
stage: first
script:
- echo "..."
deploy:
stage: third
script:
- deploy.sh
```
✅ **GOOD: Descriptive names**
```yaml
stages:
- build
- test
- security
- deploy
build:frontend:
stage: build
script:
- npm run build
test:unit:backend:
stage: test
script:
- pytest
security:sast:
stage: security
script:
- /analyzer/sast run
deploy:production:
stage: deploy
script:
- deploy-to-prod.sh
```
## 3. Performance Considerations
Optimize your pipelines for speed to provide fast feedback loops.
### 3.1. Implement Strategic Caching
Cache dependencies (`node_modules`, `vendor/`, `pip` caches) and build artifacts that are expensive to regenerate. Use dynamic cache keys to prevent conflicts between branches.
❌ **BAD: No caching or static cache keys**
```yaml
build_job:
image: node:20-alpine
script:
- npm install # Downloads dependencies every time
- npm run build
```
✅ **GOOD: Dynamic caching**
```yaml
build_job:
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}-node-modules # Cache per branch/tag
paths:
- node_modules/
policy: pull-push # Pull existing cache, push new one
script:
- npm install
- npm run build
```
### 3.2. Use Small, Specific Docker Images
Minimize image size to reduce download times for runners. Use specific versions to ensure reproducibility.
❌ **BAD: Large or generic images**
```yaml
build_job:
image: ubuntu:latest # Very large, contains many unnecessary tools
script:
- apt-get update && apt-get install -y nodejs npm
- npm install
```
✅ **GOOD: Small, specific images**
```yaml
build_job:
image: node:20-alpine # Alpine is much smaller than Debian/Ubuntu
script:
- npm install
- npm run build
```
### 3.3. Parallelize Independent Jobs
Identify jobs that don't depend on each other and run them in parallel using `needs` or the `parallel` keyword for matrix builds.
```yaml
# Using 'parallel' for matrix builds
test:
stage: test
image: python:3.11-slim-buster
script:
- pip install -r requirements.txt
- pytest
parallel:
matrix:
- PYTHON_VERSION: ["3.9", "3.10", "3.11"]
OS: ["debian", "alpine"]
```
## 4. Common Pitfalls and Gotchas
Avoid these common mistakes to prevent pipeline failures and security vulnerabilities.
### 4.1. Never Hardcode Secrets
Secrets (API keys, passwords, tokens) must never be committed to your `.gitlab-ci.yml` or repository. Use GitLab CI/CD variables.
❌ **BAD: Hardcoded secret**
```yaml
deploy_prod:
stage: deploy
script:
- export AWS_ACCESS_KEY_ID="AKIAxxxxxxxxxxxxxx" # DANGER!
- aws s3 sync build/ s3://my-bucket
```
✅ **GOOD: Use GitLab CI/CD variables**
```yaml
deploy_prod:
stage: deploy
script:
- aws s3 sync build/ s3://my-bucket
variables:
AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID # Defined in GitLab UI as protected/masked
AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY # Defined in GitLab UI as protected/masked
```
**Action**: Define `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` as **Protected** and **Masked** variables in your project's `Settings > CI/CD > Variables`.
### 4.2. Scope Variables Appropriately
Define variables at the narrowest scope possible (job-level, then global, then project/group/instance). This improves clarity and reduces unintended side effects.
❌ **BAD: Over-scoped variables**
```yaml
variables:
DEPLOY_ENV: production # This applies to ALL jobs, even build/test
build_job:
stage: build
script:
- echo "Building for ${DEPLOY_ENV}" # Misleading
```
✅ **GOOD: Job-scoped variables**
```yaml
variables:
DEFAULT_IMAGE: alpine/git:latest # Global default, sensible
build_job:
stage: build
image: node:20-alpine # Overrides default
script:
- echo "Building..."
deploy_staging:
stage: deploy
script:
- deploy.sh $DEPLOY_ENV
variables:
DEPLOY_ENV: staging # Specific to this job
```
### 4.3. Validate Your YAML
Use GitLab's built-in CI Lint tool (available in the CI/CD editor or `https://gitlab.com/<your-group>/<your-project>/-/ci/lint`) to validate your `.gitlab-ci.yml` before committing.
## 5. Configuration Management
Manage your pipeline configuration systematically.
### 5.1. Use CI/CD Components
For highly reusable and shareable pipeline logic across multiple projects, publish CI/CD Components to the CI/CD Catalog. This is the ultimate form of reusability.
```yaml
# .gitlab-ci.yml
include:
- component: gitlab.com/my-group/my-ci-components/[email protected]
inputs:
image_name: my-app
dockerfile_path: ./Dockerfile
```
**Action**: Explore the CI/CD Catalog for existing components or create your own for common tasks.
## 6. Environment Variables
Beyond secrets, environment variables are crucial for dynamic pipeline behavior.
### 6.1. Leverage Predefined CI/CD Variables
GitLab provides many predefined variables (e.g., `$CI_COMMIT_BRANCH`, `$CI_COMMIT_SHORT_SHA`, `$CI_JOB_ID`). Use these for dynamic logic and naming.
```yaml
build_docker_image:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t my-app:$CI_COMMIT_SHORT_SHA .
- docker push my-app:$CI_COMMIT_SHORT_SHA
rules:
- if: $CI_COMMIT_BRANCH == "main"
```
## 7. Logging
Clear and secure logging is essential for debugging and auditing.
### 7.1. Keep Logs Concise and Masked
Ensure sensitive information is masked in job logs. Avoid excessive output that clutters logs.
❌ **BAD: Verbose, unmasked output**
```bash
# In your script.sh
echo "AWS_SECRET_ACCESS_KEY is: $AWS_SECRET_ACCESS_KEY" # Exposes secret!
npm install --verbose # Too much noise
```
✅ **GOOD: Masked and concise output**
```bash
# In your script.sh
# Ensure AWS_SECRET_ACCESS_KEY is configured as a masked variable in GitLab
echo "Starting deployment..."
npm install --loglevel error # Only show errors
```
## 8. Testing Approaches
Integrate comprehensive testing into your pipeline.
### 8.1. Implement Dedicated Test Stages
Always include stages for unit, integration, and end-to-end tests. Make these jobs mandatory for successful pipelines.
```yaml
stages:
- build
- test
- security
- deploy
test:unit:
stage: test
script:
- npm run test:unit
allow_failure: false # Must pass
test:integration:
stage: test
script:
- npm run test:integration
allow_failure: false # Must pass
```
### 8.2. Integrate Security Scans
Embed Static Application Security Testing (SAST), Dependency Scanning, and Container Scanning into your pipeline. These should run early to catch issues quickly.
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/Container-Scanning.gitlab-ci.yml
sast:
stage: security
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_MERGE_REQUEST_IID
allow_failure: true # Initially allow failure, enforce later
dependency_scanning:
stage: security
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_MERGE_REQUEST_IID
allow_failure: true # Initially allow failure, enforce later
container_scanning:
stage: security
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_MERGE_REQUEST_IID
allow_failure: true # Initially allow failure, enforce later
```
**Action**: Start with `allow_failure: true` for security scans, then switch to `false` once your team is ready to address findings proactively.
### 8.3. Lint Shell Scripts
If your pipeline uses shell scripts, lint them with `shellcheck` and format with `shfmt`.
```yaml
shellcheck:
image: koalaman/shellcheck-alpine:stable
stage: test
script:
- shellcheck scripts/**/*.sh
shfmt:
image: mvdan/shfmt:v3.2.0-alpine
stage: test
script:
- shfmt -i 2 -ci -d scripts # Check formatting, -d for diff only
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc