Two remaining patterns before we enter GitOps territory: composite actions (step-level reuse) and mono-repo path filtering (build only what changed).

Composite Actions: Reuse at the Step Level

Every job starts the same: checkout → setup-node → npm ci. That's 3 steps repeated in every job. A composite action collapses them into one:

Before: 3 steps per job × 4 jobs = 12 steps - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci After: 1 step per job × 4 jobs = 4 steps - uses: ./.github/actions/setup Encapsulates all three ✓

Create .github/actions/setup/action.yml

name: 'Setup Project'
description: 'Checkout + Node + install deps'

inputs:
  node-version:
    default: '20'
  working-directory:
    default: '.'

runs:
  using: 'composite'
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
        cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
    - shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: npm ci

Use it

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: ./.github/actions/setup  # Replaces 3 steps!
      - run: npm test

Mono-Repo Pipelines: Build Only What Changed

Mono-Repo services/ ├── api/ ├── web/ ├── worker/ packages/ └── shared/ Change Detection Commit changes: services/api/src/index.js → Only api changed Pipelines Triggered ✓ API pipeline runs — Web skipped — Worker skipped

Path-Based Triggering

# .github/workflows/ci-api.yml
name: API CI
on:
  push:
    paths:
      - 'services/api/**'
      - 'packages/shared/**'  # Rebuild if shared code changes too!
      - '.github/workflows/ci-api.yml'
jobs:
  ci:
    uses: ./.github/workflows/reusable-ci.yml
    with:
      working-directory: services/api

Dynamic Detection with dorny/paths-filter

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      api: ${{ steps.filter.outputs.api }}
      web: ${{ steps.filter.outputs.web }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            api:
              - 'services/api/**'
              - 'packages/shared/**'
            web:
              - 'services/web/**'
              - 'packages/shared/**'

  ci-api:
    needs: changes
    if: needs.changes.outputs.api == 'true'
    uses: ./.github/workflows/reusable-ci.yml
    with: { working-directory: services/api }

  ci-web:
    needs: changes
    if: needs.changes.outputs.web == 'true'
    uses: ./.github/workflows/reusable-ci.yml
    with: { working-directory: services/web }

Architect Decision: Mono-Repo vs Multi-Repo Pipelines

Mono-RepoMulti-Repo
Pipeline per servicePath-filtered in one repoEach repo has its own workflows
Shared codepackages/ — path triggers dependentsPublished as npm packages
Reusable workflows./.github/workflows/reusable-*.ymlSeparate repo or org-level
ComplexityHigher (change detection logic)Lower per repo, more repos to manage
Best forTightly coupled services, small teamsIndependent services, larger orgs

🧠 Recall Check

  1. What's the file extension and key property that makes a composite action?
  2. In a mono-repo, what happens when you change packages/shared/?
  3. What's the difference between uses: ./.github/actions/setup and uses: ./.github/workflows/reusable-ci.yml?
Reveal answers
  1. action.yml with runs: using: 'composite'. It defines reusable steps (not jobs).
  2. All services that depend on shared should rebuild. Include 'packages/shared/**' in each service's path filter to trigger their pipelines.
  3. The first is a composite action (a set of steps, used within a job). The second is a reusable workflow (a complete workflow with its own jobs, used as a top-level job in the caller).
You've now completed the push-based CI/CD toolkit. You can build, test, containerize, deploy, and scale pipelines across services. Next: we enter GitOps territory — a fundamentally different (and better) delivery model for Kubernetes.

Next lesson: GitOps Principles — the mental model shift from push to pull.