GitHub Actions Tutorial: Automate CI/CD for Your Projects in 2026
GitHub Actions Tutorial: Automate CI/CD for Your Projects in 2026
Step-by-step GitHub Actions tutorial. Learn to automate testing, building, and deployment with practical workflow examples for Node.js and Python.
What Is CI/CD and Why Does It Matter?
CI/CD stands for Continuous Integration and Continuous Deployment. At its core, it is the practice of automating the repetitive steps that happen every time code changes:
- Continuous Integration (CI): Automatically run tests and checks when code is pushed or a pull request is opened. This ensures broken code is caught before it merges.
- Continuous Deployment (CD): Automatically deploy to a staging or production environment when code passes all checks.
Without CI/CD, teams rely on developers remembering to run tests, manually building applications, and manually deploying to servers. This is error-prone and slow. With CI/CD, these steps happen automatically and consistently.
GitHub Actions is GitHub's built-in CI/CD platform. It is deeply integrated with your repository, free for public repositories and generously free-tiered for private ones, and powerful enough to handle workflows from simple linting to complex multi-environment deployments.
Core Concepts
Workflow
A workflow is an automated process defined in a YAML file stored in .github/workflows/. A repository can have multiple workflows, each triggered by different events.
Event (Trigger)
Events define when a workflow runs. Common triggers:
pushβ When code is pushed to a branchpull_requestβ When a PR is opened, updated, or synchronizedscheduleβ On a cron scheduleworkflow_dispatchβ Triggered manually from the GitHub UI
Job
A workflow consists of one or more jobs. Jobs run in parallel by default. Each job runs on a fresh virtual machine (called a runner).
Step
Each job contains a sequence of steps. Steps run sequentially. A step can run a shell command or use a pre-built action.
Action
Actions are reusable units of work published on the GitHub Marketplace. For example, actions/checkout checks out your repository, and actions/setup-node installs Node.js.
Your First Workflow: Node.js Testing
Create .github/workflows/test.yml in your repository:
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Check TypeScript
run: npm run typecheck
This workflow runs on every push to main or develop, and on every pull request to main. It checks out the code, sets up Node.js 20 (with npm caching for speed), installs dependencies, and runs lint, tests, and type checking sequentially. If any step fails, the workflow fails and GitHub blocks the PR from merging.
Python Workflow: Lint and Test
name: Python CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install ruff pytest pytest-cov
- name: Lint with Ruff
run: ruff check .
- name: Run tests with coverage
run: pytest --cov=src --cov-report=xml
- name: Upload coverage report
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
Matrix Builds: Test Across Multiple Versions
Use a matrix strategy to run your tests against multiple versions of Node.js or Python simultaneously:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18', '20', '22']
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
This creates a separate job for each combination: 3 Node.js versions Γ 2 OS = 6 parallel jobs. If your library needs to support Node 18, 20, and 22, this catches compatibility issues automatically.
Secrets Management
Never hardcode API keys or passwords in your workflows. Use GitHub Secrets.
Setting up a secret:
- Go to your repository β Settings β Secrets and variables β Actions
- Click "New repository secret"
- Add a name (e.g.,
API_KEY) and value
Using secrets in a workflow:
steps:
- name: Deploy to production
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
echo "Deploying with API key..."
npm run deploy
Secrets are masked in logs β even if your code accidentally prints $API_KEY, GitHub will replace it with ***.
Deploy to Cloudflare Pages
Here is a complete workflow that builds a Next.js app and deploys it to Cloudflare Pages:
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
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 test
deploy:
runs-on: ubuntu-latest
needs: test # Only deploy if tests pass
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install and build
run: |
npm ci
npm run build
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: my-project
directory: .next
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
The needs: test directive ensures the deploy job only runs if the test job succeeds. The environment: production adds a required reviewer approval step for production deployments.
Caching for Faster Workflows
Slow workflows discourage developers from running them. Use caching aggressively:
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
The cache key includes a hash of the lock file. When package-lock.json or requirements.txt changes, the cache is invalidated and dependencies are reinstalled. Otherwise, the cache is restored in seconds, dramatically speeding up your workflow.
Conditional Steps and Manual Approvals
Control when steps run with conditions:
- name: Deploy to staging
if: github.ref == 'refs/heads/develop'
run: npm run deploy:staging
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: npm run deploy:production
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'β
All checks passed! Ready for review.'
})
Tips for Production-Quality Workflows
- Pin action versions β Use
actions/checkout@v4notactions/checkout@mainto avoid unexpected breaking changes - Keep workflows fast β Target under 5 minutes for CI. Slow pipelines get disabled by frustrated developers
- Use path filters β Only trigger workflows when relevant files change
- Set timeout limits β Add
timeout-minutes: 10to jobs to prevent runaway builds from consuming all your minutes - Review workflow permissions β Use
permissions: read-allor minimal necessary permissions for security
GitHub Actions is one of the most powerful tools available to modern developers. A well-designed workflow catches bugs before they reach production, ensures consistent builds across all environments, and removes the manual toil of deployment. Start with a simple test workflow today, and incrementally add deployment automation as your project matures.