ReviewOS

also looking at this

stacks/buddy

fix(reports): honour reports.enabled and reports.period, drop the key nothing could read

#1429
Merged glennmichael123 wants to merge fix/reports-config into main
7 files +118 -9

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

bin/cli.tsmodified+15-1
Changes to bin/cli.ts
@@ -3172,6 +3172,7 @@ cli
31723172 .option('--format <name>', 'Output format: markdown|json', { default: 'markdown' })
31733173 .option('--prompt <focus>', 'Focus for the AI narrative, when a provider is configured')
31743174 .option('--publish', 'Publish to the repository\'s report issue')
3175 .option('--scheduled', 'This is the workflow\'s scheduled run; honour reports.enabled')
31753176 .option('--verbose, -v', 'Enable verbose logging')
31763177 .example('buddy report --period 7d')
31773178 .example('buddy report --prompt "focus on security posture" --publish')
@@ -3180,6 +3181,7 @@ cli
31803181 format?: string
31813182 prompt?: string
31823183 publish?: boolean
3184 scheduled?: boolean
31833185 }) => {
31843186 const quiet = options.format === 'json'
31853187 const logger = options.verbose ? Logger.verbose() : quiet ? Logger.silent() : Logger.quiet()
@@ -3199,7 +3201,9 @@ cli
31993201 withNarrative,
32003202 } = await import('../src/reports')
32013203
3202 const period = (options.period ?? '30d') as '7d' | '30d' | '90d'
3204 // The flag wins, then config, then the default. `reports.period` was
3205 // documented, defaulted and never consulted — only the flag was.
3206 const period = (options.period ?? config.reports?.period ?? '30d') as '7d' | '30d' | '90d'
32033207 if (!REPORT_PERIODS.includes(period)) {
32043208 logger.error(`❌ Unknown period '${period}'. Expected one of ${REPORT_PERIODS.join(', ')}.`)
32053209 process.exit(1)
@@ -3267,6 +3271,16 @@ cli
32673271 logger,
32683272 )
32693273
3274 // The generated workflow passes --scheduled, so this is where the
3275 // documented "default: false" is actually enforced. It cannot be
3276 // enforced in the workflow: a cron trigger cannot read the config.
3277 const { shouldPublishReport } = await import('../src/reports')
3278 const gate = shouldPublishReport({ scheduled: Boolean(options.scheduled), enabled: config.reports?.enabled })
3279 if (options.publish && !gate.publish) {
3280 logger.info(`ℹ️ Not publishing: ${gate.reason}`)
3281 return
3282 }
3283
32703284 if (!options.publish) {
32713285 process.stdout.write(`${report}\n`)
32723286 return
docs/api/configuration.mdmodified+8-4
Changes to docs/api/configuration.md
@@ -315,12 +315,9 @@ Scheduled dependency-health and activity reports.
315315
316316```typescript
317317interface ReportSettings {
318 /** Generate reports (default: false) */
318 /** Publish the scheduled report (default: false) */
319319 enabled?: boolean
320320
321 /** Cron expression for the scheduled run */
322 schedule?: string
323
324321 /** Reporting window (default: `30d`) */
325322 period?: '7d' | '30d' | '90d'
326323
@@ -339,6 +336,13 @@ The report is computed from scan results and pull request history, so it works
339336with no AI provider. A configured provider adds a narrative around the
340337numbers; it never produces them.
341338
339`enabled` governs only the run the generated workflow starts — a cron trigger
340cannot read this file, so the workflow passes `--scheduled` and the command
341declines to publish unless it is `true`. Running `buddy report --publish` by
342hand publishes regardless. The cadence lives in the workflow's `schedule:`
343block; there is no config key for it, because nothing at trigger time could
344read one.
345
342346## Analysis
343347
344348```typescript
src/reports/index.tsmodified+1-0
Changes to src/reports/index.ts
@@ -21,3 +21,4 @@ export type {
2121 ReportPeriod,
2222} from './metrics'
2323export { REPORT_MARKER, renderReport, withNarrative } from './render'
24export { shouldPublishReport } from './publish-gate'
src/reports/publish-gate.tsadded+35-0
Changes to src/reports/publish-gate.ts
@@ -0,0 +1,35 @@
1/**
2 * Whether a report run should publish.
3 *
4 * `reports.enabled` was documented as the opt-in for scheduled reports and
5 * defaulted to `false`, and nothing read it — the generated workflow published
6 * every week regardless. A person at a terminal typing `--publish` has already
7 * decided; the setting exists to govern the run nobody typed.
8 *
9 * @param options - Whether this is the scheduled run, and what config says
10 * @returns Whether to publish, and why not when not
11 * @example
12 * ```ts
13 * shouldPublishReport({ scheduled: true, enabled: undefined })
14 * // { publish: false, reason: 'reports.enabled is not set' }
15 * ```
16 */
17export function shouldPublishReport(options: {
18 /** The run came from the generated workflow's schedule, not a person */
19 scheduled: boolean
20 /** `reports.enabled` from config */
21 enabled: boolean | undefined
22}): { publish: boolean, reason?: string } {
23 if (!options.scheduled)
24 return { publish: true }
25
26 if (options.enabled === true)
27 return { publish: true }
28
29 return {
30 publish: false,
31 reason: options.enabled === false
32 ? 'reports.enabled is false'
33 : 'reports.enabled is not set — scheduled reports are opt-in',
34 }
35}
src/setup.tsmodified+4-1
Changes to src/setup.ts
@@ -1915,7 +1915,10 @@ ${generateComposerSetupSteps()}
19151915 run: bun install
19161916
19171917 - name: Generate report
1918 run: bunx @buddysh/buddy report --publish --verbose
1918 # --scheduled is what makes reports.enabled mean anything: the cron
1919 # cannot read the config, so the command has to know this run came
1920 # from it rather than from a person who has already decided.
1921 run: bunx @buddysh/buddy report --publish --scheduled --verbose
19191922
19201923 # Review a pull request on open, ready-for-review, or push
19211924 review:
src/types.tsmodified+11-3
Changes to src/types.ts
@@ -226,10 +226,18 @@ export interface BuddyConfig {
226226 * the numbers; it never produces them.
227227 */
228228 reports?: {
229 /** Generate reports (default: false) */
229 /**
230 * Publish the scheduled report (default: false).
231 *
232 * Governs only the run the generated workflow starts: a cron trigger
233 * cannot read this file, so the workflow passes `--scheduled` and the
234 * command declines to publish unless this is `true`. A person running
235 * `buddy report --publish` by hand has already decided.
236 *
237 * The cadence itself lives in the workflow's `schedule:` block — there
238 * is no config key for it, because nothing at trigger time could read one.
239 */
230240 enabled?: boolean
231 /** Cron expression for the scheduled run */
232 schedule?: string
233241 /** Reporting window (default: `30d`) */
234242 period?: '7d' | '30d' | '90d'
235243 /** What the AI narrative should emphasise */
test/reports-config.test.tsadded+44-0
Changes to test/reports-config.test.ts
@@ -0,0 +1,44 @@
1import { describe, expect, it } from 'bun:test'
2import { shouldPublishReport } from '../src/reports/publish-gate'
3import { generateUnifiedWorkflow } from '../src/setup'
4
5/**
6 * `reports.enabled` was documented as the opt-in for scheduled reports and
7 * defaulted to `false`, and nothing read it — the generated workflow published
8 * every week regardless. `reports.period` was documented, defaulted and never
9 * consulted; only the `--period` flag was.
10 */
11describe('publishing a scheduled report', () => {
12 it('failure case - the scheduled run does not publish by default', () => {
13 // The documented default, enforced for the first time.
14 const gate = shouldPublishReport({ scheduled: true, enabled: undefined })
15
16 expect(gate.publish).toBe(false)
17 expect(gate.reason).toContain('opt-in')
18 })
19
20 it('failure case - the scheduled run honours an explicit false', () => {
21 expect(shouldPublishReport({ scheduled: true, enabled: false }).publish).toBe(false)
22 })
23
24 it('success case - the scheduled run publishes once enabled', () => {
25 expect(shouldPublishReport({ scheduled: true, enabled: true })).toEqual({ publish: true })
26 })
27
28 it('success case - a person at a terminal is not second-guessed', () => {
29 // `--publish` typed by hand is a decision already made; the setting exists
30 // to govern the run nobody typed.
31 expect(shouldPublishReport({ scheduled: false, enabled: false }).publish).toBe(true)
32 expect(shouldPublishReport({ scheduled: false, enabled: undefined }).publish).toBe(true)
33 })
34})
35
36describe('the generated report job', () => {
37 it('success case - marks its run as scheduled', () => {
38 // Without this the command cannot tell the cron from a person, and the
39 // opt-in has nothing to bite on.
40 const yaml = generateUnifiedWorkflow(false)
41
42 expect(yaml).toContain('buddy report --publish --scheduled')
43 })
44})