Start here
Where the file goes and what happens next
GitHub reads workflow files from one folder only: .github/workflows at the root of the default branch. Create that folder, drop the generated .yml inside it, commit, and the run starts on the next matching event. The Actions tab lists the run by the name: value at the top of the file, so give it something a teammate will recognise at 2am.
A first run fails more often than not, and the reason is almost never the YAML syntax. It is a missing lockfile, a test script the project does not define, or a secret nobody added yet. The two panels under the builder cover the last one. Every secrets.SOMETHING reference in the output gets pulled out and listed, so you know exactly what to add under Settings, then Secrets and variables, then Actions before the first push.
The bug most generators ship
Unquoted version numbers turn Python 3.10 into 3.1
YAML has no idea your version string is a version string. Written bare, python-version: 3.10 parses as a float, the trailing zero disappears, and the runner installs Python 3.1. The job then dies on a syntax error in code that works fine on your laptop. The same trap catches ruby-version: 3.20 and go-version: 1.20.
| Written as | Parsed as | Runner installs |
|---|---|---|
3.10 | Float 3.1 | Python 3.1 |
"3.10" | String | Python 3.10 |
20.x | String, the x saves it | Node 20 |
1.20 | Float 1.2 | Go 1.2 |
8.0 | Float 8 | .NET SDK lookup fails |
Every version this page writes comes out quoted, in the matrix list and in the with: block alike. Branch names get the same treatment, because a branch called 1.0 or release-2.10 hits the identical problem.
Matrix builds
A matrix key nothing references runs the same version twice
This is the quiet failure. The workflow declares a matrix, the Actions tab shows two green jobs, and both installed Node 20 because the setup step hardcoded the version instead of reading the matrix value. Duplicate work, double the minutes, zero extra coverage.
Type one version in the field and the builder writes it straight into the setup step with no matrix at all. Type two or more and it writes the matrix, then references it as {{ matrix.node-version }} so each job installs a different runtime. It also adds fail-fast: false, which keeps the 22.x job running after 20.x breaks. Without that line the first failure kills its siblings and you learn about one break per push instead of all of them.
Runner choice
Picking a runner is a billing decision
On public repositories the runners are free. On private ones GitHub charges against your minute allowance with a multiplier, and the difference is not small.
- Linux bills at 1x. A 10 minute job spends 10 minutes.
- Windows bills at 2x. The same job spends 20.
- macOS bills at 10x. The same job spends 100.
Reach for macOS when the build signs an iOS artifact or needs Xcode, and for Windows when you compile against the .NET desktop stack or test PowerShell behaviour. Everything else belongs on Ubuntu. The builder flags the choice in the right-hand panel rather than blocking it, since sometimes the expensive runner is the correct one.
timeout-minutes is the other half of the same argument. The default job limit is six hours, so a hung test suite or a prompt waiting on stdin burns the afternoon before anyone notices. Fifteen minutes suits most test jobs. Raise it for container builds, lower it for a lint-only workflow.
Two blocks worth adding
Permissions and concurrency
The GITHUB_TOKEN handed to a workflow inherits repository-wide defaults, which on older organisations still means write access to contents, issues, packages, and more. A compromised dependency running inside your build step gets that whole scope. One line at the top of the file cuts it down:
Jobs that genuinely need more ask for it individually. Choose GitHub Pages as the target and the deploy job gets its own pages: write and id-token: write block while the build job stays read-only. Choose the container registry and only the deploy job receives packages: write.
Concurrency solves a different waste. Push three commits in five minutes and you get three full runs, two of which test code nobody will ship. The generated group keys on the workflow and the ref, so a new push cancels the older run on the same branch:
cancel-in-progress or scope the group so deploys queue instead of dying halfway through an S3 sync.Caching and artifacts
Both have a rule that surprises people
The cache: input on setup-node, setup-python, and friends hashes a lockfile to build its key. No package-lock.json, no requirements.txt, no cache, and the step fails outright rather than continuing without one. If your repository leans on package.json alone, commit the lockfile first or turn caching off in the builder.
Artifacts changed harder. Version 4 of upload-artifact stopped allowing several jobs to write into one artifact name, so a matrix that uploads under a fixed name errors on the second job. The generated step appends the matrix value to the artifact name to keep each upload distinct, and the deploy job downloads the first version in your list. If a specific version should be the one that ships, put it first.
Artifacts also cost storage against your plan, which is why the generated step sets retention-days: 7 instead of the 90 day default. A build you already deployed rarely needs to sit around for three months.
Schedules and secrets
Two behaviours the documentation buries
Scheduled workflows run on UTC, ignore your repository timezone, and start on a best-effort basis. During peak load a 0 3 * * 1 job commonly begins ten to twenty minutes late, so never schedule anything that assumes an exact minute. GitHub also disables scheduled workflows in a public repository after 60 days with no commit activity, which is why an abandoned nightly job goes quiet without an email.
Secrets have a sharper edge. A pull_request event from a fork receives no secrets at all and a read-only token, on purpose, so an outside contributor cannot exfiltrate your deploy keys. That means a test suite depending on a secret API key passes for your team and fails for every fork PR. Split the workflow: run linting and unit tests on pull_request, keep anything needing credentials on push to your own branches.
NODE_ENV: production or CI: true belong here. Tokens, connection strings, and signing keys belong in repository secrets, where the runner masks them in the log.Honest limits
What this generator does not write
The output is a starting file for a single-repository pipeline, not a finished platform config. Five things it deliberately leaves out:
- Reusable workflows and composite actions. Once four repositories share a pipeline, a
workflow_callfile beats four copies of this output. - Deployment environments with approvals. Protection rules, required reviewers, and wait timers are repository settings, not YAML.
- Self-hosted runner labels. The runner list covers GitHub-hosted images only. Swap
runs-onfor your own label after generating. - Service containers. A test suite needing Postgres or Redis alongside it wants a
services:block, which depends too much on your schema to guess. - Action SHA pinning. Steps reference tags such as
@v4. Supply-chain-strict teams pin to a full commit SHA instead, which a generator cannot resolve for you.
The default commands are conventional, not universal. npm test, pytest -q, and bundle exec rspec match the majority of projects using those stacks. If yours runs vitest, tox, or a Makefile target, edit the run: line after copying. Reading the generated file once before committing catches this in about thirty seconds.
Before you commit
Four checks that catch most first-run failures
- Confirm the scripts exist.
npm run lintfails immediately whenpackage.jsonhas no lint script. Turn the step off or add the script. - Add every secret listed in the panel above. A missing secret resolves to an empty string, so the step usually fails with a confusing authentication error rather than a clear one.
- Check the branch name in the deploy job's
if:line. It uses the first branch you typed, so a repository still onmasterneeds that changed. - Push to a throwaway branch first with manual dispatch on. You get a full run without touching main, and the Actions log points at the exact failing step.
For related pieces of the same pipeline, the Dockerfile Generator writes the image this workflow builds, the Cron Expression Generator checks the schedule string before you paste it, and the gitignore Generator keeps build output from reaching the repository in the first place. For a pipeline on GitLab, Jenkins, or CircleCI, use the CI/CD Pipeline Generator instead.
