diff --git a/dangerfile.ts b/dangerfile.ts index f7891ceec..0c6c1a63a 100644 --- a/dangerfile.ts +++ b/dangerfile.ts @@ -14,10 +14,14 @@ declare function fail(params: string): void // declare function schedule(callback: (resolve: any) => void): void const checkREADME = async () => { + if (!danger.github) { + return + } + // Request a CHANGELOG entry if not declared #trivial const hasChangelog = danger.git.modified_files.includes("CHANGELOG.md") const isTrivial = (danger.github.pr.body + danger.github.pr.title).includes("#trivial") - const isGreenkeeper = danger.github.pr.user.login === "greenkeeper" + const isGreenkeeper = danger.github!.pr.user.login === "greenkeeper" // Politely ask for their name on the entry too if (!hasChangelog && !isTrivial && !isGreenkeeper) { diff --git a/source/ci_source/ci_source_helpers.ts b/source/ci_source/ci_source_helpers.ts index 0aff03b9a..a12213ae0 100644 --- a/source/ci_source/ci_source_helpers.ts +++ b/source/ci_source/ci_source_helpers.ts @@ -48,6 +48,10 @@ export function ensureEnvKeysAreInt(env: Env, keys: string[]): boolean { * If there are multiple pull requests open for a branch, returns the first. */ export async function getPullRequestIDForBranch(metadata: RepoMetaData, env: Env, branch: string): Promise { + if (process.env["DANGER_BITBUCKETSERVER_HOST"]) { + // TODO: + } + const token = env["DANGER_GITHUB_API_TOKEN"] if (!token) { return 0 diff --git a/source/commands/danger-pr.ts b/source/commands/danger-pr.ts index 107118a52..9913b9bc1 100644 --- a/source/commands/danger-pr.ts +++ b/source/commands/danger-pr.ts @@ -5,15 +5,14 @@ import * as debug from "debug" import * as jsome from "jsome" import { FakeCI } from "../ci_source/providers/Fake" -import { GitHub } from "../platforms/GitHub" -import { GitHubAPI } from "../platforms/github/GitHubAPI" -import { pullRequestParser } from "../platforms/github/pullRequestParser" +import { pullRequestParser } from "../platforms/pullRequestParser" import { dangerfilePath } from "./utils/file-utils" import validateDangerfileExists from "./utils/validateDangerfileExists" import setSharedArgs, { SharedCLI } from "./utils/sharedDangerfileArgs" import { jsonDSLGenerator } from "../runner/dslGenerator" import { prepareDangerDSL } from "./utils/runDangerSubprocess" import { runRunner } from "./ci/runner" +import { Platform, getPlatformForEnv } from "../platforms/platform" // yarn build; cat source/_tests/fixtures/danger-js-pr-384.json | node --inspect --inspect-brk distribution/commands/danger-runner.js --text-only @@ -35,7 +34,7 @@ program .on("--help", () => { log("\n") log(" Docs:") - if (!process.env["DANGER_GITHUB_API_TOKEN"]) { + if (!process.env["DANGER_GITHUB_API_TOKEN"] && !process.env["DANGER_BITBUCKETSERVER_HOST"]) { log("") log(" You don't have a DANGER_GITHUB_API_TOKEN set up, this is optional, but TBH, you want to do this.") log(" Check out: http://danger.systems/js/guides/the_dangerfile.html#working-on-your-dangerfile") @@ -71,19 +70,13 @@ if (program.args.length === 0) { // TODO: Use custom `fetch` in GitHub that stores and uses local cache if PR is closed, these PRs // shouldn't change often and there is a limit on API calls per hour. - const token = process.env["DANGER_GITHUB_API_TOKEN"] - if (!token) { - console.log("You don't have a DANGER_GITHUB_API_TOKEN set up, this is optional, but TBH, you want to do this") - console.log("Check out: http://danger.systems/js/guides/the_dangerfile.html#working-on-your-dangerfile") - } - console.log(`Starting Danger PR on ${pr.repo}#${pr.pullRequestNumber}`) if (validateDangerfileExists(dangerFile)) { d(`executing dangerfile at ${dangerFile}`) const source = new FakeCI({ DANGER_TEST_REPO: pr.repo, DANGER_TEST_PR: pr.pullRequestNumber }) - const api = new GitHubAPI(source, token) - const platform = new GitHub(api) + const platform = getPlatformForEnv(process.env, source, /* requireAuth */ false) + if (app.json || app.js) { d("getting just the JSON/JS DSL") runHalfProcessJSON(platform) @@ -102,7 +95,7 @@ if (program.args.length === 0) { } // Run the first part of a Danger Process and output the JSON to CLI -async function runHalfProcessJSON(platform: GitHub) { +async function runHalfProcessJSON(platform: Platform) { const dangerDSL = await jsonDSLGenerator(platform) const processInput = prepareDangerDSL(dangerDSL) const output = JSON.parse(processInput) diff --git a/source/commands/danger-process.ts b/source/commands/danger-process.ts index 0d09f9aed..c3a80dcf1 100644 --- a/source/commands/danger-process.ts +++ b/source/commands/danger-process.ts @@ -60,7 +60,7 @@ getRuntimeCISource(app).then(source => { if (!platform) { console.log(chalk.red(`Could not find a source code hosting platform for ${source.name}.`)) console.log( - `Currently Danger JS only supports GitHub, if you want other platforms, consider the Ruby version or help out.` + `Currently Danger JS only supports GitHub and BitBucket Server, if you want other platforms, consider the Ruby version or help out.` ) process.exitCode = 1 } diff --git a/source/dsl/DangerDSL.ts b/source/dsl/DangerDSL.ts index 503a1464b..508e42150 100644 --- a/source/dsl/DangerDSL.ts +++ b/source/dsl/DangerDSL.ts @@ -1,6 +1,6 @@ import { GitDSL, GitJSONDSL } from "../dsl/GitDSL" import { GitHubDSL } from "../dsl/GitHubDSL" -import { BitBucketServerDSL } from "../dsl/BitBucketServerDSL" +import { BitBucketServerDSL, BitBucketServerJSONDSL } from "../dsl/BitBucketServerDSL" import { DangerUtilsDSL } from "./DangerUtilsDSL" import { CliArgs } from "../runner/cli-args" @@ -50,7 +50,9 @@ export interface DangerDSLJSONType { /** The data only version of Git DSL */ git: GitJSONDSL /** The data only version of GitHub DSL */ - github: GitHubDSL + github?: GitHubDSL + /** The data only version of BitBucket Server DSL */ + bitbucket_server?: BitBucketServerJSONDSL /** * Used in the Danger JSON DSL to pass metadata between * processes. It will be undefined when used inside the Danger DSL @@ -102,7 +104,9 @@ export interface DangerDSLType { * this is the full JSON from the webhook. You can find the full * typings for those webhooks [at github-webhook-event-types](https://github.com/orta/github-webhook-event-types). */ - readonly github: GitHubDSL + readonly github?: GitHubDSL + + readonly bitbucket_server?: BitBucketServerDSL /** * Functions which are globally useful in most Dangerfiles. Right diff --git a/source/platforms/BitBucketServer.ts b/source/platforms/BitBucketServer.ts index 7fdfedf51..65400cca3 100644 --- a/source/platforms/BitBucketServer.ts +++ b/source/platforms/BitBucketServer.ts @@ -2,10 +2,11 @@ import { GitJSONDSL } from "../dsl/GitDSL" import { BitBucketServerPRDSL, BitBucketServerJSONDSL } from "../dsl/BitBucketServerDSL" import { BitBucketServerAPI } from "./bitbucket_server/BitBucketServerAPI" import gitDSLForBitBucketServer from "./bitbucket_server/BitBucketServerGit" +import { Platform } from "./platform" /** Handles conforming to the Platform Interface for BitBucketServer, API work is handle by BitBucketServerAPI */ -export class BitBucketServer { +export class BitBucketServer implements Platform { name: string constructor(public readonly api: BitBucketServerAPI) { diff --git a/source/platforms/GitHub.ts b/source/platforms/GitHub.ts index 38797f5fe..e1d418102 100644 --- a/source/platforms/GitHub.ts +++ b/source/platforms/GitHub.ts @@ -5,10 +5,11 @@ import GitHubUtils from "./github/GitHubUtils" import gitDSLForGitHub from "./github/GitHubGit" import * as NodeGitHub from "@octokit/rest" +import { Platform } from "./platform" /** Handles conforming to the Platform Interface for GitHub, API work is handle by GitHubAPI */ -export class GitHub { +export class GitHub implements Platform { name: string constructor(public readonly api: GitHubAPI) { diff --git a/source/platforms/github/_tests/_pull_request_parser.test.ts b/source/platforms/_tests/_pull_request_parser.test.ts similarity index 57% rename from source/platforms/github/_tests/_pull_request_parser.test.ts rename to source/platforms/_tests/_pull_request_parser.test.ts index 61f5b5744..96d545f04 100644 --- a/source/platforms/github/_tests/_pull_request_parser.test.ts +++ b/source/platforms/_tests/_pull_request_parser.test.ts @@ -19,4 +19,18 @@ describe("parsing urls", () => { repo: "artsy/emission", }) }) + + it("handles bitbucket server PRs", () => { + expect(pullRequestParser("http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1")).toEqual({ + pullRequestNumber: "1", + repo: "projects/PROJ/repos/repo", + }) + }) + + it("handles bitbucket server PRs (overview)", () => { + expect(pullRequestParser("http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1/overview")).toEqual({ + pullRequestNumber: "1", + repo: "projects/PROJ/repos/repo", + }) + }) }) diff --git a/source/platforms/bitbucket_server/BitBucketServerAPI.ts b/source/platforms/bitbucket_server/BitBucketServerAPI.ts index 9cfab1cbf..c5b0c4091 100644 --- a/source/platforms/bitbucket_server/BitBucketServerAPI.ts +++ b/source/platforms/bitbucket_server/BitBucketServerAPI.ts @@ -21,14 +21,13 @@ import { api as fetch } from "../../api/fetch" export class BitBucketServerAPI { fetch: typeof fetch - private readonly baseUrl = process.env["DANGER_BITBUCKETSERVER_HOST"] private readonly d = debug("danger:BitBucketServerAPI") private pr: BitBucketServerPRDSL constructor( public readonly repoMetadata: RepoMetaData, - public readonly repoCredentials: { host: string; username: string; password: string } + public readonly repoCredentials: { host: string; username?: string; password?: string } ) { // This allows Peril to DI in a new Fetch function // which can handle unique API edge-cases around integrations @@ -36,14 +35,8 @@ export class BitBucketServerAPI { } private getPRBasePath(service = "api") { - const [projectKey, repositorySlug] = this.repoMetadata.repoSlug.split("/") - const pullRequestId = this.repoMetadata.pullRequestID - return ( - `${this.baseUrl}/rest/${service}/1.0/` + - `projects/${projectKey}/` + - `repos/${repositorySlug}/` + - `pull-requests/${pullRequestId}` - ) + const { repoSlug, pullRequestID } = this.repoMetadata + return `rest/${service}/1.0/${repoSlug}/pull-requests/${pullRequestID}` } getPullRequestInfo = async (): Promise => { @@ -52,19 +45,16 @@ export class BitBucketServerAPI { } const path = this.getPRBasePath() const res = await this.get(path) + throwIfNotOk(res) const prDSL = (await res.json()) as BitBucketServerPRDSL this.pr = prDSL - - if (res.ok) { - return prDSL - } else { - throw `Could not get PR Metadata for ${path}` - } + return prDSL } getPullRequestCommits = async (): Promise => { const path = `${this.getPRBasePath()}/commits` const res = await this.get(path) + throwIfNotOk(res) return (await res.json()).values } @@ -76,18 +66,21 @@ export class BitBucketServerAPI { getPullRequestComments = async (): Promise => { const path = `${this.getPRBasePath()}/activities?fromType=COMMENT` const res = await this.get(path) + throwIfNotOk(res) return (await res.json()).values } getPullRequestActivities = async (): Promise => { const path = `${this.getPRBasePath()}/activities?fromType=ACTIVITY` const res = await this.get(path) + throwIfNotOk(res) return (await res.json()).values } getIssues = async (): Promise => { const path = `${this.getPRBasePath("jira")}/issues` const res = await this.get(path) + throwIfNotOk(res) return await res.json() } @@ -105,13 +98,8 @@ export class BitBucketServerAPI { } getFileContents = async (filePath: string) => { - const [projectKey, repositorySlug] = this.repoMetadata.repoSlug.split("/") - const path = - `${this.baseUrl}/` + - `projects/${projectKey}/` + - `repos/${repositorySlug}/` + - `raw/${filePath}` + - `?at=${this.pr.toRef.id}` + const { repoSlug } = this.repoMetadata + const path = `${repoSlug}/` + `raw/${filePath}` + `?at=${this.pr.toRef.id}` const res = await this.get(path) return await res.text() } @@ -161,7 +149,7 @@ export class BitBucketServerAPI { private api = (path: string, headers: any = {}, body: any = {}, method: string, suppressErrors?: boolean) => { if (this.repoCredentials.username) { - headers["Authorization"] = `basic ${new Buffer( + headers["Authorization"] = `Basic ${new Buffer( this.repoCredentials.username + ":" + this.repoCredentials.password ).toString("base64")}` } @@ -194,3 +182,13 @@ export class BitBucketServerAPI { delete = (path: string, headers: any = {}, body: any = {}): Promise => this.api(path, headers, JSON.stringify(body), "DELETE") } + +function throwIfNotOk(res: node_fetch.Response) { + if (!res.ok) { + let message = `${res.status} - ${res.statusText}` + if (res.status >= 400 && res.status < 500) { + message += ` (Have you set DANGER_BITBUCKETSERVER_USERNAME and DANGER_BITBUCKETSERVER_PASSWORD?)` + } + throw new Error(message) + } +} diff --git a/source/platforms/bitbucket_server/BitBucketServerGit.ts b/source/platforms/bitbucket_server/BitBucketServerGit.ts index c6339aa67..44638dc9e 100644 --- a/source/platforms/bitbucket_server/BitBucketServerGit.ts +++ b/source/platforms/bitbucket_server/BitBucketServerGit.ts @@ -1,5 +1,3 @@ -import { URL } from "url" - import { GitDSL, GitJSONDSL } from "../../dsl/GitDSL" import { BitBucketServerCommit, BitBucketServerDSL } from "../../dsl/BitBucketServerDSL" import { GitCommit } from "../../dsl/Commit" @@ -10,6 +8,7 @@ import { diffToGitJSONDSL } from "../git/diffToGitJSONDSL" import { GitJSONToGitDSLConfig, gitJSONToGitDSL } from "../git/gitJSONToGitDSL" import * as debug from "debug" +import { RepoMetaData } from "../../ci_source/ci_source" const d = debug("danger:BitBucketServerGit") /** @@ -18,11 +17,12 @@ const d = debug("danger:BitBucketServerGit") * @param {BitBucketServerCommit} ghCommit A BitBucketServer based commit * @returns {GitCommit} a Git commit representation without GH metadata */ -function bitBucketServerCommitToGitCommit(bbsCommit: BitBucketServerCommit): GitCommit { - const url = new URL( - `projects/${null}/repos/${null}/commits/${bbsCommit.id}`, // TODO: need repoMetadata here - process.env["DANGER_BITBUCKET_HOST"] - ).toString() +function bitBucketServerCommitToGitCommit( + bbsCommit: BitBucketServerCommit, + repoMetadata: RepoMetaData, + host: string +): GitCommit { + const url = `${host}/${repoMetadata.repoSlug}/commits/${bbsCommit.id}` return { sha: bbsCommit.id, parents: bbsCommit.parents.map(p => p.id), @@ -46,7 +46,9 @@ export default async function gitDSLForBitBucketServer(api: BitBucketServerAPI): // We'll need all this info to be able to generate a working GitDSL object const diff = await api.getPullRequestDiff() const gitCommits = await api.getPullRequestCommits() - const commits = gitCommits.map(bitBucketServerCommitToGitCommit) + const commits = gitCommits.map(commit => + bitBucketServerCommitToGitCommit(commit, api.repoMetadata, api.repoCredentials.host) + ) return diffToGitJSONDSL(diff, commits) } diff --git a/source/platforms/github/pullRequestParser.ts b/source/platforms/github/pullRequestParser.ts deleted file mode 100644 index 41636a0d7..000000000 --- a/source/platforms/github/pullRequestParser.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as url from "url" -import * as includes from "lodash.includes" - -export interface PullRequestParts { - pullRequestNumber: string - repo: string -} - -export function pullRequestParser(address: string): PullRequestParts | null { - const components = url.parse(address, false) - if (components && components.path && includes(components.path, "pull")) { - return { - repo: components.path.split("/pull")[0].slice(1), - pullRequestNumber: components.path.split("/pull/")[1], - } - } - return null -} diff --git a/source/platforms/platform.ts b/source/platforms/platform.ts index 24cb90231..504ffb88d 100644 --- a/source/platforms/platform.ts +++ b/source/platforms/platform.ts @@ -57,15 +57,7 @@ export interface Platform { * @param {CISource} source The existing source, to ensure they can run against each other * @returns {Platform} returns a platform if it can be supported */ -export function getPlatformForEnv(env: Env, source: CISource): Platform { - // GitHub - const ghToken = env["DANGER_GITHUB_API_TOKEN"] - if (ghToken) { - const api = new GitHubAPI(source, ghToken) - const github = new GitHub(api) - return github - } - +export function getPlatformForEnv(env: Env, source: CISource, requireAuth = true): Platform { // BitBucket Server const bbsHost = env["DANGER_BITBUCKETSERVER_HOST"] if (bbsHost) { @@ -78,6 +70,19 @@ export function getPlatformForEnv(env: Env, source: CISource): Platform { return bbs } + // GitHub + const ghToken = env["DANGER_GITHUB_API_TOKEN"] + if (ghToken || !requireAuth) { + if (!ghToken) { + console.log("You don't have a DANGER_GITHUB_API_TOKEN set up, this is optional, but TBH, you want to do this") + console.log("Check out: http://danger.systems/js/guides/the_dangerfile.html#working-on-your-dangerfile") + } + + const api = new GitHubAPI(source, ghToken) + const github = new GitHub(api) + return github + } + console.error("The DANGER_GITHUB_API_TOKEN/DANGER_BITBUCKETSERVER_HOST environmental variable is missing") console.error("Without an api token, danger will be unable to comment on a PR") throw new Error("Cannot use authenticated API requests.") diff --git a/source/platforms/pullRequestParser.ts b/source/platforms/pullRequestParser.ts new file mode 100644 index 000000000..154f07351 --- /dev/null +++ b/source/platforms/pullRequestParser.ts @@ -0,0 +1,31 @@ +import * as url from "url" +import * as includes from "lodash.includes" + +export interface PullRequestParts { + pullRequestNumber: string + repo: string +} + +export function pullRequestParser(address: string): PullRequestParts | null { + const components = url.parse(address, false) + if (components && components.path) { + // shape: http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1/overview + const parts = components.path.match(/(projects\/\w+\/repos\/\w+)\/pull-requests\/(\d+)/) + if (parts) { + return { + repo: parts[1], + pullRequestNumber: parts[2], + } + } + + // shape: http://github.com/proj/repo/pull/1 + if (includes(components.path, "pull")) { + return { + repo: components.path.split("/pull")[0].slice(1), + pullRequestNumber: components.path.split("/pull/")[1], + } + } + } + + return null +} diff --git a/source/runner/dslGenerator.ts b/source/runner/dslGenerator.ts index d8d994e11..07e3e7d41 100644 --- a/source/runner/dslGenerator.ts +++ b/source/runner/dslGenerator.ts @@ -8,7 +8,7 @@ export const jsonDSLGenerator = async (platform: Platform): Promise => { - const api = githubAPIForDSL(dsl) + const api = apiForDSL(dsl) const platformExists = [dsl.github].some(p => !!p) - const github = dsl.github && githubJSONToGitHubDSL(dsl.github, api) + const github = dsl.github && githubJSONToGitHubDSL(dsl.github, api as GitHubNodeAPI) + const bitbucket_server = dsl.bitbucket_server // const gitlab = dsl.gitlab && githubJSONToGitLabDSL(dsl.gitlab, api) let git: GitDSL if (!platformExists) { const localPlatform = new LocalGit(dsl.settings.cliArgs) git = await localPlatform.getPlatformGitRepresentation() + } else if (process.env["DANGER_BITBUCKETSERVER_HOST"]) { + git = bitBucketServerGitDSL(bitbucket_server!, dsl.git, api as BitBucketServerAPI) } else { - git = githubJSONToGitDSL(github, dsl.git) + git = githubJSONToGitDSL(github!, dsl.git) } return { git, - github: github, + github, + bitbucket_server, utils: { sentence, href, @@ -31,7 +37,18 @@ export const jsonToDSL = async (dsl: DangerDSLJSONType): Promise } } -const githubAPIForDSL = (dsl: DangerDSLJSONType) => { +const apiForDSL = (dsl: DangerDSLJSONType): GitHubNodeAPI | BitBucketServerAPI => { + if (process.env["DANGER_BITBUCKETSERVER_HOST"]) { + return new BitBucketServerAPI( + { repoSlug: "", pullRequestID: "" }, + { + host: process.env["DANGER_BITBUCKETSERVER_HOST"]!, + username: process.env["DANGER_BITBUCKETSERVER_USERNAME"], + password: process.env["DANGER_BITBUCKETSERVER_PASSWORD"], + } + ) + } + const api = new GitHubNodeAPI({ host: dsl.settings.github.baseURL, headers: {