Guides & Tutorials

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
January 23, 2025
15 min read time
GitHub ActionsCI/CDDevOpsAutomationTutorialDeployment
GitHub Actions: CI/CD Pipeline Simply Explained - Guides & Tutorials | Blog

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:

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)

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

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

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

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

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

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

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

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

Environment-Specific Secrets

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

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

Manual Caching

- 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

- 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

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:

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

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

# .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

# .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

# 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
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 ActionsCI/CDDevOpsAutomationTutorialDeployment