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:

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