Comprehensive GitHub integration with full API access, utilities for common tasks, and GitHub Actions support.
Complete access to GitHub pull request metadata, API, and utilities.
interface GitHubDSL {
readonly issue: GitHubIssue;
readonly pr: GitHubPRDSL;
readonly thisPR: GitHubAPIPR;
readonly commits: GitHubCommit[];
readonly reviews: GitHubReview[];
readonly requested_reviewers: GitHubReviewers;
readonly api: GitHub; // Full Octokit REST API
readonly utils: GitHubUtilsDSL;
setSummaryMarkdown: (markdown: string) => void;
}interface GitHubPRDSL {
number: number;
state: "closed" | "open" | "locked" | "merged";
locked: boolean;
title: string;
body: string;
created_at: string;
updated_at: string;
closed_at: string | null;
merged_at: string | null;
draft: boolean;
merged: boolean;
// User information
user: GitHubUser;
assignee: GitHubUser;
assignees: GitHubUser[];
// Statistics
comments: number;
review_comments: number;
commits: number;
additions: number;
deletions: number;
changed_files: number;
// References
head: GitHubMergeRef;
base: GitHubMergeRef;
html_url: string;
// Author association
author_association: "COLLABORATOR" | "CONTRIBUTOR" | "FIRST_TIMER" |
"FIRST_TIME_CONTRIBUTOR" | "MEMBER" | "NONE" | "OWNER";
}Usage Examples:
// Check PR size
if (danger.github.pr.additions > 500) {
warn(`Large PR: ${danger.github.pr.additions} lines added. Consider breaking into smaller PRs.`);
}
// Check PR title conventions
if (!danger.github.pr.title.match(/^(feat|fix|docs|style|refactor|test|chore):/)) {
fail("PR title must start with a type (feat:, fix:, docs:, etc.)");
}
// Check for draft PRs
if (danger.github.pr.draft) {
message("π This is a draft PR");
}
// Check author association
if (danger.github.pr.author_association === "FIRST_TIME_CONTRIBUTOR") {
message("π Welcome! Thanks for your first contribution!");
}
// Validate PR description
if (danger.github.pr.body.length < 50) {
warn("Please add a more detailed PR description");
}interface GitHubUser {
id: number;
login: string;
type: "User" | "Organization" | "Bot";
avatar_url: string;
href: string;
}interface GitHubRepo {
id: number;
name: string;
full_name: string;
owner: GitHubUser;
private: boolean;
description: string;
fork: boolean;
html_url: string;
}interface GitHubIssue {
labels: GitHubIssueLabel[];
}
interface GitHubIssueLabel {
id: number;
url: string;
name: string;
color: string;
}Usage Examples:
// Check for required labels
const hasTypeLabel = danger.github.issue.labels.some(label =>
label.name.startsWith("type:")
);
if (!hasTypeLabel) {
warn("Please add a type: label to categorize this PR");
}
// Check for breaking change labels
const hasBreakingChange = danger.github.issue.labels.some(label =>
label.name === "breaking-change"
);
if (hasBreakingChange) {
fail("β οΈ This PR contains breaking changes. Update major version.");
}interface GitHubCommit {
commit: GitCommit;
sha: string;
url: string;
author: GitHubUser;
committer: GitHubUser;
parents: any[];
}
interface GitCommit {
sha: string;
author: GitCommitAuthor;
committer: GitCommitAuthor;
message: string;
tree: any;
parents?: string[];
url: string;
}Usage Examples:
// Check commit message conventions
const invalidCommits = danger.github.commits.filter(commit =>
!commit.commit.message.match(/^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+/)
);
if (invalidCommits.length > 0) {
fail(`${invalidCommits.length} commits don't follow conventional commit format`);
}
// Check for merge commits
const mergeCommits = danger.github.commits.filter(commit =>
commit.commit.message.startsWith("Merge")
);
if (mergeCommits.length > 0) {
warn("Found merge commits. Consider rebasing to maintain a clean history.");
}interface GitHubReview {
user: GitHubUser;
id?: number;
body?: string;
commit_id?: string;
state?: "APPROVED" | "REQUEST_CHANGES" | "COMMENT" | "PENDING";
}
interface GitHubReviewers {
users: GitHubUser[];
teams: any[];
}Usage Examples:
// Check for approvals
const approvals = danger.github.reviews.filter(review =>
review.state === "APPROVED"
);
if (approvals.length === 0) {
message("β° Waiting for approval from reviewers");
}
// Check for requested changes
const changesRequested = danger.github.reviews.filter(review =>
review.state === "REQUEST_CHANGES"
);
if (changesRequested.length > 0) {
warn("π Some reviewers have requested changes");
}Direct access to the complete GitHub REST API via Octokit.
// GitHub API client (Octokit instance)
readonly api: GitHub;Usage Examples:
// Create issue comments
schedule(async () => {
await danger.github.api.rest.issues.createComment({
owner: danger.github.thisPR.owner,
repo: danger.github.thisPR.repo,
issue_number: danger.github.thisPR.number,
body: "Automated comment from Danger"
});
});
// Add labels to PR
schedule(async () => {
await danger.github.api.rest.issues.addLabels({
owner: danger.github.thisPR.owner,
repo: danger.github.thisPR.repo,
issue_number: danger.github.thisPR.number,
labels: ["needs-review", "enhancement"]
});
});
// Request reviewers
schedule(async () => {
await danger.github.api.rest.pulls.requestReviewers({
owner: danger.github.thisPR.owner,
repo: danger.github.thisPR.repo,
pull_number: danger.github.thisPR.number,
reviewers: ["senior-dev1", "senior-dev2"]
});
});
// Create GitHub Check
schedule(async () => {
await danger.github.api.rest.checks.create({
owner: danger.github.thisPR.owner,
repo: danger.github.thisPR.repo,
name: "Custom Security Check",
head_sha: danger.github.pr.head.sha,
status: "completed",
conclusion: "success",
output: {
title: "Security scan passed",
summary: "No security vulnerabilities found"
}
});
});interface GitHubAPIPR {
owner: string;
repo: string;
number: number; // @deprecated use pull_number
pull_number: number;
}Usage Examples:
// Use thisPR for API calls
const { owner, repo, pull_number } = danger.github.thisPR;
schedule(async () => {
const prFiles = await danger.github.api.rest.pulls.listFiles({
owner,
repo,
pull_number
});
const largeFiles = prFiles.data.filter(file => file.changes > 100);
if (largeFiles.length > 0) {
warn(`Large files changed: ${largeFiles.map(f => f.filename).join(", ")}`);
}
});interface GitHubUtilsDSL {
fileLinks: (paths: string[], useBasename?: boolean, repoSlug?: string, branch?: string) => string;
fileContents: (path: string, repoSlug?: string, ref?: string) => Promise<string>;
}Creates clickable links to files in the GitHub interface.
/**
* Creates HTML links for file paths pointing to GitHub source.
* @param paths - Array of file paths
* @param useBasename - Show only filename (true) or full path (false)
* @param repoSlug - Override repo (default: current PR repo)
* @param branch - Override branch (default: PR head branch)
* @returns HTML string with clickable file links
*/
fileLinks(paths: string[], useBasename?: boolean, repoSlug?: string, branch?: string): string;Usage Examples:
// Create links for modified files
const modifiedFiles = danger.git.modified_files.slice(0, 5);
const fileLinks = danger.github.utils.fileLinks(modifiedFiles);
message(`Modified files: ${fileLinks}`);
// Show full paths instead of basenames
const fullPathLinks = danger.github.utils.fileLinks(
danger.git.created_files,
false // show full paths
);
// Link to files in different repo/branch
const crossRepoLinks = danger.github.utils.fileLinks(
["docs/README.md"],
true,
"company/docs-repo", // different repo
"main" // different branch
);Downloads file contents from GitHub for analysis.
/**
* Downloads file contents via GitHub API.
* @param path - File path to download
* @param repoSlug - Override repo (default: current PR repo)
* @param ref - Override ref (default: PR head)
* @returns Promise resolving to file contents as string
*/
fileContents(path: string, repoSlug?: string, ref?: string): Promise<string>;Usage Examples:
// Check package.json changes
schedule(async () => {
if (danger.git.modified_files.includes("package.json")) {
const packageJson = await danger.github.utils.fileContents("package.json");
const pkg = JSON.parse(packageJson);
if (!pkg.engines?.node) {
warn("Consider adding Node.js version requirement to package.json");
}
}
});
// Compare files across branches
schedule(async () => {
const mainReadme = await danger.github.utils.fileContents(
"README.md",
undefined, // current repo
"main" // main branch
);
const prReadme = await danger.github.utils.fileContents("README.md");
if (mainReadme.length > prReadme.length * 2) {
warn("README.md was significantly shortened. Was this intentional?");
}
});interface GitHubUtilsDSL {
createUpdatedIssueWithID: (
id: string,
content: string,
config: {
title: string;
open: boolean;
owner: string;
repo: string;
}
) => Promise<string>;
}Usage Examples:
// Create or update tracking issue
schedule(async () => {
const issueId = "weekly-metrics";
const content = `
## Weekly Metrics Report
- PRs merged: ${weeklyStats.mergedPRs}
- Issues closed: ${weeklyStats.closedIssues}
- New contributors: ${weeklyStats.newContributors}
`;
await danger.github.utils.createUpdatedIssueWithID(
issueId,
content,
{
title: "Weekly Metrics Report",
open: true,
owner: danger.github.thisPR.owner,
repo: danger.github.thisPR.repo
}
);
});interface GitHubUtilsDSL {
createOrAddLabel: (
labelConfig: {
name: string;
color: string;
description: string;
},
repoConfig?: {
owner: string;
repo: string;
id: number;
}
) => Promise<void>;
}Usage Examples:
// Ensure labels exist and add them
schedule(async () => {
// Create/ensure label exists
await danger.github.utils.createOrAddLabel({
name: "needs-docs",
color: "fef2c0",
description: "Documentation needs to be added or updated"
});
// Add to current PR
const hasDocChanges = danger.git.modified_files.some(f =>
f.includes("docs/") || f.includes("README")
);
if (!hasDocChanges && danger.git.modified_files.some(f => f.includes("src/"))) {
await danger.github.utils.createOrAddLabel(
{
name: "needs-docs",
color: "fef2c0",
description: "Documentation needs to be added or updated"
},
{
owner: danger.github.thisPR.owner,
repo: danger.github.thisPR.repo,
id: danger.github.thisPR.number
}
);
}
});interface GitHubUtilsDSL {
createOrUpdatePR: (
config: {
title: string;
body: string;
owner?: string;
repo?: string;
commitMessage: string;
newBranchName: string;
baseBranch: string;
},
fileMap: any
) => Promise<any>;
}Set markdown summaries that appear in GitHub Actions job overview.
/**
* Sets a markdown summary for GitHub Actions jobs.
* @param markdown - Markdown content for the job summary
*/
setSummaryMarkdown: (markdown: string) => void;Usage Examples:
// Create comprehensive job summary
const summary = `
## π Code Review Summary
### π PR Statistics
- **Files changed:** ${danger.github.pr.changed_files}
- **Lines added:** ${danger.github.pr.additions}
- **Lines removed:** ${danger.github.pr.deletions}
- **Commits:** ${danger.github.pr.commits}
### β
Checks Performed
- [x] Lint check
- [x] Test coverage
- [x] Security scan
- [x] Documentation update
### π― Quality Metrics
- Test coverage: 87%
- Code quality score: A-
- Security rating: β
Pass
`;
danger.github.setSummaryMarkdown(summary);GitHub Actions provides these environment variables automatically:
GITHUB_TOKEN # Automatically provided token
GITHUB_REPOSITORY # Repository slug (owner/repo)
GITHUB_EVENT_NAME # Event that triggered the workflow
GITHUB_SHA # Commit SHA
GITHUB_REF # Branch or tag refUsage in Workflows:
- name: Run Danger
run: danger ci
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}