GitHub Rule Sets: Enforcing Quality Through Status Checks

This is the second article in our series on building secure AWS deployment pipelines. In the previous article, we explored why validation-first pipelines matter. Now we’ll implement the technical foundation: GitHub Rule Sets and parallel validation workflows.

Building Parallel Validation Pipelines

The Problem: Monolithic Workflows Don’t Scale

Our original approach used monolithic workflow files—one for develop branch PRs and another for main branch deployments. Each workflow would sequentially build code, run tests, package assets, and deploy everything with CDK. This worked for a single project, but became inefficient as our monorepo grew.

The problems were clear:

  • Slow feedback: Developers waited 16+ minutes to know if their PR was valid
  • Resource waste: A single failing test would block deployment unnecessarily
  • Merge bottlenecks: Only one validation could run at a time

The Solution: Split CI from CD

I redesigned the pipeline around two core principles:

  • Continuous Integration (CI): Fast validation that runs on PR creation
  • Continuous Deployment (CD): Deployment that only runs after PR approval and merge
NEW: Split Pipeline with Parallel Validation
┌─────────────────────────────┐
│    PR Created               │
│        ↓                    │
│ ┌─────────────────────────┐ │
│ │ CI Pipeline             │ │  ← Parallel validation
│ │                         │ │
│ │ IaC   │ Server │ Webapp │ │  ← Each project
│ │ Build │ Build  │ Build  │ │     independently
│ │ Test  │ Test   │ Test   │ │
│ └─────────────────────────┘ │
│    PR Created               │
│    Status Checks            │  ← GitHub Rule Sets
│        ↓                    │     verify all pass
│    PR Approved              │
│        ↓                    │
│    PR Merged                │
│        ↓                    │
│    CD Pipeline              │  ← Deploy only after
│    (Deploy)                 │     validation passes
└─────────────────────────────┘
Total CI Time: ~2 minutes (parallel)

Implementing GitHub Rule Sets

To make this work, I needed GitHub to understand that a PR is only ready for merge when all project validations pass. This is where Rule Sets shine—they can monitor multiple status checks simultaneously.

{
  "name": "Core Branch Protection",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": ["refs/heads/main", "refs/heads/develop"],
      "exclude": []
    }
  },
  "rules": [
    { "type": "pull_request" },
    {
      "type": "required_status_checks",
      "parameters": {
        "required_status_checks": [
          {
            "context": "CI Checks (IaC, cd serverless && npm run build && npm test)"
          },
          {
            "context": "CI Checks (Serverless, cd serverless && npm run build && npm test)"
          },
          {
            "context": "CI Checks (Webapp, cd serverless && npm run build && npm test)"
          }
        ],
        "strict_required_status_checks_policy": true
      }
    },
    { "type": "deletion" },
    { "type": "non_fast_forward" }
  ],
  "bypass_actors": [{ "actor_type": "RepositoryRole", "actor_id": 5 }]
}

💡 Key Insight: Rule Sets are more powerful than legacy Branch Protection because one set can cover multiple branches (main and develop) with the same rules.

Setting Up Rule Sets

Unlike GitHub Action workflows, Rule Sets require manual setup via the GitHub REST API:

# Apply the rule set using GitHub CLI
gh api repos/{owner}/{repo}/rulesets 
  --method POST 
  --input ruleset.json

Important considerations:

  • Create separate rule sets for different responsibilities (core protection vs. review requirements)
  • Define Rule Sets as JSON files in your repository for version control
  • Allow repository administrators to bypass rules during development (actor_id: 5)
  • Use strict_required_status_checks_policy: true to ensure checks run on the latest code

The Matrix Strategy: Parallel Validation

The magic happens in the CI workflow using GitHub’s Matrix Strategy. Instead of running validations sequentially, each project gets its own parallel job:

name: CI/CD Pipeline

on:
  pull_request:
    types: [opened, synchronize, reopened]
  workflow_dispatch:

permissions:
  contents: read
  checks: write
  pull-requests: read

jobs:
  setup:
    name: Setup Dependencies
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install all dependencies
        run: npm ci

      - name: Cache workspace with dependencies
        uses: actions/cache/save@v4
        with:
          path: .
          key: node-modules-${{ runner.os }}-${{ github.sha }}

  ci-checks:
    name: CI Checks
    runs-on: ubuntu-latest
    needs: [setup]
    strategy:
      matrix:
        task:
          - { task: "IaC", command: "cd iac && npm run build && npm test" }
          - {
              task: "Serverless",
              command: "cd serverless && npm run build && npm test",
            }
          - {
              task: "Webapp",
              command: "cd webapp && npm run build && npm test",
            }

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Cache node_modules
        uses: actions/cache@v4
        with:
          path: .
          key: node-modules-${{ runner.os }}-${{ github.sha }}
          restore-keys: |
            node-modules-${{ runner.os }}-

      - name: ${{ matrix.task }}
        run: ${{ matrix.command }}

  cleanup:
    name: Cache Cleanup
    runs-on: ubuntu-latest
    needs: [ci-checks]
    if: always()
    steps:
      - name: Delete workflow cache
        run: |
          gh cache delete "node-modules-${{ runner.os }}-${{ github.sha }}" --repo ${{ github.repository }} || echo "Cache not found or already deleted"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

How the Matrix Strategy Works

  1. A single job installs all the dependencies and saves to the GitHub cache—Remember this a monorepo with NPM Workspaces so one npm ci is enough for the entire project
  2. The GitHub matrix spawns 3 parallel jobs
    • Each job restores the cache, runs its command, and reports its status
  3. The last job removes the cache to prevent accumulation

Each project now appears as a separate status check in the PR, making it immediately clear which component has issues:

PR Status Checks:
✅ CI Checks (IaC, cd serverless && npm run build && npm test)
✅ CI Checks (Serverless, cd serverless && npm run build && npm test)
❌ CI Checks (Webapp, cd serverless && npm run build && npm test)  ← Clear failure point

Cache Management Strategy

GitHub Actions cache doesn’t support TTL (time-to-live), so I implemented manual cleanup:

  • Setup job: Creates cache with unique key node-modules-{OS}-{SHA}
  • Matrix jobs: Restore from cache, run in parallel (1-2 minutes each)
  • Cleanup job: Deletes cache to prevent accumulation

⚠️ Why cleanup matters: Without cleanup, caches accumulate and can hit GitHub’s storage limits. Each PR creates a unique cache that would otherwise persist.

Key Takeaways

  • Split CI from CD: Validate quickly, deploy only after approval
  • Use Rule Sets: More flexible than legacy branch protection
  • Matrix Strategy: Parallel validation scales with project complexity
  • Cache Management: Manual cleanup prevents storage issues
  • Status Visibility: Each project gets its own check for clear feedback

The next challenge was ensuring that only approved contributors could merge changes—which brings us to our review requirements and code ownership strategy.

What’s Coming Next

We now have parallel validation working, but we need to ensure the right people review the right code. In the next article, we’ll implement branch protection rules with designated reviewers and add cost-effective security scanning.

You’ll learn:

  • Setting up review requirements with CODEOWNERS
  • Implementing smart security scanning without GitHub’s expensive Advanced Security
  • Using path filters to target scans only where needed
  • Balancing security with development velocity

Continue with Part 3: Secure Code Review – Branch Protection and Automated Security Scanning →

Leave a Reply