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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
DISABLED_TASK_CHECKS_KEY,
ENABLED_TASK_CHECKS_KEY,
isCheckDisabledForTask,
parseDisabledTaskChecks,
withCheckDisabled,
Expand Down Expand Up @@ -103,6 +104,39 @@ describe('disabled-task-checks utils', () => {
false,
);
});

it('defaults environment-separation checks to disconnected until reconnected', () => {
for (const checkId of ['aws-environment-separation', 'gcp-environment-separation', 'azure-environment-separation']) {
expect(isCheckDisabledForTask(null, 'tsk_abc', checkId)).toBe(true);
}

expect(
isCheckDisabledForTask(
{
[ENABLED_TASK_CHECKS_KEY]: {
tsk_abc: ['gcp-environment-separation'],
},
},
'tsk_abc',
'gcp-environment-separation',
),
).toBe(false);

expect(
isCheckDisabledForTask(
{
[DISABLED_TASK_CHECKS_KEY]: {
tsk_abc: ['gcp-environment-separation'],
},
[ENABLED_TASK_CHECKS_KEY]: {
tsk_abc: ['gcp-environment-separation'],
},
},
'tsk_abc',
'gcp-environment-separation',
),
).toBe(true);
});
});

describe('withCheckDisabled', () => {
Expand Down Expand Up @@ -178,6 +212,20 @@ describe('disabled-task-checks utils', () => {
tsk_xyz: ['sanitized_inputs'],
});
});

it('removes environment-separation opt-in when disconnecting', () => {
const metadata = {
[ENABLED_TASK_CHECKS_KEY]: {
tsk_abc: ['gcp-environment-separation'],
},
};
const result = withCheckDisabled(
metadata,
'tsk_abc',
'gcp-environment-separation',
);
expect(result[ENABLED_TASK_CHECKS_KEY]).toBeUndefined();
});
});

describe('withCheckEnabled', () => {
Expand Down Expand Up @@ -240,5 +288,12 @@ describe('disabled-task-checks utils', () => {
withCheckEnabled(metadata, 'tsk_abc', 'branch_protection');
expect(JSON.stringify(metadata)).toBe(snapshot);
});

it('records opt-in when reconnecting environment-separation checks', () => {
const result = withCheckEnabled(null, 'tsk_abc', 'gcp-environment-separation');
expect(result[ENABLED_TASK_CHECKS_KEY]).toEqual({
tsk_abc: ['gcp-environment-separation'],
});
});
});
});
199 changes: 166 additions & 33 deletions apps/api/src/integration-platform/utils/disabled-task-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,32 @@
*/

export const DISABLED_TASK_CHECKS_KEY = 'disabledTaskChecks';
export const ENABLED_TASK_CHECKS_KEY = 'enabledTaskChecks';

export type DisabledTaskChecksMap = Record<string, string[]>;

const DEFAULT_DISCONNECTED_CHECK_IDS = new Set([
'aws-environment-separation',
'gcp-environment-separation',
'azure-environment-separation',
]);

