Skip to content

fix(auth): deploy correct client id #140

fix(auth): deploy correct client id

fix(auth): deploy correct client id #140

Workflow file for this run

name: CI - Build and Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
env:
DOTNET_VERSION: '10.0.x'
NODE_VERSION: '24.11.1'
jobs:
detect-changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.changes.outputs.backend }}
frontend: ${{ steps.changes.outputs.frontend }}
docs: ${{ steps.changes.outputs.docs }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Detect changes
uses: dorny/paths-filter@v3
id: changes
with:
filters: |
backend:
- 'src/api/**'
- 'src/lib/**'
- 'Menlo.slnx'
- 'Directory.Build.props'
- 'Directory.Packages.props'
- '.github/workflows/ci.yml'
- '.github/workflows/cd-backend.yml'
frontend:
- 'src/ui/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/ci.yml'
- '.github/workflows/cd-frontend.yml'
docs:
- 'docs/**'
- 'README.md'
backend-tests:
name: Backend - Build and Test
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
# Required for OIDC authentication with Azure
permissions:
id-token: write
contents: read
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: menlo_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
# Authenticate to Azure using federated credentials
- 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 }}
# Create an ephemeral client secret for testing
# This secret is short-lived (1 hour) and automatically deleted after tests
- name: Create ephemeral Azure AD client secret
id: create-secret
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |
DISPLAY_NAME="CI-Ephemeral-${{ github.run_id }}"
# Create a new client secret with 1 hour expiry
SECRET_OUTPUT=$(az ad app credential reset \
--id ${{ secrets.AZUREAD_APP_CLIENT_ID }} \
--display-name "$DISPLAY_NAME" \
--end-date "$(date -u -d '+1 hour' '+%Y-%m-%dT%H:%M:%S+00:00')" \
--append \
--query "password" \
--output tsv)
# Mask the secret to prevent it appearing in logs
echo "::add-mask::$SECRET_OUTPUT"
# List credentials to find the keyId by display name
KEY_ID=$(az ad app credential list \
--id ${{ secrets.AZUREAD_APP_CLIENT_ID }} \
--query "[?displayName=='$DISPLAY_NAME'].keyId | [0]" \
--output tsv)
# Set outputs for use in subsequent steps
echo "client-secret=$SECRET_OUTPUT" >> "$GITHUB_OUTPUT"
echo "key-id=$KEY_ID" >> "$GITHUB_OUTPUT"
echo "✅ Created ephemeral client secret with Key ID: $KEY_ID (expires in 1 hour)"
- name: Restore dependencies
run: dotnet restore
- name: Build solution
run: dotnet build --no-restore --configuration Release
- name: Run unit tests
env:
# Azure AD test configuration
# Note: Tests use mock authentication by default, but these allow
# running integration tests against real Azure AD if needed
AzureAd__Instance: "https://login.microsoftonline.com/"
AzureAd__TenantId: ${{ secrets.AZURE_TENANT_ID }}
AzureAd__ClientId: ${{ secrets.AZUREAD_APP_CLIENT_ID }}
AzureAd__ClientSecret: ${{ steps.create-secret.outputs.client-secret }}
AzureAd__CookieDomain: "localhost"
run: |
dotnet test \
--no-build \
--configuration Release \
--verbosity normal \
--collect:"XPlat Code Coverage" \
--results-directory ./coverage \
--logger trx \
--logger "console;verbosity=detailed"
# Always delete the ephemeral secret, even if tests fail
- name: Delete ephemeral Azure AD client secret
if: always() && steps.create-secret.outcome == 'success' && steps.create-secret.outputs.key-id != ''
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |
az ad app credential delete \
--id ${{ secrets.AZUREAD_APP_CLIENT_ID }} \
--key-id ${{ steps.create-secret.outputs.key-id }}
echo "✅ Ephemeral client secret deleted successfully"
- name: Upload test results
uses: actions/upload-artifact@v5
if: always()
with:
name: backend-test-results
path: |
**/*.trx
coverage/**/*
- name: Upload code coverage to Codecov
uses: codecov/codecov-action@v5
if: always()
with:
directory: ./coverage
flags: backend
name: backend-coverage
token: ${{ secrets.CODECOV_TOKEN }}
continue-on-error: true
backend-security:
name: Backend - Security Scan
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
run: dotnet restore
- name: Run security audit
run: dotnet list package --vulnerable --include-transitive
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: csharp
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
frontend-tests:
name: Frontend - Build and Test
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Angular cache
uses: actions/cache@v4
with:
path: src/ui/web/.angular/cache
key: ${{ runner.os }}-angular-cache-${{ hashFiles('src/ui/web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-angular-cache-
- name: Install dependencies (root)
run: pnpm install --frozen-lockfile
- name: Install dependencies (UI)
working-directory: src/ui/web
run: pnpm install --frozen-lockfile
- name: Lint
working-directory: src/ui/web
run: pnpm run lint
- name: Build applications
working-directory: src/ui/web
run: pnpm run build:all:prod
- name: Create version metadata
working-directory: src/ui/web
run: |
mkdir -p dist/menlo-app
cat > dist/menlo-app/version.json << EOF
{
"commit": "${{ github.sha }}",
"branch": "${{ github.ref_name }}",
"buildNumber": "${{ github.run_number }}",
"buildId": "${{ github.run_id }}",
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")",
"actor": "${{ github.actor }}",
"repository": "${{ github.repository }}"
}
EOF
- name: Verify version metadata
working-directory: src/ui/web
run: |
if [ ! -f "dist/menlo-app/version.json" ]; then
echo "❌ version.json not found"
exit 1
fi
echo "✅ version.json created successfully"
cat dist/menlo-app/version.json
- name: Run tests
working-directory: src/ui/web
run: pnpm run test:all -- --coverage --reporters=default --reporters=junit
env:
NODE_ENV: test
- name: Upload test results
uses: actions/upload-artifact@v5
if: always()
with:
name: frontend-test-results
path: |
src/ui/web/coverage/**/*
src/ui/web/test-results/**/*
- name: Upload code coverage to Codecov
uses: codecov/codecov-action@v5
if: always()
with:
directory: ./src/ui/web/coverage
flags: frontend
name: frontend-coverage
token: ${{ secrets.CODECOV_TOKEN }}
continue-on-error: true
- name: Build Storybook (App)
working-directory: src/ui/web
run: pnpm run build-storybook
- name: Build Storybook (Lib)
working-directory: src/ui/web
run: pnpm run build-storybook:lib
- name: Upload build artifacts
uses: actions/upload-artifact@v5
with:
name: frontend-dist
path: |
src/ui/web/dist/**/*
src/ui/web/storybook-static/**/*
- name: Upload Pages deployment artifact
uses: actions/upload-artifact@v5
with:
name: frontend-pages-dist
path: src/ui/web/dist/menlo-app/**/*
frontend-security:
name: Frontend - Security Scan
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies (root)
run: pnpm install --frozen-lockfile
- name: Install dependencies (UI)
working-directory: src/ui/web
run: pnpm install --frozen-lockfile
- name: Audit dependencies
working-directory: src/ui/web
run: pnpm audit --audit-level moderate
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: typescript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: [backend-tests, frontend-tests]
if: |
always() &&
(needs.backend-tests.result == 'success' || needs.backend-tests.result == 'skipped') &&
(needs.frontend-tests.result == 'success' || needs.frontend-tests.result == 'skipped')
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: menlo_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Restore .NET dependencies
run: dotnet restore
- name: Install pnpm dependencies
run: pnpm install --frozen-lockfile
- name: Install UI dependencies
working-directory: src/ui/web
run: pnpm install --frozen-lockfile
- name: Build backend
run: dotnet build --no-restore --configuration Release
- name: Build frontend
working-directory: src/ui/web
run: pnpm run build:prod
- name: Run integration tests
run: |
dotnet test \
--no-build \
--configuration Release \
--filter "Category=Integration" \
--verbosity normal \
--logger trx \
--logger "console;verbosity=detailed"
env:
ConnectionStrings__DefaultConnection: "Host=localhost;Port=5432;Database=menlo_test;Username=postgres;Password=postgres"
- name: Upload integration test results
uses: actions/upload-artifact@v5
if: always()
with:
name: integration-test-results
path: "**/*.trx"
docs-validation:
name: Documentation Validation
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.docs == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install markdownlint
run: npm install -g markdownlint-cli2
- name: Lint documentation
run: markdownlint-cli2 --config .config/.markdownlint-cli2.jsonc docs/**/*.md README.md
- name: Check for broken links
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: 'yes'
use-verbose-mode: 'no'
config-file: '.github/markdown-link-check-config.json'
quality-gate:
name: Quality Gate
runs-on: ubuntu-latest
needs: [backend-tests, frontend-tests, backend-security, frontend-security, integration-tests, docs-validation]
if: always()
steps:
- name: Check test results
run: |
echo "Backend Tests: ${{ needs.backend-tests.result }}"
echo "Frontend Tests: ${{ needs.frontend-tests.result }}"
echo "Backend Security: ${{ needs.backend-security.result }}"
echo "Frontend Security: ${{ needs.frontend-security.result }}"
echo "Integration Tests: ${{ needs.integration-tests.result }}"
echo "Docs Validation: ${{ needs.docs-validation.result }}"
# Check if any required job failed
if [[ "${{ needs.backend-tests.result }}" == "failure" ]] || \
[[ "${{ needs.frontend-tests.result }}" == "failure" ]] || \
[[ "${{ needs.backend-security.result }}" == "failure" ]] || \
[[ "${{ needs.frontend-security.result }}" == "failure" ]] || \
[[ "${{ needs.integration-tests.result }}" == "failure" ]] || \
[[ "${{ needs.docs-validation.result }}" == "failure" ]]; then
echo "❌ Quality gate failed - one or more required checks failed"
exit 1
fi
echo "✅ Quality gate passed - all checks successful"
- name: Update status
run: echo "CI pipeline completed successfully"