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.

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=trueACTIONS_STEP_DEBUG=true
Local Testing with act
# Install act (macOS) brew install act # Run workflow locally act push
Best Practices
- Use caching – Saves build time
- Never hardcode secrets – Always use Repository Secrets
- Limit concurrency – Prevents parallel deployments
- Set timeout – Stops hanging jobs
- Use specific versions –
@v4instead 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.