/**
* Parse the disabled task checks map from a connection's metadata JSON blob.
* Parse a task-check map from a connection's metadata JSON blob.
* Returns an empty map if the metadata is missing, malformed, or doesn't
* contain a `disabledTaskChecks` entry. Never throws.
* contain the requested entry. Never throws.
*/
export function parseDisabledTaskChecks(
metadata: unknown,
): DisabledTaskChecksMap {
function parseTaskChecksMap({
metadata,
key,
}: {
metadata: unknown;
key: string;
}): DisabledTaskChecksMap {
if (!metadata || typeof metadata !== 'object') {
return {};
}
const raw = (metadata as Record<string, unknown>)[DISABLED_TASK_CHECKS_KEY];
const raw = (metadata as Record<string, unknown>)[key];
if (!raw || typeof raw !== 'object') {
return {};
}
Expand All @@ -54,6 +65,84 @@ export function parseDisabledTaskChecks(
return result;
}

export function parseDisabledTaskChecks(
metadata: unknown,
): DisabledTaskChecksMap {
return parseTaskChecksMap({ metadata, key: DISABLED_TASK_CHECKS_KEY });
}

export function parseEnabledTaskChecks(
metadata: unknown,
): DisabledTaskChecksMap {
return parseTaskChecksMap({ metadata, key: ENABLED_TASK_CHECKS_KEY });
}

function hasCheck({
map,
taskId,
checkId,
}: {
map: DisabledTaskChecksMap;
taskId: string;
checkId: string;
}): boolean {
return map[taskId]?.includes(checkId) ?? false;
}

function addCheck({
map,
taskId,
checkId,
}: {
map: DisabledTaskChecksMap;
taskId: string;
checkId: string;
}): DisabledTaskChecksMap {
const current = map[taskId] ?? [];
if (current.includes(checkId)) return map;
return { ...map, [taskId]: [...current, checkId] };
}

function removeCheck({
map,
taskId,
checkId,
}: {
map: DisabledTaskChecksMap;
taskId: string;
checkId: string;
}): DisabledTaskChecksMap {
const current = map[taskId];
if (!current?.includes(checkId)) return map;

const nextChecks = current.filter((id) => id !== checkId);
const nextMap: DisabledTaskChecksMap = { ...map };
if (nextChecks.length === 0) {
delete nextMap[taskId];
} else {
nextMap[taskId] = nextChecks;
}
return nextMap;
}

function assignMap({
metadata,
key,
map,
deleteWhenEmpty = false,
}: {
metadata: Record<string, unknown>;
key: string;
map: DisabledTaskChecksMap;
deleteWhenEmpty?: boolean;
}) {
if (deleteWhenEmpty && Object.keys(map).length === 0) {
delete metadata[key];
return;
}
metadata[key] = map;
}

/**
* Returns true if the given checkId is disabled for the given taskId on this
* connection's metadata.
Expand All @@ -63,9 +152,13 @@ export function isCheckDisabledForTask(
taskId: string,
checkId: string,
): boolean {
const map = parseDisabledTaskChecks(metadata);
const disabled = map[taskId];
return Array.isArray(disabled) && disabled.includes(checkId);
if (hasCheck({ map: parseDisabledTaskChecks(metadata), taskId, checkId })) {
return true;
}
if (!DEFAULT_DISCONNECTED_CHECK_IDS.has(checkId)) {
return false;
}
return !hasCheck({ map: parseEnabledTaskChecks(metadata), taskId, checkId });
}

/**
Expand All @@ -83,20 +176,69 @@ export function withCheckDisabled(
? { ...(metadata as Record<string, unknown>) }
: {};
const map = parseDisabledTaskChecks(base);
const current = map[taskId] ?? [];
if (current.includes(checkId)) {
// Already disabled — return a merged copy so callers can safely write back.
base[DISABLED_TASK_CHECKS_KEY] = map;
return base;
}
const nextMap: DisabledTaskChecksMap = {
...map,
[taskId]: [...current, checkId],
};
base[DISABLED_TASK_CHECKS_KEY] = nextMap;
assignMap({
metadata: base,
key: DISABLED_TASK_CHECKS_KEY,
map: addCheck({
map,
taskId,
checkId,
}),
});
assignMap({
metadata: base,
key: ENABLED_TASK_CHECKS_KEY,
map: removeCheck({
map: parseEnabledTaskChecks(base),
taskId,
checkId,
}),
deleteWhenEmpty: true,
});
return base;
}

function enableDefaultDisconnectedCheck({
metadata,
taskId,
checkId,
}: {
metadata: Record<string, unknown>;
taskId: string;
checkId: string;
}) {
assignMap({
metadata,
key: ENABLED_TASK_CHECKS_KEY,
map: addCheck({
map: parseEnabledTaskChecks(metadata),
taskId,
checkId,
}),
deleteWhenEmpty: true,
});
}

function removeFromDisabledChecks({
metadata,
taskId,
checkId,
}: {
metadata: Record<string, unknown>;
taskId: string;
checkId: string;
}) {
assignMap({
metadata,
key: DISABLED_TASK_CHECKS_KEY,
map: removeCheck({
map: parseDisabledTaskChecks(metadata),
taskId,
checkId,
}),
});
}

/**
* Returns a new metadata object with the given check re-enabled for the given
* task. Cleans up empty arrays. If the check wasn't disabled, returns a merged
Expand All @@ -111,19 +253,10 @@ export function withCheckEnabled(
metadata && typeof metadata === 'object'
? { ...(metadata as Record<string, unknown>) }
: {};
const map = parseDisabledTaskChecks(base);
const current = map[taskId];
if (!current || !current.includes(checkId)) {
base[DISABLED_TASK_CHECKS_KEY] = map;
return base;
}
const nextChecks = current.filter((id) => id !== checkId);
const nextMap: DisabledTaskChecksMap = { ...map };
if (nextChecks.length === 0) {
delete nextMap[taskId];
} else {
nextMap[taskId] = nextChecks;

removeFromDisabledChecks({ metadata: base, taskId, checkId });
if (DEFAULT_DISCONNECTED_CHECK_IDS.has(checkId)) {
enableDefaultDisconnectedCheck({ metadata: base, taskId, checkId });
}
base[DISABLED_TASK_CHECKS_KEY] = nextMap;
return base;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { db, TaskFrequency } from '@db';
import { logger, schedules } from '@trigger.dev/sdk';
import { runTaskIntegrationChecks } from './run-task-integration-checks';
import { runDeviceSync } from './run-device-sync';
import { parseDisabledTaskChecks } from '../../integration-platform/utils/disabled-task-checks';
import { isCheckDisabledForTask } from '../../integration-platform/utils/disabled-task-checks';
import { isDueToday } from '../shared/is-due-today';

/**
Expand Down Expand Up @@ -175,20 +175,13 @@ export const integrationChecksSchedule = schedules.task({
);
}

// Per-task disabled checks are stored on the connection's metadata so
// users can disconnect individual checks from individual tasks without
// tearing down the whole integration. Resolve once per connection.
const disabledByTask = parseDisabledTaskChecks(connection.metadata);

for (const t of tasks) {
const disabledForThisTask = new Set(disabledByTask[t.id] ?? []);

// Find which checks apply to this task, minus any the user disabled
const checksForTask = checks
.filter(
(c) =>
c.taskMapping === t.taskTemplateId &&
!disabledForThisTask.has(c.id),
!isCheckDisabledForTask(connection.metadata, t.id, c.id),
)
.map((c) => c.id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { logger, tags, task } from '@trigger.dev/sdk';
import { triggerEmail } from '../../email/trigger-email';
import { TaskStatusChangedEmail } from '../../email/templates/task-status-changed';
import { isUserUnsubscribed } from '@trycompai/email';
import { parseDisabledTaskChecks } from '../../integration-platform/utils/disabled-task-checks';
import { isCheckDisabledForTask } from '../../integration-platform/utils/disabled-task-checks';
import {
getAccessToken,
requestValidCredentials,
Expand Down Expand Up @@ -342,11 +342,8 @@ export const runTaskIntegrationChecks = task({
// metadata and skip anything that's now disabled. The rest of the flow
// (lastSyncAt update, task status evaluation, return payload) runs as
// before — just over the filtered list instead of the original one.
const disabledForThisTask = new Set(
parseDisabledTaskChecks(connection.metadata)[taskId] ?? [],
);
const effectiveCheckIds = checkIds.filter(
(id) => !disabledForThisTask.has(id),
(id) => !isCheckDisabledForTask(connection.metadata, taskId, id),
);
if (effectiveCheckIds.length < checkIds.length) {
logger.info(
Expand Down
Loading