Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for GitLab CI/CD By James Joyner IV · · 8 min read

GitLab CI Error Guide: 'may not be used with rules' — Fix only/except Conflict

Quick answer

Fix 'may not be used with rules' in GitLab CI: you mixed rules with only/except in one job. Remove the legacy keys and express all conditions in rules.

  • #gitlab
  • #ci-cd
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

GitLab refuses to create a pipeline when a single job combines the modern rules: keyword with the legacy only:/except: keywords. The two systems are mutually exclusive per job, and the parser rejects the whole config:

Found errors in your .gitlab-ci.yml:
  jobs:test config key may not be used with `rules`: only

The except variant reads the same way (... may not be used with 'rules': except). No pipeline runs until the conflict is removed — this is a validation failure, not a runtime one.

Symptoms

  • The pipeline fails to start with a config error naming a job and the offending legacy key.
  • A job has both a rules: block and an only: or except: block, often after a partial migration.
  • The error appears right after someone added rules: to a job that already used only:.
  • An extends: base template supplies only:/except: while the child job adds rules: (or vice versa), colliding after merge.
  • CI Lint flags the job even though each block is individually valid YAML.

Common Root Causes

  • Partial migration — a job was moved to rules: but its old only:/except: lines were left behind.
  • Inheritance collision — a template referenced via extends: or include: defines only:/except:, and the job adds rules:; the merged job has both.
  • Copy-paste from mixed examples — snippets combining old and new syntax pasted into one job.
  • Default only: from workflow confusion — assuming workflow:rules and job-level only: can coexist per job (they cannot alongside job rules:).
  • Anchor reuse — a YAML anchor carrying only: merged into a job that also sets rules:.

Diagnostic Workflow

Find every job that still uses the legacy keys so you can migrate them together:

# Grep for legacy keys alongside rules to spot collisions
# grep -nE '^\s*(only|except|rules):' .gitlab-ci.yml

Validate the merged configuration (expands extends/include) so inherited only: is visible:

lint:
  image: curlimages/curl:latest
  script:
    - |
      curl --silent --header "PRIVATE-TOKEN: $LINT_TOKEN" \
        "https://$CI_SERVER_HOST/api/v4/projects/$CI_PROJECT_ID/ci/lint" \
        --data-urlencode "content=$(cat .gitlab-ci.yml)" \
        --data "include_merged_yaml=true"

Then rewrite the job so ALL conditions live in rules: and no only:/except: remains:

# BEFORE — illegal: rules + only in one job
test:
  only: [merge_requests]
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
  script: ./test.sh

# AFTER — everything expressed in rules
test:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
  script: ./test.sh

Confirm with CI Lint that the job now parses before pushing.

Example Root Cause Analysis

A project used a shared template for test jobs:

# ci/templates.yml
.test-base:
  only:
    - merge_requests
    - main

A developer added path filtering to one job using the modern syntax:

include:
  - local: ci/templates.yml

unit-test:
  extends: .test-base       # brings in `only:`
  rules:                    # adds `rules:` — now the merged job has BOTH
    - changes: ["src/**/*"]
  script: ./unit.sh

After merge, unit-test carried both only: (from the base) and rules: (from the child), producing jobs:unit-test config key may not be used with 'rules': only. The fix was to migrate the base template fully to rules: so nothing supplied only: anymore:

# ci/templates.yml
.test-base:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
unit-test:
  extends: .test-base
  rules:                    # child rules OVERRIDE base rules entirely
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes: ["src/**/*"]
  script: ./unit.sh

Because child rules: fully replace inherited rules:, the team re-stated the branch condition in the child. The pipeline then parsed and ran with the intended path filter.

Prevention Best Practices

  • Migrate whole templates — never a single job — from only:/except: to rules:, so inheritance can’t recombine them.
  • Standardize on rules: project-wide; only:/except: are legacy and should not appear in new jobs.
  • Run CI Lint with merged YAML in a pre-merge check to catch inherited-key collisions before the default branch.
  • Remember child rules: fully override inherited rules: — re-state every condition the child still needs.
  • Keep condition logic in workflow:rules (pipeline-level) and job rules: (job-level); don’t reintroduce job only:.
  • Grep for only:/except: periodically and treat any remaining usage as tech debt to convert.

Quick Command Reference

# Find legacy keys and rules collisions
grep -nE '^\s*(only|except|rules):' .gitlab-ci.yml ci/*.yml

# Validate merged config (shows inherited only/except)
glab ci lint .gitlab-ci.yml

# CI Lint API with include expansion
curl --header "PRIVATE-TOKEN: $TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/ci/lint" \
  --data-urlencode "content=$(cat .gitlab-ci.yml)" \
  --data "include_merged_yaml=true"

Conclusion

may not be used with rules: only (or except) means one job ended up with both the modern rules: and a legacy only:/except:, usually because a migration was partial or a template supplied the legacy key through extends:. The reliable fix is to move all condition logic into rules: at the template level, remembering that child rules: fully override inherited ones. Guard it with a merged-YAML CI Lint check so inheritance never quietly recombines the two systems again.

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.