ReviewOS

also looking at this

stacks/buddy

fix: keep the low-tier promises — bulk-update label, event-name validation, workflow personalisation

#1434
Merged glennmichael123 wants to merge fix/low-tier-wiring into main
9 files +325 -13
src/config-validation.tsmodified+66-0
Changes to src/config-validation.ts
@@ -1,6 +1,7 @@
11import type { BuddyConfig } from './types'
22import type { GitProviderName } from './git/provider'
33import { BUILTIN_ANALYZERS } from './analysis/engine'
4import { EVENT_NAMES } from './events'
45import { IMPLEMENTED_PROVIDERS, PROVIDER_TRACKING_ISSUES } from './git/provider'
56import { ConfigurationError } from './types'
67
@@ -177,6 +178,7 @@ export function validateConfig(config: BuddyConfig): ConfigIssue[] {
177178 validateSchedules(issues, config)
178179 validateReleaseNotes(issues, config)
179180 validateDashboard(issues, config)
181 validateNotifications(issues, config)
180182 validateAi(issues, config)
181183 validateAnalysis(issues, config)
182184
@@ -688,6 +690,70 @@ function validateDashboard(issues: ConfigIssue[], config: BuddyConfig): void {
688690 issues.push({ path: 'dashboard.title', message: `expected a non-empty string, got ${quote(dashboard.title)}` })
689691}
690692
693function validateNotifications(issues: ConfigIssue[], config: BuddyConfig): void {
694 const notifications = config.notifications
695 if (notifications === undefined)
696 return
697 if (!isPlainObject(notifications)) {
698 issues.push({ path: 'notifications', message: `expected an object, got ${quote(notifications)}` })
699 return
700 }
701
702 // An `events` filter naming an event that does not exist subscribes to
703 // nothing: the sink is built, the credential is read, and every
704 // notification is silently dropped. `EVENT_NAMES` exists for exactly this
705 // check.
706 const checkEvents = (path: string, events: unknown): void => {
707 if (events === undefined)
708 return
709 if (!Array.isArray(events)) {
710 issues.push({ path, message: `expected an array of event names, got ${quote(events)}` })
711 return
712 }
713 events.forEach((event, index) => {
714 checkEnum(issues, `${path}[${index}]`, event, EVENT_NAMES)
715 })
716 }
717
718 const checkEnvName = (path: string, value: unknown): void => {
719 if (value !== undefined && (typeof value !== 'string' || !value.trim()))
720 issues.push({ path, message: `expected an environment variable name, got ${quote(value)}` })
721 }
722
723 for (const channel of ['slack', 'discord'] as const) {
724 const target = notifications[channel]
725 if (target === undefined)
726 continue
727 if (!isPlainObject(target)) {
728 issues.push({ path: `notifications.${channel}`, message: `expected an object, got ${quote(target)}` })
729 continue
730 }
731 checkEnvName(`notifications.${channel}.webhookEnv`, target.webhookEnv)
732 checkEvents(`notifications.${channel}.events`, target.events)
733 }
734
735 const webhooks = notifications.webhooks
736 if (webhooks === undefined)
737 return
738 if (!Array.isArray(webhooks)) {
739 issues.push({ path: 'notifications.webhooks', message: `expected an array, got ${quote(webhooks)}` })
740 return
741 }
742 webhooks.forEach((webhook, index) => {
743 const base = `notifications.webhooks[${index}]`
744 if (!isPlainObject(webhook)) {
745 issues.push({ path: base, message: `expected an object, got ${quote(webhook)}` })
746 return
747 }
748 if (webhook.url === undefined)
749 issues.push({ path: `${base}.url`, message: 'a webhook needs a url' })
750 else
751 checkUrl(issues, `${base}.url`, webhook.url)
752 checkEnvName(`${base}.secretEnv`, webhook.secretEnv)
753 checkEvents(`${base}.events`, webhook.events)
754 })
755}
756
691757/**
692758 * Render validation issues as a single multi-line, human-readable block.
693759 *