Skip to content

Add resizable saved-view columns #6919

Add resizable saved-view columns

Add resizable saved-view columns #6919

Workflow file for this run

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' }}`