ServerlessBase Blog
  • GitHub Actions: CI/CD in Your Repository

    A comprehensive guide to setting up continuous integration and continuous deployment workflows directly in your GitHub repository

    GitHub Actions: CI/CD in Your Repository

    You've just pushed code to your repository, and you want to make sure it builds, tests, and deploys automatically. You could spend hours configuring Jenkins, setting up pipelines, and managing servers. Or you could use GitHub Actions, which runs directly in your repository and handles the heavy lifting for you.

    GitHub Actions is a CI/CD platform integrated into GitHub that lets you automate your software development workflows. When you push code, GitHub Actions can run tests, build your application, and deploy it to production—all without leaving the platform you're already using.

    How GitHub Actions Works

    GitHub Actions uses a workflow system based on YAML files stored in your repository. A workflow is a configurable automated process that includes one or more jobs. Each job runs in a runner environment, which is a virtual machine or container that executes your code.

    The workflow file lives in .github/workflows/ and defines when the workflow runs, which jobs to execute, and what steps each job contains. When you push code that matches the workflow's trigger conditions, GitHub runs the workflow automatically.

    Workflow Triggers

    Workflows can be triggered by various events:

    • Push: Runs when you push code to a branch
    • Pull Request: Runs when you create or update a pull request
    • Schedule: Runs on a cron schedule
    • Manual: Runs when you trigger it from the GitHub UI
    • Repository dispatch: Runs when another repository dispatches an event
    name: CI Pipeline
     
    on:
      push:
        branches: [ main, develop ]
      pull_request:
        branches: [ main ]
      schedule:
        # Run daily at 2 AM UTC
        - cron: '0 2 * * *'

    Defining Jobs and Steps

    A workflow consists of one or more jobs. Each job runs in a fresh runner environment and can depend on other jobs. Jobs are executed in parallel by default, but you can create dependencies between them.

    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
     
          - name: Set up Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
     
          - name: Install dependencies
            run: npm ci
     
          - name: Run tests
            run: npm test

    In this example, the build job runs on an Ubuntu runner, checks out your code, sets up Node.js, installs dependencies, and runs tests. Each step is an action that performs a specific task.

    Using Actions

    Actions are reusable units of code that perform specific tasks. GitHub provides a marketplace of actions, and you can also create your own. Actions can be written in any language and can interact with the GitHub API, file system, and other services.

    Built-in Actions

    GitHub provides several built-in actions for common tasks:

    • actions/checkout: Checks out your repository code
    • actions/setup-node: Sets up a specific Node.js version
    • actions/setup-python: Sets up Python
    • actions/setup-java: Sets up Java
    • actions/upload-artifact: Uploads build artifacts
    • actions/download-artifact: Downloads build artifacts

    Third-party Actions

    The GitHub marketplace contains thousands of community-maintained actions. For example, you can use aws-actions/configure-aws-credentials to configure AWS credentials, or docker/build-push-action to build and push Docker images.

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1

    Managing Secrets and Environment Variables

    Never hardcode secrets in your workflow files. Instead, use GitHub Secrets, which are encrypted values stored in your repository. Secrets are only accessible to workflows in your repository and are never exposed in logs.

    - name: Deploy to production
      env:
        DATABASE_URL: ${{ secrets.DATABASE_URL }}
        API_KEY: ${{ secrets.API_KEY }}
      run: |
        npm run deploy

    You can add secrets in your repository settings under Secrets and variables > Actions. Environment variables can be set at the workflow, job, or step level.

    Matrix Builds

    Matrix builds allow you to run a job multiple times with different configurations. This is useful for testing your code against multiple versions of a language, operating systems, or dependencies.

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

    This workflow runs tests on Node.js 16, 18, and 20 across Ubuntu, Windows, and macOS.

    Caching Dependencies

    To speed up your workflows, you can cache dependencies between runs. GitHub automatically caches dependencies for common languages like Node.js, Python, and Java, but you can also configure custom caches.

    - name: Cache node modules
      uses: actions/cache@v4
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-

    This caches the ~/.npm directory based on the hash of your package-lock.json file. If the lock file hasn't changed, the cache is restored, saving time on subsequent runs.

    Deployment Strategies

    GitHub Actions supports various deployment strategies. You can deploy directly to cloud providers, deploy to servers via SSH, or use container orchestration platforms like Kubernetes.

    Deploying to AWS

    - name: Deploy to AWS Elastic Beanstalk
      uses: einaregilsson/beanstalk-deploy@v21
      with:
        aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        application_name: my-app
        environment_name: MyApp-env
        version_label: ${{ github.sha }}
        region: us-east-1

    Deploying to Kubernetes

    - name: Deploy to Kubernetes
      uses: azure/k8s-deploy@v4
      with:
        manifests: |
          manifests/deployment.yaml
          manifests/service.yaml
        images: |
          myapp:${{ github.sha }}
        kubectl-version: 'latest'

    Deploying to a Server via SSH

    - name: Deploy to server
      uses: appleboy/ssh-action@v1.0.3
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /var/www/myapp
          git pull origin main
          npm install
          npm run build
          npm run restart

    Conditional Execution

    You can make jobs or steps conditional based on the outcome of previous steps or on branch names. This allows you to skip certain jobs or steps under specific conditions.

    - name: Run only on main branch
      if: github.ref == 'refs/heads/main'
      run: npm run deploy
     
    - name: Run only on pull requests
      if: github.event_name == 'pull_request'
      run: npm run test

    Workflow Permissions

    By default, workflows have read access to repository contents. For workflows that need to write to the repository or perform other actions, you must grant explicit permissions.

    permissions:
      contents: write
      pull-requests: write
      issues: write

    You can also set permissions at the job level:

    jobs:
      deploy:
        permissions:
          contents: write
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - run: npm run deploy

    Best Practices

    Keep Workflows Idempotent

    Your workflows should be idempotent, meaning running them multiple times produces the same result. This prevents issues when workflows are retried or run multiple times.

    Use Caching

    Always cache dependencies to speed up your workflows. GitHub provides built-in caching for common languages, and you can configure custom caches for other dependencies.

    Limit Job Duration

    Set timeouts for long-running jobs to prevent workflows from hanging indefinitely. GitHub has a default timeout of 6 hours, but you can set shorter timeouts for specific jobs.

    jobs:
      build:
        timeout-minutes: 30
        runs-on: ubuntu-latest
        steps:
          - run: npm ci
          - run: npm test

    Use Matrix Builds Wisely

    Matrix builds are powerful but can increase workflow duration. Use them judiciously and consider running tests in parallel across multiple runners rather than creating many matrix combinations.

    Test Locally First

    Before committing workflow files, test them locally using tools like act, which runs GitHub Actions locally. This saves time and prevents workflow failures.

    Monitor Workflow Runs

    Use GitHub's workflow run history to monitor your workflows. Look for failed runs, investigate the logs, and fix any issues. GitHub Actions provides notifications and can be integrated with other tools for alerting.

    Use Self-hosted Runners for Custom Needs

    For workflows that require specific tools, hardware, or network access, consider using self-hosted runners. Self-hosted runners give you full control over the execution environment.

    jobs:
      build:
        runs-on: self-hosted
        steps:
          - run: ./custom-build-script.sh

    Common Workflows

    Full CI/CD Pipeline

    A typical CI/CD pipeline includes:

    1. Linting: Check code quality
    2. Testing: Run unit and integration tests
    3. Building: Compile and package the application
    4. Security Scanning: Check for vulnerabilities
    5. Deploy: Deploy to staging or production
    name: CI/CD Pipeline
     
    on:
      push:
        branches: [ main ]
      pull_request:
        branches: [ main ]
     
    jobs:
      lint:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
          - run: npm ci
          - run: npm run lint
     
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
          - run: npm ci
          - run: npm test
          - run: npm run test:coverage
     
      security:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
          - run: npm ci
          - run: npm audit --audit-level=moderate
     
      build:
        runs-on: ubuntu-latest
        needs: [lint, test, security]
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
          - run: npm ci
          - run: npm run build
          - uses: actions/upload-artifact@v4
            with:
              name: build-output
              path: dist/
     
      deploy:
        runs-on: ubuntu-latest
        needs: build
        if: github.ref == 'refs/heads/main'
        environment:
          name: production
          url: https://myapp.com
        steps:
          - uses: actions/download-artifact@v4
            with:
              name: build-output
          - name: Deploy to server
            run: |
              scp -r dist/* user@server:/var/www/myapp

    Multi-environment Deployment

    For projects with multiple environments (development, staging, production), you can use environment names to restrict deployments to specific environments.

    jobs:
      deploy-staging:
        runs-on: ubuntu-latest
        environment:
          name: staging
          url: https://staging.myapp.com
        steps:
          - uses: actions/checkout@v4
          - run: npm run build
          - run: npm run deploy:staging
     
      deploy-production:
        runs-on: ubuntu-latest
        environment:
          name: production
          url: https://myapp.com
        steps:
          - uses: actions/checkout@v4
          - run: npm run build
          - run: npm run deploy:production

    Troubleshooting

    Workflow Fails on First Run

    If a workflow fails on the first run but succeeds on subsequent runs, it's likely a caching issue. Clear the cache and try again.

    Secrets Not Working

    Verify that secrets are correctly configured in your repository settings. Check that the secret names match exactly in your workflow file.

    Permission Denied Errors

    Ensure your workflow has the necessary permissions. If you're using self-hosted runners, check the runner's permissions.

    Slow Workflow Runs

    Optimize your workflows by:

    • Using caching for dependencies
    • Running tests in parallel
    • Using matrix builds efficiently
    • Removing unnecessary steps

    Conclusion

    GitHub Actions provides a powerful and flexible platform for automating your CI/CD workflows. By leveraging its features—triggers, jobs, steps, actions, caching, and deployment strategies—you can create robust pipelines that run automatically whenever you push code.

    The key to success is keeping your workflows simple, idempotent, and well-documented. Start with a basic workflow for testing, then gradually add more steps as your needs grow. Remember to monitor your workflow runs and continuously improve your pipelines based on feedback and performance metrics.

    Platforms like ServerlessBase can help you manage deployments and infrastructure, but GitHub Actions remains the foundation for automating your code delivery pipeline. Together, they provide a complete solution for modern software development.

    Leave comment