Convention
One workflow file per concern, named lowercase by what runs (never "CI" — once there's more than one workflow, everything is CI and the label carries zero information):
.github/workflows/
├── test.yml # name: test — lint + typecheck + unit, as parallel jobs
├── build.yml # name: build — job: verify — prod build compiles cleanly
├── e2e.yml # name: e2e — browser suite, PRs only
└── deploy.yml # name: deploy — job: deploy (<env>)
GitHub renders every check as <workflow> / <job> (event) — both halves always show, neither can be dropped. So pick workflow + job names that read as uniform task lines:
✓ build / verify
✓ e2e / e2e
✓ test / lint
✓ test / typecheck
✓ test / unit
✓ deploy / deploy (dev) # push-triggered; env suffix disambiguates on dev→main PRs
Rules
- Workflow names: lowercase, terse, content-describing (
test, build, e2e, deploy). The Actions run list shows only the workflow name per row, so it must be meaningful on its own.
- Job names: lowercase kebab task words that read well after the slash — complement the workflow name, never repeat it (
build / verify, not build / build-check).
- Granular jobs where lines are cheap: splitting test into
lint / typecheck / unit jobs makes each PR check line self-describing and parallelizes for free. The ~8s per-job setup duplication is noise at this scale; revisit only if setup cost grows.
- Independent concerns get their own workflow file, not a job — different triggers/concurrency stay clean (e.g. e2e on PRs only, deploy on pushes only), and the Actions run list stays distinguishable.
- Env-parameterized deploys suffix the job display name with the target:
deploy (${{ github.ref_name == 'main' && 'production' || 'dev' }}) — otherwise a deploy check surfacing on a promotion PR looks like it might be prod.
Why
- "CI" as a workflow name is the default trap: it reads fine with one workflow and becomes meaningless the moment a second one (e2e, deploy) appears next to it.
- The
<workflow> / <job> format is fixed; unplanned names produce mismatched (Test / check) or stuttering (Verify Build / verify-build) check lines. Naming both halves together as one readable phrase is free.
- Lowercase keeps the whole checks list visually uniform with the kebab-case job/task names.
Convention
One workflow file per concern, named lowercase by what runs (never "CI" — once there's more than one workflow, everything is CI and the label carries zero information):
GitHub renders every check as
<workflow> / <job> (event)— both halves always show, neither can be dropped. So pick workflow + job names that read as uniform task lines:Rules
test,build,e2e,deploy). The Actions run list shows only the workflow name per row, so it must be meaningful on its own.build / verify, notbuild / build-check).lint/typecheck/unitjobs makes each PR check line self-describing and parallelizes for free. The ~8s per-job setup duplication is noise at this scale; revisit only if setup cost grows.deploy (${{ github.ref_name == 'main' && 'production' || 'dev' }})— otherwise a deploy check surfacing on a promotion PR looks like it might be prod.Why
<workflow> / <job>format is fixed; unplanned names produce mismatched (Test / check) or stuttering (Verify Build / verify-build) check lines. Naming both halves together as one readable phrase is free.