GitHub Actions: Lint and Fix Your Workflow YAML

GitHub Actions: Lint and Fix Your Workflow YAML

Broken YAML, missing `on` or `jobs`, empty steps: structurally validate workflows before you push — and keep actionlint in CI.

27.08.2026
7 min read
Share this article:
GitHub Actions
CI/CD
DevOps
Validate
Formatter
Workflow
Tutorial

Why check a workflow before you merge to main?

A .github/workflows/*.yml file that does not parse, or a job with no runs-on, often shows up only after you push: red check, GitHub logs, blocked review. FastMinify does not ship actionlint. The GitHub Actions validator runs structural checks in the browser — on, jobs, steps with run or uses — and the GitHub Actions formatter reindents YAML (2-space convention). The full cluster lives on the CI/CD tools hub. To automate JS/CSS minification in the pipeline, see the CI/CD minification guide.

Catch invalid YAML, missing on/jobs, or an empty step before you push
Format 2-space indent for a readable review
100% in the browser — the workflow is never uploaded
512 KiB UTF-8 cap, same as other DevOps tools on the site
Leave actionlint (${{ }} expressions) and action pinning to CI

Workflow anatomy: what FastMinify checks (and what it does not)

The minimum shape of a GitHub Actions workflow

A workflow is a single YAML document. GitHub expects an on trigger and a jobs mapping. Each job needs a runner (runs-on), a reusable workflow (uses), or a steps list. Each step must define run or uses.

One YAML document — multi-document files (repeated ---) are rejected
The root must be a mapping, not a scalar or a list
Reusable jobs: job-level uses without steps is accepted; nested YAML is not deeply inspected
An empty jobs map or empty steps list yields a warning, not always a blocking error
The results panel counts jobs, steps, and on triggers after a successful parse
Format, validate, actionlint: three layers, not one button

FastMinify exposes two tools. Neither is actionlint. Mixing the three up produces workflows that look “valid” and still fail in CI.

Before

name: CI on: push jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test

After

name: CI on: push jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test
Format: js-yaml round-trip, 2-space indent (GitHub convention) — see format-github-actions
Validate: YAML parse plus on / jobs / step shape — see validate-github-actions
actionlint: ${{ }} expressions, permissions, IDs, action lookup — run in CI, not in FastMinify
YAML comments are dropped on format (same limit as beautify-yaml) — keep a copy if you rely on them
A “structure OK” verdict does not prove actions/checkout@v4 exists or that an expression is safe
${{ }} expressions and injection: out of browser scope

Interpolating untrusted context (github.event.issue.title, a PR label) straight into run: is a classic script-injection pattern. FastMinify does not evaluate expressions and does not flag that motif.

Pass untrusted values through env:, not by interpolating them into the script
Keep permissions: at the minimum (contents, pull-requests)
Pin action SHAs or reviewed tags instead of an unreviewed floating ref
actionlint and a human review remain the net for this risk
The FastMinify validator accepts a step that has run or uses — even if the script is unsafe

Validate, then format: a concrete workflow

Using the GitHub Actions validator

Open the GitHub Actions validator, paste a single workflow file (max 512 KiB) and wait for debounce (~300 ms). The panel shows a verdict, issues with a line hint on parse errors, and job/step/trigger counts. Empty input stays idle — not “invalid”.

Checks: on, jobs, runs-on or uses or steps, each step with run or uses
No marketplace lookup, no ${{ }} evaluation, no action pinning
Reusable jobs (job-level uses) accepted without steps
Everything stays local — no account, no upload to a FastMinify server
Format YAML before the pull request

The GitHub Actions formatter pretty-prints via js-yaml. Paste, upload, or sample: auto-format; after a manual edit, use the Format button (⌘↵). UI indent follows the 2-space convention. This is not a semantic rewriter.

Clean up a gist or a poorly indented docs example
# comments disappear on the round-trip — copy them first if you need them
Chain format → validate for a readable review and a sound structure
For YAML outside Actions, use beautify-yaml instead of this tool
Scenario — a step with neither run nor uses

A teammate added - name: empty step with no script. CI fails late, with a GitHub message that is hard to read in the PR overlay.

1

Step 1: paste the workflow into validate-github-actions

Open validate-github-actions. An invalid verdict such as “step needs run or uses” points at the empty step.

2

Step 2: add run or uses

Replace the step with uses: actions/checkout@v4 or a real run:. Re-paste until the panel shows a valid structure.

3

Step 3: format and open the PR

Send the fixed YAML through format-github-actions, copy, commit. Reviewers read indented jobs, not a blob.

Scenario — workflow pasted from a ticket, broken indent

An example copied from GitHub docs or a gist arrives with mixed spaces. You can no longer see where a job ends.

1

Step 1: format first if you cannot even read the file

Paste into format-github-actions. This step does not prove the workflow is structurally complete.

2

Step 2: validate the indented document

Copy the output into the validator. Check on, jobs, and that every step has an action.

3

Step 3: keep actionlint for CI

Once the shape is OK, expressions / IDs / marketplace stay in the pipeline — FastMinify does not replace that gate.

Keep actionlint (and minify) in CI

Example actionlint job — pin it in your repository

The browser is the fast loop before git push. Teams still keep actionlint as a merge gate for ${{ }} expressions and rules FastMinify does not implement. The snippet follows the official actionlint download script: pin a version in your repo rather than a floating main. To minify JS/CSS in the same pipeline, see the CI/CD minification guide.

Basic example

name: Lint GitHub Actions workflows on: pull_request: paths: - '.github/workflows/**' jobs: actionlint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check workflow files run: | bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) ./actionlint -color shell: bash
GitLab CI and the rest of the hub

If the repo is on GitLab, the counterparts are format-gitlab-ci and validate-gitlab-ci — same idea (YAML plus structure, not a runner). Dockerfile and Compose stay on the DevOps hub: lint a Dockerfile, validate Compose and .env.

Conclusion

Paste the workflow, fix the structure (on, jobs, steps), reindent, then push. FastMinify runs that loop locally and never uploads the YAML. It is not actionlint: expressions, advanced permissions, and whether an action exists stay in CI and review. For generic YAML, stay on beautify-yaml; for GitLab, on the same CI/CD hub.

Validate structure before every PR that touches .github/workflows
Format for review, but expect YAML comments to disappear
Do not treat a FastMinify verdict as an actionlint green light
Keep ${{ }} injection and action pinning in review plus CI
Chain validate → format, then actionlint on merge
Share this article
Share this article: