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
| Aspect | Details |
|---|---|
| CI/CD Platform | GitHub Actions |
| Container Registry | Google Artifact Registry |
| Compute | Google Cloud Run |
| Authentication | Workload 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:
-
GCP Resources Created:
- Artifact Registry repository
- Service accounts (deploy + runtime)
- Workload Identity Federation
- Cloud SQL, Redis, etc.
-
GitHub Repository Configured:
- Secrets added
- Environments created (staging, production)
-
Dockerfiles Created:
apps/web/Dockerfileapps/api/Dockerfileapps/ai/Dockerfile
GitHub Repository Secrets
Add these secrets in GitHub → Settings → Secrets → Actions:
| Secret | Description |
|---|---|
GCP_PROJECT_ID | bluewoo-hrms |
GCP_REGION | europe-west6 |
GCP_WORKLOAD_IDENTITY_PROVIDER | projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider |
GCP_SERVICE_ACCOUNT | github-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 || trueWorkflow 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
- Find previous successful deployment in Actions tab
- 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-west6Monitoring 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
- Check Docker build logs
- Verify Dockerfile syntax
- Check for missing dependencies
Deploy Fails
- Check service account permissions
- Verify secret names exist
- Check Cloud Run logs
Auth Fails
- Verify Workload Identity setup
- Check repository in attribute mapping
- Verify service account binding