├── source ├── runner │ ├── _tests │ │ ├── fixtures │ │ │ ├── __DangerfileEmpty.js │ │ │ ├── __DangerfileBadSyntax.js │ │ │ ├── __DangerfileThrows.js │ │ │ ├── __DangerfileDefaultExport.js │ │ │ ├── __DangerfileNoScheduledAsync.ts │ │ │ ├── export │ │ │ │ └── thing.js │ │ │ ├── __DangerfileImportRelative.js │ │ │ ├── __DangerfileCallback.js │ │ │ ├── results │ │ │ │ ├── __DangerfileEmpty.js.json │ │ │ │ ├── __DangerfileImportRelative.js.json │ │ │ │ ├── __DangerfileCallback.js.json │ │ │ │ ├── __DangerfileDefaultExport.js.json │ │ │ │ ├── __DangerfileNoScheduledAsync.ts.json │ │ │ │ ├── __DangerfileScheduled.js.json │ │ │ │ ├── __DangerfileTypeScript.ts.json │ │ │ │ ├── __DangerfileDefaultExportAsync.js.json │ │ │ │ ├── __DangerfileAsync.js.json │ │ │ │ ├── __DangerfileAsync.ts.json │ │ │ │ ├── __DangerfilePlugin.js.json │ │ │ │ ├── __DangerfileFullMessages.js.json │ │ │ │ ├── __DangerfileMultiScheduled.js.json │ │ │ │ ├── __DangerfileInlineResults.js.json │ │ │ │ ├── __DangerfileThrows.js.json │ │ │ │ └── __DangerfileBadSyntax.js.json │ │ │ ├── __DangerfileFullMessages.js │ │ │ ├── __DangerfileScheduled.js │ │ │ ├── __DangerfileTypeScript.ts │ │ │ ├── __DangerfileDefaultExportAsync.js │ │ │ ├── __DangerfilePlugin.js │ │ │ ├── __DangerfileInlineResults.js │ │ │ ├── __DangerfileAsync.js │ │ │ ├── __DangerfileAsync.ts │ │ │ └── __DangerfileMultiScheduled.js │ │ ├── _danger_utils.test.ts │ │ ├── jsonToDSL.test.ts │ │ └── json-to-context.test.ts │ ├── templates │ │ ├── exceptionRaisedTemplate.ts │ │ ├── _tests │ │ │ ├── __snapshots__ │ │ │ │ ├── _markdownTableTemplate.test.ts.snap │ │ │ │ └── _bitbucketServerTemplate.test.ts.snap │ │ │ ├── _markdownTableTemplate.test.ts │ │ │ └── _bitbucketServerTemplate.test.ts │ │ └── markdownTableTemplate.ts │ ├── runners │ │ ├── utils │ │ │ ├── cleanDangerfile.ts │ │ │ ├── _tests │ │ │ │ └── _transpiler.test.ts │ │ │ └── resultsForCaughtError.ts │ │ ├── _tests │ │ │ └── _cleanDangerfile.test.ts │ │ └── runner.ts │ ├── DangerUtils.ts │ ├── json-to-context.ts │ ├── dslGenerator.ts │ ├── danger-dsl-json.ts │ └── jsonToDSL.ts ├── dsl │ ├── Aliases.ts │ ├── cli-args.ts │ ├── Violation.ts │ ├── Commit.ts │ └── DangerUtilsDSL.ts ├── platforms │ ├── _tests │ │ ├── fixtures │ │ │ ├── static_file.json │ │ │ ├── bitbucket_server_issues.json │ │ │ ├── readme.md │ │ │ ├── requested_reviewers.json │ │ │ ├── static_file.98f3e73f5e419f3af9ab928c86312f28a3c87475.json │ │ │ ├── static_file.cfa8fb80d2b65f4c4fa0b54d25352a3a0ff58f75.json │ │ │ ├── github_user.json │ │ │ └── bitbucket_server_commits.json │ │ ├── _platform.test.ts │ │ └── _pull_request_parser.test.ts │ ├── git │ │ ├── _tests │ │ │ ├── localGetCommits.test.ts │ │ │ └── local_dangerfile_example.ts │ │ ├── localGetFileAtSHA.ts │ │ ├── localGetDiff.ts │ │ ├── diffToGitJSONDSL.ts │ │ └── localGetCommits.ts │ ├── github │ │ ├── comms │ │ │ ├── _tests │ │ │ │ ├── _issueCommenter.test.ts │ │ │ │ └── _checksCommenter.test.ts │ │ │ └── checks │ │ │ │ └── githubAppSupport.ts │ │ ├── _tests │ │ │ ├── _github_utils.test.ts │ │ │ └── __snapshots__ │ │ │ │ └── _github_git.test.ts.snap │ │ └── GitHubGit.ts │ ├── pullRequestParser.ts │ ├── FakePlatform.ts │ └── GitHub.ts ├── debug.ts ├── ci_source │ ├── _tests │ │ ├── fixtures │ │ │ └── dummy_ci.js │ │ └── _get_ci_source.test.ts │ ├── providers │ │ ├── local-repo.ts │ │ ├── Fake.ts │ │ ├── Surf.ts │ │ ├── Nevercode.ts │ │ ├── BuddyBuild.ts │ │ ├── Semaphore.ts │ │ ├── index.ts │ │ ├── Drone.ts │ │ ├── VSTS.ts │ │ ├── TeamCity.ts │ │ ├── Screwdriver.ts │ │ ├── _tests │ │ │ ├── _nevercode.test.ts │ │ │ ├── _drone.test.ts │ │ │ ├── _jenkins.test.ts │ │ │ ├── _bitrise.test.ts │ │ │ ├── _concourse.test.ts │ │ │ ├── _dockerCloud.test.ts │ │ │ ├── _screwdriver.test.ts │ │ │ ├── _buildkite.test.ts │ │ │ ├── _buddyBuild.test.ts │ │ │ ├── _travis.test.ts │ │ │ ├── _semaphore.test.ts │ │ │ ├── _teamcity.test.ts │ │ │ └── _vsts.test.ts │ │ ├── Buildkite.ts │ │ ├── Codeship.ts │ │ ├── DockerCloud.ts │ │ ├── Concourse.ts │ │ ├── Bitrise.ts │ │ ├── Jenkins.ts │ │ └── Circle.ts │ ├── ci_source.ts │ ├── get_ci_source.ts │ └── ci_source_helpers.ts ├── commands │ ├── danger-ci.ts │ ├── danger-reset-status.ts │ ├── init │ │ ├── get-repo-slug.ts │ │ ├── interfaces.ts │ │ └── state-setup.ts │ ├── utils │ │ ├── validateDangerfileExists.ts │ │ ├── reporting.ts │ │ ├── file-utils.ts │ │ ├── _tests │ │ │ ├── file-utils.test.ts │ │ │ └── dangerRunToRunnerCLI.test.ts │ │ ├── sharedDangerfileArgs.ts │ │ ├── getRuntimeCISource.ts │ │ ├── dangerRunToRunnerCLI.ts │ │ └── runDangerSubprocess.ts │ ├── danger-local.ts │ ├── ci │ │ ├── reset-status.ts │ │ └── runner.ts │ └── danger.ts ├── danger.ts ├── https-proxy-agent.d.ts ├── ambient.d.ts └── api │ ├── _tests │ └── fetch.test.ts │ └── fetch.ts ├── types ├── package.json ├── tslint.json ├── tsconfig.json └── test.ts ├── dangerfile.circle.js ├── .babelrc ├── .travis-just-danger.yml ├── .vscode ├── extensions.json ├── tasks.json ├── launch.json ├── spell.json └── settings.json ├── .flowconfig ├── env └── development.env.example ├── dangerfile.flow.js ├── .editorconfig ├── tsconfig.production.json ├── scripts ├── danger_runner.rb ├── create-danger-dts.ts ├── update_flow_types.js └── danger-dts.ts ├── tslint.json ├── docs ├── issue_template.md ├── architecture.md ├── guides │ └── peril.html.md └── tutorials │ ├── transpilation.html.md │ └── fast-feedback.html.md ├── .npmignore ├── appveyor.yml ├── .circleci └── config.yml ├── tsconfig.json ├── wallaby.js ├── .gitignore ├── LICENSE ├── dangerfile.lite.ts ├── dangerfile.ts ├── CONTRIBUTING.md └── VISION.md /source/runner/_tests/fixtures/__DangerfileEmpty.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/dsl/Aliases.ts: -------------------------------------------------------------------------------- 1 | export type MarkdownString = string 2 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileBadSyntax.js: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileThrows.js: -------------------------------------------------------------------------------- 1 | throw new Error("failure") 2 | -------------------------------------------------------------------------------- /types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "danger": "*", 4 | "github": "*" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileDefaultExport.js: -------------------------------------------------------------------------------- 1 | export default () => warn("Synchronous Warning") 2 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileNoScheduledAsync.ts: -------------------------------------------------------------------------------- 1 | setTimeout(() => warn("Totally Async"), 1000) 2 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/export/thing.js: -------------------------------------------------------------------------------- 1 | export default function thing() { 2 | return "thing" 3 | } 4 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileImportRelative.js: -------------------------------------------------------------------------------- 1 | import thing from "./export/thing" 2 | 3 | thing() 4 | -------------------------------------------------------------------------------- /dangerfile.circle.js: -------------------------------------------------------------------------------- 1 | // This is currently empty. Maybe it can do something in the future, but for now it's 👍 to be empty. 2 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", 3 | "rules": { 4 | "semicolon": [false] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileCallback.js: -------------------------------------------------------------------------------- 1 | schedule(done => { 2 | setTimeout(() => { 3 | warn("Scheduled a callback") 4 | done() 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "targets": { "node": "6" }, "useBuiltIns": "usage" }]], 3 | "plugins": ["@babel/plugin-transform-flow-strip-types"] 4 | } 5 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileEmpty.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [], 4 | "messages": [], 5 | "markdowns": [], 6 | "scheduled": [] 7 | } 8 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileFullMessages.js: -------------------------------------------------------------------------------- 1 | warn("this is a warning") 2 | message("this is a message") 3 | fail("this is a failure") 4 | markdown("this is a *markdown*") 5 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileImportRelative.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [], 4 | "messages": [], 5 | "markdowns": [], 6 | "scheduled": [] 7 | } 8 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileScheduled.js: -------------------------------------------------------------------------------- 1 | schedule( 2 | new Promise(res => { 3 | setTimeout(() => { 4 | warn("Asynchronous Warning") 5 | res() 6 | }, 10) 7 | }) 8 | ) 9 | -------------------------------------------------------------------------------- /.travis-just-danger.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | yarn: true 4 | directories: 5 | - node_modules 6 | 7 | node_js: node 8 | script: 9 | - yarn run link 10 | - danger run --verbose 11 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileTypeScript.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* tslint-disable */ 3 | 4 | // This doesn't exist in JS-world 5 | interface MyThing {} 6 | 7 | message("Honey, we got Types") 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Orta.vscode-jest", 4 | "esbenp.prettier-vscode", 5 | "christian-kohler.path-intellisense", 6 | "wayou.vscode-todo-highlight" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/source/.* 3 | .*/node_modules/.* 4 | 5 | [include] 6 | dangerfile.flow.js 7 | 8 | [libs] 9 | distribution/danger.js.flow 10 | 11 | [lints] 12 | 13 | [options] 14 | 15 | [strict] 16 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileDefaultExportAsync.js: -------------------------------------------------------------------------------- 1 | export default async () => 2 | new Promise(res => { 3 | setTimeout(() => { 4 | warn("Asynchronous Warning") 5 | res() 6 | }, 10) 7 | }) 8 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfilePlugin.js: -------------------------------------------------------------------------------- 1 | import { checkForTypesInDeps } from "danger-plugin-yarn" 2 | const deps = { 3 | dependencies: { 4 | added: ["@types/danger"], 5 | }, 6 | } 7 | checkForTypesInDeps(deps) 8 | -------------------------------------------------------------------------------- /env/development.env.example: -------------------------------------------------------------------------------- 1 | DANGER_FAKE_CI="sure" 2 | DANGER_TEST_REPO="artsy/emission" 3 | DANGER_TEST_PR="327" 4 | DANGER_GITHUB_API_TOKEN="123456789123456789123456789" 5 | DANGER_VERBOSE="aye" 6 | DANGER_VERBOSE_SHOW_TOKEN="yep" 7 | -------------------------------------------------------------------------------- /source/runner/templates/exceptionRaisedTemplate.ts: -------------------------------------------------------------------------------- 1 | const quotes = "```" 2 | 3 | export default (error: Error) => ` 4 | ## Danger has errored 5 | 6 | Error: ${error.name} 7 | 8 | ${quotes}sh 9 | ${error.stack} 10 | ${quotes} 11 | 12 | ` 13 | -------------------------------------------------------------------------------- /dangerfile.flow.js: -------------------------------------------------------------------------------- 1 | // This file isn't used anywhere, but it is used as a part of `yarn flow check` 2 | // to validate that flow typings work correctly for JS Dangerfiles 3 | 4 | // @flow 5 | 6 | import { danger } from "danger" 7 | 8 | danger.github.pr 9 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileInlineResults.js: -------------------------------------------------------------------------------- 1 | warn("this is a warning", "Warning.swift", "12") 2 | message("this is a message", "Message.swift", "17") 3 | fail("this is a failure", "Fail.swift", "27") 4 | markdown("this is a *markdown*") 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/static_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": "VGhlIEFsbC1EZWZlY3RvciBpcyBhIHB1cnBvcnRlZCBnbGl0Y2ggaW4gdGhlIERpbGVtbWEgUHJpc29uIHRoYXQgYXBwZWFycyB0byBwcmlzb25lcnMgYXMgdGhlbXNlbHZlcy4gVGhpcyBnb2dvbCBhbHdheXMgZGVmZWN0cywgaGVuY2UgdGhlIG5hbWUu" 3 | } 4 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileCallback.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [ 4 | { 5 | "message": "Scheduled a callback" 6 | } 7 | ], 8 | "messages": [], 9 | "markdowns": [], 10 | "scheduled": [null] 11 | } 12 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileDefaultExport.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [ 4 | { 5 | "message": "Synchronous Warning" 6 | } 7 | ], 8 | "messages": [], 9 | "markdowns": [], 10 | "scheduled": [] 11 | } 12 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileNoScheduledAsync.ts.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [ 4 | { 5 | "message": "Totally Async" 6 | } 7 | ], 8 | "messages": [], 9 | "markdowns": [], 10 | "scheduled": [] 11 | } 12 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileScheduled.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [ 4 | { 5 | "message": "Asynchronous Warning" 6 | } 7 | ], 8 | "messages": [], 9 | "markdowns": [], 10 | "scheduled": [{}] 11 | } 12 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileTypeScript.ts.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [], 4 | "messages": [ 5 | { 6 | "message": "Honey, we got Types" 7 | } 8 | ], 9 | "markdowns": [], 10 | "scheduled": [] 11 | } 12 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/bitbucket_server_issues.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "JRA-11", 4 | "url": "https://jira.atlassian.com/browse/JRA-11" 5 | }, 6 | { 7 | "key": "JRA-9", 8 | "url": "https://jira.atlassian.com/browse/JRA-9" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileDefaultExportAsync.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [ 4 | { 5 | "message": "Asynchronous Warning" 6 | } 7 | ], 8 | "messages": [], 9 | "markdowns": [], 10 | "scheduled": [] 11 | } 12 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileAsync.js: -------------------------------------------------------------------------------- 1 | const asyncAction = () => 2 | new Promise(res => { 3 | setTimeout(() => { 4 | warn("Async Function") 5 | res() 6 | }, 50) 7 | }) 8 | 9 | schedule(async () => { 10 | await asyncAction() 11 | warn("After Async Function") 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "dangerfile.ts", 5 | "dangerfile.lite.ts", 6 | "scripts", 7 | "node_modules", 8 | "source/**/fixtures/*", 9 | "source/**/_tests/*", 10 | "distribution", 11 | "types" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /scripts/danger_runner.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | str = STDIN.tty? ? "Cannot read from STDIN" : $stdin.read 4 | exit(1) unless str 5 | 6 | # Have a dumb fake response 7 | require "json" 8 | results = { fails: [], warnings: [], messages: [], markdowns: [] }.to_json 9 | 10 | STDOUT.write(results) 11 | -------------------------------------------------------------------------------- /source/dsl/cli-args.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes the possible arguments that 3 | * could be used when calling the CLI 4 | */ 5 | export interface CliArgs { 6 | base: string 7 | verbose: string 8 | externalCiProvider: string 9 | textOnly: string 10 | dangerfile: string 11 | id: string 12 | } 13 | -------------------------------------------------------------------------------- /source/platforms/_tests/_platform.test.ts: -------------------------------------------------------------------------------- 1 | import { getPlatformForEnv } from "../platform" 2 | 3 | it("should bail if there is no DANGER_GITHUB_API_TOKEN found", () => { 4 | const e = expect as any 5 | e(() => { 6 | getPlatformForEnv({} as any, {} as any) 7 | }).toThrow("Cannot use authenticated API requests") 8 | }) 9 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileAsync.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [ 4 | { 5 | "message": "Async Function" 6 | }, 7 | { 8 | "message": "After Async Function" 9 | } 10 | ], 11 | "messages": [], 12 | "markdowns": [], 13 | "scheduled": [null] 14 | } 15 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileAsync.ts.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [ 4 | { 5 | "message": "Async Function" 6 | }, 7 | { 8 | "message": "After Async Function" 9 | } 10 | ], 11 | "messages": [], 12 | "markdowns": [], 13 | "scheduled": [null] 14 | } 15 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "baseUrl": ".", 9 | "paths": { "danger": ["."] } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileAsync.ts: -------------------------------------------------------------------------------- 1 | /* tslint-disable */ 2 | 3 | const asyncAction = () => 4 | new Promise(res => { 5 | setTimeout(() => { 6 | warn("Async Function") 7 | res() 8 | }, 50) 9 | }) 10 | 11 | schedule(async () => { 12 | await asyncAction() 13 | warn("After Async Function") 14 | }) 15 | -------------------------------------------------------------------------------- /source/runner/templates/_tests/__snapshots__/_markdownTableTemplate.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generating markdown tables build a markdown table with the content given 1`] = ` 4 | "| | Warnings | 5 | | --- | --- | 6 | | ⚠️ | This is a very unimportant warning. | 7 | | ⚠️ | But, this warning is pretty important. |" 8 | `; 9 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "curly": true, 4 | "class-name": true, 5 | "jsdoc-format": true, 6 | "no-duplicate-variable": true, 7 | "no-var-keyword": true, 8 | "no-empty": true, 9 | "no-unused-expression": true, 10 | "no-var-requires": true, 11 | "no-require-imports": true 12 | }, 13 | "array-type": [true, "array-simple"] 14 | } 15 | -------------------------------------------------------------------------------- /source/debug.ts: -------------------------------------------------------------------------------- 1 | import * as debugModule from "debug" 2 | 3 | export const debug = (value: string) => { 4 | const d = debugModule(`danger:${value}`) 5 | // In Peril, when running inside Hyper, we don't get access to stderr 6 | // so bind debug to use stdout 7 | if (process.env.x_hyper_content_sha256) { 8 | d.log = console.log.bind(console) 9 | } 10 | return d 11 | } 12 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/readme.md: -------------------------------------------------------------------------------- 1 | Here's an example CURL request to add a new fixture 2 | 3 | ```sh 4 | curl \ 5 | -H "Authorization: token " \ 6 | -H "Accept: application/vnd.github.v3+json" \ 7 | --request GET https://api.github.com/repos/artsy/emission/pulls/327/requested_reviewers \ 8 | > source/platforms/_tests/fixtures/requested_reviewers.json 9 | ``` 10 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfilePlugin.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [ 3 | { 4 | "message": 5 | "@types dependencies were added to package.json, as a dependency for others.
You need to move @types/danger into \"devDependencies\"?" 6 | } 7 | ], 8 | "warnings": [], 9 | "messages": [], 10 | "markdowns": [], 11 | "scheduled": [] 12 | } 13 | -------------------------------------------------------------------------------- /source/ci_source/_tests/fixtures/dummy_ci.js: -------------------------------------------------------------------------------- 1 | class DummyCI { 2 | get name() { 3 | return "Dummy Testing CI" 4 | } 5 | 6 | get isCI() { 7 | return false 8 | } 9 | get isPR() { 10 | return true 11 | } 12 | 13 | get pullRequestID() { 14 | return this.env.pr 15 | } 16 | get repoSlug() { 17 | return this.env.repo 18 | } 19 | } 20 | 21 | module.exports = DummyCI 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "npm", 6 | "isShellCommand": true, 7 | "showOutput": "never", 8 | "suppressTaskName": true, 9 | "tasks": [ 10 | { 11 | "taskName": "build", 12 | "args": ["run", "build"] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /source/commands/danger-ci.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import * as program from "commander" 4 | 5 | import setSharedArgs, { SharedCLI } from "./utils/sharedDangerfileArgs" 6 | import { runRunner } from "./ci/runner" 7 | 8 | program.usage("[options]").description("Runs a Dangerfile in JavaScript or TypeScript.") 9 | setSharedArgs(program).parse(process.argv) 10 | 11 | const app = (program as any) as SharedCLI 12 | runRunner(app) 13 | -------------------------------------------------------------------------------- /source/commands/danger-reset-status.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import * as program from "commander" 4 | 5 | import setSharedArgs, { SharedCLI } from "./utils/sharedDangerfileArgs" 6 | import { runRunner } from "./ci/runner" 7 | 8 | program.usage("[options]").description("Reset the status of a GitHub PR to pending.") 9 | setSharedArgs(program).parse(process.argv) 10 | 11 | const app = (program as any) as SharedCLI 12 | runRunner(app) 13 | -------------------------------------------------------------------------------- /types/test.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 2.2 2 | 3 | import { danger, markdown } from "danger" 4 | 5 | // $ExpectType DangerDSLType 6 | danger 7 | 8 | // $ExpectType GitDSL 9 | danger.git 10 | 11 | // $ExpectType string[] 12 | danger.git.created_files 13 | 14 | // $ExpectType Github 15 | danger.github.api 16 | 17 | // $ExpectType GitHubPRDSL 18 | danger.github.pr 19 | 20 | // $ExpectType (array: string[]) => string 21 | danger.utils.sentence 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Jest", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": ["./node_modules/.bin/jest", "-i"], 9 | "cwd": "${workspaceRoot}", 10 | "protocol": "inspector", 11 | "console": "internalConsole", 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/distribution"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /docs/issue_template.md: -------------------------------------------------------------------------------- 1 | Hello there, thanks for the issue. 2 | 3 | If this is a support request, e.g: 4 | 5 | * How do I ...? 6 | * Does Danger support ...? 7 | * Is there a plugin for ...? 8 | 9 | Please use https://spectrum.chat/danger 10 | 11 | If this issue feels like: 12 | 13 | * Danger crashed ... 14 | * Danger should be able to ... 15 | * I didn't expect that Danger would ... 16 | * Should I create a PR for ... 17 | 18 | Then you're in the right place :+1: 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | source 2 | **/_tests 3 | flow-typed 4 | .vscode 5 | docs 6 | *.yml 7 | *.lock 8 | .flowconfig 9 | .editorconfig 10 | .babelrc 11 | scripts 12 | dangerfile.ts 13 | env 14 | jsconfig.json 15 | coverage 16 | .jest 17 | types 18 | tsconfig 19 | test-results.json 20 | test.json 21 | yarn-error.log 22 | dangerfile.circle.js 23 | dangerfile.flow.js 24 | dangerfile.lite.ts 25 | tests.json 26 | tsconfig.json 27 | tsconfig.production.json 28 | tslint.json 29 | wallaby.js 30 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileFullMessages.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [ 3 | { 4 | "message": "this is a failure" 5 | } 6 | ], 7 | "warnings": [ 8 | { 9 | "message": "this is a warning" 10 | } 11 | ], 12 | "messages": [ 13 | { 14 | "message": "this is a message" 15 | } 16 | ], 17 | "markdowns": [ 18 | { 19 | "message": "this is a *markdown*" 20 | } 21 | ], 22 | "scheduled": [] 23 | } 24 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileMultiScheduled.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [ 3 | { 4 | "message": "Asynchronous Failure" 5 | } 6 | ], 7 | "warnings": [ 8 | { 9 | "message": "Asynchronous Warning" 10 | } 11 | ], 12 | "messages": [ 13 | { 14 | "message": "Asynchronous Message" 15 | } 16 | ], 17 | "markdowns": [ 18 | { 19 | "message": "Asynchronous Markdown" 20 | } 21 | ], 22 | "scheduled": [{}, {}, {}, {}] 23 | } 24 | -------------------------------------------------------------------------------- /source/runner/templates/markdownTableTemplate.ts: -------------------------------------------------------------------------------- 1 | const buildHeader = (headers: string[]): string => 2 | `| ${headers.join(" | ")} |\n` + `| ${headers.map(_ => "---").join(" | ")} |` 3 | 4 | const buildRow = (row: string[]): string => `| ${row.join(" | ")} |` 5 | 6 | const buildRows = (rows: string[][]): string => rows.map(buildRow).join("\n") 7 | 8 | export function template(headers: string[], rows: string[][]): string { 9 | return `${buildHeader(headers)}\n` + `${buildRows(rows)}` 10 | } 11 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against this version of Node.js 2 | environment: 3 | nodejs_version: "7" 4 | 5 | # Install scripts. (runs after repo cloning) 6 | install: 7 | # Get the latest stable version of Node 8 | - ps: Install-Product node $env:nodejs_version 9 | # install modules 10 | - yarn install 11 | 12 | build: off 13 | 14 | # Post-install test scripts. 15 | test_script: 16 | # Output useful info for debugging. 17 | - node --version 18 | - yarn --version 19 | 20 | # run tests 21 | - yarn test 22 | -------------------------------------------------------------------------------- /source/platforms/git/_tests/localGetCommits.test.ts: -------------------------------------------------------------------------------- 1 | import { formatJSON } from "../localGetCommits" 2 | 3 | it("generates a JSON-like commit message", () => { 4 | expect(formatJSON).toEqual( 5 | '{ "sha": "%H", "parents": "%p", "author": {"name": "%an", "email": "%ae", "date": "%ai" }, "committer": {"name": "%cn", "email": "%ce", "date": "%ci" }, "message": "%f"},' 6 | ) 7 | 8 | const withoutComma = formatJSON.substring(0, formatJSON.length - 1) 9 | expect(() => JSON.parse(withoutComma)).not.toThrow() 10 | }) 11 | -------------------------------------------------------------------------------- /source/commands/init/get-repo-slug.ts: -------------------------------------------------------------------------------- 1 | import * as parseGitConfig from "parse-git-config" 2 | import * as parseGithubURL from "parse-github-url" 3 | 4 | export const getRepoSlug = () => { 5 | const config = parseGitConfig.sync() 6 | const possibleRemotes = [config['remote "upstream"'], config['remote "origin"']].filter(f => f) 7 | if (possibleRemotes.length === 0) { 8 | return null 9 | } 10 | 11 | const ghData = possibleRemotes.map(r => parseGithubURL(r.url)) 12 | return ghData.length ? ghData[0].repo : undefined 13 | } 14 | -------------------------------------------------------------------------------- /source/runner/templates/_tests/_markdownTableTemplate.test.ts: -------------------------------------------------------------------------------- 1 | import { template as markdownTableTemplate } from "../../templates/markdownTableTemplate" 2 | 3 | describe("generating markdown tables", () => { 4 | it("build a markdown table with the content given", () => { 5 | const headers = ["", "Warnings"] 6 | const rows = [["⚠️", "This is a very unimportant warning."], ["⚠️", "But, this warning is pretty important."]] 7 | 8 | const table = markdownTableTemplate(headers, rows) 9 | 10 | expect(table).toMatchSnapshot() 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /source/runner/runners/utils/cleanDangerfile.ts: -------------------------------------------------------------------------------- 1 | // https://regex101.com/r/dUq4yB/1 2 | const requirePattern = /^.* require\(('|")danger('|")\);?$/gm 3 | // https://regex101.com/r/dUq4yB/2 4 | const es6Pattern = /^.* from ('|")danger('|");?$/gm 5 | 6 | /** 7 | * Updates a Dangerfile to remove the import for Danger 8 | * @param {string} contents the file path for the dangerfile 9 | * @returns {string} the revised Dangerfile 10 | */ 11 | export default (contents: string): string => 12 | contents.replace(es6Pattern, "// Removed import").replace(requirePattern, "// Removed require") 13 | -------------------------------------------------------------------------------- /scripts/create-danger-dts.ts: -------------------------------------------------------------------------------- 1 | import dts from "./danger-dts" 2 | import * as fs from "fs" 3 | 4 | // This could need to exist 5 | if (!fs.existsSync("distribution")) { 6 | fs.mkdirSync("distribution") 7 | } 8 | 9 | const output = dts() 10 | 11 | // This is so you can get it for this repo 👍 12 | fs.writeFileSync("source/danger.d.ts", output) 13 | fs.writeFileSync("distribution/danger.d.ts", output) 14 | fs.writeFileSync("types/index.d.ts", "// TypeScript Version: 2.2\n" + output) 15 | 16 | console.log("Awesome - shipped to source/danger.d.ts, distribution/danger.d.ts and types/index.d.ts") 17 | -------------------------------------------------------------------------------- /source/platforms/git/localGetFileAtSHA.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "../../debug" 2 | import { exec } from "child_process" 3 | 4 | const d = debug("localGetFileAtSHA") 5 | 6 | export const localGetFileAtSHA = (path: string, _repo: string | undefined, sha: string) => 7 | new Promise(done => { 8 | const call = `git show ${sha}:"${path}"` 9 | d(call) 10 | 11 | exec(call, (err, stdout, _stderr) => { 12 | if (err) { 13 | console.error(`Could not get the file ${path} from git at ${sha}`) 14 | console.error(err) 15 | return 16 | } 17 | 18 | done(stdout) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /source/runner/DangerUtils.ts: -------------------------------------------------------------------------------- 1 | // The documentation for these are provided inline 2 | // inside DangerUtilsDSL.ts 3 | 4 | export function sentence(array: string[]): string { 5 | if ((array || []).length === 0) { 6 | return "" 7 | } 8 | if (array.length === 1) { 9 | return array[0] 10 | } 11 | return array.slice(0, array.length - 1).join(", ") + " and " + array.pop() 12 | } 13 | 14 | export function href(href?: string, text?: string): string | null { 15 | if (!href && !text) { 16 | return null 17 | } 18 | if (!href && text) { 19 | return text 20 | } 21 | return `${text || href}` 22 | } 23 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileMultiScheduled.js: -------------------------------------------------------------------------------- 1 | debugger 2 | 3 | schedule( 4 | new Promise(res => { 5 | setTimeout(() => { 6 | fail("Asynchronous Failure") 7 | res() 8 | }, 100) 9 | }) 10 | ) 11 | 12 | schedule( 13 | new Promise(res => { 14 | warn("Asynchronous Warning") 15 | res() 16 | }) 17 | ) 18 | 19 | schedule( 20 | new Promise(res => { 21 | setTimeout(() => { 22 | message("Asynchronous Message") 23 | res() 24 | }, 10) 25 | }) 26 | ) 27 | 28 | schedule( 29 | new Promise(res => { 30 | setTimeout(() => { 31 | markdown("Asynchronous Markdown") 32 | res() 33 | }, 50) 34 | }) 35 | ) 36 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileInlineResults.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [ 3 | { 4 | "message": "this is a failure", 5 | "file": "Fail.swift", 6 | "line": "27" 7 | } 8 | ], 9 | "warnings": [ 10 | { 11 | "message": "this is a warning", 12 | "file": "Warning.swift", 13 | "line": "12" 14 | } 15 | ], 16 | "messages": [ 17 | { 18 | "message": "this is a message", 19 | "file": "Message.swift", 20 | "line": "17" 21 | } 22 | ], 23 | "markdowns": [ 24 | { 25 | "message": "this is a *markdown*" 26 | } 27 | ], 28 | "scheduled": [] 29 | } 30 | -------------------------------------------------------------------------------- /source/runner/templates/_tests/__snapshots__/_bitbucketServerTemplate.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generating messages for BitBucket server summary result matches snapshot 1`] = ` 4 | " 5 | 6 | | ❌ | Fails 7 | | --- | --- | 8 | 9 | > Failing message Failing message 10 | 11 | 12 | | ⚠️ | Warnings 13 | | --- | --- | 14 | 15 | > Warning message Warning message 16 | 17 | 18 | | ✨ | Messages 19 | | --- | --- | 20 | 21 | > message 22 | 23 | 24 | markdown 25 | 26 | 27 | | | 28 | |---:| 29 | | _Generated by ❌ [dangerJS](http://github.com/danger/danger-js/)_ | 30 | 31 | 32 | [](http://danger-id-blankID;) 33 | " 34 | `; 35 | -------------------------------------------------------------------------------- /source/dsl/Violation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The result of user doing warn, message or fail, built this way for 3 | * expansion later. 4 | */ 5 | export interface Violation { 6 | /** 7 | * The string representation 8 | * 9 | * @type {string} 10 | */ 11 | message: string 12 | 13 | /** 14 | * Optional path to the file 15 | * @type {string} 16 | */ 17 | file?: string 18 | 19 | /** 20 | * Optional line in the file 21 | * @type {string} 22 | */ 23 | line?: number 24 | } 25 | 26 | /// End of Danger DSL definition 27 | 28 | export const isInline = (violation: Violation): boolean => violation.file !== undefined && violation.line !== undefined 29 | -------------------------------------------------------------------------------- /source/commands/utils/validateDangerfileExists.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | 3 | const validateDangerfileExists = (filePath: string): boolean => { 4 | let stat: fs.Stats | null = null 5 | try { 6 | stat = fs.statSync(filePath) 7 | } catch (error) { 8 | console.error(`Could not find a dangerfile at ${filePath}, not running against your PR.`) 9 | process.exitCode = 1 10 | } 11 | 12 | if (!!stat && !stat.isFile()) { 13 | console.error(`The resource at ${filePath} appears to not be a file, not running against your PR.`) 14 | process.exitCode = 1 15 | } 16 | 17 | return !!stat && stat.isFile() 18 | } 19 | 20 | export default validateDangerfileExists 21 | -------------------------------------------------------------------------------- /source/ci_source/providers/local-repo.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | 3 | export class LocalRepo implements CISource { 4 | private readonly env: Env 5 | 6 | constructor(env: Env) { 7 | const defaults = { 8 | repo: process.cwd(), 9 | pr: undefined, 10 | } 11 | 12 | this.env = { ...env, ...defaults } 13 | } 14 | get name(): string { 15 | return "local repo" 16 | } 17 | 18 | get isCI(): boolean { 19 | return true 20 | } 21 | get isPR(): boolean { 22 | return true 23 | } 24 | 25 | get pullRequestID(): string { 26 | return "" 27 | } 28 | get repoSlug(): string { 29 | return this.env.repo 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /source/danger.ts: -------------------------------------------------------------------------------- 1 | // This file represents the module that is exposed as the danger API 2 | 3 | throw ` 4 | Hey there, it looks like you're trying to import the danger module. Turns out 5 | that the code you write in a Dangerfile.js is actually a bit of a sneaky hack. 6 | 7 | When running Danger, the import or require for Danger is removed before the code 8 | is evaluated. Instead all of the imports are added to the global runtime, so if 9 | you are importing Danger to use one of it's functions - you should instead just 10 | use the global object for the root DSL elements. 11 | 12 | There is a spectrum thread for discussion here: 13 | - https://spectrum.chat/?t=0a005b56-31ec-4919-9a28-ced623949d4d 14 | 15 | ` 16 | -------------------------------------------------------------------------------- /source/runner/runners/utils/_tests/_transpiler.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock("fs", () => ({ 2 | readFileSync: jest.fn(), 3 | })) 4 | 5 | import { typescriptify } from "../transpiler" 6 | import * as fs from "fs" 7 | 8 | describe("typescriptify", () => { 9 | it("removes the module option in a tsconfig ", () => { 10 | const dangerfile = `import {a} from 'lodash'; a()` 11 | const fakeTSConfig = { 12 | compilerOptions: { 13 | target: "es5", 14 | module: "es2015", 15 | }, 16 | } 17 | const fsMock = fs.readFileSync as any 18 | fsMock.mockImplementationOnce(() => JSON.stringify(fakeTSConfig)) 19 | 20 | expect(typescriptify(dangerfile)).not.toContain("import") 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /source/platforms/github/comms/_tests/_issueCommenter.test.ts: -------------------------------------------------------------------------------- 1 | import { findPositionForInlineComment } from "../issueCommenter" 2 | import { fixturedGitHubDSL } from "../../_tests/fixturedGitHubDSL" 3 | 4 | it("finds the position of file/line for inline comment with one chunk", async () => { 5 | const dsl = await fixturedGitHubDSL() 6 | const position = await findPositionForInlineComment(dsl.git, 9, "tsconfig.json") 7 | expect(position).toBe(6) 8 | }) 9 | 10 | it("finds the position of file/line for inline comment with two chunks", async () => { 11 | const dsl = await fixturedGitHubDSL() 12 | const position = await findPositionForInlineComment(dsl.git, 28, "lib/containers/gene.js") 13 | expect(position).toBe(19) 14 | }) 15 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/danger-js 5 | docker: 6 | - image: node:7 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | key: yarn-{{ .Branch }}-{{ checksum "package.json" }} 11 | - run: 12 | name: System information 13 | command: | 14 | echo "Node $(node -v)" 15 | echo "Yarn v$(yarn --version)" 16 | - run: yarn 17 | - save_cache: 18 | key: yarn-{{ .Branch }}-{{ checksum "package.json" }} 19 | paths: 20 | - ~/danger-js/node_modules 21 | - ~/.cache/yarn/ 22 | - run: yarn add danger 23 | - run: DEBUG="*" yarn danger --dangerfile dangerfile.circle.js --text-only 24 | -------------------------------------------------------------------------------- /source/https-proxy-agent.d.ts: -------------------------------------------------------------------------------- 1 | // From: https://github.com/TooTallNate/node-https-proxy-agent/issues/27 2 | declare module "https-proxy-agent" { 3 | import * as https from "https" 4 | 5 | namespace HttpsProxyAgent { 6 | interface HttpsProxyAgentOptions { 7 | host: string 8 | port: number 9 | secureProxy?: boolean 10 | headers?: { 11 | [key: string]: string 12 | } 13 | [key: string]: any 14 | } 15 | } 16 | 17 | // HttpsProxyAgent doesnt *actually* extend https.Agent, but for my purposes I want it to pretend that it does 18 | class HttpsProxyAgent extends https.Agent { 19 | constructor(opts: string) 20 | constructor(opts: HttpsProxyAgent.HttpsProxyAgentOptions) 21 | } 22 | 23 | export = HttpsProxyAgent 24 | } 25 | -------------------------------------------------------------------------------- /source/runner/json-to-context.ts: -------------------------------------------------------------------------------- 1 | import { CliArgs } from "../dsl/cli-args" 2 | import { jsonToDSL } from "./jsonToDSL" 3 | import { contextForDanger, DangerContext } from "./Dangerfile" 4 | import { DangerDSLJSON } from "./danger-dsl-json" 5 | 6 | /** 7 | * Reads in the JSON string converts to a dsl object and gets the change context 8 | * to be used for Danger. 9 | * @param JSONString {string} from stdin 10 | * @param program {any} commander 11 | * @returns {Promise} context for danger 12 | */ 13 | export async function jsonToContext(JSONString: string, program: any): Promise { 14 | const dslJSON = { danger: new DangerDSLJSON(JSONString, program as CliArgs) } 15 | const dsl = await jsonToDSL(dslJSON.danger) 16 | return contextForDanger(dsl) 17 | } 18 | -------------------------------------------------------------------------------- /source/dsl/Commit.ts: -------------------------------------------------------------------------------- 1 | /** A platform agnostic reference to a Git commit */ 2 | export interface GitCommit { 3 | /** The SHA for the commit */ 4 | sha: string 5 | /** Who wrote the commit */ 6 | author: GitCommitAuthor 7 | /** Who deployed the commit */ 8 | committer: GitCommitAuthor 9 | /** The commit message */ 10 | message: string 11 | /** Potential parent commits, and other assorted metadata */ 12 | tree: any 13 | /** SHAs for the commit's parents */ 14 | parents?: string[] 15 | /** Link to the commit */ 16 | url: string 17 | } 18 | 19 | /** An author of a commit */ 20 | export interface GitCommitAuthor { 21 | /** The display name for the author */ 22 | name: string 23 | /** The authors email */ 24 | email: string 25 | /** ISO6801 date string */ 26 | date: string 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/spell.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "en", 3 | "ignoreWordsList": [ 4 | "GitHub", 5 | "GitLab" 6 | ], 7 | "mistakeTypeToStatus": { 8 | "Passive voice": "Hint", 9 | "Spelling": "Error", 10 | "Complex Expression": "Disable", 11 | "Hidden Verbs": "Information", 12 | "Hyphen Required": "Disable", 13 | "Redundant Expression": "Disable", 14 | "Did you mean...": "Disable", 15 | "Repeated Word": "Warning", 16 | "Missing apostrophe": "Warning", 17 | "Cliches": "Disable", 18 | "Missing Word": "Disable", 19 | "Make I uppercase": "Warning" 20 | }, 21 | "languageIDs": [ 22 | "markdown", 23 | "plaintext" 24 | ], 25 | "ignoreRegExp": [ 26 | "/\\(.*\\.(jpg|jpeg|png|md|gif|JPG|JPEG|PNG|MD|GIF)\\)/g", 27 | "/((http|https|ftp|git)\\S*)/g" 28 | ] 29 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": false, 4 | "preserveConstEnums": true, 5 | "sourceMap": true, 6 | "declaration": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "suppressImplicitAnyIndexErrors": true, 10 | "strictNullChecks": true, 11 | "noUnusedLocals": true, 12 | "noImplicitThis": true, 13 | "noUnusedParameters": true, 14 | "module": "commonjs", 15 | "moduleResolution": "node", 16 | "pretty": true, 17 | "target": "es5", 18 | "outDir": "distribution", 19 | "lib": ["dom", "es2017"], 20 | "strict": true 21 | }, 22 | "formatCodeOptions": { 23 | "indentSize": 2, 24 | "tabSize": 2 25 | }, 26 | "exclude": ["scripts", "node_modules", "source/**/fixtures/*", "source/**/_tests/*", "distribution", "types"] 27 | } 28 | -------------------------------------------------------------------------------- /source/platforms/git/localGetDiff.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "../../debug" 2 | import { spawn } from "child_process" 3 | 4 | const d = debug("localGetDiff") 5 | 6 | export const localGetDiff = (base: string, head: string) => 7 | new Promise(done => { 8 | const args = ["diff", `${base}...${head}`] 9 | let stdout = "" 10 | 11 | const child = spawn("git", args, { env: process.env }) 12 | d("> git", args.join(" ")) 13 | 14 | child.stdout.on("data", chunk => { 15 | stdout += chunk 16 | }) 17 | 18 | child.stderr.on("data", data => { 19 | console.error(`Could not get diff from git between ${base} and ${head}`) 20 | throw new Error(data.toString()) 21 | }) 22 | 23 | child.on("close", function(code) { 24 | if (code === 0) { 25 | done(stdout) 26 | } 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /source/runner/dslGenerator.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "../platforms/platform" 2 | import { DangerDSLJSONType } from "../dsl/DangerDSL" 3 | import { CliArgs } from "../dsl/cli-args" 4 | 5 | export const jsonDSLGenerator = async (platform: Platform): Promise => { 6 | const git = await platform.getPlatformGitRepresentation() 7 | const platformDSL = await platform.getPlatformDSLRepresentation() 8 | 9 | return { 10 | git, 11 | [platform.name === "BitBucketServer" ? "bitbucket_server" : "github"]: platformDSL, 12 | settings: { 13 | github: { 14 | accessToken: process.env["DANGER_GITHUB_API_TOKEN"] || "NO_TOKEN", 15 | additionalHeaders: {}, 16 | baseURL: process.env["DANGER_GITHUB_API_BASE_URL"] || undefined, 17 | }, 18 | cliArgs: {} as CliArgs, 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /source/commands/utils/reporting.ts: -------------------------------------------------------------------------------- 1 | import { DangerResults } from "../../dsl/DangerResults" 2 | 3 | export const markdownCode = (string: string): string => ` 4 | \`\`\`sh 5 | ${string} 6 | \`\`\` 7 | ` 8 | export const resultsWithFailure = (failure: string, moreMarkdown?: string): DangerResults => { 9 | const fail = { message: failure } 10 | return { 11 | warnings: [], 12 | messages: [], 13 | fails: [fail], 14 | markdowns: moreMarkdown ? [{ message: moreMarkdown }] : [], 15 | } 16 | } 17 | 18 | export const mergeResults = (left: DangerResults, right: DangerResults): DangerResults => { 19 | return { 20 | warnings: [...left.warnings, ...right.warnings], 21 | messages: [...left.messages, ...right.messages], 22 | fails: [...left.fails, ...right.fails], 23 | markdowns: [...left.markdowns, ...right.markdowns], 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /source/commands/utils/file-utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs" 2 | 3 | /** 4 | * Returns a the typical Dangerfile, depending on it's location 5 | * taking into account whether it JS or TS by whether those exists. 6 | * 7 | * Will throw if it isn't found. 8 | */ 9 | export function dangerfilePath(program: any): string { 10 | if (program.dangerfile) { 11 | return program.dangerfile 12 | } 13 | 14 | if (existsSync("dangerfile.ts")) { 15 | return "dangerfile.ts" 16 | } 17 | 18 | if (existsSync("dangerfile.js")) { 19 | return "dangerfile.js" 20 | } 21 | 22 | if (existsSync("Dangerfile.ts")) { 23 | return "Dangerfile.ts" 24 | } 25 | 26 | if (existsSync("Dangerfile.js")) { 27 | return "Dangerfile.js" 28 | } 29 | 30 | throw new Error("Could not find a `dangerfile.js` or `dangerfile.ts` in the current working directory.") 31 | } 32 | -------------------------------------------------------------------------------- /source/ci_source/ci_source.ts: -------------------------------------------------------------------------------- 1 | /** A json object that represents the outer ENV */ 2 | export type Env = any 3 | 4 | /** The shape of an object that represents an individual CI */ 5 | export interface CISource { 6 | /** The project name, mainly for showing errors */ 7 | readonly name: string 8 | 9 | /** Does this validate as being on a particular CI? */ 10 | readonly isCI: boolean 11 | 12 | /** Does this validate as being on a particular PR on a CI? */ 13 | readonly isPR: boolean 14 | 15 | /** What is the reference slug for this environment? */ 16 | readonly repoSlug: string 17 | /** What unique id can be found for the code review platform's PR */ 18 | readonly pullRequestID: string 19 | 20 | /** allows the source to do some setup */ 21 | setup?(): Promise 22 | 23 | /** Optional URL for the CI run, for a status update link */ 24 | readonly ciRunURL?: string 25 | } 26 | -------------------------------------------------------------------------------- /source/ci_source/providers/Fake.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist } from "../ci_source_helpers" 3 | 4 | export class FakeCI implements CISource { 5 | private readonly env: Env 6 | 7 | constructor(env: Env) { 8 | const defaults = { 9 | repo: env.DANGER_TEST_REPO || "artsy/emission", // TODO: default to empty string ? 10 | pr: env.DANGER_TEST_PR || "327", // TODO: default to empty string ? 11 | } 12 | 13 | this.env = { ...env, ...defaults } 14 | } 15 | get name(): string { 16 | return "Fake Testing CI" 17 | } 18 | 19 | get isCI(): boolean { 20 | return ensureEnvKeysExist(this.env, ["DANGER_FAKE_CI"]) 21 | } 22 | get isPR(): boolean { 23 | return true 24 | } 25 | 26 | get pullRequestID(): string { 27 | return this.env.pr 28 | } 29 | 30 | get repoSlug(): string { 31 | return this.env.repo 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/commands/init/interfaces.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | 3 | export interface InitState { 4 | filename: string 5 | botName: string 6 | 7 | isWindows: boolean 8 | isMac: boolean 9 | isBabel: boolean 10 | isTypeScript: boolean 11 | supportsHLinks: boolean 12 | 13 | isAnOSSRepo: boolean 14 | 15 | hasCreatedDangerfile: boolean 16 | hasSetUpAccount: boolean 17 | hasSetUpAccountToken: boolean 18 | 19 | repoSlug: string | null 20 | ciType: "travis" | "circle" | "unknown" 21 | isGitHub: boolean 22 | } 23 | 24 | export interface InitUI { 25 | header: (msg: String) => void 26 | command: (command: string) => void 27 | say: (msg: String) => void 28 | pause: (secs: number) => Promise<{}> 29 | waitForReturn: () => void 30 | link: (name: string, href: string) => string 31 | askWithAnswers: (message: string, answers: string[]) => string 32 | } 33 | 34 | export const highlight = chalk.bold.yellow as any 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Place your settings in this file to overwrite default and user settings. 3 | "javascript.validate.enable": false, 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.svn": true, 7 | "**/.hg": true, 8 | "**/.DS_Store": true, 9 | "distribution/": true, 10 | "coverage": true 11 | }, 12 | "search.exclude": { 13 | "**/node_modules": true, 14 | "distribution/": true 15 | }, 16 | "files.associations": { 17 | "**/*.js": "javascriptreact" 18 | }, 19 | "spellchecker.language": "en_US", 20 | "spellchecker.ignoreWordsList": ["GitHub", "repo", "Awesommmmee", "linters", "TODO", "repl"], 21 | "spellchecker.documentTypes": ["markdown", "latex", "plaintext"], 22 | "spellchecker.ignoreRegExp": [], 23 | "spellchecker.ignoreFileExtensions": [], 24 | "spellchecker.checkInterval": 5000, 25 | "editor.formatOnSave": true, 26 | "cSpell.words": ["APIPR", "Commenter", "PRDSL", "bitbucket"] 27 | } 28 | -------------------------------------------------------------------------------- /source/commands/utils/_tests/file-utils.test.ts: -------------------------------------------------------------------------------- 1 | let mockDangerfilePath = "" 2 | jest.mock("fs", () => ({ existsSync: p => p === mockDangerfilePath })) 3 | 4 | import { dangerfilePath } from "../file-utils" 5 | 6 | describe("dangerfilePath", () => { 7 | it("should return anything passed into the program's dangerfile", () => { 8 | expect(dangerfilePath({ dangerfile: "123" })).toEqual("123") 9 | }) 10 | 11 | it("should find a dangerfile.js if there is no program, and the .js file exists", () => { 12 | mockDangerfilePath = "dangerfile.js" 13 | expect(dangerfilePath({})).toEqual("dangerfile.js") 14 | }) 15 | 16 | it("should find a dangerfile.ts if there is no program, and the .js file does not exist", () => { 17 | mockDangerfilePath = "dangerfile.ts" 18 | expect(dangerfilePath({})).toEqual("dangerfile.ts") 19 | }) 20 | 21 | it("should raise if nothing exists", () => { 22 | mockDangerfilePath = "dangerfile.tsjs" 23 | expect(() => dangerfilePath({})).toThrow() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /source/platforms/git/diffToGitJSONDSL.ts: -------------------------------------------------------------------------------- 1 | import * as parseDiff from "parse-diff" 2 | import * as includes from "lodash.includes" 3 | import { GitCommit } from "../../dsl/Commit" 4 | import { GitJSONDSL } from "../../dsl/GitDSL" 5 | 6 | /** 7 | * This function is essentially a "go from a diff to some simple structured data" 8 | * it's the steps needed for danger process. 9 | */ 10 | 11 | export const diffToGitJSONDSL = (diff: string, commits: GitCommit[]): GitJSONDSL => { 12 | const fileDiffs: any[] = parseDiff(diff) 13 | 14 | const addedDiffs = fileDiffs.filter((diff: any) => diff["new"]) 15 | const removedDiffs = fileDiffs.filter((diff: any) => diff["deleted"]) 16 | const modifiedDiffs = fileDiffs.filter((diff: any) => !includes(addedDiffs, diff) && !includes(removedDiffs, diff)) 17 | 18 | return { 19 | modified_files: modifiedDiffs.map(d => d.to), 20 | created_files: addedDiffs.map(d => d.to), 21 | deleted_files: removedDiffs.map(d => d.from), 22 | commits: commits, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/runner/runners/utils/resultsForCaughtError.ts: -------------------------------------------------------------------------------- 1 | import * as pinpoint from "pinpoint" 2 | import { DangerResults } from "../../../dsl/DangerResults" 3 | 4 | /** Returns Markdown results to post if an exception is raised during the danger run */ 5 | const resultsForCaughtError = (file: string, contents: string, error: Error): DangerResults => { 6 | const match = /(\d+:\d+)/g.exec(error.stack!) 7 | let code 8 | if (match) { 9 | const [line, column] = match[0].split(":").map(value => parseInt(value, 10) - 1) 10 | code = pinpoint(contents, { line, column }) 11 | } else { 12 | code = contents 13 | } 14 | const failure = `Danger failed to run \`${file}\`.` 15 | const errorMD = `## Error ${error.name} 16 | \`\`\` 17 | ${error.message} 18 | ${error.stack} 19 | \`\`\` 20 | ### Dangerfile 21 | \`\`\` 22 | ${code} 23 | \`\`\` 24 | ` 25 | return { fails: [{ message: failure }], warnings: [], markdowns: [{ message: errorMD }], messages: [] } 26 | } 27 | 28 | export default resultsForCaughtError 29 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = wallaby => { 2 | const babel = JSON.parse(require("fs").readFileSync(require("path").join(__dirname, ".babelrc"))) 3 | babel.presets.push("babel-preset-jest") 4 | 5 | return { 6 | files: [ 7 | "tsconfig.json", 8 | { pattern: "source/**/fixtures/**/*.*", instrument: false }, 9 | "source/**/!(*.test).ts", 10 | { pattern: "package.json", instrument: false }, 11 | { pattern: "source/runner/runners/_tests/vm2.test.ts", instrument: true, load: false, ignore: false }, 12 | { pattern: "source/api/_tests/fetch.test.ts", instrument: true, load: false, ignore: false }, 13 | ], 14 | 15 | tests: ["source/**/*.test.ts"], 16 | 17 | env: { 18 | type: "node", 19 | }, 20 | 21 | // fixtures are not instrumented, but still need to be compiled 22 | preprocessors: { 23 | "source/**/fixtures/**/*.js?(x)": file => require("babel-core").transform(file.content, babel), 24 | }, 25 | 26 | testFramework: "jest", 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /source/commands/utils/sharedDangerfileArgs.ts: -------------------------------------------------------------------------------- 1 | import * as program from "commander" 2 | import chalk from "chalk" 3 | 4 | process.on("unhandledRejection", function(reason: string, _p: any) { 5 | console.log(chalk.red("Error: "), reason) 6 | process.exitCode = 1 7 | }) 8 | 9 | export interface SharedCLI extends program.CommanderStatic { 10 | verbose: boolean 11 | externalCiProvider: string 12 | textOnly: boolean 13 | dangerfile: string 14 | id: string 15 | repl: string 16 | } 17 | 18 | export default (command: any) => 19 | command 20 | .option("-v, --verbose", "Verbose output of files") 21 | .option("-c, --external-ci-provider [modulePath]", "Specify custom CI provider") 22 | .option("-t, --text-only", "Provide an STDOUT only interface, Danger will not post to your PR") 23 | .option("-d, --dangerfile [filePath]", "Specify a custom dangerfile path") 24 | .option("-i, --id [danger_id]", "Specify a unique Danger ID for the Danger run") 25 | .option("-b, --base [branch_name]", "Base branch") 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | distribution 40 | flow-typed 41 | env/development.env 42 | 43 | docs/doc_generate/ 44 | docs/docs_generate/ 45 | docs/js_ref_dsl_docs.json 46 | 47 | types/index.d.ts 48 | .jest/ 49 | test-results.json 50 | 51 | # Flowgen stuff 52 | source/_danger.d.tse 53 | source/_danger.d.ts 54 | -------------------------------------------------------------------------------- /source/platforms/pullRequestParser.ts: -------------------------------------------------------------------------------- 1 | import * as url from "url" 2 | import * as includes from "lodash.includes" 3 | 4 | export interface PullRequestParts { 5 | pullRequestNumber: string 6 | repo: string 7 | } 8 | 9 | export function pullRequestParser(address: string): PullRequestParts | null { 10 | const components = url.parse(address, false) 11 | 12 | if (components && components.path) { 13 | // shape: http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1/overview 14 | const parts = components.path.match(/(projects\/\w+\/repos\/[\w-]+)\/pull-requests\/(\d+)/) 15 | if (parts) { 16 | return { 17 | repo: parts[1], 18 | pullRequestNumber: parts[2], 19 | } 20 | } 21 | 22 | // shape: http://github.com/proj/repo/pull/1 23 | if (includes(components.path, "pull")) { 24 | return { 25 | repo: components.path.split("/pull")[0].slice(1), 26 | pullRequestNumber: components.path.split("/pull/")[1], 27 | } 28 | } 29 | } 30 | 31 | return null 32 | } 33 | -------------------------------------------------------------------------------- /source/dsl/DangerUtilsDSL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The Danger Utils DSL contains utility functions 3 | * that are specific to universal Danger use-cases. 4 | */ 5 | export interface DangerUtilsDSL { 6 | /** 7 | * Creates a link using HTML. 8 | * 9 | * If `href` and `text` are falsy, null is returned. 10 | * If `href` is falsy and `text` is truthy, `text` is returned. 11 | * If `href` is truthy and `text` is falsy, an tag is returned with `href` as its href and text value. 12 | * Otherwise, if `href` and `text` are truthy, an tag is returned with the `href` and `text` inserted as expected. 13 | * 14 | * @param {string} href The HTML link's destination. 15 | * @param {string} text The HTML link's text. 16 | * @returns {string|null} The HTML tag. 17 | */ 18 | href(href: string, text: string): string | null 19 | 20 | /** 21 | * Converts an array of strings into a sentence. 22 | * 23 | * @param {string[]} array The array of strings. 24 | * @returns {string} The sentence. 25 | */ 26 | sentence(array: string[]): string 27 | } 28 | -------------------------------------------------------------------------------- /source/ci_source/_tests/_get_ci_source.test.ts: -------------------------------------------------------------------------------- 1 | import { FakeCI } from "../providers/Fake" 2 | import * as DummyCI from "./fixtures/dummy_ci" 3 | import { getCISourceForEnv, getCISourceForExternal } from "../get_ci_source" 4 | 5 | describe(".getCISourceForEnv", () => { 6 | test("returns undefined if nothing is found", () => { 7 | const ci = getCISourceForEnv({}) 8 | expect(ci).toBeUndefined() 9 | }) 10 | 11 | test("falls back to the fake if DANGER_FAKE_CI exists", () => { 12 | const ci = getCISourceForEnv({ DANGER_FAKE_CI: "YES" }) 13 | expect(ci).toBeInstanceOf(FakeCI) 14 | }) 15 | }) 16 | 17 | describe(".getCISourceForExternal", async () => { 18 | test("should resolve module relatively", async () => { 19 | const ci = await getCISourceForExternal({}, "./source/ci_source/_tests/fixtures/dummy_ci.js") 20 | expect(ci).toBeInstanceOf(DummyCI) 21 | }) 22 | 23 | test("should return undefined if module resolution fails", async () => { 24 | const ci = await getCISourceForExternal({}, "./dummy_ci.js") 25 | expect(ci).toBeUndefined() 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /scripts/update_flow_types.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | 3 | // kill the two generated danger dts files 4 | if (fs.existsSync("./source/_danger.d.ts")) { 5 | fs.unlinkSync("./source/_danger.d.ts") 6 | } 7 | 8 | if (fs.existsSync("./source/_danger.d.tse")) { 9 | fs.unlinkSync("./source/_danger.d.tse") 10 | } 11 | 12 | const exportedLines = [ 13 | "function schedule", 14 | "function fail", 15 | "function warn", 16 | "function message", 17 | "function markdown", 18 | "var danger", 19 | "var results", 20 | ] 21 | 22 | var flowDef = fs.readFileSync("distribution/danger.js.flow", "utf8") 23 | exportedLines.forEach(line => { 24 | // from declare function schedule 25 | // to declare export function schedule 26 | const find = "declare " + line 27 | const newLine = "declare export " + line 28 | flowDef = flowDef.replace(find, newLine) 29 | }) 30 | 31 | const prefix = ` 32 | // This is generated in danger/danger-js/scripts/update_flow_types.js 33 | 34 | import type { GitHub } from "@octokit/rest" 35 | ` 36 | 37 | fs.writeFileSync("distribution/danger.js.flow", prefix + flowDef) 38 | -------------------------------------------------------------------------------- /source/commands/utils/getRuntimeCISource.ts: -------------------------------------------------------------------------------- 1 | import { getCISource } from "../../ci_source/get_ci_source" 2 | import { providers } from "../../ci_source/providers" 3 | import { sentence } from "../../runner/DangerUtils" 4 | import { SharedCLI } from "./sharedDangerfileArgs" 5 | import { CISource } from "../../ci_source/ci_source" 6 | 7 | const getRuntimeCISource = async (app: SharedCLI): Promise => { 8 | const source = await getCISource(process.env, app.externalCiProvider || undefined) 9 | 10 | if (!source) { 11 | console.log("Could not find a CI source for this run. Does Danger support this CI service?") 12 | console.log(`Danger supports: ${sentence(providers.map(p => p.name))}.`) 13 | 14 | if (!process.env["CI"]) { 15 | console.log("You may want to consider using `danger pr` to run Danger locally.") 16 | } 17 | 18 | process.exitCode = 1 19 | } 20 | 21 | // run the sources setup function, if it exists 22 | if (source && source.setup) { 23 | await source.setup() 24 | } 25 | 26 | return source 27 | } 28 | 29 | export default getRuntimeCISource 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Orta Therox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /source/ci_source/providers/Surf.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * You want to add `yarn danger ci` to your `build.sh` file to run Danger at the 8 | * end of your build. 9 | * 10 | * ### Token Setup 11 | * 12 | * As this is self-hosted, you will need to add the `DANGER_GITHUB_API_TOKEN` to your build user's ENV. The alternative 13 | * is to pass in the token as a prefix to the command `DANGER_GITHUB_API_TOKEN="123" yarn danger ci`. 14 | */ 15 | export class Surf implements CISource { 16 | constructor(private readonly env: Env) {} 17 | 18 | get name(): string { 19 | return "surf-build" 20 | } 21 | 22 | get isCI(): boolean { 23 | return ensureEnvKeysExist(this.env, ["SURF_REPO", "SURF_NWO"]) 24 | } 25 | 26 | get isPR(): boolean { 27 | return this.isCI 28 | } 29 | 30 | get pullRequestID(): string { 31 | const key = "SURF_PR_NUM" 32 | return ensureEnvKeysAreInt(this.env, [key]) ? this.env[key] : "" 33 | } 34 | 35 | get repoSlug(): string { 36 | return this.env["SURF_NWO"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/runner/runners/_tests/_cleanDangerfile.test.ts: -------------------------------------------------------------------------------- 1 | import cleanDangerfile from "../utils/cleanDangerfile" 2 | 3 | describe("cleaning Dangerfiles", () => { 4 | it("also handles typescript style imports", () => { 5 | const before = ` 6 | import { danger, warn, fail, message } from 'danger' 7 | import { danger, warn, fail, message } from "danger" 8 | import { danger, warn, fail, message } from "danger"; 9 | import danger from "danger" 10 | import danger from 'danger' 11 | import danger from 'danger'; 12 | ` 13 | const after = ` 14 | // Removed import 15 | // Removed import 16 | // Removed import 17 | // Removed import 18 | // Removed import 19 | // Removed import 20 | ` 21 | expect(cleanDangerfile(before)).toEqual(after) 22 | }) 23 | 24 | it("also handles require style imports", () => { 25 | const before = ` 26 | const { danger, warn, fail, message } = require('danger') 27 | var { danger, warn, fail, message } = require("danger") 28 | let { danger, warn, fail, message } = require('danger'); 29 | ` 30 | const after = ` 31 | // Removed require 32 | // Removed require 33 | // Removed require 34 | ` 35 | expect(cleanDangerfile(before)).toEqual(after) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/requested_reviewers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "login": "ArtsyOpenSource", 4 | "id": 12397828, 5 | "avatar_url": "https://avatars1.githubusercontent.com/u/12397828?v=3", 6 | "gravatar_id": "", 7 | "url": "https://api.github.com/users/ArtsyOpenSource", 8 | "html_url": "https://github.com/ArtsyOpenSource", 9 | "followers_url": "https://api.github.com/users/ArtsyOpenSource/followers", 10 | "following_url": "https://api.github.com/users/ArtsyOpenSource/following{/other_user}", 11 | "gists_url": "https://api.github.com/users/ArtsyOpenSource/gists{/gist_id}", 12 | "starred_url": "https://api.github.com/users/ArtsyOpenSource/starred{/owner}{/repo}", 13 | "subscriptions_url": "https://api.github.com/users/ArtsyOpenSource/subscriptions", 14 | "organizations_url": "https://api.github.com/users/ArtsyOpenSource/orgs", 15 | "repos_url": "https://api.github.com/users/ArtsyOpenSource/repos", 16 | "events_url": "https://api.github.com/users/ArtsyOpenSource/events{/privacy}", 17 | "received_events_url": "https://api.github.com/users/ArtsyOpenSource/received_events", 18 | "type": "User", 19 | "site_admin": false 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /source/runner/_tests/_danger_utils.test.ts: -------------------------------------------------------------------------------- 1 | import { href, sentence } from "../DangerUtils" 2 | 3 | describe("sentence()", () => { 4 | it("handles falsy input", () => { 5 | expect(sentence(null)).toEqual("") 6 | }) 7 | it("handles empty array", () => { 8 | expect(sentence([])).toEqual("") 9 | }) 10 | it("handles array with one item", () => { 11 | expect(sentence(["Hello"])).toEqual("Hello") 12 | }) 13 | it("handles array with multiple items", () => { 14 | expect(sentence(["This", "that", "the other thing"])).toEqual("This, that and the other thing") 15 | }) 16 | }) 17 | 18 | describe("href()", () => { 19 | it("returns null when href and text are falsy", () => { 20 | expect(href("", "")).toEqual(null) 21 | }) 22 | it("returns just the text when the href is missing", () => { 23 | expect(href("", "Some text")).toEqual("Some text") 24 | }) 25 | it("returns tag with href as text when text is missing", () => { 26 | expect(href("/path/to/file", "")).toEqual(`/path/to/file`) 27 | }) 28 | it("returns tag for supplied href and text", () => { 29 | expect(href("http://danger.systems", "Danger")).toEqual(`Danger`) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /source/platforms/git/_tests/local_dangerfile_example.ts: -------------------------------------------------------------------------------- 1 | // This dangerfile is for running as an integration test on CI 2 | 3 | import { DangerDSLType } from "../../../dsl/DangerDSL" 4 | declare var danger: DangerDSLType 5 | declare function markdown(params: string): void 6 | 7 | const showArray = (array: any[], mapFunc?: (any) => any) => { 8 | const defaultMap = (a: any) => a 9 | const mapper = mapFunc || defaultMap 10 | return `\n - ${array.map(mapper).join("\n - ")}\n` 11 | } 12 | 13 | const git = danger.git 14 | 15 | const goAsync = async () => { 16 | const firstFileDiff = await git.diffForFile(git.modified_files[0]) 17 | const firstJSONFile = git.modified_files.find(f => f.endsWith("json")) 18 | const jsonDiff = firstJSONFile && (await git.JSONDiffForFile(firstJSONFile)) 19 | const jsonDiffKeys = jsonDiff && showArray(Object.keys(jsonDiff)) 20 | 21 | markdown(` 22 | created: ${showArray(git.created_files)} 23 | modified: ${showArray(git.modified_files)} 24 | deleted: ${showArray(git.deleted_files)} 25 | commits: ${git.commits.length} 26 | messages: ${showArray(git.commits, c => c.message)} 27 | diffForFile keys:${showArray(Object.keys(firstFileDiff))} 28 | jsonDiff keys:${jsonDiffKeys || "no JSON files in the diff"} 29 | `) 30 | } 31 | goAsync() 32 | -------------------------------------------------------------------------------- /source/commands/danger-local.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import * as program from "commander" 4 | 5 | import setSharedArgs, { SharedCLI } from "./utils/sharedDangerfileArgs" 6 | import { runRunner } from "./ci/runner" 7 | import { LocalGit } from "../platforms/LocalGit" 8 | import { FakeCI } from "../ci_source/providers/Fake" 9 | 10 | interface App extends SharedCLI { 11 | /** What should we compare against? */ 12 | base?: string 13 | /** Should we run against current staged changes? */ 14 | staging?: boolean 15 | } 16 | 17 | program 18 | .usage("[options]") 19 | // TODO: this option 20 | // .option("-s, --staging", "Just use staged changes.") 21 | .description("Runs danger without PR metadata, useful for git hooks.") 22 | setSharedArgs(program).parse(process.argv) 23 | 24 | const app = (program as any) as App 25 | const base = app.base || "master" 26 | const localPlatform = new LocalGit({ base, staged: app.staging }) 27 | localPlatform.validateThereAreChanges().then(changes => { 28 | if (changes) { 29 | const fakeSource = new FakeCI(process.env) 30 | runRunner(app, { source: fakeSource, platform: localPlatform, additionalArgs: ["--local"] }) 31 | } else { 32 | console.log("No git changes detected between head and master.") 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /source/commands/utils/dangerRunToRunnerCLI.ts: -------------------------------------------------------------------------------- 1 | const usesProcessSeparationCommands = ["ci", "pr", "local"] 2 | 3 | const dangerRunToRunnerCLI = (argv: string[]) => { 4 | let newCommand = [] 5 | newCommand.push(argv[0]) 6 | 7 | // e.g. node --inspect distribution/commands/danger-run-ci.js --dangerfile myDangerfile.ts 8 | // or node distribution/commands/danger-pr.js --dangerfile myDangerfile.ts 9 | 10 | if (argv.length === 1) { 11 | return ["danger", "runner"] 12 | } else if (argv[0].includes("node")) { 13 | // convert 14 | let newJSFile = argv[1] 15 | usesProcessSeparationCommands.forEach(name => { 16 | newJSFile = newJSFile.replace("danger-" + name, "danger-runner") 17 | }) 18 | 19 | newCommand.push(newJSFile) 20 | for (let index = 2; index < argv.length; index++) { 21 | newCommand.push(argv[index]) 22 | } 23 | } else { 24 | // e.g. danger ci --dangerfile 25 | // if you do `danger run` start looking at args later 26 | newCommand.push("runner") 27 | let index = usesProcessSeparationCommands.includes(argv[1]) ? 2 : 1 28 | for (; index < argv.length; index++) { 29 | newCommand.push(argv[index]) 30 | } 31 | } 32 | 33 | return newCommand 34 | } 35 | 36 | export default dangerRunToRunnerCLI 37 | -------------------------------------------------------------------------------- /source/ci_source/providers/Nevercode.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * Nevercode.io CI Integration 6 | * 7 | * Environment Variables Documented: https://developer.nevercode.io/v1.0/docs/environment-variables-files 8 | */ 9 | export class Nevercode implements CISource { 10 | constructor(private readonly env: Env) {} 11 | 12 | get name(): string { 13 | return "Nevercode" 14 | } 15 | 16 | get isCI(): boolean { 17 | return ensureEnvKeysExist(this.env, ["NEVERCODE"]) 18 | } 19 | 20 | get isPR(): boolean { 21 | const mustHave = ["NEVERCODE_PULL_REQUEST", "NEVERCODE_REPO_SLUG"] 22 | const mustBeInts = ["NEVERCODE_GIT_PROVIDER_PULL_REQUEST", "NEVERCODE_PULL_REQUEST_NUMBER"] 23 | return ( 24 | ensureEnvKeysExist(this.env, mustHave) && 25 | ensureEnvKeysAreInt(this.env, mustBeInts) && 26 | this.env.NEVERCODE_PULL_REQUEST == "true" 27 | ) 28 | } 29 | 30 | get pullRequestID(): string { 31 | return this.env.NEVERCODE_PULL_REQUEST_NUMBER 32 | } 33 | 34 | get repoSlug(): string { 35 | return this.env.NEVERCODE_REPO_SLUG 36 | } 37 | 38 | get ciRunURL() { 39 | return process.env.NEVERCODE_BUILD_URL 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /source/runner/danger-dsl-json.ts: -------------------------------------------------------------------------------- 1 | import { DangerDSLJSONType } from "../dsl/DangerDSL" 2 | import { GitJSONDSL } from "../dsl/GitDSL" 3 | import { GitHubDSL } from "../dsl/GitHubDSL" 4 | import { CliArgs } from "../dsl/cli-args" 5 | 6 | /** 7 | * Using the input JSON create an DangerDSL 8 | * 9 | * @see DangerDSLJSONType for more detailed definition 10 | */ 11 | export class DangerDSLJSON implements DangerDSLJSONType { 12 | // Prettier + `git!` do not work yet 13 | // and this class uses runtime hackery 14 | 15 | // @ts-ignore 16 | git: GitJSONDSL 17 | // @ts-ignore 18 | github: GitHubDSL 19 | // @ts-ignore 20 | settings: { 21 | github: { 22 | accessToken: string 23 | baseURL: string | undefined 24 | additionalHeaders: any 25 | } 26 | cliArgs: CliArgs 27 | } 28 | /** 29 | * Parse the JSON and assign danger to this object 30 | * 31 | * Also add the arguments sent to the CLI 32 | * 33 | * @param JSONString DSL in JSON format 34 | * @param cliArgs arguments used running danger command 35 | */ 36 | constructor(JSONString: string, cliArgs: CliArgs) { 37 | const parsedString = JSON.parse(JSONString) 38 | Object.assign(this, parsedString.danger) 39 | 40 | // @ts-ignore 41 | this.settings.cliArgs = cliArgs 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/ci_source/providers/BuddyBuild.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * Buddybuild has an integration for Danger JS already built-in. 8 | * 9 | * ### Token Setup 10 | * 11 | * Login to buddybuild and select your app. Go to your *App Settings* and 12 | * in the *Build Settings* menu on the left, choose *Environment Variables*. 13 | * 14 | * #### GitHub 15 | * Add the `DANGER_GITHUB_API_TOKEN` to your build user's ENV. 16 | * 17 | */ 18 | export class BuddyBuild implements CISource { 19 | constructor(private readonly env: Env) {} 20 | 21 | get name(): string { 22 | return "buddybuild" 23 | } 24 | 25 | get isCI(): boolean { 26 | return ensureEnvKeysExist(this.env, ["BUDDYBUILD_BUILD_ID"]) 27 | } 28 | 29 | get isPR(): boolean { 30 | const mustHave = ["BUDDYBUILD_PULL_REQUEST", "BUDDYBUILD_REPO_SLUG"] 31 | const mustBeInts = ["BUDDYBUILD_PULL_REQUEST"] 32 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 33 | } 34 | 35 | get pullRequestID(): string { 36 | return this.env.BUDDYBUILD_PULL_REQUEST 37 | } 38 | 39 | get repoSlug(): string { 40 | return this.env.BUDDYBUILD_REPO_SLUG 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/ci_source/providers/Semaphore.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * For Semaphore you will want to go to the settings page of the project. Inside "Build Settings" 8 | * you should add `yarn danger ci` to the Setup thread. Note that Semaphore only provides 9 | * the build environment variables necessary for Danger on PRs across forks. 10 | * 11 | * ### Token Setup 12 | * 13 | * You can add your `DANGER_GITHUB_API_TOKEN` inside the "Environment Variables" section in the settings. 14 | * 15 | */ 16 | export class Semaphore implements CISource { 17 | constructor(private readonly env: Env) {} 18 | 19 | get name(): string { 20 | return "Semaphore" 21 | } 22 | 23 | get isCI(): boolean { 24 | return ensureEnvKeysExist(this.env, ["SEMAPHORE"]) 25 | } 26 | 27 | get isPR(): boolean { 28 | const mustHave = ["SEMAPHORE_REPO_SLUG"] 29 | const mustBeInts = ["PULL_REQUEST_NUMBER"] 30 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 31 | } 32 | 33 | get pullRequestID(): string { 34 | return this.env.PULL_REQUEST_NUMBER 35 | } 36 | 37 | get repoSlug(): string { 38 | return this.env.SEMAPHORE_REPO_SLUG 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /source/ci_source/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { Bitrise } from "./Bitrise" 2 | import { BuddyBuild } from "./BuddyBuild" 3 | import { Buildkite } from "./Buildkite" 4 | import { Circle } from "./Circle" 5 | import { Codeship } from "./Codeship" 6 | import { Concourse } from "./Concourse" 7 | import { DockerCloud } from "./DockerCloud" 8 | import { Drone } from "./Drone" 9 | import { FakeCI } from "./Fake" 10 | import { Jenkins } from "./Jenkins" 11 | import { Nevercode } from "./Nevercode" 12 | import { Semaphore } from "./Semaphore" 13 | import { Surf } from "./Surf" 14 | import { TeamCity } from "./TeamCity" 15 | import { Travis } from "./Travis" 16 | import { VSTS } from "./VSTS" 17 | import { Screwdriver } from "./Screwdriver" 18 | 19 | const providers = [ 20 | Travis, 21 | Circle, 22 | Semaphore, 23 | Nevercode, 24 | Jenkins, 25 | FakeCI, 26 | Surf, 27 | DockerCloud, 28 | Codeship, 29 | Drone, 30 | Buildkite, 31 | BuddyBuild, 32 | VSTS, 33 | Bitrise, 34 | TeamCity, 35 | Screwdriver, 36 | Concourse, 37 | ] 38 | 39 | // Mainly used for Dangerfile linting 40 | const realProviders = [ 41 | Travis, 42 | Circle, 43 | Semaphore, 44 | Nevercode, 45 | Jenkins, 46 | Surf, 47 | DockerCloud, 48 | Codeship, 49 | Drone, 50 | Buildkite, 51 | BuddyBuild, 52 | VSTS, 53 | TeamCity, 54 | Screwdriver, 55 | Concourse, 56 | ] 57 | 58 | export { providers, realProviders } 59 | -------------------------------------------------------------------------------- /source/commands/ci/reset-status.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { debug } from "../../debug" 3 | 4 | import { getPlatformForEnv } from "../../platforms/platform" 5 | import { SharedCLI } from "../utils/sharedDangerfileArgs" 6 | import getRuntimeCISource from "../utils/getRuntimeCISource" 7 | 8 | import { RunnerConfig } from "./runner" 9 | 10 | const d = debug("reset-status") 11 | 12 | export const runRunner = async (app: SharedCLI, config?: RunnerConfig) => { 13 | d(`Starting sub-process run with ${app.args}`) 14 | const source = (config && config.source) || (await getRuntimeCISource(app)) 15 | 16 | // This does not set a failing exit code 17 | if (source && !source.isPR) { 18 | console.log("Skipping Danger due to this run not executing on a PR.") 19 | } 20 | 21 | // The optimal path 22 | if (source && source.isPR) { 23 | const platform = (config && config.platform) || getPlatformForEnv(process.env, source) 24 | if (!platform) { 25 | console.log(chalk.red(`Could not find a source code hosting platform for ${source.name}.`)) 26 | console.log( 27 | `Currently Danger JS only supports GitHub, if you want other platforms, consider the Ruby version or help out.` 28 | ) 29 | process.exitCode = 1 30 | } 31 | 32 | if (platform) { 33 | await platform.updateStatus("pending", "Danger is waiting for your CI run to complete...") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileThrows.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "failures": [ 3 | { 4 | "message": 5 | "Danger failed to run `/Users/orta/dev/projects/danger/danger-js/source/runner/_tests/fixtures/__DangerfileThrows.js`." 6 | } 7 | ], 8 | "warnings": [], 9 | "messages": [], 10 | "markdowns": [ 11 | "## Error Error\n```\nfailure\nError: failure\n at Object. (/Users/orta/dev/projects/danger/danger-js/source/runner/_tests/fixtures/__DangerfileThrows.js:3:7)\n at Module._compile (module.js:624:30)\n at requireFromString (/Users/orta/dev/projects/danger/danger-js/node_modules/require-from-string/index.js:28:4)\n at Object. (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:95:21)\n at step (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:32:23)\n at Object.next (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:13:53)\n at /Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:7:71\n at new Promise ()\n at __awaiter (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:3:12)\n at Object.runDangerfileEnvironment (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:66:12)\n```\n### Dangerfile\n```\n1| throw new Error(\"failure\")\n2| \n--------^\n```\n " 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /source/runner/runners/runner.ts: -------------------------------------------------------------------------------- 1 | import { DangerResults } from "../../dsl/DangerResults" 2 | import { DangerContext } from "../Dangerfile" 3 | 4 | export interface DangerRunner { 5 | /** 6 | * Executes a Dangerfile at a specific path, with a context. 7 | * The values inside a Danger context are applied as globals to the Dangerfiles runtime. 8 | * 9 | * @param {string[]} filenames a set of file paths for the dangerfile 10 | * @param {string[] | undefined[]} originalContents optional, the JS pre-compiled 11 | * @param {DangerContext} environment the results of createDangerfileRuntimeEnvironment 12 | * @param {any | undefined} injectedObjectToExport an optional object for passing into default exports 13 | * @returns {DangerResults} the results of the run 14 | */ 15 | runDangerfileEnvironment: ( 16 | filenames: string[], 17 | originalContents: string[] | undefined[], 18 | environment: any, 19 | injectedObjectToExport?: any 20 | ) => Promise 21 | 22 | /** 23 | * Sets up the runtime environment for running Danger, this could be loading VMs 24 | * or creating new processes etc. The return value is expected to go into the environment 25 | * section of runDangerfileEnvironment. 26 | * 27 | * @param {DangerContext} dangerfileContext the global danger context, basically the DSL 28 | */ 29 | createDangerfileRuntimeEnvironment: (dangerfileContext: DangerContext) => Promise 30 | } 31 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileBadSyntax.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "failures": [ 3 | { 4 | "message": 5 | "Danger failed to run `/Users/orta/dev/projects/danger/danger-js/source/runner/_tests/fixtures/__DangerfileBadSyntax.js`." 6 | } 7 | ], 8 | "warnings": [], 9 | "messages": [], 10 | "markdowns": [ 11 | "## Error ReferenceError\n```\nhello is not defined\nReferenceError: hello is not defined\n at Object. (/Users/orta/dev/projects/danger/danger-js/source/runner/_tests/fixtures/__DangerfileBadSyntax.js:4:1)\n at Module._compile (module.js:624:30)\n at requireFromString (/Users/orta/dev/projects/danger/danger-js/node_modules/require-from-string/index.js:28:4)\n at Object. (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:95:21)\n at step (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:32:23)\n at Object.next (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:13:53)\n at /Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:7:71\n at new Promise ()\n at __awaiter (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:3:12)\n at Object.runDangerfileEnvironment (/Users/orta/dev/projects/danger/danger-js/distribution/runner/runners/inline.js:66:12)\n```\n### Dangerfile\n```\n1| hello\n--^\n2| \n```\n " 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/static_file.98f3e73f5e419f3af9ab928c86312f28a3c87475.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "name": "tsconfig.json", 4 | "path": "tsconfig.json", 5 | "sha": "90aabbabf81ac4fbc81ef1e8f40de99972b7e2ac", 6 | "size": 153, 7 | "url": 8 | "https://api.github.com/repos/artsy/emission/contents/tsconfig.json?ref=98f3e73f5e419f3af9ab928c86312f28a3c87475", 9 | "html_url": "https://github.com/artsy/emission/blob/98f3e73f5e419f3af9ab928c86312f28a3c87475/tsconfig.json", 10 | "git_url": "https://api.github.com/repos/artsy/emission/git/blobs/90aabbabf81ac4fbc81ef1e8f40de99972b7e2ac", 11 | "download_url": 12 | "https://raw.githubusercontent.com/artsy/emission/98f3e73f5e419f3af9ab928c86312f28a3c87475/tsconfig.json", 13 | "type": "file", 14 | "content": 15 | "ewogICAgImNvbXBpbGVyT3B0aW9ucyI6IHsKICAgICAgICAiYWxsb3dKcyI6\nIHRydWUKICAgIH0sCiAgICAiZXhjbHVkZSI6IFsKICAgICAgICAibm9kZV9t\nb2R1bGVzIiwKICAgICAgICAiUG9kL0Fzc2V0cyIsCiAgICAgICAgIkV4YW1w\nbGUvQnVpbGQiCiAgICBdCn0K\n", 16 | "encoding": "base64", 17 | "_links": { 18 | "self": 19 | "https://api.github.com/repos/artsy/emission/contents/tsconfig.json?ref=98f3e73f5e419f3af9ab928c86312f28a3c87475", 20 | "git": "https://api.github.com/repos/artsy/emission/git/blobs/90aabbabf81ac4fbc81ef1e8f40de99972b7e2ac", 21 | "html": "https://github.com/artsy/emission/blob/98f3e73f5e419f3af9ab928c86312f28a3c87475/tsconfig.json" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/ci_source/providers/Drone.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * 6 | * ### CI Setup 7 | * 8 | * With Drone, you run the docker images yourself, so you will want to add `yarn danger ci` at the end of 9 | * your `.drone.yml`. 10 | * 11 | * ``` shell 12 | * build: 13 | * image: golang 14 | * commands: 15 | * - ... 16 | * - yarn danger ci 17 | * ``` 18 | * 19 | * ### Token Setup 20 | * 21 | * As this is self-hosted, you will need to add the `DANGER_GITHUB_API_TOKEN` to your build user's ENV. The alternative 22 | * is to pass in the token as a prefix to the command `DANGER_GITHUB_API_TOKEN="123" yarn danger ci`. 23 | */ 24 | 25 | export class Drone implements CISource { 26 | constructor(private readonly env: Env) {} 27 | 28 | get name(): string { 29 | return "Drone" 30 | } 31 | 32 | get isCI(): boolean { 33 | return ensureEnvKeysExist(this.env, ["DRONE"]) 34 | } 35 | 36 | get isPR(): boolean { 37 | const mustHave = ["DRONE", "DRONE_PULL_REQUEST", "DRONE_REPO"] 38 | const mustBeInts = ["DRONE_PULL_REQUEST"] 39 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 40 | } 41 | 42 | get pullRequestID(): string { 43 | return this.env.DRONE_PULL_REQUEST 44 | } 45 | 46 | get repoSlug(): string { 47 | return this.env.DRONE_REPO 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/static_file.cfa8fb80d2b65f4c4fa0b54d25352a3a0ff58f75.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "name": "tsconfig.json", 4 | "path": "tsconfig.json", 5 | "sha": "c7f3c3d35c0a051b874c043b5ee8d072f3c1434b", 6 | "size": 175, 7 | "url": 8 | "https://api.github.com/repos/artsy/emission/contents/tsconfig.json?ref=cfa8fb80d2b65f4c4fa0b54d25352a3a0ff58f75", 9 | "html_url": "https://github.com/artsy/emission/blob/cfa8fb80d2b65f4c4fa0b54d25352a3a0ff58f75/tsconfig.json", 10 | "git_url": "https://api.github.com/repos/artsy/emission/git/blobs/c7f3c3d35c0a051b874c043b5ee8d072f3c1434b", 11 | "download_url": 12 | "https://raw.githubusercontent.com/artsy/emission/cfa8fb80d2b65f4c4fa0b54d25352a3a0ff58f75/tsconfig.json", 13 | "type": "file", 14 | "content": 15 | "ewogICAgImNvbXBpbGVyT3B0aW9ucyI6IHsKICAgICAgICAiYWxsb3dKcyI6\nIHRydWUKICAgIH0sCiAgICAiZXhjbHVkZSI6IFsKICAgICAgICAibm9kZV9t\nb2R1bGVzIiwKICAgICAgICAiUG9kL0Fzc2V0cyIsCiAgICAgICAgIkV4YW1w\nbGUvQnVpbGQiLAogICAgICAgICJleHRlcm5hbHMvIgogICAgXQp9Cg==\n", 16 | "encoding": "base64", 17 | "_links": { 18 | "self": 19 | "https://api.github.com/repos/artsy/emission/contents/tsconfig.json?ref=cfa8fb80d2b65f4c4fa0b54d25352a3a0ff58f75", 20 | "git": "https://api.github.com/repos/artsy/emission/git/blobs/c7f3c3d35c0a051b874c043b5ee8d072f3c1434b", 21 | "html": "https://github.com/artsy/emission/blob/cfa8fb80d2b65f4c4fa0b54d25352a3a0ff58f75/tsconfig.json" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/runner/_tests/jsonToDSL.test.ts: -------------------------------------------------------------------------------- 1 | import { jsonToDSL } from "../jsonToDSL" 2 | import { DangerDSLJSONType } from "../../dsl/DangerDSL" 3 | import { GitDSL } from "../../dsl/GitDSL" 4 | 5 | /** 6 | * Mock everything that calls externaly 7 | */ 8 | jest.mock("../../platforms/github/GitHubGit") 9 | jest.mock("../../platforms/GitHub") 10 | jest.mock("../../platforms/git/localGetDiff") 11 | jest.mock("../../platforms/git/localGetCommits") 12 | jest.mock("../../platforms/git/diffToGitJSONDSL") 13 | jest.mock("../../platforms/git/gitJSONToGitDSL") 14 | jest.mock("@octokit/rest") 15 | 16 | // tslint:disable-next-line 17 | const foo = require("../../platforms/git/localGetDiff") 18 | foo.localGetDiff = jest.fn(() => Promise.resolve({})) 19 | 20 | describe("runner/jsonToDSL", () => { 21 | let dsl 22 | beforeEach(() => { 23 | dsl = { 24 | settings: { 25 | github: {}, 26 | cliArgs: { 27 | base: "develop", 28 | }, 29 | }, 30 | } 31 | }) 32 | 33 | it("should have a function named jsonToDSL", () => { 34 | expect(jsonToDSL).toBeTruthy() 35 | }) 36 | 37 | it("should return config", async () => { 38 | const outputDsl = await jsonToDSL(dsl as DangerDSLJSONType) 39 | expect(outputDsl.github).toBeUndefined() 40 | }) 41 | 42 | it("should call LocalGit with correct base", async () => { 43 | const outputDsl = await jsonToDSL(dsl as DangerDSLJSONType) 44 | expect(foo.localGetDiff).toHaveBeenLastCalledWith("develop", "HEAD") 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /source/ci_source/providers/VSTS.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist } from "../ci_source_helpers" 3 | /** 4 | * ### CI Setup 5 | * You'll need to add a npm build step and set the custom command to "run danger" 6 | * 7 | * Only supports VSTS with github as the repository, danger doesn't yet support VSTS as a repository platform 8 | * 9 | * ### Token Setup 10 | * 11 | * You need to add the `DANGER_GITHUB_API_TOKEN` environment variable 12 | */ 13 | export class VSTS implements CISource { 14 | constructor(private readonly env: Env) {} 15 | 16 | get name(): string { 17 | return "Visual Studio Team Services" 18 | } 19 | 20 | get isCI(): boolean { 21 | return ( 22 | ensureEnvKeysExist(this.env, ["SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", "BUILD_REPOSITORY_PROVIDER"]) && 23 | this.env.BUILD_REPOSITORY_PROVIDER == "GitHub" 24 | ) 25 | } 26 | 27 | get isPR(): boolean { 28 | const mustHave = ["BUILD_SOURCEBRANCH", "BUILD_REPOSITORY_PROVIDER", "BUILD_REASON", "BUILD_REPOSITORY_NAME"] 29 | 30 | return ensureEnvKeysExist(this.env, mustHave) && this.env.BUILD_REASON == "PullRequest" 31 | } 32 | 33 | get pullRequestID(): string { 34 | const match = this.env.BUILD_SOURCEBRANCH.match(/refs\/pull\/([0-9]+)\/merge/) 35 | 36 | if (match && match.length > 1) { 37 | return match[1] 38 | } 39 | 40 | return "" 41 | } 42 | 43 | get repoSlug(): string { 44 | return this.env.BUILD_REPOSITORY_NAME 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module "parse-diff" 2 | declare module "lodash.includes" 3 | declare module "lodash.find" 4 | declare module "lodash.isobject" 5 | declare module "lodash.keys" 6 | declare module "jest-runtime" 7 | declare module "jest-haste-map" 8 | declare module "jest-environment-node" 9 | declare module "jest-config" 10 | declare module "jsome" 11 | declare module "jsonpointer" 12 | declare module "parse-link-header" 13 | declare module "pinpoint" 14 | 15 | declare module "*/package.json" 16 | 17 | declare module "require-from-string" 18 | declare module "node-eval" 19 | declare module "node-cleanup" 20 | declare module "cli-interact" 21 | 22 | declare module "hyperlinker" 23 | declare module "supports-hyperlinks" 24 | 25 | // declare module "require-from-string" { 26 | // export interface RequireOptions { 27 | // /** List of paths, that will be appended to module paths. Useful, when you want 28 | // * to be able require modules from these paths. */ 29 | // appendPaths: string[] 30 | // /** 31 | // * Same as appendPath, but paths will be prepended. 32 | // */ 33 | // prependPaths: string[] 34 | // } 35 | // /** 36 | // * Load module from string in Node. 37 | // * @param code Module code 38 | // * @param filename Optional filename 39 | // * @param opts 40 | // */ 41 | // export default function(code: string, filename?: string, opts?: Partial): any 42 | // } 43 | 44 | declare module "parse-git-config" 45 | declare module "parse-github-url" 46 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/github_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "orta", 3 | "id": 49038, 4 | "avatar_url": "https://avatars.githubusercontent.com/u/49038?v=3", 5 | "gravatar_id": "", 6 | "url": "https://api.github.com/users/orta", 7 | "html_url": "https://github.com/orta", 8 | "followers_url": "https://api.github.com/users/orta/followers", 9 | "following_url": "https://api.github.com/users/orta/following{/other_user}", 10 | "gists_url": "https://api.github.com/users/orta/gists{/gist_id}", 11 | "starred_url": "https://api.github.com/users/orta/starred{/owner}{/repo}", 12 | "subscriptions_url": "https://api.github.com/users/orta/subscriptions", 13 | "organizations_url": "https://api.github.com/users/orta/orgs", 14 | "repos_url": "https://api.github.com/users/orta/repos", 15 | "events_url": "https://api.github.com/users/orta/events{/privacy}", 16 | "received_events_url": "https://api.github.com/users/orta/received_events", 17 | "type": "User", 18 | "site_admin": false, 19 | "name": "Orta", 20 | "company": "Artsy && Danger && CocoaPods", 21 | "blog": "http://orta.io", 22 | "location": "NYC / Huddersfield", 23 | "email": "orta.therox+gh@gmail.com", 24 | "hireable": null, 25 | "bio": "HIYA THERE I AM A PROGRAMMER WHO MAKES PROGRAMS THOUGH SOMETIMES I MAKE DESIGNS AND THATS OK. THANKS EVERYONE - HAVE A GOOD DAY, BYE", 26 | "public_repos": 425, 27 | "public_gists": 79, 28 | "followers": 1576, 29 | "following": 99, 30 | "created_at": "2009-01-24T20:40:31Z", 31 | "updated_at": "2016-11-14T08:13:49Z" 32 | } 33 | -------------------------------------------------------------------------------- /dangerfile.lite.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | 3 | import { DangerDSLType } from "./source/dsl/DangerDSL" 4 | declare var danger: DangerDSLType 5 | declare function warn(params: string): void 6 | 7 | const hasChangelog = danger.git.modified_files.includes("CHANGELOG.md") 8 | const isTrivial = danger.github && (danger.github.pr.body + danger.github.pr.title).includes("#trivial") 9 | 10 | if (!hasChangelog && !isTrivial) { 11 | warn( 12 | "Please add a changelog entry for your changes. You can find it in `CHANGELOG.md` \n\nPlease add your change and name to the master section." 13 | ) 14 | } 15 | 16 | import dtsGenerator from "./scripts/danger-dts" 17 | const currentDTS = dtsGenerator() 18 | const savedDTS = fs.readFileSync("source/danger.d.ts").toString() 19 | if (currentDTS !== savedDTS) { 20 | const message = "There are changes to the Danger DSL which are not reflected in the current danger.d.ts." 21 | const idea = "Please run yarn declarations and update this PR." 22 | fail(`${message}\n - ${idea}`) 23 | } 24 | 25 | // Always ensure we name all CI providers in the README. These 26 | // regularly get forgotten on a PR adding a new one. 27 | const sentence = danger.utils.sentence 28 | 29 | import { realProviders } from "./source/ci_source/providers" 30 | const readme = fs.readFileSync("README.md").toString() 31 | const names = realProviders.map(p => new p({}).name) 32 | const missing = names.filter(n => !readme.includes(n)) 33 | if (missing.length) { 34 | warn(`These providers are missing from the README: ${sentence(missing)}`) 35 | } 36 | -------------------------------------------------------------------------------- /source/commands/danger.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import * as program from "commander" 4 | import chalk from "chalk" 5 | import { version } from "../../package.json" 6 | 7 | process.on("unhandledRejection", function(reason: string, _p: any) { 8 | console.log(chalk.red("Error: "), reason) 9 | process.exitCode = 1 10 | }) 11 | 12 | // Provides the root node to the command-line architecture 13 | 14 | program 15 | .version(version) 16 | .command("init", "Helps you get started with Danger") 17 | .command("ci", "Runs Danger on CI") 18 | .command("process", "Like `ci` but lets another process handle evaluating a Dangerfile") 19 | .command("pr", "Runs your local Dangerfile against an existing GitHub PR. Will not post on the PR") 20 | .command("runner", "Runs a dangerfile against a DSL passed in via STDIN [You probably don't need this]") 21 | .command("local", "Runs danger standalone on a repo, useful for git hooks") 22 | .command("reset-status", "Set the status of a PR to pending when a new CI run starts") 23 | .on("--help", () => { 24 | console.log("\n") 25 | console.log(" Docs:") 26 | console.log("") 27 | console.log(" -> Getting started:") 28 | console.log(" http://danger.systems/js/guides/getting_started.html") 29 | console.log("") 30 | console.log(" -> The Dangerfile") 31 | console.log(" http://danger.systems/js/guides/the_dangerfile.html") 32 | console.log("") 33 | console.log(" -> API Reference") 34 | console.log(" http://danger.systems/js/reference.html") 35 | }) 36 | 37 | program.parse(process.argv) 38 | -------------------------------------------------------------------------------- /source/platforms/_tests/_pull_request_parser.test.ts: -------------------------------------------------------------------------------- 1 | import { pullRequestParser } from "../pullRequestParser" 2 | 3 | describe("parsing urls", () => { 4 | it("handles bad data", () => { 5 | expect(pullRequestParser("kjsdbfdsjkbfks")).toBeFalsy() 6 | }) 7 | 8 | it("pulls out the repo / pr ID", () => { 9 | expect(pullRequestParser("https://github.com/facebook/jest/pull/2555")).toEqual({ 10 | pullRequestNumber: "2555", 11 | repo: "facebook/jest", 12 | }) 13 | }) 14 | 15 | it("handles query params too", () => { 16 | const longPR = "https://github.com/artsy/emission/pull/406#pullrequestreview-10994863" 17 | expect(pullRequestParser(longPR)).toEqual({ 18 | pullRequestNumber: "406", 19 | repo: "artsy/emission", 20 | }) 21 | }) 22 | 23 | it("handles bitbucket server PRs", () => { 24 | expect(pullRequestParser("http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1")).toEqual({ 25 | pullRequestNumber: "1", 26 | repo: "projects/PROJ/repos/repo", 27 | }) 28 | }) 29 | 30 | it("handles bitbucket server PRs (overview)", () => { 31 | expect(pullRequestParser("http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1/overview")).toEqual({ 32 | pullRequestNumber: "1", 33 | repo: "projects/PROJ/repos/repo", 34 | }) 35 | }) 36 | 37 | it("handles bitbucket server PRs (overview) with dashes in name", () => { 38 | expect(pullRequestParser("http://localhost:7990/projects/PROJ/repos/super-repo/pull-requests/1/overview")).toEqual({ 39 | pullRequestNumber: "1", 40 | repo: "projects/PROJ/repos/super-repo", 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /source/platforms/github/comms/checks/githubAppSupport.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from "jsonwebtoken" 2 | import fetch from "node-fetch" 3 | 4 | // Step 1 5 | 6 | /** App ID + Signing Key = initial JWT to start auth process */ 7 | const jwtForGitHubAuth = (appID: string, key: string) => { 8 | const now = Math.round(new Date().getTime() / 1000) 9 | const expires: number = now + 300 10 | const keyContent = key 11 | const payload: object = { 12 | exp: expires, 13 | iat: now, 14 | iss: appID, 15 | } 16 | 17 | return jwt.sign(payload, keyContent, { algorithm: "RS256" }) 18 | } 19 | 20 | // Step 2 - Use App signed JWT to grab a per-installation 21 | 22 | const requestAccessTokenForInstallation = (appID: string, installationID: number, key: string) => { 23 | const url = `https://api.github.com/installations/${installationID}/access_tokens` 24 | const headers = { 25 | Accept: "application/vnd.github.machine-man-preview+json", 26 | Authorization: `Bearer ${jwtForGitHubAuth(appID, key)}`, 27 | } 28 | return fetch(url, { 29 | body: JSON.stringify({}), 30 | headers, 31 | method: "POST", 32 | }) 33 | } 34 | 35 | /** Generates a temporary access token for an app's installation, 5m long */ 36 | export const getAccessTokenForInstallation = async (appID: string, installationID: number, key: string) => { 37 | const newToken = await requestAccessTokenForInstallation(appID, installationID, key) 38 | const credentials = await newToken.json() 39 | if (!newToken.ok) { 40 | console.error(`Could not get an access token for ${installationID}`) 41 | console.error(`GitHub returned: ${JSON.stringify(credentials)}`) 42 | } 43 | return credentials.token as string 44 | } 45 | -------------------------------------------------------------------------------- /source/ci_source/providers/TeamCity.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist } from "../ci_source_helpers" 3 | import { pullRequestParser } from "../../platforms/pullRequestParser" 4 | 5 | /** 6 | * 7 | * ### CI Setup 8 | * 9 | * You need to add `DANGER_GITHUB_API_TOKEN` to the ENV for the build or machine manually. 10 | * Then you also need to figure out how to provide the URL for the pull request in `PULL_REQUEST_URL` ENV. 11 | * 12 | * TeamCity provides the `%teamcity.build.branch%` variable that contains something like `pull/123` that you can use: 13 | * ```sh 14 | * PULL_REQUEST_URL='https://github.com/dager/danger-js/%teamcity.build.branch%' 15 | * ``` 16 | * 17 | */ 18 | 19 | export class TeamCity implements CISource { 20 | constructor(private readonly env: Env) {} 21 | 22 | get name(): string { 23 | return "TeamCity" 24 | } 25 | 26 | get isCI(): boolean { 27 | return ensureEnvKeysExist(this.env, ["TEAMCITY_VERSION"]) 28 | } 29 | 30 | get isPR(): boolean { 31 | if (ensureEnvKeysExist(this.env, ["PULL_REQUEST_URL"])) { 32 | return true 33 | } 34 | 35 | const mustHave = ["PULL_REQUEST_URL"] 36 | return ensureEnvKeysExist(this.env, mustHave) 37 | } 38 | 39 | get pullRequestID(): string { 40 | const parts = pullRequestParser(this.env.PULL_REQUEST_URL || "") 41 | 42 | if (parts === null) { 43 | return "" 44 | } 45 | 46 | return parts.pullRequestNumber 47 | } 48 | 49 | get repoSlug(): string { 50 | const parts = pullRequestParser(this.env.PULL_REQUEST_URL || "") 51 | 52 | if (parts === null) { 53 | return "" 54 | } 55 | 56 | return parts.repo 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /source/ci_source/providers/Screwdriver.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * Install dependencies and add a danger step to your screwdriver.yaml: 8 | * ``` yml 9 | * jobs: 10 | * danger: 11 | * requires: [~pr, ~commit] 12 | * steps: 13 | * - setup: yarn install 14 | * - danger: yarn danger ci 15 | * secrets: 16 | * - DANGER_GITHUB_API_TOKEN 17 | * ``` 18 | * 19 | * ### Token Setup 20 | * 21 | * Add the `DANGER_GITHUB_API_TOKEN` to your pipeline env as a 22 | * [build secret](https://docs.screwdriver.cd/user-guide/configuration/secrets) 23 | */ 24 | export class Screwdriver implements CISource { 25 | constructor(private readonly env: Env) {} 26 | 27 | get name(): string { 28 | return "Screwdriver" 29 | } 30 | 31 | get isCI(): boolean { 32 | return ensureEnvKeysExist(this.env, ["SCREWDRIVER"]) 33 | } 34 | 35 | get isPR(): boolean { 36 | const mustHave = ["SCM_URL"] 37 | const mustBeInts = ["SD_PULL_REQUEST"] 38 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 39 | } 40 | 41 | private _parseRepoURL(): string { 42 | const repoURL = this.env.SCM_URL 43 | const regexp = new RegExp("([/:])([^/]+/[^/.]+)(?:.git)?$") 44 | const matches = repoURL.match(regexp) 45 | return matches ? matches[2] : "" 46 | } 47 | 48 | get pullRequestID(): string { 49 | return this.env.SD_PULL_REQUEST 50 | } 51 | 52 | get repoSlug(): string { 53 | return this._parseRepoURL() 54 | } 55 | 56 | get ciRunURL() { 57 | return process.env.BUILDKITE_BUILD_URL 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_nevercode.test.ts: -------------------------------------------------------------------------------- 1 | import { Nevercode } from "../Nevercode" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | NEVERCODE: "true", 6 | NEVERCODE_REPO_SLUG: "danger/danger-js", 7 | NEVERCODE_PULL_REQUEST: "true", 8 | NEVERCODE_PULL_REQUEST_NUMBER: "2", 9 | NEVERCODE_GIT_PROVIDER_PULL_REQUEST: "123234", 10 | } 11 | 12 | describe("being found when looking for CI", () => { 13 | it("finds Nevercode with the right ENV", () => { 14 | const ci = getCISourceForEnv(correctEnv) 15 | expect(ci).toBeInstanceOf(Nevercode) 16 | }) 17 | }) 18 | 19 | describe(".isCI", () => { 20 | it("validates when all Nevercode environment vars are set", () => { 21 | const nevercode = new Nevercode(correctEnv) 22 | expect(nevercode.isCI).toBeTruthy() 23 | }) 24 | 25 | it("does not validate without env", () => { 26 | const nevercode = new Nevercode({}) 27 | expect(nevercode.isCI).toBeFalsy() 28 | }) 29 | }) 30 | 31 | describe(".isPR", () => { 32 | it("validates when all nevercode environment vars are set", () => { 33 | const nevercode = new Nevercode(correctEnv) 34 | expect(nevercode.isPR).toBeTruthy() 35 | }) 36 | 37 | it("does not validate outside of nevercode", () => { 38 | const nevercode = new Nevercode({}) 39 | expect(nevercode.isPR).toBeFalsy() 40 | }) 41 | 42 | const envs = ["NEVERCODE_PULL_REQUEST", "NEVERCODE", "NEVERCODE_GIT_PROVIDER_PULL_REQUEST"] 43 | envs.forEach((key: string) => { 44 | let env = Object.assign({}, correctEnv) 45 | env[key] = null 46 | 47 | it(`does not validate when ${key} is missing`, () => { 48 | const nevercode = new Nevercode(env) 49 | expect(nevercode.isCI && nevercode.isPR).toBeFalsy() 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/bitbucket_server_commits.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "author": { 4 | "active": true, 5 | "displayName": "DangerCI", 6 | "emailAddress": "foo@bar.com", 7 | "id": 2, 8 | "name": "danger", 9 | "slug": "danger", 10 | "type": "NORMAL" 11 | }, 12 | "authorTimestamp": 1519442341000, 13 | "committer": { 14 | "active": true, 15 | "displayName": "DangerCI", 16 | "emailAddress": "foo@bar.com", 17 | "id": 2, 18 | "name": "danger", 19 | "slug": "danger", 20 | "type": "NORMAL" 21 | }, 22 | "committerTimestamp": 1519442341000, 23 | "displayId": "d6725486c38", 24 | "id": "d6725486c38d46a33e76f622cf24b9a388c8d13d", 25 | "message": "Modify and remove files", 26 | "parents": [ 27 | { 28 | "displayId": "c62ada76533", 29 | "id": "c62ada76533a2de045d4c6062988ba84df140729" 30 | } 31 | ] 32 | }, 33 | { 34 | "author": { 35 | "active": true, 36 | "displayName": "DangerCI", 37 | "emailAddress": "foo@bar.com", 38 | "id": 2, 39 | "name": "danger", 40 | "slug": "danger", 41 | "type": "NORMAL" 42 | }, 43 | "authorTimestamp": 1518863882000, 44 | "committer": { 45 | "active": true, 46 | "displayName": "DangerCI", 47 | "emailAddress": "foo@bar.com", 48 | "id": 2, 49 | "name": "danger", 50 | "slug": "danger", 51 | "type": "NORMAL" 52 | }, 53 | "committerTimestamp": 1518863882000, 54 | "displayId": "c62ada76533", 55 | "id": "c62ada76533a2de045d4c6062988ba84df140729", 56 | "message": "add banana", 57 | "parents": [ 58 | { 59 | "displayId": "8942a1f75e4", 60 | "id": "8942a1f75e4c95df836f19ef681d20a87da2ee20" 61 | } 62 | ] 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /source/ci_source/providers/Buildkite.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * With BuildKite you run the server yourself, so you will want to run it as a part of your build process. 8 | * It is common to have build steps, so we would recommend adding this to your scrip: 9 | * 10 | * ``` shell 11 | * echo "--- Running Danger" 12 | * bundle exec danger 13 | * ``` 14 | * 15 | * ### Token Setup 16 | * 17 | * #### GitHub 18 | * 19 | * As this is self-hosted, you will need to add the `DANGER_GITHUB_API_TOKEN` to your build user's ENV. The alternative 20 | * is to pass in the token as a prefix to the command `DANGER_GITHUB_API_TOKEN="123" bundle exec danger`. 21 | */ 22 | export class Buildkite implements CISource { 23 | constructor(private readonly env: Env) {} 24 | 25 | get name(): string { 26 | return "Buildkite" 27 | } 28 | 29 | get isCI(): boolean { 30 | return ensureEnvKeysExist(this.env, ["BUILDKITE"]) 31 | } 32 | 33 | get isPR(): boolean { 34 | const mustHave = ["BUILDKITE_REPO"] 35 | const mustBeInts = ["BUILDKITE_PULL_REQUEST"] 36 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 37 | } 38 | 39 | private _parseRepoURL(): string { 40 | const repoURL = this.env.BUILDKITE_REPO 41 | const regexp = new RegExp("([/:])([^/]+/[^/.]+)(?:.git)?$") 42 | const matches = repoURL.match(regexp) 43 | return matches ? matches[2] : "" 44 | } 45 | 46 | get pullRequestID(): string { 47 | return this.env.BUILDKITE_PULL_REQUEST 48 | } 49 | 50 | get repoSlug(): string { 51 | return this._parseRepoURL() 52 | } 53 | 54 | get ciRunURL() { 55 | return process.env.BUILDKITE_BUILD_URL 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /source/platforms/git/localGetCommits.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "../../debug" 2 | import * as JSON5 from "json5" 3 | 4 | import { spawn } from "child_process" 5 | import { GitCommit } from "../../dsl/Commit" 6 | 7 | const d = debug("localGetDiff") 8 | 9 | const sha = "%H" 10 | const parents = "%p" 11 | const authorName = "%an" 12 | const authorEmail = "%ae" 13 | const authorDate = "%ai" 14 | const committerName = "%cn" 15 | const committerEmail = "%ce" 16 | const committerDate = "%ci" 17 | const message = "%f" // this is subject, not message, so it'll only be one line 18 | 19 | const author = `"author": {"name": "${authorName}", "email": "${authorEmail}", "date": "${authorDate}" }` 20 | const committer = `"committer": {"name": "${committerName}", "email": "${committerEmail}", "date": "${committerDate}" }` 21 | export const formatJSON = `{ "sha": "${sha}", "parents": "${parents}", ${author}, ${committer}, "message": "${message}"},` 22 | 23 | export const localGetCommits = (base: string, head: string) => 24 | new Promise(done => { 25 | const args = ["log", `${base}...${head}`, `--pretty=format:${formatJSON}`] 26 | const child = spawn("git", args, { env: process.env }) 27 | d("> git", args.join(" ")) 28 | child.stdout.on("data", async data => { 29 | data = data.toString() 30 | 31 | // remove trailing comma, and wrap into an array 32 | const asJSONString = `[${data.substring(0, data.length - 1)}]` 33 | const commits = JSON5.parse(asJSONString) 34 | const realCommits = commits.map((c: any) => ({ 35 | ...c, 36 | parents: c.parents.split(" "), 37 | })) 38 | 39 | done(realCommits) 40 | }) 41 | 42 | child.stderr.on("data", data => { 43 | console.error(`Could not get commits from git between ${base} and ${head}`) 44 | throw new Error(data.toString()) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /source/platforms/github/comms/_tests/_checksCommenter.test.ts: -------------------------------------------------------------------------------- 1 | import { tweetSizedResultsFromResults } from "../checksCommenter" 2 | import { 3 | failsResultsWithoutMessages, 4 | inlineMultipleWarnResults, 5 | markdownResults, 6 | summaryResults, 7 | } from "../../../../runner/_tests/fixtures/ExampleDangerResults" 8 | 9 | const checksResults = { html_url: "https://gh.com/a" } 10 | 11 | it("handles fails", () => { 12 | const newResults = tweetSizedResultsFromResults(failsResultsWithoutMessages, checksResults) 13 | expect(newResults.markdowns[0].message).toMatchInlineSnapshot( 14 | `"Danger run resulted in 2 fails; to find out more, see the [checks page](https://gh.com/a)."` 15 | ) 16 | }) 17 | 18 | it("ignores inlines", () => { 19 | const newResults = tweetSizedResultsFromResults(failsResultsWithoutMessages, checksResults) 20 | expect(newResults.markdowns[0].message).toMatchInlineSnapshot( 21 | `"Danger run resulted in 2 fails; to find out more, see the [checks page](https://gh.com/a)."` 22 | ) 23 | }) 24 | 25 | it("deals with warnings", () => { 26 | const newResults = tweetSizedResultsFromResults(inlineMultipleWarnResults, checksResults) 27 | expect(newResults.markdowns[0].message).toMatchInlineSnapshot( 28 | `"Danger run resulted in 3 warnings; to find out more, see the [checks page](https://gh.com/a)."` 29 | ) 30 | }) 31 | 32 | it("deals with *just* markdowns by returning the markdowns", () => { 33 | const newResults = tweetSizedResultsFromResults(markdownResults, checksResults) 34 | expect(newResults.markdowns[0].message).toMatchInlineSnapshot(`"### Short Markdown Message1"`) 35 | }) 36 | 37 | it("handles singular results", () => { 38 | const newResults = tweetSizedResultsFromResults(summaryResults, checksResults) 39 | expect(newResults.markdowns[0].message).toMatchInlineSnapshot( 40 | `"Danger run resulted in 1 fail, 1 warning, 1 message and 1 markdown; to find out more, see the [checks page](https://gh.com/a)."` 41 | ) 42 | }) 43 | -------------------------------------------------------------------------------- /source/ci_source/providers/Codeship.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, getPullRequestIDForBranch } from "../ci_source_helpers" 3 | 4 | // https://documentation.codeship.com/pro/builds-and-configuration/environment-variables/ 5 | 6 | /** 7 | * ### CI Setup 8 | * 9 | * To make Danger run, add a new step to the `codeship-steps.yml` file: 10 | * 11 | * ``` 12 | * - type: parallel: 13 | * ... 14 | * - name: danger 15 | * service: web 16 | * command: yarn danger ci 17 | * ``` 18 | * 19 | * If you're using Codeship Classic, add `yarn danger ci` to your 'Test Commands' 20 | * 21 | * ### Token Setup 22 | * 23 | * You'll want to edit your `codeship-services.yml` file to include a reference 24 | * to the Danger authentication token: `DANGER_GITHUB_API_TOKEN`. 25 | * 26 | * ``` 27 | * project_name: 28 | * ... 29 | * environment: 30 | * - DANGER_GITHUB_API_TOKEN=[my_token] 31 | * ``` 32 | * 33 | * If you're using Codeship Classic, add `DANGER_GITHUB_API_TOKEN` to your 34 | * 'Environment' settings. 35 | */ 36 | 37 | export class Codeship implements CISource { 38 | private default = { prID: "0" } 39 | constructor(private readonly env: Env) {} 40 | 41 | async setup(): Promise { 42 | const prID = await getPullRequestIDForBranch(this, this.env, this.branchName) 43 | this.default.prID = prID.toString() 44 | } 45 | 46 | get name(): string { 47 | return "Codeship" 48 | } 49 | 50 | get isCI(): boolean { 51 | return ensureEnvKeysExist(this.env, ["CODESHIP"]) 52 | } 53 | 54 | get isPR(): boolean { 55 | return this.pullRequestID !== "0" 56 | } 57 | 58 | get pullRequestID(): string { 59 | return this.default.prID 60 | } 61 | 62 | get repoSlug(): string { 63 | if (ensureEnvKeysExist(this.env, ["CI_REPO_NAME"])) { 64 | return this.env.CI_REPO_NAME 65 | } 66 | return "" 67 | } 68 | 69 | private get branchName(): string { 70 | return this.env.CI_BRANCH 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /source/platforms/github/_tests/_github_utils.test.ts: -------------------------------------------------------------------------------- 1 | import utils from "../GitHubUtils" 2 | 3 | import { readFileSync } from "fs" 4 | import { resolve } from "path" 5 | 6 | const fixtures = resolve(__dirname, "..", "..", "_tests", "fixtures") 7 | const fixuredData = path => JSON.parse(readFileSync(`${fixtures}/${path}`, {}).toString()) 8 | const pr = fixuredData("github_pr.json") 9 | const apiFake = { 10 | repos: { 11 | getContent: jest.fn(), 12 | }, 13 | } as any 14 | 15 | describe("fileLinks", () => { 16 | it("Should convert a few paths into links", () => { 17 | const sut = utils(pr, apiFake) 18 | const links = sut.fileLinks(["a/b/c", "d/e/f"]) 19 | const url = "https://github.com/orta/emission/blob/genevc/a/b/c" 20 | expect(links).toEqual( 21 | `c and f` 22 | ) 23 | }) 24 | 25 | it("Should convert a few paths into links showing full links", () => { 26 | const sut = utils(pr, apiFake) 27 | const links = sut.fileLinks(["a/b/c", "d/e/f"], false) 28 | const url = "https://github.com/orta/emission/blob/genevc" 29 | expect(links).toEqual(`a/b/c and d/e/f`) 30 | }) 31 | 32 | it("Should convert a few paths into links showing full link on a custom fork/branch", () => { 33 | const sut = utils(pr, apiFake) 34 | const links = sut.fileLinks(["a/b/c", "d/e/f"], false, "orta/emission", "new") 35 | const url = "https://github.com/orta/emission" 36 | 37 | expect(links).toEqual(`a/b/c and d/e/f`) 38 | }) 39 | }) 40 | 41 | describe("getContents", () => { 42 | it("should call the API's getContents", () => { 43 | const sut = utils(pr, apiFake) 44 | sut.fileContents("/a/b/c.ts") 45 | expect(apiFake.repos.getContent).toHaveBeenCalledWith({ 46 | owner: "orta", 47 | path: "/a/b/c.ts", 48 | ref: "genevc", 49 | repo: "emission", 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /source/ci_source/providers/DockerCloud.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist } from "../ci_source_helpers" 3 | 4 | /** 5 | * 6 | * ### CI Setup 7 | * 8 | * You'll want to add danger to your existing `Dockerfile.test` (or whatever you 9 | * have choosen as your `sut` Dockerfile.) 10 | * 11 | * ```sh 12 | * ... 13 | * 14 | * CMD ["yarn", "danger"] 15 | * ``` 16 | * 17 | * ### Token Setup 18 | * 19 | * #### GitHub 20 | * 21 | * Your `DANGER_GITHUB_API_TOKEN` will need to be exposed to the `sut` part of your 22 | * `docker-compose.yml`. This looks similar to: 23 | * 24 | * ``` 25 | * sut: 26 | * ... 27 | * environment: 28 | * - DANGER_GITHUB_API_TOKEN=[my_token] 29 | * ``` 30 | */ 31 | 32 | export class DockerCloud implements CISource { 33 | constructor(private readonly env: Env) {} 34 | 35 | get name(): string { 36 | return "Docker Cloud" 37 | } 38 | 39 | get isCI(): boolean { 40 | return ensureEnvKeysExist(this.env, ["DOCKER_REPO"]) 41 | } 42 | 43 | get isPR(): boolean { 44 | if (ensureEnvKeysExist(this.env, ["PULL_REQUEST_URL"])) { 45 | return true 46 | } 47 | 48 | const mustHave = ["SOURCE_REPOSITORY_URL", "PULL_REQUEST_URL"] 49 | return ensureEnvKeysExist(this.env, mustHave) 50 | } 51 | 52 | private _prParseURL(): { owner?: string; reponame?: string; id?: string } { 53 | const prUrl = this.env.PULL_REQUEST_URL || "" 54 | const splitSlug = prUrl.split("/") 55 | if (splitSlug.length === 7) { 56 | const owner = splitSlug[3] 57 | const reponame = splitSlug[4] 58 | const id = splitSlug[6] 59 | return { owner, reponame, id } 60 | } 61 | return {} 62 | } 63 | 64 | get pullRequestID(): string { 65 | const { id } = this._prParseURL() 66 | return id || "" 67 | } 68 | 69 | get repoSlug(): string { 70 | const { owner, reponame } = this._prParseURL() 71 | return owner && reponame ? `${owner}/${reponame}` : "" 72 | } 73 | 74 | get repoURL(): string { 75 | return this.env.SOURCE_REPOSITORY_URL 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /dangerfile.ts: -------------------------------------------------------------------------------- 1 | // Because we don't get to use the d.ts, we can pass in a subset here. 2 | // This means we can re-use the type infra from the app, without having to 3 | // fake the import. 4 | 5 | import yarn from "danger-plugin-yarn" 6 | import jest from "danger-plugin-jest" 7 | 8 | import { DangerDSLType } from "./source/dsl/DangerDSL" 9 | declare var danger: DangerDSLType 10 | // declare var results: any 11 | declare function warn(message: string, file?: string, line?: number): void 12 | // declare function fail(params: string): void 13 | // declare function message(params: string): void 14 | // declare function markdown(params: string): void 15 | // declare function schedule(promise: Promise): void 16 | // declare function schedule(promise: () => Promise): void 17 | // declare function schedule(callback: (resolve: any) => void): void 18 | 19 | export default async () => { 20 | if (!danger.github) { 21 | return 22 | } 23 | 24 | // Request a CHANGELOG entry if not declared #trivial 25 | const hasChangelog = danger.git.modified_files.includes("CHANGELOG.md") 26 | const isTrivial = (danger.github.pr.body + danger.github.pr.title).includes("#trivial") 27 | const isUser = danger.github!.pr.user.type === "User" 28 | 29 | // Politely ask for their name on the entry too 30 | if (!hasChangelog && !isTrivial && !isUser) { 31 | const changelogDiff = await danger.git.diffForFile("CHANGELOG.md") 32 | const contributorName = danger.github.pr.user.login 33 | if (changelogDiff && changelogDiff.diff.includes(contributorName)) { 34 | warn("Please add your GitHub name to the changelog entry, so we can attribute you correctly.") 35 | } 36 | } 37 | 38 | // Some libraries 39 | await yarn() 40 | await jest() 41 | 42 | // The thing I'm testing 43 | await danger.github.utils.createUpdatedIssueWithID("TestID", `Last PR ${danger.github.pr.number}`, { 44 | title: "My First Issue", 45 | open: true, 46 | repo: "sandbox", 47 | owner: "PerilTest", 48 | }) 49 | } 50 | 51 | // Re-run the git push hooks 52 | import "./dangerfile.lite" 53 | -------------------------------------------------------------------------------- /source/ci_source/providers/Concourse.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * Concourse CI Integration 6 | * 7 | * https://concourse-ci.org/ 8 | * 9 | * ### CI Setup 10 | * 11 | * With Concourse, you run the docker images yourself, so you will want to add `yarn danger ci` within one of your build jobs. 12 | * 13 | * ``` shell 14 | * build: 15 | * image: golang 16 | * commands: 17 | * - ... 18 | * - yarn danger ci 19 | * ``` 20 | * 21 | * ### Environment Variable Setup 22 | * 23 | * As this is self-hosted, you will need to add the `CONCOURSE` environment variable `export CONCOURSE=true` to your build environment, 24 | * as well as setting environment variables for `PULL_REQUEST_ID` and `REPO_SLUG`. Assuming you are using the github pull request resource 25 | * https://github.com/jtarchie/github-pullrequest-resource the id of the PR can be accessed from `git config --get pullrequest.id`. 26 | * 27 | * ### Token Setup 28 | * 29 | * Once again as this is self-hosted, you will need to add `DANGER_GITHUB_API_TOKEN` environment variable to the build environment. 30 | * The suggested method of storing the token is within the vault - https://concourse-ci.org/creds.html 31 | */ 32 | export class Concourse implements CISource { 33 | constructor(private readonly env: Env) {} 34 | 35 | get name(): string { 36 | return "Concourse" 37 | } 38 | 39 | get isCI(): boolean { 40 | return ensureEnvKeysExist(this.env, ["CONCOURSE"]) 41 | } 42 | 43 | get isPR(): boolean { 44 | const mustHave = ["PULL_REQUEST_ID", "REPO_SLUG"] 45 | const mustBeInts = ["PULL_REQUEST_ID"] 46 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 47 | } 48 | 49 | get pullRequestID(): string { 50 | return this.env.PULL_REQUEST_ID 51 | } 52 | 53 | get repoSlug(): string { 54 | return this.env.REPO_SLUG 55 | } 56 | 57 | get ciRunURL() { 58 | return this.env.BUILD_URL 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/commands/utils/_tests/dangerRunToRunnerCLI.test.ts: -------------------------------------------------------------------------------- 1 | import dangerRunToRunnerCLI from "../dangerRunToRunnerCLI" 2 | 3 | describe("it can handle the command", () => { 4 | it("`danger ci`", () => { 5 | expect(dangerRunToRunnerCLI(["danger", "ci"])).toEqual("danger runner".split(" ")) 6 | }) 7 | 8 | it("`danger local`", () => { 9 | expect(dangerRunToRunnerCLI(["danger", "local"])).toEqual("danger runner".split(" ")) 10 | }) 11 | 12 | it("`danger ci --dangerfile myDangerfile.ts`", () => { 13 | expect(dangerRunToRunnerCLI(["danger", "ci", "--dangerfile", "myDangerfile.ts"])).toEqual( 14 | "danger runner --dangerfile myDangerfile.ts".split(" ") 15 | ) 16 | }) 17 | 18 | it("`node distribution/commands/danger-ci.js`", () => { 19 | expect(dangerRunToRunnerCLI(["node", "distribution/commands/danger-ci.js"])).toEqual( 20 | "node distribution/commands/danger-runner.js".split(" ") 21 | ) 22 | }) 23 | 24 | it("`node distribution/commands/danger-ci.js --dangerfile myDangerfile.ts`", () => { 25 | expect( 26 | dangerRunToRunnerCLI(["node", "distribution/commands/danger-ci.js", "--dangerfile", "myDangerfile.ts"]) 27 | ).toEqual("node distribution/commands/danger-runner.js --dangerfile myDangerfile.ts".split(" ")) 28 | }) 29 | }) 30 | 31 | it("`node distribution/commands/danger-ci.js --dangerfile 'myDanger file.ts'`", () => { 32 | expect( 33 | dangerRunToRunnerCLI(["node", "distribution/commands/danger-ci.js", "--dangerfile", "myDanger file.ts"]) 34 | ).toEqual(["node", "distribution/commands/danger-runner.js", "--dangerfile", "myDanger file.ts"]) 35 | }) 36 | 37 | it("`node distribution/commands/danger-pr.js --dangerfile 'myDanger file.ts'`", () => { 38 | expect( 39 | dangerRunToRunnerCLI(["node", "distribution/commands/danger-pr.js", "--dangerfile", "myDanger file.ts"]) 40 | ).toEqual(["node", "distribution/commands/danger-runner.js", "--dangerfile", "myDanger file.ts"]) 41 | }) 42 | 43 | it("`danger pr --dangerfile 'myDanger file.ts'`", () => { 44 | expect(dangerRunToRunnerCLI(["danger", "pr", "--dangerfile", "myDanger file.ts"])).toEqual([ 45 | "danger", 46 | "runner", 47 | "--dangerfile", 48 | "myDanger file.ts", 49 | ]) 50 | }) 51 | -------------------------------------------------------------------------------- /source/ci_source/providers/Bitrise.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | /** 4 | * ### CI Setup 5 | * You need to edit your `bitrise.yml` (in version control, or directly from UI) to include `yarn danger ci`. 6 | * 7 | * You can set "is_always_run: true" to ensure that it reports even if previous steps fails 8 | * 9 | * ```yaml 10 | * workflows: 11 | * : 12 | * steps: 13 | * - yarn: 14 | * inputs: 15 | * - args: ci 16 | * - command: danger 17 | * is_always_run: true 18 | * ``` 19 | * 20 | * Adding this to your `bitrise.yml` allows Danger to fail your build, both on the Bitrise website and within your Pull Request. 21 | * With that set up, you can edit your job to add `yarn danger ci` at the build action. 22 | * 23 | * ### Token Setup 24 | * 25 | * You need to add the `DANGER_GITHUB_API_TOKEN` environment variable, to do this, 26 | * go to your repo's secrets, which should look like: `https://www.bitrise.io/app/[app_id]#/workflow` and secrets tab. 27 | * 28 | * You should check the case "Expose for Pull Requests?". 29 | */ 30 | export class Bitrise implements CISource { 31 | constructor(private readonly env: Env) {} 32 | 33 | get name(): string { 34 | return "Bitrise" 35 | } 36 | 37 | get isCI(): boolean { 38 | return ensureEnvKeysExist(this.env, ["BITRISE_IO"]) 39 | } 40 | 41 | get isPR(): boolean { 42 | const mustHave = ["GIT_REPOSITORY_URL"] 43 | const mustBeInts = ["BITRISE_PULL_REQUEST"] 44 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 45 | } 46 | 47 | private _parseRepoURL(): string { 48 | const repoURL = this.env.GIT_REPOSITORY_URL 49 | const regexp = new RegExp("([/:])([^/]+/[^/.]+)(?:.git)?$") 50 | const matches = repoURL.match(regexp) 51 | return matches ? matches[2] : "" 52 | } 53 | 54 | get pullRequestID(): string { 55 | return this.env.BITRISE_PULL_REQUEST 56 | } 57 | 58 | get repoSlug(): string { 59 | return this._parseRepoURL() 60 | } 61 | 62 | get ciRunURL() { 63 | return process.env.BITRISE_PULL_REQUEST 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | ## How does Danger JS work? 2 | 3 | Danger provides an evaluation system for creating per-application rules. Basically, it is running arbitrary JavaScript with some extra PR metadata added in at runtime. 4 | 5 | Actually doing that though, is a bit of a process. 6 | 7 | ## Setup 8 | 9 | **Step 1: CI**. Danger needs to figure out what CI we're running on. You can see them all [in `source/ci_source/providers`][provs]. These use 10 | ENV VARs to figure out which CI `danger ci` is running on and validate whether it is a pull request. 11 | 12 | **Step 2: Platform**. Danger needs to know which platform the code review is happening in. Today it's just GitHub, but BitBucket Server is around the corner. 13 | 14 | **Step 3: JSON DSL**. To allow for all of: 15 | 16 | * `danger ci` to evaluate async code correctly 17 | * `danger process` to work with other languages 18 | * `peril` to arbitrarily sandbox danger per-run on a unique docker container 19 | 20 | Danger first generates a JSON DSL. This can be passed safely between processes, or servers. For `danger ci` the exposed DSL is created 21 | as a [DangerDSLJSONType][dangerdsl] and this is passed into the hidden [command `danger runner`][runner]. 22 | 23 | **Step 4: DSL**. The JSON DSL is picked up from STDIN in `danger runner` and then converted into a [DangerDSLType][dangerdsl]. This is basically where 24 | functions are added into the DSL. 25 | 26 | **Step 5: Evaluation**. With the DSL ready, the [inline runner][in_runner] sets up a transpiled environment for evaluating your code, and adds the DSL attributes into the global evaluation context. The Dangerfile has the `import {...} from 'danger'` stripped, and then is executed inline. 27 | 28 | **Step 6: Results**. Once the `danger runner` process is finished with evaluation, the results are passed back to the the plaform. The platform then 29 | chooses whether to create/delete/edit any messages in core review. 30 | 31 | [provs]: https://github.com/danger/danger-js/tree/master/source/ci_source/providers 32 | [dangerdsl]: https://github.com/danger/danger-js/blob/master/sourformace/dsl/DangerDSL.ts 33 | [runner]: https://github.com/danger/danger-js/blob/master/source/commands/danger-runner.ts 34 | [in_runner]: https://github.com/danger/danger-js/blob/master/source/runner/runners/inline.ts 35 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_drone.test.ts: -------------------------------------------------------------------------------- 1 | import { Drone } from "../Drone" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | DRONE: "true", 6 | DRONE_PULL_REQUEST: "800", 7 | DRONE_REPO: "artsy/eigen", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds Drone with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(Drone) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | test("validates when all Drone environment vars are set", () => { 19 | const drone = new Drone(correctEnv) 20 | expect(drone.isCI).toBeTruthy() 21 | }) 22 | 23 | test("does not validate without DRONE", () => { 24 | const drone = new Drone({}) 25 | expect(drone.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | test("validates when all Drone environment vars are set", () => { 31 | const drone = new Drone(correctEnv) 32 | expect(drone.isPR).toBeTruthy() 33 | }) 34 | 35 | test("does not validate without DRONE_PULL_REQUEST", () => { 36 | const drone = new Drone({}) 37 | expect(drone.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = ["DRONE_PULL_REQUEST", "DRONE_REPO"] 41 | envs.forEach((key: string) => { 42 | let env = { 43 | DRONE: "true", 44 | DRONE_PULL_REQUEST: "800", 45 | DRONE_REPO: "artsy/eigen", 46 | } 47 | env[key] = null 48 | 49 | test(`does not validate when ${key} is missing`, () => { 50 | const drone = new Drone(env) 51 | expect(drone.isPR).toBeFalsy() 52 | }) 53 | }) 54 | 55 | it("needs to have a PR number", () => { 56 | let env = { 57 | DRONE: "true", 58 | DRONE_PULL_REQUEST: "asdasd", 59 | DRONE_REPO: "artsy/eigen", 60 | } 61 | const drone = new Drone(env) 62 | expect(drone.isPR).toBeFalsy() 63 | }) 64 | }) 65 | 66 | describe(".pullRequestID", () => { 67 | it("pulls it out of the env", () => { 68 | const drone = new Drone(correctEnv) 69 | expect(drone.pullRequestID).toEqual("800") 70 | }) 71 | }) 72 | 73 | describe(".repoSlug", () => { 74 | it("pulls it out of the env", () => { 75 | const drone = new Drone(correctEnv) 76 | expect(drone.repoSlug).toEqual("artsy/eigen") 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_jenkins.test.ts: -------------------------------------------------------------------------------- 1 | import { Jenkins } from "../Jenkins" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | ghprbGhRepository: "danger/danger-js", 6 | ghprbPullId: "50", 7 | JENKINS_URL: "https://danger.jenkins", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds Jenkins with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(Jenkins) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | it("validates when JENKINS_URL is present in environment", () => { 19 | const jenkins = new Jenkins(correctEnv) 20 | expect(jenkins.isCI).toBeTruthy() 21 | }) 22 | 23 | it("does not validate without JENKINS_URL", () => { 24 | const jenkins = new Jenkins({}) 25 | expect(jenkins.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | it("validates when all Jenkins environment variables are set", () => { 31 | const jenkins = new Jenkins(correctEnv) 32 | expect(jenkins.isPR).toBeTruthy() 33 | }) 34 | 35 | it("does not validate with required environment variables", () => { 36 | const jenkins = new Jenkins({}) 37 | expect(jenkins.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = ["JENKINS_URL", "ghprbPullId", "ghprbGhRepository"] 41 | envs.forEach((key: string) => { 42 | const env = { 43 | ...correctEnv, 44 | [key]: null, 45 | } 46 | 47 | it(`does not validate when ${key} is missing`, () => { 48 | const jenkins = new Jenkins(env) 49 | expect(jenkins.isPR).toBeFalsy() 50 | }) 51 | }) 52 | 53 | it("needs to have a PR number", () => { 54 | const env = { 55 | ...correctEnv, 56 | ghprbPullId: "not a number", 57 | } 58 | const jenkins = new Jenkins(env) 59 | expect(jenkins.isPR).toBeFalsy() 60 | }) 61 | }) 62 | 63 | describe(".pullRequestID", () => { 64 | it("pulls it out of environment", () => { 65 | const jenkins = new Jenkins(correctEnv) 66 | expect(jenkins.pullRequestID).toEqual("50") 67 | }) 68 | }) 69 | 70 | describe(".repoSlug", () => { 71 | it("pulls it out of environment", () => { 72 | const jenkins = new Jenkins(correctEnv) 73 | expect(jenkins.repoSlug).toEqual("danger/danger-js") 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /source/commands/ci/runner.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { debug } from "../../debug" 3 | 4 | import { getPlatformForEnv, Platform } from "../../platforms/platform" 5 | import { Executor, ExecutorOptions } from "../../runner/Executor" 6 | import runDangerSubprocess from "../utils/runDangerSubprocess" 7 | import { SharedCLI } from "../utils/sharedDangerfileArgs" 8 | import getRuntimeCISource from "../utils/getRuntimeCISource" 9 | 10 | import inlineRunner from "../../runner/runners/inline" 11 | import { jsonDSLGenerator } from "../../runner/dslGenerator" 12 | import dangerRunToRunnerCLI from "../utils/dangerRunToRunnerCLI" 13 | import { CISource } from "../../ci_source/ci_source" 14 | 15 | const d = debug("process_runner") 16 | 17 | export interface RunnerConfig { 18 | source?: CISource 19 | platform?: Platform 20 | additionalArgs?: string[] 21 | } 22 | 23 | export const runRunner = async (app: SharedCLI, config?: RunnerConfig) => { 24 | d(`Starting sub-process run with ${app.args}`) 25 | const source = (config && config.source) || (await getRuntimeCISource(app)) 26 | 27 | // This does not set a failing exit code 28 | if (source && !source.isPR) { 29 | console.log("Skipping Danger due to this run not executing on a PR.") 30 | } 31 | 32 | // The optimal path 33 | if (source && source.isPR) { 34 | const platform = (config && config.platform) || getPlatformForEnv(process.env, source) 35 | if (!platform) { 36 | console.log(chalk.red(`Could not find a source code hosting platform for ${source.name}.`)) 37 | console.log( 38 | `Currently Danger JS only supports GitHub, if you want other platforms, consider the Ruby version or help out.` 39 | ) 40 | process.exitCode = 1 41 | } 42 | 43 | if (platform) { 44 | const dangerJSONDSL = await jsonDSLGenerator(platform) 45 | 46 | const config: ExecutorOptions = { 47 | stdoutOnly: !platform.supportsCommenting() || app.textOnly, 48 | verbose: app.verbose, 49 | jsonOnly: false, 50 | dangerID: app.id || "default", 51 | } 52 | 53 | const runnerCommand = dangerRunToRunnerCLI(process.argv) 54 | d(`Preparing to run: ${runnerCommand}`) 55 | 56 | const exec = new Executor(source, platform, inlineRunner, config) 57 | runDangerSubprocess(runnerCommand, dangerJSONDSL, exec) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/platforms/github/GitHubGit.ts: -------------------------------------------------------------------------------- 1 | import { GitDSL, GitJSONDSL } from "../../dsl/GitDSL" 2 | import { GitHubCommit, GitHubDSL } from "../../dsl/GitHubDSL" 3 | import { GitCommit } from "../../dsl/Commit" 4 | 5 | import { GitHubAPI } from "../github/GitHubAPI" 6 | 7 | import { diffToGitJSONDSL } from "../git/diffToGitJSONDSL" 8 | import { GitJSONToGitDSLConfig, gitJSONToGitDSL } from "../git/gitJSONToGitDSL" 9 | 10 | import { debug } from "../../debug" 11 | const d = debug("GitHubGit") 12 | 13 | /** 14 | * Returns the response for the new comment 15 | * 16 | * @param {GitHubCommit} ghCommit A GitHub based commit 17 | * @returns {GitCommit} a Git commit representation without GH metadata 18 | */ 19 | function githubCommitToGitCommit(ghCommit: GitHubCommit): GitCommit { 20 | return { 21 | sha: ghCommit.sha, 22 | parents: ghCommit.parents.map(p => p.sha), 23 | author: ghCommit.commit.author, 24 | committer: ghCommit.commit.committer, 25 | message: ghCommit.commit.message, 26 | tree: ghCommit.commit.tree, 27 | url: ghCommit.url, 28 | } 29 | } 30 | 31 | export default async function gitDSLForGitHub(api: GitHubAPI): Promise { 32 | // We'll need all this info to be able to generate a working GitDSL object 33 | const diff = await api.getPullRequestDiff() 34 | const getCommits = await api.getPullRequestCommits() 35 | const commits = getCommits.map(githubCommitToGitCommit) 36 | return diffToGitJSONDSL(diff, commits) 37 | } 38 | 39 | export const gitHubGitDSL = (github: GitHubDSL, json: GitJSONDSL, githubAPI?: GitHubAPI): GitDSL => { 40 | // TODO: Remove the GitHubAPI 41 | // This is blocked by https://github.com/octokit/node-github/issues/602 42 | const ghAPI = 43 | githubAPI || 44 | new GitHubAPI( 45 | { repoSlug: github.pr.base.repo.full_name, pullRequestID: String(github.pr.number) }, 46 | process.env["DANGER_GITHUB_API_TOKEN"] 47 | ) 48 | 49 | if (!githubAPI) { 50 | d("Got no GH API, had to make it") 51 | } 52 | 53 | const config: GitJSONToGitDSLConfig = { 54 | repo: github.pr.head.repo.full_name, 55 | baseSHA: github.pr.base.sha, 56 | headSHA: github.pr.head.sha, 57 | getFileContents: github.utils.fileContents, 58 | getFullDiff: ghAPI.getPullRequestDiff, 59 | } 60 | 61 | d("Setting up git DSL with: ", config) 62 | return gitJSONToGitDSL(json, config) 63 | } 64 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_bitrise.test.ts: -------------------------------------------------------------------------------- 1 | import { Bitrise } from "../Bitrise" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | BITRISE_IO: "true", 6 | BITRISE_PULL_REQUEST: "800", 7 | GIT_REPOSITORY_URL: "https://github.com/artsy/eigen", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds Bitrise with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(Bitrise) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | it("validates when all Bitrise environment vars are set", () => { 19 | const bitrise = new Bitrise(correctEnv) 20 | expect(bitrise.isCI).toBeTruthy() 21 | }) 22 | 23 | it("does not validate without env", () => { 24 | const bitrise = new Bitrise({}) 25 | expect(bitrise.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | it("validates when all bitrise environment vars are set", () => { 31 | const bitrise = new Bitrise(correctEnv) 32 | expect(bitrise.isPR).toBeTruthy() 33 | }) 34 | 35 | it("does not validate outside of bitrise", () => { 36 | const bitrise = new Bitrise({}) 37 | expect(bitrise.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = ["BITRISE_PULL_REQUEST", "GIT_REPOSITORY_URL", "BITRISE_IO"] 41 | envs.forEach((key: string) => { 42 | let env = { ...correctEnv } 43 | env[key] = null 44 | 45 | it(`does not validate when ${key} is missing`, () => { 46 | const bitrise = new Bitrise(env) 47 | expect(bitrise.isCI && bitrise.isPR).toBeFalsy() 48 | }) 49 | }) 50 | }) 51 | 52 | describe(".pullRequestID", () => { 53 | it("pulls it out of the env", () => { 54 | const bitrise = new Bitrise({ 55 | BITRISE_PULL_REQUEST: "800", 56 | }) 57 | expect(bitrise.pullRequestID).toEqual("800") 58 | }) 59 | }) 60 | 61 | describe(".repoSlug", () => { 62 | it("derives it from the repo URL", () => { 63 | const bitrise = new Bitrise(correctEnv) 64 | expect(bitrise.repoSlug).toEqual("artsy/eigen") 65 | }) 66 | 67 | it("derives it from the repo URL in SSH format", () => { 68 | const env = { 69 | ...correctEnv, 70 | GIT_REPOSITORY_URL: "git@github.com:artsy/eigen.git", 71 | } 72 | const bitrise = new Bitrise(env) 73 | expect(bitrise.repoSlug).toEqual("artsy/eigen") 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /docs/guides/peril.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Peril 3 | subtitle: When there's not enough Danger in your life 4 | layout: guide_js 5 | order: 4 6 | blurb: When there's not enough Danger in your life 7 | --- 8 | 9 | ## TLDR: Peril 10 | 11 | Peril is a hosted instance of Danger. So instead of running on CI, it will run on a server somewhere and can respond instantly to webhooks. This gives Danger the ability to respond instantly to PR changes, and to run on more than just PRs. 12 | 13 | A lot of the information on Peril can be found on the [Artsy blog: here](http://artsy.github.io/blog/2017/09/04/Introducing-Peril/) 14 | 15 | Today Peril is self-hosted via heroku. There is a walkthrough on the [Peril repo: here](https://github.com/danger/peril/blob/master/docs/setup_for_org.md). It's still a pretty fast moving project ever 6 months into deployment so expect to maybe fix your own problem occasionally. 16 | 17 | ## Dangerfile implications 18 | 19 | Two tricky problems in Peril today: 20 | 21 | * Async is weird. 22 | * Can't do relative `import`s. 23 | 24 | Today Peril runs by inline execution of a JavaScript script. This has a serious draw-back in that async behavior doesn't work how you think it does. Here are some patterns for handling that. 25 | 26 | * **Ignore Async.** - A Dangerfile is a script, the non-blocking aspect of the node API can be ignored. E.g. use `path.xSync` instead of `path.x` 27 | 28 | * **Scheduling** - The Dangerfile DSL includes a function called `schedule`, this can handle either a promise or a function with a callback arg. For example using `async/await`: 29 | 30 | ```js 31 | import { schedule, danger } from "danger" 32 | 33 | /// [... a bunch of functions] 34 | 35 | schedule(async () => { 36 | const packageDiff = await danger.git.JSONDiffForFile("package.json") 37 | checkForRelease(packageDiff) 38 | checkForNewDependencies(packageDiff) 39 | checkForLockfileDiff(packageDiff) 40 | checkForTypesInDeps(packageDiff) 41 | }) 42 | ``` 43 | 44 | In this case, the closure is queued up and Danger waits until all `schedule` functions/promises are finished before continuing, so make sure to not cause it to lock. 45 | 46 | ## Plugin implications 47 | 48 | A plugin that runs on Peril will also have to handle the above if it uses async code. For some examples of this, see [danger-plugin-spellcheck](https://github.com/orta/danger-plugin-spellcheck#danger-plugin-spellcheck) or 49 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_concourse.test.ts: -------------------------------------------------------------------------------- 1 | import { Concourse } from "../Concourse" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | CONCOURSE: "true", 6 | REPO_SLUG: "danger/danger-js", 7 | PULL_REQUEST_ID: "2", 8 | BUILD_URL: "https://github.com/danger/danger-js/blob/master", 9 | } 10 | 11 | describe("being found when looking for CI", () => { 12 | it("finds Concourse with the right ENV", () => { 13 | const ci = getCISourceForEnv(correctEnv) 14 | expect(ci).toBeInstanceOf(Concourse) 15 | }) 16 | }) 17 | 18 | describe(".isCI", () => { 19 | it("validates when all Concourse environment vars are set", () => { 20 | const concourse = new Concourse(correctEnv) 21 | expect(concourse.isCI).toBeTruthy() 22 | }) 23 | 24 | it("does not validate without env", () => { 25 | const concourse = new Concourse({}) 26 | expect(concourse.isCI).toBeFalsy() 27 | }) 28 | }) 29 | 30 | describe(".isPR", () => { 31 | it("validates when all Concourse environment vars are set", () => { 32 | const concourse = new Concourse(correctEnv) 33 | expect(concourse.isPR).toBeTruthy() 34 | }) 35 | 36 | it("does not validate outside of Concourse", () => { 37 | const concourse = new Concourse({}) 38 | expect(concourse.isPR).toBeFalsy() 39 | }) 40 | 41 | const envs = ["CONCOURSE", "REPO_SLUG", "PULL_REQUEST_ID"] 42 | envs.forEach((key: string) => { 43 | let env = Object.assign({}, correctEnv) 44 | env[key] = null 45 | 46 | it(`does not validate when ${key} is missing`, () => { 47 | const concourse = new Concourse({}) 48 | expect(concourse.isCI && concourse.isPR).toBeFalsy() 49 | }) 50 | }) 51 | 52 | describe("repo slug", () => { 53 | it("returns correct slug", () => { 54 | const concourse = new Concourse(correctEnv) 55 | expect(concourse.repoSlug).toEqual("danger/danger-js") 56 | }) 57 | }) 58 | 59 | describe("pull request id", () => { 60 | it("returns correct id", () => { 61 | const concourse = new Concourse(correctEnv) 62 | expect(concourse.pullRequestID).toEqual("2") 63 | }) 64 | }) 65 | 66 | describe("build url", () => { 67 | it("returns correct build url", () => { 68 | const concourse = new Concourse(correctEnv) 69 | expect(concourse.ciRunURL).toEqual("https://github.com/danger/danger-js/blob/master") 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_dockerCloud.test.ts: -------------------------------------------------------------------------------- 1 | import { DockerCloud } from "../DockerCloud" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | DOCKER_REPO: "someproject", 6 | PULL_REQUEST_URL: "https://github.com/artsy/eigen/pull/800", 7 | SOURCE_REPOSITORY_URL: "https://github.com/artsy/eigen", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds DockerCloud with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(DockerCloud) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | it("validates when all DockerCloud environment vars are set", () => { 19 | const dockerCloud = new DockerCloud(correctEnv) 20 | expect(dockerCloud.isCI).toBeTruthy() 21 | }) 22 | 23 | it("does not validate without env", () => { 24 | const dockerCloud = new DockerCloud({}) 25 | expect(dockerCloud.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | it("validates when all dockerCloud environment vars are set", () => { 31 | const dockerCloud = new DockerCloud(correctEnv) 32 | expect(dockerCloud.isPR).toBeTruthy() 33 | }) 34 | 35 | it("does not validate outside of dockerCloud", () => { 36 | const dockerCloud = new DockerCloud({}) 37 | expect(dockerCloud.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = ["PULL_REQUEST_URL", "SOURCE_REPOSITORY_URL", "DOCKER_REPO"] 41 | envs.forEach((key: string) => { 42 | let env = { 43 | DOCKER_REPO: "someproject", 44 | PULL_REQUEST_URL: "https://github.com/artsy/eigen/pull/800", 45 | SOURCE_REPOSITORY_URL: "https://github.com/artsy/eigen", 46 | } 47 | env[key] = null 48 | 49 | it(`does not validate when ${key} is missing`, () => { 50 | const dockerCloud = new DockerCloud({}) 51 | expect(dockerCloud.isPR).toBeFalsy() 52 | }) 53 | }) 54 | }) 55 | 56 | describe(".pullRequestID", () => { 57 | it("pulls it out of the env", () => { 58 | const dockerCloud = new DockerCloud({ 59 | PULL_REQUEST_URL: "https://github.com/artsy/eigen/pull/800", 60 | }) 61 | expect(dockerCloud.pullRequestID).toEqual("800") 62 | }) 63 | }) 64 | 65 | describe(".repoSlug", () => { 66 | it("derives it from the PR Url", () => { 67 | const dockerCloud = new DockerCloud(correctEnv) 68 | expect(dockerCloud.repoSlug).toEqual("artsy/eigen") 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /source/ci_source/get_ci_source.ts: -------------------------------------------------------------------------------- 1 | import { providers } from "./providers" 2 | import * as fs from "fs" 3 | import { resolve } from "path" 4 | import { Env, CISource } from "./ci_source" 5 | 6 | /** 7 | * Gets a CI Source from the current environment, by asking all known 8 | * sources if they can be represented in this environment. 9 | * @param {Env} env The environment. 10 | * @returns {?CISource} a CI source if it's OK, otherwise Danger can't run. 11 | */ 12 | export function getCISourceForEnv(env: Env): CISource | undefined { 13 | const availableProviders = [...(providers as any)].map(Provider => new Provider(env)).filter(x => x.isCI) 14 | return availableProviders && availableProviders.length > 0 ? availableProviders[0] : undefined 15 | } 16 | 17 | /** 18 | * Gets a CI Source from externally provided provider module. 19 | * Module must implement CISource interface, and should export it as default 20 | * @export 21 | * @param {Env} env The environment. 22 | * @param {string} modulePath relative path to CI provider 23 | * @returns {Promise} a CI source if module loaded successfully, undefined otherwise 24 | */ 25 | export async function getCISourceForExternal(env: Env, modulePath: string): Promise { 26 | const path = resolve(process.cwd(), modulePath) 27 | return new Promise(resolve => { 28 | fs.stat(path, (error, stat) => { 29 | if (error) { 30 | console.error(`could not load CI provider at ${modulePath} due to ${error}`) 31 | } 32 | if (stat && stat.isFile()) { 33 | const externalModule = require(path) //tslint:disable-line:no-require-imports 34 | const moduleConstructor = externalModule.default || externalModule 35 | resolve(new moduleConstructor(env)) 36 | } 37 | resolve() 38 | }) 39 | }) 40 | } 41 | 42 | /** 43 | * Gets a CI Source. 44 | * @export 45 | * @param {Env} env The environment. 46 | * @param {string} modulePath relative path to CI provider 47 | * @returns {Promise} a CI source if module loaded successfully, undefined otherwise 48 | */ 49 | export async function getCISource(env: Env, modulePath: string | undefined): Promise { 50 | if (modulePath) { 51 | const external = await getCISourceForExternal(env, modulePath) 52 | if (external) { 53 | return external 54 | } 55 | } 56 | 57 | return getCISourceForEnv(env) 58 | } 59 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_screwdriver.test.ts: -------------------------------------------------------------------------------- 1 | import { Screwdriver } from "../Screwdriver" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | SCREWDRIVER: "true", 6 | SD_PULL_REQUEST: "42", 7 | SCM_URL: "git@github.com:danger/danger-js", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds Screwdriver with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(Screwdriver) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | it("validates when SCREWDRIVER is present in environment", () => { 19 | const screwdriver = new Screwdriver(correctEnv) 20 | expect(screwdriver.isCI).toBeTruthy() 21 | }) 22 | 23 | it("does not validate without SCREWDRIVER present in environment", () => { 24 | const screwdriver = new Screwdriver({}) 25 | expect(screwdriver.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | it("validates when all Screwdriver environment variables are set", () => { 31 | const screwdriver = new Screwdriver(correctEnv) 32 | expect(screwdriver.isPR).toBeTruthy() 33 | }) 34 | 35 | it("does not validate with required environment variables", () => { 36 | const screwdriver = new Screwdriver({}) 37 | expect(screwdriver.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = ["SD_PULL_REQUEST", "SCM_URL"] 41 | envs.forEach((key: string) => { 42 | const env = { 43 | ...correctEnv, 44 | [key]: null, 45 | } 46 | 47 | it(`does not validate when ${key} is missing`, () => { 48 | const screwdriver = new Screwdriver(env) 49 | expect(screwdriver.isPR).toBeFalsy() 50 | }) 51 | }) 52 | 53 | it("needs to have a PR number", () => { 54 | const env = { 55 | ...correctEnv, 56 | SD_PULL_REQUEST: "not a number", 57 | } 58 | const screwdriver = new Screwdriver(env) 59 | expect(screwdriver.isPR).toBeFalsy() 60 | }) 61 | }) 62 | 63 | describe(".pullRequestID", () => { 64 | it("pulls it out of environment", () => { 65 | const screwdriver = new Screwdriver(correctEnv) 66 | expect(screwdriver.pullRequestID).toEqual("42") 67 | }) 68 | }) 69 | 70 | describe(".repoSlug", () => { 71 | it("pulls it out of environment", () => { 72 | const screwdriver = new Screwdriver(correctEnv) 73 | expect(screwdriver.repoSlug).toEqual("danger/danger-js") 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /source/platforms/FakePlatform.ts: -------------------------------------------------------------------------------- 1 | import { GitDSL } from "../dsl/GitDSL" 2 | import { Platform, Comment } from "./platform" 3 | import { readFileSync } from "fs-extra" 4 | 5 | export class FakePlatform implements Platform { 6 | public readonly name: string 7 | 8 | constructor() { 9 | this.name = "Fake" 10 | } 11 | 12 | async getReviewInfo(): Promise { 13 | return {} 14 | } 15 | 16 | async getPlatformDSLRepresentation(): Promise { 17 | return {} 18 | } 19 | 20 | async getPlatformGitRepresentation(): Promise { 21 | return { 22 | modified_files: [], 23 | created_files: [], 24 | deleted_files: [], 25 | diffForFile: async () => ({ before: "", after: "", diff: "", added: "", removed: "" }), 26 | structuredDiffForFile: async () => ({ chunks: [] }), 27 | JSONDiffForFile: async () => ({} as any), 28 | JSONPatchForFile: async () => ({} as any), 29 | commits: [ 30 | { 31 | sha: "123", 32 | author: { name: "1", email: "1", date: "1" }, 33 | committer: { name: "1", email: "1", date: "1" }, 34 | message: "456", 35 | tree: { sha: "123", url: "123" }, 36 | url: "123", 37 | }, 38 | ], 39 | } 40 | } 41 | 42 | async getInlineComments(_: string): Promise { 43 | return [] 44 | } 45 | 46 | supportsCommenting() { 47 | return true 48 | } 49 | 50 | supportsInlineComments() { 51 | return true 52 | } 53 | 54 | async updateOrCreateComment(_dangerID: string, _newComment: string): Promise { 55 | return "https://github.com/orta/github-pages-with-jekyll/pull/5#issuecomment-383402256" 56 | } 57 | 58 | async createComment(_comment: string): Promise { 59 | return true 60 | } 61 | 62 | async createInlineComment(_git: GitDSL, _comment: string, _path: string, _line: number): Promise { 63 | return true 64 | } 65 | 66 | async updateInlineComment(_comment: string, _commentId: string): Promise { 67 | return true 68 | } 69 | 70 | async deleteInlineComment(_id: string): Promise { 71 | return true 72 | } 73 | 74 | async deleteMainComment(): Promise { 75 | return true 76 | } 77 | 78 | async updateStatus(): Promise { 79 | return true 80 | } 81 | 82 | getFileContents = (path: string) => new Promise(res => res(readFileSync(path, "utf8"))) 83 | } 84 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_buildkite.test.ts: -------------------------------------------------------------------------------- 1 | import { Buildkite } from "../Buildkite" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | BUILDKITE: "true", 6 | BUILDKITE_PULL_REQUEST: "800", 7 | BUILDKITE_REPO: "https://github.com/artsy/eigen", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds Buildkite with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(Buildkite) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | it("validates when all Buildkite environment vars are set", () => { 19 | const buildkite = new Buildkite(correctEnv) 20 | expect(buildkite.isCI).toBeTruthy() 21 | }) 22 | 23 | it("does not validate without env", () => { 24 | const buildkite = new Buildkite({}) 25 | expect(buildkite.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | it("validates when all buildkite environment vars are set", () => { 31 | const buildkite = new Buildkite(correctEnv) 32 | expect(buildkite.isPR).toBeTruthy() 33 | }) 34 | 35 | it("does not validate outside of buildkite", () => { 36 | const buildkite = new Buildkite({}) 37 | expect(buildkite.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = ["BUILDKITE_PULL_REQUEST", "BUILDKITE_REPO", "BUILDKITE"] 41 | envs.forEach((key: string) => { 42 | let env = { ...correctEnv } 43 | env[key] = null 44 | 45 | it(`does not validate when ${key} is missing`, () => { 46 | const buildkite = new Buildkite(env) 47 | expect(buildkite.isCI && buildkite.isPR).toBeFalsy() 48 | }) 49 | }) 50 | }) 51 | 52 | describe(".pullRequestID", () => { 53 | it("pulls it out of the env", () => { 54 | const buildkite = new Buildkite({ 55 | BUILDKITE_PULL_REQUEST: "800", 56 | }) 57 | expect(buildkite.pullRequestID).toEqual("800") 58 | }) 59 | }) 60 | 61 | describe(".repoSlug", () => { 62 | it("derives it from the repo URL", () => { 63 | const buildkite = new Buildkite(correctEnv) 64 | expect(buildkite.repoSlug).toEqual("artsy/eigen") 65 | }) 66 | 67 | it("derives it from the repo URL in SSH format", () => { 68 | const env = { 69 | ...correctEnv, 70 | BUILDKITE_REPO: "git@github.com:artsy/eigen.git", 71 | } 72 | const buildkite = new Buildkite(env) 73 | expect(buildkite.repoSlug).toEqual("artsy/eigen") 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /source/runner/_tests/json-to-context.test.ts: -------------------------------------------------------------------------------- 1 | import { DangerContext } from "../Dangerfile" 2 | import { jsonToContext } from "../json-to-context" 3 | 4 | jest.mock("../jsonToDSL.ts") 5 | jest.mock("../Dangerfile") 6 | 7 | /** 8 | * Mock the jsonToDSL function 9 | */ 10 | // tslint:disable-next-line 11 | const jsonToDSLMock = require("../jsonToDSL") 12 | 13 | /** 14 | * Mock the context for danger function 15 | */ 16 | // tslint:disable-next-line 17 | const bar = require("../Dangerfile") 18 | 19 | describe("runner/json-to-context", () => { 20 | let jsonString 21 | let program 22 | let context 23 | beforeEach(async () => { 24 | jsonToDSLMock.jsonToDSL = jest.fn(() => Promise.resolve({ danger: "" })) 25 | bar.contextForDanger = jest.fn(() => Promise.resolve({ danger: "" })) 26 | jsonString = JSON.stringify({ 27 | danger: { 28 | settings: { 29 | github: { 30 | baseURL: "", 31 | }, 32 | cliArgs: {}, 33 | }, 34 | }, 35 | }) 36 | 37 | program = { 38 | base: "develop", 39 | } 40 | }) 41 | 42 | it("should have a function called get context", () => { 43 | expect(jsonToContext).toBeTruthy() 44 | }) 45 | 46 | it("should return a context", async () => { 47 | context = await jsonToContext(jsonString, program) 48 | expect(context).toBeTruthy() 49 | }) 50 | 51 | it("should set the base from the input command", async () => { 52 | context = await jsonToContext(jsonString, program) 53 | expect(context.danger).toEqual("") 54 | }) 55 | 56 | it("should work if no base is set", async () => { 57 | program.base = undefined 58 | await jsonToContext(jsonString, program) 59 | expect(jsonToDSLMock.jsonToDSL).toHaveBeenCalledWith({ 60 | settings: { 61 | github: { 62 | baseURL: "", 63 | }, 64 | cliArgs: {}, 65 | }, 66 | }) 67 | }) 68 | 69 | it("should set the base to develop", async () => { 70 | await jsonToContext(jsonString, program) 71 | expect(jsonToDSLMock.jsonToDSL).toHaveBeenCalledWith({ 72 | settings: { 73 | github: { 74 | baseURL: "", 75 | }, 76 | cliArgs: { 77 | base: "develop", 78 | }, 79 | }, 80 | }) 81 | }) 82 | 83 | it("should call context for danger with dsl", async () => { 84 | await jsonToContext(jsonString, program) 85 | expect(bar.contextForDanger).toHaveBeenCalledWith({ danger: "" }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_buddyBuild.test.ts: -------------------------------------------------------------------------------- 1 | import { BuddyBuild } from "../BuddyBuild" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | BUDDYBUILD_BUILD_ID: "xxx", 6 | BUDDYBUILD_REPO_SLUG: "someone/something", 7 | BUDDYBUILD_PULL_REQUEST: "999", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds BuddyBuild with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(BuddyBuild) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | it("validates when all BuddyBuild environment vars are set", () => { 19 | const buddyBuild = new BuddyBuild(correctEnv) 20 | expect(buddyBuild.isCI).toBeTruthy() 21 | }) 22 | 23 | it("does not validate", () => { 24 | const buddyBuild = new BuddyBuild({}) 25 | expect(buddyBuild.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | it("validates when all BuddyBuild environment vars are set", () => { 31 | const buddyBuild = new BuddyBuild(correctEnv) 32 | expect(buddyBuild.isPR).toBeTruthy() 33 | }) 34 | 35 | it("does not validate outside of BuddyBuild", () => { 36 | const buddyBuild = new BuddyBuild({}) 37 | expect(buddyBuild.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = ["BUDDYBUILD_REPO_SLUG", "BUDDYBUILD_PULL_REQUEST"] 41 | envs.forEach((key: string) => { 42 | let env = { 43 | BUDDYBUILD_REPO_SLUG: "someone/something", 44 | BUDDYBUILD_PULL_REQUEST: "999", 45 | } 46 | env[key] = null 47 | 48 | it(`does not validate when ${key} is missing`, () => { 49 | const buddyBuild = new BuddyBuild(env) 50 | expect(buddyBuild.isPR).toBeFalsy() 51 | }) 52 | 53 | it("needs to have a PR number", () => { 54 | let env = { 55 | BUDDYBUILD_REPO_SLUG: "someone/something", 56 | BUDDYBUILD_PULL_REQUEST: "asdf", 57 | } 58 | const buddyBuild = new BuddyBuild(env) 59 | expect(buddyBuild.isPR).toBeFalsy() 60 | }) 61 | }) 62 | }) 63 | 64 | describe(".pullRequestID", () => { 65 | it("pulls it out of the env", () => { 66 | const buddyBuild = new BuddyBuild({ BUDDYBUILD_PULL_REQUEST: "999" }) 67 | expect(buddyBuild.pullRequestID).toEqual("999") 68 | }) 69 | }) 70 | 71 | describe(".repoSlug", () => { 72 | it("pulls it out of the env", () => { 73 | const buddyBuild = new BuddyBuild({ BUDDYBUILD_REPO_SLUG: "someone/something" }) 74 | expect(buddyBuild.repoSlug).toEqual("someone/something") 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Setup 4 | 5 | ```sh 6 | git clone https://github.com/danger/danger-js.git 7 | cd danger-js 8 | 9 | # if you don't have yarn installed 10 | npm install -g yarn 11 | 12 | yarn install 13 | ``` 14 | 15 | You can then verify your install by running the tests, and the linters: 16 | 17 | ```sh 18 | yarn test 19 | yarn lint 20 | ``` 21 | 22 | The fixers for both tslint and prettier will be applied when you commit, and on a push your code will be verified 23 | that it compiles. 24 | 25 | ### How does Danger JS work? 26 | 27 | Check the [architecture doc](https://github.com/danger/danger-js/blob/master/docs/architecture.md). 28 | 29 | ### What is the TODO? 30 | 31 | Check the issues, I try and keep my short term perspective there. Long term is in the [VISION.md](VISION.md). 32 | 33 | ### Releasing a new version of Danger 34 | 35 | Following [this commit](https://github.com/danger/danger-js/commit/a26ac3b3bd4f002acd37f6a363c8e74c9d5039ab) as a model: 36 | 37 | * Checkout the `master` branch. Ensure your working tree is clean, and make sure you have the latest changes by running `git pull`. 38 | * Update `package.json` with the new version - for the sake of this example, the new version is **0.21.0**. 39 | * Modify `changelog.md`, adding a new `### 0.21.0` heading under the `### Master` heading at the top of the file. 40 | * Commit both changes with the commit message **Version bump**. 41 | * Tag this commit - `git tag 0.21.0`. 42 | * Push the commit and tag to master - `git push origin master --follow-tags`. Travis CI will build the tagged commit and publish that tagged version to NPM. 43 | 44 | :ship: 45 | 46 | ## License, Contributor's Guidelines and Code of Conduct 47 | 48 | We try to keep as much discussion as possible in GitHub issues, but also have a pretty inactive Slack --- if you'd like an invite, ping [@Orta](https://twitter.com/orta/) a DM on Twitter with your email. It's mostly interesting if you want to stay on top of Danger without all the emails from GitHub. 49 | 50 | > This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs. 51 | > 52 | > This project subscribes to the [Moya Contributors Guidelines](https://github.com/Moya/contributors) which TLDR: means we give out push access easily and often. 53 | > 54 | > Contributors subscribe to the [Contributor Code of Conduct](http://contributor-covenant.org/version/1/3/0/) based on the [Contributor Covenant](http://contributor-covenant.org) version 1.3.0. 55 | -------------------------------------------------------------------------------- /source/ci_source/providers/Jenkins.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | // https://jenkins.io/ 5 | // https://wiki.jenkins.io/display/JENKINS/Building+a+software+project#Buildingasoftwareproject-belowJenkinsSetEnvironmentVariables 6 | 7 | /** 8 | * ### CI Setup 9 | * Ah Jenkins, so many memories. So, if you're using Jenkins, you're hosting your own environment. 10 | * 11 | * ### GitHub 12 | * You will want to be using the 13 | * [GitHub pull request builder plugin](https://wiki.jenkins.io/display/JENKINS/GitHub+pull+request+builder+plugin) 14 | * in order to ensure that you have the build environment set up for PR integration. 15 | * 16 | * ### BitBucket Server 17 | * If using Bitbucket Server, make sure to provide both `ghprbGhRepository` and `ghprbPullId` as environment variables. 18 | * `ghprbGhRepository` is the path to your repository, e.g. `projects/team/repos/repositoryname`, while `ghprbPullId` 19 | * provides the id of a pull request (usually `env.CHANGE_ID`). Danger will skip execution if this id is not provided. 20 | * 21 | * With that set up, you can edit your job to add `yarn danger ci` at the build action. 22 | * 23 | * ### Pipeline 24 | * If you're using [pipelines](https://jenkins.io/solutions/pipeline/) you should be using the 25 | * [GitHub branch source plugin](https://wiki.jenkins.io/display/JENKINS/GitHub+Branch+Source+Plugin) for easy setup and handling of PRs. 26 | * 27 | * After you've set up the plugin, add a `sh 'yarn danger ci'` line in your pipeline script and make sure that build PRs is enabled. 28 | * 29 | * ## Token Setup 30 | * 31 | * ### GitHub 32 | * As you own the machine, it's up to you to add the environment variable for the `DANGER_GITHUB_API_TOKEN`. 33 | */ 34 | export class Jenkins implements CISource { 35 | constructor(private readonly env: Env) {} 36 | 37 | get name(): string { 38 | return "Jenkins" 39 | } 40 | 41 | get isCI(): boolean { 42 | return ensureEnvKeysExist(this.env, ["JENKINS_URL"]) 43 | } 44 | 45 | get isPR(): boolean { 46 | const mustHave = ["JENKINS_URL", "ghprbPullId", "ghprbGhRepository"] 47 | const mustBeInts = ["ghprbPullId"] 48 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 49 | } 50 | 51 | get pullRequestID(): string { 52 | return this.env.ghprbPullId 53 | } 54 | 55 | get repoSlug(): string { 56 | return this.env.ghprbGhRepository 57 | } 58 | 59 | get ciRunURL() { 60 | return process.env.BUILD_URL 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/runner/jsonToDSL.ts: -------------------------------------------------------------------------------- 1 | import * as GitHubNodeAPI from "@octokit/rest" 2 | 3 | import { DangerDSLJSONType, DangerDSLType } from "../dsl/DangerDSL" 4 | import { gitHubGitDSL as githubJSONToGitDSL } from "../platforms/github/GitHubGit" 5 | import { githubJSONToGitHubDSL } from "../platforms/GitHub" 6 | import { sentence, href } from "./DangerUtils" 7 | import { LocalGit } from "../platforms/LocalGit" 8 | import { GitDSL } from "../dsl/GitDSL" 9 | import { bitBucketServerGitDSL } from "../platforms/bitbucket_server/BitBucketServerGit" 10 | import { 11 | BitBucketServerAPI, 12 | bitbucketServerRepoCredentialsFromEnv, 13 | } from "../platforms/bitbucket_server/BitBucketServerAPI" 14 | 15 | export const jsonToDSL = async (dsl: DangerDSLJSONType): Promise => { 16 | const api = apiForDSL(dsl) 17 | const platformExists = [dsl.github, dsl.bitbucket_server].some(p => !!p) 18 | const github = dsl.github && githubJSONToGitHubDSL(dsl.github, api as GitHubNodeAPI) 19 | const bitbucket_server = dsl.bitbucket_server 20 | // const gitlab = dsl.gitlab && githubJSONToGitLabDSL(dsl.gitlab, api) 21 | 22 | let git: GitDSL 23 | if (!platformExists) { 24 | const localPlatform = new LocalGit(dsl.settings.cliArgs) 25 | git = await localPlatform.getPlatformGitRepresentation() 26 | } else if (process.env["DANGER_BITBUCKETSERVER_HOST"]) { 27 | git = bitBucketServerGitDSL(bitbucket_server!, dsl.git, api as BitBucketServerAPI) 28 | } else { 29 | git = githubJSONToGitDSL(github!, dsl.git) 30 | } 31 | 32 | return { 33 | git, 34 | // Strictly speaking, this is a lie. Only one of these will _ever_ exist, but 35 | // otherwise everyone would need to have a check for GitHub/BBS in every Dangerfile 36 | // which just doesn't feel right. 37 | github: github!, 38 | bitbucket_server: bitbucket_server!, 39 | utils: { 40 | sentence, 41 | href, 42 | }, 43 | } 44 | } 45 | 46 | const apiForDSL = (dsl: DangerDSLJSONType): GitHubNodeAPI | BitBucketServerAPI => { 47 | if (process.env["DANGER_BITBUCKETSERVER_HOST"]) { 48 | return new BitBucketServerAPI(dsl.bitbucket_server!.metadata, bitbucketServerRepoCredentialsFromEnv(process.env)) 49 | } 50 | 51 | const options: GitHubNodeAPI.Options & { debug: boolean } = { 52 | debug: !!process.env.LOG_FETCH_REQUESTS, 53 | baseUrl: dsl.settings.github.baseURL, 54 | headers: { 55 | ...dsl.settings.github.additionalHeaders, 56 | }, 57 | } 58 | 59 | const api = new GitHubNodeAPI(options) 60 | if (dsl.settings.github && dsl.settings.github.accessToken) { 61 | api.authenticate({ type: "token", token: dsl.settings.github.accessToken }) 62 | } 63 | return api 64 | } 65 | -------------------------------------------------------------------------------- /scripts/danger-dts.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | 3 | const mapLines = (s: string, func: (s: string) => string) => 4 | s 5 | .split("\n") 6 | .map(func) 7 | .join("\n") 8 | 9 | const createDTS = () => { 10 | const header = `// 11 | // Autogenerated from scripts/danger-dts.ts 12 | // 13 | 14 | import * as GitHub from "@octokit/rest" 15 | 16 | ` 17 | const footer = `` 18 | 19 | let fileOutput = "" 20 | 21 | const dslFiles = fs 22 | .readdirSync("source/dsl") 23 | .filter(f => !f.startsWith("_tests")) 24 | .map(f => `source/dsl/${f}`) 25 | 26 | dslFiles.forEach(file => { 27 | // Sometimes they have more stuff, in those cases 28 | // offer a way to crop the file. 29 | const content = fs.readFileSync(file).toString() 30 | if (content.includes("/// End of Danger DSL definition")) { 31 | fileOutput += content.split("/// End of Danger DSL definition")[0] 32 | } else { 33 | fileOutput += content 34 | } 35 | fileOutput += "\n" 36 | }) 37 | 38 | // The definition of all the exposed vars is inside 39 | // the Dangerfile.js file. 40 | const allDangerfile = fs.readFileSync("source/runner/Dangerfile.ts").toString() 41 | const moduleContext = allDangerfile 42 | .split("/// Start of Danger DSL definition")[1] 43 | .split("/// End of Danger DSL definition")[0] 44 | 45 | // we need to add either `declare function` or `declare var` to the interface 46 | const context = mapLines(moduleContext, (line: string) => { 47 | if (line.length === 0 || line.includes("*")) { 48 | const newLine = line.trim() 49 | // Make sure TSLint passes 50 | if (newLine.startsWith("*")) { 51 | return " " + newLine 52 | } 53 | return newLine 54 | } 55 | if (line.includes("export type")) { 56 | return line 57 | } 58 | if (line.includes("(")) { 59 | return "declare function " + line.trim() 60 | } 61 | if (line.includes(":")) { 62 | return "declare const " + line.trim() 63 | } 64 | return "" 65 | }) 66 | 67 | fileOutput += context 68 | 69 | // Remove all JS-y bits 70 | fileOutput = fileOutput 71 | .split("\n") 72 | .filter(line => { 73 | return !line.startsWith("import") && !line.includes("* @type ") 74 | }) 75 | .join("\n") 76 | 77 | const trimmedWhitespaceLines = fileOutput.replace(/\n\s*\n\s*\n/g, "\n") 78 | const noRedundantExports = trimmedWhitespaceLines 79 | .replace(/export interface/g, "interface") 80 | .replace(/export type/g, "type") 81 | const indentedBody = mapLines(noRedundantExports, line => (line.length ? line : "")) 82 | return header + indentedBody + footer 83 | } 84 | 85 | export default createDTS 86 | -------------------------------------------------------------------------------- /source/platforms/github/_tests/__snapshots__/_github_git.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`the dangerfile gitDSL should include \`added\` text content of the file 1`] = `"+\\"Example/Build\\",+\\"externals/\\""`; 4 | 5 | exports[`the dangerfile gitDSL should include \`after\` text content of the file 1`] = `"{\\"compilerOptions\\":{\\"allowJs\\":true},\\"exclude\\":[\\"node_modules\\",\\"Pod/Assets\\",\\"Example/Build\\",\\"externals/\\"]}"`; 6 | 7 | exports[`the dangerfile gitDSL should include \`before\` text content of the file 1`] = `"{\\"compilerOptions\\":{\\"allowJs\\":true},\\"exclude\\":[\\"node_modules\\",\\"Pod/Assets\\",\\"Example/Build\\"]}"`; 8 | 9 | exports[`the dangerfile gitDSL should include \`removed\` text content of the file 1`] = `"-\\"Example/Build\\""`; 10 | 11 | exports[`the dangerfile gitDSL show diff chunks for a specific file 1`] = ` 12 | Array [ 13 | Object { 14 | "changes": Array [ 15 | Object { 16 | "content": " \\"exclude\\": [", 17 | "ln1": 5, 18 | "ln2": 5, 19 | "normal": true, 20 | "type": "normal", 21 | }, 22 | Object { 23 | "content": " \\"node_modules\\",", 24 | "ln1": 6, 25 | "ln2": 6, 26 | "normal": true, 27 | "type": "normal", 28 | }, 29 | Object { 30 | "content": " \\"Pod/Assets\\",", 31 | "ln1": 7, 32 | "ln2": 7, 33 | "normal": true, 34 | "type": "normal", 35 | }, 36 | Object { 37 | "content": "- \\"Example/Build\\"", 38 | "del": true, 39 | "ln": 8, 40 | "type": "del", 41 | }, 42 | Object { 43 | "add": true, 44 | "content": "+ \\"Example/Build\\",", 45 | "ln": 8, 46 | "type": "add", 47 | }, 48 | Object { 49 | "add": true, 50 | "content": "+ \\"externals/\\"", 51 | "ln": 9, 52 | "type": "add", 53 | }, 54 | Object { 55 | "content": " ]", 56 | "ln1": 9, 57 | "ln2": 10, 58 | "normal": true, 59 | "type": "normal", 60 | }, 61 | Object { 62 | "content": " }", 63 | "ln1": 10, 64 | "ln2": 11, 65 | "normal": true, 66 | "type": "normal", 67 | }, 68 | ], 69 | "content": "@@ -5,6 +5,7 @@", 70 | "newLines": 7, 71 | "newStart": 5, 72 | "oldLines": 6, 73 | "oldStart": 5, 74 | }, 75 | ] 76 | `; 77 | 78 | exports[`the dangerfile gitDSL shows the diff for a specific file 1`] = `"\\"exclude\\":[\\"node_modules\\",\\"Pod/Assets\\",-\\"Example/Build\\"+\\"Example/Build\\",+\\"externals/\\"]}"`; 79 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_travis.test.ts: -------------------------------------------------------------------------------- 1 | import { Travis } from "../Travis" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | HAS_JOSH_K_SEAL_OF_APPROVAL: "true", 6 | TRAVIS_PULL_REQUEST: "800", 7 | TRAVIS_REPO_SLUG: "artsy/eigen", 8 | TRAVIS_JOB_ID: "317790046", 9 | } 10 | 11 | describe("being found when looking for CI", () => { 12 | it("finds Travis with the right ENV", () => { 13 | const ci = getCISourceForEnv(correctEnv) 14 | expect(ci).toBeInstanceOf(Travis) 15 | }) 16 | }) 17 | 18 | describe(".isCI", () => { 19 | test("validates when all Travis environment vars are set and Josh K says so", () => { 20 | const travis = new Travis(correctEnv) 21 | expect(travis.isCI).toBeTruthy() 22 | }) 23 | 24 | test("does not validate without josh", () => { 25 | const travis = new Travis({}) 26 | expect(travis.isCI).toBeFalsy() 27 | }) 28 | }) 29 | 30 | describe(".isPR", () => { 31 | test("validates when all Travis environment vars are set and Josh K says so", () => { 32 | const travis = new Travis(correctEnv) 33 | expect(travis.isPR).toBeTruthy() 34 | }) 35 | 36 | test("does not validate without josh", () => { 37 | const travis = new Travis({}) 38 | expect(travis.isPR).toBeFalsy() 39 | }) 40 | 41 | const envs = ["TRAVIS_PULL_REQUEST", "TRAVIS_REPO_SLUG"] 42 | envs.forEach((key: string) => { 43 | let env = { 44 | HAS_JOSH_K_SEAL_OF_APPROVAL: "true", 45 | TRAVIS_PULL_REQUEST: "800", 46 | TRAVIS_REPO_SLUG: "artsy/eigen", 47 | } 48 | env[key] = null 49 | 50 | test(`does not validate when ${key} is missing`, () => { 51 | const travis = new Travis(env) 52 | expect(travis.isPR).toBeFalsy() 53 | }) 54 | }) 55 | 56 | it("needs to have a PR number", () => { 57 | let env = { 58 | HAS_JOSH_K_SEAL_OF_APPROVAL: "true", 59 | TRAVIS_PULL_REQUEST: "asdasd", 60 | TRAVIS_REPO_SLUG: "artsy/eigen", 61 | } 62 | const travis = new Travis(env) 63 | expect(travis.isPR).toBeFalsy() 64 | }) 65 | }) 66 | 67 | describe(".pullRequestID", () => { 68 | it("pulls it out of the env", () => { 69 | const travis = new Travis(correctEnv) 70 | expect(travis.pullRequestID).toEqual("800") 71 | }) 72 | }) 73 | 74 | describe(".repoSlug", () => { 75 | it("pulls it out of the env", () => { 76 | const travis = new Travis(correctEnv) 77 | expect(travis.repoSlug).toEqual("artsy/eigen") 78 | }) 79 | }) 80 | 81 | describe(".ciRunURL", () => { 82 | it("pulls it out of the env", () => { 83 | const travis = new Travis(correctEnv) 84 | expect(travis.ciRunURL).toEqual("https://travis-ci.org/artsy/eigen/jobs/317790046") 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /source/api/_tests/fetch.test.ts: -------------------------------------------------------------------------------- 1 | // import * as jest from 'jest'; 2 | import * as http from "http" 3 | 4 | import { api } from "../fetch" 5 | 6 | interface ResponseMock { 7 | body?: any 8 | statusCode?: number 9 | contentType?: string 10 | } 11 | 12 | class TestServer { 13 | private port = 30001 14 | private hostname = "localhost" 15 | private response: ResponseMock = null 16 | private router = (req, res) => { 17 | res.statusCode = this.response && this.response.statusCode ? this.response.statusCode : 200 18 | res.setHeader( 19 | "Content-Type", 20 | this.response && this.response.contentType ? this.response.contentType : "application/json" 21 | ) 22 | res.end(this.response ? this.response.body : null) 23 | } 24 | private server = http.createServer(this.router) 25 | 26 | start = async (response: ResponseMock): Promise => { 27 | this.response = response 28 | return new Promise((resolve, reject) => { 29 | this.server.listen(this.port, this.hostname, err => (err ? reject(err) : resolve())) 30 | }) 31 | } 32 | stop = async (): Promise => { 33 | this.response = null 34 | return new Promise((resolve, reject) => { 35 | this.server.close(err => (err ? reject(err) : resolve())) 36 | }) 37 | } 38 | } 39 | 40 | describe("fetch", () => { 41 | let url: string 42 | let server = new TestServer() 43 | 44 | beforeEach(() => { 45 | url = "http://localhost:30001/" 46 | }) 47 | 48 | afterEach(async () => { 49 | await server.stop() 50 | }) 51 | 52 | it("handles json success", async () => { 53 | let body = { key: "valid json" } 54 | await server.start({ 55 | body: JSON.stringify(body), 56 | }) 57 | 58 | let response = await api(url, {}) 59 | expect(response.ok).toBe(true) 60 | expect(response.status).toBe(200) 61 | expect(await response.json()).toMatchObject(body) 62 | }) 63 | 64 | it("handles json error", async () => { 65 | let body = { key: "valid json" } 66 | await server.start({ 67 | body: JSON.stringify(body), 68 | statusCode: 500, 69 | }) 70 | 71 | let response = await api(url, {}) 72 | expect(response.ok).toBe(false) 73 | expect(response.status).toBe(500) 74 | expect(await response.json()).toMatchObject(body) 75 | }) 76 | 77 | it("handles plain text error", async () => { 78 | let body = "any plain text response" 79 | await server.start({ 80 | body: body, 81 | statusCode: 500, 82 | contentType: "text/plain", 83 | }) 84 | 85 | let response = await api(url, {}) 86 | expect(response.ok).toBe(false) 87 | expect(response.status).toBe(500) 88 | expect(await response.text()).toBe(body) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /docs/tutorials/transpilation.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Danger + Transpilation 3 | subtitle: Danger + Transpilation 4 | layout: guide_js 5 | order: 3 6 | blurb: How Danger's TypeScript/Babel integration works. 7 | --- 8 | 9 | ### Transpilation 10 | 11 | Danger tries to pick up either Babel or TypeScript up at runtime. It does this by `require`ing both dependencies, which 12 | will follow the standard [NodeJS require resolution](https://nodejs.org/api/modules.html#modules_all_together). If 13 | either don't exist, then the Dangerfile will be treated as not needing transpilation and passed directly to the node 14 | runtime. 15 | 16 | A few notes: 17 | 18 | - TypeScript is prioritized over Babel 19 | - Babel 7 support for TypeScript is supported 20 | - Whether you use `dangerfile.ts` or `dangerfile.js` is irrelevant, the environment matters more 21 | 22 | ### TypeScript gotchas 23 | 24 | You might have a `src` folder where your actual source code is kept, and adding a `dangerfile.ts` at the root which will 25 | break compilation. The answer to this is to add the dangerfile to the `"exclude"` section. Then to get inline errors 26 | working correct, add it to the `"include"`. It's a neat little trick. You can see it working in 27 | [artsy/emission#tsconfig.json][tsconfig] 28 | 29 | ```json 30 | { 31 | "compilerOptions": {}, 32 | "include": ["src/**/*.ts", "src/**/*.tsx", "dangerfile.ts"], 33 | "exclude": ["dangerfile.ts", "node_modules"] 34 | } 35 | ``` 36 | 37 | ### The "danger" module 38 | 39 | The `danger` module is removed before evaluation, it's only there to fake your dev env into working correctly. In 40 | reality, all of the exports are added to the global environment. If you import `"danger"` in code that isn't evaluated 41 | inside Danger itself, it will raise an exception. 42 | 43 | You can use something like jest's module mocking system to fake it in tests, letting you manipulate the object 44 | `danger.github.pr` to look like whatever you want in tests: 45 | 46 | ```js 47 | jest.mock("danger", () => jest.fn()) 48 | import * as danger from "danger" 49 | const dm = danger as any 50 | 51 | import { rfc5 } from "../org/all-prs" 52 | 53 | beforeEach(() => { 54 | dm.fail = jest.fn() 55 | }) 56 | 57 | it("fails when there's no PR body", () => { 58 | dm.danger = { github: { pr: { body: "" } } } 59 | return rfc5().then(() => { 60 | expect(dm.fail).toHaveBeenCalledWith("Please add a description to your PR.") 61 | }) 62 | }) 63 | 64 | it("does nothing when there's a PR body", () => { 65 | dm.danger = { github: { pr: { body: "Hello world" } } } 66 | return rfc5().then(() => { 67 | expect(dm.fail).not.toHaveBeenCalled() 68 | }) 69 | }) 70 | ``` 71 | 72 | [tsconfig]: https://github.com/artsy/emission/blob/master/tsconfig.json 73 | -------------------------------------------------------------------------------- /source/ci_source/providers/Circle.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * For setting up Circle CI, we recommend turning on "Only Build pull requests." in "Advanced Setting." Without this enabled, 8 | * it is _really_ tricky for Danger to know whether you are in a pull request or not, as the environment metadata 9 | * isn't reliable. This may be different with Circle v2. 10 | * 11 | * With that set up, you can you add `yarn danger ci` to your `circle.yml`. If you override the default 12 | * `test:` section, then add it as an extra step. Otherwise add a new `pre` section to the test: 13 | * 14 | * ``` ruby 15 | * test: 16 | * override: 17 | * - yarn danger ci 18 | * ``` 19 | * 20 | * ### Token Setup 21 | * 22 | * There is no difference here for OSS vs Closed, add your `DANGER_GITHUB_API_TOKEN` to the Environment variable settings page. 23 | * 24 | */ 25 | export class Circle implements CISource { 26 | constructor(private readonly env: Env) {} 27 | 28 | get name(): string { 29 | return "Circle CI" 30 | } 31 | 32 | get isCI(): boolean { 33 | return ensureEnvKeysExist(this.env, ["CIRCLE_BUILD_NUM"]) 34 | } 35 | 36 | get isPR(): boolean { 37 | if (ensureEnvKeysExist(this.env, ["CI_PULL_REQUEST"]) || ensureEnvKeysExist(this.env, ["CIRCLE_PULL_REQUEST"])) { 38 | return true 39 | } 40 | 41 | const mustHave = ["CIRCLE_CI_API_TOKEN", "CIRCLE_PROJECT_USERNAME", "CIRCLE_PROJECT_REPONAME", "CIRCLE_BUILD_NUM"] 42 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, ["CIRCLE_PR_NUMBER"]) 43 | } 44 | 45 | private _prParseURL(): { owner?: string; reponame?: string; id?: string } { 46 | const prUrl = this.env.CI_PULL_REQUEST || this.env.CIRCLE_PULL_REQUEST || "" 47 | const splitSlug = prUrl.split("/") 48 | if (splitSlug.length === 7) { 49 | const owner = splitSlug[3] 50 | const reponame = splitSlug[4] 51 | const id = splitSlug[6] 52 | return { owner, reponame, id } 53 | } 54 | return {} 55 | } 56 | 57 | get pullRequestID(): string { 58 | if (this.env.CIRCLE_PR_NUMBER) { 59 | return this.env.CIRCLE_PR_NUMBER 60 | } else { 61 | const { id } = this._prParseURL() 62 | return id || "" 63 | } 64 | } 65 | 66 | get repoSlug(): string { 67 | const { owner, reponame } = this._prParseURL() 68 | return owner && reponame ? `${owner}/${reponame}` : "" 69 | } 70 | 71 | get repoURL(): string { 72 | return this.env.CIRCLE_REPOSITORY_URL 73 | } 74 | 75 | get ciRunURL() { 76 | return this.env["CIRCLE_BUILD_URL"] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_semaphore.test.ts: -------------------------------------------------------------------------------- 1 | import { Semaphore } from "../Semaphore" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | SEMAPHORE: "Yep", 6 | SEMAPHORE_REPO_SLUG: "artsy/eigen", 7 | PULL_REQUEST_NUMBER: "800", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds Semaphore with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(Semaphore) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | test("validates when all Semaphore environment vars are set", () => { 19 | const semaphore = new Semaphore(correctEnv) 20 | expect(semaphore.isCI).toBeTruthy() 21 | }) 22 | 23 | test("does not validate without josh", () => { 24 | const semaphore = new Semaphore({}) 25 | expect(semaphore.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | test("validates when all semaphore environment vars are set", () => { 31 | const semaphore = new Semaphore(correctEnv) 32 | expect(semaphore.isPR).toBeTruthy() 33 | }) 34 | 35 | test("does not validate outside of semaphore", () => { 36 | const semaphore = new Semaphore({}) 37 | expect(semaphore.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = [ 41 | "SEMAPHORE_CI_API_TOKEN", 42 | "SEMAPHORE_PROJECT_USERNAME", 43 | "SEMAPHORE_PROJECT_REPONAME", 44 | "SEMAPHORE_BUILD_NUM", 45 | ] 46 | envs.forEach((key: string) => { 47 | let env = { 48 | SEMAPHORE_CI_API_TOKEN: "xxx", 49 | SEMAPHORE_PROJECT_USERNAME: "semaphore_org", 50 | SEMAPHORE_PROJECT_REPONAME: "someproject", 51 | SEMAPHORE_BUILD_NUM: "1501", 52 | SEMAPHORE_PR_NUMBER: "800", 53 | CI_PULL_REQUEST: "https://github.com/artsy/eigen/pull/800", 54 | } 55 | env[key] = null 56 | 57 | test(`does not validate when ${key} is missing`, () => { 58 | const semaphore = new Semaphore({}) 59 | expect(semaphore.isPR).toBeFalsy() 60 | }) 61 | }) 62 | 63 | it("needs to have a PR number", () => { 64 | let env = { 65 | SEMAPHORE_PR_NUMBER: "asdasd", 66 | SEMAPHORE_REPO_SLUG: "artsy/eigen", 67 | } 68 | const semaphore = new Semaphore(env) 69 | expect(semaphore.isPR).toBeFalsy() 70 | }) 71 | }) 72 | 73 | describe(".pullRequestID", () => { 74 | it("pulls it out of the env", () => { 75 | const semaphore = new Semaphore({ PULL_REQUEST_NUMBER: "800" }) 76 | expect(semaphore.pullRequestID).toEqual("800") 77 | }) 78 | }) 79 | 80 | describe(".repoSlug", () => { 81 | it("derives it from the PR Url", () => { 82 | const semaphore = new Semaphore(correctEnv) 83 | expect(semaphore.repoSlug).toEqual("artsy/eigen") 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /source/platforms/GitHub.ts: -------------------------------------------------------------------------------- 1 | import { GitHubPRDSL, GitHubDSL, GitHubAPIPR, GitHubJSONDSL } from "../dsl/GitHubDSL" 2 | import { GitHubAPI } from "./github/GitHubAPI" 3 | import GitHubUtils from "./github/GitHubUtils" 4 | import gitDSLForGitHub from "./github/GitHubGit" 5 | 6 | import * as NodeGitHub from "@octokit/rest" 7 | import { Platform } from "./platform" 8 | 9 | import { GitHubIssueCommenter } from "./github/comms/issueCommenter" 10 | import { GitHubChecksCommenter } from "./github/comms/checksCommenter" 11 | 12 | /** Handles conforming to the Platform Interface for GitHub, API work is handle by GitHubAPI */ 13 | 14 | export type GitHubType = Platform & { api: GitHubAPI } 15 | 16 | export const GitHub = (api: GitHubAPI) => { 17 | /** 18 | * Converts the PR JSON into something easily used by the Github API client. 19 | */ 20 | const APIMetadataForPR = (pr: GitHubPRDSL): GitHubAPIPR => { 21 | return { 22 | number: pr.number, 23 | repo: pr.base.repo.name, 24 | owner: pr.base.repo.owner.login, 25 | } 26 | } 27 | 28 | /** A quick one off func to ensure there's always some labels */ 29 | const getIssue = async () => { 30 | const issue = await api.getIssue() 31 | return issue || { labels: [] } 32 | } 33 | 34 | return { 35 | name: "GitHub", 36 | 37 | api, 38 | getReviewInfo: api.getPullRequestInfo, 39 | getPlatformGitRepresentation: () => gitDSLForGitHub(api), 40 | 41 | getPlatformDSLRepresentation: async () => { 42 | let pr: GitHubPRDSL 43 | try { 44 | pr = await api.getPullRequestInfo() 45 | } catch { 46 | process.exitCode = 1 47 | throw ` 48 | Could not find pull request information, 49 | if you are using a private repo then perhaps 50 | Danger does not have permission to access that repo. 51 | ` 52 | } 53 | 54 | const issue = await getIssue() 55 | const commits = await api.getPullRequestCommits() 56 | const reviews = await api.getReviews() 57 | const requested_reviewers = await api.getReviewerRequests() 58 | 59 | const thisPR = APIMetadataForPR(pr) 60 | return { 61 | issue, 62 | pr, 63 | commits, 64 | reviews, 65 | requested_reviewers, 66 | thisPR, 67 | } 68 | }, 69 | 70 | ...GitHubIssueCommenter(api), 71 | ...(GitHubChecksCommenter(api) || {}), 72 | 73 | getFileContents: api.fileContents, 74 | } as GitHubType 75 | } 76 | 77 | // This class should get un-classed, but for now we can expand by functions 78 | export const githubJSONToGitHubDSL = (gh: GitHubJSONDSL, api: NodeGitHub): GitHubDSL => { 79 | return { 80 | ...gh, 81 | api, 82 | utils: GitHubUtils(gh.pr, api), 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_teamcity.test.ts: -------------------------------------------------------------------------------- 1 | import { TeamCity } from "../TeamCity" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | TEAMCITY_VERSION: "1.2.3", 6 | PULL_REQUEST_URL: "https://github.com/danger/danger-js/pull/541", 7 | } 8 | 9 | describe("being found when looking for CI", () => { 10 | it("finds TeamCity with the right ENV", () => { 11 | const ci = getCISourceForEnv(correctEnv) 12 | expect(ci).toBeInstanceOf(TeamCity) 13 | }) 14 | }) 15 | 16 | describe(".isCI", () => { 17 | it("validates when all TeamCity environment vars are set", () => { 18 | const teamcity = new TeamCity(correctEnv) 19 | expect(teamcity.isCI).toBeTruthy() 20 | }) 21 | 22 | it("does not validate without env", () => { 23 | const teamcity = new TeamCity({}) 24 | expect(teamcity.isCI).toBeFalsy() 25 | }) 26 | }) 27 | 28 | describe(".isPR", () => { 29 | it("validates when all TeamCity environment vars are set", () => { 30 | const teamcity = new TeamCity(correctEnv) 31 | expect(teamcity.isPR).toBeTruthy() 32 | }) 33 | 34 | it("does not validate outside of TeamCity", () => { 35 | const teamcity = new TeamCity({}) 36 | expect(teamcity.isPR).toBeFalsy() 37 | }) 38 | 39 | const envs = ["TEAMCITY_VERSION", "PULL_REQUEST_URL"] 40 | envs.forEach((key: string) => { 41 | let env = { 42 | TEAMCITY_VERSION: "1.2.3", 43 | PULL_REQUEST_URL: "https://github.com/danger/danger-js/pull/541", 44 | } 45 | env[key] = null 46 | 47 | it(`does not validate when ${key} is missing`, () => { 48 | const teamcity = new TeamCity({}) 49 | expect(teamcity.isPR).toBeFalsy() 50 | }) 51 | }) 52 | }) 53 | 54 | describe(".pullRequestID", () => { 55 | it("pulls it out of the env", () => { 56 | const teamcity = new TeamCity({ 57 | PULL_REQUEST_URL: "https://github.com/danger/danger-js/pull/541", 58 | }) 59 | expect(teamcity.pullRequestID).toEqual("541") 60 | }) 61 | 62 | it("pulls it out of the env for Bitbucket Server", () => { 63 | const teamcity = new TeamCity({ 64 | PULL_REQUEST_URL: "https://stash.test.com/projects/POR/repos/project/pull-requests/32304/overview", 65 | }) 66 | 67 | expect(teamcity.pullRequestID).toEqual("32304") 68 | }) 69 | }) 70 | 71 | describe(".repoSlug", () => { 72 | it("derives it from the PR Url", () => { 73 | const teamcity = new TeamCity(correctEnv) 74 | expect(teamcity.repoSlug).toEqual("danger/danger-js") 75 | }) 76 | 77 | it("derives it from the PR Url for Bitbucket Server", () => { 78 | const teamcity = new TeamCity({ 79 | PULL_REQUEST_URL: "https://stash.test.com/projects/POR/repos/project/pull-requests/32304/overview", 80 | }) 81 | 82 | expect(teamcity.repoSlug).toEqual("projects/POR/repos/project") 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /docs/tutorials/fast-feedback.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fast Feedback via Danger Local 3 | subtitle: Platformless 4 | layout: guide_js 5 | order: 4 6 | blurb: How to use Danger to get per-commit feedback 7 | --- 8 | 9 | ## Before we get started 10 | 11 | This tutorial continues after "[Getting Started][started]" - it's not required that you have `danger ci` set up though. 12 | 13 | ## Locality 14 | 15 | With Danger, the typical flow is to help you can check rules on CI and get feedback inside your PR. With Peril you can 16 | move those rules to run on an external server making feedback instant. `danger local` provides a somewhat hybrid 17 | approach. 18 | 19 | `danger local` provides a way to run a Dangerfile based on git-hooks. This let's you run rules while you are still in 20 | the same context as your work as opposed to later during the code review. Personally, I find this most useful on 21 | projects when I ship 90% of the code to it. 22 | 23 | ## How it works 24 | 25 | Where `danger ci` uses information from the Pull Request to figure out what has changed, `danger local` naively uses the 26 | local differences in git from master to the current commit to derive the runtime environment. This is naive because if 27 | you don't keep your master branch sync, then it will be checking across potentially many branches. 28 | 29 | Inside a Dangerfile `danger.github` and `danger.bitbucket` will be falsy in this context, so you can share a Dangerfile 30 | between `danger local` and `danger ci` as long as you verify that these objects exist before using them. 31 | 32 | When I thought about how I wanted to use `danger local` on repos in the Danger org, I opted to make a separate 33 | Dangerfile for `danger local` and import this at the end of the main Dangerfile. This new Dangerfile only contains rules 34 | which can run with just `danger.git`, e.g. CHANGELOG/README checks. I called it `dangerfile.lite.ts`. 35 | 36 | ## Getting it set up 37 | 38 | You need to add both Danger and [husky](https://www.npmjs.com/package/husky) to your project: 39 | 40 | ```sh 41 | yarn add --dev danger husky 42 | ``` 43 | 44 | When husky is in your dependencies, git-hooks are set up to respond according to matching names in the `"scripts"` 45 | section of your `package.json`. We want to use [a pre-push](https://git-scm.com/docs/githooks#_pre_push) hook to let 46 | `danger local` run before code has been submitted. 47 | 48 | ```json 49 | "scripts": { 50 | "prepush": "yarn build; yarn danger:prepush", 51 | "danger:prepush": "yarn danger local --dangerfile dangerfile.lite.ts" 52 | // [...] 53 | ``` 54 | 55 | Yes, it's a `pre-push` hook and the script is `prepush`, husky 56 | [removes the dashes](https://github.com/typicode/husky/blob/master/HOOKS.md#hooks). If `master` isn't the branch which 57 | you want as a reference then you can use `--base dev` to change the comparison base. 58 | -------------------------------------------------------------------------------- /source/api/fetch.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "../debug" 2 | import * as node_fetch from "node-fetch" 3 | 4 | const d = debug("networking") 5 | declare const global: any 6 | 7 | /** 8 | * Adds logging to every fetch request if a global var for `verbose` is set to true 9 | * 10 | * @param {(string | fetch.Request)} url the request 11 | * @param {fetch.RequestInit} [init] the usual options 12 | * @returns {Promise} network-y promise 13 | */ 14 | export function api( 15 | url: string | node_fetch.Request, 16 | init: node_fetch.RequestInit, 17 | suppressErrorReporting?: boolean 18 | ): Promise { 19 | const isTests = typeof jest !== "undefined" 20 | if (isTests && !url.toString().includes("localhost")) { 21 | const message = `No API calls in tests please: ${url}` 22 | debugger // tslint:disable-line 23 | throw new Error(message) 24 | } 25 | 26 | if (global.verbose && global.verbose === true) { 27 | const output = ["curl", "-i"] 28 | 29 | if (init.method) { 30 | output.push(`-X ${init.method}`) 31 | } 32 | 33 | const showToken = process.env["DANGER_VERBOSE_SHOW_TOKEN"] 34 | const token = process.env["DANGER_GITHUB_API_TOKEN"] 35 | 36 | if (init.headers) { 37 | for (const prop in init.headers) { 38 | if (init.headers.hasOwnProperty(prop)) { 39 | // Don't show the token for normal verbose usage 40 | if (init.headers[prop].includes(token) && !showToken) { 41 | output.push("-H", `"${prop}: [API TOKEN]"`) 42 | continue 43 | } 44 | output.push("-H", `"${prop}: ${init.headers[prop]}"`) 45 | } 46 | } 47 | } 48 | 49 | if (init.method === "POST") { 50 | // const body:string = init.body 51 | // output.concat([init.body]) 52 | } 53 | 54 | if (typeof url === "string") { 55 | output.push(url) 56 | } 57 | 58 | d(output.join(" ")) 59 | } 60 | const originalFetch: any = node_fetch 61 | return originalFetch(url, init).then(async (response: node_fetch.Response) => { 62 | // Handle failing errors 63 | if (!suppressErrorReporting && !response.ok) { 64 | // we should not modify the response when an error occur to allow body stream to be read again if needed 65 | let clonedResponse = response.clone() 66 | console.warn(`Request failed [${clonedResponse.status}]: ${clonedResponse.url}`) 67 | let responseBody = await clonedResponse.text() 68 | try { 69 | // tries to pretty print the JSON response when possible 70 | const responseJSON = await JSON.parse(responseBody.toString()) 71 | console.warn(`Response: ${JSON.stringify(responseJSON, null, " ")}`) 72 | } catch (e) { 73 | console.warn(`Response: ${responseBody}`) 74 | } 75 | } 76 | 77 | return response 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /source/commands/utils/runDangerSubprocess.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "../../debug" 2 | import * as path from "path" 3 | import { spawn } from "child_process" 4 | 5 | import { DangerDSLJSONType, DangerJSON } from "../../dsl/DangerDSL" 6 | import { Executor } from "../../runner/Executor" 7 | import { jsonToDSL } from "../../runner/jsonToDSL" 8 | import { markdownCode, resultsWithFailure, mergeResults } from "./reporting" 9 | 10 | const d = debug("runDangerSubprocess") 11 | 12 | // Sanitizes the DSL so for sending via STDOUT 13 | export const prepareDangerDSL = (dangerDSL: DangerDSLJSONType) => { 14 | if (dangerDSL.github && dangerDSL.github.api) { 15 | delete dangerDSL.github.api 16 | } 17 | 18 | const dangerJSONOutput: DangerJSON = { danger: dangerDSL } 19 | return JSON.stringify(dangerJSONOutput, null, " ") + "\n" 20 | } 21 | 22 | // Runs the Danger process, can either take a simpl 23 | const runDangerSubprocess = (subprocessName: string[], dslJSON: DangerDSLJSONType, exec: Executor) => { 24 | let processName = subprocessName[0] 25 | let args = subprocessName 26 | let results = {} as any 27 | args.shift() // mutate and remove the first element 28 | 29 | const processDisplayName = path.basename(processName) 30 | const dslJSONString = prepareDangerDSL(dslJSON) 31 | d(`Running subprocess: ${processDisplayName} - ${args}`) 32 | const child = spawn(processName, args, { env: process.env }) 33 | let allLogs = "" 34 | 35 | child.stdin.write(dslJSONString) 36 | child.stdin.end() 37 | 38 | child.stdout.on("data", async data => { 39 | data = data.toString() 40 | const trimmed = data.trim() 41 | if ( 42 | trimmed.startsWith("{") && 43 | trimmed.endsWith("}") && 44 | trimmed.includes("markdowns") && 45 | trimmed.includes("fails") && 46 | trimmed.includes("warnings") 47 | ) { 48 | d("Got JSON results from STDOUT, results: \n" + trimmed) 49 | results = JSON.parse(trimmed) 50 | } else { 51 | console.log(`${data}`) 52 | allLogs += data 53 | } 54 | }) 55 | 56 | child.stderr.on("data", data => { 57 | if (data.toString().trim().length !== 0) { 58 | console.log(`${data}`) 59 | } 60 | }) 61 | 62 | child.on("close", async code => { 63 | d(`child process exited with code ${code}`) 64 | // Submit an error back to the PR 65 | if (code) { 66 | d(`Handling fail from subprocess`) 67 | process.exitCode = code 68 | 69 | const failResults = resultsWithFailure(`${processDisplayName}\` failed.`, "### Log\n\n" + markdownCode(allLogs)) 70 | if (results) { 71 | results = mergeResults(results, failResults) 72 | } else { 73 | results = failResults 74 | } 75 | } 76 | const danger = await jsonToDSL(dslJSON) 77 | await exec.handleResults(results, danger.git) 78 | }) 79 | } 80 | 81 | export default runDangerSubprocess 82 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_vsts.test.ts: -------------------------------------------------------------------------------- 1 | import { VSTS } from "../VSTS" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const PRNum = "2398" 5 | const correctEnv = { 6 | SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: "https://test.visualstudio.com/", 7 | BUILD_REPOSITORY_PROVIDER: "GitHub", 8 | BUILD_REASON: "PullRequest", 9 | BUILD_REPOSITORY_NAME: "artsy/eigen", 10 | BUILD_SOURCEBRANCH: `refs/pull/${PRNum}/merge`, 11 | } 12 | 13 | describe("being found when looking for CI", () => { 14 | it("finds VSTS with the right ENV", () => { 15 | const ci = getCISourceForEnv(correctEnv) 16 | expect(ci).toBeInstanceOf(VSTS) 17 | }) 18 | }) 19 | 20 | describe(".isCI", () => { 21 | test("validates when all VSTS environment vars are set", () => { 22 | const vsts = new VSTS(correctEnv) 23 | expect(vsts.isCI).toBeTruthy() 24 | }) 25 | 26 | test("does not validate without environment vars", () => { 27 | const vsts = new VSTS({}) 28 | expect(vsts.isCI).toBeFalsy() 29 | }) 30 | 31 | test("does not validate without the repository provider being set to github", () => { 32 | const vsts = new VSTS({ ...correctEnv, BUILD_REPOSITORY_PROVIDER: "VSTS" }) 33 | expect(vsts.isCI).toBeFalsy() 34 | }) 35 | }) 36 | 37 | describe(".isPR", () => { 38 | test("validates when all VSTS environment vars are set", () => { 39 | const vsts = new VSTS(correctEnv) 40 | expect(vsts.isPR).toBeTruthy() 41 | }) 42 | 43 | test("does not validate without environment vars", () => { 44 | const vsts = new VSTS({}) 45 | expect(vsts.isPR).toBeFalsy() 46 | }) 47 | 48 | const envs = ["BUILD_SOURCEBRANCH", "BUILD_REPOSITORY_PROVIDER", "BUILD_REASON", "BUILD_REPOSITORY_NAME"] 49 | envs.forEach((key: string) => { 50 | let env = { ...correctEnv, [key]: null } 51 | 52 | test(`does not validate when ${key} is missing`, () => { 53 | const vsts = new VSTS(env) 54 | expect(vsts.isPR).toBeFalsy() 55 | }) 56 | }) 57 | 58 | it("needs to have a PR number", () => { 59 | let env = { ...correctEnv, BUILD_SOURCEBRANCH: null } 60 | const vsts = new VSTS(env) 61 | expect(vsts.isPR).toBeFalsy() 62 | }) 63 | 64 | it("validates with the correct build reason", () => { 65 | const vsts = new VSTS({ ...correctEnv, BUILD_REASON: "PullRequest" }) 66 | expect(vsts.isPR).toBeTruthy() 67 | }) 68 | 69 | it("does not validate without the correct build reason", () => { 70 | const vsts = new VSTS({ ...correctEnv, BUILD_REASON: "Unknown" }) 71 | expect(vsts.isPR).toBeFalsy() 72 | }) 73 | }) 74 | 75 | describe(".pullRequestID", () => { 76 | it("pulls it out of the env", () => { 77 | const vsts = new VSTS(correctEnv) 78 | expect(vsts.pullRequestID).toEqual(PRNum) 79 | }) 80 | }) 81 | 82 | describe(".repoSlug", () => { 83 | it("pulls it out of the env", () => { 84 | const vsts = new VSTS(correctEnv) 85 | expect(vsts.repoSlug).toEqual("artsy/eigen") 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /source/commands/init/state-setup.ts: -------------------------------------------------------------------------------- 1 | import * as readlineSync from "readline-sync" 2 | import * as supportsHyperlinks from "supports-hyperlinks" 3 | import * as hyperLinker from "hyperlinker" 4 | import chalk from "chalk" 5 | 6 | import { basename } from "path" 7 | import { setTimeout } from "timers" 8 | import * as fs from "fs" 9 | 10 | import { getRepoSlug } from "./get-repo-slug" 11 | import { InitState, InitUI } from "./interfaces" 12 | 13 | export const createUI = (state: InitState, app: any): InitUI => { 14 | const say = (msg: String) => console.log(msg) 15 | const fancyLink = (name: string, href: string) => hyperLinker(name, href) 16 | const inlineLink = (_name: string, href: string) => chalk.underline(href) 17 | const linkToUse = state.supportsHLinks ? fancyLink : inlineLink 18 | 19 | return { 20 | say, 21 | header: (msg: String) => say(chalk.bold("\n## " + msg + "\n")), 22 | command: (command: string) => say("> " + chalk.white.bold(command) + " \n"), 23 | link: (name: string, href: string) => linkToUse(name, href), 24 | pause: async (secs: number) => new Promise(done => setTimeout(done, secs * 1000)), 25 | waitForReturn: () => (app.impatient ? Promise.resolve() : readlineSync.question("\n↵ ")), 26 | askWithAnswers: (_message: string, answers: string[]) => { 27 | const a = readlineSync.keyInSelect(answers, "", { defaultInput: answers[0] }) 28 | return answers[a] 29 | }, 30 | } 31 | } 32 | 33 | export const generateInitialState = (osProcess: NodeJS.Process): InitState => { 34 | const isMac = osProcess.platform === "darwin" 35 | const isWindows = osProcess.platform === "win32" 36 | const folderName = capitalizeFirstLetter(camelCase(basename(osProcess.cwd()))) 37 | const isTypeScript = checkForTypeScript() 38 | const isBabel = checkForBabel() 39 | const hasTravis = fs.existsSync(".travis.yml") 40 | const hasCircle = fs.existsSync("circle.yml") 41 | const ciType = hasTravis ? "travis" : hasCircle ? "circle" : "unknown" 42 | const repoSlug = getRepoSlug() 43 | const isGitHub = !!repoSlug 44 | 45 | return { 46 | isMac, 47 | isWindows, 48 | isTypeScript, 49 | isBabel, 50 | isAnOSSRepo: true, 51 | supportsHLinks: supportsHyperlinks.stdout, 52 | filename: isTypeScript ? "dangerfile.ts" : "dangerfile.js", 53 | botName: folderName + "Bot", 54 | hasSetUpAccount: false, 55 | hasCreatedDangerfile: false, 56 | hasSetUpAccountToken: false, 57 | repoSlug, 58 | ciType, 59 | isGitHub, 60 | } 61 | } 62 | 63 | const checkForTypeScript = () => fs.existsSync("node_modules/typescript/package.json") 64 | const checkForBabel = () => 65 | fs.existsSync("node_modules/babel-core/package.json") || fs.existsSync("node_modules/@babel/core/package.json") 66 | 67 | const capitalizeFirstLetter = (string: string) => string.charAt(0).toUpperCase() + string.slice(1) 68 | const camelCase = (str: string) => str.split("-").reduce((a, b) => a + b.charAt(0).toUpperCase() + b.slice(1)) 69 | -------------------------------------------------------------------------------- /source/runner/templates/_tests/_bitbucketServerTemplate.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | emptyResults, 3 | failsResultsWithoutMessages, 4 | warnResults, 5 | failsResults, 6 | summaryResults, 7 | messagesResults, 8 | markdownResults, 9 | } from "../../_tests/fixtures/ExampleDangerResults" 10 | import { template, inlineTemplate } from "../bitbucketServerTemplate" 11 | 12 | const noEntryEmoji = "\u274C" 13 | const warningEmoji = "⚠️" 14 | const messageEmoji = "\u2728" 15 | 16 | describe("generating messages for BitBucket server", () => { 17 | it("shows no sections for empty results", () => { 18 | const issues = template("blankID", emptyResults) 19 | expect(issues).not.toContain("Fails") 20 | expect(issues).not.toContain("Warnings") 21 | expect(issues).not.toContain("Messages") 22 | }) 23 | 24 | it("shows no sections for results without messages", () => { 25 | const issues = template("blankID", failsResultsWithoutMessages) 26 | expect(issues).not.toContain("Fails") 27 | expect(issues).not.toContain("Warnings") 28 | expect(issues).not.toContain("Messages") 29 | }) 30 | 31 | it("Shows the failing messages in a section", () => { 32 | const issues = template("blankID", failsResults) 33 | expect(issues).toContain("Fails") 34 | expect(issues).not.toContain("Warnings") 35 | }) 36 | 37 | it("Shows the warning messages in a section", () => { 38 | const issues = template("blankID", warnResults) 39 | expect(issues).toContain("Warnings") 40 | expect(issues).not.toContain("Fails") 41 | }) 42 | 43 | it("summary result matches snapshot", () => { 44 | expect(template("blankID", summaryResults)).toMatchSnapshot() 45 | }) 46 | }) 47 | 48 | describe("generating inline messages", () => { 49 | it("Shows the failing message", () => { 50 | const issues = inlineTemplate("blankID", failsResults, "File.swift", 5) 51 | expect(issues).toContain(`- ${noEntryEmoji} Failing message`) 52 | expect(issues).not.toContain(`- ${warningEmoji}`) 53 | expect(issues).not.toContain(`- ${messageEmoji}`) 54 | }) 55 | 56 | it("Shows the warning message", () => { 57 | const issues = inlineTemplate("blankID", warnResults, "File.swift", 5) 58 | expect(issues).toContain(`- ${warningEmoji} Warning message`) 59 | expect(issues).not.toContain(`- ${noEntryEmoji}`) 60 | expect(issues).not.toContain(`- ${messageEmoji}`) 61 | }) 62 | 63 | it("Shows the message", () => { 64 | const issues = inlineTemplate("blankID", messagesResults, "File.swift", 5) 65 | expect(issues).toContain(`- ${messageEmoji} Message`) 66 | expect(issues).not.toContain(`- ${noEntryEmoji}`) 67 | expect(issues).not.toContain(`- ${warningEmoji}`) 68 | }) 69 | 70 | it("Shows markdowns one after another", () => { 71 | const issues = inlineTemplate("blankID", markdownResults, "File.swift", 5) 72 | const expected = ` 73 | ### Short Markdown Message1 74 | 75 | ### Short Markdown Message2 76 | ` 77 | expect(issues).toContain(expected) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /source/ci_source/ci_source_helpers.ts: -------------------------------------------------------------------------------- 1 | import { Env } from "./ci_source" 2 | import { GitHubAPI } from "../platforms/github/GitHubAPI" 3 | import { GitHubPRDSL } from "../dsl/GitHubDSL" 4 | import * as find from "lodash.find" 5 | import { 6 | BitBucketServerAPI, 7 | bitbucketServerRepoCredentialsFromEnv, 8 | } from "../platforms/bitbucket_server/BitBucketServerAPI" 9 | import { RepoMetaData } from "../dsl/BitBucketServerDSL" 10 | 11 | /** 12 | * Validates that all ENV keys exist and have a length 13 | * @param {Env} env The environment. 14 | * @param {[string]} keys Keys to ensure existence of 15 | * @returns {bool} true if they exist, false if not 16 | */ 17 | export function ensureEnvKeysExist(env: Env, keys: string[]): boolean { 18 | /*const hasKeys = keys.map((key: string): boolean => { 19 | return env.hasOwnProperty(key) && env[key] != null && env[key].length > 0 20 | }); 21 | return !includes(hasKeys, false);*/ 22 | 23 | return ( 24 | keys 25 | .map((key: string) => env.hasOwnProperty(key) && env[key] != null && env[key].length > 0) 26 | .filter(x => x === false).length === 0 27 | ) 28 | } 29 | 30 | /** 31 | * Validates that all ENV keys exist and can be turned into ints 32 | * @param {Env} env The environment. 33 | * @param {[string]} keys Keys to ensure existence and number-ness of 34 | * @returns {bool} true if they are all good, false if not 35 | */ 36 | export function ensureEnvKeysAreInt(env: Env, keys: string[]): boolean { 37 | /*const hasKeys = keys.map((key: string): boolean => { 38 | return env.hasOwnProperty(key) && !isNaN(parseInt(env[key])) 39 | }) 40 | return !includes(hasKeys, false);*/ 41 | 42 | return ( 43 | keys.map((key: string) => env.hasOwnProperty(key) && !isNaN(parseInt(env[key]))).filter(x => x === false).length === 44 | 0 45 | ) 46 | } 47 | 48 | /** 49 | * Retrieves the current pull request open for this branch from an API 50 | * @param {Env} env The environment 51 | * @param {string} branch The branch to find pull requests for 52 | * @returns {number} The pull request ID, if any. Otherwise 0 (Github starts from #1). 53 | * If there are multiple pull requests open for a branch, returns the first. 54 | */ 55 | export async function getPullRequestIDForBranch(metadata: RepoMetaData, env: Env, branch: string): Promise { 56 | if (process.env["DANGER_BITBUCKETSERVER_HOST"]) { 57 | const api = new BitBucketServerAPI(metadata, bitbucketServerRepoCredentialsFromEnv(env)) 58 | const prs = await api.getPullRequestsFromBranch(branch) 59 | if (prs.length) { 60 | return prs[0].id 61 | } 62 | return 0 63 | } 64 | 65 | const token = env["DANGER_GITHUB_API_TOKEN"] 66 | if (!token) { 67 | return 0 68 | } 69 | const api = new GitHubAPI(metadata, token) 70 | const prs = (await api.getPullRequests()) as any[] 71 | const prForBranch: GitHubPRDSL = find(prs, (pr: GitHubPRDSL) => pr.head.ref === branch) 72 | if (prForBranch) { 73 | return prForBranch.number 74 | } 75 | return 0 76 | } 77 | -------------------------------------------------------------------------------- /VISION.md: -------------------------------------------------------------------------------- 1 | # Danger for JS 2 | 3 | Danger JS is a tool to creating complex per-project rules, and messages in Code Review. One of it's key aims is to be 4 | able to run on a server, and not need direct access to the filesystem to do its work. 5 | 6 | It was started in mid-2016, and has fleshed out into a considerable set of useful tools. 7 | 8 | - You can get started in a fun way via `danger init`. 9 | - You can run danger via `danger ci`. 10 | - You can fake running on CI locally for any GitHub PR with `danger pr`. 11 | - You can run Danger rules inside git hooks, or without pull requests metadata via `danger local`. 12 | - You can share code using [danger plugins][plugins]. 13 | - Danger can run independently of CI via Peril. 14 | 15 | ## Future Plans 16 | 17 | There is only really one big target left for the future of Danger JS: 18 | 19 | - GitLab 20 | 21 | I don't plan on really using this, so I expect both to come from the community instead. 22 | 23 | My focus is going to be mainly in the Peril side of Danger. Moving to making it trivial to add Danger to any GitHub 24 | project and really unlocking some complex culture systems. Examples of these can be found on [the Artsy blog][peril]. 25 | 26 | # Why Danger JS? What about Danger Ruby? 27 | 28 | When I started Danger JS, Danger Ruby was two years old, is still doing just fine. See the 29 | [original vision file](https://github.com/danger/danger/blob/master/VISION.md). This document assumes you have read it. 30 | 31 | The amount of issues we get in comparison to the number of downloads on Rubygems makes me feel pretty confident about 32 | Danger Ruby's state of production quality and maturity. I wanted to start thinking about the larger patterns in 33 | software, because at Artsy, we are starting to use JavaScript in 34 | [for many teams](http://artsy.github.io/blog/2016/08/15/React-Native-at-Artsy/). 35 | 36 | I've explored [running JavaScript](https://github.com/danger/danger/pull/423) from the ruby Danger, 37 | ([example](https://github.com/artsy/emission/blob/d58b3d57bf41100e3cce3c2c1b1c4d6c19581a68/Dangerfile.js) from 38 | production) but this pattern isn't going to work on the larger scale: You cannot use npm modules, nor work with 39 | babel/tsc to transpile your `Dangerfile.js` and the requirements on the integrating project 40 | [feel weird](https://github.com/artsy/emission/pull/233). Running JS in Ruby isn't going to work for me. 41 | 42 | This realization came at the same time as serious thinking on a hosted version of Danger. With a JavaScript versions we 43 | can limit the exposed Danger DSL to only something that can be obtained over the API remotely. By doing this, a hosted 44 | Danger does not need to clone and run the associated projects. This is essential for my sanity. I cannot run multiple 45 | [servers like CocoaDocs](http://cocoadocs.org). So far, I'm calling this Peril. You can consult the 46 | [vision file for Peril](https://github.com/danger/peril/blob/master/VISION.md) if you'd like. 47 | 48 | [plugins]: https://www.npmjs.com/search?q=keywords:danger-plugin&page=1&ranking=optimal 49 | [peril]: http://artsy.github.io/blog/2017/09/04/Introducing-Peril/ 50 | --------------------------------------------------------------------------------