CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-danger

TypeScript-based CLI tool for code review automation that enforces team conventions in pull requests

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

platform-integrations.mddocs/

Platform Integrations

Support for multiple code hosting platforms including GitLab, Bitbucket Server, and Bitbucket Cloud with platform-specific APIs and utilities.

Capabilities

GitLab Integration

Comprehensive GitLab integration with merge request data, commit information, and full API access.

interface GitLabDSL {
  readonly metadata: RepoMetaData;
  readonly mr: Types.ExpandedMergeRequestSchema;
  readonly commits: Types.CommitSchema[];
  readonly approvals: Types.MergeRequestLevelMergeRequestApprovalSchema;
  readonly utils: {
    fileContents: (path: string, repoSlug?: string, ref?: string) => Promise<string>;
    addLabels: (...labels: string[]) => Promise<boolean>;
    removeLabels: (...labels: string[]) => Promise<boolean>;
  };
  readonly api: InstanceType<typeof Types.Gitlab>;
}

GitLab Merge Request Data

Usage Examples:

// Check MR properties (GitLab-specific)
if (danger.gitlab) {
  const mr = danger.gitlab.mr;
  
  // Check if MR is marked as WIP/Draft
  if (mr.work_in_progress) {
    message("🚧 This is a work in progress MR");
  }
  
  // Check MR labels
  if (mr.labels && mr.labels.length === 0) {
    warn("Please add labels to categorize this MR");
  }
  
  // Check approval status
  if (danger.gitlab.approvals.approved_by.length === 0) {
    message("⏰ Waiting for approvals");
  }
  
  // Check for time tracking
  if (mr.time_stats?.time_estimate > 0) {
    const hours = mr.time_stats.time_estimate / 3600;
    message(`⏱️ Estimated time: ${hours} hours`);
  }
}

GitLab Utilities

File Contents:

/**
 * Download file contents from GitLab
 * @param path - File path to download
 * @param repoSlug - Override repo (default: current MR repo)
 * @param ref - Override ref (default: MR head)
 * @returns Promise resolving to file contents
 */
fileContents(path: string, repoSlug?: string, ref?: string): Promise<string>;

Usage Examples:

// Analyze GitLab CI configuration
schedule(async () => {
  if (danger.git.modified_files.includes(".gitlab-ci.yml")) {
    const ciConfig = await danger.gitlab.utils.fileContents(".gitlab-ci.yml");
    
    if (ciConfig.includes("only:") && !ciConfig.includes("rules:")) {
      warn("Consider migrating from 'only:' to 'rules:' syntax in .gitlab-ci.yml");
    }
    
    if (!ciConfig.includes("artifacts:")) {
      message("💡 Consider adding artifacts to preserve build outputs");
    }
  }
});

Label Management:

/**
 * Add labels to the current merge request
 * @param labels - Label names to add
 * @returns Promise resolving to success boolean
 */
addLabels(...labels: string[]): Promise<boolean>;

/**
 * Remove labels from the current merge request
 * @param labels - Label names to remove
 * @returns Promise resolving to success boolean
 */
removeLabels(...labels: string[]): Promise<boolean>;

Usage Examples:

// Auto-label based on changes
schedule(async () => {
  const hasDocChanges = danger.git.modified_files.some(f => 
    f.includes("docs/") || f.includes("README")
  );
  
  const hasTestChanges = danger.git.modified_files.some(f =>
    f.includes(".test.") || f.includes(".spec.")
  );
  
  const labels = [];
  if (hasDocChanges) labels.push("documentation");
  if (hasTestChanges) labels.push("tests");
  
  if (labels.length > 0) {
    await danger.gitlab.utils.addLabels(...labels);
    message(`🏷️ Added labels: ${labels.join(", ")}`);
  }
});

// Remove labels based on completion
schedule(async () => {
  const hasAllTests = danger.git.modified_files.some(f => f.includes("src/"))
    ? danger.git.modified_files.some(f => f.includes(".test."))
    : true;
  
  if (hasAllTests) {
    await danger.gitlab.utils.removeLabels("needs-tests");
  }
});

