TypeScript-based CLI tool for code review automation that enforces team conventions in pull requests
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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 }}Install with Tessl CLI
npx tessl i tessl/npm-danger