GitHub Actions Workflow Generator

Pick triggers, a runtime, and a deploy target. The YAML updates as you type, version numbers stay quoted, and every secret the file references is listed before you commit it.

Workflow builder

Workflow
Triggers
Run this workflow on
Runtime and steps
Steps
Speed and output
Deploy
Hardening
Extra blocks
.github/workflows/node-ci.yml
  • 0 lines
  • 0 bytes
  • Generated in your browser

Secrets this file expects

Worth checking

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.

What YAML does with a version value
Written asParsed asRunner installs
3.10Float 3.1Python 3.1
"3.10"StringPython 3.10
20.xString, the x saves itNode 20
1.20Float 1.2Go 1.2
8.0Float 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.

strategy:matrix:node-version: ["20.x", "22.x"]steps:- uses: actions/setup-node@v4 with:node-version: "20.x" # nothing reads the matrix

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:

permissions:contents: read

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:

concurrency:group: {{ github.workflow }}-{{ github.ref }} cancel-in-progress: true
Where this bites: the same setting cancels an in-flight deploy when a second commit lands on main. For a workflow that pushes to production, either drop 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.

On the environment variables box: what you type there lands in the committed file as plain text. Non-sensitive flags like 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_call file 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-on for 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

  1. Confirm the scripts exist. npm run lint fails immediately when package.json has no lint script. Turn the step off or add the script.
  2. 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.
  3. Check the branch name in the deploy job's if: line. It uses the first branch you typed, so a repository still on master needs that changed.
  4. 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.

GitHub Actions workflow questions

File location, matrix builds, secrets, minutes, and what runs where.

Where exactly does the generated file go?

Into .github/workflows at the repository root, with a .yml or .yaml extension. GitHub reads that folder from the default branch only, so a workflow added on a feature branch will not appear in the Actions tab until the branch merges, unless the workflow itself is triggered by a push to that branch.

Why did my Python 3.10 job install Python 3.1?

The version was written without quotes, so YAML parsed 3.10 as the number 3.1. Quoting the value fixes it. This generator quotes every version it writes, in both the matrix list and the setup step.

When should I use a version matrix?

When the project must work on more than one runtime, such as a published library supporting Node 20 and 22. An application deployed to one server gains nothing from a matrix and doubles the minutes each run costs. Enter one version and the builder writes no matrix block at all.

Do I need to create the GITHUB_TOKEN secret?

No. GitHub generates it for every run and removes it when the job ends. The secrets panel marks it as supplied automatically. Every other secret in that list has to be added under Settings, Secrets and variables, Actions before the first run.

Why do secrets work on push but not on pull requests from forks?

GitHub withholds secrets from fork pull requests so an outside contributor cannot read your credentials by editing the workflow. Keep credential-dependent steps on push events and limit fork pull requests to linting and unit tests.

How many Actions minutes will this workflow use?

Public repositories are free. Private ones bill Linux at 1x, Windows at 2x, and macOS at 10x against your monthly allowance. A ten minute Ubuntu job spends ten minutes, the same job on macOS spends a hundred. Caching and cancelling superseded runs are the two settings that cut the bill fastest.

Is any of this sent to a server?

No. The YAML is assembled by JavaScript in your browser tab. Nothing is uploaded, logged, or stored, and the copy and download buttons work on the same local text.