Set up Heroku deployment.

This commit is contained in:
Gareth Latty
2019-06-22 21:37:59 +01:00
parent 8f75e837cc
commit 7d88752bd1
15 changed files with 13767 additions and 80 deletions
+5 -3
View File
@@ -7,7 +7,9 @@ sudo: required
env:
global:
- VERSION=${TRAVIS_TAG:-${TRAVIS_COMMIT}-dev}
- COMMIT_VERSION=${TRAVIS_COMMIT}-dev
- TAG_VERSION=${TRAVIS_TAG}
- VERSION=${TAG_VERSION:-${COMMIT_VERSION}}
matrix:
- COMPONENT=server
- COMPONENT=client
@@ -18,10 +20,10 @@ before_install:
install: true
script:
- docker build -t massivedecks/${COMPONENT}:${VERSION} --build-arg VCS_REF="${TRAVIS_COMMIT}" --build-arg BUILD_DATE="$(date --rfc-3339=seconds)" --build-arg VERSION="${VERSION}" .
- docker build -t massivedecks/${COMPONENT}:${COMMIT_VERSION} --build-arg VCS_REF="${TRAVIS_COMMIT}" --build-arg BUILD_DATE="$(date --rfc-3339=seconds)" --build-arg VERSION="${VERSION}" .
deploy:
provider: script
script: bash ../docker_push "${COMPONENT}" "${VERSION}"
script: bash ../docker_push "${COMPONENT}" "${COMMIT_VERSION}" "${TAG_VERSION}"
on:
branch: v2
+1
View File
@@ -0,0 +1 @@
web: bin/start-nginx node server/dist/index.js
+37
View File
@@ -0,0 +1,37 @@
{
"name": "massivedecks",
"repository": "https://github.com/Lattyware/massivedecks",
"website": "https://github.com/Lattyware/massivedecks",
"keywords": ["game", "party", "comedy"],
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-nginx.git"
},
{
"url": "https://github.com/heroku/heroku-buildpack-nodejs.git"
}
],
"env": {
"MD_SECRET": {
"description": "A key used by the application to secure user authentication.",
"generator": "secret"
},
"MD_CONFIG_PATH": {
"description": "The path to the config file for the server.",
"value": "server/config.json5"
},
"MD_BASE_PATH": {
"description": "The base path the proxy presents the application as. This should always have a trailing slash if it isn't empty, and never one at the start. Leaving this blank should be fine.",
"value": "",
"required": false
},
"MD_LISTEN_ON": {
"description": "The port or socket to listen on. For Heroku, nginx listens to the /tmp/nginx.socket socket.",
"value": "/tmp/nginx.socket"
},
"MD_TOUCH_ON_START": {
"description": "A file to touch when the server has started, useful for signaling to a reverse proxy that the server is up. On Heroku, we use /tmp/app-initialized.",
"value": "/tmp/app-initialized"
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "massivedecks",
"name": "client",
"version": "2.0.0",
"lockfileVersion": 1,
"requires": true,
+19 -17
View File
@@ -1,11 +1,25 @@
{
"name": "massivedecks",
"name": "client",
"description": "The game client for Massive Decks, a comedy party game.",
"version": "2.0.0",
"description": "A web party game.",
"main": "src/ts/index.ts",
"author": "Gareth Latty <gareth@lattyware.co.uk>",
"license": "AGPL-3.0",
"author": "Reread Games <team@rereadgames.com>",
"license": "AGPL-3.0-or-later",
"private": true,
"repository": "github:Lattyware/massivedecks",
"scripts": {
"prepublishOnly": "npm run build",
"build": "npx webpack --mode=production",
"dev": "npx webpack-dev-server --mode=development"
},
"files": [
"dist/*"
],
"browserslist": [
"last 2 version",
"not dead",
"Firefox ESR",
"> 0.2%"
],
"dependencies": {
"@fortawesome/fontawesome-free": "^5.9.0",
"@webcomponents/webcomponentsjs": "^2.2.10",
@@ -43,17 +57,5 @@
"webpack": "^4.35.0",
"webpack-cli": "^3.3.4",
"webpack-dev-server": "^3.7.2"
},
"browserslist": [
"last 2 version",
"not dead",
"Firefox ESR",
"> 0.2%"
],
"scripts": {
"build": "npx webpack --mode=production",
"watch": "npx webpack --mode=development --watch",
"dev": "npx webpack-dev-server --mode=development",
"errors": "npx webpack --mode=development --display-error-details"
}
}
+59
View File
@@ -0,0 +1,59 @@
# This is an nginx config suitable for heroku.
# See server/nginx.conf for a more general starting point.
daemon off;
#Heroku dynos have at least 4 cores.
worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>;
events {
use epoll;
accept_mutex on;
worker_connections <%= ENV['NGINX_WORKER_CONNECTIONS'] || 1024 %>;
}
http {
gzip on;
gzip_comp_level 2;
gzip_min_length 512;
server_tokens off;
log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id';
access_log <%= ENV['NGINX_ACCESS_LOG_PATH'] || 'logs/nginx/access.log' %> l2met;
error_log <%= ENV['NGINX_ERROR_LOG_PATH'] || 'logs/nginx/error.log' %>;
types {
application/manifest+json webmanifest;
}
include mime.types;
default_type application/octet-stream;
sendfile on;
#Must read the body in 5 seconds.
client_body_timeout 5;
upstream app_server {
server unix:/tmp/nginx.socket fail_timeout=0;
}
server {
listen <%= ENV["PORT"] %>;
server_name _;
keepalive_timeout 5;
location / {
root "/app/client/dist";
}
location /api {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app_server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
}
+36 -1
View File
@@ -1,3 +1,38 @@
#!/bin/bash
COMPONENT=${1}
COMMIT_VERSION=${2}
TAG_VERSION=${3}
IMAGE=massivedecks/${COMPONENT}
push_tag() {
docker tag "${IMAGE}:${COMMIT_VERSION}" "${IMAGE}:${1}"
docker push "${IMAGE}:${1}"
}
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker push "massivedecks/${1}:${2}"
docker push "${IMAGE}:${COMMIT_VERSION}"
if [ -n "${TAG_VERSION}" ]; then
RE="^([0-9]*)([.]([0-9]*)([.]([0-9]*)(-([0-9A-Za-z-]*))?)?)?$"
if [[ "${TAG_VERSION}" =~ ${RE} ]]; then
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[3]:-0}"
PATCH="${BASH_REMATCH[5]:-0}"
PRERELEASE="${BASH_REMATCH[7]}"
if [ -n "${PRERELEASE}" ]; then
push_tag "${MAJOR}.${MINOR}.${PATCH}-${PRERELEASE}"
else
push_tag "${MAJOR}.${MINOR}.${PATCH}"
push_tag "${MAJOR}.${MINOR}"
push_tag "${MAJOR}"
fi
else
echo "Non-semver tag name \"${TAG_VERSION}\"." >&2
exit 1
fi
fi
push_tag "latest"
+13495
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "massivedecks",
"description": "Pulls together both the server and client for Massive Decks, a comedy party game.",
"version": "2.0.0",
"author": "Reread Games <team@rereadgames.com>",
"license": "AGPL-3.0-or-later",
"private": true,
"repository": "github:Lattyware/massivedecks",
"engines": {
"node": ">=11 <12"
},
"scripts": {
"build": "npm run build-client && npm run build-server",
"build-client": "(cd client && npm ci && npm install --only=dev && npm run build)",
"build-server": "(cd server && npm ci && npm install --only=dev && npm run build && cp package*.json dist && cp config.json5 dist && (cd dist && npm ci --only=prod))"
}
}
+7 -1
View File
@@ -9,10 +9,12 @@
listenOn: 8081,
// It is also possible to listen on a unix socket.
// listenOn: "/tmp/sock",
// This will be overwritten by the environment variable MD_LISTEN_ON, if set.
// The path this is hosted behind - only relevant when behind a reverse proxy, this won't actually host on a different
// path. This should always have a trailing slash if it isn't empty, and never one at the start.
basePath: "",
// This will be overwritten by the environment variable MD_BASE_PATH, if set.
// Timeouts determine how long the server waits before taking certain actions.
// These values are all ISO 8601 durations.
@@ -87,7 +89,11 @@
checkAfter: "PT5M"
},
// A file to touch when the server is started. If null, we won't touch any files.
touchOnStart: null,
// This will be overwritten by the environment variable MD_TOUCH_ON_START, if set.
// The version of Massive Decks.
// This will be overwritten by the environment variable MD_VERSION, if set.
version: "dev"
// This will be overwritten by the environment variable MD_VERSION, if set.
}
+14 -14
View File
@@ -1151,13 +1151,13 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"eslint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-6.0.0.tgz",
"integrity": "sha512-SrrIfcd4tOgsspOKTSwamuTOAMZOUigHQhVMrzNjz4/B9Za6SHQDIocMIyIDfwDgx6MhS15nS6HC8kumCV2qBQ==",
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
"integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"ajv": "^6.10.0",
"ajv": "^6.9.1",
"chalk": "^2.1.0",
"cross-spawn": "^6.0.5",
"debug": "^4.0.1",
@@ -1165,19 +1165,18 @@
"eslint-scope": "^4.0.3",
"eslint-utils": "^1.3.1",
"eslint-visitor-keys": "^1.0.0",
"espree": "^6.0.0",
"espree": "^5.0.1",
"esquery": "^1.0.1",
"esutils": "^2.0.2",
"file-entry-cache": "^5.0.1",
"functional-red-black-tree": "^1.0.1",
"glob-parent": "^3.1.0",
"glob": "^7.1.2",
"globals": "^11.7.0",
"ignore": "^4.0.6",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"inquirer": "^6.2.2",
"is-glob": "^4.0.0",
"js-yaml": "^3.13.1",
"js-yaml": "^3.13.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.3.0",
"lodash": "^4.17.11",
@@ -1185,6 +1184,7 @@
"mkdirp": "^0.5.1",
"natural-compare": "^1.4.0",
"optionator": "^0.8.2",
"path-is-inside": "^1.0.2",
"progress": "^2.0.0",
"regexpp": "^2.0.1",
"semver": "^5.5.1",
@@ -1265,9 +1265,9 @@
"dev": true
},
"espree": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-6.0.0.tgz",
"integrity": "sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz",
"integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==",
"dev": true,
"requires": {
"acorn": "^6.0.7",
@@ -4430,9 +4430,9 @@
}
},
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
"dev": true
},
"tsutils": {
+19 -12
View File
@@ -1,22 +1,29 @@
{
"name": "server",
"description": "The game server for Massive Decks, a comedy party game.",
"version": "2.0.0",
"description": "Massive Decks server.",
"main": "dist/index.js",
"scripts": {
"generate-json-validators": "npx typescript-json-validator src/ts/action/validation.ts --collection --noExtraProps--format=full --aliasRefs --ignoreErrors && sed -i '1s;^;/* eslint-disable */\\n;' src/ts/action/validation.validator.ts",
"build": "tsc",
"prestart": "npm run build",
"dev": "nodemon .",
"debug": "nodemon --inspect .",
"generate-secret": "npm run prestart && node dist/secret.js"
},
"author": "Reread Games <team@rereadgames.com>",
"license": "AGPL-3.0-or-later",
"private": true,
"repository": "github:Lattyware/massivedecks",
"engines": {
"node": ">=0.12"
"node": ">=11 <12"
},
"main": "dist/index.js",
"scripts": {
"prepublishOnly": "npm run build",
"build": "tsc",
"prestart": "npm run build",
"start": "node dist/index.js",
"dev": "nodemon .",
"debug": "nodemon --inspect .",
"generate-json-validators": "npx typescript-json-validator src/ts/action/validation.ts --collection --noExtraProps--format=full --aliasRefs --ignoreErrors && sed -i '1s;^;/* eslint-disable */\\n;' src/ts/action/validation.validator.ts",
"generate-secret": "npm run prestart && node dist/secret.js"
},
"files": [
"dist/*",
"config.json5"
],
"dependencies": {
"ajv": "^6.10.0",
"axios": "^0.19.0",
@@ -52,7 +59,7 @@
"@types/wu": "^2.1.39",
"@typescript-eslint/eslint-plugin": "^1.10.2",
"@typescript-eslint/parser": "^1.10.2",
"eslint": "^6.0.0",
"eslint": "^5.12.1",
"eslint-config-prettier": "^5.0.0",
"eslint-plugin-prettier": "^3.1.0",
"nodemon": "^1.19.1",
+9 -9
View File
@@ -21,15 +21,15 @@ export const Schema = {
definitions: {
Action: {
anyOf: [
{
$ref: "#/definitions/Authenticate"
},
{
$ref: "#/definitions/Judge"
},
{
$ref: "#/definitions/Reveal"
},
{
$ref: "#/definitions/Authenticate"
},
{
$ref: "#/definitions/Submit"
},
@@ -64,7 +64,7 @@ export const Schema = {
description: "Authenticate with the game.",
properties: {
action: {
$ref: "#/definitions/NameType_2"
$ref: "#/definitions/NameType"
},
token: {
$ref: "#/definitions/Token"
@@ -247,7 +247,7 @@ export const Schema = {
description: "A user declares the winning play for a round.",
properties: {
action: {
$ref: "#/definitions/NameType"
$ref: "#/definitions/NameType_1"
},
winner: {
$ref: "#/definitions/Id"
@@ -263,15 +263,15 @@ export const Schema = {
type: "string"
},
NameType: {
enum: ["Judge"],
enum: ["Authenticate"],
type: "string"
},
NameType_1: {
enum: ["Reveal"],
enum: ["Judge"],
type: "string"
},
NameType_2: {
enum: ["Authenticate"],
enum: ["Reveal"],
type: "string"
},
NameType_3: {
@@ -373,7 +373,7 @@ export const Schema = {
description: "A user judges the winning play for a round.",
properties: {
action: {
$ref: "#/definitions/NameType_1"
$ref: "#/definitions/NameType_2"
},
play: {
$ref: "#/definitions/Id"
+34 -7
View File
@@ -5,11 +5,23 @@ type Duration = UnparsedDuration | ParsedDuration;
type UnparsedDuration = string;
type ParsedDuration = number;
export interface Config<D extends Duration> {
const environmental: (keyof EnvironmentalConfig)[] = [
"secret",
"listenOn",
"basePath",
"version",
"touchOnStart"
];
export interface EnvironmentalConfig {
secret: string;
listenOn: number | string; // Port or unix socket path.
basePath: string;
version: string;
touchOnStart: string;
}
export interface Config<D extends Duration> extends EnvironmentalConfig {
timeouts: Timeouts<D>;
storage: BaseStorage<D>;
cache: BaseCache<D>;
@@ -98,9 +110,24 @@ export const parseTimeouts = (
parseDuration(value)
);
export const parse = (config: Unparsed): Parsed => ({
...config,
timeouts: parseTimeouts(config.timeouts),
storage: parseStorage(config.storage),
cache: parseCache(config.cache)
});
export const parse = (config: Unparsed): Parsed =>
pullFromEnvironment({
...config,
timeouts: parseTimeouts(config.timeouts),
storage: parseStorage(config.storage),
cache: parseCache(config.cache)
});
export const pullFromEnvironment = (config: Parsed): Parsed => {
for (const name of environmental) {
const envName = `MD_${name
.split(/(?=[A-Z])/)
.join("_")
.toUpperCase()}`;
const value = process.env[envName];
if (value !== undefined) {
config[name] = value;
}
}
return config;
};
+14 -15
View File
@@ -37,25 +37,20 @@ process.on("unhandledRejection", function(reason, promise) {
process.exit(1);
});
function getConfigFilePath(): string {
const configPath = process.env["MD_CONFIG_PATH"];
return configPath === undefined ? "config.json5" : configPath;
}
async function main(): Promise<void> {
const config = serverConfig.parse(JSON5.parse(
(await promisify(fs.readFile)("config.json5")).toString()
(await promisify(fs.readFile)(getConfigFilePath())).toString()
) as serverConfig.Unparsed);
const envSecret = process.env.MD_SECRET;
if (envSecret !== undefined) {
config.secret = envSecret;
}
const envVersion = process.env.MD_VERSION;
if (envVersion !== undefined) {
config.version = envVersion;
}
const { app } = ws(express());
app.use(helmet());
app.set('trust proxy', true);
app.set("trust proxy", true);
const environment = app.get("env");
@@ -199,9 +194,13 @@ async function main(): Promise<void> {
.loadFromStore(state)
.catch(error => logging.logException("Error running store tasks:", error));
app.listen(config.listenOn, () =>
logging.logger.info(`Listening on ${config.listenOn}.`)
);
app.listen(config.listenOn, async () => {
logging.logger.info(`Listening on ${config.listenOn}.`);
if (config.touchOnStart !== null) {
const f = await promisify(fs.open)(config.touchOnStart, "w");
await promisify(fs.close)(f);
}
});
}
main().catch(error => {