Skip to content

Build(deps): Bump devcontainers features and stabilize flaky E2E test… #76

Build(deps): Bump devcontainers features and stabilize flaky E2E test…

Build(deps): Bump devcontainers features and stabilize flaky E2E test… #76

Workflow file for this run

name: CD Pipeline
on:
push:
branches: [main]
workflow_dispatch:
# Concurrency strategy:
# - NO workflow-level concurrency group. Each push starts its own CI run immediately.
# This avoids the GitHub Actions limitation where runs pending environment approval
# block the entire concurrency group, preventing newer runs from starting.
# - Deployment jobs use per-environment concurrency (see deploy-production)
# to prevent conflicting deploys to the same environment.
# - CI jobs are stateless and safe to run in parallel across commits.
permissions:
contents: read
id-token: write
packages: write
pull-requests: write
security-events: write
# ════════════════════════════════════════════════════════════════════════════════
# CI JOBS — Run on every trigger
# ════════════════════════════════════════════════════════════════════════════════
jobs:
build:
name: Build .NET Solution
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
global-json-file: global.json
- name: Restore dependencies
run: dotnet restore TechHub.slnx
- name: Build solution
run: dotnet build TechHub.slnx --configuration Release --no-restore
test-unit:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
global-json-file: global.json
- name: Restore dependencies
run: dotnet restore TechHub.slnx
- name: Run unit tests
run: |
dotnet test --project tests/TechHub.Core.Tests/TechHub.Core.Tests.csproj \
--configuration Release --no-restore \
--results-directory TestResults \
--report-xunit-trx --report-xunit-trx-filename core-test-results.trx \
-- --coverage --coverage-output-format cobertura \
--coverage-output ${{ github.workspace }}/TestResults/core-coverage.cobertura.xml
dotnet test --project tests/TechHub.Web.Tests/TechHub.Web.Tests.csproj \
--configuration Release --no-restore \
--results-directory TestResults \
--report-xunit-trx --report-xunit-trx-filename web-test-results.trx \
-- --coverage --coverage-output-format cobertura \
--coverage-output ${{ github.workspace }}/TestResults/web-coverage.cobertura.xml
- name: Upload test results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: unit-test-results
path: '**/TestResults/**'
retention-days: 7
- name: Upload coverage data
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: coverage-unit
path: '**/TestResults/*-coverage.cobertura.xml'
retention-days: 7
test-integration:
name: Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
global-json-file: global.json
- name: Restore dependencies
run: dotnet restore TechHub.slnx
- name: Run integration tests
run: |
dotnet test --project tests/TechHub.Api.Tests/TechHub.Api.Tests.csproj \
--configuration Release \
--no-restore \
--results-directory TestResults \
--report-xunit-trx \
--report-xunit-trx-filename integration-test-results.trx \
-- --coverage --coverage-output-format cobertura \
--coverage-output ${{ github.workspace }}/TestResults/api-coverage.cobertura.xml
dotnet test --project tests/TechHub.Infrastructure.Tests/TechHub.Infrastructure.Tests.csproj \
--configuration Release \
--no-restore \
--results-directory TestResults \
--report-xunit-trx \
--report-xunit-trx-filename infrastructure-test-results.trx \
-- --coverage --coverage-output-format cobertura \
--coverage-output ${{ github.workspace }}/TestResults/infrastructure-coverage.cobertura.xml
- name: Upload test results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: integration-test-results
path: '**/TestResults/**'
retention-days: 7
- name: Upload coverage data
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: coverage-integration
path: '**/TestResults/*-coverage.cobertura.xml'
retention-days: 7
test-powershell:
name: PowerShell Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Pester
shell: pwsh
run: |
Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -Scope CurrentUser
- name: Run Pester tests
shell: pwsh
run: |
$config = New-PesterConfiguration
$config.Run.Path = "tests/powershell"
$config.Run.Throw = $true
$config.Output.Verbosity = "Detailed"
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = "TestResults/pester-results.xml"
$config.TestResult.OutputFormat = "JUnitXml"
Invoke-Pester -Configuration $config
- name: Upload test results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: powershell-test-results
path: TestResults/pester-results.xml
retention-days: 7
test-javascript:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Vitest
run: npm test
lint:
name: Lint & Format Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
global-json-file: global.json
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22'
cache: 'npm'
- name: Restore .NET dependencies
run: dotnet restore TechHub.slnx
- name: Check dotnet format
run: |
dotnet format TechHub.slnx --verify-no-changes --verbosity diagnostic --severity error
- name: Install npm dependencies
run: npm ci
- name: Run markdownlint
run: |
npx markdownlint-cli2 "**/*.md" "#node_modules" "#.tmp"
security:
name: Security Scan
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
global-json-file: global.json
- name: Restore dependencies
run: dotnet restore TechHub.slnx
- name: Run dependency vulnerability scan
run: |
dotnet list TechHub.slnx package --vulnerable --include-transitive
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # 0.36.0
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
skip-dirs: 'node_modules,.tmp'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
if: always()
with:
sarif_file: 'trivy-results.sarif'
category: trivy
codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: csharp, javascript-typescript, actions
build-mode: none
config-file: .github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: codeql
code-coverage:
name: Code Coverage Report
runs-on: ubuntu-latest
needs: [test-unit, test-integration]
if: always()
steps:
- name: Download unit coverage
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage-unit
path: coverage-data/unit
continue-on-error: true
- name: Download integration coverage
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage-integration
path: coverage-data/integration
continue-on-error: true
- name: Install ReportGenerator
run: dotnet tool install -g dotnet-reportgenerator-globaltool
- name: Merge and generate coverage report
run: |
# Find all Cobertura XML files
COVERAGE_FILES=$(find coverage-data -name '*.cobertura.xml' -type f 2>/dev/null | tr '\n' ';')
if [ -z "$COVERAGE_FILES" ]; then
echo "## 📊 Code Coverage Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ No coverage data available. Test jobs may have failed." >> $GITHUB_STEP_SUMMARY
exit 0
fi
echo "Found coverage files: $COVERAGE_FILES"
reportgenerator \
-reports:"$COVERAGE_FILES" \
-targetdir:coverage-report \
-reporttypes:"Cobertura;MarkdownSummaryGithub" \
-verbosity:Warning
# Append Markdown summary to GitHub Step Summary
if [ -f coverage-report/SummaryGithub.md ]; then
echo "## 📊 Code Coverage Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
cat coverage-report/SummaryGithub.md >> $GITHUB_STEP_SUMMARY
fi
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: coverage-report
path: coverage-report/
retention-days: 7
# ══════════════════════════════════════════════════════════════════════════════
# Quality Gate — All CI checks must pass before deployment
# ══════════════════════════════════════════════════════════════════════════════
quality-gate:
name: Quality Gate
runs-on: ubuntu-latest
needs: [build, test-unit, test-integration, test-powershell, test-javascript, lint, security, codeql]
if: always()
steps:
- name: Check quality gate
run: |
echo "## 🎯 Quality Gate Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Check each job result
BUILD_STATUS="${{ needs.build.result }}"
UNIT_STATUS="${{ needs.test-unit.result }}"
INTEGRATION_STATUS="${{ needs.test-integration.result }}"
POWERSHELL_STATUS="${{ needs.test-powershell.result }}"
JAVASCRIPT_STATUS="${{ needs.test-javascript.result }}"
LINT_STATUS="${{ needs.lint.result }}"
SECURITY_STATUS="${{ needs.security.result }}"
CODEQL_STATUS="${{ needs.codeql.result }}"
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
# Build
if [ "$BUILD_STATUS" = "success" ]; then
echo "| 🏗️ Build | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🏗️ Build | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
# Unit Tests
if [ "$UNIT_STATUS" = "success" ]; then
echo "| 🧪 Unit Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🧪 Unit Tests | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
# Integration Tests
if [ "$INTEGRATION_STATUS" = "success" ]; then
echo "| 🔗 Integration Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🔗 Integration Tests | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
# PowerShell Tests
if [ "$POWERSHELL_STATUS" = "success" ]; then
echo "| 🔵 PowerShell Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🔵 PowerShell Tests | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
# JavaScript Tests
if [ "$JAVASCRIPT_STATUS" = "success" ]; then
echo "| 🟡 JavaScript Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🟡 JavaScript Tests | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
# Lint
if [ "$LINT_STATUS" = "success" ]; then
echo "| 📝 Linting & Formatting | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| 📝 Linting & Formatting | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
# Security
if [ "$SECURITY_STATUS" = "success" ]; then
echo "| 🔒 Security Scan | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🔒 Security Scan | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
# CodeQL
if [ "$CODEQL_STATUS" = "success" ]; then
echo "| 🛡️ CodeQL Analysis | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| 🛡️ CodeQL Analysis | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
# Overall result
if [ "$BUILD_STATUS" = "success" ] && \
[ "$UNIT_STATUS" = "success" ] && \
[ "$INTEGRATION_STATUS" = "success" ] && \
[ "$POWERSHELL_STATUS" = "success" ] && \
[ "$JAVASCRIPT_STATUS" = "success" ] && \
[ "$LINT_STATUS" = "success" ] && \
[ "$SECURITY_STATUS" = "success" ] && \
[ "$CODEQL_STATUS" = "success" ]; then
echo "### ✅ All quality gates passed! 🎉" >> $GITHUB_STEP_SUMMARY
exit 0
else
echo "### ❌ Quality gate failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Please fix the failing checks before deploying:**" >> $GITHUB_STEP_SUMMARY
if [ "$BUILD_STATUS" != "success" ]; then
echo "- 🏗️ Build failed - check compilation errors" >> $GITHUB_STEP_SUMMARY
fi
if [ "$UNIT_STATUS" != "success" ] || [ "$INTEGRATION_STATUS" != "success" ] || [ "$POWERSHELL_STATUS" != "success" ] || [ "$JAVASCRIPT_STATUS" != "success" ]; then
echo "- 🧪 Tests failed - see test results in job logs" >> $GITHUB_STEP_SUMMARY
fi
if [ "$LINT_STATUS" != "success" ]; then
echo "- 📝 Code formatting issues - run \`dotnet format\` and \`markdownlint-cli2 --fix\`" >> $GITHUB_STEP_SUMMARY
fi
if [ "$SECURITY_STATUS" != "success" ]; then
echo "- 🔒 Security scan found vulnerabilities - review Security tab" >> $GITHUB_STEP_SUMMARY
fi
if [ "$CODEQL_STATUS" != "success" ]; then
echo "- 🛡️ CodeQL analysis found issues - review Security tab" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "📖 See [testing documentation](docs/testing-strategy.md) for help" >> $GITHUB_STEP_SUMMARY
exit 1
fi
# ════════════════════════════════════════════════════════════════════════════════
# DEPLOYMENT JOBS — Only run on push/manual.
# All deployment jobs require quality-gate to pass first.
# Every deployment runs the full Bicep template — ARM is idempotent and only
# redeploys resources whose desired state actually changed.
# ════════════════════════════════════════════════════════════════════════════════
# ────────────────────────────────────────────
# Build & Push Docker images to ghcr.io
# Runs as soon as quality-gate passes
# ────────────────────────────────────────────
build-and-push:
name: Build & Push Docker Images
runs-on: ubuntu-latest
needs: quality-gate
concurrency:
group: deploy-build-and-push
cancel-in-progress: false
outputs:
image-tag: ${{ steps.tag.outputs.image-tag }}
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate image tag
id: tag
run: echo "image-tag=$(date -u +'%Y%m%d%H%M%S')" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Build and push images
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./scripts/Build-Images.ps1 `
-Tag "${{ steps.tag.outputs.image-tag }}"
- name: Clean up old production images (keep last 2)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ORG="techhubms"
for PKG in techhub-api techhub-web; do
echo "Cleaning up old production images for ${PKG} (keeping last 2)..."
# List production versions: tagged, no pr- prefix, sorted newest-first, skip first 2
OLD_IDS=$(gh api "/orgs/${ORG}/packages/container/${PKG}/versions?per_page=100" \
--jq '[.[] | select(
(.metadata.container.tags | length) > 0 and
(any(.metadata.container.tags[]; startswith("pr-")) | not)
)] | sort_by(.created_at) | reverse | .[2:] | .[].id' \
2>/dev/null || true)
if [ -z "$OLD_IDS" ]; then
echo " Nothing to clean up for ${PKG}"
continue
fi
while IFS= read -r VERSION_ID; do
echo " Deleting old version ${VERSION_ID}..."
gh api --method DELETE \
"/orgs/${ORG}/packages/container/${PKG}/versions/${VERSION_ID}" \
--silent \
&& echo " [OK] Deleted ${VERSION_ID}" \
|| echo " [WARN] Failed to delete ${VERSION_ID}"
done <<< "$OLD_IDS"
done
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build-and-push
concurrency:
group: deploy-production
cancel-in-progress: false
environment:
name: production
url: https://${{ steps.deploy.outputs.web-url }}
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Check for infrastructure changes
id: changes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the SHA of the last successful production deployment
LAST_DEPLOY_SHA=$(gh api "repos/${{ github.repository }}/deployments?environment=production&per_page=10" \
--jq '[.[] | select(.payload.status == "success" or .status == "success")] | .[0].sha // empty' 2>/dev/null || echo "")
if [ -z "$LAST_DEPLOY_SHA" ]; then
echo "No previous successful deployment found — deploying full infrastructure"
echo "infra-changed=true" >> "$GITHUB_OUTPUT"
else
echo "Last successful deployment: $LAST_DEPLOY_SHA"
CHANGED=$(git diff --name-only "$LAST_DEPLOY_SHA" HEAD -- infra/ scripts/ || echo "forced")
if [ -n "$CHANGED" ]; then
echo "Infrastructure/scripts changes detected:"
echo "$CHANGED"
echo "infra-changed=true" >> "$GITHUB_OUTPUT"
else
echo "No infrastructure changes — using fast deploy path"
echo "infra-changed=false" >> "$GITHUB_OUTPUT"
fi
fi
- name: Azure Login
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true
- name: Install Az.PostgreSql module
if: steps.changes.outputs.infra-changed == 'true'
shell: pwsh
run: Install-Module Az.PostgreSql -Force -Scope CurrentUser -ErrorAction Stop
- name: Deploy infrastructure (Phase 1)
if: steps.changes.outputs.infra-changed == 'true'
shell: pwsh
env:
POSTGRES_ADMIN_PASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
ADMIN_IP_ADDRESSES: ${{ vars.ADMIN_IP_ADDRESSES }}
AZURE_AD_CLIENT_SECRET: ${{ secrets.AZURE_AD_CLIENT_SECRET }}
GHCR_PAT: ${{ secrets.GHCR_PAT }}
NEWSLETTER_UNSUBSCRIBE_SECRET: ${{ secrets.NEWSLETTER_UNSUBSCRIBE_SECRET }}
WILDCARD_CERT_HUB_MS: ${{ secrets.WILDCARD_CERT_HUB_MS }}
WILDCARD_CERT_XEBIA_MS: ${{ secrets.WILDCARD_CERT_XEBIA_MS }}
run: |
./scripts/Deploy-Infrastructure.ps1 -Mode deploy
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Deploy applications via Bicep (full deploy)
if: steps.changes.outputs.infra-changed == 'true'
id: deploy-bicep
shell: pwsh
run: |
./scripts/Deploy-Applications.ps1 `
-Mode deploy `
-ImageTag "${{ needs.build-and-push.outputs.image-tag }}"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Deploy applications (fast image update)
if: steps.changes.outputs.infra-changed == 'false'
id: deploy-fast
shell: pwsh
run: |
./scripts/Deploy-Application.ps1 `
-Tag "${{ needs.build-and-push.outputs.image-tag }}"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Set deployment output
id: deploy
run: |
echo "web-url=tech.hub.ms" >> "$GITHUB_OUTPUT"
- name: Deployment summary
run: |
echo "## Production Deployment Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "**Image Tag**: ${{ needs.build-and-push.outputs.image-tag }}" >> $GITHUB_STEP_SUMMARY
echo "**Deployed By**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
echo "**Web**: https://${{ steps.deploy.outputs.web-url }}" >> $GITHUB_STEP_SUMMARY
- name: Create Application Insights release annotation
shell: pwsh
run: |
./scripts/Add-AppInsightsAnnotation.ps1 `
-SubscriptionId "bc8ab567-c645-4e51-9317-992203eb369a" `
-ResourceGroupName "rg-techhub-prod" `
-AppInsightsName "appi-techhub-prod" `
-AnnotationId "${{ github.run_id }}" `
-ReleaseName "${{ needs.build-and-push.outputs.image-tag }}" `
-Commit "${{ github.sha }}" `
-DeployedBy "${{ github.actor }}" `
-RunUrl "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# ────────────────────────────────────────────
# Production E2E tests — run after deployment
# Uses the same E2E test suite as PR preview environments,
# but skips performance tests (Category=Performance) and
# dev-environment-only tests (Category=DevEnvironment) such as
# GoogleAnalyticsTests which assert GA is absent (true in dev, false in prod).
# ────────────────────────────────────────────
production-e2e:
name: E2E Tests (Production)
runs-on: ubuntu-latest
needs: deploy-production
concurrency:
group: test-e2e-production
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
global-json-file: global.json
- name: Restore dependencies
run: dotnet restore TechHub.slnx
- name: Build solution
run: dotnet build TechHub.slnx --configuration Release --no-restore
- name: Install Playwright dependencies
run: |
cd tests/TechHub.E2E.Tests
pwsh bin/Release/net10.0/playwright.ps1 install chrome --with-deps
- name: Wait for deployment to stabilize
run: sleep 60
- name: Run E2E tests against production
run: |
dotnet test --project tests/TechHub.E2E.Tests/TechHub.E2E.Tests.csproj \
--configuration Release \
--no-build \
--results-directory TestResults \
--report-xunit-trx \
--report-xunit-trx-filename e2e-production-test-results.trx \
-- --filter-not-trait "Category=Performance" --filter-not-trait "Category=DevEnvironment"
env:
E2E_BASE_URL: https://tech.hub.ms
- name: Upload test results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-production-test-results
path: '**/TestResults/**'
retention-days: 7
- name: Upload Playwright traces
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: playwright-traces-production
path: tests/TechHub.E2E.Tests/bin/Release/net10.0/playwright-traces/
retention-days: 7