GitLab CI Error Guide: 'found duplicate key' — Fix Repeated YAML Keys
Fix 'found duplicate key' in GitLab CI: a mapping defines the same key twice, so YAML drops one. Find the repeated key, merge the blocks, and re-lint the file.
- #gitlab
- #ci-cd
- #troubleshooting
- #errors
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’s YAML parser rejects .gitlab-ci.yml when the same key appears twice inside one mapping. The pipeline fails to parse and reports the offending key and line:
Found errors in your .gitlab-ci.yml:
(<unknown>): found duplicate key "script" with value "..." (original value: "...") at line 14 column 3
Some tooling downgrades this to a warning and silently keeps only the last occurrence — which is worse, because the pipeline “works” while ignoring half your config. Either way, a duplicate key means one of your definitions is being dropped.
Symptoms
- The pipeline fails at parse time with
found duplicate key "<name>"and a line/column. - A key you set (a
variablesentry, ascript, arulesblock) appears to have no effect — the second definition overwrote the first. - A job runs only the last
script:you wrote, ignoring an earlier one in the same job. - Merging two snippets into one job produced two
before_script:/cache:/rules:keys. - CI Lint flags the duplicate even though each line is individually valid.
Common Root Causes
- Copy-paste merge — combining two job snippets left two
script:(orvariables:,rules:,cache:) keys in one mapping. - Repeated variable name — the same key defined twice under
variables:; only the last value survives. - Two
default:/global blocks — a second top-leveldefault:orvariables:mapping. - extends + inline collision authored as literal duplicate — writing the key twice instead of relying on override semantics.
- Anchor plus explicit key — a merged anchor (
<<: *base) and an explicit key that the author also duplicated literally. - Manual indentation edit — a block accidentally split so a key is re-declared at the same level.
Diagnostic Workflow
Reproduce the exact parser error locally with any strict YAML loader — GitLab uses strict duplicate-key detection:
# Validate with CI Lint (reports the duplicate key + line)
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)"
Or catch it in a pre-commit check with a strict parser that fails on duplicates:
yaml-strict:
image: python:3.12
script:
- pip install "ruamel.yaml"
- |
python - <<'PY'
from ruamel.yaml import YAML
YAML(typ="safe").load(open(".gitlab-ci.yml")) # raises on duplicate keys
print("no duplicate keys")
PY
Then locate the repeated key and decide whether to merge the two blocks or rename one:
# BEFORE — duplicate 'script' in one job; the first is silently dropped
build:
script:
- make deps
script: # <-- duplicate key
- make build
# AFTER — one script listing both commands in order
build:
script:
- make deps
- make build
Example Root Cause Analysis
A developer added caching to an existing job by pasting a snippet from documentation. The job ended up with two variables: mappings:
test:
variables:
DATABASE_URL: "postgres://ci@db/test"
variables: # <-- second 'variables' key
RAILS_ENV: "test"
script:
- bundle exec rspec
CI Lint reported found duplicate key "variables". On a lenient parser the job had actually been running with only the second block — so DATABASE_URL was silently missing and the test suite had been connecting to the wrong database, a bug that had confused the team for days. The fix merged both entries into a single mapping:
test:
variables:
DATABASE_URL: "postgres://ci@db/test"
RAILS_ENV: "test"
script:
- bundle exec rspec
With one variables: mapping, both values applied and the suite connected correctly. The team added the strict-YAML pre-commit check so a future duplicate key fails locally instead of silently dropping config.
Prevention Best Practices
- Keep exactly one of each mapping key per job/scope — one
script:, onevariables:, onerules:, onecache:; list multiple items as sequence entries under that single key. - Run CI Lint or a strict YAML loader (ruamel/yamllint with
duplicate-keysenabled) in a pre-merge check so duplicates fail fast. - When combining snippets, merge into the existing key rather than pasting a second one.
- Use
extends:/anchors for shared config instead of literally repeating keys; understand override semantics rather than duplicating. - Enable
yamllintin your editor to highlight duplicate keys as you type. - Review indentation after manual edits so a block isn’t accidentally re-declared at the same level.
Quick Command Reference
# yamllint with duplicate-key detection
yamllint -d "{rules: {key-duplicates: enable}}" .gitlab-ci.yml
# Strict parse that raises on duplicates
python -c "from ruamel.yaml import YAML; YAML(typ='safe').load(open('.gitlab-ci.yml')); print('ok')"
# Validate via GitLab CI Lint
glab ci lint .gitlab-ci.yml
# Quickly spot repeated top-level-ish keys in one job (manual scan)
grep -nE '^\s{2,4}(script|variables|rules|cache|before_script):' .gitlab-ci.yml
Conclusion
found duplicate key means one mapping defines the same key twice, so YAML keeps only the last and drops the rest — sometimes failing the pipeline, sometimes silently discarding half your config. Find the repeated key with CI Lint or a strict YAML parser, merge the two blocks into a single key with a proper sequence, and add a duplicate-key linter to your pre-merge checks so the next accidental repeat fails loudly instead of quietly changing behavior.
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.