Skip to main content
rule

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.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/gitlab-ci.mdc
Open on GitHub

What 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.yml with include
  • reuse job settings through extends and YAML anchors
  • replace deprecated only/except conditions with rules
  • declare build-to-test links with needs
  • fix YAML parsing problems caused by indentation

Example prompts for the agent

  • Review .gitlab-ci.yml and split its stages and jobs into focused files with include.
  • Replace the deploy conditions in .gitlab-ci.yml with rules instead of only/except.
  • Add needs so 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.mdc

Frontmatter: description, globs, alwaysApply

Step 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

Similar IDE rules

All IDE rules for DevOps and infrastructure →

From the same set240

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor