How to Fix GitHub Actions 'Unexpected value uses' Error on Line 25 (Only run or shell Allowed)
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: A
uses:key was nested inside arun:step block (or placed at the wrong indentation level), which is an illegal YAML structure in GitHub Actions —usesandrunare mutually exclusive step-level keys, not nestable properties. - How to fix it: Split the offending block into two separate
stepsentries: one- uses:step and one- run:step, each at the correct sibling indentation understeps:. - Fast path: Drop your full
main.ymlinto our Client-Side Sandbox above to auto-refactor this without leaking your repo secrets anywhere.
The Incident (What Does the Error Mean?)
Raw error output:
Workflow run failed: .github/workflows/main.yml
(Line: 25, Col: 14): Unexpected value 'uses'.
Only 'run' or 'shell' are allowed in this context.
GitHub Actions' workflow parser hit a uses: key at column 14 of line 25 — a position where the schema only permits run: or shell: (i.e., properties that belong inside a run-type step). The runner never starts. Every job in this workflow is dead on arrival. Any PR merge gates, deployment pipelines, or scheduled jobs wired to this workflow are fully blocked.
The Attack Vector / Blast Radius
This is a schema-level hard stop — not a warning, not a skipped step. The entire workflow file is rejected at parse time.
Cascading failure chain:
- All jobs in the workflow file fail immediately — not just the job containing line 25.
- Branch protection rules break — if this workflow is a required status check, every open PR is now unmergeable.
- Deployment pipelines stall — any
on: pushoron: releasetrigger tied to this file produces zero deployments. - Silent regression window — if this was introduced in a refactor commit, the team may not notice until a hotfix deploy is attempted under pressure.
The root structural misunderstanding: uses and run are mutually exclusive top-level keys of a single step object. A step either calls a pre-built Action (uses) or executes a shell command (run). You cannot mix them in one step, and you cannot nest uses as a sub-key of a run step.
How to Fix It (The Solution)
Basic Fix
The most common trigger for this error is indentation drift — someone added uses: one level too deep, making the parser interpret it as a property of the preceding run: step.
Broken pattern (what the parser sees):
steps:
- name: Install dependencies
- run: npm ci
- uses: actions/setup-node@v4 # WRONG: 'uses' nested under 'run' step
- with:
- node-version: '20'
Fixed pattern:
steps:
+ - name: Setup Node
+ uses: actions/setup-node@v4 # Correct: standalone step
+ with:
+ node-version: '20'
+ - name: Install dependencies
+ run: npm ci # Correct: separate step
Key rule: Every - under steps: is a new step object. uses: and run: must each be the primary key of their own - block, never nested inside each other.
Enterprise Best Practice
In larger monorepos with multiple contributors, this class of error is best caught before it ever reaches the runner.
# .github/workflows/main.yml — Corrected, production-grade structure
steps:
- - name: Checkout and build
- run: git checkout ${{ github.sha }}
- uses: actions/checkout@v4 # ILLEGAL: mixed step keys
- with:
- fetch-depth: 0
+ - name: Checkout repository
+ uses: actions/checkout@v4 # Step 1: Action-based step
+ with:
+ fetch-depth: 0
+ - name: Run build
+ run: make build # Step 2: Shell-based step
+ shell: bash
Additional hardening: Pin all uses: references to a full commit SHA, not a mutable tag, to prevent supply-chain injection:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
Never let a YAML schema violation reach the GitHub runner. Catch it in the developer's editor or in a pre-merge gate.
1. Local validation with actionlint (fastest feedback loop):
# Install
brew install actionlint # macOS
# or
curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash
# Run against all workflow files
actionlint .github/workflows/*.yml
actionlint catches uses/run co-location errors, expression syntax bugs, and invalid context references — all statically, before a single runner spins up.
2. VS Code schema binding (catch it while typing):
// .vscode/settings.json
{
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml"
}
}
This gives you red-squiggle validation on uses/run conflicts inline.
3. Pre-commit hook:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/rhysd/actionlint
rev: v1.7.3
hooks:
- id: actionlint
4. Required status check gate:
Add a separate lightweight lint-workflows job that runs actionlint on PRs. Set it as a required status check in branch protection. A broken workflow YAML can never merge if the linter job must pass first.