From 33a28c153580dd9f724a83419cea64ebad3f0aba Mon Sep 17 00:00:00 2001 From: Chris Clarke Date: Fri, 2 Jul 2021 08:10:52 -0700 Subject: [PATCH] Add CI Pipelines for CircleCI (#1178) * add firebase deploy to circleci * ci filters * ci * ci params * ci * ci * add cross-origin support for images * crossorigin changes * update service worker cors * update fetch options * workflows update * ci * ci approval step * slack notification test * test slack ci * ci * ci * custom deployment message * add deploy success message * ci application credentials and contexts * add circle-ci deployment docs * ci * ci * ci * build fix * build testing * yarn cache clear * ci * ci * ci * remove test refs * update cache paths * update env variables * env updates * ci --- .circleci/config.yml | 281 +++++++++++++++++- .env | 1 - functions/src/config/cors.md | 5 + .../docs/Deployment/circle-ci.md | 77 +++++ packages/documentation/sidebars.js | 3 + public/index.html | 1 + src/components/ImageGallery/index.tsx | 7 +- .../HowtoDescription/HowtoDescription.tsx | 3 +- .../Howto/Content/HowtoList/HowToCard.tsx | 1 + .../common/UploadedFile/ImagePreview.tsx | 1 + src/service-worker.ts | 5 + 11 files changed, 366 insertions(+), 19 deletions(-) create mode 100644 packages/documentation/docs/Deployment/circle-ci.md diff --git a/.circleci/config.yml b/.circleci/config.yml index 7f9f8d6b16..8ccae6765a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,12 @@ version: 2.1 +###################################################################################################### +# Pre-Requisites +# +# In order to use these scripts various env variables need to be set on CircleCI +# See `packages/documentation/docs/Deployment/circle-ci.md` for more information +# +# For general config info see: https://circleci.com/docs/2.0/configuration-reference +###################################################################################################### ###################################################################################################### # Orbs - preconfigured environments for running specific jobs @@ -9,6 +17,8 @@ version: 2.1 orbs: # for use with cimg image, to install web browsers browser-tools: circleci/browser-tools@1.1.3 + # used to enable slack integration (required api key set in environment) + slack: circleci/slack@4.4.2 ###################################################################################################### # Aliases - code snippets that can be included inline in any other markup @@ -25,11 +35,11 @@ aliases: keys: # https://circleci.com/docs/2.0/caching/ # https://circleci.com/docs/2.0/yarn/ - # when lock file changes, use increasingly general patterns to restore cache + # when lock file changes, use increasingly general patterns to restore cache (also need to change `save_cache` path) # NOTE - if changing base image or received cache permission denied may need to bump version v3->v4 or similar (clears after 15d) - - yarn-packages-v8-{{ .Branch }}-{{ checksum "yarn.lock" }} - - yarn-packages-v8-{{ .Branch }}- - - yarn-packages-v8- + - yarn-packages-v9-{{ .Branch }}-{{ checksum "yarn.lock" }} + - yarn-packages-v9-{{ .Branch }}- + - yarn-packages-v9- - &install_packages run: name: Install Packages @@ -50,7 +60,117 @@ aliases: # - ~/.yarn/berry/cache # local cache location - ~/project/.yarn/cache - key: yarn-packages-v8-{{ .Branch }}-{{ checksum "yarn.lock" }} + key: yarn-packages-v9-{{ .Branch }}-{{ checksum "yarn.lock" }} + + - &filter_only_production + filters: + branches: + only: + - production + - &filter_only_master + filters: + branches: + only: + - master + - &filter_only_ci_test + filters: + branches: + only: + - ci/circle-ci-release + + - &slack_custom_hold_message + # Message shown in slack to approve new deployment - edit in https://app.slack.com/block-kit-builder + # Based on: https://github.com/CircleCI-Public/slack-orb/tree/master/src/message_templates + custom: | + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": ":rocket: New Update - Ready for Launch", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Project*: $CIRCLE_PROJECT_REPONAME" + } + ], + "accessory": { + "type": "image", + "image_url": "https://yt3.ggpht.com/ytc/AAUvwni_34CcLQsIhNo1d1A2zUK0pNPzipCV9oM1gvkNNw=s900-c-k-c0x00ffffff-no-rj", + "alt_text": "Logo" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Go To Approval" + }, + "url": "https://circleci.com/workflow-run/${CIRCLE_WORKFLOW_ID}" + } + ] + } + ] + } + + - &slack_custom_success_message + custom: | + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "Deploy Success :tada:", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Job*: ${CIRCLE_JOB}" + }, + { + "type": "mrkdwn", + "text": "*When*: $(date +'%m/%d/%Y %T')" + }, + { + "type": "mrkdwn", + "text": "*Tag*: $CIRCLE_TAG" + } + ], + "accessory": { + "type": "image", + "image_url": "https://yt3.ggpht.com/ytc/AAUvwni_34CcLQsIhNo1d1A2zUK0pNPzipCV9oM1gvkNNw=s900-c-k-c0x00ffffff-no-rj", + "alt_text": "CircleCI logo" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Build Logs" + }, + "url": "${CIRCLE_BUILD_URL}" + } + ] + } + ] + } ###################################################################################################### # Commands - Reusable collections of steps @@ -65,6 +185,45 @@ commands: - *restore_yarn_cache - *install_packages - *save_yarn_cache + firebase_deploy: + description: Deploy to Firebase + parameters: + token: + type: string + default: '' + description: Firebase Deploy Token + use_application_credentials: + type: boolean + default: false + description: Specify if GOOGLE_APPLICATION_CREDENTIALS_JSON var used instead of firebase token + alias: + type: string + default: 'default' + description: Firebase project alias to deploy to + steps: + - run: + name: Install Firebase Tools + command: yarn add -D firebase-tools + - when: + condition: <> + steps: + - run: + name: Export application credentials + command: echo $GOOGLE_APPLICATION_CREDENTIALS_JSON > service_account.json + - run: + name: Deploy to Firebase + command: ./node_modules/.bin/firebase deploy -P << parameters.alias >> + environment: + GOOGLE_APPLICATION_CREDENTIALS: service_account.json + - when: + condition: + not: <> + steps: + - run: + name: Deploy to Firebase + command: ./node_modules/.bin/firebase deploy --token=<< parameters.token >> -P << parameters.alias >> + +examples: ###################################################################################################### # Jobs - Independently specified lists of tasks and environments for execution @@ -85,16 +244,47 @@ jobs: # optional environment variables to set during build process BUILD_ENV: type: string - default: "" + default: '' steps: # whilst checkout-install could be persisted from previous step, that is less efficient than just using caching - setup_repo - run: command: << parameters.BUILD_ENV >> npm run build + environment: + # Include env variable for tracking current branch in build + REACT_APP_BRANCH: $CIRCLE_BRANCH - persist_to_workspace: root: . paths: - build + deploy: + docker: *docker + parameters: + # optional environment variables to set during build process + DEPLOY_ALIAS: + type: string + default: 'default' + NOTIFY_SLACK: + type: boolean + default: true + steps: + - setup_repo + - attach_workspace: + at: '.' + - firebase_deploy: + # token: $FIREBASE_DEPLOY_TOKEN # This should be set as environment variable + alias: << parameters.DEPLOY_ALIAS >> + use_application_credentials: true + - when: + condition: << parameters.NOTIFY_SLACK >> + steps: + - slack/notify: + event: fail + mentions: '@Chris Clarke' + template: basic_fail_1 + - slack/notify: + event: pass + <<: *slack_custom_success_message # Run cypress e2e tests on chrome and firefox test_e2e: @@ -110,21 +300,20 @@ jobs: - setup_repo # retrieve build folder - attach_workspace: - at: "." + at: '.' # install testing browsers are required - when: condition: - equal: ["chrome", << parameters.CI_BROWSER >>] + equal: ['chrome', << parameters.CI_BROWSER >>] steps: - browser-tools/install-chrome - when: condition: - equal: ["firefox", << parameters.CI_BROWSER >>] + equal: ['firefox', << parameters.CI_BROWSER >>] steps: - browser-tools/install-firefox # call main testing script - run: - # TODO - CC 2021-02-24 `npm run test ci prod` a bit flaky, should be worked on and then used command: npm run test ci prod environment: CI_BROWSER: << parameters.CI_BROWSER >> @@ -136,21 +325,81 @@ jobs: ###################################################################################################### workflows: version: 2 - build_and_test: + main_workflow: # by default jobs will run concurrently, so specify requires if want to run sequentially jobs: + #---------------------- Test ---------------------- # Note - when calling test we also let the test script handle building as it injects random variables for seeding the DB - build: - name: Build Production + name: build_test matrix: parameters: BUILD_ENV: - ["FORCE_COLOR=1 REACT_APP_SITE_VARIANT=test-ci CI=false"] + [ + 'FORCE_COLOR=1 REACT_APP_SITE_VARIANT=test-ci CI=false NODE_OPTIONS=--max-old-space-size=4096', + ] - test_e2e: - requires: - - "build" name: e2e-<< matrix.CI_BROWSER >>-<< matrix.CI_NODE >> + requires: + - 'build_test' matrix: parameters: CI_NODE: [1, 2] - CI_BROWSER: ["chrome", "firefox"] + CI_BROWSER: ['chrome', 'firefox'] + #---------------------- Dev Build-Deploy ---------------------- + - build: + name: build_dev + context: community-platform-dev + requires: + - 'test_e2e' + <<: *filter_only_master + matrix: + parameters: + # Fix memory issues during build (https://github.com/facebook/create-react-app/issues/8096) + # TODO - fix lint errors so that default CI=true works + BUILD_ENV: ['CI=false NODE_OPTIONS=--max-old-space-size=4096'] + - deploy: + name: deploy_dev + requires: + - build_dev + <<: *filter_only_master + DEPLOY_ALIAS: 'default' + NOTIFY_SLACK: false + context: + - circle-ci-slack-context + - community-platform-dev + #---------------------- Production Build-Approve-Deploy ---------------------- + - build: + name: build_production + context: community-platform-production + requires: + - 'test_e2e' + <<: *filter_only_production + matrix: + parameters: + BUILD_ENV: ['CI=false NODE_OPTIONS=--max-old-space-size=4096'] + # Require manual approval on CirclCI website prior to release + - hold: + name: approve_release + type: approval + requires: + - build_production + <<: *filter_only_production + # Send a slack notification to approve the hold above (required config supplied in circleci-slack-context) + - slack/on-hold: + name: notify_slack_pending_approval + context: circle-ci-slack-context + requires: + - build_production + <<: *filter_only_production + <<: *slack_custom_hold_message + - deploy: + name: deploy_production + requires: + - 'approve_release' + <<: *filter_only_production + DEPLOY_ALIAS: 'production' + NOTIFY_SLACK: true + context: + - circle-ci-slack-context + - community-platform-production diff --git a/.env b/.env index 623edaac44..9bacaf574b 100644 --- a/.env +++ b/.env @@ -1,2 +1 @@ -REACT_APP_PLATFORM_VERSION=$npm_package_version FAST_REFRESH=false diff --git a/functions/src/config/cors.md b/functions/src/config/cors.md index 2e2cdd8057..8235c0792a 100644 --- a/functions/src/config/cors.md +++ b/functions/src/config/cors.md @@ -6,3 +6,8 @@ https://cloud.google.com/storage/docs/configuring-cors `gsutil ls` `gsutil cors set src/config/cors.json gs://[project-name].appspot.com` + + + +Also need to remember to configure code to make cross-origin requests (where required, e.g. if using service workers cross-origin) +https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests diff --git a/packages/documentation/docs/Deployment/circle-ci.md b/packages/documentation/docs/Deployment/circle-ci.md new file mode 100644 index 0000000000..126fddfee6 --- /dev/null +++ b/packages/documentation/docs/Deployment/circle-ci.md @@ -0,0 +1,77 @@ +# Deployment via CircleCI +We use CircleCI to handle automated build-test-deploy cycles when PRs and releases are created from the GitHub Repository + + +## Environment Variables +The following environment variables should be set within the [CircleCI Environment](https://circleci.com/docs/2.0/env-vars/), or via [CircleCI Contexts](https://circleci.com/docs/2.0/contexts/) + +### Firebase Deployment +The most secure way to provide the CI system access to deploy to firebase is by creating a service worker account with relevant permissions +and storing the credentials as an environment variable (see this [Github Issue](https://github.com/firebase/firebase-tools/issues/825) for more info) +``` +GOOGLE_APPLICATION_CREDENTIALS_JSON +``` +If using multiple projects (e.g. staging/prodcution) these can be configured in different contexts. + +When configuring a service account the following permissions should be assigned: +``` +Firebase Admin SDK Administrator Service Agent +Cloud Functions Admin +Firebase Hosting Admin +Cloud RuntimeConfig Admin +``` + +Alternatively, a `FIREBASE_TOKEN` environment variable can be created and set (See the [Firebase Docs](https://firebase.google.com/docs/cli#cli-ci-systems)), +however this is less preferable as the token would provide access to all a user's firebase projects + +### Slack Notifications +Send slack notificatons on deploy success/fail/approval-hold: +``` +SLACK_DEFAULT_CHANNEL +SLACK_ACCESS_TOKEN +``` +Currently passed with `circle-ci-slack-context` context +See [circleci slack orb](https://github.com/CircleCI-Public/slack-orb) for info) + +### Runtime Variables +Any variables prefixed with `REACT_APP_` are automatically included with the runtime build. Currently we require: + +Algolia places location lookup +``` +REACT_APP_ALGOLIA_PLACES_API_KEY +REACT_APP_ALGOLIA_PLACES_APP_ID +``` + +Firebase configuration +``` +REACT_APP_FIREBASE_API_KEY +REACT_APP_FIREBASE_AUTH_DOMAIN +REACT_APP_FIREBASE_DATABASE_URL +REACT_APP_FIREBASE_MESSAGING_SENDER_ID +REACT_APP_FIREBASE_PROJECT_ID +REACT_APP_FIREBASE_STORAGE_BUCKET +``` +Sentry error tracking +``` +REACT_APP_SENTRY_DSN +``` +Google Analytics +``` +REACT_APP_GA_TRACKING_ID +``` + +### Misc Variables +Proposed (but not currently implemented) +``` +LIGHTHOUSE_API_KEY +``` + +## Google APIs +To deploy from service_account the following APIs will also need to be enabled for the project: +- [Firebase Hosting API](https://console.cloud.google.com/apis/api/firebasehosting.googleapis.com) + +## Functions Variables +Additional config used in cloud functions has also been included via `firebase functions:config:set` +E.g. `discord_webhook`, `slack_webhook`, + +TODO - This requires further documentation (and possibly merging) \ No newline at end of file diff --git a/packages/documentation/sidebars.js b/packages/documentation/sidebars.js index 95fd761e56..53bc8bca9c 100644 --- a/packages/documentation/sidebars.js +++ b/packages/documentation/sidebars.js @@ -2,6 +2,9 @@ module.exports = { mainSidebar: { Home: ['doc1'], 'Getting Started': [], + 'Continuous Integration': [ + 'Deployment/circle-ci', + ], 'Server Maintenance': [ 'Server Maintenance/dataMigration', 'Server Maintenance/manualBackups', diff --git a/public/index.html b/public/index.html index 75d63cf783..73c4e0c54a 100644 --- a/public/index.html +++ b/public/index.html @@ -7,6 +7,7 @@ content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + diff --git a/src/components/ImageGallery/index.tsx b/src/components/ImageGallery/index.tsx index fc2508e3fe..667679f536 100644 --- a/src/components/ImageGallery/index.tsx +++ b/src/components/ImageGallery/index.tsx @@ -86,6 +86,7 @@ export default class ImageGallery extends PureComponent { onClick={() => { this.triggerLightbox() }} + crossOrigin="" /> @@ -99,7 +100,11 @@ export default class ImageGallery extends PureComponent { onClick={() => this.setActive(image)} key={index} > - + )) : null} diff --git a/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx b/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx index 0b695a06df..7f9eeda301 100644 --- a/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx +++ b/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { PureComponent } from 'react' import TagDisplay from 'src/components/Tags/TagDisplay/TagDisplay' import { format } from 'date-fns' import { IHowtoDB } from 'src/models/howto.models' @@ -198,6 +198,7 @@ export default class HowtoDescription extends PureComponent { height: ['100%', '450px'], }} src={howto.cover_image.downloadUrl} + crossOrigin="" alt="how-to cover" /> {howto.moderation !== 'accepted' && ( diff --git a/src/pages/Howto/Content/HowtoList/HowToCard.tsx b/src/pages/Howto/Content/HowtoList/HowToCard.tsx index ba8f69a528..580c7fcd33 100644 --- a/src/pages/Howto/Content/HowtoList/HowToCard.tsx +++ b/src/pages/Howto/Content/HowtoList/HowToCard.tsx @@ -43,6 +43,7 @@ export const HowToCard = (props: IProps) => ( }} threshold={500} src={props.howto.cover_image.downloadUrl} + crossOrigin="" /> diff --git a/src/pages/common/UploadedFile/ImagePreview.tsx b/src/pages/common/UploadedFile/ImagePreview.tsx index e67bc8ed75..ffe3e81ce0 100644 --- a/src/pages/common/UploadedFile/ImagePreview.tsx +++ b/src/pages/common/UploadedFile/ImagePreview.tsx @@ -32,6 +32,7 @@ export default class ImagePreview extends React.Component { src={this.props.imageSrc} alt={this.props.imageAlt} onLoad={this.imageLoaded} + crossOrigin="" /> {this.props.showDelete ? (