CI/CD OCD

Badge claim: Power of the Shell

With a dashboard (and QR code gathering) web app running as an Azure Static Web App, linked to a separate Azure Functions app for the backend that handles all communication with Dataverse, we needed a proper setup for efficient development, collaboration and deployment.

Using Azure DevOps to host both repos, we used Azure Pipelines for CI/CD.

The backend runs .NET 10 in Linux, using the DotNetCoreCLI@2 and AzureFunctionApp@2 tasks for build and deployment.

trigger:
  branches:
    include:
      - main

pr:
  branches:
    include:
      - main

variables:
  buildConfiguration: "Release"
  azureSubscription: "Microsoft Azure Sponsorship(30b24b6e-ef03-42c4-bba5-20a33afd68e4)"
  functionAppName: "itera-scope-creepers-api"

stages:
  - stage: BuildAndDeploy
    displayName: "Build & Deploy"
    jobs:
      - job: Build
        displayName: "Build API Function App"
        pool:
          vmImage: "ubuntu-latest"

        steps:
          - checkout: self

          - task: UseDotNet@2
            displayName: "Use .NET SDK 10.x"
            inputs:
              packageType: "sdk"
              version: "10.x"

          - task: DotNetCoreCLI@2
            displayName: "Restore NuGet packages"
            inputs:
              command: "restore"
              projects: "API.csproj"

          - task: DotNetCoreCLI@2
            displayName: "Build"
            inputs:
              command: "build"
              projects: "API.csproj"
              arguments: "--configuration $(buildConfiguration)"
              publishWebProjects: false

          - task: DotNetCoreCLI@2
            displayName: "Test (all *Tests.csproj projects if present)"
            inputs:
              command: "test"
              projects: "**/*Tests.csproj"
              arguments: "--configuration $(buildConfiguration)"
              publishTestResults: true

          - task: DotNetCoreCLI@2
            displayName: "Publish function app"
            inputs:
              command: "publish"
              projects: "API.csproj"
              arguments: "--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/publish"
              publishWebProjects: false
              zipAfterPublish: true

          - task: AzureFunctionApp@2
            displayName: "Deploy Azure Function App"
            inputs:
              azureSubscription: "$(azureSubscription)"
              appType: "functionAppLinux"
              appName: "$(functionAppName)"
              package: "$(Pipeline.Workspace)/**/*.zip"

The frontend is a client-side rendered React app, using Vite as the bundler and pnpm as the package manager for increased security, and is both built and deployed using the AzureStaticWebApp@0 task.

trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

variables:
  NODE_VERSION: '22.22.0'

steps:
  - task: NodeTool@0
    displayName: 'Use Node.js $(NODE_VERSION)'
    inputs:
      versionSpec: '$(NODE_VERSION)'

  - script: |
      corepack enable
      pnpm config set node-linker hoisted
      pnpm install --frozen-lockfile
    displayName: 'Install dependencies with pnpm'

  - script: pnpm build
    displayName: 'Build app'

  - task: AzureStaticWebApp@0
    displayName: 'Deploy to Azure Static Web App (itera-scope-creepers)'
    inputs:
      azure_static_web_apps_api_token: '$(AZURE_STATIC_WEB_APPS_API_TOKEN)'
      app_location: '/'
      api_location: ''
      output_location: 'dist'

For local development, the SWA CLI is used to emulate the linked backend.

CI/CD FTW! 🤓