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
16 changes: 16 additions & 0 deletions checks/_helpers/cause.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { unavailable } from '@harlan-zw/nuxt-checkin/external'

/**
* The published runOne archives every thrown error as the generic
* 'Check threw an exception.' and the CLI passes no onError callback, so a
* collector failure reached the daily archive without its cause (2026-09-29:
* five Cloudflare failures, no reason). The collector-driven checks wrap
* their run body with this guard and archive the error message instead.
* @param {(context: import('@harlan-zw/nuxt-checkin/external').ExternalCheckContext) => Promise<import('@harlan-zw/nuxt-checkin/external').CheckResult>} run
* @returns {(context: import('@harlan-zw/nuxt-checkin/external').ExternalCheckContext) => Promise<import('@harlan-zw/nuxt-checkin/external').CheckResult>} a run that archives a thrown cause as Unavailable
*/
export function withCause(run) {
return async context => run(context).catch((error) => {
return unavailable(error instanceof Error && error.message.trim() ? error.message : 'Check evidence collection failed.')
})
}
9 changes: 6 additions & 3 deletions checks/_helpers/collectors.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync } from 'node:fs'
import { readdir, readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { basename, join } from 'node:path'
import { readBoundedResponseText, runCheckCommand } from '@harlan-zw/nuxt-checkin/external'
import { ANALYTICS_ACCOUNT_TAG, buildCopyQuery, buildCopyTotalsQuery, buildWorkersQuery, collectWorkflowRuns, parseWorkflowName, runListArgs, summarizeCopies, summarizeWorkflowRuns } from './observability.mjs'

Expand All @@ -25,8 +25,11 @@ async function run(context, command, args) {
})
: baseEnv
const result = await runCheckCommand({ ...context, env }, command, args, { maxBytes: 20 * 1024 * 1024 })
if (result._tag === 'Err')
throw new Error(result.reason)
if (result._tag === 'Err') {
// The shared CLI's Err carries no stderr, so name the failing command:
// the archive then says which probe broke, not just that a command did.
throw new Error(`${basename(command)} ${args[0] ?? ''} failed: ${result.reason}`.trim())
}
return result.stdout.trim()
}
async function commandJson(context, command, args) {
Expand Down
5 changes: 2 additions & 3 deletions checks/external/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { defineExternalCheck, pass } from '@harlan-zw/nuxt-checkin/external'
import { withCause } from '../_helpers/cause.mjs'
import { collectAnalytics } from '../_helpers/collectors.mjs'

export default defineExternalCheck({
id: 'skilld.analytics',
async run(context) {
return pass(await collectAnalytics(context))
},
run: withCause(async context => pass(await collectAnalytics(context))),
})
5 changes: 2 additions & 3 deletions checks/external/ci.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { defineExternalCheck } from '@harlan-zw/nuxt-checkin/external'
import { withCause } from '../_helpers/cause.mjs'
import { collectCI } from '../_helpers/collectors.mjs'
import { evaluateCI } from '../_helpers/policy.mjs'

export default defineExternalCheck({
id: 'skilld.ci',
async run(context) {
return evaluateCI(await collectCI(context))
},
run: withCause(async context => evaluateCI(await collectCI(context))),
})
5 changes: 2 additions & 3 deletions checks/external/database.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { defineExternalCheck } from '@harlan-zw/nuxt-checkin/external'
import { withCause } from '../_helpers/cause.mjs'
import { collectD1 } from '../_helpers/collectors.mjs'
import { evaluateD1 } from '../_helpers/policy.mjs'

export default defineExternalCheck({
id: 'skilld.database',
async run(context) {
return evaluateD1(await collectD1(context), context.now, context.since)
},
run: withCause(async context => evaluateD1(await collectD1(context), context.now, context.since)),
})
5 changes: 3 additions & 2 deletions checks/external/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { defineExternalCheck, pass } from '@harlan-zw/nuxt-checkin/external'
import { withCause } from '../_helpers/cause.mjs'
import { collectDeploy } from '../_helpers/collectors.mjs'

export default defineExternalCheck({
id: 'skilld.deploy',
async run(context) {
run: withCause(async (context) => {
const data = await collectDeploy(context)
return data.latest?.versionId ? pass(data) : { _tag: 'Warn' as const, reason: 'Deployed Worker identity is unavailable.', evidence: data, coverage: 'incomplete' as const }
},
}),
})
5 changes: 2 additions & 3 deletions checks/external/git.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { defineExternalCheck, pass } from '@harlan-zw/nuxt-checkin/external'
import { withCause } from '../_helpers/cause.mjs'
import { collectGit } from '../_helpers/collectors.mjs'

export default defineExternalCheck({
id: 'skilld.git',
async run(context) {
return pass(await collectGit(context))
},
run: withCause(async context => pass(await collectGit(context))),
})
5 changes: 3 additions & 2 deletions checks/external/report.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { defineExternalCheck, defineReportCheck, unavailable } from '@harlan-zw/nuxt-checkin/external'
import { withCause } from '../_helpers/cause.mjs'
import { collectDeploy } from '../_helpers/collectors.mjs'

export default defineExternalCheck({
id: 'skilld.report',
async run(context) {
run: withCause(async (context) => {
const deployment = (await collectDeploy(context)).latest?.versionId
if (!deployment)
return unavailable('Deployed Worker identity is unavailable.')
Expand All @@ -17,5 +18,5 @@ export default defineExternalCheck({
required: ['skilld.daily-health', 'skilld.daily-health-coverage'],
maxAgeMs: 300_000,
}).run({ ...context, event: { rootDir: context.rootDir, since: context.since, previous: context.previous, clock: context.clock, env: { ...context.env, CHECKIN_DEPLOYMENT: deployment } } })
},
}),
})
5 changes: 3 additions & 2 deletions checks/external/workers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { defineExternalCheck, pass, warn } from '@harlan-zw/nuxt-checkin/external'
import { withCause } from '../_helpers/cause.mjs'
import { collectWorkers } from '../_helpers/collectors.mjs'

export default defineExternalCheck({
id: 'skilld.workers',
async run(context) {
run: withCause(async (context) => {
const data = await collectWorkers(context)
return data.nonOk.length ? warn('Worker requests have non-success outcomes.', data) : pass(data)
},
}),
})
9 changes: 9 additions & 0 deletions test/unit/external-checkin-collectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ it('rejects oversized Worker analytics evidence', async () => {
expect(report.results[0]?.result._tag).toBe('Unavailable')
})

// The published runOne archives every thrown error as the generic
// 'Check threw an exception.' and the CLI passes no onError callback, so the
// 2026-09-29 run's five Cloudflare failures reached the archive with no cause.
it('archives the collector failure cause instead of the generic throw message', async () => {
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Cloudflare API is unreachable.')))
const { report } = await runExternalChecks([workersCheck], { required: [workersCheck.id] }, { env: { CLOUDFLARE_USAGE_TOKEN: 'test-token' } })
expect(report.results[0]?.result).toEqual({ _tag: 'Unavailable', reason: 'Cloudflare API is unreachable.' })
})

// The ranked top page caps at 50 groups, so totals summed from that page
// silently undercount once a day holds more groups than the cap. The totals
// must come from their own untruncated rollup.
Expand Down