feat(integrations): disconnect individual checks per task#2491
feat(integrations): disconnect individual checks per task#2491
Conversation
…ring down the whole connection
Lets users disconnect a single integration check (e.g. GitHub branch protection)
from one evidence task while keeping the integration connected for every other
task that uses it. Previously users had to fully disconnect the integration and
reconnect with a subset of checks, which was slow and destructive.
Disable state lives under `IntegrationConnection.metadata.disabledTaskChecks` —
no new table, no migration. Works uniformly for static manifest-based and
dynamic integrations since both share the same connection pipeline.
- Per-task disable helpers with defensive merges (no mutation of inputs)
- `TaskIntegrationChecksService` handles enable/disable with org scoping
- New endpoints: POST `/v1/integrations/tasks/:taskId/checks/{disconnect,reconnect}`
- `runCheckForTask` returns 400 when invoked for a disabled (task, check) pair
- Daily orchestrator and per-task worker both filter disabled checks out of
the run list (orchestrator primary, worker defensive for race conditions)
- Task detail UI: per-check disconnect button with confirm dialog, new
"Disconnected from this task" section with reconnect buttons
- SWR hook exposes optimistic `disconnectCheckFromTask` / `reconnectCheckToTask`
with rollback on error
Tests: 32 new API unit tests + 4 frontend hook tests covering success,
idempotency, org scoping, unknown checks, and optimistic rollback.
PR SummaryMedium Risk Overview Updates the task integrations API to return Updates the daily scheduler and worker to filter out disabled checks (including a defensive re-check in the worker), and updates the task UI + Reviewed by Cursor Bugbot for commit 7795398. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 383f85e. Configure here.
…ect PR 1. Keep the disconnect confirmation dialog open during the async request so the "Disconnecting…" state is visible and any API error is surfaced in the dialog context. Radix's AlertDialogAction auto-closes on click, which was firing before handleConfirmDisconnect awaited — users never saw the loading state, and failures rendered on the main page behind the backdrop. Fix: e.preventDefault() in the action onClick, ignore close attempts while togglingCheck is set, and render errors inline inside the dialog. 2. Remove the unused TaskIntegrationChecksService.getDisabledCheckIdsForTask method. It was only referenced by its own tests — the controller reads disable state directly via the isCheckDisabledForTask utility, so the service method was dead code. Tests updated accordingly.
|
🎉 This PR is included in version 3.19.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |

Summary
Lets users disconnect a single integration check (e.g. GitHub Branch Protection) from one evidence task while keeping the integration fully connected for every other task that uses it. Previously the only way to drop one check was to fully disconnect the integration and reconnect with a subset — slow, destructive, and lost history.
No DB migration. Disable state lives under
IntegrationConnection.metadata.disabledTaskChecksas a map oftaskId → checkId[]. The metadata column is alreadyJson?, already returned to the frontend, and the existing PATCH connection endpoint already does a shallow merge, so there is nothing schema-level to change.Works for both static and dynamic integrations — both types share the same
IntegrationConnectiontable and the same check run pipeline (getManifest()is source-agnostic), so one filter point covers both.What changed
Backend (
apps/api)utils/disabled-task-checks.ts— pure, immutable helpers:parseDisabledTaskChecks,isCheckDisabledForTask,withCheckDisabled,withCheckEnabled.services/task-integration-checks.service.ts— orchestrates disable/enable with org scoping and validates that the task, connection, and check all belong together.controllers/task-integrations.controller.tsGET /v1/integrations/tasks/:taskId/checksnow returnsisDisabledForTaskon each check.POST /v1/integrations/tasks/:taskId/run-checkreturns 400 if the (task, check) pair is disconnected.POST /v1/integrations/tasks/:taskId/checks/disconnectPOST /v1/integrations/tasks/:taskId/checks/reconnectintegration:update.trigger/integration-platform/run-integration-checks-schedule.ts— daily orchestrator filters disabled checks out of each task's run list before batching.trigger/integration-platform/run-task-integration-checks.ts— defensive second filter in the worker for races between schedule and execution; the rest of the flow (lastSyncAt, task status eval, return payload) is untouched.Frontend (
apps/app)hooks/useIntegrationChecks.ts— addsdisconnectCheckFromTask/reconnectCheckToTaskwith SWR optimistic updates and rollback on error.components/TaskIntegrationChecks.tsxUnplugicon button on each connected check, behind anAlertDialogconfirm.Data shape (example)
{ "metadata": { "connectionName": "My GitHub", "disabledTaskChecks": { "tsk_abc123": ["branch_protection", "dependabot"], "tsk_xyz789": ["sanitized_inputs"] } } }Backward compatibility
disabledTaskCheckskey →parseDisabledTaskChecksreturns{}→ every filter/predicate no-ops → identical behavior to before.isDisabledForTaskis a new additive field on the check DTO; no existing consumer needs to change.Test plan
apps/api(Jest) — 32/32 passingutils/disabled-task-checks.spec.ts(22 tests): parse, isDisabled, withDisabled, withEnabled — empty/null inputs, malformed data, idempotency, immutability.services/task-integration-checks.service.spec.ts(10 tests): success path, idempotent disconnect, org scoping (connection from another org → 404), task not in org → 404, unknown check id → 400, reconnect cleans up empty lists.apps/app(Vitest) — 4/4 passingisDisabledForTaskfrom the API response.disconnectCheckFromTaskPOSTs to the disconnect endpoint and updates the SWR cache.reconnectCheckToTaskPOSTs to the reconnect endpoint and updates the SWR cache.Manual QA (for reviewer)
🤖 Generated with Claude Code