Skip to content
CloudOps
All prompts
AI for GitLab CI/CD Difficulty: Intermediate ClaudeChatGPT

GitLab CI/CD `rules:` Debugging Prompt

Diagnose why a GitLab job did or didn't run — decode `rules:` evaluation, `only/except` legacy syntax, workflow rules, and complex `$CI_*` variable conditions.

Target user
DevOps engineers debugging job triggering in GitLab pipelines
Difficulty
Intermediate
Tools
Claude, ChatGPT

The prompt

You are a senior DevOps engineer with deep experience reading GitLab `rules:` blocks. You know that GitLab evaluates rules top-to-bottom and the FIRST match wins — and that this is the single most common source of "why didn't my job run?" confusion.

I will provide:
- The job(s) in question and whether they "should have run" or "shouldn't have run"
- The full `.gitlab-ci.yml` `workflow:` block (if present)
- The job's `rules:` (or `only:`/`except:`)
- The pipeline source (push, merge_request_event, schedule, web, api, trigger, parent_pipeline)
- The branch name, commit SHA, MR ID (if any)
- The values of any `$CI_*` variables the rules reference
- The actual pipeline outcome: pipeline created? job present? job skipped? job manual? job allow_failure?

Your job:

1. **Resolve the pipeline source** first. `rules:` interact with `$CI_PIPELINE_SOURCE`, which can be:
   - `push` (default branch push, regular branch push)
   - `merge_request_event` (only when MR pipelines are enabled and an MR exists)
   - `schedule` (pipeline schedules)
   - `web` (manually run from UI)
   - `api`, `trigger`, `pipeline` (parent), `chat`, `external_pull_request_event`
   - **Push pipelines run by default; MR pipelines only run when configured.** Many "why two pipelines?" questions are detached MR + branch pipelines.
2. **Evaluate workflow rules** (if any):
   - `workflow:rules:` decide whether the WHOLE pipeline runs
   - If no `workflow:rules:` rule matches AND there's a `workflow:`, the pipeline is rejected entirely
3. **For each job's `rules:` block**, evaluate top-to-bottom:
   - First match wins. Subsequent rules are ignored.
   - A rule with `if:` matching but no `when:` defaults to `when: on_success`
   - `when: never` rule that matches skips the job
   - `when: manual` rule that matches adds the job but as manual
   - If no rule matches, the job is **excluded from the pipeline entirely**
4. **Common rule traps to surface**:
   - **`rules: -if: '$CI_PIPELINE_SOURCE == "merge_request_event"' -if: '$CI_COMMIT_BRANCH'`** runs BOTH on MR AND on branch pushes — duplicate pipelines, common confusion
   - **`only:` and `rules:` in the same job** → `rules:` wins; `only:` ignored
   - **`changes:`** without a parent rule context → applies to the rule only it's nested under
   - **`exists:`** evaluates against the FIRST matching commit's tree, may differ from working dir
   - **Variable expansion in `if:`** uses `$VAR` notation; comparisons need quoting (`'$VAR == "foo"'`)
   - **`$CI_COMMIT_BRANCH` is empty on tag pipelines AND on MR pipelines** — use `$CI_COMMIT_REF_NAME` if you need both
   - **MR pipelines need `merge_request_event` source** — branch-name rules like `if: $CI_COMMIT_BRANCH == "main"` never match on MR pipelines
5. **For `workflow:rules:` debugging**:
   - Default behavior (no workflow) creates pipelines on every push and on MR events if `Pipelines for merge requests` is enabled in project settings
   - A `workflow:rules:` block REPLACES the default; you need to explicitly allow what you want
   - Common pattern: prevent duplicate detached/branch pipelines:
     ```yaml
     workflow:
       rules:
         - if: $CI_PIPELINE_SOURCE == "merge_request_event"
         - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
           when: never
         - if: $CI_COMMIT_BRANCH
     ```
6. **Distinguish "job didn't run" from "job ran but no work happened"**:
   - Not in pipeline at all: rules excluded it
   - In pipeline as "skipped": rule matched with `when: never`
   - In pipeline as "manual": rule matched with `when: manual`
   - Ran, succeeded with no output: script issue, not rules
7. **Provide the FIRST rule that matches** in your output — explain why it matched and why subsequent rules don't apply.

Be exact about which `$CI_*` variable matters; many users mistype them.

---

Question: [should-have-run / should-NOT-have-run]
Pipeline context:
  - Source: [push / merge_request_event / schedule / web / api / trigger]
  - Branch / Tag / MR: [DESCRIBE]
  - Project: [DESCRIBE]
Job(s) involved: [name]
`workflow:rules:` block (if present):
```yaml
[PASTE]
```
Job's `rules:` (or `only:`/`except:`):
```yaml
[PASTE]
```
Relevant `$CI_*` variable values at evaluation time:
```
[PASTE]
```
Pipeline outcome (UI screenshot text):
[DESCRIBE]

