From 163050650a86ef6362e853733deb96f4854d55f2 Mon Sep 17 00:00:00 2001 From: Harlan GitHub Agent Date: Fri, 11 Sep 2026 08:22:54 +1000 Subject: [PATCH] fix(ops): scope check-in CI collection to main branch runs --- scripts/tools/daily-checkin-data.mjs | 11 +++++----- scripts/tools/daily-checkin-observability.mjs | 22 +++++++++++++++++++ test/unit/daily-checkin-observability.test.ts | 12 ++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/scripts/tools/daily-checkin-data.mjs b/scripts/tools/daily-checkin-data.mjs index d86ac453..b78a2d13 100644 --- a/scripts/tools/daily-checkin-data.mjs +++ b/scripts/tools/daily-checkin-data.mjs @@ -13,6 +13,7 @@ import { parseWorkflowName, readMigrationState, refreshProductionRef, + runListArgs, summarizeWorkflowRuns, } from './daily-checkin-observability.mjs' import { ghEnv, runReadOnlyProcess, subprocessEnv } from './daily-checkin-process.mjs' @@ -118,8 +119,6 @@ const deploy = probe(() => { } }) -const runFields = 'databaseId,workflowName,displayTitle,headSha,status,conclusion,createdAt,updatedAt,url' - const ci = probe(() => { const workflowDir = join(root, '.github/workflows') const definedWorkflows = readdirSync(workflowDir) @@ -130,12 +129,14 @@ const ci = probe(() => { // A low-cadence workflow can fall outside a flat recent-runs page, and an // absent row reads as `missing`, which is an observability gap rather than a // health signal. Each workflow is therefore paged on its own name, and paged - // deeper when a run of skipped guard runs hides the last verdict. + // deeper when a run of skipped guard runs hides the last verdict. runListArgs + // scopes every read to main, so a PR branch failure cannot masquerade as the + // main gate verdict. const perWorkflowRows = collectWorkflowRuns( - (name, limit) => commandJson('gh', ['run', 'list', '--workflow', name, '--limit', String(limit), '--json', runFields]), + (name, limit) => commandJson('gh', runListArgs(name, limit)), definedWorkflows, ) - const recent = commandJson('gh', ['run', 'list', '--limit', '20', '--json', runFields]) + const recent = commandJson('gh', runListArgs(null, 20)) return { workflows: summarizeWorkflowRuns(perWorkflowRows, definedWorkflows), recent: recent.slice(0, 10), diff --git a/scripts/tools/daily-checkin-observability.mjs b/scripts/tools/daily-checkin-observability.mjs index 7765abcf..a7eb3b7f 100644 --- a/scripts/tools/daily-checkin-observability.mjs +++ b/scripts/tools/daily-checkin-observability.mjs @@ -98,6 +98,28 @@ function completedState(runs) { const WORKFLOW_HEAD_SAMPLE = 10 const WORKFLOW_VERDICT_SAMPLE = 100 +const workflowRunFields = 'databaseId,workflowName,displayTitle,headSha,status,conclusion,createdAt,updatedAt,url' + +/** + * The `gh run list` arguments every CI read must use. Scoped to `main` because + * test.yml also runs on pull_request: an unscoped list let four PR branch + * failures archive a broken main gate on 2026-09-10 while main's own Test run + * on the deployed SHA passed. `workflow` is null for the recent feed. + */ +export function runListArgs(workflow, limit) { + return [ + 'run', + 'list', + ...(workflow ? ['--workflow', workflow] : []), + '--branch', + 'main', + '--limit', + String(limit), + '--json', + workflowRunFields, + ] +} + /** * Fetch enough runs per workflow that `summarizeWorkflowRuns` can reach the last * verdict. `listRuns(name, limit)` returns that workflow's runs, newest first. diff --git a/test/unit/daily-checkin-observability.test.ts b/test/unit/daily-checkin-observability.test.ts index d399698f..56bff44e 100644 --- a/test/unit/daily-checkin-observability.test.ts +++ b/test/unit/daily-checkin-observability.test.ts @@ -9,6 +9,7 @@ import { parseWorkflowName, readMigrationState, refreshProductionRef, + runListArgs, summarizeWorkflowRuns, } from '../../scripts/tools/daily-checkin-observability.mjs' @@ -105,6 +106,17 @@ describe('daily check-in observability', () => { )).toContain('filter: {scriptName: "skilld-dev"') }) + // test.yml runs on every pull_request, so on 2026-09-10 four PR branch + // failures were archived as a broken main gate while main's own Test run on + // the deployed SHA passed. Every `gh run list` the collector makes must be + // scoped to main, both the per-workflow pages and the recent feed. + it('scopes every gh run list to main so a PR failure cannot read as a main failure', () => { + expect(runListArgs('Test', 10)).toEqual(expect.arrayContaining(['--workflow', 'Test', '--branch', 'main'])) + expect(runListArgs('Test', 100)).toEqual(expect.arrayContaining(['--limit', '100'])) + expect(runListArgs(null, 20)).toEqual(expect.arrayContaining(['--branch', 'main'])) + expect(runListArgs(null, 20)).not.toContain('--workflow') + }) + it('surfaces every required workflow and preserves a failure behind an in-progress run', () => { expect(summarizeWorkflowRuns([ run('Deploy to Cloudflare', 'in_progress', '', 4),