Guides & Tutorials15 min read

GitHub Actions: CI/CD Pipeline Simply Explained

Learn how to create automated build, test and deployment pipelines with GitHub Actions. Practical examples for Node.js, Python and Docker.

Jonas Hottler
GitHub Actions: CI/CD Pipeline Simply Explained — Guides & Tutorials

GitHub Actions: CI/CD for Beginners

GitHub Actions automates your entire software development process. Builds, tests, deployments – all directly in GitHub. This guide shows you how to get started.

What is CI/CD?

Continuous Integration (CI): Automatic building and testing on every push.

Continuous Deployment (CD): Automatic deployment after successful tests.

Benefits:

  • Catch errors early
  • Consistent builds
  • Faster releases
  • Less manual work

Your First Workflow

Create .github/workflows/ci.yml:

yaml
name: CI on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm test

What's happening here?

  • Triggered on push and pull request to main
  • Ubuntu runner is started
  • Repository is checked out
  • Node.js is installed
  • Dependencies are installed
  • Tests are executed

Understanding Workflow Syntax

Triggers (on)

yaml
on: # On every push push: # Only specific branches push: branches: [main, develop] # On tags push: tags: ['v*'] # Pull Requests pull_request: branches: [main] # Scheduled (Cron) schedule: - cron: '0 0 * * *' # Daily at midnight # Manually triggerable workflow_dispatch: # On release release: types: [published]

Jobs and Steps

yaml
jobs: build: runs-on: ubuntu-latest steps: - name: Step 1 run: echo "Hello" test: runs-on: ubuntu-latest needs: build # Waits for build steps: - name: Step 1 run: echo "Testing" deploy: runs-on: ubuntu-latest needs: [build, test] # Waits for both if: github.ref == 'refs/heads/main' steps: - name: Deploy run: echo "Deploying"

Practical Examples

Node.js Project with Tests

yaml
name: Node.js CI on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [18, 20, 22] 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 run build --if-present - run: npm test 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

Python with pytest

yaml
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.11' cache: 'pip' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest pytest-cov - name: Run tests with coverage run: pytest --cov=src --cov-report=xml - name: Upload coverage uses: codecov/codecov-action@v4 with: file: ./coverage.xml

Docker Build and Push

yaml
name: Docker on: push: branches: [main] tags: ['v*'] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: | username/app:latest username/app:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max

Deployment to Vercel

yaml
name: Deploy to Vercel on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - 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'

Deployment to Server via SSH

yaml
name: Deploy to Server on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy via SSH uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /var/www/app git pull origin main npm ci npm run build pm2 restart app

Secrets and Environment Variables

Setting Up Secrets

Repository → Settings → Secrets and variables → Actions → New repository secret

Using Secrets

yaml
steps: - name: Use secret env: API_KEY: ${{ secrets.API_KEY }} run: | curl -H "Authorization: Bearer $API_KEY" https://api.example.com

Environment-Specific Secrets

yaml
jobs: deploy: runs-on: ubuntu-latest environment: production # Uses production secrets steps: - name: Deploy env: DATABASE_URL: ${{ secrets.DATABASE_URL }} run: ./deploy.sh

Caching for Faster Builds

npm Cache

yaml
- uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' # Automatic caching!

Manual Caching

yaml
- name: Cache dependencies uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} restore-keys: | ${{ runner.os }}-pip-

Storing Artifacts

Upload Build Artifacts

yaml
- name: Build run: npm run build - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build path: dist/ retention-days: 5

Use Artifacts in Another Job

yaml
jobs: build: runs-on: ubuntu-latest steps: - run: npm run build - uses: actions/upload-artifact@v4 with: name: build path: dist/ deploy: needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: build path: dist/ - run: ./deploy.sh

Matrix Builds

Test on multiple platforms/versions simultaneously:

yaml
jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node: [18, 20, 22] exclude: - os: macos-latest node: 18 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm test

Conditional Execution

yaml
jobs: deploy: if: github.event_name == 'push' && github.ref == 'refs/heads/main' notify: if: failure() # Only on failure needs: [build, test]

Reusable Workflows

Define Workflow

yaml
# .github/workflows/reusable-test.yml name: Reusable Test Workflow on: workflow_call: inputs: node-version: required: true type: string jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} - run: npm ci && npm test

Call Workflow

yaml
# .github/workflows/ci.yml name: CI on: [push] jobs: test: uses: ./.github/workflows/reusable-test.yml with: node-version: '20'

Troubleshooting and Debugging

Enable Debug Logging

Repository → Settings → Secrets → Add:

  • ACTIONS_RUNNER_DEBUG = true
  • ACTIONS_STEP_DEBUG = true

Local Testing with act

bash
# Install act (macOS) brew install act # Run workflow locally act push

Best Practices

  1. Use caching – Saves build time
  2. Never hardcode secrets – Always use Repository Secrets
  3. Limit concurrency – Prevents parallel deployments
  4. Set timeout – Stops hanging jobs
  5. Use specific versions@v4 instead of @latest
yaml
jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 10 concurrency: group: production cancel-in-progress: false

Conclusion

GitHub Actions is a powerful CI/CD tool integrated directly into GitHub. Start with simple workflows and expand gradually.

For more complex DevOps requirements, Balane Tech is happy to help you set up professional CI/CD pipelines.

Tags

GitHub Actions · CI/CD · DevOps · Automation · Tutorial · Deployment