Skip to content

Latest commit

 

History

History
106 lines (82 loc) · 3.78 KB

File metadata and controls

106 lines (82 loc) · 3.78 KB

Writing Custom Providers

Providers bridge the gap between deterministic Git hooks and your agile ecosystem. Use them to fetch rich ticket status from Jira, Linear, or custom databases without inflating the core defense-in-depth engine.

If the default file provider does not meet your needs, you can easily wire your own TicketStateProvider.

Lifecycle of a Custom Provider

sequenceDiagram
    participant Hook as Git Hook Engine
    participant CP as Custom Provider
    participant API as External Service (Linear/Jira)
    
    Hook->>CP: resolve("TK-123")
    activate CP
    CP->>API: GET /api/tickets/TK-123
    API-->>CP: JSON Response
    CP-->>Hook: TicketRef { id, phase, type }
    deactivate CP
    Note right of Hook: Pipeline passes context to Guards
Loading

The TicketStateProvider Interface

All providers must implement the TicketStateProvider interface:

export interface TicketStateProvider {
  /** 
   * Name of the provider. Used in `defense.config.yml`.
   */
  name: string;

  /**
   * Resolves context about a ticket given its ID.
   * 
   * @param ticketId - The ID of the ticket to look up.
   * @returns A promise resolving to a TicketRef or undefined if not found/error.
   */
  resolve(ticketId: string): Promise<TicketRef | undefined>;
}

Creating a Provider

Here is an example custom provider that fetches ticket identities from an external JSON API:

import type { TicketStateProvider, TicketRef, ProviderConfig } from "defense-in-depth";

export class ApiTicketProvider implements TicketStateProvider {
  readonly name = "myApi";
  private endpoints: string;

  constructor(config?: ProviderConfig) {
    this.endpoints = config?.providerConfig?.endpoint ?? "https://api.mycompany.com/tickets";
  }

  async resolve(ticketId: string): Promise<TicketRef | undefined> {
    // [Anti-Spam Guard] In production, you should check a local FileSystem cache 
    // (e.g. .git/defense-cache.json) before hitting the network to keep Git hooks < 50ms.

    // [Dangling Socket Guard] Always use an AbortController so Promise.race timeouts
    // don't leave zombie connections hanging in the background.
    const controller = new AbortController();
    const timeoutMsg = setTimeout(() => controller.abort(), 1000); 

    try {
      const response = await fetch(`${this.endpoints}/${ticketId}`, { 
        signal: controller.signal 
      });
      clearTimeout(timeoutMsg);

      if (!response.ok) return undefined;

      const data = await response.json();
      return {
        id: data.id,
        phase: data.status,
        type: data.team === 'docs' ? 'docs' : 'feat'
      };
    } catch (err) {
      clearTimeout(timeoutMsg);
      // Providers must NOT throw exceptions! They should gracefully absorb errors.
      if (err.name === 'AbortError') {
        console.warn(`\n[myApi] Timeout fetching ${ticketId}. Proceeding blindly.`);
      }
      return undefined;
    }
  }
}

Contract Rules

When developing custom providers, you must adhere to the Provider Contract:

  1. Graceful Failures: I/O is inherently unsafe. Do not throw exceptions, as it will crash the git hook. catch all asynchronous errors and return undefined with a simple warning.
  2. Speed & Timeout: Since hooks block developers from committing/pushing, providers must be fast. defense-in-depth wrappers invoke your hook with a default global timeout.
  3. No Side-Effects: Your resolve function should only read data, not modify external systems or file states.

Wiring Up The Custom Provider

Note: In v0.3.x, dynamically loaded custom providers are still experimental. Built-in providers (like the file provider) ship with the engine. Future versions will support defense-in-depth scanning for custom .js provider classes based on configuration.