Adds Docker Jobs (#63)

* Start creating action release flow

* Updates build workflow

* Removes one step

* Another try

* Tweaks to build paths

* Fixes docker ignore

* Tweaks to database migration

* Removes useless entrypoint

* Fixes to database files

* More tweaks to build

* Tweaks to base URL

* Allows for config file to override env

* Tweak to order

* Next try

* Fixes comma error

* Another try

* Adds a health endpoint

* Try Nginx proxy pass

* Re-adds environment path

* Remove auto build on branch

* Linter fixes
This commit is contained in:
Chris Anderson 2023-03-04 21:11:59 -06:00 committed by GitHub
parent 8068d4d586
commit 732d8d1f12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 236 additions and 34 deletions

View file

@ -2,4 +2,5 @@
.DS_Store .DS_Store
.env .env
node_modules node_modules
**/node_modules
build build

35
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Create & Publish Docker Image
on:
push:
tags:
- "v*.*.*"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log In to the Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build the Docker Image
run: |
npm run docker:build:push

109
apps/platform/.gitignore vendored Normal file
View file

@ -0,0 +1,109 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
build
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# VS Code
.DS_Store

View file

@ -22,7 +22,5 @@ ENV NODE_ENV="production"
USER node USER node
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app ./ COPY --chown=node:node --from=build /usr/src/app ./
RUN chmod 755 ./scripts/entrypoint.sh
EXPOSE 3001 EXPOSE 3001
ENTRYPOINT ["./scripts/entrypoint.sh", "--"]
CMD ["dumb-init", "node", "index.js"] CMD ["dumb-init", "node", "index.js"]

View file

@ -43,6 +43,8 @@
"build": "tsc --build", "build": "tsc --build",
"lint": "eslint --ext .ts --max-warnings 0 src/", "lint": "eslint --ext .ts --max-warnings 0 src/",
"test": "jest --forceExit --testTimeout 10000", "test": "jest --forceExit --testTimeout 10000",
"docker:build": "docker buildx build -f ./Dockerfile -t ghcr.io/parcelvoy/api:latest ../../",
"docker:build:push": "npm run docker:build -- --push",
"migration:create": "node ./scripts/create-migration.mjs" "migration:create": "node ./scripts/create-migration.mjs"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,7 +0,0 @@
#!/bin/sh
## Migrate the database
node ./migrate.js latest
# Hand off to the CMD
exec "$@"

View file

@ -1,5 +1,5 @@
import Api from './api' import Api from './api'
import loadDatabase, { Database, migrate } from './config/database' import loadDatabase, { Database } from './config/database'
import loadQueue from './config/queue' import loadQueue from './config/queue'
import loadStorage from './config/storage' import loadStorage from './config/storage'
import loadAuth from './config/auth' import loadAuth from './config/auth'
@ -20,11 +20,8 @@ export default class App {
} }
static async init(env: Env): Promise<App> { static async init(env: Env): Promise<App> {
// Load database // Load & migrate database
const database = loadDatabase(env.db) const database = await loadDatabase(env.db)
// Migrate to latest version
await migrate(database)
// Load queue // Load queue
const queue = loadQueue(env.queue) const queue = loadQueue(env.queue)

View file

@ -106,6 +106,13 @@ export const clientRouter = () => {
*/ */
export const publicRouter = () => { export const publicRouter = () => {
const router = new Router() const router = new Router()
router.get('/health', async (ctx) => {
ctx.body = {
status: 'ok',
environment: process.env.NODE_ENV,
time: new Date(),
}
})
return register(router, return register(router,
AuthController, AuthController,
PublicSubscriptionController, PublicSubscriptionController,

View file

@ -7,7 +7,7 @@ export interface DatabaseConnection {
port: number port: number
user: string user: string
password: string password: string
database: string database?: string
} }
export interface DatabaseConfig { export interface DatabaseConfig {
@ -15,11 +15,15 @@ export interface DatabaseConfig {
connection: DatabaseConnection connection: DatabaseConnection
} }
export default (config: DatabaseConfig) => { const connect = (config: DatabaseConfig, withDB = true) => {
const connection = config.connection
if (!withDB) {
delete connection.database
}
return knex({ return knex({
client: config.client, client: config.client,
connection: { connection: {
...config.connection, ...connection,
typeCast(field: any, next: any) { typeCast(field: any, next: any) {
if (field.type === 'TINY' && field.length === 1) { if (field.type === 'TINY' && field.length === 1) {
return field.string() === '1' return field.string() === '1'
@ -31,9 +35,25 @@ export default (config: DatabaseConfig) => {
}) })
} }
export const migrate = async (db: Database) => { const migrate = async (db: Database) => {
return db.migrate.latest({ return db.migrate.latest({
directory: './db/migrations', directory: './db/migrations',
tableName: 'migrations', tableName: 'migrations',
}) })
} }
export default async (config: DatabaseConfig) => {
// Attempt to connect & migrate
try {
const db = connect(config)
await migrate(db)
return db
} catch (error) {
// On error, try to create the database and try again
const db = connect(config, false)
db.raw('CREATE DATABASE parcelvoy')
return connect(config)
}
}

View file

@ -53,7 +53,7 @@ export const capitalizeAll = function(str: string): string {
/** /**
* Truncates a string to the specified `length`, and appends * Truncates a string to the specified `length`, and appends
* it with an elipsis, ``. * it with an ellipsis, ``.
*/ */
export const ellipsis = function(str: string, limit: number): string { export const ellipsis = function(str: string, limit: number): string {
if (!isString(str)) return '' if (!isString(str)) return ''

View file

@ -5,5 +5,14 @@
"moduleResolution": "node", "moduleResolution": "node",
"baseUrl": "./src", "baseUrl": "./src",
"outDir": "./build" "outDir": "./build"
} },
"include": [
"./src/**/*",
"tsconfig.json"
],
"exclude": [
"node_modules",
"./**/*.spec.ts",
"./**/__mocks__/*"
]
} }

View file

@ -8,6 +8,11 @@ RUN npm run build
# --------------> The production image # --------------> The production image
FROM nginx:1.23.1-alpine FROM nginx:1.23.1-alpine
EXPOSE 80 EXPOSE 3000
COPY --from=compile /usr/src/app/apps/ui/docker/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf COPY --from=compile /usr/src/app/apps/ui/docker/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY --from=compile /usr/src/app/apps/ui/build /usr/share/nginx/html COPY --from=compile /usr/src/app/apps/ui/build /usr/share/nginx/html
COPY --from=compile /usr/src/app/apps/ui/scripts /usr/share/nginx/html
WORKDIR /usr/share/nginx/html
RUN apk add --no-cache bash
RUN chmod +x env.sh
CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]

View file

@ -1,9 +1,13 @@
server { server {
listen 80; listen 3000;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
location /api {
proxy_pass http://api:3001;
}
} }

View file

@ -39,7 +39,9 @@
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"" "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
"docker:build": "docker buildx build -f ./Dockerfile -t ghcr.io/parcelvoy/ui:latest ../../",
"docker:build:push": "npm run docker:build -- --push"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

1
apps/ui/public/config.js Normal file
View file

@ -0,0 +1 @@
window.API_BASE_URL = undefined

View file

@ -12,6 +12,8 @@
<meta property="og:description" content="Parcelvoy is an open sourced automated messaging and customer engagement tool for growth companies and enterprise alike. Send data-driven emails, push notifications, and SMS using the tools you already use and love." /> <meta property="og:description" content="Parcelvoy is an open sourced automated messaging and customer engagement tool for growth companies and enterprise alike. Send data-driven emails, push notifications, and SMS using the tools you already use and love." />
<meta property="og:url" content="https://parcelvoy.com" /> <meta property="og:url" content="https://parcelvoy.com" />
<meta property="og:image" content="https://parcelvoy.com/og-image.jpg" /> <meta property="og:image" content="https://parcelvoy.com/og-image.jpg" />
<script src="%PUBLIC_URL%/config.js"></script>
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

10
apps/ui/scripts/env.sh Normal file
View file

@ -0,0 +1,10 @@
#!/bin/bash
ENV_JS="./config.js"
rm -rf ${ENV_JS}
touch ${ENV_JS}
varname='API_BASE_URL'
value=$(printf '%s\n' "${!varname}")
echo "window.$varname = \"$value\";" >> ${ENV_JS}

View file

@ -1,5 +1,9 @@
declare global {
interface Window { API_BASE_URL: string }
}
export const env = { export const env = {
api: { api: {
baseURL: process.env.REACT_APP_API_BASE_URL!, baseURL: window.API_BASE_URL ?? process.env.REACT_APP_API_BASE_URL,
}, },
} }

View file

@ -11,11 +11,11 @@ services:
- mysql_data:/var/lib/mysql - mysql_data:/var/lib/mysql
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: "/usr/bin/mysql hallow --user=root --password=$$MYSQL_ROOT_PASSWORD --execute 'SELECT 1;'" test: "/usr/bin/mysql parcelvoy --user=root --password=$$MYSQL_ROOT_PASSWORD --execute 'SELECT 1;'"
interval: 1s interval: 1s
retries: 120 retries: 120
api: api:
image: 'parcelvoy/platform/api:0.1.0' image: 'ghcr.io/parcelvoy/api:latest'
restart: always restart: always
ports: ports:
- 3001:3001 - 3001:3001
@ -35,6 +35,9 @@ services:
DB_PASSWORD: ${DB_PASSWORD} DB_PASSWORD: ${DB_PASSWORD}
DB_PORT: 3306 DB_PORT: 3306
DB_DATABASE: ${DB_DATABASE} DB_DATABASE: ${DB_DATABASE}
STORAGE_DRIVER: ${STORAGE_DRIVER}
STORAGE_BASE_URL: ${STORAGE_BASE_URL}
AWS_S3_BUCKET: ${AWS_S3_BUCKET}
QUEUE_DRIVER: ${QUEUE_DRIVER} QUEUE_DRIVER: ${QUEUE_DRIVER}
AWS_SQS_QUEUE_URL: ${AWS_SQS_QUEUE_URL} AWS_SQS_QUEUE_URL: ${AWS_SQS_QUEUE_URL}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
@ -52,15 +55,13 @@ services:
AUTH_OPENID_REDIRECT_URI: ${AUTH_OPENID_REDIRECT_URI} AUTH_OPENID_REDIRECT_URI: ${AUTH_OPENID_REDIRECT_URI}
AUTH_OPENID_DOMAIN_WHITELIST: ${AUTH_OPENID_DOMAIN_WHITELIST} AUTH_OPENID_DOMAIN_WHITELIST: ${AUTH_OPENID_DOMAIN_WHITELIST}
ui: ui:
image: 'parcelvoy/platform/ui:0.1.0' image: 'ghcr.io/parcelvoy/ui:latest'
depends_on: depends_on:
- api - api
environment: environment:
REACT_APP_API_URL: ${BASE_URL} API_BASE_URL: ${API_BASE_URL}
REACT_APP_ENVIRONMENT: ${NODE_ENV}
REACT_APP_DOCKER_HOSTED_ENV: 'true'
ports: ports:
- 3000:3000 - 80:3000
volumes: volumes:
mysql_data: mysql_data:
driver: local driver: local

View file

@ -11,6 +11,8 @@
"start": "lerna run start", "start": "lerna run start",
"build": "lerna run build", "build": "lerna run build",
"lint": "lerna run lint", "lint": "lerna run lint",
"test": "lerna run test" "test": "lerna run test",
"docker:build": "lerna run docker:build",
"docker:build:push": "lerna run docker:build:push"
} }
} }