Why this prompt works

rules: evaluation is deterministic but unintuitive — first match wins, falling-off-the-end excludes the job, and the variables available change based on pipeline source. Most “my job didn’t run” debugging conversations boil down to: which rule matched first, and why. This prompt forces an explicit walkthrough.

How to use it

  1. Always include the pipeline source. A rule that works on push may not work on MR.
  2. Capture the $CI_* variable values from the actual pipeline run. GitLab UI: pipeline → job → “Variables” tab.
  3. Include workflow:rules: even if you don’t think it’s relevant. If workflow rules reject the pipeline, no job rules matter.
  4. For MR pipelines specifically: confirm “Pipelines for merge requests” is enabled in the project’s CI/CD settings.

Useful commands

# Get the variables for a pipeline run (admin or maintainer)
curl -s --header "PRIVATE-TOKEN: <token>" \
  "https://gitlab.example.com/api/v4/projects/<id>/pipelines/<pid>/variables" | jq

# Get pipeline details (source, ref, etc.)
curl -s --header "PRIVATE-TOKEN: <token>" \
  "https://gitlab.example.com/api/v4/projects/<id>/pipelines/<pid>" | jq

# Get all jobs in a pipeline (see what was included/excluded)
curl -s --header "PRIVATE-TOKEN: <token>" \
  "https://gitlab.example.com/api/v4/projects/<id>/pipelines/<pid>/jobs" | \
  jq -r '.[] | "\(.status)\t\(.name)\t\(.stage)"' | column -t

# Lint the yaml (locally)
# In project: CI/CD → Pipeline editor → "Validate" tab
curl -s --header "PRIVATE-TOKEN: <token>" --header "Content-Type: application/json" \
  --data "{\"content\": $(cat .gitlab-ci.yml | jq -Rs)}" \
  "https://gitlab.example.com/api/v4/ci/lint" | jq

# Get the merged YAML (after `include:` resolution) for debugging
# Project → CI/CD → Pipeline editor → "Full configuration" tab

Predefined variable cheatsheet (most-misused)

VariableSet onEmpty on
$CI_COMMIT_BRANCHBranch pipelinesMR pipelines, tag pipelines
$CI_MERGE_REQUEST_IIDMR pipelinesBranch/tag pipelines
$CI_COMMIT_TAGTag pipelinesBranch pipelines, MR pipelines
$CI_COMMIT_REF_NAMEAll — branch or tag nameDetached pipelines (rare)
$CI_PIPELINE_SOURCEAllNever (always set)
$CI_DEFAULT_BRANCHAll — name of default branchNever
$CI_OPEN_MERGE_REQUESTSWhen branch has open MR(s)When branch has no open MR

Common rule patterns

Run on default branch only

deploy-prod:
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Run on MR only

test:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Run on MR OR on default branch — but never both

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never            # skip branch pipeline if MR exists
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Run only on tag pipelines, with manual approval

release:
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual

Run only when frontend changes

build-frontend:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - frontend/**/*
        - package.json

Skip on docs-only commits

workflow:
  rules:
    - if: $CI_COMMIT_MESSAGE =~ /\[skip ci\]/i
      when: never
    - when: always

Common findings this catches

  • Job has only: main AND rules:rules: wins; only: is silently ignored. Remove only:.
  • rules: - if: $CI_COMMIT_BRANCH == "main" but pipeline is from MR event → $CI_COMMIT_BRANCH is empty on MR pipelines. Use $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main".
  • workflow:rules: excludes branch pipelines, then someone wonders why pushing to a branch doesn’t run anything → workflow rules need explicit allow for branches.
  • Two pipelines per push when MR is open → standard duplicate-pipeline situation. Apply the workflow rule above.
  • rules:changes: on first commit of a branch → first commit “changes” everything; rule always matches. Use compare_to: to compare against default branch.
  • when: manual rule matches but pipeline shows the job as auto-running → there’s an earlier rule (above in the list) that matched first with default when: on_success. First match wins.

Migration: only/exceptrules:

# OLD
only:
  - main
  - tags
except:
  - schedules
# NEW
rules:
  - if: $CI_PIPELINE_SOURCE == "schedule"
    when: never
  - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  - if: $CI_COMMIT_TAG

When to escalate

  • Behavior that contradicts the rules-as-written → likely a GitLab version-specific change; check release notes (or your GitLab version’s CHANGELOG).
  • “Pipelines for merge requests” toggle disabled at project level — administrative; rules referencing merge_request_event won’t ever match.
  • Custom workflow with includes from a remote template — confirm the merged YAML, not just your local file.

Related prompts

Newsletter

Get weekly AI workflows for DevOps engineers

Practical prompts, automation ideas, and tool reviews for infrastructure engineers. One email per week. No spam.