Support for multiple code hosting platforms including GitLab, Bitbucket Server, and Bitbucket Cloud with platform-specific APIs and utilities.
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>;
}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`);
}
}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");
}
});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
}
);
}
});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;
}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(", ")}`);
}
}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(", ")}`);
}
}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 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;
}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}`);
}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>;
}// 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");
}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}`);
}// 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");
}
}
});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># .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.yml
pipelines:
pull-requests:
'**':
- step:
name: Danger
script:
- npm install danger
- danger ci
services:
- docker// 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'
}
}
}
}
}
}
}