Introduction
It is common to have both backend and frontend code in a single GitHub repository. This setup complicates how we deploy them effectively.
Desired Scenarios
These are the desired scenarios when deploying two systems in a single repository:
- Backend changes only: Deploy only the backend.
- Frontend changes only: Deploy only the frontend.
- Both change: Deploy the backend first, then the frontend.
First two cases are straightforward to achieve. With the third case, it is an interesting problem to force two deployments in order. That also, only if both backend and frontend systems are changed at one go.
Initial Attempt with Separate Workflows
First, I attempted to use two separate workflows. Yet, establishing dependencies between independent workflows proved challenging.
Using Jobs Within the Same Workflow
Alternatively, we can add two separate jobs within the same workflow to deploy the backend and frontend.
name: Backend and Frontend Deploy
on:
push:
paths:
# ensure to trigger only when there is a change to either directory
- 'backend/**'
- 'frontend/**'
jobs:
deploy-backend:
runs-on: ubuntu-latest
# condition to check if there are changes to backend directory
if: ${{ contains(github.event.commits.*.modified, 'backend/') || contains(github.event.commits.*.added, 'backend/') }}
steps:
- name: Deploy Backend
# code to deploy backend
deploy-frontend:
runs-on: ubuntu-latest
# condition to check if there are changes to frontend directory
if: ${{ contains(github.event.commits.*.modified, 'frontend/') || contains(github.event.commits.*.added, 'frontend/') }}
steps:
- name: Deploy Frontend
# code to deploy frontend
This script addresses the first two scenarios: if there's a change in the backend code, the backend deployment runs, and similarly for the frontend. However, we still can't guarantee that the backend deploys first when both codes change.
Attempting to Add Job Dependencies
To address the third scenario, I tried making the frontend deployment job depend on the backend deployment job:
deploy-backend:
runs-on: ubuntu-latest
# Backend deployment steps
deploy-frontend:
needs: deploy-backend # This adds the dependency
runs-on: ubuntu-latest
steps:
- name: Deploy Frontend
# Frontend deployment steps
However, this introduces a problem. Since the frontend deployment depends on the backend deployment's success, if there are no changes to the backend code (and thus no backend deployment), the frontend deployment won't run either.
Introducing the "check-changes" Job
To solve this, we introduce a new job called check-changes. This job checks for changes in the backend or frontend code and outputs its findings for other jobs to use. Both the frontend and backend jobs read this output to decide whether to run. Additionally, the frontend job uses the output to determine if it should wait for the backend deployment-this only happens when there are changes to the backend code.
The beauty of this solution is that if there are changes to either the backend or frontend, they deploy independently. But when both have changes, the backend deploys first, and the frontend waits for it to complete before deploying.
jobs:
check-changes:
runs-on: ubuntu-latest
# define the output variables, and set them to
# the output from `filter` step
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v2
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
This step outputs backend=true
or backend=false
depending on whether there are changes in the backend, and similarly for the frontend. Other steps use these outputs to decide whether to run or skip their respective jobs.
Here's the final code for the job (with inline comments):
name: Backend and Frontend Deploy
on:
workflow_dispatch:
push:
branches:
- main
jobs:
check-changes:
runs-on: ubuntu-latest
# define the output variables, and set them to
# the output from `filter` step
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v2
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
deploy-backend:
# depend on the `check-changes` job to succeed
needs: check-changes
# check if `check-changes` job output `backend='true'`
# or user forced with `deploy-backend` keyword in commit message
if: ${{ needs.check-changes.outputs.backend == 'true' || contains(github.event.head_commit.message, 'deploy-backend') }}
runs-on: ubuntu-latest
steps:
- name: Deploy Backend
# code to deploy backend
deploy-frontend:
# depend on the `check-changes` job to succeed
needs: check-changes
# check if `check-changes` job output `frontend='true'`
# or user forced with `deploy-frontend` keyword in commit message
if: ${{ needs.check-changes.outputs.frontend == 'true' || contains(github.event.head_commit.message, 'deploy-frontend') }}
runs-on: ubuntu-latest
steps:
# This step will force the job to wait for backend deployment
# only if the backend also has changed or forced by `deploy-backend`
# keyword in commit message
- name: Wait for backend deployment
if: ${{ needs.check-changes.outputs.backend == 'true' || contains(github.event.head_commit.message, 'deploy-backend') }}
uses: lewagon/wait-on-check-action@v1.3.1
with:
ref: ${{ github.ref }}
check-name: 'deploy-backend'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- name: Deploy Frontend
# code to deploy frontend
Additionally, we perform a keyword check. Including the keywords deploy-backend
or deploy-frontend
in the commit message forces the deployment of the respective system. This is useful when we need to deploy either service without code changes.
The image above shows both backend and frontend deployments running in parallel. However, our code-level checks ensure that the frontend deployment waits for the backend deployment when necessary
Conclusion
In conclusion, the deployment jobs run based on two conditions:
- Code Changes: Each job checks for code changes in its respective directory.
- Forced Deployment: The presence of deploy- keywords in the commit message forces deployment.
This setup ensures that both frontend and backend are deployed when their code changes. When both have changes, the backend deploys first, followed by the frontend.
Top comments (0)