Getting Started with CI/CD Using GitHub Actions
After writing code, do you find yourself manually running tests, building the project, and uploading files to a server — every single time? With CI/CD, GitHub handles all of that automatically.
This article walks through the basics of CI/CD using GitHub Actions, starting from scratch.
What Is CI/CD?
CI/CD stands for Continuous Integration and Continuous Delivery/Deployment.
| Term | Meaning |
|---|---|
| CI (Continuous Integration) | Automatically run tests and builds on every push to catch problems early |
| CD (Continuous Delivery) | Automatically deliver code that passes CI to staging or production environments |
Why CI/CD Matters
Manual testing and deployment carries these risks:
- Human error — “I forgot to run the tests” or “I uploaded the wrong file”
- Slow feedback — Bugs go unnoticed for days
- Knowledge silos — Only one person knows the deployment steps
Automation through CI/CD eliminates these risks at the process level.
What Is GitHub Actions?
GitHub Actions is a CI/CD tool built directly into GitHub. By adding a single YAML file to a repository, I can automate testing, building, and deploying.
Key Terms
| Term | Description |
|---|---|
| Workflow | The complete automation definition. One YAML file in .github/workflows/ = one workflow |
| Trigger | What starts the workflow (push, PR creation, schedule, etc.) |
| Job | A unit of execution inside a workflow. Multiple jobs can run in parallel |
| Step | An individual task inside a job (running a command or calling an action) |
| Action | A reusable processing component. Many are published on the GitHub Marketplace |
| Runner | The virtual machine that runs a job (e.g., ubuntu-latest) |
Creating My First Workflow
Step 1: Create the File
Create a .github/workflows/ directory at the repository root and place a YAML file inside it.
.github/
workflows/
ci.yml ← write the workflow hereStep 2: A Basic CI Workflow
Here’s a simple example that runs tests on every push for a Node.js project.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm testWhat Each Section Means
name: CI # Workflow name (shown in the GitHub UI)
on: # Trigger configuration
push:
branches: [main] # Run on push to main
pull_request:
branches: [main] # Run on PR creation/update targeting main
jobs:
test: # Job name (freely chosen)
runs-on: ubuntu-latest # Runtime environment (latest Ubuntu)
steps:
- uses: actions/checkout@v4 # Fetch the repo code onto the runner
- run: npm test # Execute a shell commandCommon Triggers
on:
push: # When code is pushed
branches: [main, develop]
pull_request: # When a PR is created or updated
branches: [main]
schedule: # Recurring execution (cron format)
- cron: '0 9 * * 1' # Every Monday at 9:00 UTC
workflow_dispatch: # Manual trigger from the GitHub UIPractical Examples
Example 1: Testing a Python Project
name: Python CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytestExample 2: Run a Linter on Every PR
name: Lint
on:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run lintExample 3: Auto-Deploy After Merging to Main (CD)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'About
secrets: Never write API keys or tokens directly in a YAML file. Store sensitive values in the repository’s Settings → Secrets and variables → Actions, then reference them with${{ secrets.VARIABLE_NAME }}.
Checking Workflow Status
After pushing a workflow, I can monitor execution status from the Actions tab of the repository.
- Click the “Actions” tab on the repository page
- A list of running and completed workflows appears
- Click any entry to see per-step logs
- Failed steps are shown in red with detailed error output
Common Errors and Fixes
npm ci fails
This usually means package-lock.json is missing or out of sync with package.json. Run npm install locally, commit the updated package-lock.json, and push again.
Permission denied
The script may not have execute permission. Add chmod +x script.sh as a step, or run it with bash script.sh instead.
Secret shows as undefined
The most common causes are a typo in the secret name or the restriction that prevents forked PRs from accessing secrets. Check the exact name in the repository Settings.
GitHub Actions Pricing
| Environment | Free Tier |
|---|---|
| Public repositories | Unlimited (completely free) |
| Private repositories | Up to 2,000 minutes/month free (GitHub Free) |
For personal learning or open-source projects, Actions is essentially free.
Next Steps
Once I’m comfortable with the basics, here are good next challenges:
- Matrix builds — Run tests in parallel across multiple OS and language versions
- Caching — Use
actions/cacheto cache dependencies and speed up runs - Reusable workflows — Share a common workflow across multiple repositories
- Environments and deployment gates — Set up an approval flow between staging and production
See the references for the external specifications and background sources used on this page.[1][2]
References
- GitHub, GitHub Actions Documentation
- GitHub, GitHub Actions Marketplace