Managing multiple repositories with common CI/CD pipelines quickly becomes a headache. Every time I needed to edit a workflow, whether it was updating a dependency, fixing a security issue, or improving build efficiency. I had to modify the same logic across multiple repositories. It wasn’t sustainable. 🤦🏼‍♀️

I decided to fix this by creating a centralized repository for reusable GitHub Actions workflows, so I only have to maintain them in one place. Here’s how I set it up and why it has made my life so much easier.

Here’s one of my repositories neu-residence-hub old workflow situation looked like:

name: "Build and Publish"
on:
  push:
    branches:
      - main
    tags:
      - release-*
  pull_request:
    types: [opened, synchronize, reopened]
    branches:
      - main

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    steps:
      - name: Setup build env and id
        id: setup_build_env_id
        run: |
          if [[ ${{ github.event_name }} == pull_request ]]; then
            export TAG=pr${{ github.event.number }}-$(date "+%Y%m%d%H%M%S")
          elif [[ ${{ github.event_name }} == push && ${{ github.ref_name }} == "main" ]]; then
            export TAG=main-$(date "+%Y%m%d%H%M%S")
          elif [[ ${{ github.event_name }} == push && ${{ github.ref_name }} == release-* ]]; then
            export TAG=${{ github.ref_name }}
          fi
          echo "TAG=$TAG" >> $GITHUB_OUTPUT          

      - uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.ECR_PUSH_AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.ECR_PUSH_AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-central-1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3 

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
        with:
          registries: "xxxxxxxx"

      - name: Build, tag, and push docker image to Amazon ECR
        env:
          REGISTRY: <account id>.dkr.ecr.eu-central-1.amazonaws.com
          REPOSITORY: neu-residence-hub
          IMAGE_TAG: ${{ steps.setup_build_env_id.outputs.TAG }}
        run: |
         docker buildx create --use
         docker buildx build --platform linux/arm64 -f ./backend/Dockerfile -t $REGISTRY/$REPOSITORY:$IMAGE_TAG --push ./backend/lib/lambdaFunctions/go         

Instead of maintaining separate workflows everywhere, I created a dedicated GitHub repository(SharedWorkflows) where I store all my reusable workflows.

Now, instead of duplicating workflow YAML files, my projects just reference the shared workflow like a function call.

Here’s what the shared workflow looks like:

Reusable Workflow: Docker Build, Scan, and Push

docker-build-scan-push.yaml

on:
  workflow_call:
    inputs:
      repository:
        required: true
        type: string
      dockerfile:
        required: true
        type: string
      imageTag:
        required: true
        type: string
      dockerContext:
        required: false
        type: string
        default: '.'
permissions:
  id-token: write
  contents: read
jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<account id>:role/RolX
          aws-region: eu-central-1
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and tag Docker image
        env:
          REGISTRY: <account id>.dkr.ecr.eu-central-1.amazonaws.com
          REPOSITORY: ${{ inputs.repository }}
          DOCKERFILE: ${{ inputs.dockerfile }}
          IMAGE_TAG: ${{ inputs.imageTag }}
          BUILD_CONTEXT: ${{ inputs.dockerContext }}
        run: |
          docker buildx build --platform linux/arm64 -f $DOCKERFILE -t $REGISTRY/$REPOSITORY:$IMAGE_TAG --load $BUILD_CONTEXT          

      - name: Scan image for vulnerabilities
        uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: '${{ inputs.repository }}:${{ inputs.imageTag }}'
          format: 'table'
          exit-code: '1'
          ignore-unfixed: true
          vuln-type: 'os,library'
          severity: 'CRITICAL,HIGH,LOW'

      - name: Push Docker image to Amazon ECR
        run: |
          docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG          

How My Repos Use the Shared Workflow

Don’t forget to enable access for the shared workflows in this repository from GitHub .

You can find the Access section in Repository>Settings>Actions>General and turn on the following;

  • Accessible from repositories in the ‘ ’ organization

Then, I call the shared workflow one of my repositoriesneu-residence-hub.

This simplifies the setup and ensures that any updates to the pipeline logic are automatically applied across all repositories that use this shared workflow.

Here’s how my neu-residence-hub repo references it:

call_build_scan_push:
    needs: setup
    uses: <organization name>/SharedWorkflows/.github/workflows/docker-build-scan-push.yaml@main
    with:
      repository: "<account id>.dkr.ecr.eu-central-1.amazonaws.com/neu-residence-hub"
      imageTag: ${{ needs.setup.outputs.TAG }}  
      dockerfile: "backend/Dockerfile"
      dockerContext: "./backend/lib/lambdaFunctions/go"

Setting this up took only about an hour, but it has already saved me way more time than that in maintenance work.

If you’re managing multiple repositories and think about get rid of duplicating your CI/CD workflows, I highly recommend centralizing them. It’s one of those things you won’t regret once you do it! 🙌🏻