Bluewoo HRMS
Deployment

Multi-Service CI/CD

GitHub Actions workflow for deploying HRMS web, api, and ai services

Multi-Service CI/CD

This guide explains the GitHub Actions workflow for deploying all three HRMS services (web, api, ai) to Google Cloud Run.

Overview

AspectDetails
CI/CD PlatformGitHub Actions
Container RegistryGoogle Artifact Registry
ComputeGoogle Cloud Run
AuthenticationWorkload Identity Federation (keyless)

Service Architecture

┌─────────────────────────────────────────────────────────────────┐
│                      GitHub Actions                              │
│                                                                  │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Build   │    │  Build   │    │  Build   │                  │
│  │   Web    │    │   API    │    │   AI     │                  │
│  └────┬─────┘    └────┬─────┘    └────┬─────┘                  │
│       │               │               │                          │
│       ▼               ▼               ▼                          │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │              Google Artifact Registry                    │   │
│  │   hrms-web:sha   hrms-api:sha   hrms-ai:sha             │   │
│  └─────────────────────────────────────────────────────────┘   │
│       │               │               │                          │
│       ▼               ▼               ▼                          │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Deploy  │    │  Deploy  │    │  Deploy  │                  │
│  │   Web    │    │   API    │    │   AI     │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                      Google Cloud Run                            │
│                                                                  │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │ hrms-web │    │ hrms-api │    │ hrms-ai  │                  │
│  │ Next.js  │    │  NestJS  │    │ Express  │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
└─────────────────────────────────────────────────────────────────┘

Prerequisites

Before setting up CI/CD:

  1. GCP Resources Created:

    • Artifact Registry repository
    • Service accounts (deploy + runtime)
    • Workload Identity Federation
    • Cloud SQL, Redis, etc.
  2. GitHub Repository Configured:

    • Secrets added
    • Environments created (staging, production)
  3. Dockerfiles Created:

    • apps/web/Dockerfile
    • apps/api/Dockerfile
    • apps/ai/Dockerfile

GitHub Repository Secrets

Add these secrets in GitHub → Settings → Secrets → Actions:

SecretDescription
GCP_PROJECT_IDbluewoo-hrms
GCP_REGIONeurope-west6
GCP_WORKLOAD_IDENTITY_PROVIDERprojects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider
GCP_SERVICE_ACCOUNTgithub-actions@bluewoo-hrms.iam.gserviceaccount.com

GitHub Environments

Create two environments in GitHub → Settings → Environments:

staging

  • No protection rules
  • Deploys automatically on merge to main

production

  • Required reviewers: Add yourself
  • Wait timer: 5 minutes (optional)
  • Deployment branches: main only

Complete Workflow File

Create .github/workflows/deploy-hrms.yml:

