Code Beautifier
DevOps & Config

GitHub Actions YAML: a validation checklist

The mistakes that only surface after a push: indentation, quoted booleans, cron in UTC, expression syntax, and unpinned actions. Catch them before CI does.

A workflow file has an unusual feedback loop: you cannot run it locally, so every mistake costs a commit, a push, a queue wait, and a red X. Most of those mistakes are visible in the file before the push. This is the list we check, in the order the failures usually appear.

1. It is valid YAML

Everything else depends on this. The two classic failures are a single misaligned key — one space too many under steps: — and a tab character, which YAML forbids for indentation and which most editors insert silently. Both produce bad indentation of a mapping entry or tab characters must not be used in indentation, and both are found in seconds by a validator.

Paste the file into YAML Validator before every push. It is faster than the round trip and it reports the line.

2. Booleans and versions are quoted

YAML 1.1, which the Actions parser follows in the ways that matter, reads on, off, yes, and no as booleans and reads 3.10 as the number 3.1. The first bites the on: key itself — which works, because GitHub special-cases it — and any with: input that is meant to be the string "yes". The second bites language version matrices:

strategy:
  matrix:
    python-version: ["3.9", "3.10", "3.11"]

Without the quotes, 3.10 becomes 3.1, the setup action installs Python 3.1, and the failure message mentions a version you never wrote.

3. The cron schedule means what you think

schedule: uses standard five-field cron, evaluated in UTC, with the additional rule that GitHub runs scheduled jobs on a best-effort basis and may delay them under load. Two consequences: a schedule written for 9 a.m. local time will fire at the wrong hour for most of the world, and a * * * * * schedule will not run every minute and will get the workflow throttled.

The day-of-week and day-of-month fields are ORed when both are set, which surprises everyone once. Paste the expression into Cron Parser to read it back as a sentence, and keep the cron syntax cheat sheet open for the field ranges.

4. Expressions are inside ${{ }} — and only where needed

if: github.ref == 'refs/heads/main' works without the braces because if: is always an expression. run: echo ${{ secrets.TOKEN }} needs them because run: is a string. Mixing the two — if: ${{ ... }} is harmless, run: github.ref is a literal — is the source of a large share of "it ran but did nothing" workflows.

Two more rules inside the braces: single quotes for strings ('main', not "main"), and == for comparison. Double quotes inside an expression produce a syntax error that is only reported when the job starts.

5. Secrets never reach a log

Values from secrets.* are masked in logs, but only when they appear verbatim. A secret that is base64-encoded, split, or interpolated into a URL is not recognised by the masker and prints in full. Two habits prevent it: pass secrets as environment variables (env: TOKEN: ${{ secrets.TOKEN }}) rather than into run: strings, and never echo an environment variable that came from a secret, even for debugging.

GITHUB_TOKEN deserves a permissions: block that grants only what the job uses. The default is broader than most jobs need.

6. Actions are pinned

uses: actions/checkout@v4 floats: the v4 tag moves whenever a new v4.x is published, and a compromised or simply broken release reaches your pipeline automatically. Pin to a full commit SHA with the version in a comment:

- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Dependabot can keep the SHA current. A tag cannot be trusted to point where it pointed yesterday.

7. The shape matches what GitHub expects

Beyond syntax, the file has to satisfy the workflow schema: every job needs runs-on, every step needs uses or run but not both, needs: must name jobs that exist, and on: must list events GitHub recognises. GitHub Actions Validator checks the structure against that schema and reports the path of the problem, which is the same information the "workflow file is invalid" banner gives you — except before the push.

A minimal checklist to keep in the PR template

  • Valid YAML, no tabs, consistent indentation
  • Versions and yes/no/on/off values quoted
  • Cron read back as a sentence; UTC understood
  • ${{ }} only in string contexts; single quotes inside
  • Secrets passed via env:, never echoed; permissions: set
  • Every uses: pinned to a SHA
  • Structure validated against the schema

Seven items, two minutes, one fewer red X. The Git commands for the resulting branch and PR are on the Git cheat sheet.

Cron Syntax & Schedule Reference

A complete cheat sheet for cron time fields, special characters (*, /, -, ,), and common schedule expressions.

Open cheat sheet

Git Commands & Workflow Reference

A fast reference guide for daily Git commands: branch management, interactive rebasing, stashing, and safe commit recovery.

Open cheat sheet