GitHub Actions has become the go-to CI/CD platform for developers worldwide, and in 2026, its ecosystem of reusable workflows, improved caching, and native container support make it more powerful than ever. In this guide, we’ll build a complete CI/CD pipeline that builds, tests, and deploys a full-stack application using reusable workflows — a pattern that keeps your pipelines DRY and maintainable across multiple repositories.
Why Reusable Workflows Matter
If you’ve ever copied the same workflow YAML across ten repositories, you know the pain. One security patch or config change means updating every single repo. Reusable workflows solve this by letting you define a workflow once and call it from any repository, passing inputs and secrets as needed.
Project Structure
We’ll work with a typical full-stack app:
my-app/
├── frontend/ # React/Vite app
├── backend/ # Node.js API
├── docker-compose.yml
└── .github/
└── workflows/
├── ci.yml # Main pipeline
├── reusable-test.yml # Reusable test workflow
└── reusable-deploy.yml # Reusable deploy workflowStep 1: Create a Reusable Test Workflow
Let’s start with a reusable workflow that can test any Node.js project. This lives in reusable-test.yml:
name: Reusable Test
on:
workflow_call:
inputs:
working-directory:
required: true
type: string
node-version:
required: false
type: string
default: '22'
secrets:
NPM_TOKEN:
required: false
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
- name: Install dependencies
run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Lint
run: npm run lint --if-present
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-${{ inputs.working-directory }}
path: ${{ inputs.working-directory }}/coverage/
retention-days: 7The key here is workflow_call — this trigger makes the workflow callable from other workflows. We accept inputs for configuration and secrets for sensitive data.
Step 2: Create a Reusable Deploy Workflow
Next, a reusable deployment workflow using Docker and SSH:
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-name:
required: true
type: string
dockerfile-path:
required: true
type: string
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
SSH_PRIVATE_KEY:
required: true
DEPLOY_HOST:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ inputs.dockerfile-path }}
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/${{ inputs.image-name }}:latest
${{ secrets.DOCKER_USERNAME }}/${{ inputs.image-name }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ${{ secrets.DOCKER_USERNAME }}/${{ inputs.image-name }}:latest
cd /opt/apps/${{ inputs.image-name }}
docker compose up -d --force-recreateNotice the cache-from and cache-to using GitHub Actions cache (type=gha). This dramatically speeds up Docker builds by caching layers between runs.
Step 3: The Main CI/CD Pipeline
Now we wire everything together in ci.yml:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test-frontend:
uses: ./.github/workflows/reusable-test.yml
with:
working-directory: frontend
node-version: '22'
test-backend:
uses: ./.github/workflows/reusable-test.yml
with:
working-directory: backend
node-version: '22'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
deploy-staging:
needs: [test-frontend, test-backend]
if: github.ref == 'refs/heads/develop'
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image-name: my-app-api
dockerfile-path: backend/Dockerfile
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
DEPLOY_HOST: ${{ secrets.STAGING_HOST }}
deploy-production:
needs: [test-frontend, test-backend]
if: github.ref == 'refs/heads/main'
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
image-name: my-app-api
dockerfile-path: backend/Dockerfile
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
DEPLOY_HOST: ${{ secrets.PRODUCTION_HOST }}This is clean, readable, and powerful. The frontend and backend tests run in parallel, and deployment only proceeds if both pass. Pushing to develop deploys to staging; pushing to main deploys to production.
Pro Tips for GitHub Actions in 2026
- Use
concurrencygroups to cancel outdated runs:concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true } - Pin action versions to commit SHAs instead of tags for supply-chain security:
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 - Use
workflow_dispatchalongsideworkflow_callso reusable workflows can also be triggered manually for debugging - Leverage GitHub Environments with protection rules — require approvals for production deployments
- Cache aggressively — npm, Docker layers, and build artifacts. The
actions/cachev4 supports cross-branch cache restoration
Sharing Reusable Workflows Across Repos
For organization-wide workflows, create a dedicated .github repository and reference workflows from any repo:
jobs:
test:
uses: my-org/.github/.github/workflows/reusable-test.yml@main
with:
working-directory: src
secrets: inherit # Pass all secrets automaticallyThe secrets: inherit keyword (available since 2023) eliminates the need to explicitly pass every secret — it forwards all organization and repository secrets to the called workflow.
Wrapping Up
Reusable workflows transform GitHub Actions from simple automation scripts into a proper CI/CD platform. By extracting common patterns into callable workflows, you reduce duplication, enforce consistency, and make pipeline changes a single-repo operation instead of a multi-repo headache. Start by identifying the workflows you’ve copied most often — those are your first candidates for refactoring into reusable templates.

Leave a Reply