HomeProjectsBlogContact

Free CI/CD with GitHub Actions: A Practical Starter Guide

Automate tests, linting, builds and deploys on every push, for free on public repos. A practical GitHub Actions guide with copy-paste workflows, caching, secrets and scheduled jobs.

Free CI/CD with GitHub Actions: A Practical Starter Guide

Continuous integration sounds like something for big teams with DevOps engineers. It isn't. CI simply means "every time I push code, a machine checks it for me." For a solo developer, that's a safety net catching the mistakes you make at 11 p.m. And with GitHub Actions, it's built into the platform where your code already lives.

The core concepts in one minute

  • Workflow: a YAML file in .github/workflows/ that defines automation.
  • Trigger (on): what starts it, such as a push, a pull request, a schedule or a manual click.
  • Job: a group of steps that runs on a fresh virtual machine (a runner).
  • Step: a single command, or a reusable action someone else published.

GitHub Actions is free for public repositories on standard runners, and private repositories get a monthly allowance of free minutes. Check GitHub's current billing docs for exact numbers.

Your first workflow: test every push

Create .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run build

Push it, open the Actions tab in your repository, and watch it run. Every pull request now shows a green check or a red cross.

A few notes:

  • npm ci installs exactly what's in your lockfile. It's faster and more predictable than npm install in CI.
  • cache: npm caches downloaded packages between runs, which speeds things up considerably.
  • Action versions like @v4 get new major versions over time. Dependabot can keep them updated for you.

Protect your main branch

CI is most useful when it's enforced. In your repository settings, add a branch protection rule (or ruleset) for main:

  • Require pull requests before merging.
  • Require the CI status check to pass.

Now broken code physically can't land on main without someone noticing.

Test against a real database

Integration tests often need a database. Actions can run one as a service container:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 5s
          --health-timeout 5s
          --health-retries 5
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run migrate
      - run: npm test

The health check makes the job wait until Postgres is actually ready.

Use secrets properly

Never put API keys in workflow files. Add them under Settings → Secrets and variables → Actions, then reference them:

- run: npm run deploy
  env:
    DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

GitHub masks secret values in logs. Two more safety notes:

  • Secrets aren't passed to workflows triggered by pull requests from forks, by design. Keep it that way.
  • Set minimal permissions for the built-in token at the top of your workflow:
permissions:
  contents: read

More on handling secrets: environment variables done right.

Deploying from Actions

Many hosts (Vercel, Netlify, Cloudflare) deploy automatically from Git, so you may not need a deploy step at all. Let CI test and the host deploy.

When you do deploy from Actions, gate it on tests passing and on the right branch:

jobs:
  test:
    # ... as above
  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

needs: test means deploy only runs if tests pass.

Scheduled jobs: free cron

Actions can run on a schedule, which is handy for side projects:

on:
  schedule:
    - cron: '0 3 * * *'   # every day at 03:00 UTC
  workflow_dispatch:       # plus a manual "Run" button

Use cases:

  • Nightly database backups with pg_dump, uploaded to storage.
  • Pinging a free-tier service so it doesn't pause from inactivity (check the provider's terms first).
  • Checking for broken links on your site.
  • Regenerating a sitemap or static data.

Schedules run in UTC and may start a few minutes late under load. Don't rely on them for precise timing. (Why UTC matters.) Note that scheduled workflows in inactive public repositories can be disabled automatically after a long period without activity.

Speed up slow workflows

  • Cache dependencies (the cache option in setup actions).
  • Run jobs in parallel: lint, unit tests and type checks can be separate jobs.
  • Use a matrix to test several versions at once only when you actually support several versions.
  • Cancel outdated runs when new commits arrive:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Keep workflows secure

  • Prefer well-known actions, and consider pinning third-party actions to a full commit SHA instead of a tag.
  • Don't echo secrets, even for debugging.
  • Be careful using untrusted input (like PR titles) directly inside run: scripts. Pass it through environment variables instead.

FAQ

Is GitHub Actions free?

It's free for public repositories on standard GitHub-hosted runners, and private repositories include a monthly quota of free minutes. Check GitHub's billing documentation for current limits.

What is the difference between CI and CD?

CI (continuous integration) automatically tests and builds every change. CD (continuous delivery or deployment) automatically releases changes that pass, either to staging or to production.

Can GitHub Actions run cron jobs?

Yes. Use on: schedule with a cron expression. Schedules run in UTC and may be delayed slightly during busy periods.

Keep reading

More from the workbench

All articles →