Skip to content

Fix inconsistent sidebar toolbar button order when navigating to Browse page #296

Fix inconsistent sidebar toolbar button order when navigating to Browse page

Fix inconsistent sidebar toolbar button order when navigating to Browse page #296

Workflow file for this run

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