Add per-response CSP nonces #6867
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: Build | |
| on: | |
| workflow_dispatch: | |
| repository_dispatch: | |
| types: [preview] | |
| push: | |
| branches: [main] | |
| tags: ["v*"] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| issues: write | |
| packages: write | |
| pull-requests: write | |
| env: | |
| DOTNET_NOLOGO: true | |
| DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | |
| jobs: | |
| preview-context: | |
| if: ${{ (github.event_name == 'repository_dispatch' && github.event.action == 'preview') || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'dev-preview')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| pr-number: ${{ steps.pr.outputs.pr-number }} | |
| pr-sha: ${{ steps.pr.outputs.pr-sha }} | |
| pr-short-sha: ${{ steps.pr.outputs.pr-short-sha }} | |
| branch: ${{ steps.pr.outputs.branch }} | |
| steps: | |
| - name: Resolve preview pull request | |
| id: pr | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| if (context.eventName === "repository_dispatch") { | |
| const payload = context.payload.client_payload ?? {}; | |
| const prNumber = Number(payload.pr_number); | |
| const headSha = payload.head_sha; | |
| if (!prNumber || !headSha) { | |
| core.setFailed("Preview dispatch payload is missing pr_number or head_sha."); | |
| return; | |
| } | |
| core.setOutput("pr-number", String(prNumber)); | |
| core.setOutput("pr-sha", headSha); | |
| core.setOutput("pr-short-sha", headSha.slice(0, 12)); | |
| core.setOutput("branch", payload.head_label || payload.head_ref || "unknown"); | |
| return; | |
| } | |
| const pullRequest = context.payload.pull_request; | |
| core.setOutput("pr-number", String(pullRequest.number)); | |
| core.setOutput("pr-sha", pullRequest.head.sha); | |
| core.setOutput("pr-short-sha", pullRequest.head.sha.slice(0, 12)); | |
| core.setOutput("branch", pullRequest.head.label || pullRequest.head.ref); | |
| preview-comment-running: | |
| needs: [preview-context] | |
| if: ${{ always() && needs.preview-context.outputs.pr-number != '' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Post sticky running comment | |
| uses: marocchino/sticky-pull-request-comment@v3 | |
| with: | |
| number: ${{ needs.preview-context.outputs.pr-number }} | |
| header: exceptionless-preview | |
| message: | | |
| Preview deploy running - [view progress](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | |
| Building images and deploying the shared dev environment. This comment will update with the dev URL when the deploy finishes. | |
| - Branch: `${{ needs.preview-context.outputs.branch }}` | |
| - Commit: [`${{ needs.preview-context.outputs.pr-short-sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ needs.preview-context.outputs.pr-sha }}) | |
| version: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| should_publish: ${{ (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'dev-preview')) && secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }} | |
| is_prod_deploy: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' }} | |
| is_dev_deploy: ${{ (github.event_name == 'repository_dispatch' && github.event.action == 'preview') || (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'dev-preview')) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} | |
| - name: Build Reason | |
| env: | |
| GITHUB_EVENT: ${{ toJson(github) }} | |
| PREVIEW_HEAD_REF: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref) || '' }} | |
| run: | | |
| branch=${PREVIEW_HEAD_REF:-${GITHUB_HEAD_REF:-${GITHUB_REF##*/}}} | |
| branch=${branch//\//.}. | |
| if [[ "$branch" = "main." || "$branch" = "master." || "${GITHUB_REF}" = refs/tags* ]]; then | |
| branch="" | |
| fi | |
| echo "GIT_BRANCH_SUFFIX=$branch" >> $GITHUB_ENV | |
| echo "ref: $GITHUB_REF event: $GITHUB_EVENT_NAME branch_suffix: $branch" | |
| - name: Setup .NET Core | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: 10.0.301 | |
| - name: Version | |
| id: version | |
| run: | | |
| dotnet tool install --global minver-cli --version 7.0.0 | |
| version=$(minver --tag-prefix v --default-pre-release-identifiers "preview.${GIT_BRANCH_SUFFIX}0") | |
| # If on a non-main branch, insert branch name before the height (last numeric segment) | |
| if [ -n "$GIT_BRANCH_SUFFIX" ]; then | |
| branch_name="${GIT_BRANCH_SUFFIX%.}" | |
| if [[ "$version" != *"$branch_name"* ]]; then | |
| version=$(echo "$version" | sed -E "s/\.([0-9]+)$/.${GIT_BRANCH_SUFFIX}\1/") | |
| fi | |
| fi | |
| echo "version=$version" >> $GITHUB_OUTPUT | |
| echo "### $version" >> $GITHUB_STEP_SUMMARY | |
| test-api: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup .NET Core | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: 10.0.301 | |
| - uses: actions/cache@v6 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json') }} | |
| restore-keys: | | |
| nuget-${{ runner.os }}- | |
| - name: Nuget Restore | |
| run: dotnet restore ./Exceptionless.slnx | |
| - name: Build | |
| run: dotnet build ./Exceptionless.slnx --no-restore --configuration Release | |
| - name: Run .NET Tests with Coverage | |
| run: dotnet test --no-restore --no-build --configuration Release --results-directory coverage --max-parallel-test-modules 1 -- --report-github --report-xunit-trx --report-xunit-trx-filename test-results.trx --coverage --coverage-settings ${{ github.workspace }}/tests/CodeCoverage.config --coverage-output coverage.cobertura.xml --coverage-output-format cobertura | |
| - name: Code Coverage Summary Report | |
| uses: irongut/CodeCoverageSummary@v1.3.0 | |
| with: | |
| filename: coverage/coverage.cobertura.xml | |
| badge: true | |
| format: "markdown" | |
| output: "both" | |
| - name: Add Coverage PR Comment | |
| uses: marocchino/sticky-pull-request-comment@v3 | |
| if: github.event_name == 'pull_request' | |
| with: | |
| recreate: true | |
| path: code-coverage-results.md | |
| - name: Write Coverage to Job Summary | |
| run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY | |
| test-client: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| defaults: | |
| run: | |
| working-directory: src/Exceptionless.Web/ClientApp | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup Node.js environment | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - name: Cache node_modules | |
| uses: actions/cache@v6 | |
| id: cache-node-modules | |
| with: | |
| path: src/Exceptionless.Web/ClientApp/node_modules | |
| key: node-modules-${{ hashFiles('src/Exceptionless.Web/ClientApp/package-lock.json') }} | |
| - name: Install Npm Packages | |
| if: steps.cache-node-modules.outputs.cache-hit != 'true' | |
| run: npm ci | |
| - name: Lint Client | |
| run: npm run lint | |
| - name: Check | |
| run: npm run check | |
| - name: Build | |
| run: npm run build | |
| - name: Run Unit Tests | |
| run: echo "npm run test:unit" | |
| - name: Run Integration Tests | |
| run: echo "npm run test:integration" | |
| test-e2e: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup .NET Core | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: 10.0.* | |
| dotnet-quality: ga | |
| - name: Install Aspire CLI | |
| run: | | |
| export PATH="$HOME/.dotnet/tools:$PATH" | |
| if dotnet tool list --global | grep -Eiq '^aspire\.cli[[:space:]]'; then | |
| dotnet tool update --global Aspire.Cli --version 13.3.4 | |
| else | |
| dotnet tool install --global Aspire.Cli --version 13.3.4 | |
| fi | |
| echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" | |
| aspire --version | |
| - name: Setup Node.js environment | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - uses: actions/cache@v5 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json') }} | |
| restore-keys: | | |
| nuget-${{ runner.os }}- | |
| - name: Nuget Restore | |
| run: dotnet restore ./Exceptionless.slnx | |
| - name: Build | |
| run: dotnet build ./Exceptionless.slnx --no-restore --configuration Release | |
| - name: Install Client Npm Packages | |
| working-directory: src/Exceptionless.Web/ClientApp | |
| run: npm ci | |
| - name: Install Legacy Client Npm Packages | |
| working-directory: src/Exceptionless.Web/ClientApp.angular | |
| run: npm ci | |
| - name: Install Playwright Chromium | |
| working-directory: src/Exceptionless.Web/ClientApp | |
| run: npx playwright install chromium --with-deps | |
| - name: Start AppHost | |
| run: | | |
| aspire run --non-interactive --nologo -- --ci-e2e > aspire-run.log 2>&1 & | |
| echo "$!" > aspire-run.pid | |
| sleep 5 | |
| if ! kill -0 "$(cat aspire-run.pid)" 2>/dev/null; then | |
| cat aspire-run.log | |
| exit 1 | |
| fi | |
| - name: Wait for Aspire Resources | |
| run: | | |
| for attempt in {1..60}; do | |
| if curl -fksS https://web-ex.dev.localhost:7131/api/v2/about > /dev/null && | |
| curl -fksS https://web-ex.dev.localhost:7131/next/login > /dev/null; then | |
| break | |
| fi | |
| if ! kill -0 "$(cat aspire-run.pid)" 2>/dev/null; then | |
| cat aspire-run.log | |
| exit 1 | |
| fi | |
| if [ "$attempt" -eq 60 ]; then | |
| cat aspire-run.log | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| - name: Verify E2E Endpoints | |
| run: | | |
| curl -fksS https://web-ex.dev.localhost:7131/api/v2/about > /dev/null | |
| curl -fksS https://web-ex.dev.localhost:7131/next/login > /dev/null | |
| - name: Run Playwright E2E Tests | |
| working-directory: src/Exceptionless.Web/ClientApp | |
| env: | |
| E2E_URL: https://web-ex.dev.localhost:7131 | |
| E2E_RUN_ID: ci-${{ github.run_id }}-${{ github.run_attempt }} | |
| run: npm run test:e2e:ci | |
| - name: Stop AppHost | |
| if: ${{ always() }} | |
| run: | | |
| aspire stop --all --non-interactive || true | |
| if [ -f aspire-run.pid ]; then | |
| kill "$(cat aspire-run.pid)" 2>/dev/null || true | |
| fi | |
| - name: Collect Aspire Logs | |
| if: ${{ !cancelled() }} | |
| run: | | |
| mkdir -p aspire-logs | |
| if [ -f aspire-run.log ]; then | |
| cp aspire-run.log aspire-logs/ | |
| fi | |
| if [ -f aspire-run.pid ]; then | |
| cp aspire-run.pid aspire-logs/ | |
| fi | |
| if [ -d "$HOME/.aspire/cli/logs" ]; then | |
| cp -r "$HOME/.aspire/cli/logs/." aspire-logs/ | |
| fi | |
| if [ -d "$HOME/.aspire/logs" ]; then | |
| cp -r "$HOME/.aspire/logs/." aspire-logs/ | |
| fi | |
| - name: Upload Playwright Report | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: playwright-report | |
| path: src/Exceptionless.Web/ClientApp/playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Playwright Test Results | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: playwright-test-results | |
| path: src/Exceptionless.Web/ClientApp/test-results/ | |
| retention-days: 14 | |
| - name: Upload Aspire Logs | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: aspire-logs | |
| path: aspire-logs/ | |
| retention-days: 14 | |
| docker-build: | |
| runs-on: ubuntu-latest | |
| needs: [version] | |
| timeout-minutes: 30 | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| STAGING_TAG: build-${{ github.run_id }}-${{ github.run_attempt }} | |
| SHOULD_PUBLISH: ${{ needs.version.outputs.should_publish }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| with: | |
| platforms: linux/amd64 | |
| - name: Login to GitHub Container Registry | |
| if: ${{ env.SHOULD_PUBLISH == 'true' }} | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build api docker image | |
| run: | | |
| echo "::remove-matcher owner=csc::" | |
| if [ "$SHOULD_PUBLISH" = "true" ]; then | |
| docker buildx build . --build-arg MinVerVersionOverride=$VERSION --target api --platform linux/amd64 --tag ghcr.io/exceptionless/exceptionless/api-ci:$STAGING_TAG --cache-from type=registry,ref=ghcr.io/exceptionless/exceptionless/api-ci:buildcache --cache-to type=registry,ref=ghcr.io/exceptionless/exceptionless/api-ci:buildcache,mode=max --push | |
| else | |
| docker buildx build . --build-arg MinVerVersionOverride=$VERSION --target api --platform linux/amd64 --tag exceptionless/api-ci:latest --cache-from type=gha,scope=api-ci --cache-to type=gha,scope=api-ci,mode=max --load | |
| fi | |
| - name: Build job docker image | |
| run: | | |
| echo "::remove-matcher owner=csc::" | |
| if [ "$SHOULD_PUBLISH" = "true" ]; then | |
| docker buildx build . --build-arg MinVerVersionOverride=$VERSION --target job --platform linux/amd64 --tag ghcr.io/exceptionless/exceptionless/job-ci:$STAGING_TAG --cache-from type=registry,ref=ghcr.io/exceptionless/exceptionless/job-ci:buildcache --cache-to type=registry,ref=ghcr.io/exceptionless/exceptionless/job-ci:buildcache,mode=max --push | |
| else | |
| docker buildx build . --build-arg MinVerVersionOverride=$VERSION --target job --platform linux/amd64 --tag exceptionless/job-ci:latest --cache-from type=gha,scope=job-ci --cache-to type=gha,scope=job-ci,mode=max --load | |
| fi | |
| - name: Build app docker image | |
| run: | | |
| echo "::remove-matcher owner=csc::" | |
| if [ "$SHOULD_PUBLISH" = "true" ]; then | |
| docker buildx build . --build-arg MinVerVersionOverride=$VERSION --target app --platform linux/amd64 --tag ghcr.io/exceptionless/exceptionless/app-ci:$STAGING_TAG --cache-from type=registry,ref=ghcr.io/exceptionless/exceptionless/app-ci:buildcache --cache-to type=registry,ref=ghcr.io/exceptionless/exceptionless/app-ci:buildcache,mode=max --push | |
| else | |
| docker buildx build . --build-arg MinVerVersionOverride=$VERSION --target app --platform linux/amd64 --tag exceptionless/app-ci:latest --cache-from type=gha,scope=app-ci --cache-to type=gha,scope=app-ci,mode=max --load | |
| fi | |
| - name: Build all-in-one docker image | |
| if: ${{ needs.version.outputs.is_prod_deploy == 'true' }} | |
| run: | | |
| echo "::remove-matcher owner=csc::" | |
| if [ "$SHOULD_PUBLISH" = "true" ]; then | |
| docker buildx build . --build-arg MinVerVersionOverride=$VERSION --target exceptionless --platform linux/amd64 --tag ghcr.io/exceptionless/exceptionless/exceptionless-ci:$STAGING_TAG --cache-from type=registry,ref=ghcr.io/exceptionless/exceptionless/exceptionless-ci:buildcache --cache-to type=registry,ref=ghcr.io/exceptionless/exceptionless/exceptionless-ci:buildcache,mode=max --push | |
| else | |
| docker buildx build . --build-arg MinVerVersionOverride=$VERSION --target exceptionless --platform linux/amd64 --tag exceptionless/exceptionless-ci:latest --cache-from type=gha,scope=exceptionless-ci --cache-to type=gha,scope=exceptionless-ci,mode=max --load | |
| fi | |
| docker-publish: | |
| if: ${{ needs.version.outputs.should_publish == 'true' }} | |
| runs-on: ubuntu-latest | |
| needs: [version, docker-build, test-api, test-client, test-e2e] | |
| timeout-minutes: 30 | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| STAGING_TAG: build-${{ github.run_id }}-${{ github.run_attempt }} | |
| steps: | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Login to DockerHub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ env.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Pull staged docker images | |
| run: | | |
| images=(api job app) | |
| if [ "${{ needs.version.outputs.is_prod_deploy }}" = "true" ]; then | |
| images+=(exceptionless) | |
| fi | |
| for image in "${images[@]}"; do | |
| docker pull ghcr.io/exceptionless/exceptionless/$image-ci:$STAGING_TAG | |
| done | |
| - name: Publish CI Packages | |
| run: | | |
| echo "::remove-matcher owner=csc::" | |
| # tag and push docker images | |
| images=(api job app) | |
| if [ "${{ needs.version.outputs.is_prod_deploy }}" = "true" ]; then | |
| images+=(exceptionless) | |
| fi | |
| for image in "${images[@]}"; do | |
| docker image tag ghcr.io/exceptionless/exceptionless/$image-ci:$STAGING_TAG exceptionless/$image-ci:$VERSION | |
| docker image tag ghcr.io/exceptionless/exceptionless/$image-ci:$STAGING_TAG exceptionless/$image-ci:latest | |
| docker image tag ghcr.io/exceptionless/exceptionless/$image-ci:$STAGING_TAG ghcr.io/exceptionless/exceptionless/$image-ci:$VERSION | |
| docker image tag ghcr.io/exceptionless/exceptionless/$image-ci:$STAGING_TAG ghcr.io/exceptionless/exceptionless/$image-ci:latest | |
| docker image push --all-tags exceptionless/$image-ci | |
| docker image push --all-tags ghcr.io/exceptionless/exceptionless/$image-ci | |
| done | |
| - name: Publish Release Packages | |
| if: ${{ needs.version.outputs.is_prod_deploy == 'true' }} | |
| run: | | |
| echo "::remove-matcher owner=csc::" | |
| # tag and push docker images | |
| images=(api job app exceptionless) | |
| for image in "${images[@]}"; do | |
| docker image tag ghcr.io/exceptionless/exceptionless/$image-ci:$STAGING_TAG exceptionless/$image:$VERSION | |
| docker image tag ghcr.io/exceptionless/exceptionless/$image-ci:$STAGING_TAG exceptionless/$image:latest | |
| docker image push --all-tags exceptionless/$image | |
| done | |
| - name: Summary | |
| if: ${{ always() }} | |
| run: | | |
| echo "### Build Complete" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: $VERSION" >> $GITHUB_STEP_SUMMARY | |
| echo "- **CI Images**: Published ✅" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Release Images**: ${{ needs.version.outputs.is_prod_deploy == 'true' && 'Published ✅' || 'Skipped' }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Deploy**: ${{ needs.version.outputs.is_dev_deploy == 'true' && 'Development queued' || (needs.version.outputs.is_prod_deploy == 'true' && 'Production queued' || 'Skipped') }}" >> $GITHUB_STEP_SUMMARY | |
| deploy: | |
| if: ${{ needs.version.outputs.should_publish == 'true' }} | |
| needs: [version, docker-publish] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| concurrency: | |
| group: ${{ needs.version.outputs.is_dev_deploy == 'true' && 'dev-environment-deploy' || format('deploy-{0}', github.run_id) }} | |
| cancel-in-progress: false | |
| env: | |
| VERSION: ${{ needs.version.outputs.version }} | |
| CLUSTER_NAME: ex-k8s-v6 | |
| RESOURCE_GROUP: exceptionless-v6 | |
| DEV_NAMESPACE: ex-dev | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} | |
| - name: Install Helm | |
| uses: azure/setup-helm@v5 | |
| - name: Azure Login | |
| if: ${{ needs.version.outputs.is_dev_deploy == 'true' || needs.version.outputs.is_prod_deploy == 'true' }} | |
| run: az login --service-principal --username ${{ secrets.AZ_USERNAME }} --password ${{ secrets.AZ_PASSWORD }} --tenant ${{ secrets.AZ_TENANT }} --output none | |
| - name: Get AKS Credentials | |
| if: ${{ needs.version.outputs.is_dev_deploy == 'true' || needs.version.outputs.is_prod_deploy == 'true' }} | |
| run: az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --overwrite-existing | |
| - name: Ensure Development Infrastructure is Running | |
| if: ${{ needs.version.outputs.is_dev_deploy == 'true' }} | |
| run: | | |
| kubectl scale statefulset/ex-dev-es-main --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl apply -f k8s/ex-dev-redis.yaml | |
| kubectl wait --for=condition=ready --timeout=300s pod ex-dev-es-main-0 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-kb --replicas=1 --namespace $DEV_NAMESPACE | |
| - name: Deploy Changes to Development Environment | |
| if: ${{ needs.version.outputs.is_dev_deploy == 'true' }} | |
| run: | | |
| sed -i "s/^appVersion:.*$/appVersion: '${VERSION}'/" ./k8s/exceptionless/Chart.yaml | |
| helm upgrade --set "version=${VERSION}" --reuse-values --values ./k8s/ex-dev-values.yaml ex-dev --namespace $DEV_NAMESPACE ./k8s/exceptionless | |
| - name: Ensure Development Workloads are Running | |
| if: ${{ needs.version.outputs.is_dev_deploy == 'true' }} | |
| run: | | |
| kubectl scale deployment/ex-dev-app --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-api --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-close-inactive-sessions --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-daily-summary --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-event-notifications --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-event-usage --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-event-posts --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-event-user-descriptions --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-mail-message --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-stack-event-count --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-web-hooks --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl scale deployment/ex-dev-jobs-work-item --replicas=1 --namespace $DEV_NAMESPACE | |
| kubectl patch cronjob/ex-dev-jobs-cleanup-data -p '{"spec":{"suspend": false}}' --namespace $DEV_NAMESPACE | |
| kubectl patch cronjob/ex-dev-jobs-cleanup-orphaned-data -p '{"spec":{"suspend": false}}' --namespace $DEV_NAMESPACE | |
| kubectl patch cronjob/ex-dev-jobs-download-geoip-database -p '{"spec":{"suspend": false}}' --namespace $DEV_NAMESPACE | |
| kubectl patch cronjob/ex-dev-jobs-maintain-indexes -p '{"spec":{"suspend": false}}' --namespace $DEV_NAMESPACE | |
| kubectl patch cronjob/ex-dev-jobs-migration -p '{"spec":{"suspend": false}}' --namespace $DEV_NAMESPACE | |
| wait_for_versioned_pod() { | |
| component="$1" | |
| replica_set="" | |
| for attempt in {1..12}; do | |
| replica_set=$(kubectl get replicasets --namespace "$DEV_NAMESPACE" --selector "component=${component}" --output json \ | |
| | jq --raw-output --arg image_tag ":${VERSION}" ' | |
| [.items[] | |
| | select(any(.spec.template.spec.containers[]; .image | endswith($image_tag))) | |
| | { name: .metadata.name, created: .metadata.creationTimestamp }] | |
| | sort_by(.created) | |
| | last | |
| | .name // empty') | |
| [[ -n "$replica_set" ]] && break | |
| sleep 5 | |
| done | |
| if [[ -z "$replica_set" ]]; then | |
| echo "::error::No ReplicaSet for ${component} uses version ${VERSION}." | |
| return 1 | |
| fi | |
| pod_template_hash=$(kubectl get replicaset "$replica_set" --namespace "$DEV_NAMESPACE" \ | |
| --output jsonpath='{.metadata.labels.pod-template-hash}') | |
| kubectl wait --for=create --timeout=60s pod \ | |
| --namespace "$DEV_NAMESPACE" \ | |
| --selector "component=${component},pod-template-hash=${pod_template_hash}" | |
| if ! kubectl wait --for=condition=Ready --timeout=300s pod \ | |
| --namespace "$DEV_NAMESPACE" \ | |
| --selector "component=${component},pod-template-hash=${pod_template_hash}"; then | |
| echo "::group::${component} rollout diagnostics" | |
| kubectl get pods --namespace "$DEV_NAMESPACE" \ | |
| --selector "component=${component},pod-template-hash=${pod_template_hash}" \ | |
| --output wide | |
| for pod in $(kubectl get pods --namespace "$DEV_NAMESPACE" \ | |
| --selector "component=${component},pod-template-hash=${pod_template_hash}" \ | |
| --output name); do | |
| kubectl describe "$pod" --namespace "$DEV_NAMESPACE" | |
| kubectl logs "$pod" --namespace "$DEV_NAMESPACE" --all-containers --tail=300 \ | |
| | grep --extended-regexp 'Now listening on:|Application started|Application is shutting down' || true | |
| done | |
| echo "::endgroup::" | |
| return 1 | |
| fi | |
| } | |
| wait_for_versioned_pod ex-dev-app | |
| wait_for_versioned_pod ex-dev-api | |
| wait_for_versioned_pod ex-dev-jobs-event-posts | |
| for endpoint in dev-app.exceptionless.io dev-api.exceptionless.io dev-collector.exceptionless.io; do | |
| deployed_version="" | |
| for attempt in {1..12}; do | |
| if deployed_version=$(curl --fail --silent --show-error --retry 2 --retry-all-errors --retry-delay 2 \ | |
| --header "Cache-Control: no-cache" \ | |
| "https://${endpoint}/api/v2/about?deployment=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${attempt}" \ | |
| | jq --raw-output '.informational_version'); then | |
| if [[ "$deployed_version" == "${VERSION}+"* ]]; then | |
| break | |
| fi | |
| fi | |
| sleep 5 | |
| done | |
| if [[ "$deployed_version" != "${VERSION}+"* ]]; then | |
| echo "::error::${endpoint} is serving ${deployed_version}; expected ${VERSION}." | |
| exit 1 | |
| fi | |
| done | |
| kubectl annotate namespace $DEV_NAMESPACE exceptionless.io/dev-started-at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" --overwrite | |
| kubectl annotate namespace $DEV_NAMESPACE exceptionless.io/dev-auto-stop-days="1" --overwrite | |
| kubectl annotate namespace $DEV_NAMESPACE exceptionless.io/dev-auto-stop-hours- --overwrite 2>/dev/null || true | |
| - name: Deploy Changes to Production Environment | |
| if: ${{ needs.version.outputs.is_prod_deploy == 'true' }} | |
| run: | | |
| sed -i "s/^appVersion:.*$/appVersion: '${VERSION}'/" ./k8s/exceptionless/Chart.yaml | |
| helm upgrade --set "version=${VERSION}" --reuse-values --values ./k8s/ex-prod-values.yaml ex-prod --namespace ex-prod ./k8s/exceptionless | |
| - name: Summary | |
| if: ${{ always() }} | |
| run: | | |
| echo "### Deployment Complete" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: $VERSION" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ needs.version.outputs.is_dev_deploy }}" = "true" ]; then | |
| echo "- **Environment**: Development" >> $GITHUB_STEP_SUMMARY | |
| echo "- **App**: https://dev-app.exceptionless.io" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Collector**: https://dev-collector.exceptionless.io" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.version.outputs.is_prod_deploy }}" = "true" ]; then | |
| echo "- **Environment**: Production" >> $GITHUB_STEP_SUMMARY | |
| echo "- **App**: https://be.exceptionless.io" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Collector**: https://collector.exceptionless.io" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- **Deployment**: Skipped" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| deploy-website: | |
| name: Deploy website | |
| if: ${{ needs.version.outputs.is_prod_deploy == 'true' }} | |
| needs: [version, deploy] | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| uses: ./.github/workflows/website.yml | |
| preview-comment-result: | |
| needs: [preview-context, version, test-api, test-client, docker-build, docker-publish, deploy] | |
| if: ${{ always() && needs.preview-context.outputs.pr-number != '' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Post sticky deployed comment | |
| if: ${{ needs.deploy.result == 'success' }} | |
| uses: marocchino/sticky-pull-request-comment@v3 | |
| with: | |
| number: ${{ needs.preview-context.outputs.pr-number }} | |
| header: exceptionless-preview | |
| message: | | |
| Preview deployed | |
| - **App:** https://dev-app.exceptionless.io | |
| - **Collector:** https://dev-collector.exceptionless.io | |
| - Commit: [`${{ needs.preview-context.outputs.pr-short-sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ needs.preview-context.outputs.pr-sha }}) | |
| - Version: `${{ needs.version.outputs.version }}` | |
| - Workflow run: [view logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | |
| - name: Post sticky failed comment | |
| if: ${{ needs.deploy.result != 'success' }} | |
| uses: marocchino/sticky-pull-request-comment@v3 | |
| with: | |
| number: ${{ needs.preview-context.outputs.pr-number }} | |
| header: exceptionless-preview | |
| message: | | |
| Preview deploy failed or did not complete. See [run logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). | |
| - Branch: `${{ needs.preview-context.outputs.branch }}` | |
| - Commit: [`${{ needs.preview-context.outputs.pr-short-sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ needs.preview-context.outputs.pr-sha }}) | |
| - Version: `${{ needs.version.outputs.version || 'not computed' }}` |