├── .circleci └── config.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── VISION.md ├── api ├── .babelrc ├── .dockerignore ├── .env.sample ├── Brewfile ├── README.md ├── bin │ └── lambda.zip ├── dangerfile.lite.ts ├── docker-compose.yml ├── now.staging.json ├── package.json ├── public │ └── index.html ├── scripts │ ├── deploy-staging-api.sh │ ├── deploy-staging-layer.sh │ ├── deploy-staging-runner.sh │ └── trim_node_modules.sh ├── source │ ├── ambient.d.ts │ ├── api │ │ ├── _tests │ │ │ └── fixtureAPI.ts │ │ ├── api.ts │ │ ├── auth │ │ │ ├── _tests │ │ │ │ └── _generate.test.ts │ │ │ ├── generate.ts │ │ │ ├── getJWTFromRequest.ts │ │ │ └── github.ts │ │ ├── aws │ │ │ └── lambda.ts │ │ ├── fetch.ts │ │ ├── github.ts │ │ ├── graphql │ │ │ ├── _tests │ │ │ │ └── _graphql.test.ts │ │ │ ├── api.ts │ │ │ ├── aws │ │ │ │ └── getCloudWatchForInstallation.ts │ │ │ ├── github │ │ │ │ ├── api.ts │ │ │ │ ├── createNewRepoForPerilSettings.ts │ │ │ │ ├── getAvailableReposForInstallation.ts │ │ │ │ └── sendPRForPerilSettingsRepo.ts │ │ │ ├── gql.ts │ │ │ ├── index.ts │ │ │ ├── mutations │ │ │ │ ├── _tests │ │ │ │ │ └── _dangerfileFinished.test.ts │ │ │ │ └── mutations.ts │ │ │ └── utils │ │ │ │ ├── auth.ts │ │ │ │ └── installations.ts │ │ ├── integration │ │ │ └── github.ts │ │ └── pr │ │ │ └── dsl.ts │ ├── danger │ │ ├── _tests │ │ │ ├── _danger_run.test.ts │ │ │ ├── _danger_runner.test.ts │ │ │ ├── _danger_runner_paths.test.ts │ │ │ ├── _danger_runner_peril.test.ts │ │ │ ├── _danger_runner_webhook.test.ts │ │ │ ├── _peril_platform.test.ts │ │ │ ├── fixtures │ │ │ │ ├── dangerfile_async_import.ts │ │ │ │ ├── dangerfile_empty.ts │ │ │ │ ├── dangerfile_import_complex_module.ts │ │ │ │ ├── dangerfile_import_module.ts │ │ │ │ ├── dangerfile_insecure.ts │ │ │ │ ├── dangerfile_peril_obj.ts │ │ │ │ └── file_to_import.ts │ │ │ └── sandbox_stub.ts │ │ ├── append_peril.ts │ │ ├── danger_run.ts │ │ ├── danger_runner.ts │ │ ├── peril_ci_source.ts │ │ └── peril_platform.ts │ ├── db │ │ ├── GitHubRepoSettings.ts │ │ ├── __mocks__ │ │ │ └── getDB.ts │ │ ├── _tests │ │ │ ├── __snapshots__ │ │ │ │ └── _json.test.ts.snap │ │ │ ├── _fixtures │ │ │ │ └── example_peril_orta_settings.json │ │ │ ├── _json.test.ts │ │ │ └── _mongo.test.ts │ │ ├── getDB.ts │ │ ├── index.ts │ │ ├── json.ts │ │ ├── mongo.ts │ │ ├── mongo │ │ │ └── installationAnalytics.ts │ │ ├── runtimeEnv.ts │ │ └── types.ts │ ├── github │ │ ├── _tests │ │ │ └── fixtures │ │ │ │ ├── github_comments.json │ │ │ │ ├── github_commits.json │ │ │ │ ├── github_diff.diff │ │ │ │ ├── github_issue.json │ │ │ │ ├── github_pr.json │ │ │ │ ├── github_requested_reviewers.json │ │ │ │ ├── github_reviews.json │ │ │ │ ├── github_static_file.json │ │ │ │ └── github_user.json │ │ ├── events │ │ │ ├── _tests │ │ │ │ ├── _actionForWebhook.test.ts │ │ │ │ ├── _github_runner-prs.test.ts │ │ │ │ ├── _github_runner-runs.test.ts │ │ │ │ ├── _github_runner-setup.test.ts │ │ │ │ ├── _github_runner-validations.test.ts │ │ │ │ └── fixtures │ │ │ │ │ ├── access_token.json │ │ │ │ │ ├── installation.json │ │ │ │ │ ├── integration_installation_added.json │ │ │ │ │ ├── issue_comment_created.json │ │ │ │ │ ├── issues_opened.json │ │ │ │ │ ├── ping.json │ │ │ │ │ ├── pull_request_closed.json │ │ │ │ │ ├── pull_request_labeled.json │ │ │ │ │ ├── pull_request_opened.json │ │ │ │ │ ├── pull_request_updated.json │ │ │ │ │ ├── push.json │ │ │ │ │ └── status_success.json │ │ │ ├── createPRDSL.ts │ │ │ ├── create_installation.ts │ │ │ ├── deleteInstallation.ts │ │ │ ├── github_runner.ts │ │ │ ├── handlers │ │ │ │ ├── _tests │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── _events-create-fixture.test.ts.snap │ │ │ │ │ │ ├── _events-sandbox.test.ts.snap │ │ │ │ │ │ └── _pr-create-fixture.test.ts.snap │ │ │ │ │ ├── _events-create-fixture.test.ts │ │ │ │ │ ├── _events-sandbox.test.ts │ │ │ │ │ ├── _events.test.ts │ │ │ │ │ ├── _pr-create-fixture.test.ts │ │ │ │ │ └── fixtures │ │ │ │ │ │ ├── PerilRunnerEventBootStrapExample.json │ │ │ │ │ │ └── PerilRunnerPRBootStrapExample.json │ │ │ │ ├── event.ts │ │ │ │ └── pr.ts │ │ │ ├── ping.ts │ │ │ ├── types │ │ │ │ ├── access_token.types.ts │ │ │ │ ├── installation.types.ts │ │ │ │ ├── integration_installation_added.types.ts │ │ │ │ ├── ping.types.ts │ │ │ │ ├── pull_request_closed.types.ts │ │ │ │ ├── pull_request_opened.types.ts │ │ │ │ └── pull_request_updated.types.ts │ │ │ └── utils │ │ │ │ ├── actions.ts │ │ │ │ ├── commenting.ts │ │ │ │ ├── ignore_repos.ts │ │ │ │ └── repoNameForWebhook.ts │ │ └── lib │ │ │ └── github_helpers.ts │ ├── globals.ts │ ├── index.ts │ ├── infrastructure │ │ ├── _tests │ │ │ └── _installationSlackMessaging.test.ts │ │ └── installationSlackMessaging.ts │ ├── listen.ts │ ├── logger.ts │ ├── peril.ts │ ├── plugins │ │ ├── _tests │ │ │ ├── _installationLifeCycle.test.ts │ │ │ ├── _installationSettingsUpdater.test.ts │ │ │ └── _validatesGithubWebhook.test.ts │ │ ├── installationLifeCycle.ts │ │ ├── installationSettingsUpdater.ts │ │ ├── recordWebhooks.ts │ │ ├── utils │ │ │ ├── recordWebhookWithRequest.ts │ │ │ └── sendWebhookThroughGitHubRunner.ts │ │ └── validatesGithubWebhook.ts │ ├── routing │ │ ├── _tests │ │ │ ├── _router.test.ts │ │ │ └── create-mock-response.ts │ │ └── router.ts │ ├── runner │ │ ├── _tests │ │ │ └── _customGitHubRequire.test.ts │ │ ├── customGitHubRequire.ts │ │ ├── fixtures │ │ │ ├── branch-push.json │ │ │ ├── dangerfile │ │ │ │ └── hello_world.ts │ │ │ ├── debug.json │ │ │ ├── hello-world.json │ │ │ └── pr-closed.json │ │ ├── index.ts │ │ ├── run.ts │ │ ├── runFromExternalHost.ts │ │ ├── runFromSameHost.ts │ │ ├── sandbox │ │ │ └── jwt.ts │ │ └── triggerSandboxRun.ts │ ├── scripts │ │ ├── generate-runner-deps.ts │ │ ├── json-types.ts │ │ └── setup-plugins.ts │ ├── tasks │ │ ├── _tests │ │ │ ├── _scheduleTask-heroku.test.ts │ │ │ ├── _scheduleTask-prod.test.ts │ │ │ └── _startTaskScheduler.test.ts │ │ ├── runTask.ts │ │ ├── scheduleTask.ts │ │ └── startTaskScheduler.ts │ └── testing │ │ ├── installationFactory.ts │ │ └── setupScript.js ├── tsconfig.json ├── tslint.json ├── wallaby.js └── yarn.lock ├── app.json ├── dashboard ├── .env.sample ├── .gitignore ├── LICENSE ├── README.md ├── now.staging.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── relay_data │ └── schema.graphql ├── scripts │ └── deploy_staging.sh ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── ambient.d.ts │ ├── components │ │ ├── Home.tsx │ │ ├── Installation.tsx │ │ ├── Login.tsx │ │ ├── PartialInstallation.tsx │ │ ├── __generated__ │ │ │ ├── HomeQuery.graphql.ts │ │ │ ├── InstallationQuery.graphql.ts │ │ │ └── PartialInstallationQuery.graphql.ts │ │ ├── installation │ │ │ ├── EnvVars.tsx │ │ │ ├── InstallationRules.tsx │ │ │ ├── Overview.tsx │ │ │ ├── Settings.tsx │ │ │ ├── TaskRunner.tsx │ │ │ ├── Webhooks.tsx │ │ │ ├── WebhooksHeader.tsx │ │ │ ├── Websocket.tsx │ │ │ ├── __generated__ │ │ │ │ ├── EnvVars_installation.graphql.ts │ │ │ │ ├── InstallationRules_installation.graphql.ts │ │ │ │ ├── Overview_installation.graphql.ts │ │ │ │ ├── Settings_installation.graphql.ts │ │ │ │ ├── TaskRunner_installation.graphql.ts │ │ │ │ ├── WebhooksHeader_installation.graphql.ts │ │ │ │ ├── Webhooks_installation.graphql.ts │ │ │ │ └── Websocket_installation.graphql.ts │ │ │ └── mutations │ │ │ │ ├── __generated__ │ │ │ │ ├── editEnvVarMutation.graphql.ts │ │ │ │ ├── editInstallationMutationMutation.graphql.ts │ │ │ │ ├── makeInstallationRecordMutation.graphql.ts │ │ │ │ ├── runTaskMutation.graphql.ts │ │ │ │ └── triggerWebhookMutation.graphql.ts │ │ │ │ ├── editEnvVarMutation.ts │ │ │ │ ├── editInstallationMutation.ts │ │ │ │ ├── makeInstallationRecordMutation.ts │ │ │ │ ├── runTaskMutation.ts │ │ │ │ └── triggerWebhookMutation.ts │ │ ├── layout │ │ │ ├── Layout.tsx │ │ │ └── __generated__ │ │ │ │ └── LayoutQuery.graphql.ts │ │ └── partial │ │ │ ├── SetJSONPathForm.tsx │ │ │ ├── __generated__ │ │ │ └── SetJSONPathForm_installation.graphql.ts │ │ │ └── mutations │ │ │ ├── __generated__ │ │ │ └── updateJSONURLMutation.graphql.ts │ │ │ └── updateJSONURLMutation.tsx │ ├── index.css │ ├── index.tsx │ ├── lib │ │ ├── RelayProvider.ts │ │ ├── createRelayEnvironment.ts │ │ ├── dangerfileReferenceURLs.ts │ │ └── routes.ts │ ├── logo.svg │ ├── react-app-env.d.ts │ └── serviceWorker.ts ├── tsconfig.json └── yarn.lock ├── docs ├── api_architecture.md ├── debugging.md ├── env │ └── staging.md ├── highlights.md ├── images │ ├── events-ex.png │ ├── events.png │ ├── heroku_setup.png │ └── peril-setup.png ├── local_dev.md ├── service_map.md ├── settings_repo_info.md ├── setup_for_org.md ├── terminology.md ├── updating_peril.md └── using_peril_staging.md ├── hooks ├── build └── pre_build ├── now.json ├── peril-settings-json.schema ├── runner ├── .babelrc ├── README.md ├── index.js ├── package.json ├── tsconfig.json └── yarn.lock ├── web ├── .gitignore ├── .travis.yml ├── .vscode │ ├── launch.json │ └── settings.json ├── README.md ├── data │ ├── author.json │ ├── avatars │ │ └── fabien0102.jpg │ └── blog │ │ ├── 2017-04-18--welcoming │ │ ├── index.md │ │ └── pexels-photo-253092.jpeg │ │ ├── 2017-05-02--article-1 │ │ ├── index.md │ │ └── pexels-photo-59628.jpeg │ │ └── 2017-05-02--article-2 │ │ ├── cup-of-coffee-laptop-office-macbook-89786.jpeg │ │ └── index.md ├── gatsby-browser.tsx ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.tsx ├── package.json ├── src │ ├── components │ │ ├── BlogPagination │ │ │ ├── BlogPagination.stories.tsx │ │ │ ├── BlogPagination.test.tsx │ │ │ ├── BlogPagination.tsx │ │ │ ├── README.md │ │ │ └── __snapshots__ │ │ │ │ └── BlogPagination.test.tsx.snap │ │ ├── BlogTitle.tsx │ │ ├── HeaderMenu │ │ │ ├── HeaderMenu.test.tsx │ │ │ ├── HeaderMenu.tsx │ │ │ └── README.md │ │ ├── Layout.tsx │ │ ├── Menu.ts │ │ ├── SidebarMenu │ │ │ ├── README.md │ │ │ ├── SidebarMenu.test.tsx │ │ │ ├── SidebarMenu.tsx │ │ │ └── __snapshots__ │ │ │ │ └── SidebarMenu.test.tsx.snap │ │ └── TagsCard │ │ │ ├── README.md │ │ │ ├── TagsCard.test.tsx │ │ │ ├── TagsCard.tsx │ │ │ └── __snapshots__ │ │ │ └── TagsCard.test.tsx.snap │ ├── css │ │ ├── fonts │ │ │ ├── horta-webfont.woff │ │ │ └── horta-webfont.woff2 │ │ ├── responsive.css │ │ ├── semantic.min.css │ │ ├── styles.css │ │ └── themes │ │ │ └── default │ │ │ └── assets │ │ │ ├── fonts │ │ │ ├── icons.eot │ │ │ ├── icons.otf │ │ │ ├── icons.svg │ │ │ ├── icons.ttf │ │ │ ├── icons.woff │ │ │ └── icons.woff2 │ │ │ └── images │ │ │ └── flags.png │ ├── declarations.d.ts │ ├── graphql-types.d.ts │ ├── html.tsx │ ├── pages │ │ ├── 404.tsx │ │ ├── about.tsx │ │ ├── blog.tsx │ │ └── index.tsx │ ├── store.ts │ └── templates │ │ ├── blog-page.tsx │ │ ├── blog-post.tsx │ │ └── tags-page.tsx ├── test │ ├── __mocks__ │ │ └── path.js │ ├── __snapshots__ │ │ └── gatsby-node.test.js.snap │ ├── data-integrity.test.js │ └── gatsby-node.test.js ├── tools │ └── update-post-date.js ├── tsconfig.json ├── tslint.json └── yarn.lock └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 2 | 3 | version: 2.1 4 | 5 | executors: 6 | node: 7 | parameters: 8 | dir: 9 | description: The working directory to use 10 | type: string 11 | docker: 12 | - image: circleci/node:10 13 | working_directory: ~/repo/<< parameters.dir >> 14 | 15 | commands: 16 | yarn-install: 17 | description: A command that handles installing and caching npm packages 18 | parameters: 19 | cache-key: 20 | description: String to differentiate caches 21 | type: string 22 | steps: 23 | - checkout: 24 | path: ~/repo 25 | - restore_cache: 26 | keys: 27 | - v1-<< parameters.cache-key >>-dependencies-{{ checksum "package.json" }} 28 | # fallback to using the latest cache if no exact match is found 29 | - v1-<< parameters.cache-key >>-dependencies- 30 | - run: yarn install 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-<< parameters.cache-key >>-dependencies-{{ checksum "package.json" }} 35 | 36 | 37 | jobs: 38 | # The Peril web server 39 | api: 40 | executor: 41 | name: node 42 | dir: api 43 | 44 | steps: 45 | - yarn-install: 46 | cache-key: api 47 | - run: yarn jest --max-workers=2 48 | 49 | # The Peril admin dashboard 50 | dashboard: 51 | executor: 52 | name: node 53 | dir: dashboard 54 | 55 | steps: 56 | - yarn-install: 57 | cache-key: dash 58 | - run: yarn build 59 | 60 | # The Peril public front-end 61 | web: 62 | executor: 63 | name: node 64 | dir: web 65 | 66 | steps: 67 | - yarn-install: 68 | cache-key: web 69 | - run: yarn build 70 | - run: yarn jest --max-workers=2 71 | 72 | deploy: 73 | executor: 74 | name: node 75 | dir: '' 76 | steps: 77 | - run: echo "deploy here..." 78 | 79 | 80 | workflows: 81 | build: 82 | jobs: 83 | - api 84 | - dashboard 85 | - web 86 | - deploy: 87 | filters: 88 | branches: 89 | only: master 90 | requires: 91 | - api 92 | - dashboard 93 | - web 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | pids 4 | node_modules 5 | .npm 6 | *.dat 7 | out 8 | .env 9 | .next 10 | thing.pem 11 | thing.pub 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | source/github/events/handlers/_tests/fixtures/PerilRunnerBootStrapExample.json 2 | source/github/events/handlers/_tests/fixtures/PerilRunnerEventBootStrapExample.json 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": false, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "proseWrap": "always" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["eg2.tslint"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/.DS_Store": true, 7 | "out/": true 8 | }, 9 | "search.exclude": { 10 | "**/node_modules": true, 11 | "out/": true 12 | }, 13 | "eslint.enable": false, 14 | "editor.rulers": [120], 15 | "tslint.autoFixOnSave": true, 16 | "tslint.run": "onType", 17 | "tslint.exclude": "node_modules/**/*", 18 | "editor.formatOnSave": true, 19 | "editor.renderWhitespace": "boundary", 20 | "cSpell.words": [ 21 | "GHAPI", 22 | "Graphi", 23 | "HEROKU", 24 | "PAPERTRAIL", 25 | "PRDSL", 26 | "PRJSONDSL", 27 | "Schedulable", 28 | "Sidenote", 29 | "dockerhub", 30 | "doggos", 31 | "everys", 32 | "filepaths", 33 | "mockingoose", 34 | "prioritise" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-slim 2 | 3 | ADD . /app 4 | WORKDIR /app/api 5 | 6 | # This will also trigger the build process 7 | RUN yarn install 8 | 9 | ENV PORT=80 10 | EXPOSE 80 11 | 12 | CMD yarn start 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2019 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 | -------------------------------------------------------------------------------- /VISION.md: -------------------------------------------------------------------------------- 1 | # Why Bother? 2 | 3 | Hosted Danger means being able to think about GitHub's webhook system as something trivial to build upon to make your 4 | own workflows. 5 | 6 | Hosted Danger comes with a few interesting aspects: 7 | 8 | - Installation can literally be a single click on the website. 9 | - Because Peril is not running Danger on CI, Danger can run against any webhook. 10 | - Simpler security model: 11 | 12 | - no need to consider scope for tokens 13 | - no need to ensure bot has access to repo 14 | - no need to ensure token isn't leaked 15 | 16 | I've wanted to do this for a [long, long time](https://github.com/danger/danger/issues/42) and the re-write aspect of 17 | Danger JS means that I could apply the constraints necessary for running hosted from day-1. 18 | 19 | # Minimum Viable Peril 20 | 21 | Peril started in 2016, and hopefully with launch in 2018. I have to eventually make a line in the sand and say, this is 22 | what we ship with. This is effectively the launch checklist. 23 | 24 | ## Runner 25 | 26 | - [x] Runs a Dangerfile with the Danger DSL on a PR event 27 | - [x] Runs a Dangerfile with webhook issue on other events 28 | - [x] Supports running async Dangerfiles easily 29 | - [x] Supports safely evaluating code 30 | 31 | ## Peril 32 | 33 | - [x] Allows regular scheduling of a task 34 | - [x] Allows scheduling of tasks in the future 35 | - [x] Allows deciding what events you're interested in running code from 36 | - [x] Allows storing ENV vars in a non-public way 37 | - [x] Keeps the database representation up-to-date with the repo 38 | 39 | ## Admin 40 | 41 | - [x] Can see orgs I need to set up 42 | - [x] Can see all the settings and keys for any orgs I'm in 43 | - [x] Can trigger a dev mode to record webhooks 44 | - [x] Can see the results of Danger runs inside the dashboard 45 | - [x] Can run any task from the admin to verify 46 | 47 | ## Homepage 48 | 49 | - [ ] Can understand Peril in a single page 50 | - [ ] Can describe pricing 51 | - [ ] Can sign up for Peril 52 | - [ ] Can get to guides 53 | - [ ] Can get to tutorials 54 | - [ ] Can get set up simply 55 | 56 | ![](https://ortastuff.s3.amazonaws.com/gifs/danger.gif) 57 | -------------------------------------------------------------------------------- /api/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-3"], 3 | "plugins": ["transform-flow-strip-types", "syntax-async-functions", "transform-regenerator"] 4 | } 5 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode 4 | config 5 | docs 6 | -------------------------------------------------------------------------------- /api/.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_JSON_FILE='artsy/peril-settings@peril.settings.json' 2 | PERIL_BOT_USER_ID=27295005 3 | PERIL_INTEGRATION_ID=2045 4 | PERIL_ORG_INSTALLATION_ID=23511 5 | PERIL_WEBHOOK_SECRET=webhook_secret 6 | PRIVATE_GITHUB_SIGNING_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAzfht/a9JH3zU1LHzCDzTi9qPpngV9+xP/OW6vemCeuGzFY3T\nLQDMhC25+au/6nJnwi7PKfzZ0SSRbs9NTDAdMC4DsZEaxx2ONc9L0hd9mamY8rvW\nuXgp0tGpSAsB8ZjBisi39P3Pv0G51coucuM3wo+0/htoatOYRqCLd/u9Y/S8Av7U\nYBhuGBBoQ0vr58x34YA4l7kK4jrFG5Xmg4iEC/+Iq2nqA5FXz7SZMEaY/Oh948lA\nF3d4JKEyEPk1mXf7lHvSSs2a+EpvhGTsh+mMpmAqCUEIBu4ZzYGa3PmhN34wYV3H\nGDAWRsIqkr9ocGPxLdND6S1QcSHX/EEw2uKdwQIDAQABAoIBAFH0gOeJQKJLzG/Z\nYqerdE1YqQIHFE6y89zgGB8K9AUrG1P/O8DTaY1KmI50vYdvAEQu1fWSC6WBVHDw\nAYTIPET4ejXEVBBYfUaB9lxhRnPHHPmwri7cVl/xVtc4sgxMyO4NEc1k1K34XBZq\nPXMvX/eFsPHPPAwNp7CqnvQd8ez8NACIjQypn21rbLWlMNAFfOwVywxUdHrwgq9l\ncgXkjTZ87WeaqdRuERkrWq1cMOf8aeZ3VwtaJw+dl26BeJDpthgoihekexh/itUT\nrDZZ8aSZ21Ay75nhylDoJVIHeX9thU7aXBNHq1pClXDDJNRHItaDo0UUj9H+Ml1T\nBrXKDAUCgYEA+wSgXWDAK4t6s21FEkNvzFWXaeBRuTV7t35NivyA801FAuVFZYvM\nEXYLtjDAR/G/PGoS1uCZpOv1lr0NHXqWdydvEhWbdecvfd0apKetDwg9oz9ZIEjP\nWCUhMVj4Dhv8xCC9NnA82v8rdUGtKUJNNodKt8nmOWF1A/nzvV4Fr8MCgYEA0g7s\nzAVlMq8xAuaZVg1DrSahHzIVK+ybQ4fcSXh0kr9Q9eZDcmJFr9Ua5Ns7cuustSQ2\nlmzMszXMHtYjDzFxOr2BHrmQUpa9KHxQ2vmNkeyE9BJnDtsOmv3AUCCAkrIykx1W\ni4P0EdKjcadqpM/ncGhJ1oTOvlQ6ROZ6I1zQCCsCgYA0WNOkhX+t1QmCj3/+/gc7\ngoHv93Kzf/7L1lAPcvblRjDlGLDvK0ckQQzSMrp2hmiODcTeALqp1PdDNyucASuN\nr7CPAeiInuydf9WFjt4dK2fHOne4rtZmmF+ird4U+yssL3Ol/aQDO4Fxk3+sAgKe\nI3qmdzlGsBc5/A8jZ48iBwKBgQCMw++vsRXl5z0C4qFy5ySpDi//BcLmUoKhuJPI\nsG3+QTLx9kLQjkaA1GBXaQrnskNkiEYEfBiZJ74IFBwefFWStZenjVbH4bgVP2m3\ntHBIj6VxIjvp2qZo+w87IuzGYaA+sfRjPiP7JZrd+QEgPSqtS1Xf6dPZ+/uoXIMQ\n24uVTQKBgFE0+oMv4iwwex8EPtisbnA0IkbsZMt90SA7+/eqGIkxK3YcMZds8vwb\n/pPYUJdw+uLHVm5LQFPoRiuaCDDX7g9HkVDSdZlS9WNQXdFiL0epAh63OwAHSlEz\nMerKqr4JPYFGTv8A2I2X8rXwczpExtBJCrDUHtfQklJvo15OX37z\n-----END RSA PRIVATE KEY-----\n" 7 | PUBLIC_GITHUB_SIGNING_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzfht/a9JH3zU1LHzCDzT\ni9qPpngV9+xP/OW6vemCeuGzFY3TLQDMhC25+au/6nJnwi7PKfzZ0SSRbs9NTDAd\nMC4DsZEaxx2ONc9L0hd9mamY8rvWuXgp0tGpSAsB8ZjBisi39P3Pv0G51coucuM3\nwo+0/htoatOYRqCLd/u9Y/S8Av7UYBhuGBBoQ0vr58x34YA4l7kK4jrFG5Xmg4iE\nC/+Iq2nqA5FXz7SZMEaY/Oh948lAF3d4JKEyEPk1mXf7lHvSSs2a+EpvhGTsh+mM\npmAqCUEIBu4ZzYGa3PmhN34wYV3HGDAWRsIqkr9ocGPxLdND6S1QcSHX/EEw2uKd\nwQIDAQAB\n-----END PUBLIC KEY-----" 8 | -------------------------------------------------------------------------------- /api/Brewfile: -------------------------------------------------------------------------------- 1 | brew "awscli" 2 | -------------------------------------------------------------------------------- /api/bin/lambda.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/api/bin/lambda.zip -------------------------------------------------------------------------------- /api/dangerfile.lite.ts: -------------------------------------------------------------------------------- 1 | import { danger, warn } from "danger" 2 | import { readFileSync } from "fs" 3 | 4 | const hasChangelog = danger.git.modified_files.includes("CHANGELOG.md") 5 | if (!hasChangelog) { 6 | warn("Please add a changelog entry for your changes. You can find it in `CHANGELOG.md`") 7 | } 8 | 9 | // Ensure the NodeJS versions match everywhere 10 | const nodeVersion = JSON.parse(readFileSync("package.json", "utf8")).engines.node 11 | 12 | if (!readFileSync("../.circleci/config.yml", "utf8").includes("circleci/node:" + nodeVersion)) { 13 | warn("The `.circleci/config.yml` does not have the same version of node in it (" + nodeVersion + ")") 14 | } 15 | 16 | if ( 17 | danger.git.modified_files.includes("source/db/GitHubRepoSettings.ts") && 18 | danger.git.modified_files.includes("source/db/index.ts") 19 | ) { 20 | warn("You may need to run `yarn generate:types:schema`.") 21 | } 22 | -------------------------------------------------------------------------------- /api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | peril: 4 | build: 5 | context: . 6 | env_file: 7 | - .env.sample 8 | ports: 9 | - 80:80 10 | depends_on: 11 | - peril-db 12 | peril-db: 13 | image: postgres:9.5 14 | ports: 15 | - 5432:5432 16 | ngrok: 17 | image: wernight/ngrok 18 | command: "ngrok http peril:80" 19 | ports: 20 | - 4040:4040 21 | -------------------------------------------------------------------------------- /api/now.staging.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peril-api", 3 | "alias": ["staging-api.peril.systems"], 4 | "type": "npm", 5 | "public": false, 6 | "env": { 7 | "PERIL_BOT_USER_ID": "@stag_peril_bot_user_id", 8 | "PERIL_INTEGRATION_ID": "@stag_peril_integration_id", 9 | "PERIL_WEBHOOK_SECRET": "@stag_peril_webhook_secret", 10 | 11 | "PRIVATE_GITHUB_SIGNING_KEY": "@stag_private_github_signing_key", 12 | "PUBLIC_GITHUB_SIGNING_KEY": "@stag_public_github_signing_key", 13 | 14 | "MONGODB_URI": "@stag_mongodb_uri", 15 | 16 | "PUBLIC_FACING_API": "true", 17 | "PRODUCTION": "false", 18 | "PUBLIC_API_ROOT_URL": "https://staging-api.peril.systems", 19 | "PUBLIC_WEB_ROOT_URL": "https://staging.peril.systems", 20 | "PUBLIC_GITHUB_APP_URL": "https://github.com/apps/peril-staging", 21 | 22 | "GITHUB_CLIENT_ID": "@stag_github_client_id", 23 | "GITHUB_CLIENT_SECRET": "@stag_github_client_secret", 24 | 25 | "SENTRY_DSN": "@stag_sentry_dsn", 26 | 27 | "AWS_ACCESS_KEY_ID": "@stag_aws_access_key_id", 28 | "AWS_SECRET_ACCESS_KEY": "@stag_aws_secret_access_key", 29 | "AWS_REGION": "us-east-1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Peril API 5 | 6 | 7 | 8 |

