Aggregate CI check status #6152
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI Status | |
| run-name: Aggregate CI check status | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| - 'release-[0-9]+.[0-9]+.x' | |
| merge_group: | |
| branches: | |
| - main | |
| - 'release-[0-9]+.[0-9]+.x' | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| ci-status: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 180 | |
| permissions: | |
| checks: read | |
| pull-requests: read | |
| steps: | |
| - name: Wait for CI checks to complete | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const sha = context.payload.pull_request?.head.sha ?? context.sha; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // Wait for other workflows to get queued | |
| core.info('Waiting 60s for CI workflows to get queued...'); | |
| await new Promise(r => setTimeout(r, 60000)); | |
| const excludedChecks = new Set([ | |
| 'ci-status', | |
| 'Run Integration Tests with Latest Release (Informational)', | |
| 'Check Schema Compatibility with Latest Release (Informational)', | |
| ]); | |
| const excludedApps = new Set(['mergify']); | |
| const terminalStatuses = new Set(['completed']); | |
| const successConclusions = new Set(['success', 'skipped', 'neutral']); | |
| const failureConclusions = new Set(['failure', 'timed_out', 'cancelled', 'action_required', 'startup_failure', 'stale']); | |
| const replayCheckPrefix = 'Integration Tests ('; | |
| const recordCheckPrefix = 'record-providers ('; | |
| function pathTriggersReplay(path) { | |
| if (path.startsWith('src/ogx_ui/')) return false; | |
| return path.startsWith('src/ogx/') || | |
| path.startsWith('tests/') || | |
| path === 'uv.lock' || | |
| path === 'pyproject.toml' || | |
| path === '.github/workflows/integration-tests.yml' || | |
| path === '.github/actions/setup-ollama/action.yml' || | |
| path === '.github/actions/setup-test-environment/action.yml' || | |
| path === '.github/actions/run-and-record-tests/action.yml' || | |
| path === 'scripts/integration-tests.sh' || | |
| path === 'scripts/generate_ci_matrix.py'; | |
| } | |
| let replayRequired = context.eventName === 'merge_group'; | |
| if (context.payload.pull_request) { | |
| const changedFiles = await github.paginate(github.rest.pulls.listFiles, { | |
| owner, | |
| repo, | |
| pull_number: context.payload.pull_request.number, | |
| per_page: 100, | |
| }); | |
| replayRequired = changedFiles.some(file => pathTriggersReplay(file.filename)); | |
| core.info(`Replay required: ${replayRequired} (${changedFiles.length} changed file(s) checked)`); | |
| } | |
| while (true) { | |
| const checkRuns = await github.paginate(github.rest.checks.listForRef, { | |
| owner, | |
| repo, | |
| ref: sha, | |
| per_page: 100, | |
| }); | |
| // Filter to only GitHub Actions checks, excluding ourselves and bots | |
| const relevant = checkRuns.filter(cr => { | |
| if (excludedChecks.has(cr.name)) return false; | |
| if (cr.app && excludedApps.has(cr.app.slug)) return false; | |
| // Only include GitHub Actions checks | |
| if (!cr.app || cr.app.slug !== 'github-actions') return false; | |
| return true; | |
| }); | |
| if (relevant.length === 0) { | |
| core.info('No other CI checks found yet, waiting...'); | |
| await new Promise(r => setTimeout(r, 30000)); | |
| continue; | |
| } | |
| const pending = relevant.filter(cr => !terminalStatuses.has(cr.status)); | |
| const completed = relevant.filter(cr => terminalStatuses.has(cr.status)); | |
| const replayChecks = relevant.filter(cr => cr.name.startsWith(replayCheckPrefix)); | |
| const recordChecks = relevant.filter(cr => cr.name.startsWith(recordCheckPrefix)); | |
| core.info(`Checks: ${completed.length} completed, ${pending.length} pending out of ${relevant.length} total`); | |
| core.info(`Replay checks: ${replayChecks.length}; record checks: ${recordChecks.length}`); | |
| for (const cr of completed) { | |
| core.info(` ✓ ${cr.name}: ${cr.conclusion}`); | |
| } | |
| for (const cr of pending) { | |
| core.info(` ⏳ ${cr.name}: ${cr.status}`); | |
| } | |
| if (pending.length > 0) { | |
| core.info('Waiting 30s for pending checks...'); | |
| await new Promise(r => setTimeout(r, 30000)); | |
| continue; | |
| } | |
| if (replayRequired && replayChecks.length === 0) { | |
| if (recordChecks.length > 0) { | |
| core.warning('Record checks are present, but no replay matrix checks were found for this SHA.'); | |
| } | |
| core.info('Replay checks are required for this change. Waiting 30s for the replay workflow to appear...'); | |
| await new Promise(r => setTimeout(r, 30000)); | |
| continue; | |
| } | |
| // All checks completed — evaluate conclusions | |
| const failed = completed.filter(cr => failureConclusions.has(cr.conclusion)); | |
| if (failed.length > 0) { | |
| for (const cr of failed) { | |
| core.warning(`${cr.name} concluded with: ${cr.conclusion} — waiting for re-run`); | |
| } | |
| core.info(`${failed.length} check(s) failed. Waiting 30s for re-runs before giving up...`); | |
| core.info('Re-run the failed job(s) and ci-status will pick up the result automatically.'); | |
| await new Promise(r => setTimeout(r, 30000)); | |
| continue; | |
| } | |
| const succeeded = completed.filter(cr => successConclusions.has(cr.conclusion)); | |
| core.info(`All ${succeeded.length} CI checks passed.`); | |
| return; | |
| } |