GitLab API Access

Full access to GitLab API via @gitbeaker/rest.

// GitLab API client
readonly api: InstanceType<typeof Types.Gitlab>;

Usage Examples:

// Create MR notes/comments
schedule(async () => {
  await danger.gitlab.api.MergeRequestNotes.create(
    danger.gitlab.metadata.repoSlug.split('/')[1], // project ID
    parseInt(danger.gitlab.metadata.pullRequestID),
    "Automated comment from Danger"
  );
});

// Update MR properties
schedule(async () => {
  if (danger.gitlab.mr.assignee_id === null) {
    // Auto-assign to MR author
    await danger.gitlab.api.MergeRequests.edit(
      danger.gitlab.metadata.repoSlug.split('/')[1],
      parseInt(danger.gitlab.metadata.pullRequestID),
      {
        assignee_id: danger.gitlab.mr.author.id
      }
    );
  }
});

Bitbucket Server Integration

Enterprise Bitbucket Server support with pull request data, JIRA integration, and API access.

interface BitBucketServerDSL {
  readonly metadata: RepoMetaData;
  readonly issues: JIRAIssue[];
  readonly pr: BitBucketServerPRDSL;
  readonly commits: BitBucketServerCommit[];
  readonly comments: BitBucketServerPRActivity[];
  readonly activities: BitBucketServerPRActivity[];
  readonly api: BitBucketServerAPIDSL;
}

Bitbucket Server PR Data

interface BitBucketServerPRDSL {
  id: number;
  version: number;
  title: string;
  description: string;
  state: "OPEN" | "MERGED" | "DECLINED" | "SUPERSEDED";
  open: boolean;
  closed: boolean;
  createdDate: number;    // Unix timestamp
  updatedDate: number;    // Unix timestamp
  fromRef: BitBucketServerMergeRef;
  toRef: BitBucketServerMergeRef;
  locked: boolean;
  author: BitBucketServerPRParticipant & { role: "AUTHOR" };
  reviewers: (BitBucketServerPRParticipant & { role: "REVIEWER" })[];
  participants: (BitBucketServerPRParticipant & { role: "PARTICIPANT" })[];
}

Usage Examples:

// Bitbucket Server specific checks
if (danger.bitbucket_server) {
  const pr = danger.bitbucket_server.pr;
  
  // Check PR age
  const ageInDays = (Date.now() - pr.createdDate) / (1000 * 60 * 60 * 24);
  if (ageInDays > 7) {
    warn(`This PR has been open for ${Math.round(ageInDays)} days`);
  }
  
  // Check for approvals
  const approvals = pr.reviewers.filter(r => r.approved);
  if (approvals.length === 0) {
    message("⏰ Waiting for reviewer approvals");
  }
  
  // Check JIRA integration
  if (danger.bitbucket_server.issues.length === 0) {
    warn("No JIRA issues linked to this PR");
  } else {
    const jiraKeys = danger.bitbucket_server.issues.map(i => i.key);
    message(`🎫 Linked JIRA issues: ${jiraKeys.join(", ")}`);
  }
}

JIRA Integration

interface JIRAIssue {
  key: string;  // e.g., "PROJ-123"
  url: string;  // User-facing URL
}

Usage Examples:

// Validate JIRA linking
if (danger.bitbucket_server) {
  const jiraIssues = danger.bitbucket_server.issues;
  
  if (jiraIssues.length === 0) {
    // Check if PR title contains JIRA key
    const titleHasJiraKey = /[A-Z]+-\d+/.test(danger.bitbucket_server.pr.title);
    if (!titleHasJiraKey) {
      warn("Please link a JIRA issue or include the issue key in the title");
    }
  }
  
  // Check for multiple projects
  const projects = new Set(jiraIssues.map(issue => issue.key.split('-')[0]));
  if (projects.size > 1) {
    message(`📋 Multiple JIRA projects: ${Array.from(projects).join(", ")}`);
  }
}

Bitbucket Server API

interface BitBucketServerAPIDSL {
  getFileContents: (filePath: string, repoSlug?: string, refspec?: string) => Promise<string>;
  get: (path: string, headers: any, suppressErrors?: boolean) => Promise<any>;
  post: (path: string, headers: any, body: any, suppressErrors?: boolean) => Promise<any>;
  put: (path: string, headers: any, body: any) => Promise<any>;
  delete: (path: string, headers: any, body: any) => Promise<any>;
}

Usage Examples:

// Get file contents
schedule(async () => {
  const config = await danger.bitbucket_server.api.getFileContents(
    "config/production.json"
  );
  
  const parsedConfig = JSON.parse(config);
  if (parsedConfig.debug === true) {
    fail("🚨 Debug mode enabled in production config");
  }
});

// Make custom API calls
schedule(async () => {
  // Get branch permissions
  const permissions = await danger.bitbucket_server.api.get(
    `/rest/branch-permissions/2.0/projects/${projectKey}/repos/${repoSlug}/restrictions`,
    { "Accept": "application/json" }
  );
  
  if (permissions.values.length === 0) {
    warn("No branch protection rules configured");
  }
});

Bitbucket Cloud Integration

Bitbucket Cloud support with pull request data, cloud-specific features, and API access.

interface BitBucketCloudDSL {
  readonly metadata: RepoMetaData;
  readonly pr: BitBucketCloudPRDSL;
  readonly commits: BitBucketCloudCommit[];
  readonly comments: BitBucketCloudPRComment[];
  readonly activities: BitBucketCloudPRActivity[];
  readonly api: BitBucketCloudAPIDSL;
}

Bitbucket Cloud PR Data

interface BitBucketCloudPRDSL {
  id: number;
  title: string;
  description: string;
  state: "OPEN" | "MERGED" | "DECLINED" | "SUPERSEDED";
  created_on: string;     // ISO 8601 format
  updated_on: string;     // ISO 8601 format
  source: BitBucketCloudMergeRef;
  destination: BitBucketCloudMergeRef;
  author: BitBucketCloudUser;
  reviewers: BitBucketCloudUser[];
  participants: BitBucketCloudPRParticipant[];
}

Usage Examples:

// Bitbucket Cloud specific checks
if (danger.bitbucket_cloud) {
  const pr = danger.bitbucket_cloud.pr;
  
  // Check PR state
  if (pr.state === "OPEN") {
    const createdDate = new Date(pr.created_on);
    const daysSinceCreated = (Date.now() - createdDate.getTime()) / (1000 * 60 * 60 * 24);
    
    if (daysSinceCreated > 7) {
      warn(`PR has been open for ${Math.round(daysSinceCreated)} days`);
    }
  }
  
  // Check reviewers
  if (pr.reviewers.length === 0) {
    warn("No reviewers assigned to this PR");
  }
  
  // Check for approvals
  const approvals = pr.participants.filter(p => p.approved);
  message(`👍 Approvals: ${approvals.length}/${pr.participants.length}`);
}

Bitbucket Cloud API

interface BitBucketCloudAPIDSL {
  getFileContents: (filePath: string, repoSlug?: string, refspec?: string) => Promise<string>;
  get: (path: string, headers: any, suppressErrors?: boolean) => Promise<any>;
  post: (path: string, headers: any, body: any, suppressErrors?: boolean) => Promise<any>;
  put: (path: string, headers: any, body: any) => Promise<any>;
  delete: (path: string, headers: any, body: any) => Promise<any>;
}

Cross-Platform Patterns

Platform Detection

// Detect which platform is being used
if (danger.github) {
  // GitHub-specific logic
  message("Running on GitHub");
} else if (danger.gitlab) {
  // GitLab-specific logic
  message("Running on GitLab");
} else if (danger.bitbucket_server) {
  // Bitbucket Server-specific logic
  message("Running on Bitbucket Server");
} else if (danger.bitbucket_cloud) {
  // Bitbucket Cloud-specific logic
  message("Running on Bitbucket Cloud");
}

Universal Metadata

All platforms provide consistent metadata structure:

interface RepoMetaData {
  repoSlug: string;      // "owner/repo" format
  pullRequestID: string; // PR/MR number as string
}

Usage Examples:

// Universal repo information
const getRepoInfo = () => {
  if (danger.github) return danger.github.pr;
  if (danger.gitlab) return danger.gitlab.mr;
  if (danger.bitbucket_server) return danger.bitbucket_server.pr;
  if (danger.bitbucket_cloud) return danger.bitbucket_cloud.pr;
  return null;
};

const pr = getRepoInfo();
if (pr) {
  message(`Analyzing ${pr.title}`);
}

Universal File Content Access

// Platform-agnostic file content fetching
const getFileContents = async (path: string): Promise<string | null> => {
  try {
    if (danger.github) {
      return await danger.github.utils.fileContents(path);
    } else if (danger.gitlab) {
      return await danger.gitlab.utils.fileContents(path);
    } else if (danger.bitbucket_server) {
      return await danger.bitbucket_server.api.getFileContents(path);
    } else if (danger.bitbucket_cloud) {
      return await danger.bitbucket_cloud.api.getFileContents(path);
    }
    return null;
  } catch (error) {
    return null;
  }
};

// Usage
schedule(async () => {
  const packageJson = await getFileContents("package.json");
  if (packageJson) {
    const pkg = JSON.parse(packageJson);
    if (!pkg.license) {
      warn("package.json is missing license field");
    }
  }
});

Environment Configuration

Each platform requires different authentication setup:

# GitHub
DANGER_GITHUB_API_TOKEN=<token>
DANGER_GITHUB_API_BASE_URL=<custom-github-url>  # For GitHub Enterprise

# GitLab
DANGER_GITLAB_API_TOKEN=<token>
DANGER_GITLAB_HOST=<gitlab-host>                # For self-hosted GitLab

# Bitbucket Server
DANGER_BITBUCKETSERVER_USERNAME=<username>
DANGER_BITBUCKETSERVER_PASSWORD=<password>
DANGER_BITBUCKETSERVER_HOST=<server-url>

# Bitbucket Cloud
DANGER_BITBUCKETCLOUD_USERNAME=<username>
DANGER_BITBUCKETCLOUD_PASSWORD=<app-password>

CI Integration Examples

GitLab CI

# .gitlab-ci.yml
danger:
  stage: test
  script:
    - npm install danger
    - danger ci
  only:
    - merge_requests
  variables:
    DANGER_GITLAB_API_TOKEN: $CI_JOB_TOKEN

Bitbucket Pipelines

# bitbucket-pipelines.yml
pipelines:
  pull-requests:
    '**':
      - step:
          name: Danger
          script:
            - npm install danger
            - danger ci
          services:
            - docker

Jenkins (Multi-platform)

// Jenkinsfile
pipeline {
  agent any
  stages {
    stage('Danger') {
      when { 
        anyOf {
          changeRequest()
          branch 'PR-*'
        }
      }
      steps {
        script {
          if (env.CHANGE_URL?.contains('github.com')) {
            withCredentials([string(credentialsId: 'github-token', variable: 'DANGER_GITHUB_API_TOKEN')]) {
              sh 'danger ci'
            }
          } else if (env.CHANGE_URL?.contains('gitlab.com')) {
            withCredentials([string(credentialsId: 'gitlab-token', variable: 'DANGER_GITLAB_API_TOKEN')]) {
              sh 'danger ci'
            }
          }
        }
      }
    }
  }
}

Install with Tessl CLI

npx tessl i tessl/npm-danger

docs

cli-commands.md

dangerfile-api.md

git-utilities.md

github-integration.md

index.md

platform-integrations.md

tile.json