Build(deps): Bump devcontainers features and stabilize flaky E2E test… #76
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |