├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── CI.yml │ ├── publish_package.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .release-it.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── spell.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── VISION.md ├── appveyor.yml ├── dangerfile.circle.js ├── dangerfile.flow.js ├── dangerfile.lite.ts ├── dangerfile.ts ├── docs ├── architecture.md ├── guides │ ├── faq.html.md │ ├── peril.html.md │ └── the_dangerfile.html.md ├── issue_template.md ├── tutorials │ ├── dependencies.html.md │ ├── fast-feedback.html.md │ ├── node-app.html.md │ ├── node-library.html.md │ └── transpilation.html.md └── usage │ ├── bitbucket_cloud.html.md │ ├── bitbucket_server.html.md │ ├── culture.html.md │ ├── danger-process.html.md │ ├── extending-danger.html.md │ ├── github_enterprise.html.md │ └── gitlab.html.md ├── env └── development.env.example ├── package.json ├── scripts ├── create-danger-dts.ts ├── create-homebrew-tap-pr.sh ├── danger-dts.ts ├── danger_runner.rb ├── run-fixtures.js ├── update-changelog.ts └── update_flow_types.js ├── source ├── _tests │ └── fixtures │ │ └── danger-js-pr-395.json ├── ambient.d.ts ├── api │ ├── _tests │ │ └── fetch.test.ts │ └── fetch.ts ├── ci_source │ ├── _tests │ │ ├── _get_ci_source.test.ts │ │ ├── ci_source_helper.test.ts │ │ └── fixtures │ │ │ ├── dummy_ci.d.ts │ │ │ └── dummy_ci.js │ ├── ci_source.ts │ ├── ci_source_helpers.ts │ ├── get_ci_source.ts │ └── providers │ │ ├── AppCenter.ts │ │ ├── Bamboo.ts │ │ ├── BitbucketPipelines.ts │ │ ├── Bitrise.ts │ │ ├── BuddyBuild.ts │ │ ├── BuddyWorks.ts │ │ ├── Buildkite.ts │ │ ├── Circle.ts │ │ ├── Cirrus.ts │ │ ├── CodeBuild.ts │ │ ├── Codefresh.ts │ │ ├── Codemagic.ts │ │ ├── Codeship.ts │ │ ├── Concourse.ts │ │ ├── DockerCloud.ts │ │ ├── Drone.ts │ │ ├── Fake.ts │ │ ├── GitHubActions.ts │ │ ├── GitLabCI.ts │ │ ├── Jenkins.ts │ │ ├── Netlify.ts │ │ ├── Nevercode.ts │ │ ├── Screwdriver.ts │ │ ├── Semaphore.ts │ │ ├── Surf.ts │ │ ├── TeamCity.ts │ │ ├── Travis.ts │ │ ├── VSTS.ts │ │ ├── XcodeCloud.ts │ │ ├── _tests │ │ ├── _bamboo.test.ts │ │ ├── _bitbucketPipelines.test.ts │ │ ├── _bitrise.test.ts │ │ ├── _buddyBuild.test.ts │ │ ├── _buddyWorks.test.ts │ │ ├── _buildkite.test.ts │ │ ├── _circle.test.ts │ │ ├── _codebuild.test.ts │ │ ├── _codefresh.test.ts │ │ ├── _codemagic.test.ts │ │ ├── _concourse.test.ts │ │ ├── _dockerCloud.test.ts │ │ ├── _drone.test.ts │ │ ├── _gitHubActions.test.ts │ │ ├── _gitlab.test.ts │ │ ├── _jenkins.test.ts │ │ ├── _netlify.test.ts │ │ ├── _nevercode.test.ts │ │ ├── _screwdriver.test.ts │ │ ├── _semaphore.test.ts │ │ ├── _teamcity.test.ts │ │ ├── _travis.test.ts │ │ ├── _vsts.test.ts │ │ └── _xcodeCloud.test.ts │ │ ├── index.ts │ │ └── local-repo.ts ├── commands │ ├── ci │ │ ├── _tests │ │ │ └── runner.test.ts │ │ ├── resetStatus.ts │ │ └── runner.ts │ ├── danger-ci.ts │ ├── danger-init.ts │ ├── danger-local.ts │ ├── danger-pr.ts │ ├── danger-process.ts │ ├── danger-reset-status.ts │ ├── danger-runner.ts │ ├── danger.ts │ ├── init │ │ ├── add-to-ci.ts │ │ ├── default-dangerfile.ts │ │ ├── get-repo-slug.ts │ │ ├── interfaces.ts │ │ └── state-setup.ts │ └── utils │ │ ├── _tests │ │ ├── chainsmoker.test.ts │ │ ├── dangerRunToRunnerCLI.test.ts │ │ └── file-utils.test.ts │ │ ├── chainsmoker.ts │ │ ├── dangerRunToRunnerCLI.ts │ │ ├── fileUtils.ts │ │ ├── getRuntimeCISource.ts │ │ ├── reporting.ts │ │ ├── runDangerSubprocess.ts │ │ ├── sharedDangerfileArgs.ts │ │ └── validateDangerfileExists.ts ├── danger-incoming-process-schema.json ├── danger-outgoing-process-schema.json ├── danger.d.ts ├── danger.ts ├── debug.ts ├── dsl │ ├── Aliases.ts │ ├── BitBucketCloudDSL.ts │ ├── BitBucketServerDSL.ts │ ├── Commit.ts │ ├── DangerDSL.ts │ ├── DangerResults.ts │ ├── DangerUtilsDSL.ts │ ├── GitDSL.ts │ ├── GitHubDSL.ts │ ├── GitLabDSL.ts │ ├── RepoMetaData.ts │ ├── Violation.ts │ ├── _tests │ │ ├── DangerResults.test.ts │ │ ├── __snapshots__ │ │ │ └── DangerResults.test.ts.snap │ │ └── fixtures │ │ │ └── ExampleDangerResults.ts │ └── cli-args.ts ├── platforms │ ├── BitBucketCloud.ts │ ├── BitBucketServer.ts │ ├── FakePlatform.ts │ ├── GitHub.ts │ ├── GitLab.ts │ ├── LocalGit.ts │ ├── _tests │ │ ├── __snapshots__ │ │ │ ├── _bitbucket_cloud.test.ts.snap │ │ │ └── _bitbucket_server.test.ts.snap │ │ ├── _bitbucket_cloud.test.ts │ │ ├── _bitbucket_server.test.ts │ │ ├── _encoding_parser.test.ts │ │ ├── _github.test.ts │ │ ├── _gitlab.test.ts │ │ ├── _platform.test.ts │ │ ├── _pull_request_parser.test.ts │ │ └── fixtures │ │ │ ├── bbc-dsl-input.json │ │ │ ├── bbs-dsl-input.json │ │ │ ├── bitbucket_cloud_activities.json │ │ │ ├── bitbucket_cloud_comments.json │ │ │ ├── bitbucket_cloud_commits.json │ │ │ ├── bitbucket_cloud_diff.diff │ │ │ ├── bitbucket_cloud_pr.json │ │ │ ├── bitbucket_server_activities.json │ │ │ ├── bitbucket_server_changes.json │ │ │ ├── bitbucket_server_comments.json │ │ │ ├── bitbucket_server_commits.json │ │ │ ├── bitbucket_server_diff.json │ │ │ ├── bitbucket_server_issues.json │ │ │ ├── bitbucket_server_pr.json │ │ │ ├── github_comments.json │ │ │ ├── github_commits.json │ │ │ ├── github_diff.diff │ │ │ ├── github_inline_comments.json │ │ │ ├── github_inline_comments_with_danger.json │ │ │ ├── github_issue.json │ │ │ ├── github_pr.json │ │ │ ├── github_user.json │ │ │ ├── readme.md │ │ │ ├── requested_reviewers.json │ │ │ ├── reviews.json │ │ │ ├── static_file.98f3e73f5e419f3af9ab928c86312f28a3c87475.json │ │ │ ├── static_file.cfa8fb80d2b65f4c4fa0b54d25352a3a0ff58f75.json │ │ │ └── static_file.json │ ├── bitbucket_cloud │ │ ├── BitBucketCloudAPI.ts │ │ ├── BitBucketCloudGit.ts │ │ └── _tests_ │ │ │ ├── __snapshots__ │ │ │ └── _bitbucket_cloud_git.test.ts.snap │ │ │ ├── _bitbucket_cloud_api.test.ts │ │ │ └── _bitbucket_cloud_git.test.ts │ ├── bitbucket_server │ │ ├── BitBucketServerAPI.ts │ │ ├── BitBucketServerGit.ts │ │ └── _tests │ │ │ ├── __snapshots__ │ │ │ └── _bitbucket_server_git.test.ts.snap │ │ │ ├── _bitbucket_server_api.test.ts │ │ │ └── _bitbucket_server_git.test.ts │ ├── encodingParser.ts │ ├── git │ │ ├── _tests │ │ │ ├── localGetCommits.test.ts │ │ │ ├── localLogGitCommits.test.ts │ │ │ └── local_dangerfile_example.ts │ │ ├── diffToGitJSONDSL.ts │ │ ├── gitJSONToGitDSL.ts │ │ ├── localGetCommits.ts │ │ ├── localGetDiff.ts │ │ ├── localGetFileAtSHA.ts │ │ └── localLogGitCommits.ts │ ├── github │ │ ├── GitHubAPI.ts │ │ ├── GitHubGit.ts │ │ ├── GitHubUtils.ts │ │ ├── _tests │ │ │ ├── __snapshots__ │ │ │ │ └── _github_git.test.ts.snap │ │ │ ├── _customGitHubRequire.test.ts │ │ │ ├── _github_api.test.ts │ │ │ ├── _github_git.test.ts │ │ │ ├── _github_utils.test.ts │ │ │ └── fixturedGitHubDSL.ts │ │ ├── comms │ │ │ ├── _tests │ │ │ │ ├── _checksCommenter.test.ts │ │ │ │ └── _issueCommenter.test.ts │ │ │ ├── checks │ │ │ │ ├── githubAppSupport.ts │ │ │ │ └── resultsToCheck.ts │ │ │ ├── checksCommenter.ts │ │ │ └── issueCommenter.ts │ │ └── customGitHubRequire.ts │ ├── gitlab │ │ ├── GitLabAPI.ts │ │ ├── GitLabGit.ts │ │ ├── _tests │ │ │ ├── __snapshots__ │ │ │ │ └── _gitlab_git.test.ts.snap │ │ │ ├── _fetch_polyfill.ts │ │ │ ├── _gitlab_api.test.ts │ │ │ ├── _gitlab_git.test.ts │ │ │ ├── _inline_position_parser.test.ts │ │ │ └── fixtures │ │ │ │ ├── fileContentsAfter.txt │ │ │ │ ├── fileContentsBefore.txt │ │ │ │ ├── getCompareChanges.json │ │ │ │ ├── getFileContents.json │ │ │ │ ├── getMergeRequestApprovals.json │ │ │ │ ├── getMergeRequestCommits.json │ │ │ │ ├── getMergeRequestDiffs.json │ │ │ │ ├── getMergeRequestDiscussions.json │ │ │ │ ├── getMergeRequestInfo.json │ │ │ │ ├── getMergeRequestNotes.json │ │ │ │ ├── getUser.json │ │ │ │ ├── gitlab_changes.json │ │ │ │ └── updateMergeRequestInfo.json │ │ └── inlinePositionParser.ts │ ├── platform.ts │ └── pullRequestParser.ts └── runner │ ├── DangerUtils.ts │ ├── Dangerfile.ts │ ├── Executor.ts │ ├── _tests │ ├── _danger_utils.test.ts │ ├── _executor.test.ts │ ├── fixtures │ │ ├── ExampleDangerResults.ts │ │ ├── __DangerfileAsync.js │ │ ├── __DangerfileAsync.ts │ │ ├── __DangerfileBadSyntax.js │ │ ├── __DangerfileCallback.js │ │ ├── __DangerfileCustomIconMessages.js │ │ ├── __DangerfileDefaultExport.js │ │ ├── __DangerfileDefaultExportAsync.js │ │ ├── __DangerfileEmpty.js │ │ ├── __DangerfileFullMessages.js │ │ ├── __DangerfileImportRelative.js │ │ ├── __DangerfileInlineOpts.js │ │ ├── __DangerfileInlineResults.js │ │ ├── __DangerfileMultiScheduled.js │ │ ├── __DangerfileNoScheduledAsync.ts │ │ ├── __DangerfilePlugin.js │ │ ├── __DangerfileScheduled.js │ │ ├── __DangerfileThrows.js │ │ ├── __DangerfileTypeScript.ts │ │ ├── export │ │ │ └── thing.js │ │ └── results │ │ │ ├── __DangerfileAsync.js.json │ │ │ ├── __DangerfileAsync.ts.json │ │ │ ├── __DangerfileBadSyntax.js.json │ │ │ ├── __DangerfileCallback.js.json │ │ │ ├── __DangerfileCustomIconMessages.js.json │ │ │ ├── __DangerfileDefaultExport.js.json │ │ │ ├── __DangerfileDefaultExportAsync.js.json │ │ │ ├── __DangerfileEmpty.js.json │ │ │ ├── __DangerfileFullMessages.js.json │ │ │ ├── __DangerfileImportRelative.js.json │ │ │ ├── __DangerfileInlineOpts.js.json │ │ │ ├── __DangerfileInlineResults.js.json │ │ │ ├── __DangerfileMultiScheduled.js.json │ │ │ ├── __DangerfileNoScheduledAsync.ts.json │ │ │ ├── __DangerfilePlugin.js.json │ │ │ ├── __DangerfileScheduled.js.json │ │ │ ├── __DangerfileThrows.js.json │ │ │ └── __DangerfileTypeScript.ts.json │ ├── jsonToContext.test.ts │ └── jsonToDSL.test.ts │ ├── dangerDSLJSON.ts │ ├── dslGenerator.ts │ ├── jsonToContext.ts │ ├── jsonToDSL.ts │ ├── runners │ ├── _tests │ │ └── _cleanDangerfile.test.ts │ ├── inline.ts │ ├── runner.ts │ └── utils │ │ ├── _tests │ │ └── _transpiler.test.ts │ │ ├── cleanDangerfile.ts │ │ ├── resultsForCaughtError.ts │ │ └── transpiler.ts │ └── templates │ ├── _tests │ ├── __snapshots__ │ │ ├── _bitbucketCloudTemplate.test.ts.snap │ │ ├── _bitbucketServerTemplate.test.ts.snap │ │ ├── _githubIssueTemplates.test.ts.snap │ │ └── _markdownTableTemplate.test.ts.snap │ ├── _bitbucketCloudTemplate.test.ts │ ├── _bitbucketServerTemplate.test.ts │ ├── _githubIssueTemplates.test.ts │ └── _markdownTableTemplate.test.ts │ ├── bitbucketCloudTemplate.ts │ ├── bitbucketServerTemplate.ts │ ├── exceptionRaisedTemplate.ts │ ├── githubIssueTemplate.ts │ └── markdownTableTemplate.ts ├── tsconfig.json ├── tsconfig.production.json ├── tsconfig.spec.json ├── tslint.json ├── types ├── package.json ├── test.ts ├── tsconfig.json └── tslint.json ├── wallaby.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "useBuiltIns": "usage", "corejs": "3.8", "exclude": ["transform-typeof-symbol"] }] 4 | ], 5 | "plugins": ["@babel/plugin-transform-flow-strip-types"] 6 | } 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | source/danger.d.ts 2 | source/runner/_tests/fixtures/*.ts 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ["plugin:@typescript-eslint/recommended", "plugin:jest/recommended", "prettier"], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | project: "tsconfig.spec.json", 11 | sourceType: "module", 12 | }, 13 | plugins: ["eslint-plugin-jest", "eslint-plugin-jsdoc", "@typescript-eslint"], 14 | rules: { 15 | "@typescript-eslint/naming-convention": "error", 16 | "@typescript-eslint/no-empty-function": "error", 17 | "@typescript-eslint/no-unused-expressions": "error", 18 | 19 | // Something is grumpy about these rules re: node imports - TODO 20 | "@typescript-eslint/no-require-imports": "off", 21 | "@typescript-eslint/no-var-requires": "off", 22 | 23 | curly: "error", 24 | "jsdoc/check-alignment": "error", 25 | "jsdoc/check-indentation": "off", 26 | "jsdoc/newline-after-description": "off", 27 | "no-empty": "error", 28 | // This is used intentionally in a bunch of ci_source/providers 29 | "no-empty-function": "off", 30 | "no-redeclare": "error", 31 | "no-var": "error", 32 | // There are a bunch of existing uses of 'let' where this rule would trigger 33 | "prefer-const": "off", 34 | 35 | // This project has a ton of interacting APIs, and sometimes it's helpful to be explicit, even if the type is trivial 36 | "@typescript-eslint/no-inferrable-types": "off", 37 | 38 | "no-unused-expressions": "off", 39 | "@typescript-eslint/no-unused-expressions": "error", 40 | 41 | "no-unused-vars": "off", 42 | "@typescript-eslint/no-unused-vars": [ 43 | "warn", 44 | { 45 | // Allow function args to be unused 46 | args: "none", 47 | argsIgnorePattern: "^_", 48 | varsIgnorePattern: "^_", 49 | caughtErrorsIgnorePattern: "^_", 50 | }, 51 | ], 52 | 53 | "jest/no-disabled-tests": "warn", 54 | "jest/no-focused-tests": "error", 55 | "jest/no-identical-title": "error", 56 | "jest/prefer-to-have-length": "off", 57 | "jest/valid-expect": "error", 58 | "@typescript-eslint/no-non-null-assertion": "off", 59 | // Tons of violations in the codebase 60 | "@typescript-eslint/naming-convention": "off", 61 | // Used liberally in the codebase 62 | "@typescript-eslint/no-explicit-any": "off", 63 | // This has value in communicating with other Developers even if it has no functional effect. 64 | "@typescript-eslint/no-empty-interface": "off", 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # .gitattributes documentation: https://git-scm.com/docs/gitattributes 2 | 3 | # Resolve 3-way merge conflicts on the CHANGELOG.md file using the union strategy 4 | CHANGELOG.md merge=union 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [orta] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | --- 7 | 8 | **Describe the bug** 9 | A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** 12 | Steps to reproduce the behavior: 13 | 1. 14 | 2. 15 | 3. 16 | 4. 17 | 18 | **Expected behavior** 19 | 20 | 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Your Environment** 26 | 27 | 28 | | software | version 29 | | ---------------- | ------- 30 | | danger.js | 31 | | node | 32 | | npm | 33 | | Operating System | 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: pull_request 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | # Check out, and set up the node/ruby infra 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: "20" 14 | 15 | # Get local dependencies & test 16 | - run: yarn install 17 | - run: yarn build 18 | - run: yarn test 19 | 20 | # Validate related tooling 21 | - run: yarn declarations 22 | - run: yarn tsc --esModuleInterop distribution/danger.d.ts 23 | - run: rm -rf node_modules/@types/babel-* 24 | - run: rm -rf node_modules/@types/babylon 25 | 26 | # Not many Flow users nowadays, so this probably is worth dropping 27 | # if it becomes an issue. 28 | - run: echo "Testing Flow definition file" 29 | - run: yarn build:flow-types 30 | 31 | - run: echo "Running built Danger" 32 | - run: node distribution/commands/danger-ci.js 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - run: 'echo "This is only for Integration tests on two blank projects"' 37 | 38 | - run: | 39 | mkdir danger_blank_test 40 | cd danger_blank_test 41 | yarn init --yes 42 | yarn add file:.. 43 | echo "warn('I warned you')" > dangerfile.js 44 | echo "Testing a blank Dangerfile on an empty project" 45 | DEBUG="*" yarn danger run --text-only 46 | cd .. 47 | rm -rf danger_blank_test 48 | name: "Make a CRA" 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | - run: | 53 | npm install -g create-react-app 54 | create-react-app danger_babel_test 55 | cd danger_babel_test 56 | yarn add file:.. 57 | echo "warn('Expect 2 warnings'); const a = async () => {warn('the other');}; schedule(a)" > dangerfile.js 58 | echo "Testing a blank Dangerfile on a babel CRA project" 59 | DEBUG="*" yarn danger ci --text-only 60 | cd .. 61 | rm -rf danger_babel_test 62 | name: "Make a small babel app" 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /.github/workflows/publish_package.yml: -------------------------------------------------------------------------------- 1 | name: Release Danger-JS package 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | tags: 8 | - '*' 9 | 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | env: 15 | REGISTRY: ghcr.io 16 | IMAGE_NAME: ${{ github.repository }} 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | packages: write 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Log in to the Container registry 30 | uses: docker/login-action@v2 31 | with: 32 | registry: ${{ env.REGISTRY }} 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Set up Docker metadata 37 | id: meta 38 | uses: docker/metadata-action@v4 39 | with: 40 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 | tags: | 42 | type=ref,event=branch 43 | type=ref,event=tag 44 | 45 | - name: Build and push Docker image 46 | uses: docker/build-push-action@v4 47 | with: 48 | context: . 49 | push: true 50 | tags: ${{ steps.meta.outputs.tags }} 51 | labels: ${{ steps.meta.outputs.labels }} 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release executable files for macOS 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version to deploy assets for" 8 | required: true 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | HOMEBREW_TAP_DEPLOY_SECRET_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_SECRET_KEY }} 16 | permissions: write-all 17 | steps: 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v2 20 | 21 | - name: Install ldid 22 | uses: MOZGIII/install-ldid-action@v1 23 | with: 24 | tag: v2.1.5-procursus6 25 | 26 | - name: Check out repository code 27 | uses: actions/checkout@v4 28 | 29 | - name: Use node 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: "20" 33 | 34 | - name: Install dependencies 35 | run: yarn install 36 | 37 | - name: Build 38 | run: yarn build 39 | 40 | - name: Tell release-it to do the GitHub release here 41 | run: 42 | node -e 'const fs = require("fs"); const a = JSON.parse(fs.readFileSync("./.release-it.json", "utf8")); 43 | a.github.release = true; a.hooks["after:bump"] = a.hooks["_after:bump"]; a.hooks["after:release"] = 44 | a.hooks["_after:release"]; fs.writeFileSync("./.release-it.json", JSON.stringify(a))' 45 | 46 | - name: Package 47 | run: | 48 | git config --global user.email "action@github.com" 49 | git config --global user.name "GitHub Action" 50 | npm run release -- ${{ github.event.inputs.version }} --no-npm --ci --no-git 51 | 52 | - name: Update homebrew/tap repo for new release 53 | shell: bash 54 | run: scripts/create-homebrew-tap-pr.sh 55 | env: 56 | VERSION: ${{ env.VERSION }} 57 | -------------------------------------------------------------------------------- /.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 | brew-distribution 41 | flow-typed 42 | env/development.env 43 | 44 | docs/doc_generate/ 45 | docs/docs_generate/ 46 | docs/js_ref_dsl_docs.json 47 | 48 | types/index.d.ts 49 | .jest/ 50 | test-results.json 51 | 52 | # Flowgen stuff 53 | source/_danger.d.tse 54 | source/_danger.d.ts 55 | tests.json 56 | 57 | # IDEs 58 | .idea 59 | 60 | /*.sh 61 | .DS_Store 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .editorconfig 3 | .flowconfig 4 | .jest 5 | .vscode 6 | *.lock 7 | *.yml 8 | **/_tests 9 | brew-distribution 10 | coverage 11 | dangerfile.circle.js 12 | dangerfile.flow.js 13 | dangerfile.lite.ts 14 | dangerfile.ts 15 | docs 16 | env 17 | flow-typed 18 | jsconfig.json 19 | scripts 20 | source 21 | test-results.json 22 | test.json 23 | tests.json 24 | tsconfig 25 | tsconfig.json 26 | tsconfig.production.json 27 | tslint.json 28 | types 29 | wallaby.js 30 | yarn-error.log 31 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - id: danger 2 | name: danger 3 | description: formalize your team etiquette 4 | language: node 5 | stages: 6 | - pre-commit 7 | entry: danger local 8 | args: 9 | - --base=main 10 | - --dangerfile=dangerfile.local.js 11 | - --staging 12 | pass_filenames: false 13 | always_run: true 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | source/danger-outgoing-process-schema.json 2 | source/danger-incoming-process-schema.json 3 | source/platforms/_tests/fixtures/bbs-dsl-input.json 4 | source/platforms/_tests/fixtures/bbc-dsl-input.json 5 | CHANGELOG.md 6 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "_": "CI will switch the release here to true", 3 | "github": { 4 | "release": false, 5 | "assets": "brew-distribution/*.zip", 6 | "releaseNotes": "npx auto-changelog --stdout --commit-limit false -u --template https://raw.githubusercontent.com/release-it/release-it/main/templates/changelog-compact.hbs" 7 | }, 8 | "git": { 9 | "requireCleanWorkingDir": false, 10 | "changelog": "npx auto-changelog --stdout --commit-limit false -u --template https://raw.githubusercontent.com/release-it/release-it/main/templates/changelog-compact.hbs" 11 | }, 12 | "buildCommand": "yarn package:x64; yarn package:arm64", 13 | "hooks": { 14 | "before:bump": "yarn declarations; yarn build:schemas", 15 | "after:release": "gh workflow run release.yml -f version=${version}", 16 | "_": "CI will remove the _ from both of the below lines", 17 | "_after:bump": "yarn package:x64; yarn package:arm64", 18 | "_after:release": "export VERSION=${version}; echo 'VERSION=${version}' >> $GITHUB_ENV" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Orta.vscode-jest", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "christian-kohler.path-intellisense", 7 | "wayou.vscode-todo-highlight" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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 that 23 | it compiles. 24 | 25 | ### How does Danger JS work? 26 | 27 | Check the [architecture doc](https://github.com/danger/danger-js/blob/main/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 `main` branch. Ensure your working tree is clean, and make sure you have the latest changes by running 38 | `git pull`. 39 | - Update `package.json` with the new version - for the sake of this example, the new version is **0.21.0**. 40 | - Modify `changelog.md`, adding a new `### 0.21.0` heading under the `### Main` heading at the top of the file. 41 | - Commit both changes with the commit message **Version bump**. 42 | - Tag this commit - `git tag 0.21.0`. 43 | - Push the commit and tag to master - `git push origin main --follow-tags`. GitHub Actions will build the tagged commit 44 | and publish that tagged version to NPM. 45 | 46 | :ship: 47 | 48 | ## License, Contributor's Guidelines and Code of Conduct 49 | 50 | We try to keep as much discussion as possible in GitHub issues, but also have a pretty inactive Slack --- if you'd like 51 | an invite, ping [@Orta](https://twitter.com/orta/) a DM on Twitter with your email. It's mostly interesting if you want 52 | to stay on top of Danger without all the emails from GitHub. 53 | 54 | > This project is open source under the MIT license, which means you have full access to the source code and can modify 55 | > it to fit your own needs. 56 | > 57 | > This project subscribes to the [Moya Contributors Guidelines](https://github.com/Moya/contributors) which TLDR: means 58 | > we give out push access easily and often. 59 | > 60 | > Contributors subscribe to the [Contributor Code of Conduct](http://contributor-covenant.org/version/1/3/0/) based on 61 | > the [Contributor Covenant](http://contributor-covenant.org) version 1.3.0. 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim as base 2 | 3 | LABEL maintainer="Orta Therox" 4 | LABEL "com.github.actions.name"="Danger JS Action" 5 | LABEL "com.github.actions.description"="Runs JavaScript/TypeScript Dangerfiles" 6 | LABEL "com.github.actions.icon"="zap" 7 | LABEL "com.github.actions.color"="blue" 8 | 9 | WORKDIR /usr/src/danger 10 | 11 | FROM base as build 12 | COPY package.json yarn.lock ./ 13 | RUN yarn install 14 | COPY . . 15 | RUN yarn run build:fast 16 | RUN yarn remove 'typescript' --dev && yarn add 'typescript' 17 | RUN yarn install --production --frozen-lockfile 18 | RUN chmod +x distribution/commands/danger.js 19 | 20 | FROM base 21 | ENV PATH="/usr/src/danger/node_modules/.bin:$PATH" 22 | COPY package.json ./ 23 | COPY --from=build /usr/src/danger/distribution ./dist 24 | COPY --from=build /usr/src/danger/node_modules ./node_modules 25 | RUN ln -s /usr/src/danger/dist/commands/danger.js /usr/bin/danger 26 | 27 | ENTRYPOINT ["danger", "ci"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-Present 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We only support the current SemVer major 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Feel free to reach out to orta on Twitter (@orta) or via `[yourname]@orta.io`. 10 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against this version of Node.js 2 | environment: 3 | nodejs_version: "20" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 main section." 13 | ) 14 | } 15 | 16 | // Always ensure we name all CI providers in the README. These 17 | // regularly get forgotten on a PR adding a new one. 18 | const sentence = danger.utils.sentence 19 | 20 | import { realProviders } from "./source/ci_source/providers" 21 | const readme = fs.readFileSync("README.md").toString() 22 | const names = realProviders.map((p) => new p({}).name) 23 | const missing = names.filter((n) => !readme.includes(n)) 24 | if (missing.length) { 25 | warn(`These providers are missing from the README: ${sentence(missing)}`) 26 | } 27 | -------------------------------------------------------------------------------- /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 const 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 | // Don't have folks setting the package json version 43 | const packageDiff = await danger.git.JSONDiffForFile("package.json") 44 | if (packageDiff.version && danger.github.pr.user.login !== "orta") { 45 | fail("Please don't make package version changes") 46 | } 47 | 48 | danger.github.setSummaryMarkdown("Looking good") 49 | } 50 | 51 | // Re-run the git push hooks 52 | import "./dangerfile.lite" 53 | -------------------------------------------------------------------------------- /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 4 | with some extra PR metadata added in at runtime. 5 | 6 | Actually doing that though, is a bit of a process. 7 | 8 | ## Setup 9 | 10 | **Step 1: CI**. Danger needs to figure out what CI we're running on. You can see them all in [ 11 | `source/ci_source/providers`][provs]. These use ENV VARs to figure out which CI `danger ci` is running on and validate 12 | whether it is a pull request. 13 | 14 | **Step 2: Platform**. Danger needs to know which platform the code review is happening in. Today, Danger supports 15 | Github, Gitlab, BitBucket Server, and BitBucket Cloud. You can see them all in [`source/platforms`][platforms]. 16 | 17 | **Step 3: JSON DSL**. To allow for all of: 18 | 19 | - `danger ci` to evaluate async code correctly 20 | - `danger process` to work with other languages 21 | - `peril` to arbitrarily sandbox danger per-run on a unique docker container 22 | 23 | Danger first generates a JSON DSL. This can be passed safely between processes, or servers. For `danger ci` the exposed 24 | DSL is created as a [DangerDSLJSONType][dangerdsl] and this is passed into the hidden command [`danger runner`][runner]. 25 | 26 | **Step 4: DSL**. The JSON DSL is picked up from STDIN in `danger runner` and then converted into a 27 | [DangerDSLType][dangerdsl]. This is basically where functions are added into the DSL. 28 | 29 | **Step 5: Evaluation**. With the DSL ready, the [inline runner][in_runner] sets up a transpiled environment for 30 | evaluating your code, and adds the DSL attributes into the global evaluation context. The Dangerfile has the 31 | `import {...} from 'danger'` stripped, and then is executed inline. 32 | 33 | **Step 6: Results**. Once the `danger runner` process is finished with evaluation, the results are passed back to the 34 | the platform. The platform then chooses whether to create/delete/edit any messages in core review. 35 | 36 | [provs]: https://github.com/danger/danger-js/tree/main/source/ci_source/providers 37 | [dangerdsl]: https://github.com/danger/danger-js/blob/main/source/dsl/DangerDSL.ts 38 | [runner]: https://github.com/danger/danger-js/blob/main/source/commands/danger-runner.ts 39 | [in_runner]: https://github.com/danger/danger-js/blob/main/source/runner/runners/inline.ts 40 | [platforms]: https://github.com/danger/danger-js/blob/main/source/platforms 41 | -------------------------------------------------------------------------------- /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 12 | instantly to webhooks. This gives Danger the ability to respond instantly to PR changes, and to run on more than just 13 | PRs. 14 | 15 | A lot of the information on Peril can be found on the 16 | [Artsy blog: here](http://artsy.github.io/blog/2017/09/04/Introducing-Peril/) 17 | 18 | Today Peril is self-hosted via heroku. There is a walkthrough on the 19 | [Peril repo: here](https://github.com/danger/peril/blob/main/docs/setup_for_org.md). It's still a pretty fast moving 20 | project ever 6 months into deployment so expect to maybe fix your own problem occasionally. 21 | 22 | ## Dangerfile implications 23 | 24 | Two tricky problems in Peril today: 25 | 26 | - Async is weird. 27 | - Can't do relative `import`s. 28 | 29 | Today Peril runs by inline execution of a JavaScript script. This has a serious draw-back in that async behavior doesn't 30 | work how you think it does. Here are some patterns for handling that. 31 | 32 | - **Ignore Async.** - A Dangerfile is a script, the non-blocking aspect of the node API can be ignored. E.g. use 33 | `path.xSync` instead of `path.x` 34 | 35 | - **Scheduling** - The Dangerfile DSL includes a function called `schedule`, this can handle either a promise or a 36 | function with a callback arg. For example using `async/await`: 37 | 38 | ```js 39 | import { schedule, danger } from "danger" 40 | 41 | /// [... a bunch of functions] 42 | 43 | schedule(async () => { 44 | const packageDiff = await danger.git.JSONDiffForFile("package.json") 45 | checkForRelease(packageDiff) 46 | checkForNewDependencies(packageDiff) 47 | checkForLockfileDiff(packageDiff) 48 | checkForTypesInDeps(packageDiff) 49 | }) 50 | ``` 51 | 52 | In this case, the closure is queued up and Danger waits until all `schedule` functions/promises are finished before 53 | continuing, so make sure to not cause it to lock. 54 | 55 | ## Plugin implications 56 | 57 | A plugin that runs on Peril will also have to handle the above if it uses async code. For some examples of this, see 58 | [danger-plugin-spellcheck](https://github.com/orta/danger-plugin-spellcheck#danger-plugin-spellcheck). 59 | -------------------------------------------------------------------------------- /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 discussions: https://github.com/danger/danger-js/discussions 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 | -------------------------------------------------------------------------------- /docs/usage/github_enterprise.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GitHub Enteprise 3 | subtitle: Danger on GHE 4 | layout: guide_js 5 | order: 4 6 | blurb: An overview of using Danger with GitHub Enterprise, and some examples 7 | --- 8 | 9 | If you are using DangerJS on GitHub Enteprise, you will need to set the Danger user ID to the GitHub Actions bot. This 10 | will enable Danger to correctly comment and update on PRs. 11 | 12 | If you include Danger as a dev-dependency, then you can call danger directly as another build-step after your tests: 13 | 14 | ```yml 15 | name: Node CI 16 | on: [pull_request] 17 | 18 | jobs: 19 | test: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@master 23 | - name: Use Node.js 10.x 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 10.x 27 | - name: install yarn 28 | run: npm install -g yarn 29 | - name: yarn install, build, and test 30 | run: | 31 | yarn install --frozen-lockfile 32 | yarn build 33 | yarn test 34 | - name: Danger 35 | run: yarn danger ci 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | DANGER_GHE_ACTIONS_BOT_USER_ID: *user_id* 39 | ``` 40 | 41 | If you are not running in a JavaScript ecosystem, or don't want to include the dependency then you can use Danger JS as 42 | an action. 43 | 44 | ```yml 45 | name: "Danger JS" 46 | on: [pull_request] 47 | 48 | jobs: 49 | build: 50 | name: Danger JS 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v1 54 | - name: Danger 55 | uses: danger/danger-js@9.1.6 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | DANGER_GHE_ACTIONS_BOT_USER_ID: *user_id* 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/usage/gitlab.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Danger + GitLab 3 | subtitle: Self-Hosted 4 | layout: guide_js 5 | order: 4 6 | blurb: An overview of using Danger with GitLab, and some examples 7 | --- 8 | 9 | To use Danger JS with GitLab: you'll need to create a new account for Danger to use, then set the following environment 10 | variable on your CI system: 11 | 12 | You need access token _OR_ oauth token 13 | 14 | - `DANGER_GITLAB_API_TOKEN` = An access token for the account which will post comments 15 | 16 | - `DANGER_GITLAB_API_OAUTH_TOKEN` = An oauth token for the account which will post comments 17 | 18 | If you are using a GitLab version prior to 11.7 you will also need to define the following environment variable: 19 | 20 | - `DANGER_GITLAB_HOST` = Defaults to `https://gitlab.com` but you can use it for your own url 21 | 22 | Then in your Dangerfiles you will have a fully fleshed out `danger.gitlab` object to work with. For example: 23 | 24 | ```ts 25 | import { danger, warn } from "danger" 26 | 27 | if (danger.gitlab.mr.title.includes("WIP")) { 28 | warn("PR is considered WIP") 29 | } 30 | ``` 31 | 32 | The DSL is expansive, you can see all the details inside the 33 | [Danger JS Reference](https://danger.systems/js/reference.html), but the TLDR is: 34 | 35 | ```ts 36 | danger.gitlab. 37 | /** The pull request and repository metadata */ 38 | metadata: RepoMetaData 39 | /** The Merge Request metadata */ 40 | mr: GitLabMR 41 | /** The commits associated with the merge request */ 42 | commits: GitLabMRCommit[] 43 | ``` 44 | 45 | --- 46 | 47 | If you want danger to open threads (discussions) instead of just commenting in merge requests, set an environment 48 | variable `DANGER_GITLAB_USE_THREADS` with value `1` or `true`. 49 | -------------------------------------------------------------------------------- /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 | DANGER_DISABLE_TRANSPILATION="false" 8 | -------------------------------------------------------------------------------- /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 | import * as ts from "typescript" 17 | 18 | const program = ts.createProgram(["source/danger.d.ts"], { noEmit: true, esModuleInterop: true }) 19 | const emitResult = program.emit() 20 | const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics) 21 | if (allDiagnostics.length) { 22 | process.exitCode = 1 23 | console.log("Found an error in the generated DTS file, you probably need to edit scripts/danger-dts.ts") 24 | console.log( 25 | "\nIf you've added something new to the DSL, and the errors are about something missing, you may need to add an interface in `source/dsl/*`." 26 | ) 27 | allDiagnostics.forEach((diagnostic) => { 28 | if (diagnostic.file) { 29 | let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!) 30 | let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n") 31 | console.error(`${diagnostic.file.fileName}:${line + 1}:${character + 1} - ${message}`) 32 | } else { 33 | console.error(`${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`) 34 | } 35 | }) 36 | } else { 37 | console.log("Awesome - shipped to source/danger.d.ts, distribution/danger.d.ts and types/index.d.ts") 38 | } 39 | -------------------------------------------------------------------------------- /scripts/create-homebrew-tap-pr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [ -z ${VERSION+x} ] && { echo "VERSION is missing"; exit 1; } 4 | 5 | FILE_X64=brew-distribution/danger-macos-x64.zip 6 | FILE_ARM64=brew-distribution/danger-macos-arm64.zip 7 | 8 | if [ ! -f ${FILE_X64} ]; then 9 | echo ${FILE_X64} not found! 10 | exit 1 11 | fi 12 | if [ ! -f ${FILE_ARM64} ]; then 13 | echo ${FILE_ARM64} not found! 14 | exit 1 15 | fi 16 | 17 | SHA_X64=$(shasum -a 256 ${FILE_X64} | cut -f 1 -d " ") 18 | echo "SHA_X64=$SHA_X64" 19 | SHA_ARM64=$(shasum -a 256 ${FILE_ARM64} | cut -f 1 -d " ") 20 | echo "SHA_ARM64=$SHA_ARM64" 21 | 22 | # Set up SSH 23 | mkdir -p ~/.ssh 24 | echo "${HOMEBREW_TAP_DEPLOY_SECRET_KEY}" > ~/.ssh/id_rsa 25 | chmod 600 ~/.ssh/id_rsa 26 | git config --global user.name danger 27 | git config --global user.email danger@users.noreply.github.com 28 | eval "$(ssh-agent -s)" 29 | ssh-add ~/.ssh/id_rsa 30 | ssh-keyscan -H github.com >> ~/.ssh/known_hosts 31 | ssh -o StrictHostKeyChecking=no -F /dev/null -vT git@github.com 32 | 33 | # Clone tap repo 34 | HOMEBREW_TAP_TMPDIR=$(mktemp -d) 35 | git clone --depth 1 git@github.com:danger/homebrew-tap.git "$HOMEBREW_TAP_TMPDIR" 36 | cd "$HOMEBREW_TAP_TMPDIR" || exit 1 37 | 38 | # Write formula 39 | echo "class DangerJs < Formula" > danger-js.rb 40 | echo " homepage \"https://github.com/danger/danger-js\"" >> danger-js.rb 41 | echo >> danger-js.rb 42 | echo " if Hardware::CPU.intel?" >> danger-js.rb 43 | echo " url \"https://github.com/danger/danger-js/releases/download/${VERSION}/danger-macos-x64.zip\"" >> danger-js.rb 44 | echo " sha256 \"${SHA_X64}\"" >> danger-js.rb 45 | echo >> danger-js.rb 46 | echo " def install" >> danger-js.rb 47 | echo " bin.install \"danger-x64\" => \"danger\"" >> danger-js.rb 48 | echo " end" >> danger-js.rb 49 | echo " end" >> danger-js.rb 50 | echo >> danger-js.rb 51 | echo " if Hardware::CPU.arm?" >> danger-js.rb 52 | echo " url \"https://github.com/danger/danger-js/releases/download/${VERSION}/danger-macos-arm64.zip\"" >> danger-js.rb 53 | echo " sha256 \"${SHA_ARM64}\"" >> danger-js.rb 54 | echo >> danger-js.rb 55 | echo " def install" >> danger-js.rb 56 | echo " bin.install \"danger-arm64\" => \"danger\"" >> danger-js.rb 57 | echo " end" >> danger-js.rb 58 | echo " end" >> danger-js.rb 59 | echo "end" >> danger-js.rb 60 | 61 | # Commit changes 62 | git add danger-js.rb 63 | git commit -m "Releasing danger-js version ${VERSION}" 64 | git remote rm origin 65 | git remote add origin git@github.com:danger/homebrew-tap.git 66 | git push origin master -------------------------------------------------------------------------------- /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 | puts "Hello from ruby!" 9 | results = { fails: [], warnings: [], messages: [], markdowns: [] }.to_json 10 | 11 | STDOUT.write(results) 12 | -------------------------------------------------------------------------------- /scripts/update-changelog.ts: -------------------------------------------------------------------------------- 1 | // import * as fs from "fs" 2 | 3 | // // Update the CHANGELOG with the new version 4 | 5 | // const changelog = fs.readFileSync("CHANGELOG.md", "utf8") 6 | // const newCHANGELOG = changelog.replace( 7 | // "", 8 | // ` 9 | 10 | // # ${process.env.VERSION} 11 | // ` 12 | // ) 13 | // fs.writeFileSync("CHANGELOG.md", newCHANGELOG, "utf8") 14 | -------------------------------------------------------------------------------- /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/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module "lodash.includes" 2 | declare module "lodash.find" 3 | declare module "lodash.isobject" 4 | declare module "lodash.keys" 5 | declare module "jest-runtime" 6 | declare module "jest-haste-map" 7 | declare module "jest-environment-node" 8 | declare module "jest-config" 9 | declare module "parse-link-header" 10 | declare module "pinpoint" 11 | declare module "prettyjson" 12 | 13 | declare module "*/package.json" 14 | 15 | declare module "require-from-string" 16 | declare module "node-eval" 17 | declare module "node-cleanup" 18 | declare module "cli-interact" 19 | 20 | declare module "hyperlinker" 21 | declare module "supports-hyperlinks" 22 | 23 | // declare module "require-from-string" { 24 | // export interface RequireOptions { 25 | // /** List of paths, that will be appended to module paths. Useful, when you want 26 | // * to be able require modules from these paths. */ 27 | // appendPaths: string[] 28 | // /** 29 | // * Same as appendPath, but paths will be prepended. 30 | // */ 31 | // prependPaths: string[] 32 | // } 33 | // /** 34 | // * Load module from string in Node. 35 | // * @param code Module code 36 | // * @param filename Optional filename 37 | // * @param opts 38 | // */ 39 | // export default function(code: string, filename?: string, opts?: Partial): any 40 | // } 41 | 42 | declare module "parse-github-url" 43 | // Basically does one thing 44 | declare module "override-require" 45 | 46 | declare namespace NodeJS { 47 | interface Process { 48 | // https://github.com/zeit/pkg#snapshot-filesystem 49 | pkg?: { 50 | entrypoint: string 51 | defaultEntrypoint: string 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /source/ci_source/_tests/_get_ci_source.test.ts: -------------------------------------------------------------------------------- 1 | import { FakeCI } from "../providers/Fake" 2 | import 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", () => { 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 | -------------------------------------------------------------------------------- /source/ci_source/_tests/ci_source_helper.test.ts: -------------------------------------------------------------------------------- 1 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 2 | 3 | describe(".ensureEnvKeysExist", () => { 4 | const env = { 5 | COMMIT: "9d2f95a4019935f91ed10e9d716d2b7551dcbcc1", 6 | REPO_SLUG: "bar", 7 | PR_ID: "30", 8 | PR_DESTINATION_BRANCH: "develop", 9 | REPO_OWNER: "foo", 10 | } 11 | test("return true if every key is in env", () => { 12 | const result = ensureEnvKeysExist(env, ["COMMIT", "REPO_SLUG"]) 13 | expect(result).toBe(true) 14 | }) 15 | 16 | test("return false if any key is not in env", () => { 17 | const result0 = ensureEnvKeysExist(env, ["test"]) 18 | expect(result0).toBe(false) 19 | 20 | const result1 = ensureEnvKeysExist(env, [""]) 21 | expect(result1).toBe(false) 22 | }) 23 | }) 24 | 25 | describe(".ensureEnvKeysAreInt", () => { 26 | const env = { 27 | COMMIT: "9d2f95a4019935f91ed10e9d716d2b7551dcbcc1", 28 | REPO_SLUG: "bar", 29 | PR_ID: "30", 30 | PR_DESTINATION_BRANCH: "develop", 31 | REPO_OWNER: "foo", 32 | } 33 | 34 | test("return true if key is in env and value is int", () => { 35 | const result = ensureEnvKeysAreInt(env, ["PR_ID"]) 36 | expect(result).toBe(true) 37 | }) 38 | 39 | test("return false if any key is not in env", () => { 40 | const result0 = ensureEnvKeysAreInt(env, ["test"]) 41 | expect(result0).toBe(false) 42 | 43 | const result1 = ensureEnvKeysAreInt(env, [""]) 44 | expect(result1).toBe(false) 45 | }) 46 | 47 | test("return false if any key is in env, but vlaue is not int", () => { 48 | const result = ensureEnvKeysAreInt(env, ["REPO_OWNER"]) 49 | expect(result).toBe(false) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /source/ci_source/_tests/fixtures/dummy_ci.d.ts: -------------------------------------------------------------------------------- 1 | export default class DummyCI {} 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 18 | /** What unique id can be found for the code review platform's PR */ 19 | readonly pullRequestID: string 20 | 21 | readonly commitHash?: string 22 | 23 | /** allows the source to do some setup */ 24 | setup?(): Promise 25 | 26 | /** Optional URL for the CI run, for a status update link */ 27 | readonly ciRunURL?: string 28 | 29 | /** Supports running without a Pull Request */ 30 | readonly useEventDSL?: boolean 31 | } 32 | -------------------------------------------------------------------------------- /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 | // eslint-disable-next-line 34 | const externalModule = require(path) // @typescript-eslint/no-var-requires @typescript-eslint/no-require-imports 35 | const moduleConstructor = externalModule.default || externalModule 36 | resolve(new moduleConstructor(env)) 37 | } 38 | resolve(undefined) 39 | }) 40 | }) 41 | } 42 | 43 | /** 44 | * Gets a CI Source. 45 | * @export 46 | * @param {Env} env The environment. 47 | * @param {string} modulePath relative path to CI provider 48 | * @returns {Promise} a CI source if module loaded successfully, undefined otherwise 49 | */ 50 | export async function getCISource(env: Env, modulePath: string | undefined): Promise { 51 | if (modulePath) { 52 | const external = await getCISourceForExternal(env, modulePath) 53 | if (external) { 54 | return external 55 | } 56 | } 57 | 58 | return getCISourceForEnv(env) 59 | } 60 | -------------------------------------------------------------------------------- /source/ci_source/providers/AppCenter.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, getPullRequestIDForBranch } from "../ci_source_helpers" 3 | import * as url from "url" 4 | 5 | // AppCenter Build scripts: https://docs.microsoft.com/en-us/appcenter/build/custom/scripts/ 6 | // AppCenter Environment variables: https://docs.microsoft.com/en-us/appcenter/build/custom/variables/ 7 | 8 | /** 9 | * ### CI Setup 10 | * 11 | * To make Danger run, add following lines to the `appcenter-pre-build.sh` file: 12 | * 13 | * ``` 14 | * - cd $APPCENTER_SOURCE_DIRECTORY 15 | * - npm install -g danger 16 | * - swift build 17 | * - swift run danger-swift ci 18 | * ``` 19 | * 20 | * 21 | * 22 | * ### Token Setup 23 | * 24 | * Add the `DANGER_GITHUB_API_TOKEN` to your environment variables. 25 | * 26 | */ 27 | 28 | export class AppCenter implements CISource { 29 | private default = { prID: "0" } 30 | constructor(private readonly env: Env) {} 31 | 32 | async setup(): Promise { 33 | const prID = await getPullRequestIDForBranch(this, this.env, this.branchName) 34 | this.default.prID = prID.toString() 35 | } 36 | 37 | get name(): string { 38 | return "AppCenter" 39 | } 40 | 41 | get isCI(): boolean { 42 | if (ensureEnvKeysExist(this.env, ["APPCENTER_BUILD_ID"])) { 43 | return true 44 | } else { 45 | return false 46 | } 47 | } 48 | 49 | get isPR(): boolean { 50 | return this.env["BUILD_REASON"] == "PullRequest" 51 | } 52 | 53 | get pullRequestID(): string { 54 | return this.default.prID 55 | } 56 | 57 | get repoSlug(): string { 58 | if ( 59 | ensureEnvKeysExist(this.env, ["BUILD_REPOSITORY_NAME"]) && 60 | ensureEnvKeysExist(this.env, ["BUILD_REPOSITORY_URI"]) 61 | ) { 62 | const repositoryName = this.env["BUILD_REPOSITORY_NAME"] 63 | const components = url.parse(this.env["BUILD_REPOSITORY_URI"], false) 64 | if (components && components.path) { 65 | const owner = components.path.split("/")[1] 66 | return `${owner}/${repositoryName}` 67 | } 68 | } 69 | return "" 70 | } 71 | 72 | private get branchName(): string { 73 | return this.env["BUILD_SOURCEBRANCHNAME"] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /source/ci_source/providers/BitbucketPipelines.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 `bitbucket-pipelines.yml`. 8 | * For improving the performance, you may need to cache `node_modules`. 9 | * 10 | * ```yml 11 | * image: node:10.15.0 12 | * pipelines: 13 | * pull-requests: 14 | * "**": 15 | * - step: 16 | * caches: 17 | * - node 18 | * script: 19 | * - export LANG="C.UTF-8" 20 | * - yarn install 21 | * - yarn danger ci 22 | * definitions: 23 | * caches: 24 | * node: node_modules 25 | * ``` 26 | * 27 | * ### Token Setup 28 | * 29 | * You can add `DANGER_BITBUCKETCLOUD_USERNAME` and `DANGER_BITBUCKETCLOUD_PASSWORD` 30 | * or add `DANGER_BITBUCKETCLOUD_OAUTH_KEY` and `DANGER_BITBUCKETCLOUD_OAUTH_SECRET` 31 | * or add `DANGER_BITBUCKETCLOUD_REPO_ACCESSTOKEN` 32 | * - 33 | */ 34 | 35 | export class BitbucketPipelines implements CISource { 36 | constructor(private readonly env: Env) {} 37 | 38 | get name(): string { 39 | return "bitbucketPipelines" 40 | } 41 | 42 | get isCI(): boolean { 43 | return ensureEnvKeysExist(this.env, ["BITBUCKET_BUILD_NUMBER"]) 44 | } 45 | 46 | get isPR(): boolean { 47 | const mustHave = ["BITBUCKET_GIT_HTTP_ORIGIN", "BITBUCKET_REPO_OWNER", "BITBUCKET_REPO_SLUG"] 48 | const mustBeInts = ["BITBUCKET_PR_ID"] 49 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 50 | } 51 | 52 | get pullRequestID(): string { 53 | return this.env.BITBUCKET_PR_ID 54 | } 55 | 56 | get repoSlug(): string { 57 | return `${this.env.BITBUCKET_REPO_FULL_NAME}` 58 | } 59 | 60 | get repoURL(): string { 61 | return this.env.BITBUCKET_GIT_HTTP_ORIGIN 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /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 | * 6 | * You need to edit your `bitrise.yml` (in version control, or directly from UI) to include `yarn danger ci`. 7 | * 8 | * You can set `is_always_run: true` to ensure that it reports even if previous steps fails 9 | * 10 | * ```yaml 11 | * workflows: 12 | * : 13 | * steps: 14 | * - yarn: 15 | * inputs: 16 | * - args: ci 17 | * - command: danger 18 | * is_always_run: true 19 | * ``` 20 | * 21 | * Adding this to your `bitrise.yml` allows Danger to fail your build, both on the Bitrise website and within your Pull Request. 22 | * With that set up, you can edit your job to add `yarn danger ci` at the build action. 23 | * 24 | * 25 | * 26 | * 27 | * No instructions yet, but basically: 28 | * 29 | * - Install Danger JS globally 30 | * - Run `swift build` 31 | * - Run `swift run danger-swift ci` 32 | * 33 | * 34 | * 35 | * ### Token Setup 36 | * 37 | * You need to add the platform environment variables, to do this, 38 | * go to your repo's secrets, which should look like: `https://www.bitrise.io/app/[app_id]#/workflow` and secrets tab. 39 | * 40 | * You should make sure to check the case "Expose for Pull Requests?". 41 | */ 42 | export class Bitrise implements CISource { 43 | constructor(private readonly env: Env) {} 44 | 45 | get name(): string { 46 | return "Bitrise" 47 | } 48 | 49 | get isCI(): boolean { 50 | return ensureEnvKeysExist(this.env, ["BITRISE_IO"]) 51 | } 52 | 53 | get isPR(): boolean { 54 | const mustHave = ["BITRISEIO_GIT_REPOSITORY_OWNER", "BITRISEIO_GIT_REPOSITORY_SLUG"] 55 | const mustBeInts = ["BITRISE_PULL_REQUEST"] 56 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 57 | } 58 | 59 | get pullRequestID(): string { 60 | return this.env.BITRISE_PULL_REQUEST 61 | } 62 | 63 | get repoSlug(): string { 64 | return `${this.env.BITRISEIO_GIT_REPOSITORY_OWNER}/${this.env.BITRISEIO_GIT_REPOSITORY_SLUG}` 65 | } 66 | 67 | get ciRunURL() { 68 | return this.env.BITRISE_BUILD_URL 69 | } 70 | 71 | get commitHash() { 72 | return this.env.BITRISE_GIT_COMMIT 73 | } 74 | } 75 | 76 | // See https://devcenter.bitrise.io/builds/available-environment-variables/ 77 | -------------------------------------------------------------------------------- /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 | * 16 | * Add the `DANGER_GITHUB_API_TOKEN` to your build user's ENV. 17 | * 18 | */ 19 | export class BuddyBuild implements CISource { 20 | constructor(private readonly env: Env) {} 21 | 22 | get name(): string { 23 | return "buddybuild" 24 | } 25 | 26 | get isCI(): boolean { 27 | return ensureEnvKeysExist(this.env, ["BUDDYBUILD_BUILD_ID"]) 28 | } 29 | 30 | get isPR(): boolean { 31 | const mustHave = ["BUDDYBUILD_PULL_REQUEST", "BUDDYBUILD_REPO_SLUG"] 32 | const mustBeInts = ["BUDDYBUILD_PULL_REQUEST"] 33 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 34 | } 35 | 36 | get pullRequestID(): string { 37 | return this.env.BUDDYBUILD_PULL_REQUEST 38 | } 39 | 40 | get repoSlug(): string { 41 | return this.env.BUDDYBUILD_REPO_SLUG 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/ci_source/providers/BuddyWorks.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | /** 4 | * ### CI Setup 5 | * 6 | * To make Danger run: 7 | * 8 | * - Create a new pipeline named, DangerJS, which is triggered on every push 9 | * - Add a NodeJS environment as an Action 10 | * - Go into it, head over to the bash editor and type the following 11 | * - `yarn install && yarn danger ci` 12 | * - or your npm script 13 | * - Set the `DANGER_GITHUB_API_TOKEN` at the Variables section 14 | * - You're done 🎉 15 | * 16 | */ 17 | export class BuddyWorks implements CISource { 18 | constructor(private readonly env: Env) {} 19 | 20 | get name(): string { 21 | return "Buddy.works" 22 | } 23 | 24 | get isCI(): boolean { 25 | return ensureEnvKeysExist(this.env, ["BUDDY_PIPELINE_ID"]) 26 | } 27 | 28 | get isPR(): boolean { 29 | const mustHave = ["BUDDY_PIPELINE_ID", "BUDDY_EXECUTION_PULL_REQUEST_NO", "BUDDY_REPO_SLUG"] 30 | const mustBeInts = ["BUDDY_EXECUTION_PULL_REQUEST_NO"] 31 | 32 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 33 | } 34 | 35 | get pullRequestID(): string { 36 | return this.env.BUDDY_EXECUTION_PULL_REQUEST_NO 37 | } 38 | 39 | get repoSlug(): string { 40 | return this.env.BUDDY_REPO_SLUG 41 | } 42 | 43 | get ciRunURL() { 44 | return this.env.BUDDY_EXECUTION_URL 45 | } 46 | } 47 | 48 | // Default ENV vars provided by Buddy.works 49 | // https://buddy.works/docs/pipelines/environment-variables#default-environment-variables 50 | -------------------------------------------------------------------------------- /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 script: 9 | * 10 | * ``` shell 11 | * echo "--- Running Danger" 12 | * [run_command] 13 | * ``` 14 | * 15 | * ### Token Setup 16 | * 17 | * #### GitHub 18 | * 19 | * As this is self-hosted, you will need to add the API tokens 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" [run_command]`. 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/ci_source/providers/Cirrus.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * You need to edit your `.cirrus.yml` to add a `script` like this: 8 | * 9 | * ```yaml 10 | * danger_script: 11 | * - yarn danger ci 12 | * ``` 13 | * 14 | * ### Token Setup 15 | * 16 | * You need to add the `DANGER_GITHUB_API_TOKEN` environment variable, to do this, 17 | * go to your repo's settings, by clicking the gear at `https://cirrus-ci.com/github/[user]/[repo]`. 18 | * Generate the encrypted value, and add it to your `env` block. 19 | * 20 | * Once you have added it, trigger a build. 21 | */ 22 | export class Cirrus implements CISource { 23 | constructor(private readonly env: Env) {} 24 | 25 | get name(): string { 26 | return "Cirrus CI" 27 | } 28 | 29 | get isCI(): boolean { 30 | return ensureEnvKeysExist(this.env, ["CIRRUS_CI"]) 31 | } 32 | 33 | get isPR(): boolean { 34 | const mustHave = ["CIRRUS_CI", "CIRRUS_PR", "CIRRUS_REPO_FULL_NAME"] 35 | const mustBeInts = ["CIRRUS_PR"] 36 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 37 | } 38 | 39 | get pullRequestID(): string { 40 | return this.env.CIRRUS_PR 41 | } 42 | 43 | get repoSlug(): string { 44 | return this.env.CIRRUS_REPO_FULL_NAME 45 | } 46 | 47 | get ciRunURL() { 48 | return `https://cirrus-ci.com/task/${this.env.CIRRUS_TASK_ID}` 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /source/ci_source/providers/Codefresh.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * To set up Danger on Codefresh, create a freestyle step in your Codefresh yaml configuration: 8 | * 9 | * ```yml 10 | * Danger: 11 | * title: Run Danger 12 | * image: node:latest 13 | * working_directory: ${{main_clone}} 14 | * entry_point: '/bin/bash' 15 | * cmd: 16 | * - '-ce' 17 | * - | 18 | * npm install -g yarn 19 | * yarn add danger --dev 20 | * yarn danger ci --failOnErrors 21 | * when: 22 | * steps: 23 | * - name: main_clone 24 | * on: 25 | * - success 26 | * ``` 27 | * 28 | * The `failOnErrors` option is required in order to ensure that the step fails properly when Danger fails. If you don't want this behavior, you can remove this option. 29 | * 30 | * Don't forget to add the `DANGER_GITHUB_API_TOKEN` variable to your pipeline settings so that Danger can properly post comments to your pull request. 31 | * 32 | */ 33 | 34 | export class Codefresh implements CISource { 35 | constructor(private readonly env: Env) {} 36 | 37 | get name(): string { 38 | return "Codefresh" 39 | } 40 | 41 | get isCI(): boolean { 42 | return ensureEnvKeysExist(this.env, ["CF_BUILD_ID", "CF_BUILD_URL"]) 43 | } 44 | 45 | get isPR(): boolean { 46 | return ( 47 | ensureEnvKeysExist(this.env, ["CF_PULL_REQUEST_NUMBER"]) && 48 | ensureEnvKeysAreInt(this.env, ["CF_PULL_REQUEST_NUMBER"]) 49 | ) 50 | } 51 | 52 | get pullRequestID(): string { 53 | if (this.env.CF_PULL_REQUEST_NUMBER) { 54 | return this.env.CF_PULL_REQUEST_NUMBER 55 | } else { 56 | return "" 57 | } 58 | } 59 | 60 | get repoSlug(): string { 61 | const splitSlug = this.env.CF_COMMIT_URL.split("/") 62 | if (splitSlug.length === 7) { 63 | const owner = splitSlug[3] 64 | const reponame = splitSlug[4] 65 | return owner && reponame ? `${owner}/${reponame}` : "" 66 | } 67 | return "" 68 | } 69 | 70 | get ciRunURL() { 71 | return this.env.CF_BUILD_URL 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /source/ci_source/providers/Codemagic.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * Codemagic.io CI Integration 6 | * 7 | * Environment Variables Documented: https://docs.codemagic.io/building/environment-variables/ 8 | * Notice a bug in the docs?: https://github.com/codemagic-ci-cd/codemagic-docs 9 | * 10 | * Need support/advice? https://slack.codemagic.io/ 11 | */ 12 | export class Codemagic implements CISource { 13 | constructor(private readonly env: Env) {} 14 | 15 | get name(): string { 16 | return "Codemagic" 17 | } 18 | 19 | get isCI(): boolean { 20 | // Codemagic developer relations confirmed this is fine to use for this purpose 21 | return ensureEnvKeysExist(this.env, ["FCI_BUILD_ID"]) 22 | } 23 | 24 | get isPR(): boolean { 25 | const mustHave = ["FCI_PULL_REQUEST", "FCI_REPO_SLUG", "FCI_PROJECT_ID", "FCI_BUILD_ID"] 26 | const mustBeInts = ["BUILD_NUMBER", "FCI_PULL_REQUEST_NUMBER"] 27 | return ( 28 | ensureEnvKeysExist(this.env, mustHave) && 29 | ensureEnvKeysAreInt(this.env, mustBeInts) && 30 | this.env.FCI_PULL_REQUEST === "true" 31 | ) 32 | } 33 | 34 | get pullRequestID(): string { 35 | return this.env.FCI_PULL_REQUEST_NUMBER 36 | } 37 | 38 | get repoSlug(): string { 39 | return this.env.FCI_REPO_SLUG 40 | } 41 | 42 | get ciRunURL() { 43 | const { FCI_BUILD_ID, FCI_PROJECT_ID } = process.env 44 | return `https://codemagic.io/app/${FCI_PROJECT_ID}/build/${FCI_BUILD_ID}` 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/ci_source/providers/Codeship.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, getPullRequestIDForBranch } from "../ci_source_helpers" 3 | 4 | // Codeship Pro: https://documentation.codeship.com/pro/builds-and-configuration/environment-variables/ 5 | // Codeship Basic: https://documentation.codeship.com/basic/builds-and-configuration/set-environment-variables/ 6 | 7 | /** 8 | * ### CI Setup 9 | * 10 | * To make Danger run, add a new step to the `codeship-steps.yml` file: 11 | * 12 | * ``` 13 | * - type: parallel: 14 | * ... 15 | * - name: danger 16 | * service: web 17 | * command: [run_command] 18 | * ``` 19 | * 20 | * If you're using Codeship Classic, add `[run_command]` to your 'Test Commands' 21 | * 22 | * ### Token Setup 23 | * 24 | * You'll want to edit your `codeship-services.yml` file to include a reference 25 | * to the Danger authentication token: `DANGER_GITHUB_API_TOKEN`. 26 | * 27 | * ``` 28 | * project_name: 29 | * ... 30 | * environment: 31 | * - DANGER_GITHUB_API_TOKEN=[my_token] 32 | * ``` 33 | * 34 | * If you're using Codeship Classic, add `DANGER_GITHUB_API_TOKEN` to your 35 | * 'Environment' settings. 36 | */ 37 | 38 | export class Codeship implements CISource { 39 | private default = { prID: "0" } 40 | constructor(private readonly env: Env) {} 41 | 42 | async setup(): Promise { 43 | const prID = await getPullRequestIDForBranch(this, this.env, this.branchName) 44 | this.default.prID = prID.toString() 45 | } 46 | 47 | get name(): string { 48 | return "Codeship" 49 | } 50 | 51 | get isCI(): boolean { 52 | if (ensureEnvKeysExist(this.env, ["CI_NAME"]) && this.env.CI_NAME === "codeship") { 53 | return true 54 | } 55 | return false 56 | } 57 | 58 | get isPR(): boolean { 59 | return this.pullRequestID !== "0" 60 | } 61 | 62 | get pullRequestID(): string { 63 | return this.default.prID 64 | } 65 | 66 | get repoSlug(): string { 67 | if (ensureEnvKeysExist(this.env, ["CI_REPO_NAME"])) { 68 | return this.env.CI_REPO_NAME 69 | } 70 | return "" 71 | } 72 | 73 | private get branchName(): string { 74 | return this.env.CI_BRANCH 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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 `[run_command]` within one of your build jobs. 12 | * 13 | * ``` shell 14 | * build: 15 | * image: golang 16 | * commands: 17 | * - ... 18 | * - [run_command] 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/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 chosen as your `sut` Dockerfile.) 10 | * 11 | * ```sh 12 | * ... 13 | * 14 | * CMD [run_command_split] 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 | -------------------------------------------------------------------------------- /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 `[run_command]` at the end of 9 | * your `.drone.yml`. 10 | * 11 | * ``` shell 12 | * build: 13 | * image: golang 14 | * commands: 15 | * - ... 16 | * - [run_command] 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/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 || env.DANGER_MANUAL_GH_REPO || "artsy/emission", // TODO: default to empty string ? 10 | pr: env.DANGER_TEST_PR || env.DANGER_MANUAL_PR_NUM || "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 ( 21 | ensureEnvKeysExist(this.env, ["DANGER_FAKE_CI"]) || 22 | ensureEnvKeysExist(this.env, ["DANGER_LOCAL_NO_CI"]) || 23 | ensureEnvKeysExist(this.env, ["DANGER_MANUAL_CI"]) 24 | ) 25 | } 26 | 27 | get isPR(): boolean { 28 | return true 29 | } 30 | 31 | get pullRequestID(): string { 32 | return this.env.pr 33 | } 34 | 35 | get repoSlug(): string { 36 | return this.env.repo 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/ci_source/providers/GitLabCI.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | export class GitLabCI implements CISource { 5 | constructor(private readonly env: Env) {} 6 | 7 | get name(): string { 8 | return "GitLab CI" 9 | } 10 | 11 | get isCI(): boolean { 12 | return ensureEnvKeysExist(this.env, ["GITLAB_CI"]) 13 | } 14 | 15 | get isPR(): boolean { 16 | const mustHave = ["CI_MERGE_REQUEST_IID", "CI_PROJECT_PATH"] 17 | const mustBeInts = ["CI_MERGE_REQUEST_IID"] 18 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 19 | } 20 | 21 | get pullRequestID(): string { 22 | return this.env.CI_MERGE_REQUEST_IID 23 | } 24 | 25 | get repoSlug(): string { 26 | return this.env.CI_MERGE_REQUEST_PROJECT_PATH || this.env.CI_PROJECT_PATH 27 | } 28 | 29 | get commitHash(): string { 30 | return this.env.CI_COMMIT_SHA 31 | } 32 | } 33 | 34 | // See https://docs.gitlab.com/ee/ci/variables/predefined_variables.html 35 | -------------------------------------------------------------------------------- /source/ci_source/providers/Netlify.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 1. Log in to your Netlify account and add the `DANGER_GITHUB_API_TOKEN` 7 | * environment variable to your site's deploy settings. 8 | * `https://app.netlify.com/sites/[site-name]/settings/deploys#build-environment-variables`. 9 | * 10 | * 2. Prepend `yarn danger ci && ` to your build command in the Netlify web UI 11 | * or in your netlify.toml. For example, `yarn danger ci && yarn build` 12 | */ 13 | export class Netlify implements CISource { 14 | constructor(private readonly env: Env) {} 15 | 16 | get name(): string { 17 | return "Netlify" 18 | } 19 | 20 | get isCI(): boolean { 21 | return ensureEnvKeysExist(this.env, ["NETLIFY_BUILD_BASE"]) 22 | } 23 | 24 | get isPR(): boolean { 25 | const mustHave = ["REVIEW_ID", "REPOSITORY_URL"] 26 | const mustBeInts = ["REVIEW_ID"] 27 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 28 | } 29 | 30 | get pullRequestID(): string { 31 | return this.env.REVIEW_ID 32 | } 33 | 34 | get repoSlug(): string { 35 | return this.env.REPOSITORY_URL.replace(/^https:\/\/[^/]+\//, "") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 | * Note: The company that runs Nevercode is migrating all customers 10 | * to their new service Codemagic.io in Spring of 2021 11 | * - billing is migrated through Customer Support 12 | * - the CI Configuration is managed in your repo instead of in a web-dashboard. 13 | * 14 | * TODO @fbartho delete this integration when it's fully offline. 15 | */ 16 | export class Nevercode implements CISource { 17 | constructor(private readonly env: Env) {} 18 | 19 | get name(): string { 20 | return "Nevercode" 21 | } 22 | 23 | get isCI(): boolean { 24 | return ensureEnvKeysExist(this.env, ["NEVERCODE"]) 25 | } 26 | 27 | get isPR(): boolean { 28 | const mustHave = ["NEVERCODE_PULL_REQUEST", "NEVERCODE_REPO_SLUG"] 29 | const mustBeInts = ["NEVERCODE_GIT_PROVIDER_PULL_REQUEST", "NEVERCODE_PULL_REQUEST_NUMBER"] 30 | return ( 31 | ensureEnvKeysExist(this.env, mustHave) && 32 | ensureEnvKeysAreInt(this.env, mustBeInts) && 33 | this.env.NEVERCODE_PULL_REQUEST == "true" 34 | ) 35 | } 36 | 37 | get pullRequestID(): string { 38 | return this.env.NEVERCODE_PULL_REQUEST_NUMBER 39 | } 40 | 41 | get repoSlug(): string { 42 | return this.env.NEVERCODE_REPO_SLUG 43 | } 44 | 45 | get ciRunURL() { 46 | return process.env.NEVERCODE_BUILD_URL 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | * 9 | * ```yml 10 | * jobs: 11 | * danger: 12 | * requires: [~pr, ~commit] 13 | * steps: 14 | * - setup: yarn install 15 | * - danger: yarn danger ci 16 | * secrets: 17 | * - DANGER_GITHUB_API_TOKEN 18 | * ``` 19 | * 20 | * ### Token Setup 21 | * 22 | * Add the `DANGER_GITHUB_API_TOKEN` to your pipeline env as a 23 | * [build secret](https://docs.screwdriver.cd/user-guide/configuration/secrets) 24 | */ 25 | export class Screwdriver implements CISource { 26 | constructor(private readonly env: Env) {} 27 | 28 | get name(): string { 29 | return "Screwdriver" 30 | } 31 | 32 | get isCI(): boolean { 33 | return ensureEnvKeysExist(this.env, ["SCREWDRIVER"]) 34 | } 35 | 36 | get isPR(): boolean { 37 | const mustHave = ["SCM_URL"] 38 | const mustBeInts = ["SD_PULL_REQUEST"] 39 | return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) 40 | } 41 | 42 | private _parseRepoURL(): string { 43 | const repoURL = this.env.SCM_URL 44 | const regexp = new RegExp("([/:])([^/]+/[^/.]+)(?:.git)?$") 45 | const matches = repoURL.match(regexp) 46 | return matches ? matches[2] : "" 47 | } 48 | 49 | get pullRequestID(): string { 50 | return this.env.SD_PULL_REQUEST 51 | } 52 | 53 | get repoSlug(): string { 54 | return this._parseRepoURL() 55 | } 56 | 57 | get ciRunURL() { 58 | return process.env.BUILDKITE_BUILD_URL 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 `[run_command]` 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/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 `[run_command]` 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" [run_command]`. 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/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 | export class TeamCity implements CISource { 19 | constructor(private readonly env: Env) {} 20 | 21 | get name(): string { 22 | return "TeamCity" 23 | } 24 | 25 | get isCI(): boolean { 26 | return ensureEnvKeysExist(this.env, ["TEAMCITY_VERSION"]) 27 | } 28 | 29 | get isPR(): boolean { 30 | if (ensureEnvKeysExist(this.env, ["PULL_REQUEST_URL"])) { 31 | return true 32 | } 33 | 34 | const mustHave = ["PULL_REQUEST_URL"] 35 | return ensureEnvKeysExist(this.env, mustHave) 36 | } 37 | 38 | get pullRequestID(): string { 39 | const parts = pullRequestParser(this.env.PULL_REQUEST_URL || "") 40 | 41 | if (parts === null) { 42 | return "" 43 | } 44 | 45 | return parts.pullRequestNumber 46 | } 47 | 48 | get repoSlug(): string { 49 | const parts = pullRequestParser(this.env.PULL_REQUEST_URL || "") 50 | 51 | if (parts === null) { 52 | return "" 53 | } 54 | 55 | return parts.repo 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | * 6 | * You'll need to add a build step and set the custom command to `[run_command]`. 7 | * 8 | * Danger only supports using VSTS with GitHub as the repository, Danger doesn't yet support VSTS as a repository 9 | * platform for providing feedback 10 | * 11 | * ### Token Setup 12 | * 13 | * You need to add the `DANGER_GITHUB_API_TOKEN` environment variable 14 | */ 15 | export class VSTS implements CISource { 16 | constructor(private readonly env: Env) {} 17 | 18 | get name(): string { 19 | return "Visual Studio Team Services" 20 | } 21 | 22 | get isCI(): boolean { 23 | return ( 24 | ensureEnvKeysExist(this.env, ["SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", "BUILD_REPOSITORY_PROVIDER"]) && 25 | this.env.BUILD_REPOSITORY_PROVIDER == "GitHub" 26 | ) 27 | } 28 | 29 | get isPR(): boolean { 30 | const mustHave = ["BUILD_SOURCEBRANCH", "BUILD_REPOSITORY_PROVIDER", "BUILD_REASON", "BUILD_REPOSITORY_NAME"] 31 | 32 | return ensureEnvKeysExist(this.env, mustHave) && this.env.BUILD_REASON == "PullRequest" 33 | } 34 | 35 | get pullRequestID(): string { 36 | const match = this.env.BUILD_SOURCEBRANCH.match(/refs\/pull\/([0-9]+)\/merge/) 37 | 38 | if (match && match.length > 1) { 39 | return match[1] 40 | } 41 | 42 | return "" 43 | } 44 | 45 | get repoSlug(): string { 46 | return this.env.BUILD_REPOSITORY_NAME 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /source/ci_source/providers/XcodeCloud.ts: -------------------------------------------------------------------------------- 1 | import { Env, CISource } from "../ci_source" 2 | import { ensureEnvKeysExist } from "../ci_source_helpers" 3 | 4 | /** 5 | * ### CI Setup 6 | * 7 | * Install dependencies and add a danger step to the custom build scripts. 8 | * See the Xcode Cloud documentation [here](https://developer.apple.com/documentation/xcode/writing-custom-build-scripts) 9 | * 10 | * ### Token Setup 11 | * 12 | * Setup the acesss token (for github `DANGER_GITHUB_API_TOKEN`) environment variable for your workflow. 13 | * See the Xcode Cloud documentation [here](https://developer.apple.com/documentation/xcode/xcode-cloud-workflow-reference#Custom-Environment-Variables) 14 | */ 15 | export class XcodeCloud implements CISource { 16 | constructor(private readonly env: Env) {} 17 | 18 | get name(): string { 19 | return "Xcode Cloud" 20 | } 21 | 22 | get isCI(): boolean { 23 | const mustHave = ["CI", "CI_XCODEBUILD_ACTION"] 24 | return ensureEnvKeysExist(this.env, mustHave) && this.env.CI == "TRUE" 25 | } 26 | 27 | get isPR(): boolean { 28 | const mustHave = ["CI_PULL_REQUEST_NUMBER", "CI_PULL_REQUEST_TARGET_REPO"] 29 | return ensureEnvKeysExist(this.env, mustHave) 30 | } 31 | 32 | get repoSlug(): string { 33 | return this.env.CI_PULL_REQUEST_TARGET_REPO 34 | } 35 | 36 | get pullRequestID(): string { 37 | return this.env.CI_PULL_REQUEST_NUMBER 38 | } 39 | 40 | get commitHash(): string { 41 | return this.env.CI_COMMIT 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/ci_source/providers/_tests/_codemagic.test.ts: -------------------------------------------------------------------------------- 1 | import { Codemagic } from "../Codemagic" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | FCI_PROJECT_ID: "abcdef123-app-uuid", 6 | FCI_BUILD_ID: "abcdef123-build-uuid", 7 | FCI_REPO_SLUG: "danger/danger-js", 8 | FCI_PULL_REQUEST: "true", 9 | FCI_PULL_REQUEST_NUMBER: "2", 10 | BUILD_NUMBER: "42", 11 | } 12 | 13 | describe("being found when looking for CI", () => { 14 | it("finds Codemagic with the right ENV", () => { 15 | const ci = getCISourceForEnv(correctEnv) 16 | expect(ci).toBeInstanceOf(Codemagic) 17 | }) 18 | }) 19 | 20 | describe(".isCI", () => { 21 | it("validates when all Codemagic environment vars are set", () => { 22 | const codemagic = new Codemagic(correctEnv) 23 | expect(codemagic.isCI).toBeTruthy() 24 | }) 25 | 26 | it("does not validate without env", () => { 27 | const codemagic = new Codemagic({}) 28 | expect(codemagic.isCI).toBeFalsy() 29 | }) 30 | }) 31 | 32 | describe(".isPR", () => { 33 | it("validates when all codemagic environment vars are set", () => { 34 | const codemagic = new Codemagic(correctEnv) 35 | expect(codemagic.isPR).toBeTruthy() 36 | }) 37 | 38 | it("does not validate outside of codemagic", () => { 39 | const codemagic = new Codemagic({}) 40 | expect(codemagic.isPR).toBeFalsy() 41 | }) 42 | 43 | const envs = [ 44 | "FCI_PULL_REQUEST", 45 | "FCI_REPO_SLUG", 46 | "FCI_PROJECT_ID", 47 | "FCI_BUILD_ID", 48 | "BUILD_NUMBER", 49 | "FCI_PULL_REQUEST_NUMBER", 50 | ] 51 | envs.forEach((key: string) => { 52 | let env = Object.assign({}, correctEnv) 53 | delete env[key] 54 | 55 | it(`does not validate when ${key} is missing`, () => { 56 | const codemagic = new Codemagic(env) 57 | expect(codemagic.isCI && codemagic.isPR).toBeFalsy() 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /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/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/_gitlab.test.ts: -------------------------------------------------------------------------------- 1 | import { GitLabCI } from "../GitLabCI" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | GITLAB_CI: "true", 6 | CI_MERGE_REQUEST_IID: "27117", 7 | CI_PROJECT_PATH: "gitlab-org/gitlab-foss", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds GitLab with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(GitLabCI) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | it("validates when all GitLab environment vars are set", async () => { 19 | const result = new GitLabCI(correctEnv) 20 | expect(result.isCI).toBeTruthy() 21 | }) 22 | 23 | it("does not validate without env", async () => { 24 | const result = new GitLabCI({}) 25 | expect(result.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".pullRequestID", () => { 30 | it("pulls it out of the env", () => { 31 | const result = new GitLabCI(correctEnv) 32 | expect(result.pullRequestID).toEqual("27117") 33 | }) 34 | }) 35 | 36 | describe(".repoSlug", () => { 37 | it("derives it from 'CI_PROJECT_PATH' env var", () => { 38 | const result = new GitLabCI(correctEnv) 39 | expect(result.repoSlug).toEqual("gitlab-org/gitlab-foss") 40 | }) 41 | 42 | it("derives it form 'CI_MERGE_REQUEST_PROJECT_PATH' env var if set", () => { 43 | correctEnv["CI_MERGE_REQUEST_PROJECT_PATH"] = "gitlab-org/release-tools" 44 | const result = new GitLabCI(correctEnv) 45 | expect(result.repoSlug).toEqual("gitlab-org/release-tools") 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /source/ci_source/providers/_tests/_netlify.test.ts: -------------------------------------------------------------------------------- 1 | import { Netlify } from "../Netlify" 2 | import { getCISourceForEnv } from "../../get_ci_source" 3 | 4 | const correctEnv = { 5 | NETLIFY_BUILD_BASE: "opt/build", 6 | REPOSITORY_URL: "https://github.com/someone/something", 7 | REVIEW_ID: "999", 8 | } 9 | 10 | describe("being found when looking for CI", () => { 11 | it("finds Netlify with the right ENV", () => { 12 | const ci = getCISourceForEnv(correctEnv) 13 | expect(ci).toBeInstanceOf(Netlify) 14 | }) 15 | }) 16 | 17 | describe(".isCI", () => { 18 | it("validates when all Netlify environment vars are set", () => { 19 | const netlify = new Netlify(correctEnv) 20 | expect(netlify.isCI).toBeTruthy() 21 | }) 22 | 23 | it("does not validate", () => { 24 | const netlify = new Netlify({}) 25 | expect(netlify.isCI).toBeFalsy() 26 | }) 27 | }) 28 | 29 | describe(".isPR", () => { 30 | it("validates when all Netlify environment vars are set", () => { 31 | const netlify = new Netlify(correctEnv) 32 | expect(netlify.isPR).toBeTruthy() 33 | }) 34 | 35 | it("does not validate outside of Netlify", () => { 36 | const netlify = new Netlify({}) 37 | expect(netlify.isPR).toBeFalsy() 38 | }) 39 | 40 | const envs = ["REPOSITORY_URL", "REVIEW_ID"] 41 | envs.forEach((key: string) => { 42 | let env = { 43 | REPOSITORY_URL: "https://github.com/someone/something", 44 | REVIEW_ID: "999", 45 | } 46 | env[key] = null 47 | 48 | it(`does not validate when ${key} is missing`, () => { 49 | const netlify = new Netlify(env) 50 | expect(netlify.isPR).toBeFalsy() 51 | }) 52 | 53 | it("needs to have a PR number", () => { 54 | let env = { 55 | REPOSITORY_URL: "https://github.com/someone/something", 56 | REVIEW_ID: "asdf", 57 | } 58 | const netlify = new Netlify(env) 59 | expect(netlify.isPR).toBeFalsy() 60 | }) 61 | }) 62 | }) 63 | 64 | describe(".pullRequestID", () => { 65 | it("pulls it out of the env", () => { 66 | const netlify = new Netlify({ REVIEW_ID: "999" }) 67 | expect(netlify.pullRequestID).toEqual("999") 68 | }) 69 | }) 70 | 71 | describe(".repoSlug", () => { 72 | it("pulls it out of the env", () => { 73 | const netlify = new Netlify({ REPOSITORY_URL: "https://x-access-token:v1.9xxx0@github.com/someone/something" }) 74 | expect(netlify.repoSlug).toEqual("someone/something") 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /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/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/ci_source/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { AppCenter } from "./AppCenter" 2 | import { Bamboo } from "./Bamboo" 3 | import { BitbucketPipelines } from "./BitbucketPipelines" 4 | import { Bitrise } from "./Bitrise" 5 | import { BuddyBuild } from "./BuddyBuild" 6 | import { BuddyWorks } from "./BuddyWorks" 7 | import { Buildkite } from "./Buildkite" 8 | import { Circle } from "./Circle" 9 | import { Cirrus } from "./Cirrus" 10 | import { CodeBuild } from "./CodeBuild" 11 | import { Codefresh } from "./Codefresh" 12 | import { Codeship } from "./Codeship" 13 | import { Codemagic } from "./Codemagic" 14 | import { Concourse } from "./Concourse" 15 | import { DockerCloud } from "./DockerCloud" 16 | import { Drone } from "./Drone" 17 | import { FakeCI } from "./Fake" 18 | import { GitHubActions } from "./GitHubActions" 19 | import { GitLabCI } from "./GitLabCI" 20 | import { Jenkins } from "./Jenkins" 21 | import { Netlify } from "./Netlify" 22 | import { Nevercode } from "./Nevercode" 23 | import { Screwdriver } from "./Screwdriver" 24 | import { Semaphore } from "./Semaphore" 25 | import { Surf } from "./Surf" 26 | import { TeamCity } from "./TeamCity" 27 | import { Travis } from "./Travis" 28 | import { VSTS } from "./VSTS" 29 | import { XcodeCloud } from "./XcodeCloud" 30 | 31 | const providers = [ 32 | FakeCI, 33 | GitHubActions, 34 | GitLabCI, 35 | Travis, 36 | Circle, 37 | Semaphore, 38 | Nevercode, 39 | Jenkins, 40 | Surf, 41 | DockerCloud, 42 | Codeship, 43 | Drone, 44 | Buildkite, 45 | BuddyBuild, 46 | BuddyWorks, 47 | VSTS, 48 | Bitrise, 49 | TeamCity, 50 | Screwdriver, 51 | Concourse, 52 | Netlify, 53 | CodeBuild, 54 | Codefresh, 55 | AppCenter, 56 | BitbucketPipelines, 57 | Cirrus, 58 | Bamboo, 59 | Codemagic, 60 | XcodeCloud, 61 | ] 62 | 63 | // Mainly used for Dangerfile linting 64 | const realProviders = [ 65 | GitHubActions, 66 | GitLabCI, 67 | Travis, 68 | Circle, 69 | Semaphore, 70 | Nevercode, 71 | Jenkins, 72 | Surf, 73 | DockerCloud, 74 | Codeship, 75 | Drone, 76 | Buildkite, 77 | BuddyBuild, 78 | BuddyWorks, 79 | VSTS, 80 | TeamCity, 81 | Screwdriver, 82 | Concourse, 83 | Netlify, 84 | CodeBuild, 85 | Codefresh, 86 | AppCenter, 87 | Cirrus, 88 | Bamboo, 89 | Codemagic, 90 | XcodeCloud, 91 | ] 92 | 93 | export { providers, realProviders } 94 | -------------------------------------------------------------------------------- /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/commands/ci/resetStatus.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( 34 | "pending", 35 | "Danger is waiting for your CI run to complete...", 36 | undefined, 37 | app.id, 38 | source.commitHash 39 | ) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/commands/danger-ci.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import program from "commander" 4 | 5 | import setSharedArgs, { SharedCLI } from "./utils/sharedDangerfileArgs" 6 | import { runRunner } from "./ci/runner" 7 | 8 | program 9 | .usage("[options]") 10 | .description("Runs a Dangerfile in JavaScript or TypeScript.") 11 | .option("--no-publish-check", "Don't add Danger check to PR", false) 12 | .allowUnknownOption(true) 13 | 14 | setSharedArgs(program).parse(process.argv) 15 | 16 | const app = program as any as SharedCLI 17 | runRunner(app) 18 | -------------------------------------------------------------------------------- /source/commands/danger-local.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import 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 | .description("Runs danger without PR metadata, useful for git hooks.") 20 | .option("-s, --staging", "Just use staged changes.") 21 | .option("-b, --base [branch_name]", "Use a different base branch", "master") 22 | .option("-j, --outputJSON", "Outputs the resulting JSON to STDOUT") 23 | .allowUnknownOption(true) 24 | setSharedArgs(program).parse(process.argv) 25 | 26 | const app = program as any as App 27 | const base = app.base 28 | 29 | const localPlatform = new LocalGit({ base, staging: app.staging }) 30 | localPlatform.validateThereAreChanges().then((changes) => { 31 | if (changes) { 32 | const fakeSource = new FakeCI(process.env) 33 | // By setting the custom env var we can be sure that the runner doesn't 34 | // try to find the CI danger is running on and use that. 35 | runRunner(app, { source: fakeSource, platform: localPlatform, additionalEnvVars: { DANGER_LOCAL_NO_CI: "yep" } }) 36 | } else { 37 | console.log(`No git changes detected between head and ${base}.`) 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /source/commands/danger-reset-status.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import program from "commander" 4 | 5 | import setSharedArgs, { SharedCLI } from "./utils/sharedDangerfileArgs" 6 | import { runRunner } from "./ci/resetStatus" 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 | -------------------------------------------------------------------------------- /source/commands/danger.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import 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/commands/init/get-repo-slug.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | import ini from "ini" 4 | import parseGithubURL from "parse-github-url" 5 | 6 | export const getRepoSlug = () => { 7 | try { 8 | const gitConfigPath = path.join(process.cwd(), ".git", "config") 9 | 10 | if (!fs.existsSync(gitConfigPath)) { 11 | return null 12 | } 13 | 14 | const configContent = fs.readFileSync(gitConfigPath, "utf8") 15 | const parsedConfig = ini.parse(configContent) 16 | const remotes: Record = {} 17 | 18 | for (const key in parsedConfig) { 19 | if (key.startsWith('remote "')) { 20 | const remoteName = key.substring(8, key.length - 1) 21 | remotes[remoteName] = parsedConfig[key] 22 | } 23 | } 24 | 25 | const possibleRemoteNames = ["upstream", "origin"] 26 | const possibleRemotes = possibleRemoteNames.map((name) => remotes[name]).filter((remote) => remote && remote.url) 27 | 28 | if (possibleRemotes.length === 0) { 29 | return null 30 | } 31 | const ghData = possibleRemotes.map((r) => parseGithubURL(r.url)) 32 | return ghData.length && ghData[0] ? ghData[0].repo : undefined 33 | } catch (error) { 34 | console.error("Error reading git config:", error) 35 | return null 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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: "gh-actions" | "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 | -------------------------------------------------------------------------------- /source/commands/utils/_tests/chainsmoker.test.ts: -------------------------------------------------------------------------------- 1 | import chainsmoker, { _MatchResult } from "../chainsmoker" 2 | 3 | describe("chainsmoker", () => { 4 | const keyedPaths = { 5 | created: ["added.js", "also/added.js", "this/was/also/Added.js"], 6 | modified: ["changed.js", "also/changed.js", "changed.md", "this_is_changed.sh"], 7 | deleted: ["deleted.js", "also/deleted.md"], 8 | } 9 | 10 | const fileMatch = chainsmoker(keyedPaths) 11 | 12 | it.each<[string[], typeof keyedPaths, _MatchResult]>([ 13 | [ 14 | ["**/*.md"], 15 | { created: [], modified: ["changed.md"], deleted: ["also/deleted.md"] }, 16 | { 17 | created: false, 18 | modified: true, 19 | deleted: true, 20 | }, 21 | ], 22 | [ 23 | ["**/*.js"], 24 | { 25 | created: ["added.js", "also/added.js", "this/was/also/Added.js"], 26 | modified: ["changed.js", "also/changed.js"], 27 | deleted: ["deleted.js"], 28 | }, 29 | { 30 | created: true, 31 | modified: true, 32 | deleted: true, 33 | }, 34 | ], 35 | [ 36 | ["**/*[A-Z]*.js"], 37 | { 38 | created: ["this/was/also/Added.js"], 39 | modified: [], 40 | deleted: [], 41 | }, 42 | { 43 | created: true, 44 | modified: false, 45 | deleted: false, 46 | }, 47 | ], 48 | [ 49 | ["**/*_*"], 50 | { 51 | created: [], 52 | modified: ["this_is_changed.sh"], 53 | deleted: [], 54 | }, 55 | { 56 | created: false, 57 | modified: true, 58 | deleted: false, 59 | }, 60 | ], 61 | [ 62 | ["also/*", "!**/*.md"], 63 | { 64 | created: ["also/added.js"], 65 | modified: ["also/changed.js"], 66 | deleted: [], 67 | }, 68 | { 69 | created: true, 70 | modified: true, 71 | deleted: false, 72 | }, 73 | ], 74 | ])("fileMatch(%s)", (patterns, keyedPaths, matchResult) => { 75 | const matched = fileMatch(...patterns) 76 | expect(matched.getKeyedPaths()).toEqual(keyedPaths) 77 | expect(matched).toEqual({ 78 | ...matchResult, 79 | getKeyedPaths: expect.any(Function), 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /source/commands/utils/_tests/file-utils.test.ts: -------------------------------------------------------------------------------- 1 | let mockDangerfilePath = "" 2 | jest.mock("fs", () => ({ existsSync: (p: any) => p === mockDangerfilePath })) 3 | 4 | import { dangerfilePath } from "../fileUtils" 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/commands/utils/chainsmoker.ts: -------------------------------------------------------------------------------- 1 | // Graciously vendored from 2 | // https://github.com/paulmelnikow/chainsmoker 3 | // 4 | 5 | import micromatch from "micromatch" 6 | import mapValues from "lodash.mapvalues" 7 | 8 | export type Pattern = string 9 | export type Path = string 10 | export type KeyedPatterns = { readonly [K in keyof T]: Pattern[] } 11 | export type KeyedPaths = { readonly [K in keyof T]: Path[] } 12 | export type _MatchResult = { readonly [K in keyof T]: boolean } 13 | export type MatchResult = _MatchResult & { 14 | /** Returns an object containing arrays of matched files instead of the usual boolean values. */ 15 | getKeyedPaths(): KeyedPaths 16 | } 17 | 18 | /** A vendored copy of the Chainsmoker module on NPM */ 19 | export type Chainsmoker = (...patterns: Pattern[]) => MatchResult 20 | 21 | const isExclude = (p: Pattern) => p.startsWith("!") 22 | 23 | export default function chainsmoker(keyedPaths: KeyedPaths): Chainsmoker { 24 | function matchPatterns(patterns: Pattern[]): KeyedPaths { 25 | return mapValues(keyedPaths, (paths: Path[]) => { 26 | const excludePatterns = patterns.filter((p) => isExclude(p)) 27 | const includePatterns = patterns.filter((p) => !isExclude(p)) 28 | 29 | const included = includePatterns.reduce( 30 | (accum, pattern) => accum.concat(micromatch.match(paths, pattern)), 31 | [] as Path[] 32 | ) 33 | 34 | return excludePatterns.reduce((accum, pattern) => micromatch.match(accum, pattern), included) 35 | }) as KeyedPaths 36 | } 37 | 38 | function finalize(keyedPaths: KeyedPaths): MatchResult { 39 | return { 40 | ...mapValues(keyedPaths, (paths: Path[]) => paths.length > 0), 41 | getKeyedPaths: () => keyedPaths, 42 | } as MatchResult 43 | } 44 | 45 | return (...patterns) => finalize(matchPatterns(patterns)) 46 | } 47 | -------------------------------------------------------------------------------- /source/commands/utils/dangerRunToRunnerCLI.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path" 2 | 3 | const usesProcessSeparationCommands = ["ci", "pr", "local"] 4 | 5 | const dangerRunToRunnerCLI = (argv: string[]) => { 6 | let newCommand = [] 7 | newCommand.push(argv[0]) 8 | 9 | // e.g. node --inspect distribution/commands/danger-run-ci.js --dangerfile myDangerfile.ts 10 | // or node distribution/commands/danger-pr.js --dangerfile myDangerfile.ts 11 | 12 | if (argv.length === 1) { 13 | return ["danger", "runner"] 14 | } else if (argv[0].includes("node") || process.pkg != null) { 15 | // convert 16 | let newJSFile = argv[1] 17 | usesProcessSeparationCommands.forEach((name) => { 18 | const re = new RegExp(`danger-${name}(\.js)?$`) 19 | newJSFile = newJSFile.replace(re, "danger-runner$1") 20 | }) 21 | 22 | // Support re-routing internally in npx for danger-ts 23 | // If I recall, npm 7 is getting an npx re-write, so it might 24 | // be worth recommending yarn, but that requires folks using yarn 2 25 | // which I'm not sure will ever get the same level of a adoption of yarn v1 26 | // 27 | if (newJSFile.includes("npx") && newJSFile.endsWith("danger-ts")) { 28 | newJSFile = join( 29 | newJSFile, 30 | "..", 31 | "..", 32 | "lib", 33 | "node_modules", 34 | "danger-ts", 35 | "node_modules", 36 | "danger", 37 | "distribution", 38 | "commands", 39 | "danger-runner.js" 40 | ) 41 | } 42 | newCommand.push(newJSFile) 43 | for (let index = 2; index < argv.length; index++) { 44 | newCommand.push(argv[index]) 45 | } 46 | } else { 47 | // e.g. danger ci --dangerfile 48 | // if you do `danger run` start looking at args later 49 | newCommand.push("runner") 50 | let index = usesProcessSeparationCommands.includes(argv[1]) ? 2 : 1 51 | for (; index < argv.length; index++) { 52 | newCommand.push(argv[index]) 53 | } 54 | } 55 | 56 | return newCommand 57 | } 58 | 59 | export default dangerRunToRunnerCLI 60 | -------------------------------------------------------------------------------- /source/commands/utils/fileUtils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs" 2 | import { cwd } from "process" 3 | 4 | /** 5 | * Returns a the typical Dangerfile, depending on it's location 6 | * taking into account whether it JS or TS by whether those exists. 7 | * 8 | * Will throw if it isn't found. 9 | */ 10 | export function dangerfilePath(program: any): string { 11 | if (program.dangerfile) { 12 | return program.dangerfile 13 | } 14 | 15 | 16 | if (existsSync("dangerfile.mts")) { 17 | return "dangerfile.mts" 18 | } 19 | 20 | if (existsSync("dangerfile.mjs")) { 21 | return "dangerfile.mjs" 22 | } 23 | 24 | if (existsSync("dangerfile.ts")) { 25 | return "dangerfile.ts" 26 | } 27 | 28 | if (existsSync("dangerfile.js")) { 29 | return "dangerfile.js" 30 | } 31 | 32 | if (existsSync("Dangerfile.mts")) { 33 | return "Dangerfile.mts" 34 | } 35 | 36 | if (existsSync("Dangerfile.mjs")) { 37 | return "Dangerfile.mjs" 38 | } 39 | 40 | if (existsSync("Dangerfile.ts")) { 41 | return "Dangerfile.ts" 42 | } 43 | 44 | if (existsSync("Dangerfile.js")) { 45 | return "Dangerfile.js" 46 | } 47 | 48 | throw new Error(`Could not find a 'dangerfile.js' or 'dangerfile.ts' in the current working directory (${cwd()}).`) 49 | } 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /source/commands/utils/reporting.ts: -------------------------------------------------------------------------------- 1 | import { DangerResults } from "../../dsl/DangerResults" 2 | 3 | export const markdownCode = (string: string): string => ` 4 | \`\`\` 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/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/danger-incoming-process-schema.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/danger-js/bdccecb77e0144055fbaea9224f10cf8b1229b68/source/danger-incoming-process-schema.json -------------------------------------------------------------------------------- /source/danger-outgoing-process-schema.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/danger-js/bdccecb77e0144055fbaea9224f10cf8b1229b68/source/danger-outgoing-process-schema.json -------------------------------------------------------------------------------- /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 thread for discussion here: 13 | - https://github.com/danger/danger-js/discussions/1153#discussioncomment-10981472 14 | ` 15 | -------------------------------------------------------------------------------- /source/debug.ts: -------------------------------------------------------------------------------- 1 | import 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/dsl/Aliases.ts: -------------------------------------------------------------------------------- 1 | // Please don't have includes in here that aren't inside the DSL folder, or the d.ts/flow defs break 2 | 3 | export type MarkdownString = string 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/dsl/GitLabDSL.ts: -------------------------------------------------------------------------------- 1 | // Please don't have includes in here that aren't inside the DSL folder, or the d.ts/flow defs break 2 | import type * as Types from "@gitbeaker/rest" 3 | import { RepoMetaData } from "./RepoMetaData" 4 | 5 | // getPlatformReviewDSLRepresentation 6 | export interface GitLabJSONDSL { 7 | /** Info about the repo */ 8 | metadata: RepoMetaData 9 | /** Info about the merge request */ 10 | mr: Types.ExpandedMergeRequestSchema 11 | /** All the individual commits in the merge request */ 12 | commits: Types.CommitSchema[] 13 | /** Merge Request-level MR approvals Configuration */ 14 | approvals: Types.MergeRequestLevelMergeRequestApprovalSchema 15 | } 16 | 17 | // danger.gitlab 18 | /** The GitLab metadata for your MR */ 19 | export interface GitLabDSL extends GitLabJSONDSL { 20 | utils: { 21 | fileContents(path: string, repoSlug?: string, ref?: string): Promise 22 | addLabels(...labels: string[]): Promise 23 | removeLabels(...labels: string[]): Promise 24 | } 25 | api: InstanceType 26 | } 27 | -------------------------------------------------------------------------------- /source/dsl/RepoMetaData.ts: -------------------------------------------------------------------------------- 1 | /** Key details about a repo */ 2 | export interface RepoMetaData { 3 | /** A path like "artsy/eigen" */ 4 | repoSlug: string 5 | /** The ID for the pull/merge request "11" */ 6 | pullRequestID: string 7 | } 8 | -------------------------------------------------------------------------------- /source/dsl/Violation.ts: -------------------------------------------------------------------------------- 1 | // Please don't have includes in here that aren't inside the DSL folder, or the d.ts/flow defs break 2 | 3 | /** 4 | * The result of user doing warn, message or fail, built this way for 5 | * expansion later. 6 | */ 7 | export interface Violation { 8 | /** The string representation */ 9 | message: string 10 | 11 | /** Optional path to the file */ 12 | file?: string 13 | 14 | /** Optional line in the file */ 15 | line?: number 16 | 17 | /** Optional icon for table (Only valid for messages) */ 18 | icon?: string 19 | } 20 | 21 | /// End of Danger DSL definition 22 | 23 | export const isInline = (violation: Violation): boolean => violation.file !== undefined && violation.line !== undefined 24 | -------------------------------------------------------------------------------- /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 | /** The base reference used by danger PR (e.g. not master) */ 7 | base: string 8 | /** For debugging */ 9 | verbose: string 10 | /** Used by danger-js o allow having a custom CI */ 11 | externalCiProvider: string 12 | /** Use SDTOUT instead of posting to the code review side */ 13 | textOnly: string 14 | /** A custom path for the dangerfile (can also be a remote reference) */ 15 | dangerfile: string 16 | /** So you can have many danger runs in one code review */ 17 | id: string 18 | /** Use staged changes */ 19 | staging?: boolean 20 | } 21 | 22 | // NOTE: if add something new here, you need to change dslGenerator.ts 23 | -------------------------------------------------------------------------------- /source/platforms/_tests/_encoding_parser.test.ts: -------------------------------------------------------------------------------- 1 | import { encodingParser } from "../encodingParser" 2 | 3 | describe("parsing encoding", () => { 4 | it("handles base64", () => { 5 | expect(encodingParser("base64")).toEqual("base64") 6 | }) 7 | 8 | it("handles utf8", () => { 9 | expect(encodingParser("utf8")).toEqual("utf8") 10 | }) 11 | 12 | it("throws on unknown encoding", () => { 13 | expect(() => { 14 | encodingParser("unknownencoding") 15 | }).toThrowError() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /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 | expect(() => { 5 | getPlatformForEnv({} as any, {} as any) 6 | }).toThrow("Cannot use authenticated API requests") 7 | }) 8 | -------------------------------------------------------------------------------- /source/platforms/_tests/fixtures/bitbucket_server_changes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "MODIFY", 4 | "path": { 5 | "toString": ".gitignore" 6 | } 7 | }, 8 | { 9 | "type": "UNKNOWN" 10 | }, 11 | { 12 | "type": "ADD", 13 | "path": { 14 | "toString": "banana" 15 | } 16 | }, 17 | { 18 | "type": "COPY", 19 | "path": { 20 | "toString": "orange" 21 | } 22 | }, 23 | { 24 | "type": "MOVE", 25 | "path": { 26 | "toString": ".babelrc" 27 | }, 28 | "srcPath": { 29 | "toString": ".babelrc.example" 30 | } 31 | }, 32 | { 33 | "type": "DELETE", 34 | "path": { 35 | "toString": "jest.eslint.config.js" 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/platforms/_tests/fixtures/static_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": "VGhlIEFsbC1EZWZlY3RvciBpcyBhIHB1cnBvcnRlZCBnbGl0Y2ggaW4gdGhlIERpbGVtbWEgUHJpc29uIHRoYXQgYXBwZWFycyB0byBwcmlzb25lcnMgYXMgdGhlbXNlbHZlcy4gVGhpcyBnb2dvbCBhbHdheXMgZGVmZWN0cywgaGVuY2UgdGhlIG5hbWUu" 3 | } 4 | -------------------------------------------------------------------------------- /source/platforms/encodingParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Verifies that the given encoding is a valid BufferEncoding. 3 | * @throws in case the encoding is unsupported. 4 | */ 5 | export function encodingParser(encoding: string): BufferEncoding { 6 | if (Buffer.isEncoding(encoding)) { 7 | return encoding as BufferEncoding 8 | } 9 | throw new Error(`Unsupported buffer encoding ${encoding}`) 10 | } 11 | -------------------------------------------------------------------------------- /source/platforms/git/_tests/localGetCommits.test.ts: -------------------------------------------------------------------------------- 1 | import { localGetCommits } from "../localGetCommits" 2 | import { logGitCommits } from "../localLogGitCommits" 3 | 4 | const hash = "hash" 5 | const abbrevParentHashes = "abbrevParentHashes" 6 | const treeHash = "treeHash" 7 | const authorName = "authorName" 8 | const authorEmail = "authorEmail" 9 | const authorDate = "authorDate" 10 | const committerName = "committerName" 11 | const committerEmail = "committerEmail" 12 | const committerDate = "committerDate" 13 | const subject = "subject" 14 | 15 | const gitLogCommitMock = { 16 | hash, 17 | abbrevParentHashes, 18 | treeHash, 19 | authorName, 20 | authorEmail, 21 | authorDate, 22 | committerName, 23 | committerEmail, 24 | committerDate, 25 | subject, 26 | } 27 | 28 | jest.mock("../localLogGitCommits", () => ({ 29 | __esModule: true, 30 | logGitCommits: jest.fn(() => [gitLogCommitMock]), 31 | })) 32 | 33 | it("generates a JSON-like commit message", () => { 34 | const base = "base-branch" 35 | const head = "head-branch" 36 | 37 | const result = localGetCommits(base, head) 38 | 39 | expect(logGitCommits).toHaveBeenCalledWith({ 40 | number: expect.any(Number), 41 | branch: `${base}...${head}`, 42 | fields: expect.any(Array), 43 | }) 44 | 45 | expect(result).toEqual([ 46 | { 47 | sha: hash, 48 | author: { 49 | name: authorName, 50 | email: authorEmail, 51 | date: authorDate, 52 | }, 53 | committer: { 54 | name: committerName, 55 | email: committerEmail, 56 | date: committerDate, 57 | }, 58 | message: subject, 59 | tree: treeHash, 60 | url: expect.stringContaining(hash), 61 | }, 62 | ]) 63 | }) 64 | -------------------------------------------------------------------------------- /source/platforms/git/_tests/localLogGitCommits.test.ts: -------------------------------------------------------------------------------- 1 | import { execFileSync } from "child_process" 2 | 3 | import { logGitCommits } from "../localLogGitCommits" 4 | 5 | const COMMAND_OUTPUT = "" 6 | 7 | jest.mock("child_process", () => ({ 8 | __esModule: true, 9 | execFileSync: jest.fn(() => COMMAND_OUTPUT), 10 | })) 11 | 12 | it("get git commits from the 'git log' command", () => { 13 | const options = { 14 | number: 10, 15 | branch: "test_branch", 16 | fields: ["hash", "subject"] as const, 17 | } 18 | 19 | const result = logGitCommits(options) 20 | 21 | expect(execFileSync).toHaveBeenCalledWith("git", [ 22 | "log", 23 | "-l0", 24 | `-n ${options.number}`, 25 | "--pretty=@begin@" + "\t%H\t%s" + "@end@", 26 | options.branch, 27 | ]) 28 | 29 | expect(result).toEqual([]) 30 | }) 31 | -------------------------------------------------------------------------------- /source/platforms/git/_tests/local_dangerfile_example.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | // This dangerfile is for running as an integration test on CI 3 | 4 | import { DangerDSLType } from "../../../dsl/DangerDSL" 5 | declare var danger: DangerDSLType 6 | declare function markdown(params: string): void 7 | 8 | const showArray = (array: any[], mapFunc?: (a: any) => any) => { 9 | const defaultMap = (a: any) => a 10 | const mapper = mapFunc || defaultMap 11 | return `\n - ${array.map(mapper).join("\n - ")}\n` 12 | } 13 | 14 | const git = danger.git 15 | 16 | const goAsync = async () => { 17 | const firstFileDiff = await git.diffForFile(git.modified_files[0]) 18 | const firstJSONFile = git.modified_files.find((f) => f.endsWith("json")) 19 | const jsonDiff = firstJSONFile && (await git.JSONDiffForFile(firstJSONFile)) 20 | const jsonDiffKeys = jsonDiff && showArray(Object.keys(jsonDiff)) 21 | 22 | markdown(` 23 | created: ${showArray(git.created_files)} 24 | modified: ${showArray(git.modified_files)} 25 | deleted: ${showArray(git.deleted_files)} 26 | commits: ${git.commits.length} 27 | messages: ${showArray(git.commits, (c) => c.message)} 28 | diffForFile keys:${firstFileDiff && showArray(Object.keys(firstFileDiff))} 29 | jsonDiff keys:${jsonDiffKeys || "no JSON files in the diff"} 30 | `) 31 | } 32 | goAsync() 33 | -------------------------------------------------------------------------------- /source/platforms/git/diffToGitJSONDSL.ts: -------------------------------------------------------------------------------- 1 | import parseDiff from "parse-diff" 2 | import 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: parseDiff.File[] = parseDiff(diff) 13 | 14 | const addedDiffs = fileDiffs.filter((diff: parseDiff.File) => diff.new == true) as any[] 15 | const removedDiffs = fileDiffs.filter((diff: parseDiff.File) => diff.deleted == true) as any[] 16 | const modifiedDiffs = fileDiffs.filter( 17 | (diff: any) => !includes(addedDiffs, diff) && !includes(removedDiffs, diff) 18 | ) as any[] 19 | 20 | return { 21 | // Work around for danger/danger-js#807 22 | modified_files: modifiedDiffs.map((d) => d.to || (d.from && d.from.split(" b/")[0])), 23 | created_files: addedDiffs.map((d) => d.to), 24 | deleted_files: removedDiffs.map((d) => d.from), 25 | commits: commits, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /source/platforms/git/localGetCommits.ts: -------------------------------------------------------------------------------- 1 | import { logGitCommits } from "./localLogGitCommits" 2 | import { GitCommit } from "../../dsl/Commit" 3 | 4 | export const localGetCommits = (base: string, head: string) => { 5 | const options = { 6 | number: 100, 7 | branch: `${base}...${head}`, 8 | fields: [ 9 | "hash", 10 | "abbrevParentHashes", 11 | "treeHash", 12 | "authorName", 13 | "authorEmail", 14 | "authorDate", 15 | "committerName", 16 | "committerEmail", 17 | "committerDate", 18 | "subject", 19 | ] as const, 20 | } 21 | 22 | const commits: GitCommit[] = logGitCommits(options).map((commit) => ({ 23 | sha: commit.hash, 24 | author: { 25 | name: commit.authorName, 26 | email: commit.authorEmail, 27 | date: commit.authorDate, 28 | }, 29 | committer: { 30 | name: commit.committerName, 31 | email: commit.committerEmail, 32 | date: commit.committerDate, 33 | }, 34 | message: commit.subject, 35 | tree: commit.treeHash, 36 | url: "fake.danger.systems/" + commit.hash, 37 | })) 38 | 39 | return commits 40 | } 41 | -------------------------------------------------------------------------------- /source/platforms/git/localGetDiff.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "../../debug" 2 | import { spawn } from "child_process" 3 | 4 | const d = debug("localGetDiff") 5 | const useCommittedDiffArgs = (base: string, head: string) => [ 6 | "diff", 7 | "--src-prefix='a/' --dst-prefix='b/'", 8 | `${base}...${head}`, 9 | ] 10 | const useStagingChanges = () => ["diff", "--src-prefix='a/' --dst-prefix='b/'", "--staged"] 11 | 12 | export const localGetDiff = (base: string, head: string, staging: boolean = false) => 13 | new Promise((done) => { 14 | const args = staging ? useStagingChanges() : useCommittedDiffArgs(base, head) 15 | let stdout = "" 16 | 17 | const child = spawn("git", args, { env: process.env }) 18 | d("> git", args.join(" ")) 19 | 20 | child.stdout.on("data", (chunk) => { 21 | stdout += chunk 22 | }) 23 | 24 | child.stderr.on("data", (data) => { 25 | console.error(`Could not get diff from git between ${base} and ${head}`) 26 | throw new Error(data.toString()) 27 | }) 28 | 29 | child.on("close", function (code) { 30 | if (code === 0) { 31 | done(stdout) 32 | } 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /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/platforms/git/localLogGitCommits.ts: -------------------------------------------------------------------------------- 1 | import { execFileSync } from "child_process" 2 | 3 | const delimiter = "\t" 4 | const fieldMap = { 5 | hash: "%H", 6 | treeHash: "%T", 7 | abbrevParentHashes: "%P", 8 | authorName: "%an", 9 | authorEmail: "%ae", 10 | authorDate: "%ai", 11 | committerName: "%cn", 12 | committerEmail: "%ce", 13 | committerDate: "%cd", 14 | subject: "%s", 15 | } 16 | 17 | export type GitLogOptions = { 18 | number: number 19 | branch: string 20 | fields: ReadonlyArray> 21 | } 22 | 23 | export type GitLogCommit = { 24 | hash: string 25 | treeHash: string 26 | abbrevParentHashes: string 27 | authorName: string 28 | authorEmail: string 29 | authorDate: string 30 | committerName: string 31 | committerEmail: string 32 | committerDate: string 33 | subject: string 34 | } 35 | 36 | const createCommandArguments = (options: GitLogOptions) => { 37 | // Start constructing command 38 | let command: string[] = ["log", "-l0"] 39 | 40 | command.push(`-n ${options.number}`) 41 | 42 | // Start of custom format 43 | let prettyArgument = "--pretty=@begin@" 44 | 45 | // Iterating through the fields and adding them to the custom format 46 | if (options.fields) { 47 | options.fields.forEach((field) => { 48 | prettyArgument += delimiter + fieldMap[field] 49 | }) 50 | } 51 | 52 | // Close custom format 53 | prettyArgument += "@end@" 54 | command.push(prettyArgument) 55 | 56 | // Append branch (revision range) if specified 57 | if (options.branch) { 58 | command.push(options.branch) 59 | } 60 | 61 | return command 62 | } 63 | 64 | const parseCommits = (commits: readonly string[], fields: readonly string[]) => 65 | commits.map((rawCommit) => { 66 | const parts = rawCommit.split("@end@") 67 | const commit = parts[0].split(delimiter) 68 | 69 | // Remove the first empty char from the array 70 | commit.shift() 71 | 72 | const parsed = {} 73 | 74 | commit.forEach((commitField, index) => { 75 | if (!fields[index]) { 76 | return 77 | } 78 | 79 | parsed[fields[index]] = commitField 80 | }) 81 | 82 | return parsed as GitLogCommit 83 | }) 84 | 85 | export const logGitCommits = (options: GitLogOptions) => { 86 | const commandArguments = createCommandArguments(options) 87 | 88 | const stdout = execFileSync("git", commandArguments).toString() 89 | 90 | const commits = stdout.split("@begin@") 91 | 92 | if (commits[0] === "") { 93 | commits.shift() 94 | } 95 | 96 | return parseCommits(commits, options.fields) 97 | } 98 | -------------------------------------------------------------------------------- /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"] || process.env["GITHUB_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 | 65 | export const emptyGitJSON = (): GitJSONDSL => ({ 66 | commits: [], 67 | created_files: [], 68 | deleted_files: [], 69 | modified_files: [], 70 | }) 71 | -------------------------------------------------------------------------------- /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 fixturedData = (path: string) => JSON.parse(readFileSync(`${fixtures}/${path}`, {}).toString()) 8 | const pr = fixturedData("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("getContent", () => { 42 | it("should call the API's getContent", () => { 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/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/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 | -------------------------------------------------------------------------------- /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 apiUrl = process.env["DANGER_GITHUB_API_BASE_URL"] 24 | ? process.env["DANGER_GITHUB_API_BASE_URL"] 25 | : "https://api.github.com" 26 | const url = `${apiUrl}/app/installations/${installationID}/access_tokens` 27 | const headers = { 28 | Accept: "application/vnd.github.machine-man-preview+json", 29 | Authorization: `Bearer ${jwtForGitHubAuth(appID, key)}`, 30 | } 31 | return fetch(url, { 32 | body: JSON.stringify({}), 33 | headers, 34 | method: "POST", 35 | }) 36 | } 37 | 38 | /** Generates a temporary access token for an app's installation, 5m long */ 39 | export const getAccessTokenForInstallation = async (appID: string, installationID: number, key: string) => { 40 | const newToken = await requestAccessTokenForInstallation(appID, installationID, key) 41 | const credentials = await newToken.json() 42 | if (!newToken.ok) { 43 | console.error(`Could not get an access token for ${installationID}`) 44 | console.error(`GitHub returned: ${JSON.stringify(credentials)}`) 45 | } 46 | return credentials.token as string 47 | } 48 | -------------------------------------------------------------------------------- /source/platforms/gitlab/GitLabGit.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "../../debug" 2 | import { GitLabDSL } from "../../dsl/GitLabDSL" 3 | import { GitDSL, GitJSONDSL } from "../../dsl/GitDSL" 4 | import type * as Types from "@gitbeaker/rest" 5 | import { gitJSONToGitDSL, GitJSONToGitDSLConfig } from "../git/gitJSONToGitDSL" 6 | import GitLabAPI from "./GitLabAPI" 7 | 8 | const d = debug("GitLabGit") 9 | 10 | export const gitLabGitDSL = (gitlab: GitLabDSL, json: GitJSONDSL, gitlabAPI: GitLabAPI): GitDSL => { 11 | const config: GitJSONToGitDSLConfig = { 12 | repo: `${gitlab.mr.project_id}`, // we don't get the repo slug, but `project_id` is equivalent in API calls 13 | baseSHA: gitlab.mr.diff_refs ? gitlab.mr.diff_refs.base_sha : "", 14 | headSHA: gitlab.mr.diff_refs ? gitlab.mr.diff_refs.head_sha : "", 15 | getFileContents: gitlab.utils.fileContents, 16 | getFullDiff: async (base: string, head: string) => { 17 | const changes = await gitlabAPI.getCompareChanges(base, head) 18 | return gitlabChangesToDiff(changes) 19 | }, 20 | } 21 | 22 | d("Setting up git DSL with: ", config) 23 | return gitJSONToGitDSL(json, config) 24 | } 25 | 26 | export const gitlabChangesToDiff = (changes: Types.CommitDiffSchema[]): string => { 27 | d("Converting GitLab Changes to Diff") 28 | // Gitlab doesn't return full raw git diff, relevant issue: https://gitlab.com/gitlab-org/gitlab/issues/24913 29 | return changes 30 | .map((change) => { 31 | const { diff } = change 32 | if (diff.startsWith("diff --git a/") || diff.startsWith("--- a/") || diff.startsWith("--- /dev/null")) { 33 | return diff 34 | } 35 | 36 | return `\ 37 | diff --git a/${change.old_path} b/${change.new_path} 38 | ${change.new_file ? `new file mode ${change.b_mode}` : ""}\ 39 | ${change.deleted_file ? `deleted file mode ${change.a_mode}` : ""}\ 40 | ${change.renamed_file ? `rename from ${change.old_path}\nrename to ${change.new_path}` : ""} 41 | --- ${change.new_file ? "/dev/null" : "a/" + change.old_path} 42 | +++ ${change.deleted_file ? "/dev/null" : "b/" + change.new_path} 43 | ${diff}` 44 | }) 45 | .join("\n") 46 | } 47 | -------------------------------------------------------------------------------- /source/platforms/gitlab/_tests/_fetch_polyfill.ts: -------------------------------------------------------------------------------- 1 | // Nock does not support native fetch until 4.0.0 release which at the time of writing is in beta. 2 | // Furthermore it is currently unsure if 4.0.0 will contain support for recording fixtures. 3 | // Until nock is updated >= 4.0.0 the polyfill is needed when testing against frameworks leveraging native fetch. 4 | 5 | import fetch from "node-fetch" 6 | 7 | let global = globalThis as any 8 | global.fetch = fetch 9 | -------------------------------------------------------------------------------- /source/platforms/gitlab/_tests/fixtures/getFileContents.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://gitlab.com", 4 | "method": "GET", 5 | "path": "/api/v4/projects/gitlab-org%2Fgitlab-foss/repository/files/Gemfile?ref=master", 6 | "body": "", 7 | "status": 200, 8 | "response": { 9 | "file_name": "Gemfile", 10 | "file_path": "Gemfile", 11 | "size": 4989, 12 | "encoding": "base64", 13 | "content_sha256": "d1db2dff734c9a5f59b9e994b2f610317a0bdbdf938d4dc2797bb0bd384233f6", 14 | "ref": "master", 15 | "blob_id": "78b0a38bcaae68ca1bd5fc967c9901c854f3d9ab", 16 | "commit_id": "86461e8c72db13d6d334fd107d38aa1b021d030e", 17 | "last_commit_id": "a16398e10f87edd229a12be2f1fc87cd4a76f902", 18 | "execute_filemode": false, 19 | "content": "c291cmNlICdodHRwczovL3J1YnlnZW1zLm9yZycK" 20 | } 21 | }, 22 | { 23 | "scope": "https://gitlab.com", 24 | "method": "GET", 25 | "path": "/api/v4/projects/gitlab-org%2Fgitlab-foss/repository/files/FileNotExist?ref=master", 26 | "body": "", 27 | "status": 404, 28 | "response": {} 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /source/platforms/pullRequestParser.ts: -------------------------------------------------------------------------------- 1 | import * as url from "url" 2 | import includes from "lodash.includes" 3 | import { BitBucketServer } from "./BitBucketServer" 4 | import { GitHub } from "./GitHub" 5 | import GitLab from "./GitLab" 6 | import { BitBucketCloud } from "./BitBucketCloud" 7 | 8 | export interface PullRequestParts { 9 | pullRequestNumber: string 10 | repo: string 11 | platform: string 12 | } 13 | 14 | export function pullRequestParser(address: string): PullRequestParts | null { 15 | const components = url.parse(address, false) 16 | 17 | if (components && components.path) { 18 | // shape: http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1/overview 19 | const parts = components.path.match(/(projects\/~?\w+\/repos\/[\w-_.]+)\/pull-requests\/(\d+)/) 20 | if (parts) { 21 | return { 22 | platform: BitBucketServer.name, 23 | repo: parts[1], 24 | pullRequestNumber: parts[2], 25 | } 26 | } 27 | 28 | // shape: https://bitbucket.org/proj/repo/pull-requests/1 29 | if (includes(components.path, "pull-requests")) { 30 | return { 31 | platform: BitBucketCloud.name, 32 | repo: components.path.split("/pull-requests")[0].slice(1), 33 | pullRequestNumber: components.path.split("/pull-requests/")[1].split("/")[0], 34 | } 35 | } 36 | 37 | // shape: http://github.com/proj/repo/pull/1 38 | if (includes(components.path, "pull")) { 39 | return { 40 | platform: GitHub.name, 41 | repo: components.path.split("/pull")[0].slice(1), 42 | pullRequestNumber: components.path.split("/pull/")[1], 43 | } 44 | } 45 | 46 | // shape: https://gitlab.com/GROUP[/SUBGROUP]/PROJ/merge_requests/123 47 | if (includes(components.path, "merge_requests")) { 48 | const regex = /\/(.+)\/merge_requests\/(\d+)/ 49 | const parts = components.path.match(regex) 50 | if (parts) { 51 | return { 52 | platform: GitLab.name, 53 | repo: parts[1].replace(/\/-$/, ""), 54 | pullRequestNumber: parts[2], 55 | } 56 | } 57 | } 58 | } 59 | 60 | return null 61 | } 62 | -------------------------------------------------------------------------------- /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 | 24 | export const compliment = () => { 25 | const compliments = ["Well done.", "Congrats.", "Woo!", "Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."] 26 | return compliments[Math.floor(Math.random() * compliments.length)] 27 | } 28 | -------------------------------------------------------------------------------- /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 as any)).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/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 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileAsync.ts: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileBadSyntax.js: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileCallback.js: -------------------------------------------------------------------------------- 1 | schedule(done => { 2 | setTimeout(() => { 3 | warn("Scheduled a callback") 4 | done() 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileCustomIconMessages.js: -------------------------------------------------------------------------------- 1 | message("this is a simple message") 2 | message("this is a message with custom icon", { icon: "📝" }) 3 | message("this is a message with custom icon2", { icon: "🔔" }) -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileDefaultExport.js: -------------------------------------------------------------------------------- 1 | export default () => warn("Synchronous Warning") 2 | -------------------------------------------------------------------------------- /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/__DangerfileEmpty.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/danger-js/bdccecb77e0144055fbaea9224f10cf8b1229b68/source/runner/_tests/fixtures/__DangerfileEmpty.js -------------------------------------------------------------------------------- /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/__DangerfileImportRelative.js: -------------------------------------------------------------------------------- 1 | import thing from "./export/thing" 2 | 3 | thing() 4 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileInlineOpts.js: -------------------------------------------------------------------------------- 1 | warn("this is a warning", "Warning.swift", "12") 2 | message("this is a message", { file: "Message.swift", line: "17" }) 3 | fail("this is a failure", "Fail.swift", "27") 4 | markdown("this is a *markdown*") 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/__DangerfileNoScheduledAsync.ts: -------------------------------------------------------------------------------- 1 | setTimeout(() => warn("Totally Async"), 1000) 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileThrows.js: -------------------------------------------------------------------------------- 1 | throw new Error("failure") 2 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/__DangerfileTypeScript.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // This doesn't exist in JS-world 4 | interface MyThing {} 5 | 6 | // @ts-ignore 7 | message("Honey, we got Types") 8 | -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/export/thing.js: -------------------------------------------------------------------------------- 1 | export default function thing() { 2 | return "thing" 3 | } 4 | -------------------------------------------------------------------------------- /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": [ 14 | null 15 | ] 16 | } -------------------------------------------------------------------------------- /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": [ 14 | null 15 | ] 16 | } -------------------------------------------------------------------------------- /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/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": [ 11 | null 12 | ] 13 | } -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileCustomIconMessages.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [], 4 | "messages": [ 5 | { 6 | "message": "this is a simple message" 7 | }, 8 | { 9 | "message": "this is a message with custom icon", 10 | "icon": "📝" 11 | }, 12 | { 13 | "message": "this is a message with custom icon2", 14 | "icon": "🔔" 15 | } 16 | ], 17 | "markdowns": [], 18 | "scheduled": [] 19 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileEmpty.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [], 4 | "messages": [], 5 | "markdowns": [], 6 | "scheduled": [] 7 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileImportRelative.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [], 3 | "warnings": [], 4 | "messages": [], 5 | "markdowns": [], 6 | "scheduled": [] 7 | } -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfileInlineOpts.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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | {}, 25 | {}, 26 | {} 27 | ] 28 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /source/runner/_tests/fixtures/results/__DangerfilePlugin.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fails": [ 3 | { 4 | "message": "@types dependencies were added to package.json, as a dependency for others.
You need to move @types/danger into \"devDependencies\"?" 5 | } 6 | ], 7 | "warnings": [], 8 | "messages": [], 9 | "markdowns": [], 10 | "scheduled": [] 11 | } -------------------------------------------------------------------------------- /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 | ] 13 | } -------------------------------------------------------------------------------- /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/_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 | } -------------------------------------------------------------------------------- /source/runner/_tests/jsonToContext.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock("../jsonToDSL", () => ({ jsonToDSL: jest.fn(() => Promise.resolve({ danger: "" })) })) 2 | jest.mock("../Dangerfile", () => ({ contextForDanger: jest.fn(() => Promise.resolve({ danger: "" })) })) 3 | 4 | import { jsonToContext } from "../jsonToContext" 5 | import { CISource } from "../../ci_source/ci_source" 6 | import { FakeCI } from "../../ci_source/providers/Fake" 7 | 8 | import { jsonToDSL } from "../jsonToDSL" 9 | import { contextForDanger } from "../Dangerfile" 10 | 11 | describe("runner/json-to-context", () => { 12 | let jsonString: any 13 | let program: any 14 | let context: any 15 | let source: CISource 16 | 17 | beforeEach(() => { 18 | jsonString = JSON.stringify({ 19 | danger: { 20 | settings: { 21 | github: { 22 | baseURL: "", 23 | }, 24 | cliArgs: {}, 25 | }, 26 | }, 27 | }) 28 | 29 | program = { 30 | base: "develop", 31 | } 32 | source = new FakeCI({}) 33 | }) 34 | 35 | it("should have a function called get context", () => { 36 | expect(jsonToContext).toBeTruthy() 37 | }) 38 | 39 | it("should return a context", async () => { 40 | context = await jsonToContext(jsonString, program, source) 41 | expect(context).toBeTruthy() 42 | }) 43 | 44 | it("should set the base from the input command", async () => { 45 | context = await jsonToContext(jsonString, program, source) 46 | expect(context.danger).toEqual("") 47 | }) 48 | 49 | it("should work if no base is set", async () => { 50 | program.base = undefined 51 | await jsonToContext(jsonString, program, source) 52 | expect(jsonToDSL).toHaveBeenCalledWith( 53 | { 54 | settings: { 55 | github: { 56 | baseURL: "", 57 | }, 58 | cliArgs: {}, 59 | }, 60 | }, 61 | { env: { pr: "327", repo: "artsy/emission" } } 62 | ) 63 | }) 64 | 65 | it("should set the base to develop", async () => { 66 | await jsonToContext(jsonString, program, source) 67 | expect(jsonToDSL).toHaveBeenCalledWith( 68 | { 69 | settings: { 70 | github: { 71 | baseURL: "", 72 | }, 73 | cliArgs: { 74 | base: "develop", 75 | }, 76 | }, 77 | }, 78 | { env: { pr: "327", repo: "artsy/emission" } } 79 | ) 80 | }) 81 | 82 | it("should call context for danger with dsl", async () => { 83 | await jsonToContext(jsonString, program, source) 84 | expect(contextForDanger).toHaveBeenCalledWith({ danger: "" }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /source/runner/dangerDSLJSON.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { DangerDSLJSONType } from "../dsl/DangerDSL" 3 | import { GitJSONDSL } from "../dsl/GitDSL" 4 | import { GitHubDSL } from "../dsl/GitHubDSL" 5 | import { CliArgs } from "../dsl/cli-args" 6 | 7 | /** 8 | * Using the input JSON create an DangerDSL 9 | * 10 | * @see DangerDSLJSONType for more detailed definition 11 | */ 12 | export class DangerDSLJSON implements DangerDSLJSONType { 13 | // Prettier + `git!` do not work yet 14 | // and this class uses runtime hackery 15 | 16 | // @ts-ignore 17 | git: GitJSONDSL 18 | // @ts-ignore 19 | github: GitHubDSL 20 | // @ts-ignore 21 | settings: { 22 | github: { 23 | accessToken: string 24 | baseURL: string | undefined 25 | additionalHeaders: any 26 | } 27 | cliArgs: CliArgs 28 | } 29 | /** 30 | * Parse the JSON and assign danger to this object 31 | * 32 | * Also add the arguments sent to the CLI 33 | * 34 | * @param JSONString DSL in JSON format 35 | * @param cliArgs arguments used running danger command 36 | */ 37 | constructor(JSONString: string, cliArgs: CliArgs) { 38 | const parsedString = JSON.parse(JSONString) 39 | Object.assign(this, parsedString.danger) 40 | 41 | // Merge the command line settings with the settings from the 42 | // original command invocation. This is needed because some 43 | // commands like danger-local have options that are unknown to 44 | // danger-runner 45 | // @ts-ignore 46 | Object.assign(this.settings.cliArgs, cliArgs) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /source/runner/dslGenerator.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "../platforms/platform" 2 | import { DangerDSLJSONType } from "../dsl/DangerDSL" 3 | import { CliArgs } from "../dsl/cli-args" 4 | import { CISource } from "../ci_source/ci_source" 5 | import { emptyGitJSON } from "../platforms/github/GitHubGit" 6 | import { CommanderStatic } from "commander" 7 | 8 | export const jsonDSLGenerator = async ( 9 | platform: Platform, 10 | source: CISource, 11 | program: CommanderStatic 12 | ): Promise => { 13 | const useSimpleDSL = platform.getPlatformReviewSimpleRepresentation && source.useEventDSL 14 | 15 | const git = useSimpleDSL ? emptyGitJSON() : await platform.getPlatformGitRepresentation() 16 | 17 | const getDSLFunc = useSimpleDSL 18 | ? platform.getPlatformReviewSimpleRepresentation 19 | : platform.getPlatformReviewDSLRepresentation 20 | 21 | const platformDSL = await getDSLFunc!() 22 | 23 | const cliArgs: CliArgs = { 24 | base: program.base, 25 | dangerfile: program.dangerfile, 26 | externalCiProvider: program.externalCiProvider, 27 | id: program.id, 28 | textOnly: program.textOnly, 29 | verbose: program.verbose, 30 | staging: program.staging, 31 | } 32 | 33 | const dslPlatformName = jsonDSLPlatformName(platform) 34 | 35 | return { 36 | git, 37 | [dslPlatformName]: platformDSL, 38 | settings: { 39 | github: { 40 | accessToken: process.env["DANGER_GITHUB_API_TOKEN"] || process.env["GITHUB_TOKEN"] || "NO_TOKEN", 41 | additionalHeaders: {}, 42 | baseURL: process.env["DANGER_GITHUB_API_BASE_URL"] || "https://api.github.com", 43 | }, 44 | cliArgs, 45 | }, 46 | } 47 | } 48 | 49 | const jsonDSLPlatformName = (platform: Platform): string => { 50 | switch (platform.name) { 51 | case "BitBucketServer": 52 | return "bitbucket_server" 53 | case "BitBucketCloud": 54 | return "bitbucket_cloud" 55 | case "GitLab": 56 | return "gitlab" 57 | case "GitHub": 58 | return "github" 59 | default: 60 | return platform.name.split(" ").join("_") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/runner/jsonToContext.ts: -------------------------------------------------------------------------------- 1 | import { CliArgs } from "../dsl/cli-args" 2 | import { jsonToDSL } from "./jsonToDSL" 3 | import { contextForDanger, DangerContext } from "./Dangerfile" 4 | import { DangerDSLJSON } from "./dangerDSLJSON" 5 | import { CISource } from "../ci_source/ci_source" 6 | import { CommanderStatic } from "commander" 7 | 8 | /** 9 | * Reads in the JSON string converts to a dsl object and gets the change context 10 | * to be used for Danger. 11 | * @param JSONString {string} from stdin 12 | * @param program {any} commander 13 | * @returns {Promise} context for danger 14 | */ 15 | export async function jsonToContext( 16 | JSONString: string, 17 | program: CommanderStatic, 18 | source: CISource 19 | ): Promise { 20 | const dslJSON = { danger: new DangerDSLJSON(JSONString, program as any as CliArgs) } 21 | const dsl = await jsonToDSL(dslJSON.danger, source) 22 | return contextForDanger(dsl) 23 | } 24 | -------------------------------------------------------------------------------- /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, fail, markdown, schedule, warn, message, results } from "danger"; 7 | 8 | import { 9 | danger, 10 | fail, 11 | markdown, 12 | schedule, 13 | warn, 14 | message, 15 | results, 16 | } from "danger"; 17 | 18 | import danger = require("danger"); 19 | 20 | import { danger, warn, fail, message } from 'danger' 21 | import { danger, warn, fail, message } from "danger" 22 | import { danger, warn, fail, message } from "danger"; 23 | import danger from "danger" 24 | import danger from 'danger' 25 | import danger from 'danger'; 26 | ` 27 | expect(cleanDangerfile(before)).toMatchInlineSnapshot(` 28 | " 29 | // Removed import { danger, fail, markdown, schedule, warn, message, results } from \\"danger\\"; 30 | 31 | // Removed import { 32 | // danger, 33 | // fail, 34 | // markdown, 35 | // schedule, 36 | // warn, 37 | // message, 38 | // results, 39 | // } from \\"danger\\"; 40 | 41 | // Removed import danger = require(\\"danger\\"); 42 | 43 | // Removed import { danger, warn, fail, message } from 'danger' 44 | // Removed import { danger, warn, fail, message } from \\"danger\\" 45 | // Removed import { danger, warn, fail, message } from \\"danger\\"; 46 | // Removed import danger from \\"danger\\" 47 | // Removed import danger from 'danger' 48 | // Removed import danger from 'danger'; 49 | " 50 | `) 51 | }) 52 | 53 | it("also handles require style imports", () => { 54 | const before = ` 55 | const { danger, fail, warn } = require('danger'); 56 | const fs = require('fs') 57 | const {danger} = require("danger"); 58 | const { 59 | danger, 60 | fail, 61 | markdown, 62 | schedule, 63 | warn, 64 | message, 65 | results, 66 | } = require("danger"); 67 | let Danger = require("danger"); 68 | var D = require("danger"); 69 | ` 70 | expect(cleanDangerfile(before)).toMatchInlineSnapshot(` 71 | " 72 | // Removed require; Original: const { danger, fail, warn } = require('danger'); 73 | // Removed require; Original: const fs = require('fs') 74 | // const {danger} = require(\\"danger\\"); 75 | // Removed require; Original: const { 76 | // danger, 77 | // fail, 78 | // markdown, 79 | // schedule, 80 | // warn, 81 | // message, 82 | // results, 83 | // } = require(\\"danger\\"); 84 | // Removed require; Original: let Danger = require(\\"danger\\"); 85 | // Removed require; Original: var D = require(\\"danger\\"); 86 | " 87 | `) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /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, boolean][]} filenames a set of tuples of file paths for the dangerfile, with a boolean indicating 10 | * if it is a remote path 11 | * @param {string[] | undefined[]} originalContents optional, the JS pre-compiled 12 | * @param {DangerContext} environment the results of createDangerfileRuntimeEnvironment 13 | * @param {any | undefined} injectedObjectToExport an optional object for passing into default exports 14 | * @returns {DangerResults} the results of the run 15 | */ 16 | runDangerfileEnvironment: ( 17 | filenames: [string, boolean][], 18 | originalContents: string[] | undefined[] | undefined, 19 | environment: any, 20 | injectedObjectToExport?: any 21 | ) => Promise 22 | 23 | /** 24 | * Sets up the runtime environment for running Danger, this could be loading VMs 25 | * or creating new processes etc. The return value is expected to go into the environment 26 | * section of runDangerfileEnvironment. 27 | * 28 | * @param {DangerContext} dangerfileContext the global danger context, basically the DSL 29 | */ 30 | createDangerfileRuntimeEnvironment: (dangerfileContext: DangerContext) => Promise 31 | } 32 | -------------------------------------------------------------------------------- /source/runner/runners/utils/cleanDangerfile.ts: -------------------------------------------------------------------------------- 1 | // https://regex101.com/r/Jxa3KX/4 2 | const requirePattern = /(const|let|var)(.|\n)*? require\(('|")danger('|")\);?$/gm 3 | // https://regex101.com/r/hdEpzO/3 4 | const es6Pattern = /import((?!from)(?!require)(.|\n))*?(from|require\()\s?('|")danger('|")\)?;?$/gm 5 | 6 | /** 7 | * This produces a closure that can be passed to string.replace 8 | * It preserves the passed in code, adding simple comments. 9 | * 10 | * This should keep line numbers the same when errors get thrown parsing dangerfiles! 11 | */ 12 | const nNewLinesReplacer = (comment: string) => (substring: string) => 13 | substring 14 | .split("\n") 15 | .map((chunk, index) => { 16 | return index === 0 ? comment + " " + chunk : "// " + chunk 17 | }) 18 | .join("\n") 19 | 20 | const importReplacer = nNewLinesReplacer("// Removed" /* import will be the next word!*/) 21 | const requireReplacer = nNewLinesReplacer("// Removed require; Original: ") 22 | 23 | /** 24 | * Updates a Dangerfile to remove the import for Danger 25 | * @param {string} contents the file path for the dangerfile 26 | * @returns {string} the revised Dangerfile 27 | */ 28 | export default (contents: string): string => 29 | contents.replace(es6Pattern, importReplacer).replace(requirePattern, requireReplacer) 30 | -------------------------------------------------------------------------------- /source/runner/runners/utils/resultsForCaughtError.ts: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /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, with a commit 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](https://danger.systems/js) against e70f3d6468f61a4bef68c9e6eaba9166b096e23c_ | 30 | 31 | 32 | [](http://danger-id-blankID;) 33 | " 34 | `; 35 | 36 | exports[`generating messages for BitBucket server summary result matches snapshot, without a commit 1`] = ` 37 | " 38 | 39 | | ❌ | Fails 40 | | --- | --- | 41 | 42 | > Failing message Failing message 43 | 44 | 45 | | ⚠️ | Warnings 46 | | --- | --- | 47 | 48 | > Warning message Warning message 49 | 50 | 51 | | ✨ | Messages 52 | | --- | --- | 53 | 54 | > message 55 | 56 | 57 | markdown 58 | 59 | 60 | | | 61 | |---:| 62 | | _Generated by ⚠️ [dangerJS](https://danger.systems/js)_ | 63 | 64 | 65 | [](http://danger-id-blankID;) 66 | " 67 | `; 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = [ 7 | ["⚠️", "This is a very unimportant warning."], 8 | ["⚠️", "But, this warning is pretty important."], 9 | ] 10 | 11 | const table = markdownTableTemplate(headers, rows) 12 | 13 | expect(table).toMatchSnapshot() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "esModuleInterop": true, 15 | "module": "commonjs", 16 | "moduleResolution": "node16", 17 | "pretty": true, 18 | "target": "es5", 19 | "outDir": "distribution", 20 | "lib": ["dom", "es2017"], 21 | "strict": true, 22 | "skipLibCheck": true 23 | }, 24 | "formatCodeOptions": { 25 | "indentSize": 2, 26 | "tabSize": 2 27 | }, 28 | "exclude": ["scripts", "node_modules", "source/**/fixtures/*", "distribution", "types"] 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "distribution", "types"] 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "danger": "*", 4 | "github": "*" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", 3 | "rules": { 4 | "semicolon": [false] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = wallaby => { 2 | return { 3 | files: [ 4 | "tsconfig.json", 5 | { pattern: "source/**/fixtures/**/*.*", instrument: false }, 6 | "source/**/!(*.test).ts", 7 | { pattern: "package.json", instrument: false }, 8 | ], 9 | 10 | tests: ["source/**/*.test.ts", "!source/runner/_tests/json-to-context.test.s"], 11 | 12 | env: { 13 | type: "node", 14 | }, 15 | 16 | // fixtures are not instrumented, but still need to be compiled 17 | preprocessors: { 18 | "source/**/fixtures/**/*.js?(x)": wallaby.compilers.babel(), 19 | }, 20 | 21 | testFramework: "jest", 22 | } 23 | } 24 | --------------------------------------------------------------------------------