You push your code, confident that the new CI/CD pipeline will work perfectly. Two minutes later, you get that dreaded notification: "Pipeline failed." You open the logs and see a YAML parsing error. Sound familiar?
CI/CD pipelines have transformed how we ship software, but they've also introduced a new class of problems. Unlike application code that you can test locally, pipeline configurations often fail in ways that only show up when the CI runner tries to execute them. And because most CI/CD systems use YAML for configuration, even experienced developers find themselves wrestling with syntax errors.
This guide walks through the mistakes that actually happen in production environments, based on real pipeline configurations across GitHub Actions, GitLab CI, Jenkins, and other platforms. Each section includes the broken version, the fix, and why the error occurs.
Mistake 1: Incorrect String Quoting in Environment Variables
Environment variables are where many pipeline errors originate. Different CI platforms handle variable expansion differently, and YAML's own string interpretation adds another layer of complexity.
Here's a GitHub Actions workflow that looks fine but breaks:
env:
DATABASE_URL: postgres://user:password@localhost:5432/db
API_KEY: ${{ secrets.API_KEY }}
VERSION: 1.2.3-betaThe problem is subtle. The DATABASE_URL contains a colon after the protocol, which YAML interprets as the start of a nested key-value pair. The fix requires quotes:
env:
DATABASE_URL: "postgres://user:password@localhost:5432/db"
API_KEY: ${{ secrets.API_KEY }}
VERSION: "1.2.3-beta"Strings containing colons, hashes, or starting with special characters need quotes. The version string might work without quotes, but quoting it prevents issues if you later change it to a format that needs quoting.
Mistake 2: Anchor and Alias Misuse
YAML anchors and aliases let you reuse configuration blocks, which is great for keeping pipeline files maintainable. But they're easy to get wrong.
This GitLab CI configuration tries to reuse a deployment script:
deploy-staging:
stage: deploy
script: &deploy-script
- npm run build
- npm run deploy
deploy-production:
stage: deploy
script: *deploy-script
- npm run notifyThis fails because you can't add items to an aliased array. The alias replaces the entire script block, so the notify command never runs. The correct approach is:
.deploy-base: &deploy-script
- npm run build
- npm run deploy
deploy-staging:
stage: deploy
script: *deploy-script
deploy-production:
stage: deploy
script:
- *deploy-script
- npm run notifyWait, that still won't work. You can't merge sequences like this in YAML. The actual fix requires extends or a different structure:
.deploy-base:
script:
- npm run build
- npm run deploy
deploy-production:
extends: .deploy-base
script:
- npm run build
- npm run deploy
- npm run notifyOr use before_script and script separately to compose behaviors.
Mistake 3: Job Dependencies and Needs
Modern CI systems allow jobs to run in parallel with explicit dependencies. GitHub Actions uses the needs keyword, but it's easy to create circular dependencies or reference jobs that don't exist.
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: [test, integration-test]
runs-on: ubuntu-latest
steps:
- run: npm run deployThis configuration references integration-test in the needs array, but no such job exists. The pipeline fails immediately when GitHub tries to parse the workflow file. Always verify that job names in dependency lists match actual job definitions.
Mistake 4: Matrix Strategy Syntax Errors
Matrix builds let you test across multiple configurations. The syntax varies between platforms and is easy to mess up.
Here's a broken GitHub Actions matrix:
strategy:
matrix:
node-version: [14, 16, 18]
os: [ubuntu-latest, windows-latest]
include:
node-version: 20
os: ubuntu-latestThe include should be an array, not a single object:
strategy:
matrix:
node-version: [14, 16, 18]
os: [ubuntu-latest, windows-latest]
include:
- node-version: 20
os: ubuntu-latestAnother common mistake is trying to use matrix variables incorrectly in job steps. You need to reference them with the matrix context:
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}Mistake 5: Conditional Execution Syntax
Running jobs or steps conditionally based on branch names, event types, or previous step outcomes requires getting the condition syntax exactly right.
This looks logical but fails:
deploy:
if: github.ref == 'refs/heads/main' && success()
runs-on: ubuntu-latest
steps:
- run: echo "Deploying"The problem is that GitHub Actions conditions need to be wrapped in the expression syntax:
deploy:
if: ${{ github.ref == 'refs/heads/main' && success() }}
runs-on: ubuntu-latest
steps:
- run: echo "Deploying"Actually, GitHub Actions doesn't require the dollar-brace syntax for if conditions, but it does for other contexts. The inconsistency trips people up. Both versions work for if, but only the wrapped version works everywhere.
Mistake 6: Multiline Script Blocks
When your build or deployment script spans multiple lines, YAML offers several ways to format it. Choosing the wrong one causes problems.
script: - | echo "Starting build" npm run build echo "Build complete"
This fails because the pipe character creates a multiline string, but the subsequent lines aren't indented. The correct version:
script:
- |
echo "Starting build"
npm run build
echo "Build complete"Or use the greater-than character to fold lines:
script:
- >
echo "This is a very long command that spans
multiple lines but will be treated as a
single line when executed"Many developers also forget that each line in a script array is a separate command. If one fails, subsequent commands don't run unless you chain them:
script: - npm install - npm test
If npm install fails, npm test never runs. For complex sequences, use a shell script or chain with && operators.
Mistake 7: Cache Configuration Errors
Caching dependencies speeds up builds significantly, but cache configuration has its own pitfalls.
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}This looks fine but might not work as expected. If your repository has multiple package-lock.json files in different directories, the hash includes all of them. If you only care about the root one:
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}Also, forgetting the restore-keys means the cache only hits on exact matches. A near-miss provides no benefit:
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-Mistake 8: Secrets and Variable Scope
Secrets need careful handling. Using them incorrectly exposes credentials or causes pipeline failures.
env:
DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
jobs:
test:
steps:
- run: echo $DATABASE_PASSWORDThis works, but it exposes the password in logs. GitHub masks secrets, but only if you reference them correctly. Better to pass secrets directly to steps that need them:
jobs:
test:
steps:
- run: ./deploy.sh
env:
DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}This limits the scope of the secret to just that step.
Mistake 9: Path and Working Directory Issues
Steps run in a default working directory, but sometimes you need to run commands from a specific subdirectory.
- run: npm install working-directory: ./frontend - run: npm test
The test command runs in the default directory, not in ./frontend. Each step's working-directory is independent:
- run: npm install working-directory: ./frontend - run: npm test working-directory: ./frontend
Or set a default for all steps in a job:
defaults:
run:
working-directory: ./frontend
steps:
- run: npm install
- run: npm testValidating Your Pipeline Configuration
Most platforms provide tools to validate YAML before committing. GitHub CLI can validate Actions workflows locally. GitLab has a CI lint tool in the web interface. Jenkins offers pipeline syntax validators.
For general YAML syntax validation, use a YAML validator to catch structural errors before pushing. This won't catch platform-specific issues, but it prevents basic syntax mistakes from breaking your pipeline.
The best validation is running your pipeline on a feature branch before merging to main. Create a test workflow that triggers on any branch, work out the kinks, then gate your production pipeline to only run on main or release branches.
Debugging Failed Pipelines
When a pipeline fails with a YAML error, the error message usually includes a line number. Start there, but remember that YAML parsers sometimes report the error at the point where the consequence appears, not where the actual mistake is.
Use an online YAML parser to validate your configuration outside the CI environment. Paste your workflow file into a validator and see if it identifies the issue. Many YAML errors are easier to spot when the parser highlights them in a different interface.
For platform-specific issues, the documentation for GitHub Actions, GitLab CI, or your chosen platform often includes troubleshooting guides for common errors. Search for the exact error message you're seeing.
Building Better Pipelines
The best way to avoid YAML syntax errors is to build your pipelines incrementally. Start with a minimal working pipeline, then add features one at a time. After each addition, push and verify the pipeline runs.
Use your platform's reusable workflows or shared libraries. GitHub has reusable workflows, GitLab has includes and extends, Jenkins has shared libraries. These reduce duplication and centralize configuration, making errors less likely.
Comment your pipeline files, especially complex conditionals or matrix strategies. Your future self will appreciate understanding why a particular configuration exists.
Finally, keep your pipeline configurations in version control alongside your code. This seems obvious, but it means you can review changes, revert problematic updates, and see the evolution of your CI/CD setup over time.