I use CircleCI to maintain code quality and automate the deploy process for many of the web applications I manage. I recently sought to achieve that same configuration for my Hubs Cloud instances. This tutorial assumes that you already have a CircleCI account and understand the deploy process of Mozilla Hubs. If you’d like info on deployment, check out this blog post before you get started to get an overview of the manual deploy process.
The process is as simple as the following:
- Make a Production Branch
- Create a Login Script to be used by CircleCI
- Add login script to package.json
- Add
.circleci
directory to root - Add CircleCI
.yml
file inside of.circleci
- Push changes to Production branch
Let’s get into it.
Add a production branch to a private fork of Hubs. This is what will be used as the deployment branch for your live site. I like to keep one fork private for my production instances and a separate public instance mirrored for contribution or sharing code. The goal of this configuration is to allow the deploy process to initiate when a pull request is merged. You can use this same approach to automate any number of tasks.
Create a login script for CircleCI:
Mozilla Hubs Cloud by default has a login script called login.js
that is used as a step in the npm run deploy
command. This command requires human input to provide the Hubs Cloud instance url and administrator email address. We can work around the human input in CircleCI by hard coding our information in a similar script. CAUTION, you don’t want to be showing off your admin email in a public fork so try to utilize circleci environment variables when defining your admin address. For this tutorial example, I’m plain texting them because it will be easier to understand.
Your login script will live in hubs/scripts
alongside the other npm scripts. It’s almost identical to the login.js with a few tweaks to the variables. Below is a copy of mine. Feel free to make your own flavor!
import readline from "readline"; | |
import { connectToReticulum } from "../src/utils/phoenix-utils"; | |
import Store from "../src/storage/store"; | |
import AuthChannel from "../src/utils/auth-channel"; | |
import configs from "../src/utils/configs.js"; | |
import { Socket } from "phoenix-channels"; | |
import { writeFileSync } from "fs"; | |
const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | |
(async () => { | |
console.log("Logging into Hubs Cloud.\n"); | |
const host = "your.hubsinstance.com"; | |
if (!host) { | |
console.log("Invalid host."); | |
process.exit(1); | |
} | |
const url = `https://${host}/api/v1/meta`; | |
try { | |
const res = await fetch(url); | |
const meta = await res.json(); | |
if (!meta.phx_host) { | |
throw new Error(); | |
} | |
} catch (e) { | |
console.log("Sorry, that doesn't look like a Hubs Cloud server."); | |
process.exit(0); | |
} | |
configs.RETICULUM_SERVER = host; | |
configs.RETICULUM_SOCKET_PROTOCOL = "wss:"; | |
const socket = await connectToReticulum(false, null, Socket); | |
const store = new Store(); | |
const email = "[email protected]"; | |
console.log(`Logging into ${host} as ${email}. Click on the link in your email to continue.`); | |
const authChannel = new AuthChannel(store); | |
authChannel.setSocket(socket); | |
const { authComplete } = await authChannel.startAuthentication(email); | |
await authComplete; | |
const { token } = store.state.credentials; | |
const creds = { | |
host, | |
email, | |
token | |
}; | |
writeFileSync(".ret.credentials", JSON.stringify(creds)); | |
rl.close(); | |
console.log("Login successful.\nCredentials written to .ret.credentials. Run npm run logout to remove credentials."); | |
process.exit(0); | |
})(); |
Add your login script to package.json
Next, you’ll need to add this custom login script to package.json
so that the automation has a reference. In line 27 below, I added mine called login-bp
.
{ | |
"name": "hubs", | |
"version": "0.0.1", | |
"description": "Duck-themed multi-user virtual spaces in WebVR.", | |
"main": "src/index.js", | |
"license": "MPL-2.0", | |
"homepage": "https://github.com/mozilla/hubs#readme", | |
"repository": { | |
"type": "git", | |
"url": "https://github.com/mozilla/hubs.git" | |
}, | |
"bugs": { | |
"url": "https://github.com/mozilla/hubs/issues" | |
}, | |
"scripts": { | |
"start": "webpack-dev-server --mode=development --env.loadAppConfig", | |
"dev": "webpack-dev-server --mode=development --env.remoteDev", | |
"local": "webpack-dev-server --mode=development --env.localDev", | |
"build": "rimraf ./dist && webpack --mode=production", | |
"bundle-analyzer": "webpack-dev-server --mode=production --env.dev --env.bundleAnalyzer", | |
"doc": "node ./scripts/doc/build.js", | |
"prettier": "prettier --write '*.js' 'src/**/*.js'", | |
"lint:js": "eslint '*.js' 'scripts/**/*.js' 'src/**/*.js'", | |
"lint:html": "htmlhint 'src/**/*.html' && node scripts/indent-linter.js 'src/**/*.html'", | |
"lint": "npm run lint:js && npm run lint:html", | |
"login": "node -r @babel/register -r esm -r ./scripts/shim scripts/login.js", | |
"login-bp": "node -r @babel/register -r esm -r ./scripts/shim scripts/login-bp.js", | |
"logout": "node -r @babel/register -r esm -r ./scripts/shim scripts/logout.js", | |
"deploy": "node -r @babel/register -r esm -r ./scripts/shim scripts/deploy.js", | |
"undeploy": "node -r @babel/register -r esm -r ./scripts/shim scripts/undeploy.js", | |
"test": "npm run lint && npm run test:unit && npm run build", | |
"test:unit": "ava", | |
"stats": "rimraf ./dist && webpack --mode=production --json", | |
"spritesheet": "npm run spritesheet:system-action && npm run spritesheet:system-notice", | |
"spritesheet:system-action": "spritesheet-js -f json -p src/assets/images/spritesheets/ --padding 8 --divisibleByTwo -n sprite-system-action-spritesheet --powerOfTwo src/assets/images/sprites/action/*", | |
"spritesheet:system-notice": "spritesheet-js -f json -p src/assets/images/spritesheets/ --padding 8 --divisibleByTwo -n sprite-system-notice-spritesheet --powerOfTwo src/assets/images/sprites/notice/*" | |
}, | |
"ava": { | |
"files": [ | |
"./test/unit" | |
], | |
"sources": [ | |
"src/**/*.js" | |
], | |
"require": [ | |
"@babel/register", | |
"esm" | |
] | |
}, | |
"dependencies": { | |
"@fortawesome/fontawesome-svg-core": "^1.2.2", | |
"@fortawesome/free-solid-svg-icons": "^5.2.0", | |
"@fortawesome/react-fontawesome": "^0.1.0", | |
"@mozillareality/easing-functions": "^0.1.1", | |
"@mozillareality/three-batch-manager": "github:mozillareality/three-batch-manager#master", | |
"aframe": "github:mozillareality/aframe#hubs/master", | |
"aframe-rounded": "^1.0.3", | |
"aframe-slice9-component": "^1.0.0", | |
"ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", | |
"ammo.js": "github:mozillareality/ammo.js#hubs/master", | |
"animejs": "github:mozillareality/anime#hubs/master", | |
"buffered-interpolation": "^0.2.5", | |
"classnames": "^2.2.5", | |
"color": "^3.1.2", | |
"copy-to-clipboard": "^3.0.8", | |
"dashjs": "^3.1.0", | |
"dayjs-ext": "^2.2.0", | |
"deepmerge": "^2.1.1", | |
"detect-browser": "^3.0.1", | |
"draft-js": "^0.10.5", | |
"draft-js-counter-plugin": "^2.0.1", | |
"draft-js-emoji-plugin": "^2.1.1", | |
"draft-js-hashtag-plugin": "^2.0.3", | |
"draft-js-linkify-plugin": "^2.0.1", | |
"draft-js-plugins-editor": "^2.1.1", | |
"event-target-shim": "^3.0.1", | |
"form-data": "^3.0.0", | |
"form-urlencoded": "^2.0.4", | |
"history": "^4.7.2", | |
"hls.js": "^0.13.2", | |
"js-cookie": "^2.2.0", | |
"jsonschema": "^1.2.2", | |
"jwt-decode": "^2.2.0", | |
"lib-hubs": "github:mozillareality/lib-hubs#master", | |
"linkify-it": "^2.0.3", | |
"markdown-it": "^8.4.2", | |
"moving-average": "^1.0.0", | |
"naf-janus-adapter": "^4.0.1", | |
"networked-aframe": "github:mozillareality/networked-aframe#master", | |
"nipplejs": "github:mozillareality/nipplejs#mr-social-client/master", | |
"node-ensure": "0.0.0", | |
"pdfjs-dist": "^2.1.266", | |
"phoenix": "github:gfodor/phoenix-js#master", | |
"prop-types": "^15.7.2", | |
"raven-js": "^3.20.1", | |
"react": "^16.13.1", | |
"react-dom": "^16.13.1", | |
"react-emoji-render": "^0.4.6", | |
"react-infinite-scroller": "^1.2.2", | |
"react-intl": "^2.4.0", | |
"react-linkify": "^0.2.2", | |
"react-router": "^5.1.2", | |
"react-router-dom": "^5.1.2", | |
"screenfull": "^4.0.1", | |
"three": "github:mozillareality/three.js#hubs/master", | |
"three-ammo": "^1.0.11", | |
"three-bmfont-text": "github:mozillareality/three-bmfont-text#hubs/master", | |
"three-mesh-bvh": "^0.1.2", | |
"three-pathfinding": "github:MozillaReality/three-pathfinding#hubs/master2", | |
"three-to-ammo": "github:infinitelee/three-to-ammo", | |
"uuid": "^3.2.1", | |
"webrtc-adapter": "^6.0.2", | |
"zip-loader": "^1.1.0" | |
}, | |
"devDependencies": { | |
"@babel/core": "^7.3.3", | |
"@babel/plugin-proposal-class-properties": "^7.3.3", | |
"@babel/plugin-proposal-object-rest-spread": "^7.3.2", | |
"@babel/polyfill": "^7.4.4", | |
"@babel/preset-env": "^7.9.0", | |
"@babel/preset-react": "^7.0.0", | |
"@babel/register": "^7.0.0", | |
"@iarna/toml": "^2.2.3", | |
"acorn": "^6.4.1", | |
"ava": "^1.4.1", | |
"babel-eslint": "^10.0.1", | |
"babel-loader": "^8.0.5", | |
"babel-plugin-react-intl": "^3.0.1", | |
"babel-plugin-transform-react-jsx-img-import": "^0.1.4", | |
"copy-webpack-plugin": "^4.5.1", | |
"cors": "^2.8.4", | |
"css-loader": "^1.0.0", | |
"dotenv": "^5.0.1", | |
"eslint": "^5.16.0", | |
"eslint-config-prettier": "^2.9.0", | |
"eslint-plugin-prettier": "^2.6.2", | |
"eslint-plugin-react": "^7.10.0", | |
"eslint-plugin-react-hooks": "^4.0.0", | |
"esm": "^3.2.5", | |
"fast-plural-rules": "0.0.3", | |
"file-loader": "^1.1.10", | |
"html-loader": "^0.5.5", | |
"html-webpack-plugin": "^4.2.0", | |
"htmlhint": "^0.11.0", | |
"jsdom": "^15.1.1", | |
"localstorage-memory": "^1.0.3", | |
"mediasoup-client": "github:versatica/mediasoup-client#v3", | |
"mini-css-extract-plugin": "^0.8.0", | |
"ncp": "^2.0.0", | |
"node-fetch": "^2.6.0", | |
"node-sass": "^4.13.0", | |
"ora": "^4.0.2", | |
"phoenix-channels": "^1.0.0", | |
"prettier": "^1.7.0", | |
"protoo-client": "^4.0.4", | |
"raw-loader": "^0.5.1", | |
"request": "^2.88.2", | |
"rimraf": "^2.6.2", | |
"sass-loader": "^6.0.7", | |
"selfsigned": "^1.10.2", | |
"shelljs": "^0.8.1", | |
"spritesheet-js": "github:mozillareality/spritesheet.js#hubs/master", | |
"style-loader": "^0.20.2", | |
"stylelint": "^9.10.1", | |
"stylelint-config-recommended-scss": "^3.2.0", | |
"stylelint-scss": "^3.5.3", | |
"svg-inline-loader": "^0.8.0", | |
"tar": "^5.0.5", | |
"url-loader": "^1.0.1", | |
"webpack": "^4.32.2", | |
"webpack-bundle-analyzer": "^3.3.2", | |
"webpack-cli": "^3.2.3", | |
"webpack-dev-server": "^3.1.14", | |
"worker-loader": "^2.0.0" | |
}, | |
"optionalDependencies": { | |
"fsevents": "^2.1.3" | |
} | |
} |
Add a CircleCI directory and config.
Create a directory named .circleci
in the root of your repo and create a config.yml file inside that matches the below. Be sure to change the script name in line 36 to whatever you define for your login script.
version: 2 | |
jobs: | |
build: | |
docker: | |
- image: circleci/node:10-browsers | |
working_directory: ~/repo | |
steps: | |
- checkout | |
- restore_cache: | |
keys: | |
- v1-dependencies-{{ checksum "package-lock.json" }} | |
- v1-dependencies- | |
- run: npm ci | |
- save_cache: | |
paths: | |
- node_modules | |
key: v1-dependencies-{{ checksum "package-lock.json" }} | |
- run: npm test | |
- store_artifacts: | |
path: dist | |
deploy: | |
docker: | |
- image: circleci/node:10-browsers | |
working_directory: ~/repo | |
steps: | |
- checkout | |
- restore_cache: | |
keys: | |
- v1-dependencies-{{ checksum "package-lock.json" }} | |
- v1-dependencies- | |
- run: npm ci | |
- save_cache: | |
paths: | |
- node_modules | |
key: v1-dependencies-{{ checksum "package-lock.json" }} | |
- run: npm run login-bp | |
- run: npm run deploy | |
- store_artifacts: | |
path: dist | |
workflows: | |
version: 2 | |
build-and-deploy: | |
jobs: | |
- build | |
- deploy: | |
requires: | |
- build | |
filters: | |
branches: | |
only: production |
Push those changes to your production branch and you should be all set. Time to test!
Create a new branch to pull request and test.
Create a new branch named however you would like and make some visually different changes to your site. Once done, commit the changes and start a pull request against the production
branch. You should see that the PR is starting tests to make sure that the incoming changes are deployable.
When you’re ready to merge those changes, have your phone or mail client near because the script will initiate the AWS SES to email you a login request from CircleCI. Click the login link, and let CircleCI do the rest. After a while you’ll see the build is a success which means that you can check your live site to see the changes reflected.
You did it! Hopefully. If not, let me know in a comment where you struggled and I'll see if I can help.