YAML in GitHub Actions Workflows
Every GitHub Actions workflow lives in .github/workflows/ as a YAML file, and once you've read one carefully, the rest start looking pretty similar. Here's a small CI workflow, explained piece by piece.
name: Run tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
The three top-level pieces
name is just the label GitHub shows in the Actions tab. on defines what triggers the workflow — here, a push to main or any pull request. jobs is where the actual work is defined, and each key underneath it (like test) is one independent job that can run on its own machine.
Steps run in order, top to bottom
steps is a list, and each item runs one after another on the same runner. uses: pulls in a pre-built action (here, checking out your repo's code), while run: executes a literal shell command. A step can have a name too, which is just what shows in the logs — it's optional, but worth adding once a workflow has more than two or three steps.
Where people get stuck
- Multiple triggers under
on. Forgetting thatpushtakes a nestedbrancheslist, rather than a flat value, is a common first mistake. - Multi-line
runcommands. Running several shell commands in one step needs YAML's block scalar syntax (a pipe|right afterrun:) — plain multi-line text without it won't behave the way you expect. - Secrets referenced wrong.
${{ secrets.MY_TOKEN }}needs to be inside quotes in some contexts to avoid YAML trying to interpret the curly braces itself.
Because a broken workflow file often just silently fails to trigger rather than showing a clear error in the GitHub UI, it's worth validating it locally first with our YAML validator before pushing.