GitHub Actions lets you automate tasks that normally happen after you push code.
Instead of manually running:
npm install
npm test
npm run build
you can make GitHub do it automatically whenever code is pushed or a pull request is opened.
This is one of the simplest ways to introduce CI/CD into a project.
What is CI?
Continuous Integration (CI) means automatically checking whether new code works before it is merged.
A typical flow looks like:
Push code
↓
Install dependencies
↓
Lint
↓
Test
↓
Build
↓
Pass / Fail
The goal is simple:
Catch problems before they reach production.
Where GitHub Actions Lives
Workflows are stored inside:
.github/workflows/
For example:
my-project/
├── src/
├── package.json
└── .github/
└── workflows/
└── ci.yml
Each .yml file describes an automated workflow.
The Mental Model
GitHub Actions becomes much easier once you understand this structure:
Event
↓
Workflow
↓
Job
↓
Runner
↓
Steps
Event
Something happens in your repository.
For example:
push
pull request
manual trigger
Workflow
The automation that should run.
Job
A group of related tasks.
Examples:
test
build
deploy
Runner
The machine executing the job.
For example:
runs-on: ubuntu-latest
Steps
The individual commands inside the job.
Your First Workflow
Create:
.github/workflows/ci.yml
Then add:
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
This is enough for a useful CI pipeline.
Understanding the Workflow
name
name: CI
This is simply the workflow name shown in GitHub.
on
on:
push:
branches:
- main
pull_request:
branches:
- main
This defines when the workflow should run.
In this case:
Push to main
OR
Pull request into main
↓
Run CI
jobs
jobs:
test:
A workflow can contain one or more jobs.
Here we have one job called:
test
runs-on
runs-on: ubuntu-latest
This tells GitHub to run the job on an Ubuntu machine.
Think of it as GitHub temporarily creating a clean computer for your workflow.
uses vs run
You will see these constantly.
uses
- uses: actions/checkout@v6
This runs an existing reusable GitHub Action.
For example:
actions/checkout
copies your repository onto the runner.
run
- run: npm test
This executes a normal shell command.
So the difference is:
uses
→ run an existing action
run
→ execute a command
Why Checkout the Repository?
A runner starts as a fresh environment.
This:
- uses: actions/checkout@v6
makes your repository available inside it.
After that, commands like:
npm ci
npm test
npm run build
can work with your project files.
Why Use npm ci?
In CI, prefer:
npm ci
when your project has a lockfile.
It installs the exact dependency versions defined by your lockfile, which makes builds more predictable.
Your basic pipeline becomes:
Checkout
↓
Setup Node
↓
npm ci
↓
npm test
↓
npm run build
Multiple Jobs
As a project grows, you may separate responsibilities.
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
By default, independent jobs can run separately.
If one job should wait for another, use:
needs:
Example:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
Now:
Test
↓
Pass
↓
Deploy
If the test fails, deployment does not continue.
Secrets
Never put credentials directly inside your workflow.
Bad:
env:
API_KEY: "my-secret-key"
Instead, store the value in GitHub Secrets and access it like:
env:
API_KEY: ${{ secrets.API_KEY }}
This is commonly used for:
deployment tokens
API keys
cloud credentials
database credentials
A Good Workflow for an Astro Project
For a simple Astro project, this is usually enough:
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Check
run: npm run check --if-present
- name: Test
run: npm test --if-present
- name: Build
run: npm run build
That gives you a practical CI pipeline without overengineering the project.
Common Mistakes
Forgetting checkout
Without:
uses: actions/checkout@v6
your repository files may not be available to later steps.
Hardcoding secrets
Never commit:
API_KEY: "secret"
Use GitHub Secrets instead.
Forgetting to build
Tests passing does not always mean the production build works.
For frontend projects, include:
- run: npm run build
in CI.
Making workflows too complicated
A small project does not need:
10 jobs
5 environments
multiple matrices
custom runners
complex deployment logic
Start with:
install
test
build
Add complexity only when the project actually needs it.
Final Mental Model
When reading any GitHub Actions file, ask:
What triggers it?
↓
What jobs run?
↓
What machine runs them?
↓
What steps execute?
↓
What happens if something fails?
And remember:
Event
↓
Workflow
↓
Job
↓
Runner
↓
Steps
GitHub Actions is not really about YAML.
It is about turning your development process into something repeatable and automatic.
For most projects, a good starting point is simply:
Push code
↓
Install dependencies
↓
Test
↓
Build
That alone gives you most of the practical value of CI without unnecessary complexity.