name: Deploy HRMS

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
  REGION: ${{ secrets.GCP_REGION }}
  REGISTRY: ${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/hrms-images

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # ============================================
  # JOB 1: Build and Test
  # ============================================
  build-and-test:
    name: Build & Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npm run types:check

      - name: Unit tests
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        if: always()

  # ============================================
  # JOB 2: Build Docker Images
  # ============================================
  build-images:
    name: Build Images
    needs: build-and-test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    outputs:
      web-image: ${{ steps.build.outputs.web-image }}
      api-image: ${{ steps.build.outputs.api-image }}
      ai-image: ${{ steps.build.outputs.ai-image }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Google Auth
        id: auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Configure Docker for Artifact Registry
        run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet

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

      - name: Build and Push Web
        uses: docker/build-push-action@v5
        with:
          context: .
          file: apps/web/Dockerfile
          push: true
          tags: ${{ env.REGISTRY }}/hrms-web:${{ github.sha }}
          cache-from: type=gha,scope=web
          cache-to: type=gha,mode=max,scope=web

      - name: Build and Push API
        uses: docker/build-push-action@v5
        with:
          context: .
          file: apps/api/Dockerfile
          push: true
          tags: ${{ env.REGISTRY }}/hrms-api:${{ github.sha }}
          cache-from: type=gha,scope=api
          cache-to: type=gha,mode=max,scope=api

      - name: Build and Push AI
        uses: docker/build-push-action@v5
        with:
          context: .
          file: apps/ai/Dockerfile
          push: true
          tags: ${{ env.REGISTRY }}/hrms-ai:${{ github.sha }}
          cache-from: type=gha,scope=ai
          cache-to: type=gha,mode=max,scope=ai

      - name: Output image tags
        id: build
        run: |
          echo "web-image=${{ env.REGISTRY }}/hrms-web:${{ github.sha }}" >> $GITHUB_OUTPUT
          echo "api-image=${{ env.REGISTRY }}/hrms-api:${{ github.sha }}" >> $GITHUB_OUTPUT
          echo "ai-image=${{ env.REGISTRY }}/hrms-ai:${{ github.sha }}" >> $GITHUB_OUTPUT

  # ============================================
  # JOB 3: Deploy to Staging
  # ============================================
  deploy-staging:
    name: Deploy Staging
    needs: build-images
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Deploy API to Staging
        id: deploy-api
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-api-staging
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.api-image }}
          flags: |
            --service-account=hrms-runtime@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
            --vpc-connector=hrms-connector
            --allow-unauthenticated
            --min-instances=0
            --max-instances=3
            --memory=512Mi
            --cpu=1
            --set-secrets=DATABASE_URL=hrms-database-url:latest,JWT_SECRET=hrms-jwt-secret:latest,REDIS_URL=hrms-redis-url:latest
            --add-cloudsql-instances=${{ env.PROJECT_ID }}:${{ env.REGION }}:hrms-db

      - name: Deploy Web to Staging
        id: deploy-web
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-web-staging
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.web-image }}
          env_vars: |
            NEXT_PUBLIC_API_URL=${{ steps.deploy-api.outputs.url }}
          flags: |
            --allow-unauthenticated
            --min-instances=0
            --max-instances=3
            --memory=512Mi
            --cpu=1
            --set-secrets=AUTH_SECRET=hrms-auth-secret:latest,GOOGLE_CLIENT_ID=hrms-google-client-id:latest,GOOGLE_CLIENT_SECRET=hrms-google-client-secret:latest

      - name: Deploy AI to Staging
        id: deploy-ai
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-ai-staging
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.ai-image }}
          flags: |
            --allow-unauthenticated
            --min-instances=0
            --max-instances=2
            --memory=1Gi
            --cpu=1
            --set-secrets=MONGODB_URI=hrms-mongodb-uri:latest,OPENAI_API_KEY=hrms-openai-api-key:latest

      - name: Staging URLs
        run: |
          echo "### Staging Deployment Complete! 🚀" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| Service | URL |" >> $GITHUB_STEP_SUMMARY
          echo "|---------|-----|" >> $GITHUB_STEP_SUMMARY
          echo "| Web | ${{ steps.deploy-web.outputs.url }} |" >> $GITHUB_STEP_SUMMARY
          echo "| API | ${{ steps.deploy-api.outputs.url }} |" >> $GITHUB_STEP_SUMMARY
          echo "| AI | ${{ steps.deploy-ai.outputs.url }} |" >> $GITHUB_STEP_SUMMARY

  # ============================================
  # JOB 4: Smoke Tests on Staging
  # ============================================
  smoke-tests:
    name: Smoke Tests
    needs: deploy-staging
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Health Check - API
        run: |
          curl -f https://hrms-api-staging-*.run.app/health || exit 1

      - name: Health Check - Web
        run: |
          curl -f https://hrms-web-staging-*.run.app || exit 1

      - name: Health Check - AI
        run: |
          curl -f https://hrms-ai-staging-*.run.app/health || exit 1

  # ============================================
  # JOB 5: Deploy to Production
  # ============================================
  deploy-production:
    name: Deploy Production
    needs: [build-images, smoke-tests]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Deploy API to Production
        id: deploy-api
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-api
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.api-image }}
          flags: |
            --service-account=hrms-runtime@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
            --vpc-connector=hrms-connector
            --allow-unauthenticated
            --min-instances=1
            --max-instances=10
            --memory=1Gi
            --cpu=2
            --set-secrets=DATABASE_URL=hrms-database-url:latest,JWT_SECRET=hrms-jwt-secret:latest,REDIS_URL=hrms-redis-url:latest
            --add-cloudsql-instances=${{ env.PROJECT_ID }}:${{ env.REGION }}:hrms-db

      - name: Deploy Web to Production
        id: deploy-web
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-web
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.web-image }}
          env_vars: |
            NEXT_PUBLIC_API_URL=https://api.hrms.bluewoo.com
            AUTH_URL=https://app.hrms.bluewoo.com
          flags: |
            --allow-unauthenticated
            --min-instances=1
            --max-instances=10
            --memory=1Gi
            --cpu=2
            --set-secrets=AUTH_SECRET=hrms-auth-secret:latest,GOOGLE_CLIENT_ID=hrms-google-client-id:latest,GOOGLE_CLIENT_SECRET=hrms-google-client-secret:latest

      - name: Deploy AI to Production
        id: deploy-ai
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-ai
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.ai-image }}
          flags: |
            --allow-unauthenticated
            --min-instances=0
            --max-instances=5
            --memory=2Gi
            --cpu=2
            --set-secrets=MONGODB_URI=hrms-mongodb-uri:latest,OPENAI_API_KEY=hrms-openai-api-key:latest

      - name: Production URLs
        run: |
          echo "### Production Deployment Complete! 🎉" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| Service | URL |" >> $GITHUB_STEP_SUMMARY
          echo "|---------|-----|" >> $GITHUB_STEP_SUMMARY
          echo "| Web | https://app.hrms.bluewoo.com |" >> $GITHUB_STEP_SUMMARY
          echo "| API | https://api.hrms.bluewoo.com |" >> $GITHUB_STEP_SUMMARY
          echo "| AI | https://ai.hrms.bluewoo.com |" >> $GITHUB_STEP_SUMMARY

  # ============================================
  # JOB 6: Deploy Preview (PRs only)
  # ============================================
  deploy-preview:
    name: Deploy Preview
    needs: build-images
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      pull-requests: write
    steps:
      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Deploy Web Preview
        id: deploy-preview
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: hrms-web-pr-${{ github.event.pull_request.number }}
          region: ${{ env.REGION }}
          image: ${{ needs.build-images.outputs.web-image }}
          flags: |
            --allow-unauthenticated
            --min-instances=0
            --max-instances=1
            --memory=512Mi

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🚀 **Preview deployed!**\n\n' +
                    '| Service | URL |\n' +
                    '|---------|-----|\n' +
                    '| Web | ${{ steps.deploy-preview.outputs.url }} |'
            })

  # ============================================
  # JOB 7: Cleanup Preview on PR Close
  # ============================================
  cleanup-preview:
    name: Cleanup Preview
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Delete Preview Service
        run: |
          gcloud run services delete hrms-web-pr-${{ github.event.pull_request.number }} \
            --region=${{ env.REGION }} \
            --quiet || true

