GitHub Actions: Adding Optional Dependencies Between Jobs — Deploy Backend First, Then Frontend

nmmanas - Sep 16 - - Dev Community

Frontend and backend deployments running one after the other, visually seen in GitHub actions summary for the workflow.

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:

  1. Backend changes only: Deploy only the backend.
  2. Frontend changes only: Deploy only the frontend.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/**'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

GitHub Actions job dependency in a visual format

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:

  1. Code Changes: Each job checks for code changes in its respective directory.
  2. 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.

.
Terabox Video Player