Skip to content

Multi-Integration Support Plan #96

@dcramer

Description

@dcramer

Multi-Integration Support Plan

Summary

Enable multiple external integrations (GitHub, GitLab, Linear, Jira, Bitbucket) to run in parallel, with all integrations being optional.

Current State

  • GitHub is tightly coupled - GitHubSyncService directly used by TaskService
  • Task metadata has fixed github field in task.metadata.github
  • Config has sync.github section only
  • No abstraction layer exists for remote systems

Key Changes Needed

1. Create Sync Service Interface

New file: src/core/sync/interface.ts

export type IntegrationId = "github" | "gitlab" | "linear" | "jira" | "bitbucket";

export interface SyncService<T extends IntegrationMetadata = IntegrationMetadata> {
  readonly id: IntegrationId;
  readonly displayName: string;
  syncTask(task: Task, store: TaskStore): Promise<SyncResult<T> | null>;
  syncAll(store: TaskStore, options?: SyncAllOptions): Promise<SyncResult<T>[]>;
  getRemoteId(task: Task): string | number | null;
  getRemoteUrl(task: Task): string | null;
}

2. Create Sync Registry

New file: src/core/sync/registry.ts

Manages multiple sync services, allowing parallel execution:

export class SyncRegistry {
  register(service: SyncService): void;
  get(id: IntegrationId): SyncService | undefined;
  getAll(): SyncService[];
  hasServices(): boolean;
}

3. Update Task Metadata Schema

File: src/types.ts

Add integrations container to support multiple providers:

export const TaskMetadataSchema = z.object({
  commit: CommitMetadataSchema.optional(),
  github: GithubMetadataSchema.optional(),  // Keep for backward compat
  integrations: z.object({
    github: GithubMetadataSchema.optional(),
    gitlab: GitlabMetadataSchema.optional(),
    linear: LinearMetadataSchema.optional(),
    jira: JiraMetadataSchema.optional(),
    bitbucket: BitbucketMetadataSchema.optional(),
  }).optional(),
}).nullable();

4. Update Configuration

File: src/core/config.ts

Add base interface and provider-specific configs:

export interface IntegrationSyncConfig {
  enabled?: boolean;
  auto?: { on_change?: boolean; max_age?: string };
}

export interface SyncConfig {
  github?: GitHubSyncConfig;
  gitlab?: GitLabSyncConfig;
  linear?: LinearSyncConfig;
  jira?: JiraSyncConfig;
  bitbucket?: BitbucketSyncConfig;
}

5. Update TaskService

File: src/core/task-service.ts

Replace single syncService with syncRegistry:

export interface TaskServiceOptions {
  storage?: StorageEngine | string;
  archiveStorage?: ArchiveStorage;
  syncRegistry?: SyncRegistry | null;  // Replaces syncService
  syncConfig?: SyncConfig | null;
}

// New method to sync to all integrations in parallel
private async syncToIntegrations(task: Task): Promise<void> {
  if (!this.syncRegistry?.hasServices()) return;
  const services = this.syncRegistry.getAll();
  await Promise.allSettled(services.map(s => s.syncTask(task, store)));
}

6. Adapt GitHub Service

File: src/core/github/sync.ts

Implement the new SyncService interface:

export class GitHubSyncService implements SyncService<GithubMetadata> {
  readonly id = "github" as const;
  readonly displayName = "GitHub";

  getRemoteId(task: Task): number | null {
    // Check new format first, fall back to legacy
    return task.metadata?.integrations?.github?.issueNumber
        ?? task.metadata?.github?.issueNumber ?? null;
  }
  // ... existing methods
}

New Directory Structure

src/core/
├── sync/                     # NEW
│   ├── index.ts              # Re-exports
│   ├── interface.ts          # Core interfaces
│   ├── registry.ts           # SyncRegistry class
│   └── factory.ts            # Creates registry from config
├── github/                   # Existing, updated to implement interface
├── gitlab/                   # Future
├── linear/                   # Future
├── jira/                     # Future
└── bitbucket/                # Future

Migration Strategy

  1. Read from both, write to both: Support metadata.github and metadata.integrations.github
  2. Schema preprocessor migrates old format automatically
  3. Eventually deprecate legacy metadata.github field

Files to Modify

File Changes
src/types.ts Add IntegrationsMetadataSchema, provider-specific schemas
src/core/config.ts Add IntegrationSyncConfig base, provider configs
src/core/task-service.ts Replace syncService with syncRegistry, parallel sync
src/core/github/sync.ts Implement SyncService interface
src/core/github/sync-factory.ts Return service that implements interface
src/bootstrap.ts Use createSyncRegistry() instead of single factory
src/cli/sync.ts Support multiple integrations

New Files to Create

File Purpose
src/core/sync/interface.ts SyncService, SyncResult, IntegrationId types
src/core/sync/registry.ts SyncRegistry class
src/core/sync/factory.ts createSyncRegistry() from config
src/core/sync/index.ts Public exports

Example Config (Future)

[sync.github]
enabled = true
label_prefix = "dex"

[sync.linear]
enabled = true
team_key = "ENG"

[sync.github.auto]
on_change = true

[sync.linear.auto]
on_change = false
max_age = "1h"

Verification

  1. Run existing tests: pnpm test
  2. Test backward compatibility with existing GitHub sync
  3. Verify config loading with multiple integrations
  4. Test parallel sync behavior
  5. Verify metadata migration works correctly

Tasks

Update TaskService for Multiple Sync Services

Description

