also looking at this
fix(reports): honour reports.enabled and reports.period, drop the key nothing could read
#1429
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.
| @@ -3172,6 +3172,7 @@ cli | ||
| 3172 | 3172 | .option('--format <name>', 'Output format: markdown|json', { default: 'markdown' }) |
| 3173 | 3173 | .option('--prompt <focus>', 'Focus for the AI narrative, when a provider is configured') |
| 3174 | 3174 | .option('--publish', 'Publish to the repository\'s report issue') |
| 3175 | .option('--scheduled', 'This is the workflow\'s scheduled run; honour reports.enabled') | |
| 3175 | 3176 | .option('--verbose, -v', 'Enable verbose logging') |
| 3176 | 3177 | .example('buddy report --period 7d') |
| 3177 | 3178 | .example('buddy report --prompt "focus on security posture" --publish') |
| @@ -3180,6 +3181,7 @@ cli | ||
| 3180 | 3181 | format?: string |
| 3181 | 3182 | prompt?: string |
| 3182 | 3183 | publish?: boolean |
| 3184 | scheduled?: boolean | |
| 3183 | 3185 | }) => { |
| 3184 | 3186 | const quiet = options.format === 'json' |
| 3185 | 3187 | const logger = options.verbose ? Logger.verbose() : quiet ? Logger.silent() : Logger.quiet() |
| @@ -3199,7 +3201,9 @@ cli | ||
| 3199 | 3201 | withNarrative, |
| 3200 | 3202 | } = await import('../src/reports') |
| 3201 | 3203 | |
| 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' | |
| 3203 | 3207 | if (!REPORT_PERIODS.includes(period)) { |
| 3204 | 3208 | logger.error(`❌ Unknown period '${period}'. Expected one of ${REPORT_PERIODS.join(', ')}.`) |
| 3205 | 3209 | process.exit(1) |
| @@ -3267,6 +3271,16 @@ cli | ||
| 3267 | 3271 | logger, |
| 3268 | 3272 | ) |
| 3269 | 3273 | |
| 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 | ||
| 3270 | 3284 | if (!options.publish) { |
| 3271 | 3285 | process.stdout.write(`${report}\n`) |
| 3272 | 3286 | return |
| @@ -315,12 +315,9 @@ Scheduled dependency-health and activity reports. | ||
| 315 | 315 | |
| 316 | 316 | ```typescript |
| 317 | 317 | interface ReportSettings { |
| 318 | /** Generate reports (default: false) */ | |
| 318 | /** Publish the scheduled report (default: false) */ | |
| 319 | 319 | enabled?: boolean |
| 320 | 320 | |
| 321 | /** Cron expression for the scheduled run */ | |
| 322 | schedule?: string | |
| 323 | ||
| 324 | 321 | /** Reporting window (default: `30d`) */ |
| 325 | 322 | period?: '7d' | '30d' | '90d' |
| 326 | 323 | |
| @@ -339,6 +336,13 @@ The report is computed from scan results and pull request history, so it works | ||
| 339 | 336 | with no AI provider. A configured provider adds a narrative around the |
| 340 | 337 | numbers; it never produces them. |
| 341 | 338 | |
| 339 | `enabled` governs only the run the generated workflow starts — a cron trigger | |
| 340 | cannot read this file, so the workflow passes `--scheduled` and the command | |
| 341 | declines to publish unless it is `true`. Running `buddy report --publish` by | |
| 342 | hand publishes regardless. The cadence lives in the workflow's `schedule:` | |
| 343 | block; there is no config key for it, because nothing at trigger time could | |
| 344 | read one. | |
| 345 | ||
| 342 | 346 | ## Analysis |
| 343 | 347 | |
| 344 | 348 | ```typescript |
| @@ -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 | */ | |
| 17 | export 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 | } | |
| @@ -1915,7 +1915,10 @@ ${generateComposerSetupSteps()} | ||
| 1915 | 1915 | run: bun install |
| 1916 | 1916 | |
| 1917 | 1917 | - 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 | |
| 1919 | 1922 | |
| 1920 | 1923 | # Review a pull request on open, ready-for-review, or push |
| 1921 | 1924 | review: |
| @@ -226,10 +226,18 @@ export interface BuddyConfig { | ||
| 226 | 226 | * the numbers; it never produces them. |
| 227 | 227 | */ |
| 228 | 228 | 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 | */ | |
| 230 | 240 | enabled?: boolean |
| 231 | /** Cron expression for the scheduled run */ | |
| 232 | schedule?: string | |
| 233 | 241 | /** Reporting window (default: `30d`) */ |
| 234 | 242 | period?: '7d' | '30d' | '90d' |
| 235 | 243 | /** What the AI narrative should emphasise */ |
| @@ -0,0 +1,44 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { shouldPublishReport } from '../src/reports/publish-gate' | |
| 3 | import { 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 | */ | |
| 11 | describe('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 | ||
| 36 | describe('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 | }) | |