Skip to main content
Back to Blog
16 August 202514 min read

GitHub Actions: Building Modern CI/CD Pipelines

GitHub ActionsCI/CDDevOpsAutomation

Creating efficient CI/CD workflows with GitHub Actions. Reusable workflows, matrix builds, security scanning, and deployment strategies.


GitHub Actions: Building Modern CI/CD Pipelines

GitHub Actions provides powerful CI/CD capabilities directly integrated with your repository. Understanding workflow syntax, reusable components, and best practices enables building efficient, secure automation pipelines.

Workflow Fundamentals

Workflow Structure

# .github/workflows/ci.yml name: CI Pipeline on: push: branches: [main, develop] pull_request: branches: [main] workflow_dispatch: inputs: environment: description: 'Environment to deploy' required: true default: 'staging' type: choice options: - staging - production env: NODE_VERSION: '20' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts path: dist/ retention-days: 7

Trigger Events

EventDescriptionUse Case
pushBranch commitsCI on feature branches
pull_requestPR eventsPR validation
workflow_dispatchManual triggerOn-demand deployments
scheduleCron scheduleNightly builds
releaseRelease eventsProduction deployments

Matrix Builds

Testing Across Versions

jobs: test: name: Test (${{ matrix.os }}, Node ${{ matrix.node }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] node: [18, 20, 22] exclude: - os: windows-latest node: 18 include: - os: ubuntu-latest node: 20 coverage: true steps: - uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Upload coverage if: matrix.coverage uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage/lcov.info

Reusable Workflows

Defining a Reusable Workflow

# .github/workflows/reusable-build.yml name: Reusable Build Workflow on: workflow_call: inputs: node-version: description: 'Node.js version' required: false type: string default: '20' environment: description: 'Target environment' required: true type: string secrets: npm-token: description: 'NPM authentication token' required: false outputs: artifact-name: description: 'Name of uploaded artifact' value: ${{ jobs.build.outputs.artifact-name }} jobs: build: name: Build runs-on: ubuntu-latest outputs: artifact-name: ${{ steps.artifact.outputs.name }} steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} cache: 'npm' registry-url: 'https://npm.pkg.github.com' - name: Install dependencies run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.npm-token }} - name: Build run: npm run build env: ENVIRONMENT: ${{ inputs.environment }} - name: Set artifact name id: artifact run: echo "name=build-${{ inputs.environment }}-${{ github.sha }}" >> $GITHUB_OUTPUT - name: Upload artifact uses: actions/upload-artifact@v4 with: name: ${{ steps.artifact.outputs.name }} path: dist/

Calling a Reusable Workflow

# .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: build: uses: ./.github/workflows/reusable-build.yml with: node-version: '20' environment: production secrets: npm-token: ${{ secrets.NPM_TOKEN }} deploy: needs: build runs-on: ubuntu-latest environment: production steps: - name: Download artifact uses: actions/download-artifact@v4 with: name: ${{ needs.build.outputs.artifact-name }} path: dist/ - name: Deploy to production run: | echo "Deploying ${{ needs.build.outputs.artifact-name }}"

Composite Actions

Creating a Composite Action

# .github/actions/setup-project/action.yml name: Setup Project description: Setup Node.js project with dependencies inputs: node-version: description: 'Node.js version' required: false default: '20' install-command: description: 'Install command' required: false default: 'npm ci' cache-key-prefix: description: 'Cache key prefix' required: false default: 'deps' outputs: cache-hit: description: 'Whether cache was hit' value: ${{ steps.cache.outputs.cache-hit }} runs: using: composite steps: - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} - name: Get npm cache directory id: npm-cache-dir shell: bash run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT - name: Cache dependencies id: cache uses: actions/cache@v4 with: path: | ${{ steps.npm-cache-dir.outputs.dir }} node_modules key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-node${{ inputs.node-version }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ inputs.cache-key-prefix }}-${{ runner.os }}-node${{ inputs.node-version }}- - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' shell: bash run: ${{ inputs.install-command }}

Using Composite Actions

jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup project uses: ./.github/actions/setup-project with: node-version: '20' - name: Build run: npm run build

Security Scanning

Complete Security Pipeline

name: Security on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 0 * * 1' # Weekly on Monday jobs: secrets-scan: name: Secret Scanning runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Gitleaks scan uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} dependency-scan: name: Dependency Scanning 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: NPM Audit run: npm audit --audit-level=high - name: Snyk scan uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high sast-scan: name: SAST Scanning runs-on: ubuntu-latest permissions: security-events: write steps: - uses: actions/checkout@v4 - name: Semgrep scan uses: semgrep/semgrep-action@v1 with: config: >- p/security-audit p/secrets p/owasp-top-ten p/typescript - name: CodeQL analysis uses: github/codeql-action/init@v3 with: languages: javascript,typescript - name: Perform CodeQL analysis uses: github/codeql-action/analyze@v3 container-scan: name: Container Scanning runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build image run: docker build -t app:${{ github.sha }} . - name: Trivy scan uses: aquasecurity/trivy-action@master with: image-ref: app:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload results uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif'

Deployment Strategies

Blue-Green Deployment

name: Blue-Green Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: eu-west-1 - name: Get current target group id: current run: | CURRENT=$(aws elbv2 describe-listeners \ --listener-arn ${{ vars.LISTENER_ARN }} \ --query 'Listeners[0].DefaultActions[0].TargetGroupArn' \ --output text) echo "target-group=$CURRENT" >> $GITHUB_OUTPUT - name: Determine new target group id: new run: | if [[ "${{ steps.current.outputs.target-group }}" == "${{ vars.BLUE_TG_ARN }}" ]]; then echo "target-group=${{ vars.GREEN_TG_ARN }}" >> $GITHUB_OUTPUT echo "color=green" >> $GITHUB_OUTPUT else echo "target-group=${{ vars.BLUE_TG_ARN }}" >> $GITHUB_OUTPUT echo "color=blue" >> $GITHUB_OUTPUT fi - name: Deploy to ${{ steps.new.outputs.color }} run: | # Deploy to new target group aws ecs update-service \ --cluster ${{ vars.ECS_CLUSTER }} \ --service ${{ vars.ECS_SERVICE }}-${{ steps.new.outputs.color }} \ --force-new-deployment - name: Wait for deployment run: | aws ecs wait services-stable \ --cluster ${{ vars.ECS_CLUSTER }} \ --services ${{ vars.ECS_SERVICE }}-${{ steps.new.outputs.color }} - name: Health check run: | # Run health checks against new deployment ./scripts/health-check.sh ${{ steps.new.outputs.color }} - name: Switch traffic run: | aws elbv2 modify-listener \ --listener-arn ${{ vars.LISTENER_ARN }} \ --default-actions Type=forward,TargetGroupArn=${{ steps.new.outputs.target-group }} - name: Notify on failure if: failure() uses: slackapi/slack-github-action@v1 with: payload: | { "text": "Deployment failed for ${{ github.repository }}", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": ":x: Deployment failed\n*Repository:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Workflow:* ${{ github.workflow }}" } } ] } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

OIDC Authentication

AWS OIDC Integration

name: Deploy with OIDC on: push: branches: [main] permissions: id-token: write contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole aws-region: eu-west-1 role-session-name: GitHubActions-${{ github.run_id }} - name: Deploy run: | aws s3 sync ./dist s3://my-bucket/

Key Takeaways

  1. Reusable workflows: Extract common patterns into reusable workflows

  2. Composite actions: Package related steps into composite actions

  3. Matrix builds: Test across multiple configurations efficiently

  4. OIDC authentication: Use OIDC instead of long-lived credentials

  5. Security scanning: Integrate SAST, SCA, and secret scanning

  6. Caching: Cache dependencies and build outputs to speed up workflows

  7. Environments: Use environments for deployment protection rules

  8. Artifacts: Share data between jobs with artifacts

GitHub Actions provides flexible, powerful CI/CD capabilities. Invest in reusable components and security scanning for maintainable, secure pipelines.

Share this article