GitHub Actions CI/CD Configuration Tutorial: Automated Build, Test, and Deployment
GitHub Actions is GitHub's built-in CI/CD platform. Its main value is not just running tests automatically; it turns build, validation, release, and rollback steps into versioned repository code.
1. CI/CD basics
CI (Continuous Integration): build and test on every code change
CD (Continuous Deployment): deploy automatically after checks pass
1.1 Pick the pipeline goal first
Different repositories need different outcomes.
| Scenario | Main goal |
|---|---|
| Web frontend | Fast validation and release |
| Node.js API | Test, build, image publish |
| Documentation repo | Quality checks and previews |
| Infrastructure code | Plan, approval, deployment |
2. Core concepts
| Term | Meaning |
|---|---|
| Workflow | Automated process definition |
| Job | A unit of execution |
| Step | A single action inside a job |
| Action | Reusable component |
| Runner | The machine that executes the workflow |
2.1 When to split jobs
If a failed step should not force the entire pipeline to rerun, split it into its own job. Logs become cleaner and parallel execution becomes possible.
3. A practical Node.js workflow
name: Node.js CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-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
3.1 A version with deploy stages
name: Node.js CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
lint:
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
test:
needs: lint
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:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- name: Deploy to VPS
uses: appleboy/[email protected]
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: 'dist/'
target: '/var/www/app/'
4. Secrets and server setup
Add the following secrets in GitHub → Settings → Secrets and variables → Actions:
SERVER_HOST
SERVER_USER
SSH_PRIVATE_KEY
On the server, prepare a repeatable deployment target. A container-based restart path is usually the least error-prone.
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
restart: always
4.1 Permission guidance
Keep permissions as small as possible.
| Scope | Recommendation |
|---|---|
contents |
read |
packages |
enable only if needed |
actions |
keep minimal |
5. Best practices
- Split lint, test, and deploy into separate jobs.
- Cache dependencies to reduce repeat install time.
- Deploy only from main; let pull requests validate code.
- Use least-privilege permissions.
- Add path filters if docs-only changes should skip heavy jobs.
5.1 Common enhancements
| Enhancement | Value |
|---|---|
| Dependency cache | Shorter builds |
| Path filters | Only run heavy jobs when needed |
| Environment protection | Approval before deployment |
| Parallel jobs | Higher throughput |
6. Troubleshooting tip
git status
Start by checking the local workspace, then inspect the Actions log. That usually tells you whether the failure is caused by code, dependency installation, or secret configuration.
6.1 Debug order
- Check whether the trigger condition matched.
- Check whether checkout succeeded.
- Check whether dependency installation failed.
- Check whether tests failed.
- Check whether deploy credentials and target paths are correct.
Reference points worth checking include the GitHub Actions docs plus the docs for actions/checkout, actions/setup-node, and appleboy/scp-action.
7. Environments and rollback
If the project already has staging and production, connect environment protection as well. PRs can validate only, merges can move through staging first, and rollback becomes easier to trace.
| Environment | Purpose |
|---|---|
| Preview | Review and preview |
| Staging | Integration testing and acceptance |
| Production | Live release |
Rollback should be scripted, not improvised during an incident. A fixed script or container-version rollback is usually the fastest way to restore service while you investigate the root cause.
7.1 Matrix builds and release tags
If the project must support multiple Node.js versions or operating systems, a matrix lets you test several combinations in one workflow. That catches compatibility issues before users do.
strategy:
matrix:
node: [18, 20]
os: [ubuntu-latest, windows-latest]
For releases, it also helps to tie tags to release artifacts so the shipped version always maps back to a specific commit. That is much easier to audit than relying on team memory.
If the team needs better visibility, send a post-deploy notification to Slack, email, or a chat bot. Keeping the result in the repository history makes release tracking much easier later.
It is also a good idea to keep build artifacts for a limited period, such as the last few successful releases. That way, rollback and audit tasks can use a verified binary instead of forcing a fresh rebuild under pressure.
It is a small detail, but it saves a lot of time during incident recovery.
It is especially useful for teams that ship often.
7.2 Maintenance and troubleshooting habits
Check secrets, caches, and third-party Action versions on a regular schedule. CI is not something you write once and forget; long-term maintenance matters too.
| Item | Suggested action |
|---|---|
| Secrets | Rotate periodically |
| Cache | Clear when needed |
| Action | Pin the version |
7.3 A minimal deploy template
If your team is just starting with automation, begin with a two-step flow: verification and deployment. Do not overload the workflow with notifications, matrices, and multiple environments on day one. The first goal is to make every merge into the main branch validate automatically and then deploy once.
name: Minimal Deploy
on:
push:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
deploy:
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
7.4 Minimum release checks
| Item | Checkpoint | Why it matters |
|---|---|---|
| Trigger condition | Release only on main | Avoid accidental drafts |
| Dependency install | Is npm ci stable? |
Keeps versions locked |
| Build output | Does the artifact actually exist? | Validate before deploy |
| Target path | Is the destination directory correct? | Avoid overwriting the wrong folder |
| Rollback | Can you revert quickly? | Restore service first during incidents |
7.5 A common failure scenario
Some repositories work locally but fail in Actions. That usually does not mean the platform is unstable; it means the environment is different. The usual gaps are Node versions, missing environment variables, insufficient permissions, or scripts that depend on local files. When that happens, pull out the first failing command from the log and reproduce it in isolation.
- Confirm the runner version.
- Confirm dependency versions are pinned.
- Confirm secrets are present.
- Confirm the deployment path and permissions.
Once those four checks become routine, CI/CD stops being something that only works sometimes and starts becoming something the team can rely on.