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
31 changes: 28 additions & 3 deletions apps/web/src/trpc/commands/automations/settings-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import {
db,
DEFAULT_CONFLICT_RESOLVER_LABEL,
deploymentSettings,
getAutomationRuntime,
getBackgroundAgentSettingsForDeployment,
MANAGER_CHANNEL_STARTER_AUTOMATION_SETTINGS,
upsertAutomation,
} from '@roomote/db/server';
import { resolveAutomationRuntimeDestination } from '@roomote/sdk/server';
import { validateSuggestionRoutingInstructions } from '@roomote/cloud-agents/server';
import { FeatureFlag } from '@roomote/feature-flags';
import { resolveConfiguredGitHubAppSlug } from '@roomote/github';
Expand All @@ -29,6 +31,7 @@ import {
hasActiveGitHubInstallation,
hasActiveRepository,
hasActiveSentryIntegration,
hasActiveSlackInstallation,
} from './automation-requirements';
import {
mergeLegacySingleChannelAutoStartRows,
Expand Down Expand Up @@ -679,9 +682,31 @@ export async function updateBackgroundAgentSettingsCommand(
}

if (!validation.channelId && !sharedManagerChannelId) {
const label =
getTriggerableBackgroundAutomationDescriptorByKey(validation.key)
?.label ?? validation.key;
// Without any Slack-level channel, the automation can still run when
// its runner supports another connected comms surface and a
// destination resolves there (an existing teams/telegram target, or
// the primary-conversation fallback on Slack-less deployments).
const descriptor = getTriggerableBackgroundAutomationDescriptorByKey(
validation.key,
);
const nonSlackProviders =
descriptor?.supportedCommunicationProviders.filter(
(provider) => provider !== 'slack',
) ?? [];

if (nonSlackProviders.length > 0) {
const runtime = await getAutomationRuntime(validation.key);
const destination = await resolveAutomationRuntimeDestination({
runtime,
slackConnected: await hasActiveSlackInstallation(),
});

if (destination && nonSlackProviders.includes(destination.provider)) {
continue;
}
}

const label = descriptor?.label ?? validation.key;
fieldErrors[validation.field] =
fieldErrors[validation.field] ||
`Choose a Slack channel before enabling ${label}.`;
Expand Down
34 changes: 29 additions & 5 deletions apps/web/src/trpc/commands/automations/trigger-agent.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
getTriggerableBackgroundAutomationDescriptorByKey,
isTriggerableBackgroundAutomationKey,
type CommunicationProvider,
type TriggerableBackgroundAutomationKey,
} from '@roomote/types';
import { getAutomationRuntime } from '@roomote/db/server';
import {
resolveAutomationRuntimeDestination,
runAutomationNow,
type AutomationRunNowResult,
} from '@roomote/sdk/server';
Expand Down Expand Up @@ -37,16 +39,38 @@ async function assertManualTriggerIsRunnable(
);
}

if (descriptor.usesManagerChannel && !runtime.slackChannelId) {
throw new Error(
`Set a Manager Channel before running ${descriptor.label}.`,
);
const slackConnected = await hasActiveSlackInstallation();
const destination = descriptor.usesManagerChannel
? await resolveAutomationRuntimeDestination({ runtime, slackConnected })
: null;

if (descriptor.usesManagerChannel) {
if (!destination) {
throw new Error(
`Set a Manager Channel before running ${descriptor.label}.`,
);
}

const supportedProviders: readonly CommunicationProvider[] =
descriptor.supportedCommunicationProviders;

if (!supportedProviders.includes(destination.provider)) {
throw new Error(
`${descriptor.label} cannot report to ${destination.provider} yet. Choose a Slack channel or the shared Manager Channel.`,
);
}
}

for (const requirement of descriptor.manualTriggerRequirements) {
switch (requirement) {
case 'slack':
if (!(await hasActiveSlackInstallation())) {
// A supported non-Slack destination satisfies the comms requirement;
// the Slack connection itself is only needed when the report goes to
// Slack.
if (destination && destination.provider !== 'slack') {
break;
}
if (!slackConnected) {
throw new Error(`Connect Slack before running ${descriptor.label}.`);
}
break;
Expand Down
50 changes: 50 additions & 0 deletions packages/db/src/lib/automations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,46 @@ export function resolveAutomationSlackChannelId(
return getAutomationSlackChannelTarget(automation) ?? managerSlackChannelId;
}

/** Provider-neutral resolved destination an automation reports to. */
export type AutomationDestination = {
provider: 'slack' | 'teams' | 'telegram';
channelId: string;
};

const DESTINATION_TARGET_KINDS = [
['slack', 'slack_channel'],
['teams', 'teams_channel'],
['telegram', 'telegram_chat'],
] as const;

/**
* Provider-neutral destination waterfall: the automation's own channel
* target wins (Slack first when several providers are targeted), otherwise
* the deployment-wide Slack manager channel. The primary-conversation
* fallbacks for Teams/Telegram deployments without any Slack live in the
* sdk runner layer, which owns those surfaces' installation lookups.
*/
export function resolveAutomationDestination(
automation: Pick<Automation, 'targets'> | undefined,
managerSlackChannelId: string | null,
): AutomationDestination | null {
for (const [provider, targetKind] of DESTINATION_TARGET_KINDS) {
const channelId = getAutomationTargetRefs(
automation,
provider,
targetKind,
)[0];

if (channelId) {
return { provider, channelId };
}
}

return managerSlackChannelId
? { provider: 'slack', channelId: managerSlackChannelId }
: null;
}

function getChannelAutoStartTargets(
automation: Automation | undefined,
): BackgroundAgentSettings['channelAutoStartSlackChannels'] {
Expand Down Expand Up @@ -546,6 +586,12 @@ export type AutomationRuntime = {
/** Automation slack_channel target, falling back to the manager channel. */
slackChannelId: string | null;
managerSlackChannelId: string | null;
/**
* Provider-neutral destination waterfall result (own target on any comms
* provider, else the Slack manager channel). Null when neither is set;
* runners may still fall back to a primary Teams/Telegram conversation.
*/
destination: AutomationDestination | null;
};

export async function getAutomationRuntime(
Expand Down Expand Up @@ -576,6 +622,10 @@ export async function getAutomationRuntime(
managerSlackChannelId,
),
managerSlackChannelId,
destination: resolveAutomationDestination(
automation ?? undefined,
managerSlackChannelId,
),
};
}

Expand Down
Loading
Loading