Stackvora.tech

GitHub Actions: Build, Test, Deploy in 60 Lines of YAML

GitHub Actions provides a powerful, event-driven automation framework directly integrated within GitHub repositories. This article details how to…

GitHub Actions provides a powerful, event-driven automation framework directly integrated within GitHub repositories. This article details how to construct a robust Continuous Integration/Continuous Deployment (CI/CD) pipeline using GitHub Actions, focusing on building, testing, and deploying a sample Node.js application in a concise YAML definition.

Understanding GitHub Actions Fundamentals

GitHub Actions workflows are defined in YAML files (typically .github/workflows/*.yml) within your repository. Each workflow is triggered by specific events and comprises one or more "jobs." A "job" is a set of "steps" that execute on a "runner," which is a virtual machine (e.g., Ubuntu, Windows, macOS) or a container. "Steps" can be commands executed directly on the runner or pre-built "actions" from the GitHub Marketplace.

Workflow Structure Overview

  • name: A human-readable name for your workflow.
  • on: Defines the events that trigger the workflow (e.g., push, pull_request, workflow_dispatch).
  • jobs: A collection of jobs that run in parallel by default, or sequentially if dependencies are defined.
  • jobs..runs-on: Specifies the type of runner to use (e.g., ubuntu-latest, windows-2022).
  • jobs..steps: A sequence of tasks to be performed within the job.
  • uses: Executes a community or custom action. Format: owner/repo@ref or ./.github/actions/my-action.
  • run: Executes a command-line script.

Setting Up the Basic CI Pipeline (Build & Test)

Let's start with a foundational CI pipeline for a Node.js application. This will check out the code, install dependencies, and run tests. For this example, we'll assume a standard Node.js project with package.json and a test script defined.

name: CI Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]
  workflow_dispatch: # Allows manual trigger from GitHub UI

jobs:
  build_and_test:
    runs-on: ubuntu-latest # Or 'windows-latest', 'macos-latest'
    strategy:
      matrix:
        node-version: [18.x, 20.x] # Test against multiple Node.js versions

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4 # Action to check out your repository code.
        
      - name: Set up Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4 # Action to set up a Node.js environment.
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm' # Caches node modules for faster subsequent runs.

      - name: Install dependencies
        run: npm ci # 'npm ci' for clean install in CI environments, uses package-lock.json

      - name: Run tests
        run: npm test # Executes the 'test' script defined in package.json

      - name: Build application (if applicable)
        run: npm run build # Executes the 'build' script. Omit if not needed.

In this snippet:

  • on.push.branches and on.pull_request.branches specify that the workflow runs only for changes on main and develop branches.
  • workflow_dispatch enables manual triggering from the "Actions" tab in GitHub.
  • The strategy.matrix configuration allows testing against Node.js 18 and 20 concurrently, generating two separate job runs. This is crucial for compatibility checks.
  • actions/checkout@v4 checks out your repository code. Always specify a version (e.g., @v4) to ensure stability and reproducibility.
  • actions/setup-node@v4 configures the Node.js environment. The cache: 'npm' option optimizes dependency installation by caching the node_modules directory, significantly reducing job execution time on subsequent runs.
  • npm ci is preferred over npm install in CI/CD environments as it ensures a clean installation based on package-lock.json, preventing dependency drift.
  • npm run build executes your project's build command, generating production-ready assets.

Integrating Advanced Testing and Linting

Beyond basic unit tests, CI pipelines often include linting, code style checks, and more comprehensive testing suites.

Linting and Static Analysis

Adding a linting step ensures code quality and consistency.

...
      - name: Run ESLint
        run: npm run lint # Assumes 'lint' script in package.json (e.g., 'eslint .')
        continue-on-error: true # Allows subsequent steps to run even if linting fails, but marks job as 'failed'
...

continue-on-error: true is useful if you want to see all failures (tests, build, lint) in a single run, rather than stopping at the first linting error. The job will still be marked as failed if any step with this option fails.

Test Reporting and Code Coverage

Tools like Jest can generate JUnit XML reports and code coverage reports. These can be uploaded as artifacts.

...
      - name: Run tests with coverage
        run: npm test -- --coverage --ci --json --outputFile=test-results.json # Example for Jest
        
      - name: Upload test results
        uses: actions/upload-artifact@v4
        with:
          name: test-results-${{ matrix.node-version }}
          path: test-results.json
          retention-days: 7 # How long to keep the artifact

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report-${{ matrix.node-version }}
          path: coverage/lcov-report # Assuming lcov-report is generated by Jest
          retention-days: 7
...

actions/upload-artifact@v4 allows you to store files generated during the workflow run. These artifacts can be downloaded later from the GitHub Actions UI, useful for debugging or compliance.

Implementing Deployment Logic (CD)

Deployment strategies vary widely. Here, we'll demonstrate a simple deployment to an S3 bucket for a static site, triggered only on pushes to the main branch, and dependent on the successful completion of the build_and_test job.

name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]
  workflow_dispatch:

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20.x] # Only build/test with Node 20.x for deployment for simplicity

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Set up Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build application
        run: npm run build

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: build/ # Assuming your build output is in a 'build/' directory
          retention-days: 1 # Only keep artifact for 1 day, useful for quick deploys

  deploy_to_s3:
    needs: build_and_test # This job will only run after build_and_test successfully completes
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' # Only deploy if the push is to the 'main' branch

    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: build/ # Download the artifact to this directory

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4 # Action for setting up AWS access
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1 # Specify your AWS region

      - name: Deploy to S3
        run: aws s3 sync build/ s3://my-static-website-bucket --delete # Syncs local build/ to S3
        env:
          AWS_MAX_ATTEMPTS: 5 # Retry failed S3 operations
          AWS_RETRY_MODE: standard

Key elements of the deployment job:

  • needs: build_and_test: Establishes a dependency, ensuring deployment only proceeds if the CI job is successful.
  • if: github.ref == 'refs/heads/main': A conditional expression that restricts this job to run only for pushes to the main branch. github.ref holds the branch or tag that triggered the workflow.
  • actions/download-artifact@v4: Retrieves the build output generated by the build_and_test job. This decouples the build and deploy environments, making the pipeline more resilient.
  • aws-actions/configure-aws-credentials@v4: An official AWS action for securely configuring AWS credentials. It uses OpenID Connect (OIDC) recommended approach if configured, or falls back to traditional access keys. Ensure AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are stored as GitHub Secrets in your repository settings.
  • aws s3 sync build/ s3://my-static-website-bucket --delete: Uses the AWS CLI (pre-installed on ubuntu-latest runners) to synchronize the local build/ directory with your S3 bucket. --delete ensures that files removed from your local build are also removed from S3.

Common Pitfalls and Troubleshooting

  • Missing package-lock.json (or equivalent): Using npm ci without a package-lock.json will fail. Always commit your lock files.
  • Secrets Management: Never hardcode sensitive information. Use GitHub Secrets (${{ secrets.SECRET_NAME }}) for API keys, AWS credentials, etc.
  • Incorrect Paths: Relative paths in run commands or actions/upload-artifact might be incorrect depending on the runner's current working directory. Always double-check.
  • Race Conditions with Artifacts: If multiple jobs create artifacts with the same name, they can overwrite each other. Use unique names, potentially including ${{ github.run_id }} or job context variables.
  • Action Versions: Always pin actions to specific versions (e.g., @v4, @main, or a full SHA) instead of floating versions (e.g., @latest) to ensure reproducibility and prevent unexpected breaking changes.
  • Debugging Failures: GitHub Actions provides detailed logs for each step. Examine these logs carefully. You can also re-run failed jobs. For complex issues, consider adding echo or ls -laR commands in your steps to inspect the environment or file system.
  • Runner Resource Limits: Be aware of the free tier limits for GitHub-hosted runners (2000 minutes/month for public repos, 500 minutes for private repos, subject to change). Optimize caching and parallelization to minimize run time.

Back to the knowledge base · Ask the AI assistant