Skip to content

Fooj CI/CD

Section: 17 — Fooj
Last Updated: 2026-05-30
Scope: Angular 18 SSR build, ACA deployment, pipeline structure


Overview

Fooj uses an Azure DevOps pipeline that builds an Angular 18 SSR (Server-Side Rendering) application, packages it as a Docker container, pushes it to ACR, and deploys it to Azure Container Apps.

Git Push → Build Angular SSR → Docker Build → Push to ACR → Deploy to ACA

Pipeline File

Devops/azure/pipelines/fooj/deploy-fooj.yml

Trigger Configuration

yaml
trigger:
  branches:
    include:
      - main
      - stage
      - staging
  paths:
    include:
      - FoojApp/**

pr:
  branches:
    include:
      - main

parameters:
  - name: environment
    type: string
    default: stg
    values: [stg, prod]
  - name: forceRedeploy
    type: boolean
    default: false

Pipeline Stages

Stage 1 — Lint and Type Check

yaml
- stage: Validate
  jobs:
    - job: LintAndTypeCheck
      pool:
        vmImage: ubuntu-latest
      steps:
        - task: NodeTool@0
          inputs:
            versionSpec: '20.x'

        - script: npm ci
          workingDirectory: FoojApp
          displayName: Install dependencies

        - script: npx ng lint --max-warnings=0
          workingDirectory: FoojApp
          displayName: ESLint

        - script: npx tsc --noEmit
          workingDirectory: FoojApp
          displayName: TypeScript type check

Stage 2 — Unit Tests

yaml
- stage: Test
  dependsOn: Validate
  jobs:
    - job: UnitTests
      steps:
        - script: |
            npm ci
            npx ng test --watch=false --browsers=ChromeHeadless \
              --code-coverage true
          workingDirectory: FoojApp
          displayName: Run unit tests

        - task: PublishTestResults@2
          inputs:
            testResultsFormat: JUnit
            testResultsFiles: FoojApp/test-results/*.xml

        - task: PublishCodeCoverageResults@2
          inputs:
            codeCoverageTool: Cobertura
            summaryFileLocation: FoojApp/coverage/cobertura-coverage.xml

Stage 3 — Angular SSR Build

Angular 18 SSR is built using @angular/ssr (successor to Angular Universal):

yaml
- stage: Build
  dependsOn: Test
  jobs:
    - job: BuildSSR
      steps:
        - script: npm ci
          workingDirectory: FoojApp

        - script: |
            npx ng build --configuration=${{ parameters.environment }}
          workingDirectory: FoojApp
          displayName: Build Angular SSR

        - publish: FoojApp/dist
          artifact: ssr-dist
          displayName: Publish SSR dist artifact

Angular 18 SSR output structure

FoojApp/dist/fooj/
├── browser/         ← Static assets (served by CDN or Node server)
├── server/          ← Node.js SSR server bundle
└── prerendered/     ← Statically pre-rendered routes (if configured)

The Dockerfile copies both browser/ and server/ into the image. At runtime, the Node.js server in server/main.server.mjs serves SSR responses and proxies static assets.

Stage 4 — Docker Build and Push

yaml
- stage: DockerBuild
  dependsOn: Build
  jobs:
    - job: BuildAndPush
      steps:
        - download: current
          artifact: ssr-dist

        - task: Docker@2
          displayName: Build and push SSR image
          inputs:
            command: buildAndPush
            containerRegistry: fooj-${{ parameters.environment }}-acr-connection
            repository: fooj-ssr
            dockerfile: FoojApp/Dockerfile
            buildContext: .
            tags: |
              $(Build.BuildId)
              latest

Dockerfile (Angular 18 SSR)

dockerfile
# Stage 1: Base node image
FROM node:20-alpine AS base
WORKDIR /app

# Stage 2: Build (pre-built artifact is copied in)
FROM base AS runner
COPY dist/fooj/browser ./browser
COPY dist/fooj/server  ./server
COPY dist/fooj/prerendered ./prerendered 2>/dev/null || true

# Install only production dependencies for the server bundle
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

EXPOSE 4000
ENV PORT=4000
ENV NODE_ENV=production

CMD ["node", "server/main.server.mjs"]

Why Alpine?

The Alpine base image keeps the Fooj SSR container under 200 MB. The Angular 18 server bundle has no native Node addons, so Alpine's musl libc is fully compatible.

Stage 5 — Deploy to ACA

yaml
- stage: Deploy
  dependsOn: DockerBuild
  jobs:
    - deployment: DeployToACA
      environment: fooj-${{ parameters.environment }}
      strategy:
        runOnce:
          deploy:
            steps:
              - task: AzureCLI@2
                displayName: Update ACA image
                inputs:
                  azureSubscription: fooj-azure-connection
                  scriptType: bash
                  scriptLocation: inlineScript
                  inlineScript: |
                    ACR="fooj${{ parameters.environment }}acr"
                    RG="fooj-${{ parameters.environment }}-containers-rg"

                    az containerapp update \
                      --name fooj-ssr \
                      --resource-group $RG \
                      --image "${ACR}.azurecr.io/fooj-ssr:$(Build.BuildId)" \
                      --revision-suffix "$(Build.BuildId)"

Deployment uses ACA revision-based rolling update — new traffic is directed to the new revision only after its replicas pass health checks.

Stage 6 — Smoke Test

yaml
- stage: SmokeTest
  dependsOn: Deploy
  jobs:
    - job: Smoke
      steps:
        - script: |
            BASE_URL="https://stg.fooj.sa"
            if [[ "${{ parameters.environment }}" == "prod" ]]; then
              BASE_URL="https://fooj.sa"
            fi

            # Home page returns 200 with SSR content
            HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/")
            if [ "$HTTP_CODE" != "200" ]; then
              echo "Smoke test FAILED: $BASE_URL returned $HTTP_CODE"
              exit 1
            fi

            # Health endpoint
            curl -sf "$BASE_URL/health" | grep -q '"status":"Healthy"'
            echo "Smoke tests PASSED"

Environment-Specific Build Configurations

Angular's environment.ts files map to the --configuration flag:

--configurationEnvironment fileAPI URL
stgenvironment.stg.tshttps://api.stg.fooj.sa
prodenvironment.prod.tshttps://api.fooj.sa
typescript
// FoojApp/src/environments/environment.stg.ts
export const environment = {
  production: false,
  apiBaseUrl: 'https://api.stg.fooj.sa',
  ssrEnabled: true,
};

Rollback

If a deployment causes issues, roll back to the previous revision:

bash
# List revisions
az containerapp revision list \
  --name fooj-ssr \
  --resource-group fooj-prod-containers-rg \
  --query "[].{name:name, active:properties.active, created:properties.createdTime}" \
  -o table

# Activate the previous revision
az containerapp revision activate \
  --revision fooj-ssr--<previous-build-id> \
  --resource-group fooj-prod-containers-rg

# Deactivate the bad revision
az containerapp revision deactivate \
  --revision fooj-ssr--<bad-build-id> \
  --resource-group fooj-prod-containers-rg

Pre-Production Checklist

Before deploying to production:

  • [ ] Staging smoke tests passing
  • [ ] Production Key Vault secrets verified (fooj-prod-kv)
  • [ ] ACR image verified in foojprodacr
  • [ ] DNS records pointing to production ACA environment
  • [ ] ACA custom domain certificate active
  • [ ] Min replicas set to 2 in production ACA app
  • [ ] Monitoring alerts enabled in Azure Monitor

Internal Documentation — Microtec Platform Team