Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions scripts/tools/daily-checkin-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
parseWorkflowName,
readMigrationState,
refreshProductionRef,
runListArgs,
summarizeWorkflowRuns,
} from './daily-checkin-observability.mjs'
import { ghEnv, runReadOnlyProcess, subprocessEnv } from './daily-checkin-process.mjs'
Expand Down Expand Up @@ -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)
Expand All @@ -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),
Expand Down
22 changes: 22 additions & 0 deletions scripts/tools/daily-checkin-observability.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions test/unit/daily-checkin-observability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
parseWorkflowName,
readMigrationState,
refreshProductionRef,
runListArgs,
summarizeWorkflowRuns,
} from '../../scripts/tools/daily-checkin-observability.mjs'

Expand Down Expand Up @@ -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),
Expand Down
Loading