Set Up CI With GitHub Actions in Ten Minutes
Continuous integration runs your checks on every push and pull request, so broken code is caught before it merges — whoever, or whatever, wrote it. A working GitHub Actions workflow for a Node project, what each line does, how to make checks required, and the mistakes that make CI slow or insecure.
When an AI agent opens a pull request, how do you know it didn't break the build? You could check out the branch and run everything yourself. Or you could have a machine do it, automatically, on every push — and refuse to merge until it passes.
That's continuous integration (CI), and on GitHub it takes one small file.
What CI does
Every time code is pushed or a pull request is opened, CI:
- Starts a fresh machine.
- Checks out the code.
- Installs dependencies.
- Runs your checks: type checking, linting, tests, the production build.
- Reports pass or fail on the commit and the pull request.
Because the machine is fresh every time, CI also catches "works on my machine" problems: a missing dependency, an uncommitted file, a lockfile out of date.
A working workflow
Create .github/workflows/ci.yml:
name: CI
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm test
- run: npm run build
Commit it, push, and open the Actions tab on GitHub to watch it run.
What each part does
on— when to run: on every pull request, and on pushes tomain.concurrency— if you push again while a run is in progress on the same branch, cancel the old one. Saves minutes and gets you the relevant result faster.permissions: contents: read— the workflow's GitHub token can only read the repository. Least privilege by default; grant more per job only when needed.runs-on: ubuntu-latest— a fresh Linux machine hosted by GitHub.timeout-minutes— kill a hung run instead of burning minutes for hours.actions/checkout— gets your code onto the machine.actions/setup-nodewithcache: npm— installs Node and caches downloaded packages between runs, which speeds up installs considerably.npm ci— a clean install exactly from the lockfile. Fails ifpackage.jsonand the lockfile disagree — which is a feature. (What Are npm and package.json?)- The checks — each
runis a step; the job fails at the first one that fails.
Make sure the scripts exist in your package.json (typecheck is typically tsc --noEmit). (Why TypeScript Makes AI-Generated Code Safer.)
Adding a database for integration tests
If tests need Postgres, GitHub Actions can run it as a service container alongside the job:
jobs:
check:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: app_test
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U test"
--health-interval 5s
--health-retries 10
env:
DATABASE_URL: postgres://test:test@localhost:5432/app_test
steps:
# ...checkout, setup-node, npm ci...
- run: npm run db:migrate
- run: npm test
The health check makes the job wait until Postgres is ready. Running migrations in CI also verifies they apply cleanly to an empty database — a cheap check that saves painful deploys. (What Are Database Migrations?)
Make the checks required
CI that reports failures but still lets you merge is advice, not protection. In your repository: Settings → Branches (or Rules → Rulesets) → add a rule for main:
- Require a pull request before merging.
- Require status checks to pass — select your CI job.
- Optionally, require branches to be up to date before merging.
Now nothing reaches main — from you, a teammate, or an AI agent — without passing CI. This matters especially with agents: it turns "the agent said the tests pass" into "the tests passed." (What Is a Pull Request?)
Secrets in CI
If a job needs a credential (for example, to deploy), store it in Settings → Secrets and variables → Actions and reference it as ${{ secrets.NAME }}. A few rules:
- Never echo secrets or pass them to commands that might print them.
- Scope them to the job that needs them, not the whole workflow.
- Pull requests from forks don't receive secrets — by design. Don't work around it with
pull_request_targetunless you fully understand the risks: it runs with secrets in the context of untrusted code, which is a well-known way to leak credentials. - Prefer short-lived credentials — many clouds support OIDC federation from GitHub Actions, so no long-lived key is stored at all.
Keeping CI fast
Slow CI gets ignored, and agents wait on it. Keep it quick:
- Cache dependencies (the
cache: npmline). - Cancel superseded runs (the
concurrencyblock). - Run independent checks in parallel as separate jobs — lint, typecheck, and tests don't need to wait for each other.
- Split slow end-to-end tests into a separate workflow, or run them only on
mainand before releases.
Common mistakes
- Using
npm installinstead ofnpm ci— CI should install exactly what's locked. - No timeout — a hung test burns hours of minutes.
- Broad default permissions — set
permissionsexplicitly. - Unpinned third-party actions — for actions outside GitHub's own
actions/organisation, consider pinning to a full commit SHA rather than a tag, since tags can be moved. - Checks that aren't required — a red cross nobody is forced to respect.
Beyond the basics
Once CI is in place, the same system can do much more: deploy on merge, build preview environments per pull request (Preview Environments for Every Branch), open dependency-update PRs with Dependabot (How to Update Your App's Dependencies Safely), or run Claude Code itself on pull requests and issues (Running Claude Code Headless).
EasySpawn works with GitHub as the canonical home of your code: Claude Code runs checks in its workspace before pushing, opens pull requests, and your CI verifies them independently before anything merges. See how it works or join the waitlist.
Related: Getting AI to Write Tests That Actually Catch Bugs · How to Review a Pull Request Written by an AI Agent · What Is CI/CD? · Linters and Formatters Explained
Keep reading
Getting AI to Write Tests That Actually Catch Bugs
Ask an AI for tests and you'll get plenty: tests that mock everything, assert nothing useful, and pass no matter what the code does. How to get tests that fail when behaviour breaks — what to test, how to prompt, how to check a test is real, and how tests become the agent's safety net.
Why TypeScript Makes AI-Generated Code Safer
Types turn a whole class of AI mistakes — invented properties, wrong arguments, forgotten null checks — into errors caught before the code runs. How TypeScript acts as a feedback loop for agents, the settings that matter, and the escape hatches AI uses to switch it off.