Welcome to the Peril API

9 |

10 | 11 |

12 |

There isn't much to do on here TBH, you're probably better off going to 13 | peril.systems 14 |

15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /api/scripts/deploy-staging-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Installing Now (if needed)" 4 | command -v now >/dev/null 2>&1 || { npm install -g now; } 5 | 6 | echo "Deploying, and aliasing" 7 | now deploy --local-config now.staging.json --team peril --token $NOW_TOKEN --npm 8 | now alias --local-config now.staging.json --team peril --token $NOW_TOKEN 9 | 10 | echo "Killing old instances" 11 | now rm peril-api --local-config now.staging.json --team peril --token $NOW_TOKEN --safe --yes 12 | -------------------------------------------------------------------------------- /api/scripts/deploy-staging-layer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Building with TypeScript" 4 | yarn build 5 | 6 | echo "Generating the Runner package.json" 7 | yarn ts-node source/scripts/generate-runner-deps.ts 8 | 9 | echo "Installing the deps" 10 | cd ../runner 11 | yarn install 12 | 13 | cd ../api 14 | 15 | echo "Copying over the node_modules" 16 | cp -rf ../runner/node_modules out 17 | 18 | echo "Trimming any dts files" 19 | ./scripts/trim_node_modules.sh 20 | 21 | echo "Zipping and uploading" 22 | zip runner.zip -r out 23 | aws lambda publish-layer-version --layer-name peril-staging-runtime --zip-file fileb://runner.zip --profile peril 24 | rm runner.zip 25 | 26 | # Based on https://claudiajs.com/tutorials/aws-cli-tricks.html 27 | 28 | echo Zipping up function 29 | zip bin/lambda.zip ../runner/index.js ../runner/tsconfig.json ../runner/app-module-path.js -j 30 | 31 | # Grab all of the lambdas and scope it to only Peril staging instances 32 | lambdas=$(aws lambda list-functions --profile peril --query 'Functions[?starts_with(FunctionName, `s-`)].FunctionName' --output text) 33 | 34 | # Grab the current runtime ARN ( e.g. arn:aws:lambda:us-east-1:123456:layer:peril-staging-runtime:11 ) 35 | runtime=$(aws lambda list-layers --profile peril --query 'Layers[?starts_with(LayerName, `peril-s`)].LatestMatchingVersion.LayerVersionArn' --output text) 36 | echo Updating all functions to use $runtime 37 | 38 | for lambda in $lambdas; do 39 | echo Updating the layer for $lambda 40 | aws lambda update-function-configuration --function-name $lambda --layers $runtime --profile peril 41 | done 42 | -------------------------------------------------------------------------------- /api/scripts/deploy-staging-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Based on https://claudiajs.com/tutorials/aws-cli-tricks.html 4 | 5 | echo Zipping up function 6 | 7 | # -j = ignore files 8 | # -X = ignore metadata (deterministic-ish builds) 9 | zip bin/lambda.zip ../runner/index.js ../runner/tsconfig.json ../runner/.babelrc -j -X 10 | 11 | # Grab all of the lambdas and scope it to only Peril staging instances 12 | lambdas=$(aws lambda list-functions --profile peril --query 'Functions[?starts_with(FunctionName, `s-`)].FunctionName' --output text) 13 | 14 | # Grab the current runtime ARN ( e.g. arn:aws:lambda:us-east-1:123456:layer:peril-staging-runtime:11 ) 15 | runtime=$(aws lambda list-layers --profile peril --query 'Layers[?starts_with(LayerName, `s-`)].LatestMatchingVersion.LayerVersionArn' --output text) 16 | 17 | for lambda in $lambdas; do 18 | echo Updating the code for $lambda 19 | aws lambda update-function-code --function-name $lambda --zip-file fileb://bin/lambda.zip --profile peril 20 | done 21 | -------------------------------------------------------------------------------- /api/scripts/trim_node_modules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove some of the largest dependencies 4 | rm -rf out/node_modules/@types \ 5 | out/node_modules/prettier \ 6 | out/node_modules/aws-lambda \ 7 | out/node_modules/prettier \ 8 | out/node_modules/rxjs \ 9 | out/node_modules/rx \ 10 | out/node_modules/fsevents \ 11 | out/node_modules/ice-cap \ 12 | out/node_modules/aws-sdk/dist/aws-sdk-react-native.js 13 | 14 | # Kill the types from modules 15 | find ./out -name "*.d.ts" -delete 16 | 17 | 18 | -------------------------------------------------------------------------------- /api/source/api/_tests/fixtureAPI.ts: -------------------------------------------------------------------------------- 1 | import { GitHub, GitHubType } from "danger/distribution/platforms/GitHub" 2 | import { GitHubAPI } from "danger/distribution/platforms/github/GitHubAPI" 3 | 4 | import { readFileSync } from "fs" 5 | 6 | import { resolve } from "path" 7 | 8 | const githubFixtures = resolve(__dirname, "..", "..", "github", "_tests", "fixtures") 9 | 10 | /** Returns JSON from the fixtured dir */ 11 | const requestWithFixturedJSON = (path: string): (() => Promise) => () => 12 | Promise.resolve(JSON.parse(readFileSync(`${githubFixtures}/${path}`, {}).toString())) 13 | 14 | /** Returns arbitrary text value from a request */ 15 | const requestWithFixturedContent = (path: string): (() => Promise) => () => 16 | Promise.resolve(readFileSync(`${githubFixtures}/${path}`, {}).toString()) 17 | 18 | /** Returns a fixtured GitHub instance */ 19 | 20 | export const fixturedAPI = (repoSlug?: string, pullRequestID?: string): GitHubType => { 21 | repoSlug = repoSlug || "artsy/emission" 22 | pullRequestID = pullRequestID || "1" 23 | const api = new GitHubAPI({ repoSlug, pullRequestID }, "ABCDE") 24 | const platform = GitHub(api) 25 | 26 | api.getPullRequestInfo = requestWithFixturedJSON("github_pr.json") 27 | api.getPullRequestDiff = requestWithFixturedContent("github_diff.diff") 28 | api.getPullRequestCommits = requestWithFixturedJSON("github_commits.json") 29 | api.getReviewerRequests = requestWithFixturedJSON("github_requested_reviewers.json") 30 | api.getReviews = requestWithFixturedJSON("github_reviews.json") 31 | api.getIssue = requestWithFixturedJSON("github_issue.json") 32 | 33 | return platform 34 | } 35 | -------------------------------------------------------------------------------- /api/source/api/auth/_tests/_generate.test.ts: -------------------------------------------------------------------------------- 1 | import { createPerilUserJWT, getDetailsFromPerilJWT, PerilOAuthUser } from "../generate" 2 | 3 | global.Date = jest.fn(() => ({ getTime: () => 1000 })) as any 4 | global.Date.now = () => 2000 5 | 6 | it("creates a JWT for a stubbed user", () => { 7 | const user: PerilOAuthUser = { 8 | name: "MurphDog", 9 | avatar_url: "123", 10 | } 11 | const jwt = createPerilUserJWT(user, []) 12 | // Just a prefix for now 13 | expect(jwt).toContain("eyJh") 14 | }) 15 | 16 | it("gets data from a JWT it generated", async () => { 17 | const user: PerilOAuthUser = { 18 | name: "MurphDog", 19 | avatar_url: "123", 20 | } 21 | const installations = [1] 22 | const jwt = createPerilUserJWT(user, installations) 23 | // Just a prefix for now 24 | const data = await getDetailsFromPerilJWT(jwt) 25 | expect(data).toEqual({ data: { user: { avatar_url: "123", name: "MurphDog" } }, exp: 7776001, iat: 1, iss: ["1"] }) 26 | }) 27 | 28 | it("raises when then JWT is invalid", async () => { 29 | await expect(getDetailsFromPerilJWT("AASDAFSDF.ASDADSADSAD.ASDASDASD")).rejects.toThrow() 30 | }) 31 | -------------------------------------------------------------------------------- /api/source/api/auth/generate.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from "jsonwebtoken" 2 | 3 | import { isString } from "util" 4 | import { PRIVATE_GITHUB_SIGNING_KEY, PUBLIC_GITHUB_SIGNING_KEY } from "../../globals" 5 | 6 | // A JWT is a special type of string 7 | type JWT = string 8 | 9 | type PerilUserJWT = JWT 10 | 11 | // The Decoded JWT data 12 | export interface PerilJWT { 13 | exp?: number 14 | iat: number 15 | iss: string[] 16 | data: { 17 | user: PerilOAuthUser 18 | } 19 | } 20 | 21 | export interface PerilOAuthUser { 22 | name: string 23 | avatar_url: string 24 | } 25 | 26 | /** 27 | * Takes a user with details from GH Oauth and generates 28 | * a JWT which can be used to make authenticated requests 29 | * against Peril. 30 | */ 31 | export const createPerilUserJWT = (user: PerilOAuthUser, installationIDs: number[]): PerilUserJWT => { 32 | const now = Math.round(new Date().getTime() / 1000) 33 | const keyContent = PRIVATE_GITHUB_SIGNING_KEY 34 | const payload: PerilJWT = { 35 | iat: now, 36 | iss: installationIDs.map(id => String(id)), 37 | data: { 38 | user, 39 | }, 40 | } 41 | 42 | return jwt.sign(payload, keyContent, { algorithm: "RS256", expiresIn: "90 days" }) 43 | } 44 | 45 | /** 46 | * Decode and verifies a JWT generated by createPerilJWT above 47 | * @param token the JWT 48 | */ 49 | export const getDetailsFromPerilJWT = (token: PerilUserJWT) => 50 | new Promise((res, rej) => { 51 | const options = { algorithms: ["RS256"] } 52 | jwt.verify(token, PUBLIC_GITHUB_SIGNING_KEY, options, (err, decoded) => { 53 | if (err) { 54 | rej(err) 55 | } else { 56 | if (isString(decoded)) { 57 | res(JSON.parse(decoded as string)) 58 | } else { 59 | res(decoded as any) 60 | } 61 | } 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /api/source/api/auth/getJWTFromRequest.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "cookie" 2 | import * as cookieParser from "cookie-parser" 3 | import { isString } from "util" 4 | 5 | /** Pulls out the JWT from the request, it can either be implicit via the cookies (client), or explicit in a header (server) */ 6 | export const getJWTFromRequest = (req: any) => { 7 | // Support JWT via cookies from the user session 8 | const cookies = (req && req.cookies) || (req && req.headers && req.headers.cookie) 9 | if (cookies && isString(cookies)) { 10 | return cookies && parse(cookies).jwt 11 | } 12 | 13 | if (cookies && cookieParser.JSONCookies(cookies).jwt) { 14 | return cookieParser.JSONCookies(cookies).jwt 15 | } 16 | 17 | // Support standard auth: "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" 18 | const basicAuth = req.headers.Authorization || req.headers.authorization 19 | if (basicAuth) { 20 | if (basicAuth.includes("Basic ")) { 21 | return basicAuth.split("Basic ")[1] 22 | } 23 | } 24 | 25 | // No other auth routes 26 | return undefined 27 | } 28 | -------------------------------------------------------------------------------- /api/source/api/aws/lambda.ts: -------------------------------------------------------------------------------- 1 | import * as awsSDK from "aws-sdk" 2 | import { readFileSync } from "fs" 3 | import { join } from "path" 4 | 5 | export const createLambdaFunctionForInstallation = async (installationName: string) => { 6 | const lambda = new awsSDK.Lambda() 7 | const isProd = process.env.PRODUCTION === "true" 8 | const prefix = isProd ? "p" : "s" 9 | const randoSuffix = Math.random() 10 | .toString(36) 11 | .substring(7) 12 | 13 | const name = `${prefix}-${installationName}-${randoSuffix}` 14 | 15 | const layer = await getLatestLayer() 16 | const lambdaZip = readFileSync(join(__dirname, "..", "..", "..", "bin", "lambda.zip")) 17 | await lambda 18 | .createFunction( 19 | { 20 | FunctionName: name, 21 | MemorySize: 512, 22 | Layers: [layer.LatestMatchingVersion!.LayerVersionArn!], 23 | Timeout: 30, 24 | Code: { 25 | ZipFile: lambdaZip, 26 | }, 27 | Role: "arn:aws:iam::656992703780:role/peril-minimal-access", 28 | Runtime: "nodejs8.10", 29 | Handler: "index.handler", 30 | }, 31 | undefined 32 | ) 33 | .promise() 34 | 35 | return { 36 | success: true, 37 | name, 38 | } 39 | } 40 | 41 | export const deleteLambdaFunctionNamed = (name: string) => { 42 | const lambda = new awsSDK.Lambda() 43 | return lambda.deleteFunction({ FunctionName: name }, undefined).promise() 44 | } 45 | 46 | export const invokeLambda = (name: string, payloadString: string) => { 47 | const lambda = new awsSDK.Lambda() 48 | return lambda.invokeAsync({ FunctionName: name, InvokeArgs: payloadString }, undefined).promise() 49 | } 50 | 51 | export const getLatestLayer = async () => { 52 | const lambda = new awsSDK.Lambda() 53 | 54 | const isProd = process.env.PRODUCTION === "true" 55 | const runtime = isProd ? "production" : "staging" 56 | const layerName = `peril-${runtime}-runtime` 57 | 58 | const allLayers = await lambda.listLayers({}).promise() 59 | const { data } = allLayers.$response 60 | if (data && data.Layers) { 61 | const thisLayer = data.Layers.find(l => l.LayerName === layerName) 62 | if (!thisLayer) { 63 | throw new Error("Could not get find layer from AWS") 64 | } 65 | 66 | return thisLayer 67 | } 68 | 69 | throw new Error("Could not get the layer from AWS") 70 | } 71 | -------------------------------------------------------------------------------- /api/source/api/github.ts: -------------------------------------------------------------------------------- 1 | import { PERIL_INTEGRATION_ID, PRIVATE_GITHUB_SIGNING_KEY } from "../globals" 2 | 3 | import { App } from "@octokit/app" 4 | 5 | export const GithubApp = new App({ id: PERIL_INTEGRATION_ID, privateKey: PRIVATE_GITHUB_SIGNING_KEY }) 6 | 7 | export async function getTemporaryAccessTokenForInstallation(installationId: number): Promise { 8 | return await GithubApp.getInstallationAccessToken({ installationId }) 9 | } 10 | -------------------------------------------------------------------------------- /api/source/api/graphql/api.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from "../../api/fetch" 2 | import logger from "../../logger" 3 | 4 | export const graphqlAPI = (url: string, query: string) => 5 | fetch(`${url}/api/graphql`, { 6 | method: "POST", 7 | body: JSON.stringify({ query }), 8 | headers: { "Content-Type": "application/json", Accept: "application/json" }, 9 | }) 10 | .then(res => { 11 | if (res.ok) { 12 | return res.json() 13 | } else { 14 | throw new Error(`GraphQL API HTTP error\n> ${res.status} ${res.statusText} \n\nQuery:\n${query}}`) 15 | } 16 | }) 17 | .then(body => { 18 | if (body.errors) { 19 | logger.info("Received errors from the GraphQL API") 20 | logger.info(JSON.parse(body.errors)) 21 | } 22 | return body 23 | }) 24 | .catch(e => { 25 | logger.error("Error making an API call to the GraphQL API") 26 | logger.error(e) 27 | }) 28 | -------------------------------------------------------------------------------- /api/source/api/graphql/aws/getCloudWatchForInstallation.ts: -------------------------------------------------------------------------------- 1 | // import * as awsSDK from "aws-sdk" 2 | // import { getDB } from "../../../db/getDB" 3 | 4 | // export const getCloudWatchForInstallation = async (iID: number, time: string) => { 5 | // const installation = await getDB().getInstallation(iID) 6 | // if (!installation || !installation.lambdaName) { 7 | // return undefined 8 | // } 9 | 10 | // const logs = new awsSDK.CloudWatchLogs() 11 | // const groups = await logs.describeLogGroups({ logGroupNamePrefix: installation.lambdaName }, undefined).promise 12 | // } 13 | -------------------------------------------------------------------------------- /api/source/api/graphql/github/api.ts: -------------------------------------------------------------------------------- 1 | import GitHub from "@octokit/rest" 2 | import { getTemporaryAccessTokenForInstallation } from "../../github" 3 | 4 | export const octokitForInstallation = async (installationID: number) => { 5 | const gh = new GitHub() 6 | const token = await getTemporaryAccessTokenForInstallation(installationID) 7 | gh.authenticate({ type: "app", token }) 8 | return gh 9 | } 10 | -------------------------------------------------------------------------------- /api/source/api/graphql/github/createNewRepoForPerilSettings.ts: -------------------------------------------------------------------------------- 1 | import { getDB } from "../../../db/getDB" 2 | import { octokitForInstallation } from "./api" 3 | 4 | // This is so we can present a UI where you can create a repo 5 | // 6 | // Note: this only works on a GitHub org, not a user! 7 | // 8 | export const createNewRepoForPerilSettings = async (installationID: number, repoName: string, privateRepo: boolean) => { 9 | const db = getDB() 10 | const installation = await db.getInstallation(installationID) 11 | 12 | if (!installation) { 13 | throw new Error(`Installation not found`) 14 | } 15 | 16 | const gh = await octokitForInstallation(installationID) 17 | 18 | const description = "The Peril Settings repo" 19 | const homepage = "https://peril.systems/docs" 20 | 21 | const newRepo = await gh.repos.createInOrg({ 22 | has_wiki: false, 23 | org: installation.login, 24 | name: repoName, 25 | private: privateRepo, 26 | description, 27 | homepage, 28 | }) 29 | 30 | return newRepo.data 31 | } 32 | -------------------------------------------------------------------------------- /api/source/api/graphql/github/getAvailableReposForInstallation.ts: -------------------------------------------------------------------------------- 1 | import { getDB } from "../../../db/getDB" 2 | import { octokitForInstallation } from "./api" 3 | 4 | // This is so we can present a UI where you can pick a repo 5 | export const getAvailableReposForInstallation = async (installationID: number) => { 6 | const db = getDB() 7 | const installation = await db.getInstallation(installationID) 8 | 9 | if (!installation) { 10 | throw new Error(`Installation not found`) 11 | } 12 | 13 | const gh = await octokitForInstallation(installationID) 14 | // Probably works? 15 | const allRepos = await gh.paginate(gh.apps.listRepos) 16 | 17 | return allRepos 18 | } 19 | -------------------------------------------------------------------------------- /api/source/api/graphql/github/sendPRForPerilSettingsRepo.ts: -------------------------------------------------------------------------------- 1 | import { getDB } from "../../../db/getDB" 2 | import { octokitForInstallation } from "./api" 3 | 4 | import { fileMapForPerilSettingsRepo, NewRepoOptions } from "@peril/utils" 5 | 6 | import { createOrUpdatePR } from "danger/distribution/platforms/github/GitHubUtils" 7 | 8 | // Submits a PR with the metadata, and returns the PR JSON 9 | export const sendPRForPerilSettingsRepo = async (installationID: number, options: NewRepoOptions) => { 10 | const db = getDB() 11 | const installation = await db.getInstallation(installationID) 12 | 13 | if (!installation) { 14 | throw new Error(`Installation not found`) 15 | } 16 | 17 | const gh = await octokitForInstallation(installationID) 18 | 19 | const fileMap = await fileMapForPerilSettingsRepo(gh as any, options) 20 | const builder = createOrUpdatePR(undefined, gh) 21 | 22 | const newPR = await builder( 23 | { 24 | owner: installation.login, 25 | repo: options.repo.name, 26 | title: "Initial setup for your Peril Repo", 27 | baseBranch: "master", 28 | commitMessage: "Initial Commit", 29 | body: "Welcome to Peril", 30 | newBranchName: "peril_settings_init", 31 | }, 32 | fileMap 33 | ) 34 | return newPR.data 35 | } 36 | -------------------------------------------------------------------------------- /api/source/api/graphql/gql.ts: -------------------------------------------------------------------------------- 1 | // This is a template string function, which returns the original string 2 | // It's based on https://github.com/lleaff/tagged-template-noop 3 | // Which is MIT licensed to lleaff 4 | // 5 | 6 | export const gql = (strings: any, ...keys: any[]) => { 7 | const lastIndex = strings.length - 1 8 | return strings.slice(0, lastIndex).reduce((p: any, s: any, i: number) => p + s + keys[i], "") + strings[lastIndex] 9 | } 10 | -------------------------------------------------------------------------------- /api/source/api/graphql/mutations/_tests/_dangerfileFinished.test.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "graphql" 2 | import { gql } from "../../gql" 3 | import { schema } from "../../index" 4 | 5 | jest.mock("../../../../db/getDB") 6 | import { MockDB } from "../../../../db/__mocks__/getDB" 7 | import { getDB } from "../../../../db/getDB" 8 | import { createPerilSandboxAPIJWT } from "../../../../runner/sandbox/jwt" 9 | 10 | jest.mock("../../../api", () => ({ 11 | sendMessageToConnectionsWithAccessToInstallation: jest.fn(), 12 | sendAsyncMessageToConnectionsWithAccessToInstallation: jest.fn(), 13 | })) 14 | import { sendMessageToConnectionsWithAccessToInstallation } from "../../../api" 15 | 16 | const mockDB = getDB() as MockDB 17 | 18 | beforeEach(() => mockDB.clear()) 19 | 20 | describe("handle mutations", () => { 21 | it("sends a message to all connected clients", async () => { 22 | mockDB.getInstallation.mockReturnValueOnce({ iID: 1 }) 23 | const sandboxJWT = createPerilSandboxAPIJWT(1, ["dangerfileFinished"]) 24 | 25 | const mutate = gql` 26 | mutation { 27 | dangerfileFinished( 28 | jwt: "${sandboxJWT}", 29 | name: "mockEvent", 30 | dangerfiles: ["app.ts"], 31 | time: 123, 32 | hyperCallID: "123-654" 33 | ) { 34 | success 35 | } 36 | } 37 | ` 38 | 39 | const result = await graphql(schema, mutate, null, {}) 40 | expect(result).toEqual({ data: { dangerfileFinished: { success: true } } }) 41 | 42 | expect(sendMessageToConnectionsWithAccessToInstallation).toBeCalledWith(1, { 43 | event: "mockEvent", 44 | action: "finished", 45 | filenames: ["app.ts"], 46 | time: 123, 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /api/source/api/graphql/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { combineResolvers } from "graphql-resolvers" 2 | import { GraphQLContext } from "../../api" 3 | 4 | // A any'd resolver type, with the right context 5 | export type Resolver = (obj: any, params: any, context: GraphQLContext) => Promise 6 | 7 | // A combined resolver that checks for auth before running the resolver 8 | export const authD = (resolver: Resolver) => combineResolvers(isAuthenticated, resolver) 9 | 10 | export const isAuthenticated = (_: any, __: any, context: GraphQLContext) => { 11 | if (!context.jwt) { 12 | return new Error("Not authenticated") 13 | } 14 | return undefined 15 | } 16 | -------------------------------------------------------------------------------- /api/source/api/graphql/utils/installations.ts: -------------------------------------------------------------------------------- 1 | import { getDB } from "../../../db/getDB" 2 | import { MongoDB } from "../../../db/mongo" 3 | import { getDetailsFromPerilJWT } from "../../auth/generate" 4 | 5 | export const getUserInstallations = async (jwt: string) => { 6 | const decodedJWT = await getDetailsFromPerilJWT(jwt) 7 | const db = getDB() as MongoDB 8 | return await db.getInstallations(decodedJWT.iss.map(i => parseInt(i, 10))) 9 | } 10 | -------------------------------------------------------------------------------- /api/source/api/integration/github.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express" 2 | import { PUBLIC_GITHUB_APP_URL } from "../../globals" 3 | 4 | export const redirectForGHInstallation = (_: Request, res: Response, ___: NextFunction) => { 5 | res.redirect(PUBLIC_GITHUB_APP_URL) 6 | } 7 | -------------------------------------------------------------------------------- /api/source/api/pr/dsl.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | import winston from "../../logger" 3 | 4 | import { GitHub } from "danger/distribution/platforms/GitHub" 5 | import inline from "danger/distribution/runner/runners/inline" 6 | 7 | import { getTemporaryAccessTokenForInstallation } from "../../api/github" 8 | import { RunType } from "../../danger/danger_run" 9 | import { executorForInstallation } from "../../danger/danger_runner" 10 | import { getPerilPlatformForDSL } from "../../danger/peril_platform" 11 | import { githubAPIForCommentable } from "../../github/events/utils/commenting" 12 | import { PERIL_ORG_INSTALLATION_ID } from "../../globals" 13 | 14 | export const prDSLRunner = async (req: express.Request, res: express.Response, _: express.NextFunction) => { 15 | winston.info("[router] -- Received OK") 16 | 17 | const query = req.query 18 | if (!query.owner) { 19 | return res.status(422).jsonp({ error: "No `owner` query param sent." }) 20 | } 21 | 22 | if (!query.repo) { 23 | return res.status(422).jsonp({ error: "No `repo` query param sent." }) 24 | } 25 | 26 | if (!query.number) { 27 | return res.status(422).jsonp({ error: "No `number` query param sent." }) 28 | } 29 | 30 | // This has to be set for public usage. 31 | if (!PERIL_ORG_INSTALLATION_ID) { 32 | throw new Error("You can't support PR DSLs without setting up the PERIL_ORG_INSTALLATION_ID") 33 | } 34 | 35 | const token = await getTemporaryAccessTokenForInstallation(PERIL_ORG_INSTALLATION_ID) 36 | 37 | const ghDetails = { 38 | fullName: query.owner + "/" + query.repo, 39 | prID: query.number, 40 | } 41 | 42 | const githubAPI = githubAPIForCommentable(token, ghDetails.fullName, ghDetails.prID) 43 | 44 | const gh = GitHub(githubAPI) 45 | const platform = getPerilPlatformForDSL(RunType.pr, gh, {}) 46 | 47 | const exec = await executorForInstallation(platform, inline, {}) 48 | const dangerDSL = await exec.dslForDanger() 49 | 50 | // Remove this to reduce data 51 | if (dangerDSL.github) { 52 | dangerDSL.github.api = {} as any 53 | } 54 | 55 | // TODO: include Danger version number in JSON 56 | return res.status(400).jsonp({ 57 | danger: dangerDSL, 58 | status: "OK", 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /api/source/danger/_tests/_danger_runner_paths.test.ts: -------------------------------------------------------------------------------- 1 | const mockRunDangerfileEnvironment = jest.fn() 2 | jest.mock("danger/distribution/runner/runners/inline", () => ({ 3 | default: { 4 | createDangerfileRuntimeEnvironment: () => ({}), 5 | runDangerfileEnvironment: mockRunDangerfileEnvironment, 6 | }, 7 | })) 8 | 9 | import { DangerDSLJSONType } from "danger/distribution/dsl/DangerDSL" 10 | import { RunType } from "../danger_run" 11 | import { runDangerForInstallation } from "../danger_runner" 12 | 13 | const defaultSettings = { 14 | env_vars: [], 15 | ignored_repos: [], 16 | modules: [], 17 | } 18 | 19 | const installationSettings = { 20 | iID: 123, 21 | settings: defaultSettings, 22 | } 23 | 24 | const blankPayload = { dsl: {} as DangerDSLJSONType, webhook: {} } 25 | 26 | jest.mock("../../api/github", () => ({ 27 | getTemporaryAccessTokenForInstallation: () => Promise.resolve("123"), 28 | })) 29 | 30 | describe("paths", () => { 31 | it("passes an absolute string to runDangerfileEnvironment", async () => { 32 | await runDangerForInstallation( 33 | "mockEvent", 34 | [`dangerfile_empty.ts`], 35 | [""], 36 | null, 37 | RunType.pr, 38 | installationSettings, 39 | blankPayload 40 | ) 41 | 42 | const paths = mockRunDangerfileEnvironment.mock.calls[0][0] 43 | expect(paths[0].startsWith("/")).toBeTruthy() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /api/source/danger/_tests/_danger_runner_peril.test.ts: -------------------------------------------------------------------------------- 1 | import { appendPerilContextToDSL } from "../../danger/append_peril" 2 | import { stubSandbox } from "./sandbox_stub" 3 | 4 | const mockToken = "1234535345" 5 | jest.mock("../../api/github", () => ({ 6 | getTemporaryAccessTokenForInstallation: () => Promise.resolve(mockToken), 7 | })) 8 | 9 | it("adds peril to the DSL", async () => { 10 | const sandbox = stubSandbox() 11 | const perilDSL = { perilDSL: true } as any 12 | 13 | await appendPerilContextToDSL(123, undefined, sandbox, perilDSL) 14 | expect(sandbox.peril).toEqual({ perilDSL: true }) 15 | }) 16 | 17 | it("adds a GH API object to the DSL", async () => { 18 | const sandbox = stubSandbox() 19 | const perilDSL = { perilDSL: true } as any 20 | 21 | await appendPerilContextToDSL(123, undefined, sandbox, perilDSL) 22 | 23 | expect(sandbox.danger.github.api).toBeTruthy() 24 | 25 | // The older API used to allow checking for the auth methods 26 | // as of version 14, you can't get to it as it's a plugin 27 | // expect(sandbox.danger.github.api.auth).toEqual({ token: mockToken, type: "integration" }) 28 | }) 29 | -------------------------------------------------------------------------------- /api/source/danger/_tests/_danger_runner_webhook.test.ts: -------------------------------------------------------------------------------- 1 | import { DangerDSLJSONType, PerilDSL } from "danger/distribution/dsl/DangerDSL" 2 | import vm2 from "danger/distribution/runner/runners/inline" 3 | 4 | import { resolve } from "path" 5 | import { fixturedAPI } from "../../api/_tests/fixtureAPI" 6 | import { executorForInstallation, runDangerAgainstFileInline } from "../danger_runner" 7 | 8 | const dangerfilesFixtures = resolve(__dirname, "fixtures") 9 | const peril: PerilDSL = { env: {}, runTask: async () => undefined } 10 | 11 | const blankPayload = { 12 | dsl: { 13 | github: { 14 | issue: { 15 | id: 1, 16 | } as any, 17 | }, 18 | } as DangerDSLJSONType, 19 | webhook: {}, 20 | } 21 | 22 | jest.mock("../../api/github", () => ({ 23 | getTemporaryAccessTokenForInstallation: () => Promise.resolve("123"), 24 | })) 25 | 26 | const emptySettings = { 27 | env_vars: [], 28 | ignored_repos: [], 29 | modules: [], 30 | } 31 | 32 | const installationSettings = { 33 | iID: 123, 34 | settings: emptySettings, 35 | } 36 | 37 | // @ts-ignore 38 | global.regeneratorRuntime = {} 39 | 40 | describe("evaling an issue", () => { 41 | it.skip("runs a typescript dangerfile with fixtured data", async () => { 42 | const platform = fixturedAPI() 43 | const executor = executorForInstallation(platform, vm2, {}) 44 | const dangerfile = ` 45 | warn("Issue number: " + danger.github.issue.id) 46 | ` 47 | const results = await runDangerAgainstFileInline( 48 | [`${dangerfilesFixtures}/dangerfile_issue.ts`], 49 | [dangerfile], 50 | installationSettings, 51 | executor, 52 | peril, 53 | blankPayload 54 | ) 55 | 56 | expect(results).toEqual({ 57 | fails: [], 58 | markdowns: [], 59 | messages: [], 60 | warnings: [{ file: undefined, line: undefined, message: "Issue number: 1" }], 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /api/source/danger/_tests/_peril_platform.test.ts: -------------------------------------------------------------------------------- 1 | import { RunType } from "../danger_run" 2 | 3 | import { GitHub } from "danger/distribution/platforms/GitHub" 4 | import { getPerilPlatformForDSL } from "../peril_platform" 5 | 6 | it("Provides the Danger GitHub DSL for a PR", () => { 7 | const myAPI = {} as any 8 | const myEvent = { event: true } 9 | const platform = getPerilPlatformForDSL(RunType.pr, myAPI, myEvent) 10 | 11 | expect(platform).toBe(myAPI) 12 | }) 13 | 14 | it("Uses the event json when it's a non-PR event", async () => { 15 | const gh = GitHub({ 16 | getExternalAPI: () => ({ api: true }), 17 | fileContents: () => "", 18 | } as any) 19 | 20 | const myEvent = { event: true } 21 | 22 | const platform = getPerilPlatformForDSL(RunType.import, gh, myEvent) 23 | const platformDSL = await platform.getPlatformReviewDSLRepresentation() 24 | 25 | expect(platformDSL).toEqual({ 26 | api: { api: true }, 27 | event: true, 28 | utils: expect.anything(), 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /api/source/danger/_tests/fixtures/dangerfile_async_import.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-default-export 2 | export default async () => { 3 | const imported = await import("./file_to_import") 4 | // @ts-ignore 5 | markdown(imported.importedString) 6 | } 7 | -------------------------------------------------------------------------------- /api/source/danger/_tests/fixtures/dangerfile_empty.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | warn("OK") 3 | -------------------------------------------------------------------------------- /api/source/danger/_tests/fixtures/dangerfile_import_complex_module.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | 3 | import { schedule } from "danger" 4 | import spellcheck from "danger-plugin-spellcheck" 5 | 6 | schedule(spellcheck()) 7 | -------------------------------------------------------------------------------- /api/source/danger/_tests/fixtures/dangerfile_import_module.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { checkForRelease } from "danger-plugin-yarn" 3 | checkForRelease({ version: { before: "1.0.0", after: "1.0.1" } }) 4 | -------------------------------------------------------------------------------- /api/source/danger/_tests/fixtures/dangerfile_insecure.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | markdown("`Object.keys(process.env).length` is " + Object.keys(process.env).length) 3 | -------------------------------------------------------------------------------- /api/source/danger/_tests/fixtures/dangerfile_peril_obj.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | markdown(JSON.stringify(peril, null, " ")) 3 | -------------------------------------------------------------------------------- /api/source/danger/_tests/fixtures/file_to_import.ts: -------------------------------------------------------------------------------- 1 | // Mainly to prove that this all works 2 | export const importedString = "Imported Correctly" 3 | -------------------------------------------------------------------------------- /api/source/danger/_tests/sandbox_stub.ts: -------------------------------------------------------------------------------- 1 | export const stubSandbox = () => 2 | ({ danger: { github: { pr: { head: { ref: "ace123", repo: { full_name: "hi/hi" } } } } } } as any) 3 | -------------------------------------------------------------------------------- /api/source/danger/peril_ci_source.ts: -------------------------------------------------------------------------------- 1 | import { CISource } from "danger/distribution/ci_source/ci_source" 2 | 3 | export const source: CISource = { 4 | isCI: true, 5 | isPR: true, 6 | name: "Peril", 7 | pullRequestID: "not used", 8 | repoSlug: "not used", 9 | } 10 | -------------------------------------------------------------------------------- /api/source/db/GitHubRepoSettings.ts: -------------------------------------------------------------------------------- 1 | export interface GitHubInstallationSettings { 2 | /** 3 | * An array of modules for Peril to install, requires a re-deploy of the server to update. 4 | * They will be `yarn install`'d on the deploy, and available for Dangerfiles. 5 | * 6 | * @see not used in Peril staging/prod. You can find those in the Runner package.json 7 | */ 8 | modules?: string[] 9 | /** 10 | * An array of allowed ENV vars which are passed into Dangerfiles. 11 | * 12 | * @see not used in Peril staging/prod. These are managed via the admin dashboard 13 | */ 14 | env_vars?: string[] 15 | /** 16 | * An array of repos that should not run any Peril dangerfiles. This is so that you can 17 | * turn on Peril for an entire org, and just make the occasional edge case. 18 | */ 19 | ignored_repos?: string[] 20 | /** 21 | * Disables using GitHub checks in the messages posted by Danger. 22 | */ 23 | disable_github_check?: boolean 24 | } 25 | -------------------------------------------------------------------------------- /api/source/db/_tests/__snapshots__/_json.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Raises with a bad URL 1`] = `[Error: Could not find find a JSON file for Peril settings. It's likely that Peril cannot connect to orta/other@settings.json, check the logs for more info above here. You'll probably need to make changes to the "DATABASE_JSON_FILE" in your ENV vars.]`; 4 | 5 | exports[`makes the right calls to GitHub with a legit stubbed JSON file 1`] = ` 6 | Object { 7 | "avatarURL": "", 8 | "iID": 23511, 9 | "installationSlackUpdateWebhookURL": undefined, 10 | "lambdaName": "", 11 | "login": "unknown", 12 | "perilSettingsJSONURL": "orta/peril@settings.json", 13 | "repos": Object { 14 | "orta/ORStackView": Object { 15 | "issue.created": "orta/peril@lock_issues.ts", 16 | }, 17 | }, 18 | "rules": Object { 19 | "issue": "orta/peril@issue.ts", 20 | "pull_request": "orta/peril@pr.ts", 21 | }, 22 | "scheduler": Object {}, 23 | "settings": Object { 24 | "disable_github_check": false, 25 | "env_vars": Array [], 26 | "ignored_repos": Array [], 27 | "modules": Array [], 28 | }, 29 | "tasks": Object {}, 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /api/source/db/_tests/_fixtures/example_peril_orta_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "settings": { 4 | "plugins": ["danger-plugin-yarn", "danger-plugin-spellcheck"] 5 | }, 6 | "rules": { 7 | "pull_request": "orta/peril@pr.ts", 8 | "issue": "orta/peril@issue.ts" 9 | }, 10 | "repos": { 11 | "orta/ORStackView": { 12 | "issue.created": "orta/peril@lock_issues.ts" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/source/db/_tests/_json.test.ts: -------------------------------------------------------------------------------- 1 | const legitSettings = `{ 2 | "id": 1, 3 | "settings": { 4 | }, 5 | "rules": { 6 | "pull_request": "orta/peril@pr.ts", 7 | "issue": "orta/peril@issue.ts" 8 | }, 9 | "repos" : { 10 | "orta/ORStackView": { 11 | "issue.created": "orta/peril@lock_issues.ts" 12 | } 13 | } 14 | }` 15 | 16 | const mockGHContents = jest.fn() 17 | jest.mock("../../github/lib/github_helpers", () => ({ 18 | getGitHubFileContentsWithoutToken: mockGHContents, 19 | })) 20 | 21 | import { DatabaseAdaptor } from "../index" 22 | import { jsonDatabase } from "../json" 23 | 24 | describe("makes the right calls to GitHub", () => { 25 | let db: DatabaseAdaptor = null as any 26 | 27 | const setup = async () => { 28 | mockGHContents.mockImplementationOnce(() => Promise.resolve(legitSettings)) 29 | 30 | db = jsonDatabase("orta/peril@settings.json") 31 | return db.setup() 32 | } 33 | 34 | it("with a legit stubbed JSON file", async () => { 35 | await setup() 36 | 37 | const org = await db.getInstallation(1) 38 | expect(org).toMatchSnapshot() 39 | }) 40 | }) 41 | 42 | it("Raises with a bad URL", async () => { 43 | mockGHContents.mockImplementationOnce(() => Promise.resolve("")) 44 | expect.assertions(1) 45 | 46 | try { 47 | const db = jsonDatabase("orta/other@settings.json") 48 | await db.setup() 49 | } catch (error) { 50 | expect(error).toMatchSnapshot() 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /api/source/db/_tests/_mongo.test.ts: -------------------------------------------------------------------------------- 1 | import { convertDBRepresentationToModel, mongoDatabase, prepareToSave } from "../mongo" 2 | 3 | it("converts $ and . in user input to something mongo safe", () => { 4 | const before = { 5 | rules: { 6 | "thing.thing$": "ok", 7 | }, 8 | } 9 | const after = { 10 | rules: { 11 | "thing___thing^^^": "ok", 12 | }, 13 | } 14 | 15 | // to mongo 16 | expect(prepareToSave(before)).toMatchObject(after) 17 | }) 18 | 19 | it("converts $ and . from to user input", () => { 20 | const before = { 21 | rules: { 22 | "thing.thing$": "ok", 23 | }, 24 | } 25 | const after = { 26 | rules: { 27 | "thing___thing^^^": "ok", 28 | }, 29 | } 30 | 31 | // from mongo 32 | expect(convertDBRepresentationToModel(after as any)).toMatchObject(before) 33 | }) 34 | 35 | it("handles missing data", () => { 36 | const before = { 37 | rules: { 38 | hello: "ok", 39 | }, 40 | } 41 | const after = { 42 | repos: {}, 43 | rules: { 44 | hello: "ok", 45 | }, 46 | scheduler: {}, 47 | settings: {}, 48 | tasks: {}, 49 | envVars: {}, 50 | } 51 | 52 | // from mongo 53 | expect(convertDBRepresentationToModel(before as any)).toEqual(after) 54 | }) 55 | 56 | describe(mongoDatabase.updateInstallation, () => { 57 | it("doesn't let users overwrite internal fields", async () => { 58 | // TODO: Add this 59 | // updateInstallation is pretty long 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /api/source/db/getDB.ts: -------------------------------------------------------------------------------- 1 | import { jsonDatabase } from "./json" 2 | import { mongoDatabase } from "./mongo" 3 | 4 | import { DatabaseAdaptor } from "." 5 | import { RuntimeEnvironment } from "./runtimeEnv" 6 | 7 | const isJest = typeof jest !== "undefined" 8 | 9 | const hasJSONDef = !!process.env.DATABASE_JSON_FILE 10 | const hasPerilAPIURL = !!process.env.PUBLIC_API_ROOT_URL 11 | 12 | const hasLambdaEnv = !!process.env._HANDLER 13 | 14 | /** There are three runtime environments for Peril, this says which one it is */ 15 | export const runtimeEnvironment = hasJSONDef 16 | ? RuntimeEnvironment.Standalone 17 | : hasPerilAPIURL 18 | ? RuntimeEnvironment.Peril 19 | : hasLambdaEnv 20 | ? RuntimeEnvironment.Runner 21 | : RuntimeEnvironment.Unknown 22 | 23 | const getDatabaseForEnv = (env: any): DatabaseAdaptor | null => { 24 | if (env.DATABASE_JSON_FILE || isJest) { 25 | const json = jsonDatabase(env.DATABASE_JSON_FILE) 26 | json.setup() 27 | return json 28 | } 29 | 30 | if (env.MONGODB_URI) { 31 | if (!isJest) { 32 | mongoDatabase.setup() 33 | } 34 | return mongoDatabase 35 | } 36 | 37 | return null 38 | } 39 | 40 | let db: DatabaseAdaptor | null = null 41 | /** Gets the Current DB for this runtime environment */ 42 | export const getDB = () => { 43 | if (!db) { 44 | db = getDatabaseForEnv(process.env) 45 | } 46 | 47 | if (!db) { 48 | throw new Error("No default DB was set up") 49 | } 50 | 51 | return db 52 | } 53 | -------------------------------------------------------------------------------- /api/source/db/mongo/installationAnalytics.ts: -------------------------------------------------------------------------------- 1 | // // WIP 2 | 3 | // import { Document, model, Schema } from "mongoose" 4 | 5 | // interface InstallationAnalytics extends Document { 6 | // iID: number 7 | // numberOfRuns: number 8 | // totalTime: number 9 | // } 10 | 11 | // const InstallationAnalyticsModel = model( 12 | // "InstallationAnalytics", 13 | // new Schema({ 14 | // iID: Number, 15 | // numberOfRuns: Number, 16 | // totalTime: Number, 17 | // }) 18 | // ) 19 | 20 | // export const installationAnalytics = () => ({ 21 | // updateAnalyticsForInstallation: (installationID: number, runID: string) => 22 | // InstallationAnalyticsModel.findOne({ iID: installationID, runID }), 23 | // }) 24 | -------------------------------------------------------------------------------- /api/source/db/runtimeEnv.ts: -------------------------------------------------------------------------------- 1 | export enum RuntimeEnvironment { 2 | /** On Heroku */ 3 | Standalone, 4 | /** Peril prod/staging */ 5 | Peril, 6 | /** Inside the peril runner in a docker */ 7 | Runner, 8 | /** dunno */ 9 | Unknown, 10 | } 11 | -------------------------------------------------------------------------------- /api/source/db/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A GitHub user account 3 | */ 4 | export interface GitHubUser { 5 | /** 6 | * Generic UUID 7 | * @type {number} 8 | */ 9 | id: number 10 | /** 11 | * The handle for the user/org 12 | * @type {string} 13 | */ 14 | login: string 15 | /** 16 | * Whether the user is an org, or a user 17 | * @type {string} 18 | */ 19 | type: "User" | "Organization" 20 | } 21 | 22 | /** 23 | * A GitHub Repo 24 | */ 25 | export interface GitHubRepo { 26 | /** 27 | * Generic UUID 28 | * @type {number} 29 | */ 30 | id: number 31 | 32 | /** 33 | * The name of the repo, e.g. "Danger-JS" 34 | * @type {string} 35 | */ 36 | name: string 37 | 38 | /** 39 | * The full name of the owner + repo, e.g. "Danger/Danger-JS" 40 | * @type {string} 41 | */ 42 | full_name: string 43 | 44 | /** 45 | * The owner of the repo 46 | * @type {GitHubUser} 47 | */ 48 | owner: GitHubUser 49 | 50 | /** 51 | * Is the repo publicly accessible? 52 | * @type {bool} 53 | */ 54 | private: boolean 55 | 56 | /** 57 | * The textual description of the repo 58 | * @type {string} 59 | */ 60 | description: string 61 | 62 | /** 63 | * Is the repo a fork? 64 | * @type {bool} 65 | */ 66 | fork: false 67 | } 68 | -------------------------------------------------------------------------------- /api/source/github/_tests/fixtures/github_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 | -------------------------------------------------------------------------------- /api/source/github/_tests/fixtures/github_static_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": "VGhlIEFsbC1EZWZlY3RvciBpcyBhIHB1cnBvcnRlZCBnbGl0Y2ggaW4gdGhlIERpbGVtbWEgUHJpc29uIHRoYXQgYXBwZWFycyB0byBwcmlzb25lcnMgYXMgdGhlbXNlbHZlcy4gVGhpcyBnb2dvbCBhbHdheXMgZGVmZWN0cywgaGVuY2UgdGhlIG5hbWUu" 3 | } 4 | -------------------------------------------------------------------------------- /api/source/github/_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 | -------------------------------------------------------------------------------- /api/source/github/events/_tests/_actionForWebhook.test.ts: -------------------------------------------------------------------------------- 1 | import { actionForWebhook } from "../utils/actions" 2 | 3 | it("gets the action from a JSON payload", () => { 4 | expect(actionForWebhook({ action: "runs" })).toBe("runs") 5 | }) 6 | 7 | it("gets the state from a JSON payload", () => { 8 | expect(actionForWebhook({ state: "runs" })).toBe("runs") 9 | }) 10 | 11 | it("prioritises action from a JSON payload", () => { 12 | expect(actionForWebhook({ action: "pause", state: "runs" })).toBe("pause") 13 | }) 14 | 15 | it("gives null otherwise", () => { 16 | expect(actionForWebhook({ deploy: "now" })).toBe(null) 17 | }) 18 | -------------------------------------------------------------------------------- /api/source/github/events/_tests/_github_runner-setup.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock("../../../db/getDB", () => ({ 2 | getDB: () => ({ 3 | getInstallation: () => Promise.resolve({ repos: {} }), 4 | }), 5 | })) 6 | 7 | import { readFileSync } from "fs" 8 | import { resolve } from "path" 9 | import { setupForRequest } from "../github_runner" 10 | 11 | /** Returns JSON from the fixtured dir */ 12 | const requestWithFixturedJSON = (name: string): any => { 13 | const path = resolve(__dirname, "fixtures", `${name}.json`) 14 | return { 15 | body: JSON.parse(readFileSync(path, "utf8")), 16 | headers: { "X-GitHub-Delivery": "12345" }, 17 | } 18 | } 19 | 20 | describe("makes the right settings for", () => { 21 | it("a pull_request_opened event", async () => { 22 | const pr = requestWithFixturedJSON("pull_request_opened") 23 | const settings = await setupForRequest(pr, {}) 24 | 25 | expect(settings).toEqual({ 26 | commentableID: 2, 27 | eventID: "12345", 28 | hasRelatedCommentable: true, 29 | installationID: 4766, 30 | installationSettings: {}, 31 | isRepoEvent: true, 32 | isTriggeredByUser: true, 33 | repoName: "danger/peril", 34 | repoSpecificRules: {}, 35 | triggeredByUsername: "orta", 36 | }) 37 | }) 38 | 39 | it("an installation event", async () => { 40 | const pr = requestWithFixturedJSON("installation") 41 | const settings = await setupForRequest(pr, {}) 42 | 43 | expect(settings).toEqual({ 44 | commentableID: null, 45 | eventID: "12345", 46 | hasRelatedCommentable: false, 47 | installationID: 4766, 48 | installationSettings: {}, 49 | isRepoEvent: false, 50 | isTriggeredByUser: true, 51 | repoName: false, 52 | repoSpecificRules: {}, 53 | triggeredByUsername: "orta", 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /api/source/github/events/_tests/_github_runner-validations.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs" 2 | import { resolve } from "path" 3 | import { generateInstallation } from "../../../testing/installationFactory" 4 | 5 | const apiFixtures = resolve(__dirname, "fixtures") 6 | const fixture = (file: string) => JSON.parse(readFileSync(resolve(apiFixtures, file), "utf8")) 7 | const body = fixture("pull_request_opened.json") 8 | 9 | const mockInstallationSettings = generateInstallation({ 10 | iID: 123, 11 | settings: { 12 | env_vars: [], 13 | ignored_repos: [body.pull_request.head.repo.full_name], 14 | modules: [], 15 | }, 16 | }) 17 | 18 | jest.doMock("../../../db/getDB", () => ({ 19 | getDB: () => ({ 20 | getInstallation: () => Promise.resolve(mockInstallationSettings), 21 | }), 22 | })) 23 | 24 | import { githubDangerRunner } from "../github_runner" 25 | 26 | it("Does not run a dangerfile in an ignored repo", async () => { 27 | const request = { body, headers: { "X-GitHub-Delivery": "12345" } } as any 28 | 29 | const send = { send: jest.fn() } 30 | const response = { status: jest.fn(() => send) } as any 31 | 32 | await githubDangerRunner("pull_request_opened", request, response, () => "") 33 | 34 | expect(response.status).toHaveBeenCalledWith(204) 35 | expect(send.send).toHaveBeenCalledWith({ error: "The installation has no settings path", iID: 4766 }) 36 | }) 37 | -------------------------------------------------------------------------------- /api/source/github/events/_tests/fixtures/access_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "v1.3a82912317a7d47caf83656f43094273ef21850", 3 | "expires_at": "2016-12-18T20:01:20Z", 4 | "on_behalf_of": null 5 | } 6 | -------------------------------------------------------------------------------- /api/source/github/events/_tests/fixtures/ping.json: -------------------------------------------------------------------------------- 1 | { 2 | "zen": "Design for failure.", 3 | "hook_id": 11188596, 4 | "hook": { 5 | "type": "Integration", 6 | "id": 11188596, 7 | "name": "web", 8 | "active": true, 9 | "events": [ 10 | "commit_comment", 11 | "issues", 12 | "issue_comment", 13 | "pull_request", 14 | "pull_request_review", 15 | "pull_request_review_comment" 16 | ], 17 | "config": { 18 | "content_type": "json", 19 | "insecure_ssl": "0", 20 | "url": "http://danger.systems" 21 | }, 22 | "updated_at": "2016-12-17T01:05:27Z", 23 | "created_at": "2016-12-17T01:05:27Z", 24 | "integration_id": 865 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/source/github/events/createPRDSL.ts: -------------------------------------------------------------------------------- 1 | import { jsonDSLGenerator } from "danger/distribution/runner/dslGenerator" 2 | import { jsonToDSL } from "danger/distribution/runner/jsonToDSL" 3 | import { getPerilPlatformForDSL } from "../../danger/peril_platform" 4 | 5 | import { GitHub } from "danger/distribution/platforms/GitHub" 6 | import { GitHubAPI } from "danger/distribution/platforms/github/GitHubAPI" 7 | import { RunType } from "../../danger/danger_run" 8 | import { source } from "../../danger/peril_ci_source" 9 | 10 | /** 11 | * Generates a full DSL for a PR 12 | * 13 | * @param githubAPI the Danger GithubAPI instance 14 | */ 15 | export const createPRDSL = async (githubAPI: GitHubAPI) => { 16 | const jsonDSL = await createPRJSONDSL(githubAPI) 17 | // Danger JS expects the Github access token to be set on 18 | // settings.github.accessToken, so put it there if we have one 19 | if (githubAPI.token !== undefined) { 20 | jsonDSL.settings.github.accessToken = githubAPI.token 21 | } 22 | return await jsonToDSL(jsonDSL, source) 23 | } 24 | 25 | /** 26 | * Generates a full DSL for a PR 27 | * 28 | * @param githubAPI the Danger GithubAPI instance 29 | */ 30 | export const createPRJSONDSL = async (githubAPI: GitHubAPI) => { 31 | const gh = GitHub(githubAPI) 32 | const platform = getPerilPlatformForDSL(RunType.pr, gh, {}) 33 | // These are what Danger JS uses to pass info to sub-commands 34 | // peril scopes all of its settings elsewhere, so a blank is fine 35 | const cliArgs = {} as any 36 | return await jsonDSLGenerator(platform, source, cliArgs) 37 | } 38 | -------------------------------------------------------------------------------- /api/source/github/events/create_installation.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | 3 | import { Installation } from "../events/types/installation.types" 4 | 5 | import { getDB } from "../../db/getDB" 6 | import { generateInstallation } from "../../testing/installationFactory" 7 | 8 | export async function createInstallation(installationJSON: Installation, _: express.Request, res: express.Response) { 9 | const installation = generateInstallation({ 10 | iID: installationJSON.id, 11 | login: installationJSON.account.login, 12 | avatarURL: installationJSON.account.avatar_url, 13 | }) 14 | 15 | const db = getDB() 16 | const existingInstallation = await db.getInstallation(installation.iID) 17 | if (existingInstallation) { 18 | res.status(204) 19 | res.send("Did not create new installation, it already existed.") 20 | } else { 21 | await db.saveInstallation(installation) 22 | res.status(200) 23 | res.send("Creating new installation.") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /api/source/github/events/deleteInstallation.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | import { deleteLambdaFunctionNamed } from "../../api/aws/lambda" 3 | import { getDB } from "../../db/getDB" 4 | import { Installation } from "./types/installation.types" 5 | 6 | export async function deleteInstallation(installationJSON: Installation, _: express.Request, res: express.Response) { 7 | const iID = installationJSON.id 8 | 9 | const db = getDB() 10 | const installation = await db.getInstallation(iID) 11 | 12 | if (!installation) { 13 | res.status(404) 14 | res.send("Could not find installation for deletion.") 15 | return 16 | } 17 | 18 | // Remove the lambda so AWS doesn't get filled up with every exploration 19 | if (installation.lambdaName) { 20 | await deleteLambdaFunctionNamed(installation.lambdaName) 21 | } 22 | 23 | // Kill it from the DB 24 | db.deleteInstallation(iID) 25 | 26 | res.status(200) 27 | res.send("Deleted installation.") 28 | } 29 | -------------------------------------------------------------------------------- /api/source/github/events/handlers/_tests/__snapshots__/_pr-create-fixture.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`passes the right args to the hyper functions when it's a PR 1`] = ` 4 | Object { 5 | "dslType": "pr", 6 | "installation": Object { 7 | "iID": 4766, 8 | "settings": Object {}, 9 | }, 10 | "paths": Array [ 11 | "danger/peril-settings@testing/logger.ts", 12 | ], 13 | "payload": Object { 14 | "dsl": Object { 15 | "settings": Object { 16 | "cliArgs": Object {}, 17 | "github": Object { 18 | "accessToken": "12345", 19 | "additionalHeaders": Object { 20 | "Accept": "application/vnd.github.machine-man-preview+json", 21 | }, 22 | "baseURL": undefined, 23 | }, 24 | }, 25 | }, 26 | "webhook": null, 27 | }, 28 | "perilSettings": Object { 29 | "envVars": Object { 30 | "hello": "world", 31 | }, 32 | "event": "eventName", 33 | "perilAPIRoot": undefined, 34 | "perilJWT": "12345", 35 | "perilRunID": "[run-id]", 36 | }, 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /api/source/github/events/handlers/_tests/_events-sandbox.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock("../../../../db/getDB") 2 | import { MockDB } from "../../../../db/__mocks__/getDB" 3 | import { getDB } from "../../../../db/getDB" 4 | const mockDB = getDB() as MockDB 5 | 6 | jest.mock("../../../../api/github", () => ({ 7 | getTemporaryAccessTokenForInstallation: () => Promise.resolve("token"), 8 | })) 9 | 10 | jest.mock("../../../../github/lib/github_helpers", () => ({ 11 | getGitHubFileContents: jest.fn(), 12 | })) 13 | import { getGitHubFileContents } from "../../../lib/github_helpers" 14 | const mockGetGitHubFileContents: any = getGitHubFileContents 15 | 16 | import { readFileSync } from "fs" 17 | import { resolve } from "path" 18 | import { dangerRunForRules } from "../../../../danger/danger_run" 19 | import { triggerSandboxDangerRun } from "../../../../runner/triggerSandboxRun" 20 | import { setupForRequest } from "../../github_runner" 21 | import { runEventRun } from "../event" 22 | 23 | jest.mock("../../../../runner/triggerSandboxRun", () => ({ 24 | triggerSandboxDangerRun: jest.fn(), 25 | })) 26 | 27 | const apiFixtures = resolve(__dirname, "../../_tests/fixtures") 28 | const fixture = (file: string) => JSON.parse(readFileSync(resolve(apiFixtures, file), "utf8")) 29 | 30 | it("sets up the right call to trigger sandbox run", async () => { 31 | mockDB.getInstallation.mockReturnValueOnce({ iID: "123", repos: {} }) 32 | 33 | const body = fixture("issue_comment_created.json") 34 | const req = { body, headers: { "X-GitHub-Delivery": "123" } } as any 35 | const settings = await setupForRequest(req, {}) 36 | 37 | const dangerfileForRun = "warn(danger.github.api)" 38 | mockGetGitHubFileContents.mockImplementationOnce(() => Promise.resolve(dangerfileForRun)) 39 | 40 | const run = dangerRunForRules("issue_comment", "created", { issue_comment: "warn_with_api" }, body)[0] 41 | 42 | await runEventRun("mockEvent", [run], settings, "token", body) 43 | const mock = (triggerSandboxDangerRun as any).mock.calls[0] 44 | 45 | // Check and remove github.api for snapshot 46 | expect(mock[4].dsl.github.api).toBeDefined() 47 | delete mock[4].dsl.github.api 48 | 49 | expect(mock[1]).toMatchSnapshot("type") 50 | expect(mock[2]).toMatchSnapshot("installation") 51 | expect(mock[3]).toMatchSnapshot("paths") 52 | expect(mock[4]).toMatchSnapshot("payload") 53 | }) 54 | -------------------------------------------------------------------------------- /api/source/github/events/handlers/_tests/fixtures/PerilRunnerPRBootStrapExample.json: -------------------------------------------------------------------------------- 1 | { 2 | "installation": { 3 | "iID": 4766, 4 | "settings": {} 5 | }, 6 | "payload": { 7 | "dsl": { 8 | "settings": { 9 | "github": { 10 | "accessToken": "12345", 11 | "additionalHeaders": { 12 | "Accept": "application/vnd.github.machine-man-preview+json" 13 | } 14 | }, 15 | "cliArgs": {} 16 | } 17 | }, 18 | "webhook": null 19 | }, 20 | "dslType": "pr", 21 | "perilSettings": { 22 | "perilJWT": "12345", 23 | "envVars": { 24 | "hello": "world" 25 | }, 26 | "perilRunID": "[run-id]", 27 | "event": "eventName" 28 | }, 29 | "paths": ["danger/peril-settings@testing/logger.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /api/source/github/events/ping.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | 3 | export function ping(_: express.Request, res: express.Response) { 4 | res.status(200).send("pong") 5 | } 6 | -------------------------------------------------------------------------------- /api/source/github/events/types/access_token.types.ts: -------------------------------------------------------------------------------- 1 | export interface RootObject { 2 | token: string 3 | expires_at: string 4 | on_behalf_of?: any 5 | } 6 | -------------------------------------------------------------------------------- /api/source/github/events/types/installation.types.ts: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | login: string 3 | id: number 4 | avatar_url: string 5 | gravatar_id: string 6 | url: string 7 | html_url: string 8 | followers_url: string 9 | following_url: string 10 | gists_url: string 11 | starred_url: string 12 | subscriptions_url: string 13 | organizations_url: string 14 | repos_url: string 15 | events_url: string 16 | received_events_url: string 17 | type: string 18 | site_admin: boolean 19 | } 20 | 21 | export interface Installation { 22 | id: number 23 | account: Account 24 | access_tokens_url: string 25 | repositories_url: string 26 | html_url: string 27 | } 28 | 29 | export interface Sender { 30 | login: string 31 | id: number 32 | avatar_url: string 33 | gravatar_id: string 34 | url: string 35 | html_url: string 36 | followers_url: string 37 | following_url: string 38 | gists_url: string 39 | starred_url: string 40 | subscriptions_url: string 41 | organizations_url: string 42 | repos_url: string 43 | events_url: string 44 | received_events_url: string 45 | type: string 46 | site_admin: boolean 47 | } 48 | 49 | export interface RootObject { 50 | action: string 51 | installation: Installation 52 | sender: Sender 53 | } 54 | -------------------------------------------------------------------------------- /api/source/github/events/types/integration_installation_added.types.ts: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | login: string 3 | id: number 4 | avatar_url: string 5 | gravatar_id: string 6 | url: string 7 | html_url: string 8 | followers_url: string 9 | following_url: string 10 | gists_url: string 11 | starred_url: string 12 | subscriptions_url: string 13 | organizations_url: string 14 | repos_url: string 15 | events_url: string 16 | received_events_url: string 17 | type: string 18 | site_admin: boolean 19 | } 20 | 21 | export interface Installation { 22 | id: number 23 | account: Account 24 | access_tokens_url: string 25 | repositories_url: string 26 | html_url: string 27 | } 28 | 29 | export interface Repositories_added { 30 | id: number 31 | name: string 32 | full_name: string 33 | } 34 | 35 | export interface Sender { 36 | login: string 37 | id: number 38 | avatar_url: string 39 | gravatar_id: string 40 | url: string 41 | html_url: string 42 | followers_url: string 43 | following_url: string 44 | gists_url: string 45 | starred_url: string 46 | subscriptions_url: string 47 | organizations_url: string 48 | repos_url: string 49 | events_url: string 50 | received_events_url: string 51 | type: string 52 | site_admin: boolean 53 | } 54 | 55 | export interface RootObject { 56 | action: string 57 | installation: Installation 58 | repository_selection: string 59 | repositories_added: Repositories_added[] 60 | repositories_removed: any[] 61 | sender: Sender 62 | } 63 | -------------------------------------------------------------------------------- /api/source/github/events/types/ping.types.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | content_type: string 3 | insecure_ssl: string 4 | url: string 5 | } 6 | 7 | export interface Hook { 8 | type: string 9 | id: number 10 | name: string 11 | active: boolean 12 | events: string[] 13 | config: Config 14 | updated_at: string 15 | created_at: string 16 | integration_id: number 17 | } 18 | 19 | export interface RootObject { 20 | zen: string 21 | hook_id: number 22 | hook: Hook 23 | } 24 | -------------------------------------------------------------------------------- /api/source/github/events/utils/actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * There's value in not running Dangerfiles for every webhook 3 | * mainly that it's wasteful, so this gives us 4 | */ 5 | export const actionForWebhook = (webhook: any): string | null => { 6 | // PR/Issues etc 7 | if (webhook.action) { 8 | return webhook.action 9 | } 10 | 11 | // Are there more worth adding? 12 | 13 | // Fallback for Status 14 | // https://developer.github.com/v3/activity/events/types/#statusevent 15 | if (webhook.state) { 16 | return webhook.state 17 | } 18 | 19 | return null 20 | } 21 | -------------------------------------------------------------------------------- /api/source/github/events/utils/ignore_repos.ts: -------------------------------------------------------------------------------- 1 | import { GitHubInstallation } from "../../../db" 2 | 3 | export const repoIsIgnored = (name: string | null, installation: GitHubInstallation) => 4 | name && 5 | installation.settings && 6 | installation.settings.ignored_repos && 7 | installation.settings.ignored_repos.includes(name) 8 | -------------------------------------------------------------------------------- /api/source/github/events/utils/repoNameForWebhook.ts: -------------------------------------------------------------------------------- 1 | import { Payload, ValidatedPayload } from "../../../danger/danger_runner" 2 | 3 | /** 4 | * Try to get a repo string for the context of the payload 5 | */ 6 | export const repoNameForPayload = (payload: Payload): string | null => { 7 | // Issues/Comments/Statuses etc 8 | if (payload.webhook && payload.webhook.repository) { 9 | return payload.webhook.repository.full_name 10 | } 11 | 12 | // PRs 13 | if (payload.dsl && (payload.dsl as any).git) { 14 | const dslPayload = payload as ValidatedPayload 15 | return dslPayload.dsl.github!.pr.base.repo.full_name 16 | } 17 | 18 | // Are there more worth adding? 19 | return null 20 | } 21 | -------------------------------------------------------------------------------- /api/source/index.ts: -------------------------------------------------------------------------------- 1 | import * as cluster from "cluster" 2 | import * as os from "os" 3 | 4 | import logger from "./logger" 5 | import { peril } from "./peril" 6 | 7 | const WORKERS = process.env.NODE_ENV === "production" ? process.env.WEB_CONCURRENCY || os.cpus().length : 1 8 | const log = (message: string) => { 9 | if (WORKERS > 1) { 10 | logger.info(message) 11 | } 12 | } 13 | 14 | if (cluster.isMaster) { 15 | log(`[CLUSTER] Master cluster setting up ${WORKERS} workers...`) 16 | for (let i = 0; i < WORKERS; i++) { 17 | cluster.fork() // create a worker 18 | } 19 | 20 | cluster.on("online", worker => { 21 | log(`[CLUSTER] Worker ${worker.process.pid} is online`) 22 | }) 23 | 24 | cluster.on("exit", (worker, code, signal) => { 25 | log(`[CLUSTER] Worker ${worker.process.pid} died with code: ${code}, and signal: ${signal}`) 26 | log("[CLUSTER] Starting a new worker") 27 | // start a new worker when it crashes 28 | cluster.fork() 29 | }) 30 | } else { 31 | peril() 32 | } 33 | -------------------------------------------------------------------------------- /api/source/infrastructure/_tests/_installationSlackMessaging.test.ts: -------------------------------------------------------------------------------- 1 | import { replaceAllKeysInString } from "../installationSlackMessaging" 2 | 3 | describe("removes keys", () => { 4 | it("does string changes easy ones", () => { 5 | let output = replaceAllKeysInString({ MY_KEY: "secret" }, "thing with a secret word") 6 | expect(output).not.toContain("secret") 7 | 8 | output = replaceAllKeysInString({ MY_KEY: "secret" }, "thing with a secret word secret ok secreting") 9 | expect(output).not.toContain("secret") 10 | }) 11 | 12 | it("handles null objects", () => { 13 | const output = replaceAllKeysInString(undefined, "thing with a secret word") 14 | expect(output).toContain("secret") 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /api/source/listen.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | 3 | import { createServer } from "http" 4 | import * as Primus from "primus" 5 | import { setupPublicWebsocket } from "./api/api" 6 | import { PUBLIC_API_ROOT_URL } from "./globals" 7 | import logger from "./logger" 8 | import { tick } from "./peril" 9 | 10 | export let primus: any = null 11 | 12 | export const startApp = (app: express.Express, callback: any) => { 13 | // Skip primus setup 14 | if (!PUBLIC_API_ROOT_URL) { 15 | app.listen(app.get("port"), callback) 16 | logger.info("") 17 | return 18 | } 19 | 20 | const httpServer = createServer(app as any) 21 | 22 | primus = new Primus(httpServer, { transformer: "websockets", iknowclusterwillbreakconnections: true }) 23 | setupPublicWebsocket() 24 | 25 | // Call engine.listen instead of app.listen(port) 26 | app.listen(app.get("port"), () => { 27 | callback() 28 | 29 | logger.info(" - " + tick + " Primus Sockets") 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /api/source/logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from "winston" 2 | 3 | const logger = winston 4 | require("winston-papertrail").Papertrail // tslint:disable-line 5 | 6 | import { PAPERTRAIL_PORT, PAPERTRAIL_URL } from "./globals" 7 | 8 | // Optional, adds papertrail to the logging systems 9 | if (PAPERTRAIL_URL) { 10 | const transports = winston.transports as any 11 | winston.add( 12 | new transports.Papertrail({ 13 | host: PAPERTRAIL_URL, 14 | port: parseInt(PAPERTRAIL_PORT as string, 10), 15 | }) 16 | ) 17 | } else { 18 | // On AWS, or tests, or whatever 19 | logger.add( 20 | new winston.transports.Console({ 21 | format: winston.format.simple(), 22 | }) 23 | ) 24 | } 25 | 26 | // tslint:disable-next-line:no-default-export 27 | export default logger 28 | -------------------------------------------------------------------------------- /api/source/plugins/_tests/_installationLifeCycle.test.ts: -------------------------------------------------------------------------------- 1 | import { installationLifeCycle } from "../installationLifeCycle" 2 | 3 | jest.mock("../../db/getDB") 4 | import { createInstallation } from "../../github/events/create_installation" 5 | import { deleteInstallation } from "../../github/events/deleteInstallation" 6 | 7 | jest.mock("../../github/events/create_installation", () => ({ createInstallation: jest.fn() })) 8 | jest.mock("../../github/events/deleteInstallation", () => ({ deleteInstallation: jest.fn() })) 9 | 10 | const validRequest = { body: { installation: { id: 123 } } } as any 11 | 12 | describe("routing for GitHub", () => { 13 | it("creates an installation when an integration is created", async () => { 14 | const body = { action: "created", installation: { account: { login: "Orta" } } } 15 | await installationLifeCycle("installation", { ...validRequest, body }, {} as any, {} as any) 16 | expect(createInstallation).toBeCalled() 17 | }) 18 | 19 | it("deletes an installation when an integration is removed", () => { 20 | const body = { action: "deleted", installation: { id: 12345 } } 21 | installationLifeCycle("installation", { ...validRequest, body }, {} as any, {} as any) 22 | 23 | expect(deleteInstallation).toBeCalled() 24 | }) 25 | 26 | it("skips a non-installation event", async () => { 27 | const body = { 28 | action: "created", 29 | created: false, 30 | deleted: false, 31 | forced: false, 32 | } 33 | installationLifeCycle("push", { ...validRequest, body }, {} as any, {} as any) 34 | expect(createInstallation).toBeCalled() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /api/source/plugins/_tests/_validatesGithubWebhook.test.ts: -------------------------------------------------------------------------------- 1 | import { createMockResponse } from "../../routing/_tests/create-mock-response" 2 | import { validatesGithubWebhook } from "../validatesGithubWebhook" 3 | 4 | const validRequest = { isXHub: true, isXHubValid: () => true, body: { installation: { id: 123 } } } as any 5 | const noXhub = { isXHub: false } as any 6 | const badXHub = { isXHub: true, isXHubValid: () => false } as any 7 | 8 | describe("validating webhooks from GitHub", () => { 9 | it("fails when there is is no x-hub", () => { 10 | const res = createMockResponse() 11 | validatesGithubWebhook("pong", noXhub, res, {} as any) 12 | expect(res.status).toBeCalledWith(400) 13 | expect(res.send).toBeCalledWith( 14 | "Request did not include x-hub header - You need to set a secret in the GitHub App + PERIL_WEBHOOK_SECRET." 15 | ) 16 | }) 17 | 18 | it("fails when there is no x-hub", () => { 19 | const res = createMockResponse() 20 | 21 | validatesGithubWebhook("pong", badXHub, res, {} as any) 22 | expect(res.status).toBeCalledWith(401) 23 | 24 | expect(res.send).toBeCalledWith( 25 | "Request did not have a valid x-hub header. Perhaps PERIL_WEBHOOK_SECRET is not set up right?" 26 | ) 27 | }) 28 | 29 | it("passes when there is real creds", () => { 30 | const res = createMockResponse() 31 | const response = validatesGithubWebhook("pong", validRequest, res, {} as any) 32 | 33 | expect(response).toBeTruthy() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /api/source/plugins/installationLifeCycle.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | import { createInstallation } from "../github/events/create_installation" 3 | import { deleteInstallation } from "../github/events/deleteInstallation" 4 | import { RootObject as InstallationCreated } from "../github/events/types/installation.types" 5 | import logger from "../logger" 6 | 7 | export const installationLifeCycle = (event: string, req: express.Request, res: express.Response, ___: any) => { 8 | if (event === "installation") { 9 | const request = req.body as InstallationCreated 10 | const action = request.action 11 | const installation = request.installation 12 | 13 | // Create a db entry for any new installation 14 | if (action === "created") { 15 | logger.info("") 16 | logger.info(`## Creating new installation for ${request.installation.account.login}`) 17 | createInstallation(installation, req, res) 18 | } 19 | 20 | // Delete any integrations that have uninstalled Peril :wave: 21 | if (action === "deleted") { 22 | logger.info("") 23 | logger.info(`## Deleting installation ${installation.id}`) 24 | deleteInstallation(installation, req, res) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/source/plugins/recordWebhooks.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | import { getDB } from "../db/getDB" 3 | import { MongoGithubInstallationModel } from "../db/mongo" 4 | import { recordWebhookWithRequest } from "./utils/recordWebhookWithRequest" 5 | 6 | export const recordWebhook = async (_: string, req: express.Request, __: express.Response, ___: any) => { 7 | const installationID = req.body.installation.id as number 8 | const db = getDB() 9 | const installation = await db.getInstallation(installationID) 10 | 11 | // Only deal with mongo installations, not JSON ones 12 | if (!installation || !("recordWebhooksUntilTime" in installation)) { 13 | return 14 | } 15 | 16 | const mongoInstallation = installation as MongoGithubInstallationModel 17 | if (!mongoInstallation.recordWebhooksUntilTime) { 18 | return 19 | } 20 | 21 | // If the time is in the future, then record the webhooks 22 | const recordExpirationDateIsFuture = mongoInstallation.recordWebhooksUntilTime > new Date() 23 | if (recordExpirationDateIsFuture) { 24 | recordWebhookWithRequest(req) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/source/plugins/utils/sendWebhookThroughGitHubRunner.ts: -------------------------------------------------------------------------------- 1 | import { createRequest, createResponse } from "node-mocks-http" 2 | import { githubDangerRunner } from "../../github/events/github_runner" 3 | import { RecordedWebhook } from "./recordWebhookWithRequest" 4 | 5 | /** Takes a recorded webhook and sends it through the Peril runner */ 6 | export const sendWebhookThroughGitHubRunner = async (webhook: RecordedWebhook) => { 7 | const request = createRequest({ 8 | headers: { 9 | "X-GitHub-Delivery": webhook.eventID, 10 | }, 11 | body: webhook.json, 12 | }) 13 | const response = createResponse() 14 | await githubDangerRunner(webhook.event, request, response, () => null) 15 | } 16 | -------------------------------------------------------------------------------- /api/source/plugins/validatesGithubWebhook.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express" 2 | 3 | export const validatesGithubWebhook = async (_: string, req: express.Request, res: express.Response, __: any) => { 4 | const xhubReq = req as any 5 | if (!xhubReq.isXHub) { 6 | return res 7 | .status(400) 8 | .send("Request did not include x-hub header - You need to set a secret in the GitHub App + PERIL_WEBHOOK_SECRET.") 9 | } 10 | 11 | if (!xhubReq.isXHubValid()) { 12 | return res 13 | .status(401) 14 | .send("Request did not have a valid x-hub header. Perhaps PERIL_WEBHOOK_SECRET is not set up right?") 15 | } 16 | 17 | return false 18 | } 19 | -------------------------------------------------------------------------------- /api/source/routing/_tests/_router.test.ts: -------------------------------------------------------------------------------- 1 | const mockGithubRunner = jest.fn() 2 | jest.mock("../../github/events/github_runner", () => ({ 3 | githubDangerRunner: mockGithubRunner, 4 | })) 5 | 6 | jest.mock("../../plugins/installationSettingsUpdater", () => ({ 7 | installationSettingsUpdater: jest.fn(), 8 | })) 9 | 10 | jest.mock("../../plugins/recordWebhooks", () => ({ 11 | recordWebhook: jest.fn(), 12 | })) 13 | 14 | import { installationSettingsUpdater } from "../../plugins/installationSettingsUpdater" 15 | import { recordWebhook } from "../../plugins/recordWebhooks" 16 | import { githubRouter } from "../router" 17 | 18 | const validRequestWithEvent = (header: string) => 19 | ({ isXHub: true, isXHubValid: () => true, body: { installation: { id: 123 } }, header: () => header } as any) 20 | 21 | it("calls the GitHub runner for any events", () => { 22 | const req = validRequestWithEvent("issue") 23 | githubRouter(req, {} as any, {} as any) 24 | expect(mockGithubRunner).toBeCalledWith("issue", req, {}, {}) 25 | }) 26 | 27 | it("calls plugins", () => { 28 | const req = validRequestWithEvent("push") 29 | githubRouter(req, {} as any, {} as any) 30 | 31 | expect(mockGithubRunner).toBeCalledWith("push", req, {}, {}) 32 | expect(installationSettingsUpdater).toBeCalled() 33 | expect(recordWebhook).toBeCalled() 34 | }) 35 | -------------------------------------------------------------------------------- /api/source/routing/_tests/create-mock-response.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express" 2 | 3 | export const createMockResponse = () => { 4 | const res = jest.fn() as jest.Mock & Response 5 | res.status = jest.fn(() => res) 6 | res.send = jest.fn() 7 | return res 8 | } 9 | -------------------------------------------------------------------------------- /api/source/routing/router.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express" 2 | import winston from "../logger" 3 | 4 | import { githubDangerRunner } from "../github/events/github_runner" 5 | 6 | import { installationLifeCycle } from "../plugins/installationLifeCycle" 7 | import { installationSettingsUpdater } from "../plugins/installationSettingsUpdater" 8 | import { recordWebhook } from "../plugins/recordWebhooks" 9 | import { validatesGithubWebhook } from "../plugins/validatesGithubWebhook" 10 | 11 | export const githubRouter = (req: Request, res: Response, next: NextFunction) => { 12 | const event = req.header("X-GitHub-Event") 13 | if (!event) { 14 | return 15 | } 16 | winston.info(`[router] -- Received ${event}:`) 17 | 18 | // Creating / Removing installations from the DB 19 | installationLifeCycle(event, req, res, next) 20 | 21 | // There are some webhook events that shouldn't be passed through to users/plugins 22 | if (webhookSkipListForPeril.includes(event)) { 23 | return 24 | } 25 | 26 | githubEventPluginHandler(event, req, res, next) 27 | 28 | // The Peril/Danger runner 29 | githubDangerRunner(event, req, res, next) 30 | } 31 | 32 | // TODO: 33 | // Type the plugins 34 | // Make a context obj with installation and others 35 | // Remove the next fn 36 | 37 | export const githubEventPluginHandler = (event: string, req: Request, res: Response, next: NextFunction) => { 38 | // Use XHub to verify the request was sent from GH 39 | if (!validatesGithubWebhook(event, req, res, next)) { 40 | return 41 | } 42 | 43 | // Allow a dev mode 44 | recordWebhook(event, req, res, next) 45 | 46 | // Updating an install when the JSON changes 47 | installationSettingsUpdater(event, req, res, next) 48 | } 49 | 50 | // Installation addition/removal isn't too useful, and knowing when the repos 51 | // have changed isn't of much value to peril considering how the JSON file is set up 52 | // integration_installation is deprecated, but let's keep ignoring it. 53 | export const webhookSkipListForPeril = ["integration_installation", "installation"] 54 | -------------------------------------------------------------------------------- /api/source/runner/_tests/_customGitHubRequire.test.ts: -------------------------------------------------------------------------------- 1 | import { customGitHubResolveRequest, perilPrefix, shouldUseGitHubOverride } from "../customGitHubRequire" 2 | 3 | jest.mock("../../github/lib/github_helpers") 4 | import { getGitHubFileContentsFromLocation } from "../../github/lib/github_helpers" 5 | const mockGH = getGitHubFileContentsFromLocation as jest.Mock 6 | 7 | jest.mock("danger/distribution/runner/runners/utils/transpiler") 8 | import transpiler from "danger/distribution/runner/runners/utils/transpiler" 9 | const mockTranspiler = transpiler as jest.Mock 10 | 11 | describe("shouldUseGitHubOverride", () => { 12 | it("ignores module imports ", () => { 13 | const module = "peril" 14 | const parent: any = { filename: "index.js" } 15 | expect(shouldUseGitHubOverride(module, parent)).toBeFalsy() 16 | }) 17 | 18 | it("ignores relative imports in other modules ", () => { 19 | const module = "./peril" 20 | const parent: any = { filename: "node_modules/danger/index.js" } 21 | expect(shouldUseGitHubOverride(module, parent)).toBeFalsy() 22 | }) 23 | 24 | it("accepts relative imports in modules with a parent that has the right prefix ", () => { 25 | const module = "./peril" 26 | const parent: any = { filename: perilPrefix + "./my-import" } 27 | expect(shouldUseGitHubOverride(module, parent)).toBeTruthy() 28 | }) 29 | }) 30 | 31 | describe("customGitHubResolveRequest", () => { 32 | it("makes the right GH request for the relative file", async () => { 33 | const module = "./myapp/peril-resolver" 34 | const parent: any = { filename: perilPrefix + "orta/peril-settings@my-import" } 35 | const token = "1231231231" 36 | const resolver = customGitHubResolveRequest(token) 37 | 38 | mockGH.mockResolvedValueOnce("NOOP") // the transpiler handles this 39 | mockTranspiler.mockReturnValueOnce("module.exports = { hello: 'world' }") 40 | 41 | const result = await resolver(module, parent) 42 | 43 | // It should make the right API call to 44 | expect(mockGH).toBeCalledWith( 45 | token, 46 | { 47 | branch: "master", 48 | dangerfilePath: "myapp/peril-resolver.js", 49 | referenceString: "orta/peril-settings@/myapp/peril-resolver.js", 50 | repoSlug: "orta/peril-settings", 51 | }, 52 | "orta/peril-settings" 53 | ) 54 | 55 | // It should return the transpiled module 56 | expect(result).toEqual({ hello: "world" }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /api/source/runner/fixtures/dangerfile/hello_world.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-console 2 | 3 | declare const danger: any 4 | 5 | // tslint:disable-next-line:no-console 6 | console.log("Hello world") 7 | // tslint:disable-next-line:no-console 8 | console.log(danger.github.hello) 9 | -------------------------------------------------------------------------------- /api/source/runner/fixtures/hello-world.json: -------------------------------------------------------------------------------- 1 | { 2 | "installation": { 3 | "id": 74679, 4 | "settings": { 5 | "env_vars": [], 6 | "ignored_repos": [], 7 | "modules": ["danger-plugin-spellcheck", "danger-plugin-yarn", "@slack/client", "semver-sort"] 8 | } 9 | }, 10 | "payload": { 11 | "dsl": { 12 | "github": { 13 | "hello": "Hi there, from the DSL" 14 | }, 15 | "settings": { 16 | "github": { 17 | "accessToken": "12345", 18 | "baseURL": null, 19 | "additionalHeaders": { 20 | "Accept": "application/vnd.github.machine-man-preview+json" 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | "dslType": "run", 27 | "token": "12345", 28 | "peril": { "env": {} }, 29 | "path": "danger/peril@source/runner/fixtures/dangerfile/hello_world.ts" 30 | } 31 | -------------------------------------------------------------------------------- /api/source/runner/index.ts: -------------------------------------------------------------------------------- 1 | import * as getSTDIN from "get-stdin" 2 | import logger from "../logger" 3 | import { run } from "./run" 4 | 5 | /// This is only used by "runFromSameHost.ts" 6 | 7 | try { 8 | // Provide a timeout mechanism for the STDIN from the hyper func host 9 | let foundDSL = false 10 | getSTDIN().then(stdin => { 11 | foundDSL = true 12 | run(stdin) 13 | }) 14 | 15 | process.on("unhandledRejection", (error: Error) => { 16 | logger.error("unhandledRejection:", error.message, error.stack) 17 | process.exitCode = 1 18 | }) 19 | 20 | // Add a timeout so that CI doesn't run forever if something has broken. 21 | setTimeout(() => { 22 | if (!foundDSL) { 23 | logger.error("Timeout: Failed to get the Peril DSL after 5 seconds") 24 | process.exitCode = 1 25 | process.exit(1) 26 | } 27 | }, 5000) 28 | } catch (error) { 29 | const err = error as Error 30 | logger.error(`Error ${err.name} in the runner: ${err.message}\n${err.stack}`) 31 | throw error 32 | } 33 | -------------------------------------------------------------------------------- /api/source/runner/runFromExternalHost.ts: -------------------------------------------------------------------------------- 1 | import { GitHubInstallation } from "../db" 2 | import { sendSlackMessageToInstallationID } from "../infrastructure/installationSlackMessaging" 3 | import logger from "../logger" 4 | 5 | import { invokeLambda } from "../api/aws/lambda" 6 | import { PerilRunnerBootstrapJSON } from "./triggerSandboxRun" 7 | 8 | export const runExternally = async ( 9 | stdOUT: PerilRunnerBootstrapJSON, 10 | eventName: string, 11 | installation: GitHubInstallation 12 | ) => { 13 | try { 14 | const call = await invokeLambda(installation.lambdaName, JSON.stringify(stdOUT)) 15 | 16 | if (call.Status === 202) { 17 | logger.info(` Logs`) 18 | logger.info(` Running job: ${call.$response.requestId}`) 19 | 20 | // prettier-ignore 21 | const cloudWatchURL = `https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#logStream:group=/aws/lambda/${installation.lambdaName};` 22 | logger.info(` ${cloudWatchURL}`) 23 | } else { 24 | const errorMessage = `# Lambda call failed for ${eventName} - ${call.Status}` 25 | logger.error(errorMessage) 26 | sendSlackMessageToInstallationID(errorMessage, installation.iID) 27 | } 28 | } catch (error) { 29 | const errorMessage = `# Lambda call failed for ${eventName}: \n\n${error}` 30 | logger.error(errorMessage) 31 | sendSlackMessageToInstallationID(errorMessage, installation.iID) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /api/source/runner/runFromSameHost.ts: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync } from "child_process" 2 | import debug from "debug" 3 | import { InstallationToRun } from "../danger/danger_runner" 4 | import { sendSlackMessageToInstallationID } from "../infrastructure/installationSlackMessaging" 5 | import { PerilRunnerBootstrapJSON } from "./triggerSandboxRun" 6 | 7 | const d = debug("runFromSameHost") 8 | 9 | export const runFromSameHost = async ( 10 | stdOUT: PerilRunnerBootstrapJSON, 11 | // tslint:disable-next-line:variable-name 12 | _eventName: string, 13 | installation: InstallationToRun 14 | ) => { 15 | const which = spawnSync("/usr/bin/which", ["node"]) 16 | const nodePath = which.stdout.toString().trim() 17 | 18 | const path = "out/runner/index.js" 19 | const child = spawn(nodePath, [path], { env: { ...process.env, ...stdOUT.perilSettings.envVars } }) 20 | 21 | // Pipe in the STDOUT 22 | child.stdin.write(JSON.stringify(stdOUT)) 23 | child.stdin.end() 24 | 25 | let allLogs = "" 26 | child.stdout.on("data", async data => { 27 | const stdout = data.toString() 28 | allLogs += stdout 29 | d(stdout) 30 | }) 31 | 32 | child.stderr.on("data", data => { 33 | const stderr = data.toString() 34 | allLogs += stderr 35 | d(stderr) 36 | }) 37 | 38 | child.on("close", async code => { 39 | d(`child process exited with code ${code}`) 40 | if (code) { 41 | sendSlackMessageToInstallationID(allLogs, installation.iID) 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /api/source/runner/sandbox/jwt.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from "jsonwebtoken" 2 | 3 | import { isString } from "util" 4 | import { PRIVATE_GITHUB_SIGNING_KEY, PUBLIC_GITHUB_SIGNING_KEY } from "../../globals" 5 | 6 | type JWT = string 7 | 8 | type PerilSandboxAuthJWT = JWT 9 | 10 | // The Decoded JWT data 11 | export interface PerilSandboxAuth { 12 | exp?: number 13 | iat: number 14 | iss: string[] 15 | data: { 16 | actions: string[] 17 | } 18 | } 19 | 20 | /** 21 | * A JWT which lasts 2 minutes which can be used to make authenticated 22 | * requests to Peril for a specific installation. 23 | */ 24 | export const createPerilSandboxAPIJWT = (installationID: number, actions: string[]): PerilSandboxAuthJWT => { 25 | const now = Math.round(new Date().getTime() / 1000) 26 | const keyContent = PRIVATE_GITHUB_SIGNING_KEY 27 | const payload: PerilSandboxAuth = { 28 | iat: now, 29 | iss: [String(installationID)], 30 | data: { 31 | actions, 32 | }, 33 | } 34 | 35 | return jwt.sign(payload, keyContent, { algorithm: "RS256", expiresIn: "2 min" }) 36 | } 37 | 38 | /** 39 | * Decode and verifies a JWT generated by createPerilSandboxAPIJWT above 40 | * @param token the JWT 41 | */ 42 | export const getDetailsFromPerilSandboxAPIJWT = (token: PerilSandboxAuthJWT) => 43 | new Promise((res, rej) => { 44 | const options = { algorithms: ["RS256"] } 45 | jwt.verify(token, PUBLIC_GITHUB_SIGNING_KEY, options, (err, decoded) => { 46 | if (err) { 47 | rej(err) 48 | } else { 49 | if (isString(decoded)) { 50 | res(JSON.parse(decoded as string)) 51 | } else { 52 | res(decoded as any) 53 | } 54 | } 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /api/source/scripts/json-types.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as glob from "glob" 3 | import * as tsToJSON from "json2ts" 4 | import logger from "../logger" 5 | 6 | // Globs through our event fixtures and converts them into 7 | // TypeScript files 8 | 9 | glob("source/github/events/__tests__/fixtures/*.json", (error, files: string[]) => { 10 | if (error) { 11 | logger.error(error.message) 12 | return 13 | } 14 | files.forEach(file => { 15 | const contents = fs.readFileSync(file).toString() 16 | const types = tsToJSON.convert(contents) 17 | const newFileName = file.replace(".json", ".types.ts").replace("__tests__/fixtures", "types") 18 | fs.writeFileSync(newFileName, types) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /api/source/scripts/setup-plugins.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process" 2 | 3 | import { jsonDatabase } from "../db/json" 4 | import { DATABASE_JSON_FILE } from "../globals" 5 | 6 | const log = console.log 7 | 8 | const go = async () => { 9 | // Download settings 10 | const db = jsonDatabase(DATABASE_JSON_FILE) 11 | await db.setup() 12 | 13 | const installation = await db.getInstallation(0) 14 | if (!installation) { 15 | return 16 | } 17 | // Look for plugins 18 | if (installation.settings.modules && installation.settings.modules.length !== 0) { 19 | const modules = installation.settings.modules 20 | log("Installing: " + modules.join(", ")) 21 | 22 | const yarn = child_process.spawn("yarn", ["add", ...modules, "--ignore-scripts"], { 23 | env: { ...process.env, NO_RECURSE: "YES" }, 24 | }) 25 | 26 | yarn.stdout.on("data", data => log(`-> : ${data}`)) 27 | yarn.stderr.on("data", data => log(`! -> : ${data}`)) 28 | 29 | yarn.on("close", code => { 30 | log(`child process exited with code ${code}`) 31 | process.exit(code) 32 | }) 33 | } else { 34 | log("Not adding any plugins") 35 | process.exit(0) 36 | } 37 | } 38 | 39 | go() 40 | -------------------------------------------------------------------------------- /api/source/tasks/_tests/_scheduleTask-heroku.test.ts: -------------------------------------------------------------------------------- 1 | import { generateTaskSchedulerForInstallation } from "../../tasks/scheduleTask" 2 | import { triggerAFutureDangerRun } from "../startTaskScheduler" 3 | 4 | jest.mock("../startTaskScheduler", () => ({ 5 | agenda: { schedule: jest.fn() }, 6 | runDangerfileTaskName: "mockTask", 7 | hasAgendaInRuntime: () => true, 8 | triggerAFutureDangerRun: jest.fn(), 9 | })) 10 | 11 | it("handles passing the task directly to agenda", () => { 12 | const scheduleFunc = generateTaskSchedulerForInstallation(123, undefined) 13 | scheduleFunc("My Task", "in 1 day", { hello: "world" }) 14 | expect(triggerAFutureDangerRun).toBeCalledWith("1 day", { 15 | data: { hello: "world" }, 16 | installationID: 123, 17 | taskName: "My Task", 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /api/source/tasks/_tests/_scheduleTask-prod.test.ts: -------------------------------------------------------------------------------- 1 | import { generateTaskSchedulerForInstallation } from "../../tasks/scheduleTask" 2 | 3 | jest.mock("../startTaskScheduler", () => ({ 4 | agenda: undefined, 5 | runDangerfileTaskName: "mockTask", 6 | hasAgendaInRuntime: () => false, 7 | })) 8 | 9 | jest.mock("../../api/fetch", () => ({ 10 | fetch: jest.fn(() => Promise.resolve()), 11 | })) 12 | 13 | import { fetch } from "../../api/fetch" 14 | import { gql } from "../../api/graphql/gql" 15 | import { PerilRunnerBootstrapJSON } from "../../runner/triggerSandboxRun" 16 | 17 | it("handles making a working graphql mutation", () => { 18 | const bootstrap: any = { 19 | perilSettings: { 20 | perilJWT: "123.asd.zxc", 21 | perilAPIRoot: "https://murphdog.com", 22 | }, 23 | } as Partial 24 | 25 | const scheduleFunc = generateTaskSchedulerForInstallation(123, bootstrap) 26 | scheduleFunc("My Task", "1 month", { hello: "world" }) 27 | 28 | expect(fetch).toBeCalledWith("https://murphdog.com/api/graphql", { 29 | body: expect.anything(), 30 | headers: expect.anything(), 31 | method: "POST", 32 | }) 33 | 34 | // Verify the query feels right 35 | const mockFetch = fetch as any 36 | const body = mockFetch.mock.calls[0][1].body 37 | const response = JSON.parse(body) 38 | 39 | expect(response.query.replace(/\s/g, "").replace(/\\/g, "")).toEqual( 40 | gql` 41 | mutation { 42 | scheduleTask(jwt: "123.asd.zxc", task: "My Task", time: "1 month", data: "{ \"hello\": \"world\" }") { 43 | success 44 | } 45 | } 46 | ` 47 | .replace(/\s/g, "") 48 | .replace(/\\/g, "") 49 | ) 50 | }) 51 | -------------------------------------------------------------------------------- /api/source/tasks/_tests/_startTaskScheduler.test.ts: -------------------------------------------------------------------------------- 1 | let mockGenda: any 2 | 3 | class MockGenda { 4 | public define = jest.fn() 5 | public start = jest.fn() 6 | public every = jest.fn() 7 | 8 | constructor(public config: any) { 9 | mockGenda = this 10 | } 11 | 12 | public on(key: string, func: any) { 13 | if (key === "ready") { 14 | func() 15 | } 16 | } 17 | } 18 | 19 | jest.doMock("agenda", () => MockGenda) 20 | 21 | import { startTaskScheduler } from "../startTaskScheduler" 22 | 23 | it("it sets up correctly", async () => { 24 | await startTaskScheduler() 25 | 26 | // We have to get it started 27 | expect(mockGenda.start).toBeCalled() 28 | 29 | // And we need the right definitions 30 | const definitions = mockGenda.define.mock.calls.map((m: string[]) => m[0]) 31 | expect(definitions).toMatchInlineSnapshot(` 32 | Array [ 33 | "runDangerfile", 34 | "hourly", 35 | "daily", 36 | "weekly", 37 | "monday-morning-est", 38 | "tuesday-morning-est", 39 | "wednesday-morning-est", 40 | "thursday-morning-est", 41 | "friday-morning-est", 42 | ] 43 | `) 44 | 45 | // And we need to make sure all the time-y one 46 | const everys = mockGenda.every.mock.calls.map((m: string[]) => m[0]) 47 | expect(everys).toMatchInlineSnapshot(` 48 | Array [ 49 | "1 hour", 50 | "1 day", 51 | "1 week", 52 | "0 0 9 * * 1", 53 | "0 0 9 * * 2", 54 | "0 0 9 * * 3", 55 | "0 0 9 * * 4", 56 | "0 0 9 * * 5", 57 | ] 58 | `) 59 | }) 60 | -------------------------------------------------------------------------------- /api/source/tasks/runTask.ts: -------------------------------------------------------------------------------- 1 | import { getTemporaryAccessTokenForInstallation } from "../api/github" 2 | import { dangerRepresentationForPath, RunType } from "../danger/danger_run" 3 | import { runDangerForInstallation, ValidatedPayload } from "../danger/danger_runner" 4 | import { DangerfileReferenceString, GitHubInstallation } from "../db/index" 5 | import { getGitHubFileContents } from "../github/lib/github_helpers" 6 | import logger from "../logger" 7 | 8 | export const runTask = async ( 9 | taskName: string, 10 | installation: GitHubInstallation, 11 | references: DangerfileReferenceString[], 12 | data: any 13 | ) => { 14 | // Get representations that are also prefixed by the global settings JSON repo if needed 15 | const prefixedReps = references.map(dangerfileRef => { 16 | const rep = dangerRepresentationForPath(dangerfileRef) 17 | if (!rep.repoSlug) { 18 | // If you don't provide a repo slug, assume that the 19 | // dangerfile comes from inside the same repo as your settings. 20 | rep.repoSlug = dangerRepresentationForPath(installation.perilSettingsJSONURL).repoSlug 21 | rep.referenceString = `${rep.repoSlug}@${rep.dangerfilePath}` 22 | } 23 | return rep 24 | }) 25 | 26 | logger.info(`\n## task ${references} on ${installation.login}.`) 27 | 28 | const payload: ValidatedPayload = { 29 | dsl: {} as any, // This can't have a DSL for git, etc 30 | webhook: data, 31 | } 32 | 33 | const token = await getTemporaryAccessTokenForInstallation(installation.iID) 34 | 35 | // Get all the dangerfiles, this is needed for inline (JSON-based) runs 36 | const dangerfiles = [] 37 | for (const rep of prefixedReps) { 38 | const dangerfile = await getGitHubFileContents(token, rep.repoSlug!, rep.dangerfilePath, rep.branch) 39 | dangerfiles.push(dangerfile) 40 | } 41 | 42 | return runDangerForInstallation( 43 | taskName, 44 | dangerfiles, 45 | prefixedReps.map(r => r.referenceString), 46 | null, 47 | RunType.import, 48 | installation, 49 | payload 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /api/source/tasks/scheduleTask.ts: -------------------------------------------------------------------------------- 1 | import { graphqlAPI } from "../api/graphql/api" 2 | import { gql } from "../api/graphql/gql" 3 | import { PerilRunnerBootstrapJSON } from "../runner/triggerSandboxRun" 4 | import { DangerFileTaskConfig, hasAgendaInRuntime, triggerAFutureDangerRun } from "./startTaskScheduler" 5 | 6 | export const generateTaskSchedulerForInstallation = ( 7 | installationID: number, 8 | sandboxSettings?: PerilRunnerBootstrapJSON 9 | ) => { 10 | // Awkward JS so that I can get the types set up correct 11 | 12 | /** 13 | * Run a pre-set up task 14 | * 15 | * @param taskName 16 | * @param time 17 | * @param data 18 | */ 19 | const taskScheduler = async (taskName: string, time: string, data: any) => { 20 | const config: DangerFileTaskConfig = { 21 | taskName, 22 | data, 23 | installationID, 24 | } 25 | 26 | // Removes the "in " 27 | let sanitizedTime = time 28 | if (time.startsWith("in ")) { 29 | sanitizedTime = time.substr(3) 30 | } 31 | 32 | // If you're running on your own server, you can just call agenda 33 | // but if you're not then you're going to need to make an API call 34 | if (hasAgendaInRuntime()) { 35 | triggerAFutureDangerRun(sanitizedTime, config) 36 | } else { 37 | const settings = sandboxSettings! 38 | const mutationData = JSON.stringify(data).replace(/\"/g, '\\"') 39 | const query = gql` 40 | mutation { 41 | scheduleTask( 42 | jwt: "${settings.perilSettings.perilJWT}", 43 | task: "${taskName}", 44 | time: "${sanitizedTime}", 45 | data: "${mutationData}" 46 | ) { 47 | success 48 | } 49 | }` 50 | 51 | // Make the API call 52 | await graphqlAPI(settings.perilSettings.perilAPIRoot, query) 53 | } 54 | } 55 | 56 | return taskScheduler 57 | } 58 | -------------------------------------------------------------------------------- /api/source/testing/installationFactory.ts: -------------------------------------------------------------------------------- 1 | import { GitHubInstallation } from "../db/index" 2 | 3 | const emptyInstallation: GitHubInstallation = { 4 | iID: 123, 5 | login: "", 6 | avatarURL: "", 7 | repos: {}, 8 | rules: {}, 9 | lambdaName: "", 10 | scheduler: {}, 11 | settings: { 12 | env_vars: [], 13 | ignored_repos: [], 14 | modules: [], 15 | }, 16 | tasks: {}, 17 | perilSettingsJSONURL: "", 18 | installationSlackUpdateWebhookURL: "", 19 | } 20 | 21 | /** Creates an installation from a blank template */ 22 | export const generateInstallation = (diff: Partial): GitHubInstallation => 23 | Object.assign({}, emptyInstallation, diff) 24 | -------------------------------------------------------------------------------- /api/source/testing/setupScript.js: -------------------------------------------------------------------------------- 1 | // Without this any mongoose call blocks tests 2 | require("mockingoose") 3 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "allowSyntheticDefaultImports": true, 5 | "target": "es2017", 6 | "outDir": "out", 7 | "lib": ["esnext", "dom"], // Re:dom https://github.com/graphcool/graphql-request/issues/26#issuecomment-354482903 8 | "sourceMap": true, 9 | "rootDir": "source", 10 | "allowJs": true, 11 | "strictNullChecks": true, 12 | "alwaysStrict": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitThis": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "strict": true, 19 | "pretty": true 20 | }, 21 | "exclude": [ 22 | "out", 23 | "source/danger/_tests/fixtures", 24 | "runner/*", 25 | "dangerfile_runtime_env", 26 | "node_modules", 27 | "wallaby.js", 28 | "migrations", 29 | "dangerfile.lite.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | // "no-default-export": true, 5 | "object-literal-sort-keys": false, 6 | "interface-name": [true, "never-prefix"], 7 | "no-default-export": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function(wallaby) { 2 | return { 3 | files: [ 4 | "tsconfig.json", 5 | "jsconfig.json", 6 | ".env.sample", 7 | "source/testing/setupScript.js", 8 | "source/**/*.ts?(x)", 9 | "source/**/*.snap", 10 | "source/**/*.json", 11 | "source/**/*.diff", 12 | "!source/**/*.test.ts?(x)", 13 | ], 14 | tests: ["source/**/*.test.ts?(x)"], 15 | 16 | preprocessors: { 17 | "**/*.js": file => 18 | require("babel-core").transform(file.content, { sourceMap: true, presets: ["babel-preset-jest"] }), 19 | }, 20 | 21 | env: { 22 | type: "node", 23 | runner: "node", 24 | }, 25 | 26 | testFramework: "jest", 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /dashboard/.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_PUBLIC_API_ROOT_URL=https://staging-api.peril.systems 2 | # REACT_APP_PUBLIC_WEB_ROOT_URL=https://staging.peril.system 3 | REACT_APP_PUBLIC_WEB_ROOT_URL=http://localhost:3000 4 | REACT_APP_PUBLIC_IS_PRODUCTION="false" 5 | NOW_TOKEN=XXYYZZ 6 | -------------------------------------------------------------------------------- /dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .env 25 | -------------------------------------------------------------------------------- /dashboard/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Orta 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 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /dashboard/now.staging.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peril-dashboard", 3 | "alias": ["staging-dashboard.peril.systems"], 4 | "type": "npm", 5 | "public": false, 6 | "env": { 7 | "REACT_APP_PUBLIC_API_ROOT_URL": "https://staging-api.peril.systems", 8 | "REACT_APP_PUBLIC_WEB_ROOT_URL": "https://staging-dashboard.peril.systems", 9 | "REACT_APP_PUBLIC_IS_PRODUCTION": "false" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "^24.0.12", 7 | "@types/node": "12.0.0", 8 | "@types/react": "^16.8.17", 9 | "@types/react-dom": "^16.8.4", 10 | "babel-plugin-relay": "^4.0.0", 11 | "graphql": "^14.3.0", 12 | "isomorphic-unfetch": "^2.0.0", 13 | "react": "^16.8.6", 14 | "react-dom": "^16.8.6", 15 | "react-relay": "^4.0.0", 16 | "react-router-dom": "^4.3.1", 17 | "react-scripts": "3.0.1", 18 | "relay-compiler": "^4.0.0", 19 | "relay-compiler-language-typescript": "^4.2.0", 20 | "semantic-ui-react": "^0.81.1", 21 | "serve": "^8.2.0", 22 | "tiny-relative-date": "^1.3.0", 23 | "typescript": "3.4.5", 24 | "universal-cookie": "^2.1.5" 25 | }, 26 | "devDependencies": { 27 | "@types/dotenv": "^6.1.1", 28 | "@types/prop-types": "^15.7.1", 29 | "@types/react-relay": "^1.3.14", 30 | "@types/react-router-dom": "^4.3.3", 31 | "@types/universal-cookie": "^2.2.0", 32 | "concurrently": "^3.5.1" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject", 39 | "schema:download:staging": "yarn graphql get-schema -e https://staging-api.peril.systems/api/graphql -o relay_data/schema.graphql --no-all", 40 | "schema:download:local": "yarn graphql get-schema -e http://localhost:5000/api/graphql -o relay_data/schema.graphql --no-all", 41 | "deploy:staging": "yarn build && nf run scripts/deploy_staging.sh", 42 | "relay": "yarn relay-compiler --src ./ --include 'src/components/**/*' 'src/pages/**/*' --schema relay_data/schema.graphql --language typescript" 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /dashboard/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/dashboard/public/favicon.ico -------------------------------------------------------------------------------- /dashboard/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /dashboard/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /dashboard/scripts/deploy_staging.sh: -------------------------------------------------------------------------------- 1 | echo "Installing Now (if needed)" 2 | command -v now >/dev/null 2>&1 || { npm install -g now; } 3 | 4 | echo "Deploying" 5 | now deploy --local-config now.staging.json --team peril --token $NOW_TOKEN --npm && now alias --local-config now.staging.json --team peril --token $NOW_TOKEN 6 | -------------------------------------------------------------------------------- /dashboard/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dashboard/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /dashboard/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { BrowserRouter as Router, Redirect, Route } from "react-router-dom" 3 | 4 | import Cookies from "universal-cookie" 5 | import Home from "./components/Home" 6 | import Installation from "./components/Installation" 7 | import Layout from "./components/layout/Layout" 8 | import Login from "./components/Login" 9 | import PartialInstallation from "./components/PartialInstallation" 10 | 11 | class App extends React.Component { 12 | public render() { 13 | const cookies = new Cookies() 14 | 15 | return ( 16 | 17 | 18 |
19 | { 23 | const params = new URL(document.location as any).searchParams 24 | cookies.set("jwt", params.get("perilJWT")!) 25 | return 26 | }} 27 | /> 28 | 29 | { 33 | if (cookies.get("jwt")) { 34 | return 35 | } else { 36 | return 37 | } 38 | }} 39 | /> 40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 | ) 48 | } 49 | } 50 | 51 | export default App 52 | -------------------------------------------------------------------------------- /dashboard/src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module "isomorphic-unfetch" 2 | declare module "tiny-relative-date" 3 | declare module 'babel-plugin-relay/macro' 4 | -------------------------------------------------------------------------------- /dashboard/src/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Button, Grid, Segment } from "semantic-ui-react" 3 | import { addPerilURL, loginURL } from "../lib/routes" 4 | 5 | export default () => ( 6 |
7 | 14 | 15 | 16 | Logo 22 | 23 | 24 | 25 | 28 | 29 | 30 | Sign up to Peril 31 | {process.env.REACT_APP_PUBLIC_IS_PRODUCTION !== "true" && ( 32 | 33 |
Note: to use Staging you need to be in the danger org 34 |
35 | )} 36 |
37 |
38 |
39 | ) 40 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/InstallationRules.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { createFragmentContainer, RelayProp } from "react-relay" 3 | import graphql from 'babel-plugin-relay/macro'; 4 | 5 | import { Segment } from "semantic-ui-react" 6 | import { githubURLForReference } from "../../lib/dangerfileReferenceURLs" 7 | import { InstallationRules_installation } from "./__generated__/InstallationRules_installation.graphql" 8 | 9 | interface Props { 10 | installation: InstallationRules_installation 11 | } 12 | 13 | export const InstallationRules: any = (props: Props & { relay: RelayProp }) => { 14 | if (!props.installation) { 15 | return
16 | } 17 | 18 | const visibleSettings = { 19 | rules: props.installation.rules, 20 | repos: props.installation.repos, 21 | tasks: props.installation.tasks, 22 | scheduler: props.installation.scheduler, 23 | } 24 | const url = props.installation.perilSettingsJSONURL 25 | return ( 26 | 27 |
Peril settings
28 |
29 | {url} 30 |
31 | 32 |
{JSON.stringify(visibleSettings, null, "  ")}
33 |
34 | ) 35 | } 36 | 37 | export default createFragmentContainer( 38 | InstallationRules, 39 | { installation: 40 | graphql` 41 | fragment InstallationRules_installation on Installation { 42 | iID 43 | repos 44 | rules 45 | tasks 46 | scheduler 47 | perilSettingsJSONURL 48 | } 49 | ` 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/Overview.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { createFragmentContainer } from "react-relay" 4 | import graphql from 'babel-plugin-relay/macro'; 5 | 6 | import { Image, Header } from "semantic-ui-react" 7 | import { Overview_installation } from "./__generated__/Overview_installation.graphql" 8 | 9 | const OverviewInternal = (props: Props) => ( 10 |
11 |
12 | {props.installation.login} 13 |
14 |
15 | ) 16 | 17 | interface Props { 18 | installation: Overview_installation 19 | } 20 | 21 | export default createFragmentContainer( 22 | OverviewInternal, 23 | { installation: graphql` 24 | fragment Overview_installation on Installation { 25 | iID 26 | login 27 | avatarURL 28 | } 29 | `} 30 | ) 31 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/WebhooksHeader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { createFragmentContainer, RelayProp } from "react-relay" 3 | import graphql from 'babel-plugin-relay/macro'; 4 | 5 | import { Button, Header, Icon, Segment } from "semantic-ui-react" 6 | import relativeDate from "tiny-relative-date" 7 | import { runRecordWebhooksMutation } from "./mutations/makeInstallationRecordMutation" 8 | 9 | interface Props { 10 | installation: any 11 | relay?: RelayProp 12 | } 13 | 14 | const WebhooksHeaderInternal = (props: Props) => { 15 | const endRecording = props.installation.recordWebhooksUntilTime 16 | const startRecording = props.installation.startedRecordingWebhooksTime 17 | let isRecording = false 18 | if (endRecording && startRecording) { 19 | const endDate = new Date(endRecording) 20 | isRecording = new Date() < endDate 21 | } 22 | 23 | return ( 24 |
25 |
Saved GitHub events
26 | 27 | 37 | 38 |

39 | Peril can record webhooks sent from GitHub for a 5 minutes period, then you can re-trigger them at any point. 40 |

41 | {startRecording && ( 42 |

Last recorded {relativeDate(new Date(startRecording))}.

43 | )} 44 |
45 |
46 | ) 47 | } 48 | 49 | export default createFragmentContainer( 50 | WebhooksHeaderInternal, 51 | { installation: graphql` 52 | fragment WebhooksHeader_installation on Installation { 53 | iID 54 | 55 | recordWebhooksUntilTime 56 | startedRecordingWebhooksTime 57 | } 58 | `} 59 | ) 60 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/__generated__/EnvVars_installation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { ReaderFragment } from "relay-runtime"; 4 | export type EnvVars_installation$ref = any; 5 | export type EnvVars_installation = { 6 | readonly iID: number; 7 | readonly envVars: any | null; 8 | readonly " $refType": EnvVars_installation$ref; 9 | }; 10 | 11 | 12 | 13 | const node: ReaderFragment = { 14 | "kind": "Fragment", 15 | "name": "EnvVars_installation", 16 | "type": "Installation", 17 | "metadata": null, 18 | "argumentDefinitions": [], 19 | "selections": [ 20 | { 21 | "kind": "ScalarField", 22 | "alias": null, 23 | "name": "iID", 24 | "args": null, 25 | "storageKey": null 26 | }, 27 | { 28 | "kind": "ScalarField", 29 | "alias": null, 30 | "name": "envVars", 31 | "args": null, 32 | "storageKey": null 33 | } 34 | ] 35 | }; 36 | (node as any).hash = '94a4ffee4e437b8e6134aed9db80f6cd'; 37 | export default node; 38 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/__generated__/InstallationRules_installation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { ReaderFragment } from "relay-runtime"; 4 | export type InstallationRules_installation$ref = any; 5 | export type InstallationRules_installation = { 6 | readonly iID: number; 7 | readonly repos: any; 8 | readonly rules: any; 9 | readonly tasks: any; 10 | readonly scheduler: any; 11 | readonly perilSettingsJSONURL: string; 12 | readonly " $refType": InstallationRules_installation$ref; 13 | }; 14 | 15 | 16 | 17 | const node: ReaderFragment = { 18 | "kind": "Fragment", 19 | "name": "InstallationRules_installation", 20 | "type": "Installation", 21 | "metadata": null, 22 | "argumentDefinitions": [], 23 | "selections": [ 24 | { 25 | "kind": "ScalarField", 26 | "alias": null, 27 | "name": "iID", 28 | "args": null, 29 | "storageKey": null 30 | }, 31 | { 32 | "kind": "ScalarField", 33 | "alias": null, 34 | "name": "repos", 35 | "args": null, 36 | "storageKey": null 37 | }, 38 | { 39 | "kind": "ScalarField", 40 | "alias": null, 41 | "name": "rules", 42 | "args": null, 43 | "storageKey": null 44 | }, 45 | { 46 | "kind": "ScalarField", 47 | "alias": null, 48 | "name": "tasks", 49 | "args": null, 50 | "storageKey": null 51 | }, 52 | { 53 | "kind": "ScalarField", 54 | "alias": null, 55 | "name": "scheduler", 56 | "args": null, 57 | "storageKey": null 58 | }, 59 | { 60 | "kind": "ScalarField", 61 | "alias": null, 62 | "name": "perilSettingsJSONURL", 63 | "args": null, 64 | "storageKey": null 65 | } 66 | ] 67 | }; 68 | (node as any).hash = 'a4457047b69e823ccbf3c4892ae6e11a'; 69 | export default node; 70 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/__generated__/Overview_installation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { ReaderFragment } from "relay-runtime"; 4 | export type Overview_installation$ref = any; 5 | export type Overview_installation = { 6 | readonly iID: number; 7 | readonly login: string; 8 | readonly avatarURL: string | null; 9 | readonly " $refType": Overview_installation$ref; 10 | }; 11 | 12 | 13 | 14 | const node: ReaderFragment = { 15 | "kind": "Fragment", 16 | "name": "Overview_installation", 17 | "type": "Installation", 18 | "metadata": null, 19 | "argumentDefinitions": [], 20 | "selections": [ 21 | { 22 | "kind": "ScalarField", 23 | "alias": null, 24 | "name": "iID", 25 | "args": null, 26 | "storageKey": null 27 | }, 28 | { 29 | "kind": "ScalarField", 30 | "alias": null, 31 | "name": "login", 32 | "args": null, 33 | "storageKey": null 34 | }, 35 | { 36 | "kind": "ScalarField", 37 | "alias": null, 38 | "name": "avatarURL", 39 | "args": null, 40 | "storageKey": null 41 | } 42 | ] 43 | }; 44 | (node as any).hash = 'e57b695313b998e59c0d118f9d35e342'; 45 | export default node; 46 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/__generated__/Settings_installation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { ReaderFragment } from "relay-runtime"; 4 | export type Settings_installation$ref = any; 5 | export type Settings_installation = { 6 | readonly iID: number; 7 | readonly installationSlackUpdateWebhookURL: string | null; 8 | readonly perilSettingsJSONURL: string; 9 | readonly " $refType": Settings_installation$ref; 10 | }; 11 | 12 | 13 | 14 | const node: ReaderFragment = { 15 | "kind": "Fragment", 16 | "name": "Settings_installation", 17 | "type": "Installation", 18 | "metadata": null, 19 | "argumentDefinitions": [], 20 | "selections": [ 21 | { 22 | "kind": "ScalarField", 23 | "alias": null, 24 | "name": "iID", 25 | "args": null, 26 | "storageKey": null 27 | }, 28 | { 29 | "kind": "ScalarField", 30 | "alias": null, 31 | "name": "installationSlackUpdateWebhookURL", 32 | "args": null, 33 | "storageKey": null 34 | }, 35 | { 36 | "kind": "ScalarField", 37 | "alias": null, 38 | "name": "perilSettingsJSONURL", 39 | "args": null, 40 | "storageKey": null 41 | } 42 | ] 43 | }; 44 | (node as any).hash = '86b2fea621074057a25edb244f6c24ac'; 45 | export default node; 46 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/__generated__/TaskRunner_installation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { ReaderFragment } from "relay-runtime"; 4 | export type TaskRunner_installation$ref = any; 5 | export type TaskRunner_installation = { 6 | readonly iID: number; 7 | readonly tasks: any; 8 | readonly " $refType": TaskRunner_installation$ref; 9 | }; 10 | 11 | 12 | 13 | const node: ReaderFragment = { 14 | "kind": "Fragment", 15 | "name": "TaskRunner_installation", 16 | "type": "Installation", 17 | "metadata": null, 18 | "argumentDefinitions": [], 19 | "selections": [ 20 | { 21 | "kind": "ScalarField", 22 | "alias": null, 23 | "name": "iID", 24 | "args": null, 25 | "storageKey": null 26 | }, 27 | { 28 | "kind": "ScalarField", 29 | "alias": null, 30 | "name": "tasks", 31 | "args": null, 32 | "storageKey": null 33 | } 34 | ] 35 | }; 36 | (node as any).hash = '7aac742944a40d27ff1307ae5b9b5029'; 37 | export default node; 38 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/__generated__/WebhooksHeader_installation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { ReaderFragment } from "relay-runtime"; 4 | export type WebhooksHeader_installation$ref = any; 5 | export type WebhooksHeader_installation = { 6 | readonly iID: number; 7 | readonly recordWebhooksUntilTime: string | null; 8 | readonly startedRecordingWebhooksTime: string | null; 9 | readonly " $refType": WebhooksHeader_installation$ref; 10 | }; 11 | 12 | 13 | 14 | const node: ReaderFragment = { 15 | "kind": "Fragment", 16 | "name": "WebhooksHeader_installation", 17 | "type": "Installation", 18 | "metadata": null, 19 | "argumentDefinitions": [], 20 | "selections": [ 21 | { 22 | "kind": "ScalarField", 23 | "alias": null, 24 | "name": "iID", 25 | "args": null, 26 | "storageKey": null 27 | }, 28 | { 29 | "kind": "ScalarField", 30 | "alias": null, 31 | "name": "recordWebhooksUntilTime", 32 | "args": null, 33 | "storageKey": null 34 | }, 35 | { 36 | "kind": "ScalarField", 37 | "alias": null, 38 | "name": "startedRecordingWebhooksTime", 39 | "args": null, 40 | "storageKey": null 41 | } 42 | ] 43 | }; 44 | (node as any).hash = '8b0a4042a9cccff0bf61aef13beb57ec'; 45 | export default node; 46 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/__generated__/Websocket_installation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { ReaderFragment } from "relay-runtime"; 4 | export type Websocket_installation$ref = any; 5 | export type Websocket_installation = { 6 | readonly iID: number; 7 | readonly perilSettingsJSONURL: string; 8 | readonly " $refType": Websocket_installation$ref; 9 | }; 10 | 11 | 12 | 13 | const node: ReaderFragment = { 14 | "kind": "Fragment", 15 | "name": "Websocket_installation", 16 | "type": "Installation", 17 | "metadata": null, 18 | "argumentDefinitions": [], 19 | "selections": [ 20 | { 21 | "kind": "ScalarField", 22 | "alias": null, 23 | "name": "iID", 24 | "args": null, 25 | "storageKey": null 26 | }, 27 | { 28 | "kind": "ScalarField", 29 | "alias": null, 30 | "name": "perilSettingsJSONURL", 31 | "args": null, 32 | "storageKey": null 33 | } 34 | ] 35 | }; 36 | (node as any).hash = '8f9489007f1ee6e8e28390a7de26e47a'; 37 | export default node; 38 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/mutations/editEnvVarMutation.ts: -------------------------------------------------------------------------------- 1 | import { commitMutation } from "react-relay" 2 | import graphql from 'babel-plugin-relay/macro'; 3 | 4 | import { Environment } from "relay-runtime" 5 | 6 | const mutation = graphql` 7 | mutation editEnvVarMutation($iID: Int!, $key: String!, $value: String) { 8 | changeEnvVarForInstallation(iID: $iID, key: $key, value: $value) 9 | } 10 | ` 11 | 12 | export const runEditEnvVarsMutation = ( 13 | environment: Environment, 14 | variables: { 15 | iID: number 16 | key: string 17 | value?: string 18 | }, 19 | onCompleted: (res: any) => void 20 | ) => { 21 | commitMutation(environment, { 22 | mutation, 23 | variables, 24 | onCompleted, 25 | onError: err => console.error(err), 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/mutations/editInstallationMutation.ts: -------------------------------------------------------------------------------- 1 | import { commitMutation } from "react-relay" 2 | import graphql from 'babel-plugin-relay/macro'; 3 | 4 | import { Environment } from "relay-runtime" 5 | 6 | export interface EditInstallationMutationOptions { 7 | iID: number 8 | perilSettingsJSONURL?: string 9 | installationSlackUpdateWebhookURL?: string 10 | } 11 | 12 | const mutation = graphql` 13 | mutation editInstallationMutationMutation( 14 | $iID: Int! 15 | $perilSettingsJSONURL: String 16 | $installationSlackUpdateWebhookURL: String 17 | ) { 18 | editInstallation( 19 | iID: $iID 20 | perilSettingsJSONURL: $perilSettingsJSONURL 21 | installationSlackUpdateWebhookURL: $installationSlackUpdateWebhookURL 22 | ) { 23 | ... on Installation { 24 | perilSettingsJSONURL 25 | } 26 | ... on MutationError { 27 | error { 28 | description 29 | } 30 | } 31 | } 32 | } 33 | ` 34 | 35 | export const editInstallationMutation = ( 36 | environment: Environment, 37 | options: EditInstallationMutationOptions, 38 | onCompleted: (res: any) => void 39 | ) => 40 | commitMutation(environment, { 41 | mutation, 42 | variables: options, 43 | onError: err => { 44 | throw err 45 | }, 46 | onCompleted, 47 | }) 48 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/mutations/makeInstallationRecordMutation.ts: -------------------------------------------------------------------------------- 1 | import { commitMutation } from "react-relay" 2 | import { Environment } from "relay-runtime" 3 | import graphql from 'babel-plugin-relay/macro'; 4 | 5 | 6 | const mutation = graphql` 7 | mutation makeInstallationRecordMutation($iID: Int!) { 8 | makeInstallationRecord(iID: $iID) { 9 | ... on Installation { 10 | login 11 | } 12 | ... on MutationError { 13 | error { 14 | description 15 | } 16 | } 17 | } 18 | } 19 | ` 20 | 21 | export const runRecordWebhooksMutation = (environment: Environment, installationID: number) => { 22 | const variables = { 23 | iID: installationID, 24 | } 25 | console.log(variables) 26 | commitMutation(environment, { 27 | mutation, 28 | variables, 29 | onError: err => console.error(err), 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/mutations/runTaskMutation.ts: -------------------------------------------------------------------------------- 1 | import { commitMutation } from "react-relay" 2 | import graphql from 'babel-plugin-relay/macro'; 3 | 4 | import { Environment } from "relay-runtime" 5 | 6 | export interface RunTaskMutationOptions { 7 | iID: number 8 | task: string 9 | data?: any 10 | } 11 | 12 | const mutation = graphql` 13 | mutation runTaskMutation($iID: Int!, $task: String!, $data: JSON!) { 14 | runTask(iID: $iID, task: $task, data: $data) { 15 | success 16 | } 17 | } 18 | ` 19 | 20 | export const runTaskMutation = (environment: Environment, options: RunTaskMutationOptions) => { 21 | commitMutation(environment, { 22 | mutation, 23 | variables: options, 24 | onError: err => console.error(err), 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /dashboard/src/components/installation/mutations/triggerWebhookMutation.ts: -------------------------------------------------------------------------------- 1 | import { commitMutation} from "react-relay" 2 | import graphql from 'babel-plugin-relay/macro'; 3 | 4 | import { Environment } from "relay-runtime" 5 | 6 | const mutation = graphql` 7 | mutation triggerWebhookMutation($iID: Int!, $eventID: String!) { 8 | sendWebhookForInstallation(iID: $iID, eventID: $eventID) { 9 | event 10 | } 11 | } 12 | ` 13 | 14 | export const triggerWebhookMutation = (environment: Environment, installationID: number, eventID: string) => { 15 | const variables = { 16 | iID: installationID, 17 | eventID, 18 | } 19 | 20 | console.log(variables) 21 | commitMutation(environment, { 22 | mutation, 23 | variables, 24 | onError: err => console.error(err), 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /dashboard/src/components/partial/__generated__/SetJSONPathForm_installation.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { ReaderFragment } from "relay-runtime"; 4 | export type SetJSONPathForm_installation$ref = any; 5 | export type SetJSONPathForm_installation = { 6 | readonly iID: number; 7 | readonly " $refType": SetJSONPathForm_installation$ref; 8 | }; 9 | 10 | 11 | 12 | const node: ReaderFragment = { 13 | "kind": "Fragment", 14 | "name": "SetJSONPathForm_installation", 15 | "type": "Installation", 16 | "metadata": null, 17 | "argumentDefinitions": [], 18 | "selections": [ 19 | { 20 | "kind": "ScalarField", 21 | "alias": null, 22 | "name": "iID", 23 | "args": null, 24 | "storageKey": null 25 | } 26 | ] 27 | }; 28 | (node as any).hash = 'c0971694f3ca6aab4d7b2d46f885e036'; 29 | export default node; 30 | -------------------------------------------------------------------------------- /dashboard/src/components/partial/mutations/updateJSONURLMutation.tsx: -------------------------------------------------------------------------------- 1 | import { commitMutation } from "react-relay" 2 | import graphql from 'babel-plugin-relay/macro'; 3 | 4 | import { Environment } from "relay-runtime" 5 | 6 | export interface UpdateJSONURLMutationOptions { 7 | iID: number 8 | perilSettingsJSONURL: string 9 | } 10 | 11 | const mutation = graphql` 12 | mutation updateJSONURLMutation($iID: Int!, $perilSettingsJSONURL: String!) { 13 | convertPartialInstallation(iID: $iID, perilSettingsJSONURL: $perilSettingsJSONURL) { 14 | ... on Installation { 15 | perilSettingsJSONURL 16 | } 17 | ... on MutationError { 18 | error { 19 | description 20 | } 21 | } 22 | } 23 | } 24 | ` 25 | 26 | export const updateJSONURLMutation = ( 27 | environment: Environment, 28 | options: UpdateJSONURLMutationOptions, 29 | onCompleted: (res: any) => void 30 | ) => 31 | commitMutation(environment, { 32 | mutation, 33 | variables: options, 34 | onError: err => { 35 | throw err 36 | }, 37 | onCompleted, 38 | }) 39 | -------------------------------------------------------------------------------- /dashboard/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /dashboard/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /dashboard/src/lib/RelayProvider.ts: -------------------------------------------------------------------------------- 1 | import * as PropTypes from "prop-types" 2 | import * as React from "react" 3 | 4 | // Thank you https://github.com/robrichard 5 | // https://github.com/robrichard/relay-context-provider 6 | interface ProviderProps { 7 | environment: any 8 | variables: any 9 | } 10 | 11 | class RelayProvider extends React.Component { 12 | public getChildContext() { 13 | return { 14 | relay: { 15 | environment: this.props.environment, 16 | variables: this.props.variables 17 | } 18 | } 19 | } 20 | public render() { 21 | return this.props.children 22 | } 23 | } 24 | 25 | ;(RelayProvider as any).childContextTypes = { 26 | relay: PropTypes.object.isRequired 27 | } 28 | 29 | export default RelayProvider 30 | -------------------------------------------------------------------------------- /dashboard/src/lib/createRelayEnvironment.ts: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-unfetch" 2 | import { Environment, Network, RecordSource, Store } from "relay-runtime" 3 | import Cookies from "universal-cookie" 4 | 5 | let relayEnvironment: Environment | null = null 6 | 7 | // Define a function that returns the fetch for the results of an operation (query/mutation/etc) 8 | // and returns its results as a Promise: 9 | export const fetchQuery = (operation: { text: string }, variables: any, _?: any, __?: any) => { 10 | const cookies = new Cookies() 11 | const auth = { Authorization: `Basic ${cookies.get("jwt")}` } 12 | 13 | return ( 14 | fetch(`${process.env.REACT_APP_PUBLIC_API_ROOT_URL}/api/graphql`, { 15 | method: "POST", 16 | headers: { 17 | Accept: "application/json", 18 | "Content-Type": "application/json", 19 | ...auth, 20 | }, 21 | body: JSON.stringify({ 22 | query: operation.text, // GraphQL text from input 23 | variables, 24 | }), 25 | }) 26 | .then((response: any) => { 27 | return response.json() 28 | }) 29 | // For debugging 30 | .then((json: any) => { 31 | return json 32 | }) 33 | ) 34 | } 35 | 36 | export default function initEnvironment() { 37 | // Create a network layer from the fetch function 38 | const network = Network.create(fetchQuery) 39 | const store = new Store(new RecordSource({})) 40 | 41 | // reuse Relay environment on client-side 42 | if (!relayEnvironment) { 43 | relayEnvironment = new Environment({ 44 | network, 45 | store, 46 | }) 47 | } 48 | 49 | return relayEnvironment 50 | } 51 | -------------------------------------------------------------------------------- /dashboard/src/lib/dangerfileReferenceURLs.ts: -------------------------------------------------------------------------------- 1 | export interface RepresentationForURL { 2 | /** The path the the file aka folder/file.ts */ 3 | dangerfilePath: string 4 | /** The branch to find the dangerfile on */ 5 | branch: string 6 | /** An optional repo */ 7 | repoSlug: string | undefined 8 | /** The original full string, with repo etc */ 9 | referenceString: string 10 | } 11 | 12 | /** Takes a DangerfileReferenceString and lets you know where to find it globally */ 13 | export const dangerRepresentationForPath = (value: string): RepresentationForURL => { 14 | const afterAt = value.includes("@") ? value.split("@")[1] : value 15 | return { 16 | branch: value.includes("#") ? value.split("#")[1] : "master", 17 | dangerfilePath: afterAt.split("#")[0], 18 | repoSlug: value.includes("@") ? value.split("@")[0] : undefined, 19 | referenceString: value, 20 | } 21 | } 22 | 23 | /** 24 | * From: danger/peril-settings@settings.json 25 | * To: https://github.com/danger/peril-settings/blob/master/settings.json 26 | */ 27 | export const githubURLForReference = (value: string, repo?: string) => { 28 | const rep = dangerRepresentationForPath(value) 29 | const repoSlug = rep.repoSlug || repo 30 | if (!repoSlug) { 31 | throw new Error("no repo found") 32 | } 33 | 34 | return `https://github.com/${repoSlug}/blob/${rep.branch}/${rep.dangerfilePath}` 35 | } 36 | -------------------------------------------------------------------------------- /dashboard/src/lib/routes.ts: -------------------------------------------------------------------------------- 1 | const thisServer = process.env.REACT_APP_PUBLIC_WEB_ROOT_URL 2 | const api = process.env.REACT_APP_PUBLIC_API_ROOT_URL 3 | 4 | /** URL to the main dashboard */ 5 | export const successfulLoginURL = thisServer + "/success" 6 | /** URL send someone to log into Peril */ 7 | export const loginURL = api + "/api/auth/peril/github/start?redirect=" + encodeURIComponent(successfulLoginURL) 8 | /** URL to an partial installation */ 9 | export const partialInstallation = (iID: number) => thisServer + "/partial/" + iID 10 | /** When you want to send someone somewhere nice */ 11 | export const customLoginRedirect = (url: string) => 12 | api + "/api/auth/peril/github/start?redirect=" + encodeURIComponent(url) 13 | /** URL to add Peril to an org */ 14 | export const addPerilURL = api + "/api/integrate/github" 15 | -------------------------------------------------------------------------------- /dashboard/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "preserve" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /docs/api_architecture.md: -------------------------------------------------------------------------------- 1 | ## Peril's API Architecture 2 | 3 | ## GitHub Integration 4 | 5 | Each GitHub org that is hooked up to Peril is called an installation. Today, everyone is doing 1 installation, one 6 | server of Peril. However, Peril has been built to handle multiple installations from day one. 7 | 8 | #### Launch 9 | 10 | On launch, Peril needs to get enough information to set up the Dangerfile routing system. When you're hosting a 11 | stand-alone server (e.g. for one org, on heroku) then Peril will make an auth'd request for the JSON that represents 12 | your Peril settings. This is classed as a [JSON db][json_db]. 13 | 14 | For Peril staging/production, then the information comes at runtime via Mongo which stores info per-org. 15 | 16 | #### A GitHub Webhook arrives 17 | 18 | The Webhook arrives and is picked up in the [github router][gh_router], which has the responsibilities in handling any 19 | Peril specific Webhooks (account creation / deletion etc) and then passing it forward to the [GitHub runner][gh_runner]. 20 | 21 | #### GitHub runner 22 | 23 | This is where Peril uses the Peril settings from the JSON db for that installation to decide what to do with the 24 | webhook. There are four main types of events you can get a webhook for. 25 | 26 | - Event is org based (no repo, DSL is event JSON) 27 | - Event is repo based (has a reference to a repo, but nothing to comment on) 28 | - Event is PR based (has a repo + issue, can comment, gets normal DangerDSL) 29 | - Event is issue based (has a repo + issue, can comment, gets event DSL ) 30 | 31 | Each one has different trade-offs. PRs need the full Danger DSL. An issue should support `fail`, `warn` etc. An org/repo 32 | event can only really use the Octokit API. 33 | 34 | The GitHub runner's responsibility is to get enough of the DSL set up to be able to be passed to the [Danger 35 | Runner][danger_runner]. 36 | 37 | #### Danger Runner 38 | 39 | The Danger runner is responsible for handling the Peril specifics for the Danger DSL, and for executing the job. 40 | 41 | [json_db]: https://github.com/danger/peril/blob/master/source/db/json.ts 42 | [gh_router]: https://github.com/danger/peril/blob/master/source/routing/router.ts 43 | [gh_runner]: https://github.com/danger/peril/blob/master/source/github/events/github_runner.ts#L1 44 | [danger_runner]: https://github.com/danger/peril/blob/master/source/danger/danger_runner.ts 45 | -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | ### How do debug agenda 2 | 3 | Edit `now.staging.json` to include `DEBUG` 4 | 5 | ```diff 6 | { 7 | "env": { 8 | + "DEBUG": "agenda:*" 9 | } 10 | } 11 | ``` 12 | 13 | You can throw up a UI to the db using: 14 | 15 | ```sh 16 | echo "http://localhost:3001"; npx agendash --db=[get this from .env] --collection=agendaJobs --port=3001 17 | ``` 18 | 19 | ## How to debug hyper runs 20 | 21 | For staging: 22 | 23 | ```sh 24 | hyper func inspect peril-staging 25 | ``` 26 | 27 | To get the ENV, and set debug to true: 28 | 29 | ```sh 30 | hyper func update --env PERIL_BOT_USER_ID=34651588 --env DEBUG=* peril-staging 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/env/staging.md: -------------------------------------------------------------------------------- 1 | ## Peril Staging 2 | 3 | **Setup:** 4 | 5 | - Now.js: https://zeit.co/teams/peril 6 | - DB: [Mongo Atlas](https://cloud.mongodb.com/v2/5adafbc80bd66b23d635b2bb#clusters) 7 | - GitHub app: https://github.com/organizations/danger/settings/apps/danger-in-peril 8 | - Dashboard: https://staging-dashboard.peril.systems 9 | - Consumer front-end: https://staging-web.peril.systems 10 | - API: https://staging-api.peril.systems 11 | - GraphiQL: https://staging-api.peril.systems/api/graphiql 12 | 13 | --- 14 | 15 | **Scripts:** 16 | 17 | - Logs: `logs:staging` 18 | - Deploy: `deploy:staging` 19 | 20 | --- 21 | 22 | **Tricky Bits** 23 | 24 | - Secrets vars are a bit weird in now. You have team-wide secrets, that are then re-used in the env vars by aliases. 25 | - Adding the pem to now is hard, I ended up making a file copy of both private and public, then doing this: 26 | `now secrets -T peril add stag_private_github_signing_key (cat thing.pem | base64)`. Fish only. 27 | -------------------------------------------------------------------------------- /docs/images/events-ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/docs/images/events-ex.png -------------------------------------------------------------------------------- /docs/images/events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/docs/images/events.png -------------------------------------------------------------------------------- /docs/images/heroku_setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/docs/images/heroku_setup.png -------------------------------------------------------------------------------- /docs/images/peril-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/docs/images/peril-setup.png -------------------------------------------------------------------------------- /docs/service_map.md: -------------------------------------------------------------------------------- 1 | # Peril Service Map 2 | 3 | Peril Staging/Production is a combination of 5 different services: 4 | 5 | - peril.systems 6 | - dashboard.peril.systems 7 | - api.peril.systems 8 | - Lambda runners 9 | - Danger JS 10 | 11 | ## Peril Systems (`/web`) 12 | 13 | The public facing website for Peril, a Gatsby site which sells and documents Peril. 14 | 15 | ## Dashboard (`/dashboard`) 16 | 17 | A Create-React App which uses TS + Relay against `api.peril.systems` to show logs, edit configuration and toggle dev 18 | modes. 19 | 20 | ## API (`/api`) 21 | 22 | Handles: 23 | 24 | - User account management 25 | - Org installation management 26 | - Receiving webhooks from GitHub 27 | - Assigning work for the runner based on the webhook 28 | - Exposing a GraphQL API for the Web/Dashboard/Runner 29 | 30 | For more, see [API architecture](https://github.com/danger/peril/blob/master/docs/api_architecture.md) 31 | 32 | ## Lambda Runners (`/api/source/runner`) 33 | 34 | The environment which downloads, transpiles and executes JavaScript in. It receives an extended version of the same JSON 35 | which occurs inside Danger JS (with additions like auth, webhook JSON etc) and when the JS is done - the runner 36 | communicates back to the API and for PR events, back to the PR. 37 | 38 | ## Danger JS 39 | 40 | The private API in Danger JS is used a bunch in Peril. Danger JS was built with Peril in mind from day 1 - so it's worth 41 | considering a part of the Peril services. 42 | -------------------------------------------------------------------------------- /docs/terminology.md: -------------------------------------------------------------------------------- 1 | # Terminology 2 | 3 | This gets confusing, so I'll try map out all the terms used inside Peril. 4 | 5 | - _Danger_ - the nodejs CLI tool that runs your code for cultural rules. 6 | - _Peril_ - the hosted server that takes GitHub events and runs danger against them. 7 | - _GitHub_ - currently the only core review site that Danger supports, and so also the only one for Peril. 8 | - _Org_ - A GitHub Organization. A collection of users and repos. 9 | - _Repo_ - A GitHub Repo. 10 | - _GitHub App_ - A GitHub API type, it allows external apps to get a feed of events and can have "[bot]" accounts. 11 | - _Installation_ - When a GitHub user adds a GitHub app to an org, or a set of repos. Each repo (or one for all repos) 12 | is an installation. 13 | - _[bot] account_ - An account for an integration, it has the word [bot] next to its name. 14 | - _Event_ - Any user interaction that happens on GitHub, within the context of your org. See this 15 | [link for all events](https://developer.github.com/webhooks/#events). 16 | - _Action_ - An event can have a sub-action, so a `"issues"` event can have the actions of `"created"`, `"updated"`, 17 | etc. 18 | - _Staging/Prod_ - Orta's hosted version of Peril, that handles multiple settings repos for many org/users. It is Mongo 19 | based, and uses a Docker Runner 20 | - _JSON based host_ - A Peril instance that uses a single JSON repo, it doesn't keep a copy of the installation inside 21 | the MongoDB 22 | - _Mongo based host_ - A Peril instance that does not use `DATABASE_JSON_FILE`, but instead works via GitHub webhooks 23 | and the GraphQL API 24 | - _Inline Danger Runner_ - When the dangerfile is eval'd inside the same process as Peril, you must trust all code in 25 | this situation. 26 | - _Docker Danger Runner_ - Using Peril as a docker container, from the runner tag on 27 | [dangersystems/peril](https://hub.docker.com/r/dangersystems/peril/) to evaluate JS code in a safe, fresh environment 28 | each time 29 | -------------------------------------------------------------------------------- /docs/updating_peril.md: -------------------------------------------------------------------------------- 1 | ## Updating Peril 2 | 3 | If you have deployed peril using Heroku, you can update your deployment by cloning your deployment locally through your Heroku Git URL: 4 | 5 | ``` 6 | git clone https://git.heroku.com/[your-app-name].git 7 | cd [your-app-name] 8 | ``` 9 | 10 | Then you can add the peril main repo as a remote and pull the latest changes: 11 | ``` 12 | git remote add peril https://github.com/danger/peril.git 13 | git pull peril master 14 | git push heroku master 15 | ``` 16 | 17 | 18 | ## Updating pre-monorepo to post-monorepo 19 | 20 | See: https://github.com/danger/peril/pull/423 21 | -------------------------------------------------------------------------------- /docs/using_peril_staging.md: -------------------------------------------------------------------------------- 1 | ## Peril Staging 2 | 3 | Peril Staging and Peril Production are orta-hosted instances of Peril with a few differences: 4 | 5 | - There's a peril dashboard: https://staging-dashboard.peril.systems 6 | - You can have many orgs running on the same server (I run artsy, danger, orta, CocoaPods, PerilTest for example) 7 | - Each org gets its own AWS lambda 8 | - Peril can send logs of a Dangerfile run to Slack, or to the admin dashboard (WIP, currently broken, currently 9 | re-thinking) 10 | - Peril can store 5 minutes of webhooks from GitHub to your org, and replay them so you can work on a feature 11 | - The scheduler is set up, allowing you to have repeat tasks or to run a dangerfile in the future 12 | 13 | It's structured like this: 14 | 15 | 16 | 17 | ## Setup 18 | 19 | Anyone can add Peril to their org, via the GitHub app using the sign-up link on: https://staging-dashboard.peril.systems 20 | 21 | But only people who are in the danger org can turn the Peril on for their org. 22 | 23 | ## Docs 24 | 25 | The only docs are in this repo: 26 | 27 | - https://github.com/danger/peril/blob/master/docs/service_map.md 28 | - https://github.com/danger/peril/blob/master/docs/settings_repo_info.md 29 | - https://github.com/danger/peril/blob/master/docs/api_architecture.md 30 | 31 | I'm working on user-facing docs, but they're not there today. It'll be on https://staging.peril.systems one of days. I 32 | waited until the dashboard was done before thinking about the user-facing docs. 33 | -------------------------------------------------------------------------------- /hooks/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This is so we can get the commit into the build log of a Dangerfile runner 4 | # These come from https://docs.docker.com/docker-cloud/builds/advanced/ 5 | 6 | # For debugging all env vars 7 | # printenv 8 | 9 | # Convert the location "/Dockerfile" to "Dockerfile" 10 | FILE=$(echo -n $BUILD_PATH | tail -c +2) 11 | 12 | if [ -z "${DOCKER_TAG}" ]; then 13 | docker build --build-arg=COMMIT=$(git rev-parse --short HEAD) --build-arg=BRANCH=$SOURCE_BRANCH -t $IMAGE_NAME -f $FILE . 14 | else 15 | docker build --build-arg=COMMIT=$(git rev-parse --short HEAD) --build-arg=BRANCH=$DOCKER_TAG -t $IMAGE_NAME -f $FILE . 16 | fi 17 | -------------------------------------------------------------------------------- /hooks/pre_build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd api 3 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peril-api-dev", 3 | "type": "npm", 4 | "public": false, 5 | "version": 2, 6 | "builds": [ 7 | { "src": "api/source/index.ts", "use": "@now/node" }, 8 | { 9 | "src": "dashboard/package.json", 10 | "use": "@now/static-build", 11 | "config": { "distDir": "build" } 12 | }, 13 | { 14 | "src": "web/package.json", 15 | "use": "@now/static-build", 16 | "config": { "distDir": "public" } 17 | } 18 | ], 19 | "routes": {}, 20 | "env": { 21 | "PERIL_BOT_USER_ID": "@stag_peril_bot_user_id", 22 | "PERIL_INTEGRATION_ID": "@stag_peril_integration_id", 23 | "PERIL_WEBHOOK_SECRET": "@stag_peril_webhook_secret", 24 | 25 | "PRIVATE_GITHUB_SIGNING_KEY": "@stag_private_github_signing_key", 26 | "PUBLIC_GITHUB_SIGNING_KEY": "@stag_public_github_signing_key", 27 | 28 | "MONGODB_URI": "@stag_mongodb_uri", 29 | 30 | "PUBLIC_FACING_API": "true", 31 | "PRODUCTION": "false", 32 | "PUBLIC_API_ROOT_URL": "https://staging-api.peril.systems", 33 | "PUBLIC_WEB_ROOT_URL": "https://staging.peril.systems", 34 | "PUBLIC_GITHUB_APP_URL": "https://github.com/apps/peril-staging", 35 | 36 | "GITHUB_CLIENT_ID": "@stag_github_client_id", 37 | "GITHUB_CLIENT_SECRET": "@stag_github_client_secret", 38 | 39 | "SENTRY_DSN": "@stag_sentry_dsn", 40 | 41 | "AWS_ACCESS_KEY_ID": "@stag_aws_access_key_id", 42 | "AWS_SECRET_ACCESS_KEY": "@stag_aws_secret_access_key", 43 | "AWS_REGION": "us-east-1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /runner/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"], 3 | "plugins": ["@babel/proposal-class-properties", "@babel/proposal-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /runner/README.md: -------------------------------------------------------------------------------- 1 | ## Peril Runner 2 | 3 | This folder is Peril's Dangerfile runtime. Its packages are exactly what the Peril AWS layer uses for your Dangerfiles 4 | with, and so can reliably be used in a peril-settings repo. 5 | 6 | The package.json is a mix of human curated `devDependencies` and human generated `dependencies` (via 7 | [`../api/source/scripts/generate-runner-deps.ts][]). 8 | 9 | ## Aim 10 | 11 | Bootstrap Peril's runtime, and run the peril runner. 12 | 13 | This needs to be _really_ small, so, avoid use node_modules in here as the function zip ends up in memory in Peril. 14 | 15 | ## TODO 16 | 17 | - Files for Babel setup 18 | -------------------------------------------------------------------------------- /runner/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const perilRoot = path.join("..", "..", "opt", "out") 3 | 4 | // Change our directory so that it's basically the layer 5 | process.chdir(perilRoot) 6 | 7 | // What the lambda calls 8 | exports.handler = function(event, context, callback) { 9 | // Reach back to the main layer, and pull out Peril runtime export 10 | const app = require(path.join(perilRoot, "runner", "run")) 11 | callback(null, app.run(JSON.stringify(event))) 12 | } 13 | -------------------------------------------------------------------------------- /runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_": "These are ones that we want to add to make life easier for people using the runtime", 3 | "devDependencies": { 4 | "@graphql-inspector/core": "0.13.2", 5 | "@babel/core": "7.3.3", 6 | "@babel/plugin-proposal-class-properties": "7.3.3", 7 | "@babel/plugin-proposal-object-rest-spread": "7.3.2", 8 | "@babel/plugin-transform-flow-strip-types": "7.2.3", 9 | "@babel/plugin-transform-regenerator": "7.0.0", 10 | "@babel/preset-env": "7.3.1", 11 | "@babel/preset-typescript": "7.3.3", 12 | "@types/lodash": "^4.14.116", 13 | "@types/node": "^9.1.2", 14 | "@types/node-fetch": "^2.1.2", 15 | "danger-plugin-spellcheck": "^1.2.3", 16 | "danger-plugin-yarn": "^1.2.1", 17 | "googleapis": "^36.0.0", 18 | "graphql-relay-tools": "0.1.1", 19 | "graphql-schema-utils": "0.6.6", 20 | "jira-client": "6.4.1", 21 | "semver-sort": "0.0.4" 22 | }, 23 | "__": "These are auto-generated by the script generate-runner-deps.ts", 24 | "dependencies": { 25 | "@octokit/rest": "16.22.0", 26 | "@sentry/node": "4.1.1", 27 | "@slack/client": "4.8.0", 28 | "agenda": "1.0.3", 29 | "apollo-server-express": "1.4.0", 30 | "async-exit-hook": "2.0.1", 31 | "aws-sdk": "2.374.0", 32 | "babel-polyfill": "7.0.0-alpha.19", 33 | "body-parser": "1.18.3", 34 | "chalk": "2.4.2", 35 | "cookie": "0.3.1", 36 | "cookie-parser": "1.4.3", 37 | "cors": "2.8.4", 38 | "danger": "7.0.19", 39 | "debug": "4.1.1", 40 | "dotenv": "5.0.1", 41 | "express": "4.16.4", 42 | "express-x-hub": "1.0.4", 43 | "github-webhook-event-types": "1.2.1", 44 | "graphql": "0.13.2", 45 | "graphql-playground-middleware-express": "1.7.5", 46 | "graphql-relay": "0.5.5", 47 | "graphql-resolvers": "0.2.2", 48 | "graphql-tools": "3.1.1", 49 | "graphql-tools-types": "1.1.26", 50 | "json5": "2.1.0", 51 | "jsonwebtoken": "8.5.1", 52 | "lodash": "4.17.11", 53 | "mongoose": "5.3.4", 54 | "node-fetch": "2.3.0", 55 | "node-mocks-http": "1.7.3", 56 | "override-require": "1.1.1", 57 | "primus": "7.2.3", 58 | "require-from-string": "2.0.2", 59 | "url": "0.10.3", 60 | "uuid": "3.3.2", 61 | "winston": "3.2.1", 62 | "winston-papertrail": "1.0.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "allowSyntheticDefaultImports": true, 5 | "target": "es2017", 6 | "lib": ["esnext", "dom"], // Re:dom https://github.com/graphcool/graphql-request/issues/26#issuecomment-354482903 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "strictNullChecks": true, 10 | "alwaysStrict": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitThis": true, 13 | "noImplicitReturns": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "strict": false, 17 | "pretty": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.9.1" 4 | os: 5 | - linux 6 | - osx 7 | sudo: false 8 | addons: 9 | apt: 10 | sources: 11 | - ubuntu-toolchain-r-test 12 | packages: 13 | - g++-4.8 14 | code_climate: 15 | repo_token: ae474865d00b66dab32c385b1799e62a27442b93ea28e655da358d7e0d8587a4 16 | osx_image: xcode8 17 | before_install: 18 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8; fi 19 | after_script: 20 | - npm run codeclimate -------------------------------------------------------------------------------- /web/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Start a dev server", 11 | "program": "${workspaceRoot}/node_modules/gatsby/dist/bin/cli.js", 12 | "args": [ 13 | "develop" 14 | ], 15 | "env":{ 16 | "REDUX_DEVTOOLS": "true" 17 | } 18 | }, 19 | { 20 | "type": "node", 21 | "request": "launch", 22 | "name": "Build a static site", 23 | "program": "${workspaceRoot}/node_modules/gatsby/dist/bin/cli.js", 24 | "args": [ 25 | "build" 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "xo.enable": true, 3 | "editor.formatOnSave": true 4 | } -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Gatsby 2.0 starter 2 | -------------------------------------------------------------------------------- /web/data/author.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "Fabien BERNARD", 4 | "bio": "JavaScript Developer with ♥ #Craftman #Lean #JS #FRP. Member at @GatsbyJS.", 5 | "avatar": "avatars/fabien0102.jpg", 6 | "twitter": "@fabien0102", 7 | "github": "@fabien0102" 8 | } 9 | ] -------------------------------------------------------------------------------- /web/data/avatars/fabien0102.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/data/avatars/fabien0102.jpg -------------------------------------------------------------------------------- /web/data/blog/2017-04-18--welcoming/pexels-photo-253092.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/data/blog/2017-04-18--welcoming/pexels-photo-253092.jpeg -------------------------------------------------------------------------------- /web/data/blog/2017-05-02--article-1/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Article #1' 3 | createdDate: '2017-05-02' 4 | updatedDate: '2017-05-06' 5 | author: Fabien BERNARD 6 | tags: 7 | - test 8 | image: pexels-photo-59628.jpeg 9 | draft: false 10 | --- 11 | 12 | My awesome article 13 | 14 | ## TODO 15 | 16 | - [x] Replace image 17 | - [ ] Write an awesome article 18 | -------------------------------------------------------------------------------- /web/data/blog/2017-05-02--article-1/pexels-photo-59628.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/data/blog/2017-05-02--article-1/pexels-photo-59628.jpeg -------------------------------------------------------------------------------- /web/data/blog/2017-05-02--article-2/cup-of-coffee-laptop-office-macbook-89786.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/data/blog/2017-05-02--article-2/cup-of-coffee-laptop-office-macbook-89786.jpeg -------------------------------------------------------------------------------- /web/data/blog/2017-05-02--article-2/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Article #2' 3 | createdDate: '2017-05-02' 4 | updatedDate: '2017-05-06' 5 | author: Fabien BERNARD 6 | tags: 7 | - test 8 | image: cup-of-coffee-laptop-office-macbook-89786.jpeg 9 | draft: false 10 | --- 11 | 12 | My awesome article 13 | 14 | ## TODO 15 | 16 | - [x] Replace image 17 | - [ ] Write an awesome article 18 | -------------------------------------------------------------------------------- /web/gatsby-browser.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Provider } from "react-redux"; 3 | 4 | import { store } from "./src/store"; 5 | 6 | export const wrapRootElement = ({ element }) => 7 | 8 | {element} 9 | ; 10 | -------------------------------------------------------------------------------- /web/gatsby-ssr.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Provider } from "react-redux"; 3 | import { renderToString } from "react-dom/server"; 4 | 5 | import { store } from "./src/store"; 6 | 7 | exports.replaceRenderer = ({ bodyComponent, replaceBodyHTMLString }) => { 8 | const ConnectedBody = () => ( 9 | 10 | {bodyComponent} 11 | 12 | ); 13 | replaceBodyHTMLString(renderToString()); 14 | }; 15 | -------------------------------------------------------------------------------- /web/src/components/BlogPagination/BlogPagination.stories.tsx: -------------------------------------------------------------------------------- 1 | /* tslint:disable no-var-requires */ 2 | const withReadme = (require("storybook-readme/with-readme") as any).default; 3 | const BlogPaginationReadme = require("./README.md"); 4 | 5 | import * as React from "react"; 6 | import { storiesOf } from "@storybook/react"; 7 | import { action } from "@storybook/addon-actions"; 8 | import { withKnobs, number } from "@storybook/addon-knobs"; 9 | import BlogPagination from "./BlogPagination"; 10 | 11 | const LinkStub = ((props: any) => 12 |
{props.children}
) as any; 13 | 14 | storiesOf("BlogPagination", module) 15 | .addDecorator(withReadme(BlogPaginationReadme)) 16 | .addDecorator(withKnobs) 17 | .add("default", () => { 18 | const activePage = number("activePage", 1); 19 | const pathname = `/blog/page/${activePage}/`; 20 | const pageCount = number("pageCount", 10); 21 | 22 | return ( 23 | 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /web/src/components/BlogPagination/BlogPagination.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, configure } from "enzyme"; 2 | import "jest"; 3 | import * as React from "react"; 4 | import BlogPagination from "./BlogPagination"; 5 | 6 | // Configure enzyme with react 16 adapter 7 | const Adapter: any = require("enzyme-adapter-react-16"); 8 | configure({ adapter: new Adapter() }); 9 | 10 | const LinkStub = ((props: any) =>
) as any; 11 | 12 | describe("BlogPagination component", () => { 13 | it("should render nothing if only 1 page", () => { 14 | const pathname: string = "/blog/page/1/"; 15 | const pageCount: number = 1; 16 | 17 | const wrapper = render(); 18 | expect(wrapper).toMatchSnapshot(); 19 | }); 20 | 21 | it("should render correctly 5 pages", () => { 22 | const pathname: string = "/blog/page/2/"; 23 | const pageCount: number = 5; 24 | 25 | const wrapper = render(); 26 | expect(wrapper).toMatchSnapshot(); 27 | }); 28 | 29 | it("should render correctly 10 pages", () => { 30 | const pathname: string = "/blog/page/5/"; 31 | const pageCount: number = 10; 32 | 33 | const wrapper = render(); 34 | expect(wrapper).toMatchSnapshot(); 35 | }); 36 | 37 | it("should render correctly 20 pages", () => { 38 | const pathname: string = "/blog/page/5/"; 39 | const pageCount: number = 20; 40 | 41 | const wrapper = render(); 42 | expect(wrapper).toMatchSnapshot(); 43 | }); 44 | 45 | it("should have first link active if no match", () => { 46 | const pathname: string = "/plop"; 47 | const pageCount: number = 10; 48 | 49 | const wrapper = render(); 50 | expect(wrapper).toMatchSnapshot(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /web/src/components/BlogPagination/BlogPagination.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GatsbyLinkProps } from "gatsby-link"; 3 | import { Menu } from "semantic-ui-react"; 4 | import { times } from "lodash"; 5 | 6 | interface BlogPaginationProps extends React.HTMLProps { 7 | pathname: string; 8 | Link: React.ComponentClass>; 9 | pageCount: number; 10 | } 11 | 12 | export default (props: BlogPaginationProps) => { 13 | if (props.pageCount === 1) { return null; } 14 | const activeItem = props.pathname.startsWith("/blog/page/") 15 | ? props.pathname.split("/")[3] 16 | : "1"; 17 | 18 | return ( 19 | 20 | {times(props.pageCount, (index) => { 21 | const pageIndex = (index + 1).toString(); 22 | 23 | const rangeStep = props.pageCount < 10 ? 5 : 3; 24 | const isInRange = (+pageIndex - rangeStep < +activeItem && +pageIndex + rangeStep > +activeItem); 25 | const isLastPage = (+pageIndex === props.pageCount); 26 | const isFirstPage = (+pageIndex === 1); 27 | if (isInRange || isFirstPage || isLastPage) { 28 | return ( 29 | 37 | ); 38 | } else { 39 | return (+pageIndex === props.pageCount - 1 || +pageIndex === 2) 40 | ? ... 41 | : null; 42 | } 43 | })} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /web/src/components/BlogPagination/README.md: -------------------------------------------------------------------------------- 1 | # BlogPagination component 2 | 3 | Blog pagination component. 4 | 5 | ## Source 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/src/components/BlogTitle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Header, Segment, Icon } from "semantic-ui-react"; 3 | 4 | export default () => { 5 | return ( 6 | 7 |
8 | 9 | 10 | Blog 11 | 12 | All about this starter kit 13 | 14 | 15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /web/src/components/HeaderMenu/HeaderMenu.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow, configure } from "enzyme" 2 | import "jest" 3 | import * as React from "react" 4 | import { HeaderMenu } from "./HeaderMenu" 5 | 6 | // Configure enzyme with react 16 adapter 7 | const Adapter: any = require("enzyme-adapter-react-16") 8 | configure({ adapter: new Adapter() }) 9 | 10 | const items = [ 11 | { name: "Home", path: "/", exact: true }, 12 | { name: "About", path: "/about/", exact: true }, 13 | { name: "Blog", path: "/blog/", exact: false }, 14 | ] 15 | 16 | const LinkStub = (props: any) =>
17 | const dispatchStub = (a: any) => a 18 | 19 | describe("HeaderMenu component", () => { 20 | it("should nothing active", () => { 21 | const wrapper = shallow() 22 | expect(wrapper.find({ active: true }).length).toBe(0) 23 | }) 24 | 25 | it("should have about as active (match exact)", () => { 26 | const wrapper = shallow() 27 | expect(wrapper.find({ name: "About" }).prop("active")).toBeTruthy() 28 | }) 29 | 30 | it("should have blog as active (match not exact)", () => { 31 | const wrapper = shallow() 32 | expect(wrapper.find({ name: "Blog" }).prop("active")).toBeTruthy() 33 | }) 34 | 35 | it("should have inverted style", () => { 36 | const wrapper = shallow( 37 | 38 | ) 39 | expect(wrapper.find({ inverted: true }).length).toBe(1) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /web/src/components/HeaderMenu/HeaderMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Dispatch } from "redux"; 4 | import { toggleSidebar } from "../../store"; 5 | import { Container, Label, Menu, Icon } from "semantic-ui-react"; 6 | import { MenuProps } from "../Menu"; 7 | 8 | interface HeaderMenuProps extends MenuProps { 9 | dispatch?: Dispatch; 10 | inverted?: boolean; 11 | } 12 | 13 | export const HeaderMenu = ({ items, pathname, Link, inverted, dispatch }: HeaderMenuProps) => 14 | 15 | 16 | dispatch && dispatch(toggleSidebar())} /> 17 | 18 | {items.map((item) => { 19 | const active = (item.exact) ? pathname === item.path : pathname.startsWith(item.path); 20 | return ; 28 | })} 29 | 30 | ; 31 | 32 | export default connect()(HeaderMenu); 33 | -------------------------------------------------------------------------------- /web/src/components/HeaderMenu/README.md: -------------------------------------------------------------------------------- 1 | # HeaderMenu component 2 | 3 | Header menu component. 4 | 5 | ## Source 6 | 7 | 8 | 9 | ## Example of items 10 | 11 | const items = [ 12 | {name: "Home", path: "/", exact: true}, 13 | {name: "About", path: "/about/", exact: true}, 14 | {name: "Blog", path: "/blog/", exact: false}, 15 | ]; 16 | 17 | If `exact` is `false`, any `pathname` that starts with `path` will provide an active item. 18 | -------------------------------------------------------------------------------- /web/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby"; 2 | import * as React from "react"; 3 | import HeaderMenu from "./HeaderMenu/HeaderMenu"; 4 | import SidebarMenu from "./SidebarMenu/SidebarMenu"; 5 | import { Segment, Icon, Container, Sidebar } from "semantic-ui-react"; 6 | import "../css/styles.css"; 7 | import "../css/responsive.css"; 8 | import "../css/semantic.min.css"; 9 | import "prismjs/themes/prism-okaidia.css"; 10 | import { Provider } from "react-redux"; 11 | import { store } from "../store"; 12 | 13 | export const menuItems = [ 14 | { name: "Home", path: "/", exact: true, icon: "home", inverted: true }, 15 | { name: "About", path: "/about/", exact: true, icon: "info circle" }, 16 | { name: "Blog", path: "/blog/", exact: false, icon: "newspaper" }, 17 | ]; 18 | 19 | export interface LayoutProps { 20 | location: { 21 | pathname: string; 22 | }; 23 | children: any; 24 | } 25 | 26 | const Layout = (props: LayoutProps) => { 27 | const { pathname } = props.location; 28 | const isHome = pathname === "/"; 29 | 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | {/* Header */} 38 | {isHome ? null : } 43 | 44 | {/* Render children pages */} 45 |
46 | {props.children} 47 |
48 | 49 | {/* Footer */} 50 | 51 | 52 |

Powered with by Gatsby 2.0

53 |
54 |
55 |
56 |
57 |
58 | ); 59 | }; 60 | 61 | export default Layout; 62 | 63 | export const withLayout =

(WrappedComponent: React.ComponentType

) => 64 | class WithLayout extends React.Component

{ 65 | render() { 66 | return ( 67 | 68 | 69 | 70 | ); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /web/src/components/Menu.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GatsbyLinkProps } from "gatsby-link"; 3 | 4 | export interface MenuItem { 5 | name: string; 6 | path: string; 7 | exact: boolean; 8 | icon?: string; 9 | inverted?: boolean; 10 | } 11 | 12 | export interface MenuProps extends React.HTMLProps { 13 | items: MenuItem[]; 14 | pathname: string; 15 | Link: React.ComponentClass> | any; 16 | } 17 | -------------------------------------------------------------------------------- /web/src/components/SidebarMenu/README.md: -------------------------------------------------------------------------------- 1 | # SidebarMenu component 2 | 3 | Sidebar menu component. 4 | 5 | ## Source 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/src/components/SidebarMenu/SidebarMenu.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, configure } from "enzyme"; 2 | import "jest"; 3 | import * as React from "react"; 4 | import { SidebarMenu } from "./SidebarMenu"; 5 | 6 | // Configure enzyme with react 16 adapter 7 | const Adapter: any = require("enzyme-adapter-react-16"); 8 | configure({ adapter: new Adapter() }); 9 | 10 | const items = [ 11 | { name: "Home", path: "/", exact: true }, 12 | { name: "About", path: "/about/", exact: true }, 13 | { name: "Blog", path: "/blog/", exact: false }, 14 | ]; 15 | 16 | const LinkStub: any = (props: any) =>

; 17 | 18 | describe("SidebarMenu component", () => { 19 | it("should render correctly", () => { 20 | 21 | const wrapper = render(); 22 | expect(wrapper).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /web/src/components/SidebarMenu/SidebarMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Dispatch } from "redux"; 4 | import { GatsbyLinkProps } from "gatsby-link"; 5 | import { StoreState } from "../../store"; 6 | import { MenuProps, MenuItem } from "../Menu"; 7 | import { Menu, Icon, Sidebar } from "semantic-ui-react"; 8 | import { SemanticICONS } from "semantic-ui-react"; 9 | 10 | interface SidebarMenuProps extends MenuProps { 11 | visible?: boolean; 12 | dispatch?: Dispatch; 13 | Link: React.ComponentClass>; 14 | } 15 | 16 | export const SidebarMenu = ({ items, pathname, Link, visible }: SidebarMenuProps) => { 17 | const isActive = (item: MenuItem) => (item.exact) ? pathname === item.path : pathname.startsWith(item.path); 18 | const activeItem = items.find((item: MenuItem) => isActive(item)) || {} as MenuItem; 19 | return ( 20 | 22 | {items.map((item) => { 23 | const active = isActive(item); 24 | return ( 25 | 26 | 27 | {item.name} 28 | 29 | ); 30 | })} 31 | 32 | ); 33 | }; 34 | 35 | const mapStateToProps = (state: StoreState) => ({ 36 | visible: state.isSidebarVisible, 37 | }); 38 | 39 | export default connect(mapStateToProps)(SidebarMenu); 40 | -------------------------------------------------------------------------------- /web/src/components/TagsCard/README.md: -------------------------------------------------------------------------------- 1 | # TagsCard component 2 | 3 | Component to have a pretty tags list from all posts. 4 | 5 | ## Source 6 | 7 | 8 | 9 | ## With tag property 10 | 11 | 12 | 13 | ## GraphQL query 14 | 15 | { 16 | # Get tags 17 | tags: allMarkdownRemark(frontmatter: {draft: {ne: true}}) { 18 | groupBy(field: frontmatter___tags) { 19 | fieldValue 20 | totalCount 21 | } 22 | } 23 | } 24 | 25 | [open in graphiql](http://localhost:8000/graphql?query=%7B%0A%20%20%23%20Get%20tags%0A%20%20tags%3A%20allMarkdownRemark(frontmatter%3A%20%7Bdraft%3A%20%7Bne%3A%20true%7D%7D)%20%7B%0A%20%20%20%20groupBy(field%3A%20frontmatter___tags)%20%7B%0A%20%20%20%20%20%20fieldValue%0A%20%20%20%20%20%20totalCount%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D) (local only) 26 | -------------------------------------------------------------------------------- /web/src/components/TagsCard/TagsCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow, configure } from "enzyme"; 2 | import "jest"; 3 | import * as React from "react"; 4 | import TagsCard from "./TagsCard"; 5 | 6 | import { Card, List } from "semantic-ui-react"; 7 | import { markdownRemarkGroupConnectionConnection } from "../../graphql-types"; 8 | 9 | // Configure enzyme with react 16 adapter 10 | const Adapter: any = require("enzyme-adapter-react-16"); 11 | configure({ adapter: new Adapter() }); 12 | 13 | describe("TagsCard component", () => { 14 | let LinkStub: any; 15 | 16 | beforeEach(() => { 17 | LinkStub = (props: any) => 18 |
{props.children}
; 19 | }); 20 | 21 | it("should list all the tags", () => { 22 | const tags = [ 23 | { fieldValue: "tag01", totalCount: 2 }, 24 | { fieldValue: "tag02", totalCount: 4 }, 25 | { fieldValue: "tag03", totalCount: 6 }, 26 | ] as markdownRemarkGroupConnectionConnection[]; 27 | 28 | const wrapper = shallow(); 29 | 30 | expect(wrapper.find(List.Item)).toHaveLength(3); 31 | }); 32 | 33 | it("should have on tag active", () => { 34 | const tags = [ 35 | { fieldValue: "tag01", totalCount: 2 }, 36 | { fieldValue: "tag02", totalCount: 4 }, 37 | { fieldValue: "tag03", totalCount: 6 }, 38 | ] as markdownRemarkGroupConnectionConnection[]; 39 | 40 | const wrapper = shallow(); 41 | 42 | expect(wrapper).toMatchSnapshot(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /web/src/components/TagsCard/TagsCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GatsbyLinkProps } from "gatsby-link"; 3 | import { Card, List } from "semantic-ui-react"; 4 | import { markdownRemarkGroupConnectionConnection } from "../../graphql-types"; 5 | 6 | interface TagsCardProps extends React.HTMLProps { 7 | tags: markdownRemarkGroupConnectionConnection[]; 8 | Link: React.ComponentClass>; 9 | tag?: string; 10 | } 11 | 12 | export default (props: TagsCardProps) => { 13 | return ( 14 | 15 | 16 | 17 | Tags 18 | 19 | 20 | 21 | 22 | {props.tags.map((tag) => { 23 | const isActive = tag.fieldValue === props.tag; 24 | const activeStyle = { 25 | fontWeight: "700", 26 | }; 27 | const tagLink = isActive ? `/blog` : `/blog/tags/${tag.fieldValue}/`; 28 | return ( 29 | 30 | 31 | 32 | 33 | {tag.fieldValue} ({tag.totalCount}) 34 | 35 | 36 | 37 | ); 38 | })} 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /web/src/components/TagsCard/__snapshots__/TagsCard.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TagsCard component should have on tag active 1`] = `ShallowWrapper {}`; 4 | -------------------------------------------------------------------------------- /web/src/css/fonts/horta-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/src/css/fonts/horta-webfont.woff -------------------------------------------------------------------------------- /web/src/css/fonts/horta-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/src/css/fonts/horta-webfont.woff2 -------------------------------------------------------------------------------- /web/src/css/themes/default/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/src/css/themes/default/assets/fonts/icons.eot -------------------------------------------------------------------------------- /web/src/css/themes/default/assets/fonts/icons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/src/css/themes/default/assets/fonts/icons.otf -------------------------------------------------------------------------------- /web/src/css/themes/default/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/src/css/themes/default/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /web/src/css/themes/default/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/src/css/themes/default/assets/fonts/icons.woff -------------------------------------------------------------------------------- /web/src/css/themes/default/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/src/css/themes/default/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /web/src/css/themes/default/assets/images/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danger/peril/4c7109895ee956183411a3cb635942825d58bd97/web/src/css/themes/default/assets/images/flags.png -------------------------------------------------------------------------------- /web/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | // package.json 2 | declare module "*/package.json" { 3 | export const version: string 4 | export const author: string 5 | } 6 | 7 | declare const graphql: (query: TemplateStringsArray) => void 8 | -------------------------------------------------------------------------------- /web/src/html.tsx: -------------------------------------------------------------------------------- 1 | /* tslint:disable no-var-requires */ 2 | /* tslint:disable no-console */ 3 | 4 | import * as React from "react" 5 | import Helmet from "react-helmet" 6 | 7 | const config = require("../gatsby-config.js") 8 | 9 | interface HtmlProps { 10 | body: any 11 | postBodyComponents: any 12 | headComponents: any 13 | } 14 | 15 | export default (props: HtmlProps) => { 16 | const head = Helmet.rewind() 17 | 18 | const verification = 19 | config.siteMetadata && config.siteMetadata.googleVerification ? ( 20 | 21 | ) : null 22 | 23 | return ( 24 | 25 | 26 | {props.headComponents} 27 | My website 28 | 29 | 30 | 31 | {head.title.toComponent()} 32 | {head.meta.toComponent()} 33 | {head.link.toComponent()} 34 | {verification} 35 | 36 | 37 |
38 | {props.postBodyComponents} 39 | 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /web/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Header, Icon, Grid } from "semantic-ui-react"; 3 | import {withLayout} from "../components/Layout"; 4 | 5 | const NotFoundPage = () => 6 | 11 | 12 | 13 | 14 |
You are here!
15 |
But nothing found for you #404
16 |
17 |
18 |
; 19 | 20 | export default withLayout(NotFoundPage); 21 | -------------------------------------------------------------------------------- /web/src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Header, Container, Segment, Icon } from "semantic-ui-react"; 3 | import {withLayout} from "../components/Layout"; 4 | 5 | const AboutPage = () => { 6 | return ( 7 | 8 | 9 |
10 | 11 | 12 | About 13 | 14 |
15 |
16 | 17 |

18 | This starter was created by @fabien0102. 19 |

20 |

21 | For any question, I'm on discord #reactiflux/gatsby 22 |

23 |

24 | For any issues, any PR are welcoming 25 | on this repository 26 |

27 |
28 |
29 | ); 30 | }; 31 | 32 | export default withLayout(AboutPage); 33 | -------------------------------------------------------------------------------- /web/src/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux" 2 | import { devToolsEnhancer } from "redux-devtools-extension" 3 | import { get } from "lodash" 4 | 5 | export interface StoreState { 6 | isSidebarVisible: boolean 7 | } 8 | 9 | // Actions 10 | export const TOGGLE_SIDEBAR = "TOGGLE_SIDEBAR" 11 | export type TOGGLE_SIDEBAR = typeof TOGGLE_SIDEBAR 12 | export interface ToggleSidebar { 13 | type: TOGGLE_SIDEBAR 14 | } 15 | export const toggleSidebar = () => ({ type: TOGGLE_SIDEBAR }) 16 | 17 | // Reducer 18 | export const reducer = (state: StoreState, action: ToggleSidebar): StoreState => { 19 | switch (action.type) { 20 | case TOGGLE_SIDEBAR: 21 | return Object.assign({}, state, { isSidebarVisible: !state.isSidebarVisible }) 22 | default: 23 | return state 24 | } 25 | } 26 | 27 | // Store 28 | export const initialState: StoreState = { isSidebarVisible: false } 29 | export const store = createStore(reducer, initialState, devToolsEnhancer({})) 30 | -------------------------------------------------------------------------------- /web/src/templates/blog-page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Blog from "../pages/blog" 3 | import { graphql } from "gatsby" 4 | 5 | export default Blog 6 | 7 | export const pageQuery = graphql` 8 | query TemplateBlogPage($skip: Int) { 9 | # Get tags 10 | tags: allMarkdownRemark(filter: { frontmatter: { draft: { ne: true } } }) { 11 | group(field: frontmatter___tags) { 12 | fieldValue 13 | totalCount 14 | } 15 | } 16 | 17 | # Get posts 18 | posts: allMarkdownRemark( 19 | sort: { order: DESC, fields: [frontmatter___updatedDate] } 20 | filter: { frontmatter: { draft: { ne: true } }, fileAbsolutePath: { regex: "/blog/" } } 21 | limit: 10 22 | skip: $skip 23 | ) { 24 | totalCount 25 | edges { 26 | node { 27 | excerpt 28 | timeToRead 29 | fields { 30 | slug 31 | } 32 | frontmatter { 33 | title 34 | updatedDate(formatString: "DD MMMM, YYYY") 35 | image { 36 | children { 37 | ... on ImageSharp { 38 | fixed(width: 700, height: 100) { 39 | src 40 | srcSet 41 | } 42 | } 43 | } 44 | } 45 | author { 46 | id 47 | avatar { 48 | children { 49 | ... on ImageSharp { 50 | fixed(width: 35, height: 35) { 51 | src 52 | srcSet 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | ` 64 | -------------------------------------------------------------------------------- /web/src/templates/tags-page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Blog from "../pages/blog" 3 | import { graphql } from "gatsby" 4 | 5 | export default Blog 6 | 7 | export const pageQuery = graphql` 8 | query TemplateTagPage($tag: String) { 9 | # Get tags 10 | tags: allMarkdownRemark(filter: { frontmatter: { draft: { ne: true } } }) { 11 | group(field: frontmatter___tags) { 12 | fieldValue 13 | totalCount 14 | } 15 | } 16 | 17 | # Get posts 18 | posts: allMarkdownRemark( 19 | sort: { order: DESC, fields: [frontmatter___updatedDate] } 20 | filter: { frontmatter: { draft: { ne: true }, tags: { in: [$tag] } }, fileAbsolutePath: { regex: "/blog/" } } 21 | ) { 22 | totalCount 23 | edges { 24 | node { 25 | excerpt 26 | timeToRead 27 | fields { 28 | slug 29 | } 30 | frontmatter { 31 | title 32 | updatedDate(formatString: "DD MMMM, YYYY") 33 | image { 34 | children { 35 | ... on ImageSharp { 36 | fixed(width: 700, height: 100) { 37 | src 38 | srcSet 39 | } 40 | } 41 | } 42 | } 43 | author { 44 | id 45 | avatar { 46 | children { 47 | ... on ImageSharp { 48 | fixed(width: 35, height: 35) { 49 | src 50 | srcSet 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | ` 62 | -------------------------------------------------------------------------------- /web/test/__mocks__/path.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const path = jest.genMockFromModule('path'); 3 | 4 | path.resolve = (...pathSegment) => 5 | ['base-path', ...pathSegment].join('/'); 6 | 7 | module.exports = path; 8 | -------------------------------------------------------------------------------- /web/test/data-integrity.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, max-nested-callbacks */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const matter = require('gray-matter'); 5 | const _ = require('lodash'); 6 | const authors = require('../data/author.json'); 7 | 8 | describe('data integrity', () => { 9 | describe('authors', () => { 10 | const requiredFields = ['id', 'bio', 'avatar', 'twitter', 'github']; 11 | 12 | authors.forEach(author => { 13 | describe(`${author.id}`, () => { 14 | // Check required fields 15 | requiredFields.forEach(field => { 16 | it(`should have ${field} field`, () => { 17 | expect(Object.keys(author).includes(field)).toBeTruthy(); 18 | }); 19 | }); 20 | 21 | // Check if avatar image is in the repo 22 | it('should have avatar image in the repo', () => { 23 | const avatarPath = path.join('data/', author.avatar); 24 | expect(fs.existsSync(avatarPath)).toBeTruthy(); 25 | }); 26 | }); 27 | }); 28 | }); 29 | 30 | describe('blog posts', () => { 31 | const posts = fs.readdirSync('data/blog'); 32 | const validators = [ 33 | {key: 'title', validator: _.isString}, 34 | {key: 'createdDate', validator: val => _.isDate(new Date(val))}, 35 | {key: 'updatedDate', validator: val => _.isDate(new Date(val))}, 36 | {key: 'author', validator: val => _.map(authors, 'id').includes(val)}, 37 | {key: 'tags', validator: _.isArray}, 38 | {key: 'image', validator: (val, post) => fs.existsSync(`data/blog/${post}/${val}`)}, 39 | {key: 'draft', validator: _.isBoolean} 40 | ]; 41 | 42 | posts.forEach(post => { 43 | describe(`${post}`, () => { 44 | const {data} = matter.read(`data/blog/${post}/index.md`); 45 | validators.forEach(field => { 46 | it(`should have correct format for ${field.key}`, () => { 47 | expect(field.validator(data[field.key], post)).toBeTruthy(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /web/tools/update-post-date.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const slash = require('slash'); 5 | const matter = require('gray-matter'); 6 | 7 | // Get files given by lint-staged (*.md files into staged) 8 | process.argv.slice(3).forEach(dirtyPath => { 9 | // Make sure it will works on windows 10 | const path = slash(dirtyPath); 11 | 12 | // Only parse blog posts 13 | if (!path.includes('/data/blog/')) { 14 | return; 15 | } 16 | 17 | // Get file from file system and parse it with gray-matter 18 | const orig = fs.readFileSync(path, 'utf-8'); 19 | const parsedFile = matter(orig); 20 | 21 | // Get current date and update `updatedDate` data 22 | const updatedDate = new Date().toISOString().split('T')[0]; 23 | const updatedData = Object.assign({}, parsedFile.data, {updatedDate}); 24 | 25 | // Recompose content and updated data 26 | const updatedContent = matter.stringify(parsedFile.content, updatedData); 27 | 28 | // Update file 29 | fs.writeFileSync(path, updatedContent, {encoding: 'utf-8'}); 30 | }); 31 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "esnext", 8 | "jsx": "react", 9 | "lib": ["dom", "esnext"] 10 | }, 11 | "include": [ 12 | "./src/**/*" 13 | ] 14 | } -------------------------------------------------------------------------------- /web/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "interface-name": [ "never-prefix" ], 9 | "member-access": [ 10 | false 11 | ], 12 | "ordered-imports": [ 13 | false 14 | ], 15 | "no-console": [ 16 | false 17 | ], 18 | "no-var-requires": false 19 | }, 20 | "rulesDirectory": [] 21 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------