GitHub Actions CI/CD: A Practical Guide
Continuous Integration and Continuous Deployment (CI/CD) have become essential practices in modern software development. GitHub Actions, as GitHub’s native automation tool, offers simplicity, generous free tier, and deep integration with the GitHub ecosystem, making it a popular choice for developers and teams.
In this guide, we’ll build a complete CI/CD pipeline from scratch using real production configurations, covering frontend/backend separation, Docker image building, multi-platform binary releases, and automated issue management.
Why GitHub Actions?
Before diving into the implementation, let’s understand the core advantages:
Key Benefits
- Native Integration - No third-party platforms needed, configure directly in your repository
- Free Tier - Unlimited for public repos, 2000 minutes/month for private repos
- Rich Ecosystem - Thousands of reusable Actions in GitHub Marketplace
- Flexible Triggers - Push, PR, schedule, manual triggers, and more
- Matrix Builds - Easy multi-version, multi-platform parallel testing
- GitHub Ecosystem - Native support for Packages, Issues, Releases
Comparison with Other CI/CD Tools
| Feature | GitHub Actions | Jenkins | GitLab CI | CircleCI |
|---|---|---|---|---|
| Setup Complexity | Low | High | Medium | Medium |
| Free Tier | 2000 min/mo | Self-hosted | 400 min/mo | 6000 min/mo |
| Marketplace | Rich | Many plugins | Medium | Medium |
| GitHub Integration | Native | Requires plugins | Requires config | Requires config |
| Learning Curve | Gentle | Steep | Moderate | Moderate |
Project Architecture
We’ll build a CI/CD pipeline for a full-stack application with frontend/backend separation:
project/
├── frontend/ # React/Vue frontend
│ ├── src/
│ ├── package.json
│ └── Dockerfile
├── backend/ # Go/Node backend
│ ├── main.go
│ ├── go.mod
│ └── Dockerfile
└── .github/
└── workflows/
├── frontend-ci.yml # Frontend CI
├── backend-ci.yml # Backend CI
├── backend-cd.yml # Backend CD
├── backend-lint.yml # Backend linting
└── release.yml # Multi-platform release
Part 1: Frontend CI Pipeline
Use Case
A Node.js + npm frontend project that needs to automatically run the following on every commit and PR:
- Dependency installation
- Linting (ESLint)
- Unit tests
- Build verification
- Auto-create Issue on failure
Complete Configuration
Create .github/workflows/frontend-ci.yml:
name: Frontend CI
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
name: Test Frontend
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: |
cd frontend
npm ci
- name: Run lint
run: |
cd frontend
npm run lint
- name: Run tests
run: |
cd frontend
npm run test:run
- name: Build
run: |
cd frontend
npm run build
- name: Create issue on failure
if: failure() && github.event_name == 'push'
uses: actions/github-script@v7
with:
script: |
const title = `Frontend CI Failed on ${context.ref.replace('refs/heads/', '')}`;
const body = `## Frontend CI Failure Report
**Branch:** ${context.ref.replace('refs/heads/', '')}
**Commit:** ${context.sha.substring(0, 7)}
**Workflow Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}
**Triggered by:** @${context.actor}
The frontend CI pipeline has failed. Please check the workflow run for details.
### Steps to investigate:
1. Check the [workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for error logs
2. Review the recent changes in the frontend directory
3. Reproduce the issue locally
---
*This issue was automatically created by GitHub Actions*`;
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'ci-failure,frontend'
});
if (issues.data.length === 0) {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['ci-failure', 'frontend', 'bug']
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issues.data[0].number,
body: `Frontend CI failed again on commit ${context.sha.substring(0, 7)}. [View run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
});
}
Configuration Breakdown
1. Trigger Configuration
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
- push: Triggers on code push to main/master branches
- pull_request: Triggers when creating or updating a PR
- Catches issues before merging to protect main branch
2. Dependency Caching
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
Key Points:
cache: 'npm'automatically caches node_modulescache-dependency-pathspecifies package-lock.json location- Reduces dependency installation from minutes to seconds
3. npm ci vs npm install
- name: Install dependencies
run: |
cd frontend
npm ci
Why npm ci instead of npm install?
| Feature | npm ci | npm install |
|---|---|---|
| Speed | Faster | Slower |
| package-lock | Strictly follows | May update |
| node_modules | Deletes then installs | Incremental update |
| Use Case | CI/CD environments | Local development |
4. Automated Issue Creation
This is a powerful feature that automatically creates Issues when CI fails:
Core Logic:
- Only triggers on push events and failures (
failure() && github.event_name == 'push') - Checks if an Issue with the same labels already exists
- Creates new Issue if none exists, otherwise comments on existing Issue
- Includes detailed error information and debugging steps
Benefits:
- Immediate problem notification
- Avoids duplicate Issues
- Tracks problem history automatically
- Provides debugging guidance
Part 2: Backend CI/CD Separation
In production environments, we typically separate CI (testing) from CD (deployment) for:
- Clearer separation of concerns
- Independent trigger and failure handling
- More flexible deployment control
Backend CI - Testing Pipeline
Create .github/workflows/backend-ci.yml:
name: Backend CI
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
name: Test Backend
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Install dependencies
run: |
cd backend
go mod download
go mod tidy
- name: Run go vet
run: |
cd backend
go vet ./...
- name: Run tests
run: |
cd backend
go test -v ./...
- name: Create issue on failure
if: failure() && github.event_name == 'push'
uses: actions/github-script@v7
with:
script: |
const title = `Backend CI Failed on ${context.ref.replace('refs/heads/', '')}`;
const body = `## Backend CI Failure Report
**Branch:** ${context.ref.replace('refs/heads/', '')}
**Commit:** ${context.sha.substring(0, 7)}
**Workflow Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}
**Triggered by:** @${context.actor}
The backend CI pipeline has failed. Please check the workflow run for details.
### Steps to investigate:
1. Check the [workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for error logs
2. Review the recent changes in the backend directory
3. Reproduce the issue locally with \`go test -v ./...\`
---
*This issue was automatically created by GitHub Actions*`;
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'ci-failure,backend'
});
if (issues.data.length === 0) {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['ci-failure', 'backend', 'bug']
});
}
Backend CD - Docker Build and Push
Create .github/workflows/backend-cd.yml:
name: Backend CD - Build and Push Docker Image
on:
workflow_run:
workflows: [ "Backend CI" ]
branches: [ main, master ]
types: [ completed ]
workflow_dispatch:
inputs:
docker_tag:
description: 'Custom Docker image tag (optional, defaults to branch-commit)'
required: false
type: string
permissions:
contents: read
packages: write
jobs:
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set Docker metadata
id: meta
run: |
REPO=${{ github.repository }}
REPO_LOWER=$(echo $REPO | tr '[:upper:]' '[:lower:]')
if [ -n "${{ github.event.inputs.docker_tag }}" ]; then
TAG=${{ github.event.inputs.docker_tag }}
else
BRANCH=$(echo ${{ github.ref }} | sed 's|refs/heads/||')
COMMIT=$(echo ${{ github.sha }} | cut -c1-7)
TAG="$BRANCH-$COMMIT"
fi
echo "registry=ghcr.io" >> $GITHUB_OUTPUT
echo "image=ghcr.io/$REPO_LOWER/backend" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./backend
file: ./backend/Dockerfile
push: true
tags: |
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }}
${{ steps.meta.outputs.image }}:latest
cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:buildcache
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:buildcache,mode=max
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
- name: Print deployment info
run: |
echo "✅ Docker image built and pushed successfully!"
echo ""
echo "📦 Image Details:"
echo " Image: ${{ steps.meta.outputs.image }}"
echo " Tag: ${{ steps.meta.outputs.tag }}"
echo ""
echo "🚀 Pull the image:"
echo " docker pull ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }}"
Key CD Configuration Points
1. workflow_run Trigger
on:
workflow_run:
workflows: [ "Backend CI" ]
branches: [ main, master ]
types: [ completed ]
Benefits:
- CD only runs after CI succeeds
- Prevents deploying untested code
- Clear dependency relationship
Important:
- Must explicitly check
workflow_run.conclusion == 'success' - Workflow name must match exactly
2. workflow_dispatch - Manual Trigger
workflow_dispatch:
inputs:
docker_tag:
description: 'Custom Docker image tag'
required: false
type: string
Use Cases:
- Emergency fixes requiring immediate deployment
- Rolling back to previous versions
- Testing CD pipeline
- Rebuilding images
Usage: Navigate to Actions tab in GitHub repo, click the workflow, and select “Run workflow”.
3. GHCR (GitHub Container Registry)
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
Why Choose GHCR?
| Feature | GHCR | Docker Hub |
|---|---|---|
| Integration | Native | Requires extra config |
| Private Repos | Unlimited free | 1 free |
| Speed | Fast | Average |
| Permissions | GitHub permissions | Separate management |
| Best For | GitHub projects | Public images |
4. Docker BuildKit Cache
cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:buildcache
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:buildcache,mode=max
Cache Performance:
- First build: 5-10 minutes
- With cache: 30 seconds - 2 minutes
- Saves 80-90% build time
How it Works:
cache-from: Pull cache layers from registrycache-to: Push cache after buildmode=max: Cache all intermediate layers
Part 3: Multi-Platform Binary Release
For compiled languages like Go or Rust, we can use GitHub Actions for cross-compilation and automatically release multi-platform binaries.
Complete Release Pipeline
Create .github/workflows/release.yml:
name: Release
on:
workflow_run:
workflows: ["Backend CI"]
types: [ completed ]
branches: [ main, master ]
jobs:
release:
name: Build and Release
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Generate version
id: version
run: |
VERSION=$(date -u +'%Y.%m.%d.%H%M%S')
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"
- name: Build frontend
run: |
cd frontend
npm ci
npm run build
- name: Build backend binaries
run: |
cd backend
mkdir -p ../builds
# Windows AMD64
GOOS=windows GOARCH=amd64 go build -o ../builds/backend-windows-amd64.exe .
# Linux AMD64
GOOS=linux GOARCH=amd64 go build -o ../builds/backend-linux-amd64 .
# Linux ARM64
GOOS=linux GOARCH=arm64 go build -o ../builds/backend-linux-arm64 .
# macOS Intel
GOOS=darwin GOARCH=amd64 go build -o ../builds/backend-darwin-amd64 .
# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 go build -o ../builds/backend-darwin-arm64 .
- name: Package frontend build
run: |
cd frontend/dist
tar -czf ../../builds/frontend-dist.tar.gz .
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.version.outputs.VERSION }}
release_name: Release v${{ steps.version.outputs.VERSION }}
body: |
## Release v${{ steps.version.outputs.VERSION }}
Auto-generated release from commit ${{ github.sha }}
### Downloads
- **Windows (AMD64)**: `backend-windows-amd64.exe`
- **Linux (AMD64)**: `backend-linux-amd64`
- **Linux (ARM64)**: `backend-linux-arm64`
- **macOS (Intel)**: `backend-darwin-amd64`
- **macOS (Apple Silicon)**: `backend-darwin-arm64`
- **Frontend**: `frontend-dist.tar.gz`
### Installation
1. Download the appropriate binary for your platform
2. Make it executable (Unix): `chmod +x backend-*`
3. Run: `./backend-*`
draft: false
prerelease: false
- name: Upload Windows Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./builds/backend-windows-amd64.exe
asset_name: backend-windows-amd64.exe
asset_content_type: application/octet-stream
- name: Upload Linux AMD64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./builds/backend-linux-amd64
asset_name: backend-linux-amd64
asset_content_type: application/octet-stream
- name: Upload Frontend Build
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./builds/frontend-dist.tar.gz
asset_name: frontend-dist.tar.gz
asset_content_type: application/gzip
Go Cross-Compilation
Go’s cross-compilation is straightforward with environment variables:
GOOS=<target-os> GOARCH=<target-arch> go build
Supported Platform Combinations
| GOOS | GOARCH | Platform | Use Case |
|---|---|---|---|
| linux | amd64 | Linux 64-bit | Servers, cloud hosts |
| linux | arm64 | Linux ARM64 | Raspberry Pi, ARM servers |
| darwin | amd64 | macOS Intel | Intel Macs |
| darwin | arm64 | macOS ARM | M1/M2 Macs |
| windows | amd64 | Windows 64-bit | Windows PCs |
| windows | 386 | Windows 32-bit | Legacy Windows |
Version Generation Strategy
VERSION=$(date -u +'%Y.%m.%d.%H%M%S')
Example Output: 2025.11.04.142530
Alternative Strategies:
- Semantic Versioning -
v1.2.3 - Git Tag - Based on latest git tag
- Commit Hash -
git rev-parse --short HEAD - Hybrid -
v1.2.3-g<commit-hash>
Recommendations:
- Official releases: use semantic versioning
- Automated releases: use timestamps
- Test versions: add
-betasuffix
Part 4: Code Quality Checks
Linting should run early in the CI process, ideally separate from testing.
Backend Go Linting
Create .github/workflows/backend-lint.yml:
name: Backend Lint
on:
push:
branches: [ main, master ]
paths:
- 'backend/**'
- '.github/workflows/backend-lint.yml'
pull_request:
branches: [ main, master ]
paths:
- 'backend/**'
jobs:
lint:
name: Go Vet Linting
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Install dependencies
run: |
cd backend
go mod download
go mod tidy
- name: Run go vet
id: govet
continue-on-error: true
run: |
cd backend
OUTPUT=$(go vet ./... 2>&1 || true)
echo "$OUTPUT"
echo "vet_output<<EOF" >> $GITHUB_OUTPUT
echo "$OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
if [ ! -z "$OUTPUT" ]; then
exit 1
fi
- name: Create issue on failure
if: failure() && github.event_name == 'push'
uses: actions/github-script@v7
with:
script: |
const title = `⚠️ Backend Go Vet Issues on ${context.ref.replace('refs/heads/', '')}`;
const vetOutput = `${{ steps.govet.outputs.vet_output }}`;
const body = `## Go Vet Linting Issues
**Branch:** ${context.ref.replace('refs/heads/', '')}
**Commit:** ${context.sha.substring(0, 7)}
### Go Vet Output
\`\`\`
${vetOutput}
\`\`\`
### Steps to fix:
1. Run \`go vet ./...\` locally in the backend directory
2. Fix the issues and commit the changes
---
*This issue was automatically created by GitHub Actions*`;
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'linting,backend'
});
if (issues.data.length === 0) {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['linting', 'backend', 'code-quality']
});
}
Path Filtering
on:
push:
paths:
- 'backend/**'
- '.github/workflows/backend-lint.yml'
Benefits:
- Only triggers when relevant files change
- Saves CI minutes
- Improves execution efficiency
Best Practices:
- Include the workflow file itself to test workflow changes
- Use
paths-ignoreto exclude irrelevant files - Combine with
branchesfor better control
Best Practices
1. Workflow Organization
.github/workflows/
├── frontend-ci.yml # Frontend CI
├── frontend-lint.yml # Frontend linting
├── backend-ci.yml # Backend CI
├── backend-lint.yml # Backend linting
├── backend-cd.yml # Backend CD
└── release.yml # Release management
Principles:
- One workflow per clear task
- Separate CI from CD
- Independent lint jobs
- Chain workflows with workflow_run
2. Caching Strategies
npm Dependencies
- uses: actions/setup-node@v4
with:
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
Go Modules
- uses: actions/setup-go@v5
with:
cache: true
cache-dependency-path: backend/go.sum
Docker Image Layers
- uses: docker/build-push-action@v5
with:
cache-from: type=registry,ref=myimage:buildcache
cache-to: type=registry,ref=myimage:buildcache,mode=max
Cache Performance:
| Operation | Without Cache | With Cache | Savings |
|---|---|---|---|
| npm install | 2-3 min | 10-20 sec | 85% |
| go mod download | 1-2 min | 5-10 sec | 90% |
| Docker build | 5-10 min | 30 sec-2 min | 80% |
3. Secret Management
Adding Secrets
- Go to repository Settings → Secrets and variables → Actions
- Click “New repository secret”
- Enter name and value
Using in Workflows
- name: Deploy to production
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.sh
Secret Types:
- Repository secrets - Available to single repository
- Organization secrets - Shared across organization repos
- Environment secrets - Specific to environments (e.g., production)
Important Notes:
- Secrets are automatically masked in logs
- Avoid using sensitive secrets in PRs
- Use Environment protection rules for deployment restrictions
4. Concurrency Control
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Effect:
- New commits cancel old workflows on same branch
- Saves CI time
- Prevents resource waste
Use Cases:
- Frequent commits on dev branches
- Workflows where intermediate states don’t matter
- PR preview deployments
Not Suitable For:
- Production deployment workflows
- Workflows requiring complete execution history
- Stateful operations
5. Matrix Builds
For testing multiple versions:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [18, 20, 22]
exclude:
- os: macos-latest
node-version: 18
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Benefits:
- Automatically generates multiple jobs
- Parallel execution
- Covers multiple environments
6. Conditional Execution
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy.sh
- name: Comment on PR
if: github.event_name == 'pull_request'
run: echo "PR #${{ github.event.pull_request.number }}"
- name: Cleanup on failure
if: failure()
run: ./cleanup.sh
Common Conditions:
success()- All previous steps succeededfailure()- Any step failedalways()- Always executecancelled()- Workflow was cancelledgithub.ref == 'refs/heads/main'- Main branchgithub.event_name == 'push'- Push event
Common Issues and Solutions
1. workflow_run Not Triggering
Problem: CD workflow with workflow_run doesn’t trigger after CI completes
Causes:
- Workflow file not in default branch
- Workflow name mismatch
- Permission issues
Solution:
on:
workflow_run:
workflows: [ "Backend CI" ] # Must match CI name exactly
types: [ completed ]
2. Insufficient GITHUB_TOKEN Permissions
Problem: Pushing Docker images or creating Releases fails
Solution:
permissions:
contents: write # For creating Releases
packages: write # For pushing to GHCR
issues: write # For creating Issues
pull-requests: write # For PR comments
3. Cache Not Working
Problem: Dependencies downloaded every time
Causes:
- Cache key changes
- Cache expired (7 days)
- Cache size limit exceeded (10GB)
Solution:
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
4. Slow Docker Builds
Optimization Strategies:
- Use BuildKit Cache
cache-from: type=registry,ref=myimage:buildcache
cache-to: type=registry,ref=myimage:buildcache,mode=max
- Optimize Dockerfile Layer Order
# Put rarely changing layers first
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Put frequently changing layers last
COPY . .
RUN npm run build
- Use Multi-Stage Builds
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
5. Timeout Issues
Default Timeout: 6 hours (per job)
Set Custom Timeout:
jobs:
build:
timeout-minutes: 30 # Job level
steps:
- name: Long running task
timeout-minutes: 10 # Step level
run: ./slow-task.sh
Performance Optimization
Before vs After Optimization
| Optimization | Before | After | Improvement |
|---|---|---|---|
| Dependency Install | 3 min | 15 sec | 91% |
| Docker Build | 8 min | 1.5 min | 81% |
| Total Execution | 15 min | 3 min | 80% |
Optimization Checklist
- Enable dependency caching
- Use Docker BuildKit cache
- Optimize Dockerfile layer order
- Use path filtering for unnecessary triggers
- Concurrency control to cancel old runs
- Matrix builds for parallel testing
- Minimize dependency installation
- Pre-build base images
Advanced Topics
1. Reusable Workflows
Create a reusable workflow:
# .github/workflows/reusable-docker-build.yml
name: Reusable Docker Build
on:
workflow_call:
inputs:
image-name:
required: true
type: string
context:
required: true
type: string
outputs:
image-tag:
value: ${{ jobs.build.outputs.tag }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.meta.outputs.tag }}
steps:
# Build steps...
Use reusable workflow:
jobs:
build-backend:
uses: ./.github/workflows/reusable-docker-build.yml
with:
image-name: backend
context: ./backend
2. Scheduled Tasks
name: Scheduled Tasks
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight
- cron: '0 */6 * * *' # Every 6 hours
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Cleanup old artifacts
run: echo "Cleaning up..."
Cron Expression Format:
┌─────────── minute (0 - 59)
│ ┌───────── hour (0 - 23)
│ │ ┌─────── day (1 - 31)
│ │ │ ┌───── month (1 - 12)
│ │ │ │ ┌─── weekday (0 - 6) (0 is Sunday)
│ │ │ │ │
* * * * *
Common Examples:
0 0 * * *- Daily at midnight0 */6 * * *- Every 6 hours0 9 * * 1-5- Weekdays at 9 AM*/15 * * * *- Every 15 minutes
Learning Resources
Official Documentation
Community Resources
- Awesome Actions - Curated list of Actions
- Actions Toolkit - Toolkit for building custom Actions
- Act - Run GitHub Actions locally
Useful Tools
- actionlint - Workflow syntax checker
- GitHub Actions VSCode Extension
- Cron Expression Generator
Summary
In this guide, we’ve covered:
-
Fundamentals
- GitHub Actions core concepts
- Workflow file structure
- Triggers and events
-
CI/CD Pipelines
- Frontend CI: testing, building, deployment
- Backend CI/CD separation architecture
- Automated Docker image building
-
Automation
- Auto-create Issues on failure
- Multi-platform binary releases
- GitHub Release management
-
Best Practices
- Caching optimization strategies
- Secret security management
- Concurrency control
- Matrix builds
-
Advanced Techniques
- Reusable workflows
- Custom Actions
- Multi-environment deployment
- Scheduled tasks
Next Steps
Now that you’ve mastered GitHub Actions fundamentals, you can:
- Implement these configurations in your own projects
- Adjust and optimize based on your needs
- Explore GitHub Marketplace for more tools
- Develop custom Actions for special requirements
- Follow upcoming articles on Kubernetes deployment, monitoring integration, etc.