feat(alerts): fan out notifications to every configured channel #12618
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: Main | |
| on: | |
| push: | |
| branches: [main, v1] | |
| pull_request: | |
| branches: [main, v1] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| lint: | |
| timeout-minutes: 8 | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| # Full history so `nx affected` can diff against the PR base ref. | |
| # Blobless, not treeless: `nx affected` runs `git diff --name-only`, | |
| # which needs the trees. `tree:0` makes that diff pay a lazy fetch | |
| # round trip and comes out slower than the bytes it saves. | |
| fetch-depth: 0 | |
| filter: blob:none | |
| - name: Setup node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache-dependency-path: 'yarn.lock' | |
| cache: 'yarn' | |
| - name: Install root dependencies | |
| run: yarn install --immutable | |
| - name: Install core libs | |
| run: sudo apt-get install --yes curl bc | |
| # Restore-only on PRs: a PR-scoped cache entry can't be read by any other | |
| # branch, so saving one buys nothing and just evicts other entries out of | |
| # the repo's shared 10GB budget (including the integration job's buildx | |
| # layer cache). Only push runs write, and PRs read main's entry. | |
| - name: Restore nx computation cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: .nx/cache | |
| key: nx-${{ runner.os }}-${{ github.sha }} | |
| restore-keys: | | |
| nx-${{ runner.os }}- | |
| # Upstream `ci:build` runs automatically via the `dependsOn` wiring in | |
| # nx.json, so unchanged projects are served from the nx cache. | |
| - name: Run lint + type check (affected) | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: npx nx affected -t ci:lint --base="origin/${BASE_REF}" | |
| - name: Run lint + type check (full) | |
| if: github.event_name != 'pull_request' | |
| run: npx nx run-many -t ci:lint | |
| # Whole-repo escape-hatch ratchet — runs unconditionally (not via `nx | |
| # affected`, and not via `make ci-lint`, which this job no longer calls), | |
| # so the gate can't be silently skipped for a PR that touches few projects. | |
| - name: Escape-hatch ratchet tests | |
| run: node --test scripts/ci/__tests__/ratchet.test.mjs | |
| - name: Escape-hatch ratchet | |
| run: node scripts/ci/ratchet.mjs | |
| # Runs even when lint fails, so a red main still refreshes the cache that | |
| # PR runs restore from. | |
| - name: Save nx computation cache | |
| if: always() && github.event_name == 'push' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: .nx/cache | |
| key: nx-${{ runner.os }}-${{ github.sha }} | |
| unit: | |
| timeout-minutes: 8 | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache-dependency-path: 'yarn.lock' | |
| cache: 'yarn' | |
| - name: Install root dependencies | |
| run: yarn install --immutable | |
| - name: Build dependencies | |
| run: make ci-build | |
| - name: Run unit tests | |
| run: make ci-unit | |
| integration-shards: | |
| name: Integration - Shard ${{ matrix.shard }} | |
| timeout-minutes: 16 | |
| runs-on: ubuntu-24.04 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache-dependency-path: 'yarn.lock' | |
| cache: 'yarn' | |
| - name: Install root dependencies | |
| run: yarn install --immutable | |
| - name: Expose GitHub Runtime | |
| uses: crazy-max/ghaction-github-runtime@v4 | |
| - name: Spin up docker services | |
| run: | | |
| docker buildx create --use --driver=docker-container | |
| docker buildx bake -f ./docker-compose.ci.yml --set *.cache-to="type=gha,mode=max,scope=ci-integration" --set *.cache-from="type=gha,scope=ci-integration" --load | |
| - name: Build dependencies | |
| run: make ci-build | |
| # Integration tests boot a MockServer on fixed ports and share a single | |
| # MongoDB/ClickHouse database, so they must stay serial (--runInBand) | |
| # within a runner. We shard ACROSS runners instead: each shard is its own | |
| # VM with its own Docker stack, and jest's --shard splits the test files | |
| # so the shards run in parallel. --shard is forwarded through nx (args | |
| # after `--` reach the underlying jest invocations). | |
| # | |
| # Runs inline (not `make ci-int`) so the compose project stays `hdx-ci` | |
| # (matching the bake step above); make ci-int's `-p int-<slot>` is local | |
| # multi-worktree isolation that CI doesn't need, and routing through it | |
| # would reintroduce a project-name mismatch against bake. Teardown and | |
| # log-archival are omitted intentionally — the runner is ephemeral and | |
| # Actions streams stdout. | |
| - name: Run integration tests | |
| run: | | |
| docker compose -f ./docker-compose.ci.yml up -d --quiet-pull | |
| npx nx run-many -t ci:int --parallel=false --output-style=stream -- --shard=${{ matrix.shard }}/${{ strategy.job-total }} | |
| # Stable aggregator context: `integration` is green only when all four shards | |
| # pass. Merge-queue / required-status checks target this single context, since | |
| # the per-shard contexts (Integration - Shard N) are not stable check names. | |
| integration: | |
| needs: [integration-shards] | |
| if: always() | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Verify all integration shards passed | |
| if: needs.integration-shards.result != 'success' | |
| run: | | |
| echo "::error::One or more integration shards failed" | |
| exit 1 | |
| otel-unit-test: | |
| timeout-minutes: 8 | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Get changed files | |
| id: changed-files | |
| uses: tj-actions/changed-files@v47.0.6 | |
| with: | |
| files: | | |
| packages/otel-collector/** | |
| - name: Setup Go | |
| if: steps.changed-files.outputs.any_changed == 'true' | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: packages/otel-collector/go.mod | |
| cache-dependency-path: packages/otel-collector/go.sum | |
| - name: Run unit tests | |
| if: steps.changed-files.outputs.any_changed == 'true' | |
| working-directory: ./packages/otel-collector | |
| run: go test ./... | |
| otel-smoke-test: | |
| timeout-minutes: 16 | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Get changed OTEL collector files | |
| id: changed-files | |
| uses: tj-actions/changed-files@v47.0.6 | |
| with: | |
| files: | | |
| docker/otel-collector/** | |
| smoke-tests/otel-collector/** | |
| - name: Install required tooling | |
| if: steps.changed-files.outputs.any_changed == 'true' | |
| env: | |
| DEBIAN_FRONTEND: noninteractive | |
| run: | | |
| sudo apt-get install -y apt-transport-https ca-certificates curl gnupg | |
| curl -fsSL 'https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key' | sudo gpg --dearmor -o /usr/share/keyrings/clickhouse-keyring.gpg | |
| ARCH=$(dpkg --print-architecture) | |
| echo "deb [signed-by=/usr/share/keyrings/clickhouse-keyring.gpg arch=${ARCH}] https://packages.clickhouse.com/deb stable main" | sudo tee /etc/apt/sources.list.d/clickhouse.list | |
| sudo apt-get update | |
| sudo apt-get install --yes curl bats clickhouse-client | |
| - name: Run Smoke Tests | |
| if: steps.changed-files.outputs.any_changed == 'true' | |
| working-directory: ./smoke-tests/otel-collector | |
| run: bats . | |
| e2e-tests: | |
| uses: ./.github/workflows/e2e-tests.yml | |
| permissions: | |
| contents: read | |
| e2e-report: | |
| name: End-to-End Tests | |
| if: always() | |
| needs: e2e-tests | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Download all test results | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: test-results-* | |
| path: all-test-results | |
| - name: Aggregate test results | |
| id: test-results | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v9 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| let totalPassed = 0; | |
| let totalFailed = 0; | |
| let totalFlaky = 0; | |
| let totalSkipped = 0; | |
| let totalDuration = 0; | |
| let foundResults = false; | |
| try { | |
| const resultsDir = 'all-test-results'; | |
| const shards = fs.readdirSync(resultsDir); | |
| for (const shard of shards) { | |
| const resultsPath = path.join(resultsDir, shard, 'results.json'); | |
| if (fs.existsSync(resultsPath)) { | |
| foundResults = true; | |
| const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8')); | |
| const { stats } = results; | |
| totalPassed += stats.expected || 0; | |
| totalFailed += stats.unexpected || 0; | |
| totalFlaky += stats.flaky || 0; | |
| totalSkipped += stats.skipped || 0; | |
| totalDuration += stats.duration || 0; | |
| } | |
| } | |
| if (foundResults) { | |
| const duration = Math.round(totalDuration / 1000); | |
| const summary = totalFailed > 0 | |
| ? `❌ **${totalFailed} test${totalFailed > 1 ? 's' : ''} failed**` | |
| : `✅ **All tests passed**`; | |
| return `## E2E Test Results | |
| ${summary} • ${totalPassed} passed • ${totalSkipped} skipped • ${duration}s | |
| | Status | Count | | |
| |--------|-------| | |
| | ✅ Passed | ${totalPassed} | | |
| | ❌ Failed | ${totalFailed} | | |
| | ⚠️ Flaky | ${totalFlaky} | | |
| | ⏭️ Skipped | ${totalSkipped} | | |
| Tests ran across ${shards.length} shards in parallel. | |
| [View full report →](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`; | |
| } else { | |
| return `## E2E Test Results | |
| ❌ **Test results file not found** | |
| [View full report →](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`; | |
| } | |
| } catch (error) { | |
| console.log('Could not parse test results:', error.message); | |
| return `## E2E Test Results | |
| ❌ **Error reading test results**: ${error.message} | |
| [View full report →](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`; | |
| } | |
| - name: Comment PR with test results | |
| uses: mshick/add-pr-comment@v3 | |
| # Skip for fork PRs: GITHUB_TOKEN cannot write to the base repo | |
| if: | |
| always() && github.event_name == 'pull_request' && | |
| github.event.pull_request.head.repo.fork != true | |
| with: | |
| message: ${{ steps.test-results.outputs.result }} | |
| message-id: e2e-test-results | |
| - name: Check test results | |
| id: check-results | |
| run: | | |
| total_failed=0 | |
| for dir in all-test-results/*/; do | |
| if [ -f "${dir}results.json" ]; then | |
| unexpected=$(jq -r '.stats.unexpected // 0' "${dir}results.json") | |
| total_failed=$((total_failed + unexpected)) | |
| fi | |
| done | |
| if [ "$total_failed" -gt 0 ]; then | |
| echo "::error::$total_failed E2E test(s) failed" | |
| exit 1 | |
| fi | |
| # Fail when any shard failed even if we couldn't read failure count from artifacts | |
| if [ "${{ needs.e2e-tests.result }}" = "failure" ]; then | |
| echo "::error::One or more E2E test shards failed" | |
| exit 1 | |
| fi | |
| clickhouse-static-build: | |
| name: ClickHouse Bundle Build | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache-dependency-path: 'yarn.lock' | |
| cache: 'yarn' | |
| - name: Install dependencies | |
| run: yarn install --immutable | |
| - name: Build App | |
| run: yarn build:clickhouse | |
| - name: Verify output directory exists and has size | |
| run: | | |
| if [ ! -d "packages/app/out" ]; then | |
| echo "::error::Output directory 'packages/app/out' does not exist" | |
| exit 1 | |
| fi | |
| echo "✓ Output directory exists" | |
| # Calculate size in bytes and convert to MB | |
| size_kb=$(du -sk packages/app/out | cut -f1) | |
| size_mb=$((size_kb / 1024)) | |
| echo "Output directory size: ${size_mb} MB (${size_kb} KB)" | |
| if [ "$size_mb" -lt 10 ]; then | |
| echo "::error::Output directory is only ${size_mb} MB, expected at least 10 MB" | |
| exit 1 | |
| fi | |
| echo "✓ Output directory size check passed (${size_mb} MB)" |