diff --git a/.env.staging b/.env.staging new file mode 100644 index 00000000..f3a8cac1 --- /dev/null +++ b/.env.staging @@ -0,0 +1,7 @@ +# Staging – uses staging API (api-stg.core.ac.uk) +VITE_APP_NAME=CORE Dashboard (staging) +VITE_APP_API_BASE_URL=https://api-stg.core.ac.uk +VITE_API_URL=https://api-stg.core.ac.uk +VITE_IDP_URL=https://api-stg.core.ac.uk +VITE_SENTRY_DSN=https://4d292e22294f991799b49f3ffa238ff7@o318858.ingest.us.sentry.io/4511235416981504 +VITE_GA_TRACKING_CODE= diff --git a/.github/workflows/aks_deploy.yml b/.github/workflows/aks_deploy_prod.yml similarity index 92% rename from .github/workflows/aks_deploy.yml rename to .github/workflows/aks_deploy_prod.yml index 55638f32..9d48ed1d 100644 --- a/.github/workflows/aks_deploy.yml +++ b/.github/workflows/aks_deploy_prod.yml @@ -1,4 +1,4 @@ -name: AKS deployment - core-frontend-dashboard +name: AKS Prod on: push: @@ -12,6 +12,8 @@ env: AKS_CLUSTER_NAME: core-frontend-kubernetes-cluster NAMESPACE: core-frontend DEPLOYMENT_NAME: core-frontend-dashboard + APP_ENV: production + KUBE_DIR: kube/prod SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 jobs: @@ -54,6 +56,7 @@ jobs: docker buildx build \ --platform linux/arm64 \ --push \ + --build-arg APP_ENV=${{ env.APP_ENV }} \ --build-arg SENTRY_DSN=${{ env.SENTRY_DSN }} \ --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ --build-arg GA_TRACKING_CODE=${{ secrets.GA_TRACKING_CODE }} \ @@ -82,13 +85,13 @@ jobs: - name: Apply Service and HTTPRoute run: | - kubectl apply -f kube/service.yaml - kubectl apply -f kube/httproute.yaml + kubectl apply -f ${{ env.KUBE_DIR }}/service.yaml + kubectl apply -f ${{ env.KUBE_DIR }}/httproute.yaml - name: Deploy to AKS run: | export IMAGE="${{ steps.image.outputs.ref }}" - envsubst < kube/deployment.yaml | kubectl apply -f - + envsubst < ${{ env.KUBE_DIR }}/deployment.yaml | kubectl apply -f - - name: Wait for rollout run: | @@ -133,7 +136,7 @@ jobs: run: rm -f .npmrc - name: Build web app - run: pnpm run build + run: pnpm run build:${{ env.APP_ENV }} env: NODE_OPTIONS: --max-old-space-size=4096 SENTRY_DSN: ${{ env.SENTRY_DSN }} diff --git a/.github/workflows/aks_deploy_stage.yml b/.github/workflows/aks_deploy_stage.yml new file mode 100644 index 00000000..0e9f7c47 --- /dev/null +++ b/.github/workflows/aks_deploy_stage.yml @@ -0,0 +1,157 @@ +name: AKS Stage + +on: + push: + branches: [stage] + workflow_dispatch: + +env: + ACR_NAME: corecontainers + IMAGE_NAME: core-frontend-dashboard-stage + RESOURCE_GROUP: core-frontend + AKS_CLUSTER_NAME: core-frontend-kubernetes-cluster + NAMESPACE: core-frontend-stage + DEPLOYMENT_NAME: core-frontend-dashboard + APP_ENV: staging + KUBE_DIR: kube/stage + SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 + +jobs: + build-and-deploy: + runs-on: ubuntu-24.04-arm + environment: azure + permissions: + id-token: write + contents: read + outputs: + release: ${{ steps.image.outputs.release }} + sha: ${{ github.sha }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set image reference + id: image + run: | + TAG=$(echo ${{ github.sha }} | cut -c1-7) + echo "ref=${{ env.ACR_NAME }}.azurecr.io/aks-${{ env.IMAGE_NAME }}:$TAG" >> $GITHUB_OUTPUT + echo "release=$TAG" >> $GITHUB_OUTPUT + + - name: Azure Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Docker ACR Login + run: az acr login --name ${{ env.ACR_NAME }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker build and push + run: | + docker buildx build \ + --platform linux/arm64 \ + --push \ + --build-arg APP_ENV=${{ env.APP_ENV }} \ + --build-arg SENTRY_DSN=${{ env.SENTRY_DSN }} \ + --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ + --build-arg GA_TRACKING_CODE=${{ secrets.GA_TRACKING_CODE }} \ + --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ + --build-arg API_KEY=${{ secrets.API_KEY }} \ + --build-arg SENTRY_RELEASE=${{ steps.image.outputs.release }} \ + -t ${{ steps.image.outputs.ref }} . + + - name: Get AKS credentials + run: | + az aks get-credentials \ + --resource-group ${{ env.RESOURCE_GROUP }} \ + --name ${{ env.AKS_CLUSTER_NAME }} \ + --admin \ + --overwrite-existing + + - name: Sync secrets to AKS + run: | + kubectl create secret generic core-frontend-dashboard-secrets \ + --namespace ${{ env.NAMESPACE }} \ + --from-literal=SENTRY_DSN="${{ env.SENTRY_DSN }}" \ + --from-literal=SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}" \ + --from-literal=GA_TRACKING_CODE="${{ secrets.GA_TRACKING_CODE }}" \ + --from-literal=API_KEY="${{ secrets.API_KEY }}" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Apply Service and HTTPRoute + run: | + kubectl apply -f ${{ env.KUBE_DIR }}/service.yaml + kubectl apply -f ${{ env.KUBE_DIR }}/httproute.yaml + + - name: Deploy to AKS + run: | + export IMAGE="${{ steps.image.outputs.ref }}" + envsubst < ${{ env.KUBE_DIR }}/deployment.yaml | kubectl apply -f - + + - name: Wait for rollout + run: | + kubectl rollout status deployment/${{ env.DEPLOYMENT_NAME }} \ + --namespace ${{ env.NAMESPACE }} \ + --timeout=300s + + upload-sentry-sourcemaps: + name: Upload Sentry sourcemaps (stage) + needs: [build-and-deploy] + runs-on: ubuntu-latest + environment: azure + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.build-and-deploy.outputs.sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Configure npm auth for @oacore + run: | + printf "@oacore:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=%s\n" "${{ secrets.NPM_TOKEN }}" > .npmrc + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Remove npm auth + if: always() + run: rm -f .npmrc + + - name: Build web app + run: pnpm run build:${{ env.APP_ENV }} + env: + NODE_OPTIONS: --max-old-space-size=4096 + SENTRY_DSN: ${{ env.SENTRY_DSN }} + SENTRY_RELEASE: ${{ needs.build-and-deploy.outputs.release }} + GA_TRACKING_CODE: ${{ secrets.GA_TRACKING_CODE }} + API_KEY: ${{ secrets.API_KEY }} + + - name: Create Sentry release + uses: getsentry/action-release@v3 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: oacore + SENTRY_PROJECT: dashboard-pk + with: + environment: production + release: ${{ needs.build-and-deploy.outputs.release }} + sourcemaps: ./dist/assets + url_prefix: '~/assets' diff --git a/.github/workflows/prod-stg-deploy.yml b/.github/workflows/prod-stg-deploy.yml deleted file mode 100644 index 55baf3f7..00000000 --- a/.github/workflows/prod-stg-deploy.yml +++ /dev/null @@ -1,267 +0,0 @@ -name: Container App Prod/Staging Deploy - -on: - push: - branches: - - main - - schedule: - - cron: "0 1 * * *" - - workflow_dispatch: - inputs: - target_env: - description: "Where to deploy" - required: true - type: choice - options: - - staging - - prod - default: staging - branch: - description: "Branch to build (default = main)" - required: true - default: main - -env: - ACR_NAME: corecontainers - IMAGE_NAME: core-frontend-dashboard - - RESOURCE_GROUP: core-frontend - PROD_APP_NAME: core-frontend-dashboard - - STAGING_RESOURCE_GROUP: core-frontend-stage - STAGING_APP_NAME: core-frontend-stage-dashboard - - SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 - PORT: 8080 - - ALLOWED_PROD_BRANCH: main - -permissions: - id-token: write - contents: read - -jobs: - build-and-push: - runs-on: ubuntu-latest - environment: azure - outputs: - tag: ${{ steps.vars.outputs.tag }} - deploy_env: ${{ steps.vars.outputs.deploy_env }} - app_name: ${{ steps.vars.outputs.app_name }} - ref_to_build: ${{ steps.vars.outputs.ref_to_build }} - branch_tag: ${{ steps.vars.outputs.branch_tag }} - image_tag: ${{ steps.vars.outputs.image_tag }} - - steps: - - name: Compute variables - id: vars - shell: bash - run: | - SHA7="$(echo "${GITHUB_SHA}" | cut -c1-7)" - - # ✅ ADDED: schedule always deploys PROD from the frozen branch - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then - DEPLOY_ENV="prod" - REF_TO_BUILD="${{ env.ALLOWED_PROD_BRANCH }}" - - elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - DEPLOY_ENV="${{ inputs.target_env }}" - REF_TO_BUILD="${{ inputs.branch }}" - - # existing: Freeze PROD to only one branch; fail if any other branch is selected for PROD - if [[ "${DEPLOY_ENV}" == "prod" && "${REF_TO_BUILD}" != "${{ env.ALLOWED_PROD_BRANCH }}" ]]; then - echo "::error title=Blocked prod deployment::Prod deployments are only allowed from '${{ env.ALLOWED_PROD_BRANCH }}'. You selected '${REF_TO_BUILD}'." - exit 1 - fi - else - DEPLOY_ENV="staging" - REF_TO_BUILD="${GITHUB_REF_NAME}" - fi - - BRANCH_TAG="$(echo "${REF_TO_BUILD}" | tr '[:upper:]' '[:lower:]' \ - | sed -E 's#[^a-z0-9_.-]+#-#g' | sed -E 's#^-+##; s#-+$##' | cut -c1-40)" - - if [[ "${DEPLOY_ENV}" == "prod" ]]; then - APP_NAME="${{ env.PROD_APP_NAME }}" - IMAGE_TAG="prod" - else - APP_NAME="${{ env.STAGING_APP_NAME }}" - IMAGE_TAG="stg-${BRANCH_TAG}" - fi - - echo "tag=${SHA7}" >> "$GITHUB_OUTPUT" - echo "deploy_env=${DEPLOY_ENV}" >> "$GITHUB_OUTPUT" - echo "app_name=${APP_NAME}" >> "$GITHUB_OUTPUT" - echo "ref_to_build=${REF_TO_BUILD}" >> "$GITHUB_OUTPUT" - echo "branch_tag=${BRANCH_TAG}" >> "$GITHUB_OUTPUT" - echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" - - - - name: Summary - run: | - { - echo "## Deploy plan" - echo "- **Trigger:** ${{ github.event_name }}" - echo "- **Env:** ${{ steps.vars.outputs.deploy_env }}" - echo "- **Branch:** ${{ steps.vars.outputs.ref_to_build }}" - echo "- **App:** ${{ steps.vars.outputs.app_name }}" - echo "- **ACR:** ${{ env.ACR_NAME }}" - echo "- **Image:** ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}" - echo "- **Tag (sha7):** ${{ steps.vars.outputs.tag }}" - echo "- **Tag (env):** ${{ steps.vars.outputs.image_tag }}" - } >> "$GITHUB_STEP_SUMMARY" - - - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ steps.vars.outputs.ref_to_build }} - - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Docker ACR Login - run: az acr login --name ${{ env.ACR_NAME }} - - - name: Docker Build - shell: bash - run: | - if [[ "${{ steps.vars.outputs.deploy_env }}" == "staging" ]]; then - NODE_ENV=development - else - NODE_ENV=production - fi - - docker build \ - --build-arg NODE_ENV=$NODE_ENV \ - --build-arg SENTRY_DSN=${{env.SENTRY_DSN}} \ - --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ - --build-arg GA_TRACKING_CODE=${{secrets.GA_TRACKING_CODE}} \ - --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ - --build-arg API_KEY=${{ secrets.API_KEY }} \ - -t ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }} \ - -t ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.image_tag }} . - - - name: Docker Push - run: | - docker push ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }} - docker push ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.image_tag }} - - deploy: - needs: build-and-push - runs-on: ubuntu-latest - environment: azure - env: - TAG: ${{ needs.build-and-push.outputs.tag }} - IMAGE_TAG: ${{ needs.build-and-push.outputs.image_tag }} - - steps: - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Resolve deploy target - id: target - shell: bash - run: | - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then - ENV="prod" - elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - ENV="${{ inputs.target_env }}" - else - ENV="staging" - fi - - if [[ "$ENV" == "prod" ]]; then - echo "APP=${{ env.PROD_APP_NAME }}" >> $GITHUB_OUTPUT - echo "RG=${{ env.RESOURCE_GROUP }}" >> $GITHUB_OUTPUT - else - echo "APP=${{ env.STAGING_APP_NAME }}" >> $GITHUB_OUTPUT - echo "RG=${{ env.STAGING_RESOURCE_GROUP }}" >> $GITHUB_OUTPUT - fi - - - name: Enforce prod branch freeze - shell: bash - run: | - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - if [[ "${{ inputs.target_env }}" == "prod" && "${{ needs.build-and-push.outputs.ref_to_build }}" != "${{ env.ALLOWED_PROD_BRANCH }}" ]]; then - echo "::error title=Blocked prod deployment::Prod deployments are only allowed from '${{ env.ALLOWED_PROD_BRANCH }}'." - exit 1 - fi - fi - - - name: Deploy to Azure Container App - shell: bash - run: | - REDEPLOY_AT="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-$(date -u +%Y%m%d%H%M%S)" - - az containerapp update \ - --name ${{ steps.target.outputs.APP }} \ - --resource-group ${{ steps.target.outputs.RG }} \ - --image ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ env.TAG }} \ - --container-name ${{ steps.target.outputs.APP }} \ - --set configuration.ingress.targetPort=${{ env.PORT }} \ - --set-env-vars REDEPLOY_AT="$REDEPLOY_AT" - - upload-sentry-sourcemaps: - name: Upload Sentry sourcemaps (prod) - needs: [deploy] - runs-on: ubuntu-latest - environment: azure - permissions: - id-token: write - contents: read - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ needs.build-and-push.outputs.ref_to_build }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Configure npm auth for @oacore - run: | - printf "@oacore:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=%s\n" "${{ secrets.NPM_TOKEN }}" > .npmrc - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Remove npm auth - if: always() - run: rm -f .npmrc - - - name: Build web app - run: pnpm run build - env: - NODE_OPTIONS: --max-old-space-size=4096 - - - name: Create Sentry release - uses: getsentry/action-release@v3 - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: oacore - SENTRY_PROJECT: dashboard-pk - with: - environment: production - sourcemaps: ./dist/assets - url_prefix: '~/assets' diff --git a/Dockerfile b/Dockerfile index 32b7c9dd..1a559cd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM node:24-alpine AS builder ARG NODE_ENV=production +ARG APP_ENV=production ARG BUILD_TARGET=azure ARG NPM_TOKEN ARG SENTRY_DSN="" @@ -10,6 +11,7 @@ ARG GA_TRACKING_CODE="" # VITE_API_URL, VITE_IDP_URL come from committed .env.development / .env.production # SENTRY_AUTH_TOKEN: build-only; enables Vite sourcemaps + Sentry upload (see vite.config.ts) ENV NODE_ENV=${NODE_ENV} \ + APP_ENV=${APP_ENV} \ BUILD_TARGET=${BUILD_TARGET} \ NPM_TOKEN=${NPM_TOKEN} \ SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} \ @@ -34,7 +36,12 @@ RUN set -eux; \ COPY . . -RUN if [ "$NODE_ENV" = "production" ]; then pnpm run build; else pnpm run build:dev; fi +RUN case "$APP_ENV" in \ + staging) pnpm run build:staging ;; \ + development) pnpm run build:dev ;; \ + production) pnpm run build ;; \ + *) pnpm run build ;; \ + esac FROM nginx:1.27-alpine AS runtime diff --git a/kube/preview.yml b/kube/preview/preview.yml similarity index 100% rename from kube/preview.yml rename to kube/preview/preview.yml diff --git a/kube/deployment.yaml b/kube/prod/deployment.yaml similarity index 100% rename from kube/deployment.yaml rename to kube/prod/deployment.yaml diff --git a/kube/httproute.yaml b/kube/prod/httproute.yaml similarity index 100% rename from kube/httproute.yaml rename to kube/prod/httproute.yaml diff --git a/kube/service.yaml b/kube/prod/service.yaml similarity index 100% rename from kube/service.yaml rename to kube/prod/service.yaml diff --git a/kube/stage/deployment.yaml b/kube/stage/deployment.yaml new file mode 100644 index 00000000..892381b6 --- /dev/null +++ b/kube/stage/deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core-frontend-dashboard + namespace: core-frontend-stage + labels: + app: core-frontend-dashboard +spec: + replicas: 1 + revisionHistoryLimit: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: core-frontend-dashboard + template: + metadata: + labels: + app: core-frontend-dashboard + spec: + nodeSelector: + agentpool: userpool + containers: + - name: core-frontend-dashboard + image: ${IMAGE} + env: + - name: NODE_ENV + value: production + imagePullPolicy: IfNotPresent + envFrom: + - secretRef: + name: core-frontend-dashboard-secrets + ports: + - name: http + containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 1000Mi diff --git a/kube/stage/httproute.yaml b/kube/stage/httproute.yaml new file mode 100644 index 00000000..7f54f6d1 --- /dev/null +++ b/kube/stage/httproute.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: core-frontend-dashboard + namespace: core-frontend-stage +spec: + parentRefs: + - name: public-gateway + namespace: gateway-system + hostnames: + - dashboard-stg.core.ac.uk + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: core-frontend-dashboard + port: 80 diff --git a/kube/stage/service.yaml b/kube/stage/service.yaml new file mode 100644 index 00000000..92b3da72 --- /dev/null +++ b/kube/stage/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: core-frontend-dashboard + namespace: core-frontend-stage + labels: + app: core-frontend-dashboard +spec: + type: ClusterIP + selector: + app: core-frontend-dashboard + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP \ No newline at end of file diff --git a/package.json b/package.json index bc5ee066..443e334e 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,11 @@ "build:core-ui": "cd ../core-ui && pnpm run build:lib", "dev": "vite --mode development", "dev:local": "vite --mode local", + "dev:staging": "vite --mode staging", "build": "tsc -b && vite build --mode production", + "build:production": "tsc -b && vite build --mode production", "build:dev": "tsc -b && vite build --mode development", + "build:staging": "tsc -b && vite build --mode staging", "docker:build": "sh scripts/docker-build.sh", "lint": "eslint . --format compact", "preview": "vite preview", diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh index d8d2882e..68dabd86 100755 --- a/scripts/docker-build.sh +++ b/scripts/docker-build.sh @@ -13,6 +13,7 @@ fi # URLs come from committed .env.development / .env.production exec docker build \ --build-arg NODE_ENV="${NODE_ENV:-production}" \ + --build-arg APP_ENV="${APP_ENV:-${NODE_ENV:-production}}" \ --build-arg BUILD_TARGET="${BUILD_TARGET:-azure}" \ --build-arg SENTRY_DSN="$SENTRY_DSN" \ --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" \