GitHub Actions Tutorial: CI/CD from Push to Deploy (2026)
Learn GitHub Actions: write your first workflow, run tests automatically, use secrets safely, deploy via SSH, cache dependencies, and run matrix builds.
On this page
GitHub Actions is how modern teams automate everything from running tests on every pull request to deploying code to production without touching a server. If you've never set one up before, this guide walks through the whole thing: how workflows work, your first working file, and a real deploy pipeline you can copy into your own repo.
By the end you'll have a workflow that runs your test suite on every push to main, deploys automatically when tests pass, and caches dependencies so CI runs finish in seconds instead of minutes.
- 1
What GitHub Actions Actually Does
When you push code, open a pull request, or trigger another event in your repo, GitHub Actions can automatically run a script in response. That script lives in a YAML file inside your repository at
.github/workflows/. GitHub runs it on its own servers: no CI service to pay for, no external tool to configure.A workflow file is built from four pieces. Once you understand these, you can read and write any GitHub Actions workflow:
- Workflow - the whole YAML file. One automation, one file.
- Event (trigger) - what starts it: a push, a pull request, a release, or a schedule.
- Job - a group of steps that run together on the same virtual machine.
- Step - a single command or a reusable action, run in order inside a job.
That's the entire mental model. Everything below is just different combinations of those four pieces.
- 2
Your First Workflow: Hello World
Create a file at
.github/workflows/hello.ymlin your repository:yaml โ .github/workflows/hello.ymlname: Hello World on: push: branches: [main] jobs: say-hello: runs-on: ubuntu-latest steps: - name: Print hello run: echo "Hello, World!"Push this file, then open the Actions tab on GitHub. You'll see the workflow run and print "Hello, World!" to the log. That's a working GitHub Actions workflow. Here's what each line does:
- `name` - a label shown in the Actions tab. Purely cosmetic.
- `on.push.branches: [main]` - run this workflow on every push to
main. This is the "run on push to main" trigger. - `runs-on: ubuntu-latest` - the virtual machine type. Ubuntu is the default and fastest option for most projects.
- `steps` - a list of commands to run, in order. Here there's just one.
- 3
Running Tests on Every Push
This is the most common use case: every time someone pushes code or opens a pull request, run the test suite automatically and block the merge if anything fails.
yaml โ .github/workflows/tests.ymlname: Tests 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' - run: npm ci - run: npm testuses:references a reusable action from the GitHub Marketplace instead of a raw shell command.actions/checkout@v4clones your repository onto the runner (without it, none of your code is even there).actions/setup-node@v4installs the Node.js version you specify. Thennpm ciinstalls dependencies from your lockfile (faster and more reproducible thannpm installin CI), andnpm testruns your test suite.If any step fails, GitHub marks the whole run as failed and puts a red X on the commit or pull request. Combine this with a branch protection rule and your team can't merge a pull request until tests pass.
- 4
Using Secrets Safely
Never put API keys, database URLs, or deploy keys directly in a YAML file: anyone with read access to the repository can see them, and they stay in git history forever. GitHub Actions has a secrets store built in for exactly this.
- Open your repo settings - go to Settings โ Secrets and variables โ Actions.
- Add a new repository secret - give it a name like
DEPLOY_KEYorSSH_PRIVATE_KEYand paste the value. - Reference it in your workflow - secrets are exposed through the
secretscontext, never as plain text.
yamlenv: DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}Or scope it to a single step instead of the whole job:
yaml- name: Deploy run: ./deploy.sh env: DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} - 5
Deploying to a Server via SSH
This is the most-searched workflow pattern in this guide: push to
main, run the tests, and if they pass, SSH into a server and deploy.yaml โ .github/workflows/deploy.ymlname: Deploy on: push: branches: [main] jobs: test-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npm test - name: Deploy via SSH uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /var/www/myapp git pull npm ci --production pm2 restart myappAdd
SERVER_HOST,SERVER_USER, andSSH_PRIVATE_KEYas repository secrets using the steps from the previous section.appleboy/ssh-actionconnects to your server over SSH, runs the script you give it, and reports success or failure back to the workflow.Steps run in order within a job, and each step only runs if the previous ones succeeded. That means
Deploy via SSHonly runs afternpm testpasses: a broken build never reaches production. - 6
Caching Dependencies
Without caching, every workflow run reinstalls
node_modulesfrom scratch. With caching, a typical install drops from around 45 seconds to under 5.yaml- uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node-Add this step before
npm ci. Thekeyis built from a hash ofpackage-lock.json: if your dependencies haven't changed, the cache hits and npm skips the download entirely. Ifpackage-lock.jsonchanges, the key changes too, the cache misses, and npm reinstalls clean.The same idea applies outside the npm ecosystem. If you're on a Python project managed with uv instead of pip, see the GitHub Actions CI setup for uv projects for the equivalent caching pattern.
- 7
Matrix Builds Across Node Versions
A matrix build runs the same job multiple times with different inputs, in parallel. The most common use is testing against several language versions at once:
yamljobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [18, 20, 22] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm testGitHub runs three separate jobs in parallel, one per Node.js version, each substituting
matrix.node-versionintosetup-node. If your code breaks on Node.js 18 but passes on 20 and 22, the matrix tells you exactly which version failed instead of leaving you to guess. - 8
Common Errors and How to Fix Them
Five mistakes account for almost every broken workflow. Here's what each one looks like and how to fix it.
If you want to debug a workflow without pushing a dozen commits, tools like
actrun GitHub Actions workflows locally inside Docker, which catches most of these issues before they reach GitHub at all.
| Pattern | YAML |
|---|---|
| Trigger on push to main | on: { push: { branches: [main] } } |
| Trigger on pull request | on: pull_request |
| Set up Node.js 20 | uses: actions/setup-node@v4, with: { node-version: '20' } |
| Run tests | run: npm test |
| Cache node_modules | uses: actions/cache@v4 |
| Use a secret | ${{ secrets.SECRET_NAME }} |
| Deploy via SSH | uses: appleboy/ssh-action@v1 |
Frequently Asked Questions
What is GitHub Actions used for?
GitHub Actions is GitHub's built-in automation platform. It runs scripts in response to events in your repository: pushes, pull requests, releases, issue comments, or a schedule.
- CI (continuous integration) - run tests and linters on every push or pull request.
- CD (continuous deployment) - deploy to a server, container registry, or cloud platform when code merges.
- Automation beyond code - label issues, close stale pull requests, post release notes, sync data on a schedule.
Is GitHub Actions free?
Yes, with limits. Public repositories get unlimited free minutes on standard runners. Private repositories get a monthly free quota and pay per minute after that, with Linux runners costing the least and Windows or macOS runners costing more per minute.
GitHub Actions vs Jenkins or CircleCI: which should I use?
| GitHub Actions | Jenkins / CircleCI | |
|---|---|---|
| Setup | Built into GitHub, no separate server | Separate service or self-hosted server to configure |
| Config location | .github/workflows/*.yml | Jenkinsfile or .circleci/config.yml |
| Reusable steps | Large marketplace of community actions | Plugins (Jenkins) or orbs (CircleCI) |
| Best for | Projects already hosted on GitHub | Multi-host repos or existing pipeline investment |
If your code is on GitHub, GitHub Actions removes an entire piece of infrastructure since there's no separate CI server to maintain. Reach for Jenkins or CircleCI when you need a CI system that spans multiple source hosts or you already have pipelines built there.
How do I make a workflow run only on push to main?
Use the on.push.branches key with your branch name:
on:
push:
branches: [main]Why is my GitHub Actions secret showing as empty?
Almost always one of three causes:
- Name mismatch - secret names are case-sensitive;
secrets.API_KEYwon't match a secret namedApi_Key. - Wrong scope - the secret was added to a different environment or repository than the one the workflow runs in.
- Fork pull requests - secrets aren't passed to workflows triggered by pull requests from forks, by design, to stop secrets leaking to untrusted code.
Can I test a GitHub Actions workflow locally before pushing?
Yes. The open-source act tool reads your .github/workflows/ files and runs them locally inside Docker containers, simulating GitHub's runners. It isn't a perfect match (some actions and contexts behave slightly differently), but it catches most YAML and logic errors without using up CI minutes or filling your history with debug commits.
Can GitHub Actions run AI code review or auto-fix failing pull requests?
Yes. A workflow can call any CLI tool as a step, including AI coding agents. If you're using Claude Code, the Claude Code cheatsheet covers the /autofix-pr workflow: when CI fails, Claude reads the failure logs, patches the issue, and pushes a follow-up commit to the same branch automatically.
Start small: a single workflow that runs your tests on every push to main already catches the majority of regressions before they reach production. Add secrets, caching, and a deploy step once that foundation is solid.
Every pattern in this guide, from the Hello World workflow to the SSH deploy job, is copy-paste ready. Save the quick reference table above and adapt the YAML to your project's package manager and deploy target.
Once your workflows are running, the next step is locking them down. The companion guide, GitHub Actions Security: 7 Misconfigurations to Avoid, covers the permission, secret, and caching mistakes that turn a working pipeline into an attack vector.
Related Articles
Husky + Prettier + lint-staged Setup for Next.js
Set up Husky v9, Prettier, and lint-staged in your Next.js project. Step-by-step guide covering pre-commit hooks with the correct 2026 config.
How to Switch to uv: Replace pip, virtualenv, and Poetry in Your Python Project
uv replaces pip, virtualenv, and Poetry with a single fast binary. Step-by-step guide to migrating your existing Python project and setting up GitHub Actions CI.
Claude Code Cheatsheet: Commands, Hooks & Subagents
The complete Claude Code reference: every slash command, keyboard shortcut, hook, subagent, and CLAUDE.md tip, with real examples for developers.