Modify src/core/task-service.ts:

  • Replace syncService?: GitHubSyncService with syncRegistry?: SyncRegistry
  • Rename syncToGitHub() to syncToIntegrations()
  • Implement parallel sync using Promise.allSettled across all registered services
  • Update saveIntegrationMetadata() to write to metadata.integrations[provider]
  • Update TaskServiceOptions interface

This is part of 'Multi-Integration Support Plan'.

Result

Updated TaskService to use SyncRegistry pattern for multi-integration support:

  • Created SyncRegistry class (src/core/sync/registry.ts) with RegisterableSyncService interface
  • Replaced syncService with syncRegistry in TaskServiceOptions
  • Renamed syncToGitHub() to syncToIntegrations() with Promise.allSettled for parallel sync
  • Updated saveIntegrationMetadata to write to both legacy metadata.github and new metadata.integrations[provider] for backward compatibility
  • Added id/displayName properties to GitHubSyncService for registry compatibility
  • Updated bootstrap.ts with createSyncRegistry() factory function
  • Updated CLI, MCP server, and index.ts to use new pattern
  • Added IntegrationsMetadataSchema to types.ts

Verification:

  • Build passes: pnpm build ✓
  • All 790 tests pass: pnpm test ✓
  • Code reviewed by code-simplifier agent
Update Task Metadata Schema

Description

Modify src/types.ts to add integrations container:

  • Add IntegrationsMetadataSchema with optional github, gitlab, linear, jira, bitbucket fields
  • Keep existing github field for backward compatibility
  • Add schema preprocessor migration to copy metadata.github to metadata.integrations.github
  • Add placeholder schemas for GitLab, Linear, Jira, Bitbucket metadata

This is part of 'Multi-Integration Support Plan'.

Result

Added IntegrationsMetadataSchema to src/types.ts:

  • Created IntegrationsMetadataSchema with z.object containing github field (future fields can be added)
  • Updated TaskMetadataSchema to include both:
    • github (legacy location for backward compatibility)
    • integrations (new multi-integration container)

The schema supports both read/write of metadata.github (legacy) and
metadata.integrations.github (new) for seamless migration.

Verification:

  • Build passes: pnpm build ✓
  • All 790 tests pass: pnpm test ✓
Create Sync Registry

Description

Create new file src/core/sync/registry.ts with SyncRegistry class:

  • register(service: SyncService): void
  • get(id: IntegrationId): SyncService | undefined
  • getAll(): SyncService[]
  • hasServices(): boolean
  • count(): number

Manages multiple sync services and allows parallel execution.

This is part of 'Multi-Integration Support Plan'.

Result

Created SyncRegistry class in src/core/sync/registry.ts:

  • register(service: RegisterableSyncService): void - Register a sync service
  • get(id: IntegrationId): RegisterableSyncService | undefined - Get service by ID
  • getAll(): RegisterableSyncService[] - Get all registered services
  • hasServices(): boolean - Check if any services are registered

Also created RegisterableSyncService interface and LegacySyncResult type to support
both new SyncService interface format and legacy GitHubSyncService format.

Verification:

  • Build passes: pnpm build ✓
  • All 790 tests pass: pnpm test ✓
Create Sync Service Interface

Description

Create new file src/core/sync/interface.ts with core abstractions:

  • IntegrationId type ('github' | 'gitlab' | 'linear' | 'jira' | 'bitbucket')
  • IntegrationMetadata base interface
  • SyncResult interface
  • SyncProgress interface
  • SyncAllOptions interface
  • SyncService interface with: id, displayName, syncTask(), syncAll(), getRemoteId(), getRemoteUrl()

This is part of 'Multi-Integration Support Plan'.

Result

Created src/core/sync/interface.ts with core abstractions for multi-integration sync support:

  • IntegrationId type ('github' | 'gitlab' | 'linear' | 'jira' | 'bitbucket')
  • IntegrationMetadata base interface with remoteId and remoteUrl
  • SyncResult generic interface for sync operation results
  • SyncProgress interface for progress callbacks during syncAll
  • SyncAllOptions interface for batch sync configuration
  • SyncService interface with: id, displayName, syncTask(), syncAll(), getRemoteId(), getRemoteUrl()

Also created src/core/sync/index.ts barrel export file.

Verification:

  • Build passes: pnpm build ✓
  • Tests pass: pnpm test ✓
  • Code reviewed by code-simplifier agent
Adapt GitHub Service to SyncService Interface

Description

Modify src/core/github/sync.ts:

  • Add 'implements SyncService' to GitHubSyncService
  • Add readonly id = 'github' property
  • Add readonly displayName = 'GitHub' property
  • Implement getRemoteId() reading from both old and new metadata formats
  • Implement getRemoteUrl() reading from both old and new metadata formats
  • Update sync-factory.ts to work with new registry pattern
  • Create src/core/sync/factory.ts with createSyncRegistry()

This is part of 'Multi-Integration Support Plan'.

Result

Added getRemoteId() and getRemoteUrl() methods to GitHubSyncService to support the SyncService interface. Both methods handle legacy and new metadata formats for backward compatibility.

Update Configuration Types

Description

Modify src/core/config.ts:

  • Add IntegrationSyncConfig base interface with enabled and auto fields
  • Add GitLabSyncConfig, LinearSyncConfig, JiraSyncConfig, BitbucketSyncConfig interfaces
  • Update SyncConfig to include all provider config options
  • Update mergeSyncConfig() to handle multiple integrations

This is part of 'Multi-Integration Support Plan'.

Result

Added IntegrationSyncAuto and IntegrationSyncConfig base interfaces. GitHubSyncConfig now extends IntegrationSyncConfig. Added generic mergeIntegrationConfig() helper for config merging. Build and all 791 tests pass.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions