or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

cli-commands.mddangerfile-api.mdgit-utilities.mdgithub-integration.mdindex.mdplatform-integrations.md
tile.json

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'
            }
          }
        }
      }
    }
  }
}