Workflow Stages

1. Build & Test

  • Runs on every push and PR
  • Installs dependencies
  • Runs linting and type checking
  • Runs unit tests with coverage

2. Build Images

  • Only runs after tests pass
  • Builds Docker images for all 3 services
  • Pushes to Artifact Registry with SHA tag
  • Uses build cache for faster builds

3. Deploy Staging

  • Automatically deploys on merge to main
  • Deploys all services to staging environment
  • Uses staging-specific configuration

4. Smoke Tests

  • Runs after staging deployment
  • Verifies health endpoints respond
  • Blocks production if tests fail

5. Deploy Production

  • Requires manual approval
  • Uses production-specific configuration
  • Higher resource limits
  • Custom domain environment variables

6. Preview (PRs)

  • Creates temporary deployment for PRs
  • Comments URL on PR
  • Auto-deleted when PR closes

Dockerfile Examples

Web (Next.js)

# apps/web/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
COPY apps/web/package*.json ./apps/web/
RUN npm ci

COPY . .
RUN npm run build --workspace=apps/web

FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=8080

COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./.next/static
COPY --from=builder /app/apps/web/public ./public

EXPOSE 8080
CMD ["node", "server.js"]

API (NestJS)

# apps/api/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
COPY apps/api/package*.json ./apps/api/
COPY packages/database/package*.json ./packages/database/
RUN npm ci

COPY . .
RUN npm run build --workspace=apps/api

FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=8080

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/apps/api/dist ./dist
COPY --from=builder /app/packages/database ./packages/database

EXPOSE 8080
CMD ["node", "dist/main.js"]

AI (Express)

# apps/ai/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
COPY apps/ai/package*.json ./apps/ai/
RUN npm ci

COPY . .
RUN npm run build --workspace=apps/ai

FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=8080

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/apps/ai/dist ./dist

EXPOSE 8080
CMD ["node", "dist/index.js"]

Rollback Procedure

Via GitHub Actions

  1. Find previous successful deployment in Actions tab
  2. Click "Re-run all jobs" on that workflow run

Via gcloud CLI

# List revisions
gcloud run revisions list --service=hrms-api --region=europe-west6

# Route traffic to previous revision
gcloud run services update-traffic hrms-api \
  --to-revisions=hrms-api-00005-abc=100 \
  --region=europe-west6

Monitoring Deployments

GitHub Actions Dashboard

  • View workflow runs in Actions tab
  • Check job logs for errors
  • View deployment summary

Cloud Run Console

  • Monitor service health
  • View container logs
  • Check resource usage

Alerts

Set up alerts for failed deployments:

# Add to workflow
- name: Notify on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    channel-id: 'deployments'
    slack-message: 'Deployment failed! ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'

Troubleshooting

Build Fails

  1. Check Docker build logs
  2. Verify Dockerfile syntax
  3. Check for missing dependencies

Deploy Fails

  1. Check service account permissions
  2. Verify secret names exist
  3. Check Cloud Run logs

Auth Fails

  1. Verify Workload Identity setup
  2. Check repository in attribute mapping
  3. Verify service account binding