Fix inconsistent sidebar toolbar button order when navigating to Browse page #296
Workflow file for this run
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: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| 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-staging, 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 | |
| pull-requests: read | |
| security-events: write | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # CI JOBS — Run on every trigger (push, PR, manual) | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| jobs: | |
| build: | |
| name: Build .NET Solution | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.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: Upload build artifacts | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: build-output | |
| path: | | |
| **/bin/Release/ | |
| **/obj/Release/ | |
| retention-days: 1 | |
| test-unit: | |
| name: Unit Tests | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: always() | |
| with: | |
| name: unit-test-results | |
| path: '**/TestResults/**' | |
| retention-days: 7 | |
| - name: Upload coverage data | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: always() | |
| with: | |
| name: coverage-unit | |
| path: '**/TestResults/*-coverage.cobertura.xml' | |
| retention-days: 7 | |
| test-integration: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: always() | |
| with: | |
| name: integration-test-results | |
| path: '**/TestResults/**' | |
| retention-days: 7 | |
| - name: Upload coverage data | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: always() | |
| with: | |
| name: coverage-integration | |
| path: '**/TestResults/*-coverage.cobertura.xml' | |
| retention-days: 7 | |
| test-e2e: | |
| name: E2E Tests | |
| runs-on: ubuntu-latest | |
| needs: build | |
| services: | |
| postgres: | |
| image: postgres:17-alpine | |
| env: | |
| POSTGRES_DB: techhub | |
| POSTGRES_USER: techhub | |
| POSTGRES_PASSWORD: localdev | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U techhub -d techhub" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.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 --with-deps | |
| - name: Generate dev certificate | |
| run: dotnet dev-certs https | |
| - name: Start API server | |
| run: | | |
| nohup dotnet run --project src/TechHub.Api/TechHub.Api.csproj \ | |
| --launch-profile Development --no-build --configuration Release \ | |
| > /tmp/api-server.log 2>&1 & | |
| echo $! > /tmp/api-server.pid | |
| echo "API server started (PID: $(cat /tmp/api-server.pid))" | |
| env: | |
| AppSettings__Content__CollectionsPath: ${{ github.workspace }}/collections | |
| Database__ConnectionString: "Host=localhost;Port=5432;Database=techhub;Username=techhub;Password=localdev" | |
| - name: Start Web server | |
| run: | | |
| nohup dotnet run --project src/TechHub.Web/TechHub.Web.csproj \ | |
| --launch-profile Development --no-build --configuration Release \ | |
| > /tmp/web-server.log 2>&1 & | |
| echo $! > /tmp/web-server.pid | |
| echo "Web server started (PID: $(cat /tmp/web-server.pid))" | |
| - name: Wait for servers to be ready | |
| run: | | |
| API_READY=false | |
| echo "Waiting for API to be ready (health check must return 200)..." | |
| for i in $(seq 1 120); do | |
| HTTP_CODE=$(curl -sk -o /dev/null -w '%{http_code}' https://localhost:5001/health 2>/dev/null || true) | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo "" | |
| echo "API is ready! (health returned 200)" | |
| API_READY=true | |
| break | |
| fi | |
| echo "Attempt $i - API not ready yet (HTTP $HTTP_CODE)..." | |
| sleep 5 | |
| done | |
| if [ "$API_READY" = "false" ]; then | |
| echo "::error::API server failed to start within timeout" | |
| echo "--- Listening ports ---" | |
| ss -tlnp 2>/dev/null | grep -E '5001|5003' || echo "No relevant ports listening" | |
| echo "--- API Server Log (first 30 lines, for Kestrel binding) ---" | |
| head -30 /tmp/api-server.log || true | |
| echo "--- API Server Log (last 80 lines) ---" | |
| tail -80 /tmp/api-server.log || true | |
| echo "--- API PID check ---" | |
| ps -p $(cat /tmp/api-server.pid 2>/dev/null) 2>/dev/null || echo "Process not running" | |
| exit 1 | |
| fi | |
| WEB_READY=false | |
| echo "Waiting for Web to be ready..." | |
| for i in $(seq 1 60); do | |
| if curl -sk -o /dev/null -w '%{http_code}' https://localhost:5003 2>/dev/null | grep -q '200'; then | |
| echo "" | |
| echo "Web is ready!" | |
| WEB_READY=true | |
| break | |
| fi | |
| echo "Attempt $i - Web not ready yet..." | |
| sleep 5 | |
| done | |
| if [ "$WEB_READY" = "false" ]; then | |
| echo "::error::Web server failed to start within timeout" | |
| echo "--- Listening ports ---" | |
| ss -tlnp 2>/dev/null | grep -E '5001|5003' || echo "No relevant ports listening" | |
| echo "--- Web Server Log (first 30 lines, for Kestrel binding) ---" | |
| head -30 /tmp/web-server.log || true | |
| echo "--- Web Server Log (last 80 lines) ---" | |
| tail -80 /tmp/web-server.log || true | |
| echo "--- Web PID check ---" | |
| ps -p $(cat /tmp/web-server.pid 2>/dev/null) 2>/dev/null || echo "Process not running" | |
| exit 1 | |
| fi | |
| - name: Run E2E tests | |
| 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-test-results.trx | |
| env: | |
| AppSettings__Content__CollectionsPath: ${{ github.workspace }}/collections | |
| Database__ConnectionString: "Host=localhost;Port=5432;Database=techhub;Username=techhub;Password=localdev" | |
| - name: Upload test results | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: always() | |
| with: | |
| name: e2e-test-results | |
| path: '**/TestResults/**' | |
| retention-days: 7 | |
| - name: Upload Playwright traces | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: failure() | |
| with: | |
| name: playwright-traces | |
| path: tests/TechHub.E2E.Tests/bin/Release/net10.0/playwright-traces/ | |
| retention-days: 7 | |
| - name: Upload server logs | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: failure() | |
| with: | |
| name: e2e-server-logs | |
| path: /tmp/*-server.log | |
| retention-days: 7 | |
| test-powershell: | |
| name: PowerShell Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install Pester and dependencies | |
| shell: pwsh | |
| run: | | |
| Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -Scope CurrentUser | |
| Install-Module -Name HtmlToMarkdown -Force -Scope CurrentUser | |
| - name: Run Pester tests | |
| shell: pwsh | |
| run: | | |
| # Pre-load all content-processing functions (required by tests) | |
| . "$PWD/tests/powershell/Initialize-BeforeAll.ps1" | |
| $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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: always() | |
| with: | |
| name: powershell-test-results | |
| path: TestResults/pester-results.xml | |
| retention-days: 7 | |
| lint: | |
| name: Lint & Format Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 | |
| with: | |
| global-json-file: global.json | |
| - name: Setup Node.js | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.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@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 | |
| with: | |
| scan-type: 'fs' | |
| scan-ref: '.' | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| severity: 'CRITICAL,HIGH' | |
| exit-code: '1' | |
| skip-dirs: 'node_modules,.tmp,scripts/data/rss-cache' | |
| - name: Upload Trivy results to GitHub Security | |
| uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 | |
| if: always() | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| codeql: | |
| name: CodeQL Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 | |
| with: | |
| languages: csharp, javascript-typescript, actions | |
| build-mode: none | |
| - name: Perform CodeQL Analysis | |
| uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 | |
| 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| 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-e2e, test-powershell, 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 }}" | |
| E2E_STATUS="${{ needs.test-e2e.result }}" | |
| POWERSHELL_STATUS="${{ needs.test-powershell.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 | |
| # E2E Tests | |
| if [ "$E2E_STATUS" = "success" ]; then | |
| echo "| 🌐 E2E Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| 🌐 E2E 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 | |
| # 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" ] && \ | |
| [ "$E2E_STATUS" = "success" ] && \ | |
| [ "$POWERSHELL_STATUS" = "success" ] && \ | |
| [ "$LINT_STATUS" = "success" ] && \ | |
| [ "$SECURITY_STATUS" = "success" ] && \ | |
| [ "$CODEQL_STATUS" = "success" ]; then | |
| echo "### ✅ All quality gates passed! 🎉" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # PR-specific guidance | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "**Next Steps:**" >> $GITHUB_STEP_SUMMARY | |
| echo "- 🎉 This PR is ready for review and merge!" >> $GITHUB_STEP_SUMMARY | |
| echo "- 📊 Code coverage reports available in artifacts" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| exit 0 | |
| else | |
| echo "### ❌ Quality gate failed" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Please fix the failing checks before merging:**" >> $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" ] || [ "$E2E_STATUS" != "success" ] || [ "$POWERSHELL_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 (NOT on PRs) | |
| # 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. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # ────────────────────────────────────────────── | |
| # Shared infrastructure (ACR) — always runs | |
| # ────────────────────────────────────────────── | |
| deploy-shared-infra: | |
| name: Deploy Shared Infrastructure | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'pull_request' | |
| needs: quality-gate | |
| concurrency: | |
| group: deploy-shared-infra | |
| cancel-in-progress: false | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Azure Login | |
| uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 | |
| with: | |
| creds: ${{ secrets.AZURE_CREDENTIALS }} | |
| - name: Deploy shared infrastructure | |
| shell: pwsh | |
| run: | | |
| ./scripts/Deploy-Infrastructure.ps1 ` | |
| -Environment shared ` | |
| -Mode deploy | |
| # ────────────────────────────────────────────── | |
| # Build & push Docker images | |
| # Runs after shared infra (ACR must exist) | |
| # ────────────────────────────────────────────── | |
| build-and-push: | |
| name: Build & Push Docker Images | |
| runs-on: ubuntu-latest | |
| needs: deploy-shared-infra | |
| 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Generate image tag | |
| id: tag | |
| run: echo "image-tag=$(date -u +'%Y%m%d%H%M%S')" >> "$GITHUB_OUTPUT" | |
| - name: Azure Login | |
| uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 | |
| with: | |
| creds: ${{ secrets.AZURE_CREDENTIALS }} | |
| - name: Build and push images | |
| shell: pwsh | |
| run: | | |
| ./scripts/Deploy-Application.ps1 ` | |
| -Environment staging ` | |
| -Tag "${{ steps.tag.outputs.image-tag }}" ` | |
| -SkipDeploy | |
| # ────────────────────────────────────────────── | |
| # Deploy to Staging (infra + app in one shot) | |
| # ────────────────────────────────────────────── | |
| deploy-staging: | |
| name: Deploy to Staging | |
| runs-on: ubuntu-latest | |
| needs: build-and-push | |
| concurrency: | |
| group: deploy-staging | |
| cancel-in-progress: false | |
| environment: | |
| name: staging | |
| url: https://${{ steps.deploy.outputs.web-url }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Azure Login | |
| uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 | |
| with: | |
| creds: ${{ secrets.AZURE_CREDENTIALS }} | |
| - name: Deploy staging infrastructure and application | |
| id: deploy | |
| shell: pwsh | |
| env: | |
| POSTGRES_ADMIN_PASSWORD: ${{ secrets.POSTGRES_ACC_PW }} | |
| run: | | |
| ./scripts/Deploy-Infrastructure.ps1 ` | |
| -Environment staging ` | |
| -Mode deploy ` | |
| -ImageTag "${{ needs.build-and-push.outputs.image-tag }}" | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| # Use the known primary hostname (wildcard custom domains like *.hub.ms are not valid URLs) | |
| "web-url=staging-tech.hub.ms" | Out-File -Append -FilePath $env:GITHUB_OUTPUT | |
| - name: Smoke tests | |
| shell: pwsh | |
| run: | | |
| $webUrl = "https://${{ steps.deploy.outputs.web-url }}" | |
| Write-Host "Waiting 30 seconds for deployment to stabilize..." | |
| Start-Sleep -Seconds 30 | |
| # Health check | |
| $health = try { Invoke-WebRequest -Uri "$webUrl/health" -TimeoutSec 30 -UseBasicParsing } catch { $null } | |
| if ($health -and $health.StatusCode -eq 200) { | |
| Write-Host "[OK] Health check passed ($webUrl/health)" | |
| } else { | |
| Write-Host "::error::Health check failed ($webUrl/health)" | |
| exit 1 | |
| } | |
| # Homepage check | |
| $homepage = try { Invoke-WebRequest -Uri $webUrl -TimeoutSec 30 -UseBasicParsing } catch { $null } | |
| if ($homepage -and $homepage.StatusCode -eq 200) { | |
| Write-Host "[OK] Homepage accessible ($webUrl)" | |
| } else { | |
| Write-Host "::error::Homepage not accessible ($webUrl)" | |
| exit 1 | |
| } | |
| - name: Deployment summary | |
| run: | | |
| echo "## Staging 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 "**Web**: https://${{ steps.deploy.outputs.web-url }}" >> $GITHUB_STEP_SUMMARY | |
| # ────────────────────────────────────────────── | |
| # Deploy to Production (infra + app in one shot) | |
| # Uses GitHub environment protection for approval | |
| # ────────────────────────────────────────────── | |
| deploy-production: | |
| name: Deploy to Production | |
| runs-on: ubuntu-latest | |
| needs: [build-and-push, deploy-staging] | |
| 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Azure Login | |
| uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 | |
| with: | |
| creds: ${{ secrets.AZURE_CREDENTIALS }} | |
| - name: Deploy production infrastructure and application | |
| id: deploy | |
| shell: pwsh | |
| env: | |
| POSTGRES_ADMIN_PASSWORD: ${{ secrets.POSTGRES_PROD_PW }} | |
| run: | | |
| ./scripts/Deploy-Infrastructure.ps1 ` | |
| -Environment production ` | |
| -Mode deploy ` | |
| -ImageTag "${{ needs.build-and-push.outputs.image-tag }}" | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| # Use the known primary hostname (wildcard custom domains like *.hub.ms are not valid URLs) | |
| "web-url=tech.hub.ms" | Out-File -Append -FilePath $env:GITHUB_OUTPUT | |
| - name: Smoke tests | |
| shell: pwsh | |
| run: | | |
| $primaryUrls = @('https://tech.hub.ms', 'https://tech.xebia.ms') | |
| Write-Host "Waiting 60 seconds for deployment to stabilize..." | |
| Start-Sleep -Seconds 60 | |
| foreach ($webUrl in $primaryUrls) { | |
| # Health check | |
| $health = try { Invoke-WebRequest -Uri "$webUrl/health" -TimeoutSec 30 -UseBasicParsing } catch { $null } | |
| if ($health -and $health.StatusCode -eq 200) { | |
| Write-Host "[OK] Health check passed ($webUrl/health)" | |
| } else { | |
| Write-Host "::error::Health check failed ($webUrl/health)" | |
| exit 1 | |
| } | |
| # Homepage check | |
| $homepage = try { Invoke-WebRequest -Uri $webUrl -TimeoutSec 30 -UseBasicParsing } catch { $null } | |
| if ($homepage -and $homepage.StatusCode -eq 200) { | |
| Write-Host "[OK] Homepage accessible ($webUrl)" | |
| } else { | |
| Write-Host "::error::Homepage not accessible ($webUrl)" | |
| exit 1 | |
| } | |
| } | |
| - 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 |