diff --git a/fly.js b/fly.js index 74b6fb3..b2ddbf7 100755 --- a/fly.js +++ b/fly.js @@ -15,6 +15,10 @@ GDF.extend(class extends GDF { // create volume for sqlite3 if (this.sqlite3) this.flyMakeVolume() + if (this.setupScriptType === 'dbsetup') { + this.flySetCmd() + } + // setup swap if (this.options.swap != null) this.flySetSwap() @@ -22,7 +26,10 @@ GDF.extend(class extends GDF { if (this.litefs) this.flyAttachConsul(this.flyApp) // set secrets, healthcheck for remix apps - if (this.remix) { + if (this.shopify) { + this.flyShopifyEnv(this.flyApp) + this.flyShopifyConfig(this.flyApp) + } else if (this.remix) { this.flyRemixSecrets(this.flyApp) this.flyHealthCheck('/healthcheck') } @@ -62,7 +69,7 @@ GDF.extend(class extends GDF { this.flyToml = fs.readFileSync(this.flyTomlFile, 'utf-8') // parse app name from fly.toml - this.flyApp = this.flyToml.match(/^app\s*=\s*"?([-\w]+)"?/m)?.[1] + this.flyApp = this.flyToml.match(/^app\s*=\s*["']?([-\w]+)["']?/m)?.[1] // see if flyctl is in the path const paths = (process.env.PATH || '') @@ -159,6 +166,27 @@ GDF.extend(class extends GDF { } } + // override command in fly.toml to include dbsetup.js + flySetCmd() { + if (this.flyToml.includes('[processes]')) return + + let cmd = this.startCommand + + const dockerfile = fs.readFileSync('Dockerfile', 'utf8') + + const match = dockerfile.match(/^\s*CMD\s+(\[.*\]|".*")/mi) + if (match) { + try { + cmd = JSON.parse(match[1]) + } catch { } + } + + if (Array.isArray(cmd)) cmd = cmd.join(' ') + cmd = `${this.bun ? 'bun' : 'node'} ./dbsetup.js ${cmd}` + this.flyToml += `\n[processes]\n app = ${JSON.stringify(cmd)}\n` + fs.writeFileSync(this.flyTomlFile, this.flyToml) + } + // add volume to fly.toml and create it if app exists flyAttachConsul(app) { if (!app) return @@ -283,6 +311,42 @@ GDF.extend(class extends GDF { }) } + // set environment and secrets for Shopify apps + flyShopifyEnv(app) { + const env = { + PORT: 3000, + SHOPIFY_APP_URL: `https://${app}.fly.dev` + } + + try { + const stdout = execSync('shopify app env show', { encoding: 'utf8' }) + for (const match of stdout.matchAll(/^\s*(\w+)=(.*)/mg)) { + if (match[1] === 'SHOPIFY_API_SECRET') { + console.log(`${chalk.bold.green('execute'.padStart(11))} flyctl secrets set SHOPIFY_API_SECRET`) + execSync(`${this.flyctl} secrets set SHOPIFY_API_SECRET=${match[2]} --app ${app}`, { stdio: 'inherit' }) + } else { + env[match[1]] = match[2] + } + } + } catch { } + + if (this.flyToml.includes('[env]')) return + this.flyToml += '\n[env]\n' + Object.entries(env).map(([key, value]) => ` ${key} = ${JSON.stringify(value)}`).join('\n') + '\n' + fs.writeFileSync(this.flyTomlFile, this.flyToml) + } + + // update config for Shopify apps + flyShopifyConfig(app) { + const original = fs.readFileSync('shopify.app.toml', 'utf-8') + const config = original.replaceAll(/"https:\/\/[-\w.]+/g, `"https://${app}.fly.dev`) + if (original !== config) { + console.log(`${chalk.bold.green('update'.padStart(11, ' '))} shopify.app.toml`) + fs.writeFileSync('shopify.app.toml', config) + console.log(`${chalk.bold.green('execute'.padStart(11))} shopify app deploy`) + execSync('shopify app deploy', { stdio: 'inherit' }) + } + } + // prep for deployment via GitHub actions, including setting up a staging app flyGitHubPrep() { const deploy = fs.readFileSync('.github/workflows/deploy.yml', 'utf-8') diff --git a/gdf.js b/gdf.js index 5f25176..91a0edf 100755 --- a/gdf.js +++ b/gdf.js @@ -82,6 +82,9 @@ export class GDF { // exit code #exitCode = 0 + // dockerfile exists at the time of invocation + #dockerfileExists = false + get variant() { return this.options.alpine ? 'alpine' : 'slim' } @@ -147,6 +150,11 @@ export class GDF { this.#pj.dependencies?.['@remix-run/node']) } + // Does this application use shopify? + get shopify() { + return fs.existsSync(path.join(this._appdir, 'shopify.app.toml')) + } + // Is this an EpicStack application? get epicStack() { return !!this.#pj['epic-stack'] @@ -934,6 +942,7 @@ export class GDF { this.options = options this._appdir = appdir this.#pj = JSON.parse(fs.readFileSync(path.join(appdir, 'package.json'), 'utf-8')) + this.#dockerfileExists = fs.existsSync(path.join(appdir, 'Dockerfile')) // backwards compatibility with previous definition of --build=defer if (options.build === 'defer') { @@ -967,7 +976,11 @@ export class GDF { } if (this.entrypoint) { - templates['docker-entrypoint.ejs'] = `${this.configDir}docker-entrypoint.js` + if (!this.#dockerfileExists) { + templates['docker-entrypoint.ejs'] = `${this.configDir}docker-entrypoint.js` + } else if (this.options.skip && fs.existsSync(path.join(appdir, 'fly.toml'))) { + templates['docker-entrypoint.ejs'] = `${this.configDir}dbsetup.js` + } } if (this.litefs) { @@ -1028,7 +1041,11 @@ export class GDF { runner.apply(this) } - process.exit(this.#exitCode) + if (this.#exitCode) process.exit(this.#exitCode) + } + + get setupScriptType() { + return (this.options.skip && this.#dockerfileExists) ? 'dbsetup' : 'docker' } setExit(code) { @@ -1046,6 +1063,9 @@ export class GDF { if (current === proposed) { console.log(`${chalk.bold.blue('identical'.padStart(11))} ${name}`) return dest + } else if (this.options.skip) { + console.log(`${chalk.bold.yellow('skip'.padStart(11))} ${name}`) + return current } let prompt diff --git a/index.js b/index.js index 3256609..512c5b8 100755 --- a/index.js +++ b/index.js @@ -89,6 +89,10 @@ const options = yargs((hideBin(process.argv))) describe: 'expose port', type: 'integer' }) + .option('skip', { + describe: 'skip overwrite of existing files', + type: 'boolean' + }) .option('swap', { alias: 's', describe: 'allocate swap space (eg. 1G, 1GiB, 1024M)', diff --git a/templates/docker-entrypoint.ejs b/templates/docker-entrypoint.ejs index 9271ca6..c361132 100755 --- a/templates/docker-entrypoint.ejs +++ b/templates/docker-entrypoint.ejs @@ -42,76 +42,96 @@ if (process.env.DATABASE_URL) { } <% } -%> +<% n = typeModule ? 0 : 1 -%> +<% if (!typeModule) { -%> ;(async() => { +<% } -%> <% if (options.swap && !flySetup()) { -%> -<%= tab(1) %>// allocate swap space -<%= tab(1) %>await exec('fallocate -l <%= options.swap %> /swapfile') -<%= tab(1) %>await exec('chmod 0600 /swapfile') -<%= tab(1) %>await exec('mkswap /swapfile') -<%= tab(1) %>writeFileSync('/proc/sys/vm/swappiness', '10') -<%= tab(1) %>await exec('swapon /swapfile') -<%= tab(1) %>writeFileSync('/proc/sys/vm/overcommit_memory', '1') +<%= tab(n) %>// allocate swap space +<%= tab(n) %>await exec('fallocate -l <%= options.swap %> /swapfile') +<%= tab(n) %>await exec('chmod 0600 /swapfile') +<%= tab(n) %>await exec('mkswap /swapfile') +<%= tab(n) %>writeFileSync('/proc/sys/vm/swappiness', '10') +<%= tab(n) %>await exec('swapon /swapfile') +<%= tab(n) %>writeFileSync('/proc/sys/vm/overcommit_memory', '1') <% } -%> <% if (prisma || (build && options.deferBuild) || nextjsGeneration) { -%> +<% if (setupScriptType == 'docker') { -%> <% if (prisma && sqlite3) { -%> -<%= tab(1) %>// If running the web server then migrate existing database +<%= tab(n) %>// If running the web server then migrate existing database <% } else { -%> -<%= tab(1) %>// If running the web server then prerender pages +<%= tab(n) %>// If running the web server then prerender pages <% } -%> -<%= tab(1) %>if (process.argv.slice(2).join(' ') === '<%- +<%= tab(n) %>if (process.argv.slice(2).join(' ') === '<%- Array.isArray(startCommand) ? startCommand.join(" ") : startCommand %>'<% if (litefs) { %> && process.env.FLY_REGION === process.env.PRIMARY_REGION<% } %>) { +<% n++ -%> +<% } -%> <% if (prisma) { -%> <% if (prismaFile) { -%> -<%= tab(2) %><%= nuxtjs ? 'let' : 'const' %> source = path.resolve('<%- prismaFile %>') -<%= tab(2) %>const target = '/data/' + path.basename(source) -<%= tab(2) %>if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) +<%= tab(n) %>// place Sqlite3 database on volume +<%= tab(n) %><%= nuxtjs ? 'let' : 'const' %> source = path.resolve('<%- prismaFile %>') +<%= tab(n) %>const target = '/data/' + path.basename(source) +<%= tab(n) %>if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) <% if (nuxtjs) { -%> -<%= tab(2) %>source = path.resolve('./.output/server', '<%- prismaFile %>') -<%= tab(2) %>if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) +<%= tab(n) %>source = path.resolve('./.output/server', '<%- prismaFile %>') +<%= tab(n) %>if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) <% } -%> <% } else if (prismaSeed && sqlite3 && prismaEnv) { -%> -<%= tab(2) %>const url = new URL(process.env.<%= prismaEnv %>) -<%= tab(2) %>const target = url.protocol === 'file:' && url.pathname -<%= tab(2) %><%= litestream ? 'let' : 'const' %> newDb = target && !fs.existsSync(target) +<%= tab(n) %>const url = new URL(process.env.<%= prismaEnv %>) +<%= tab(n) %>const target = url.protocol === 'file:' && url.pathname +<% if (litestream && sqlite3 && (prismaFile || prismaEnv)) { -%> + +<%= tab(n) %>// restore database if not present and replica exists <% } -%> -<% if (prismaFile && prismaSeed && sqlite3) { -%> -<%= tab(2) %><%= litestream ? 'let' : 'const' %> newDb = !fs.existsSync(target) <% } -%> -<% if (litestream && prismaSeed && sqlite3 && (prismaFile || prismaEnv)) { -%> -<%= tab(2) %>if (newDb && process.env.BUCKET_NAME) { -<%= tab(3) %>await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) -<%= tab(3) %>newDb = !fs.existsSync(target) -<%= tab(2) %>} +<% if (sqlite3 && (prismaSeed || litestream) && (prismaFile || prismaEnv)) { -%> +<%= tab(n) %><%= litestream ? 'let' : 'const' %> newDb = <%- !prismaFile ? 'target && ' : '' %>!fs.existsSync(target) <% } -%> -<% if (sqlite3) { -%> -<%= tab(2) %>await exec('<%= npx %> prisma migrate deploy') +<% if (litestream && sqlite3 && (prismaFile || prismaEnv)) { -%> +<%= tab(n) %>if (newDb && process.env.BUCKET_NAME) { +<%= tab(n+1) %>await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) +<% if (prismaSeed && sqlite3 && (prismaFile || prismaEnv)) { -%> +<%= tab(n+1) %>newDb = !fs.existsSync(target) +<% } -%> +<%= tab(n) %>} +<% } -%> +<% if (sqlite3 && !shopify) { -%> +<% if (prismaFile) { -%> + +<%= tab(n) %>// prepare database +<% } -%> +<%= tab(n) %>await exec('<%= npx %> prisma migrate deploy') <% } -%> <% if (prismaSeed && sqlite3 && (prismaFile || prismaEnv)) { -%> -<%= tab(2) %>if (newDb) await exec('npx prisma db seed') +<%= tab(n) %>if (newDb) await exec('npx prisma db seed') <% } -%> <% } -%> <% if (nextjsGeneration) { -%> -<%= tab(2) %>await exec('<%= npx %> next build --experimental-build-mode generate') +<%= tab(n) %>await exec('<%= npx %> next build --experimental-build-mode generate') <% } -%> <% if (build && options.deferBuild) { -%> -<%= tab(2) %>await exec('<%= packager %> run build') +<%= tab(n) %>await exec('<%= packager %> run build') +<% } -%> +<% if (setupScriptType == 'docker') { -%> +<%= tab(--n) %>} <% } -%> -<%= tab(1) %>} <% } -%> -<%= tab(1) %>// launch application +<%= tab(n) %>// launch application <% if (litestream) { -%> -<%= tab(1) %>if (process.env.BUCKET_NAME) { -<%= tab(2) %>await exec(`litestream replicate -config litestream.yml -exec ${JSON.stringify(process.argv.slice(2).join(' '))}`) -<%= tab(1) %>} else { -<%= tab(2) %>await exec(process.argv.slice(2).join(' ')) -<%= tab(1) %>} +<%= tab(n) %>if (process.env.BUCKET_NAME) { +<%= tab(n+1) %>await exec(`litestream replicate -config litestream.yml -exec ${JSON.stringify(process.argv.slice(2).join(' '))}`) +<%= tab(n) %>} else { +<%= tab(n+1) %>await exec(process.argv.slice(2).join(' ')) +<%= tab(n) %>} <% } else { -%> -<%= tab(1) %>await exec(process.argv.slice(2).join(' ')) +<%= tab(n) %>await exec(process.argv.slice(2).join(' ')) <% } -%> +<% if (!typeModule) { -%> })() +<% } -%> <%= tab(0) %>function exec(command) { <%= tab(1) %>const child = spawn(command, { shell: true, stdio: 'inherit', env }) diff --git a/test/base/defer-build/docker-entrypoint.js b/test/base/defer-build/docker-entrypoint.js index 19674de..61392d4 100755 --- a/test/base/defer-build/docker-entrypoint.js +++ b/test/base/defer-build/docker-entrypoint.js @@ -9,9 +9,12 @@ const env = { ...process.env } ;(async() => { // If running the web server then migrate existing database if (process.argv.slice(2).join(' ') === 'npm run start') { + // place Sqlite3 database on volume const source = path.resolve('./dev.db') const target = '/data/' + path.basename(source) if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) + + // prepare database await exec('npx prisma migrate deploy') await exec('npm run build') } diff --git a/test/frameworks/nuxt-prisma/docker-entrypoint.js b/test/frameworks/nuxt-prisma/docker-entrypoint.js index 814e38e..95e8329 100755 --- a/test/frameworks/nuxt-prisma/docker-entrypoint.js +++ b/test/frameworks/nuxt-prisma/docker-entrypoint.js @@ -9,6 +9,7 @@ const env = { ...process.env } ;(async() => { // If running the web server then migrate existing database if (process.argv.slice(2).join(' ') === 'node .output/server/index.mjs') { + // place Sqlite3 database on volume let source = path.resolve('./dev.db') const target = '/data/' + path.basename(source) if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) @@ -19,6 +20,8 @@ const env = { ...process.env } await exec(`litestream restore -config litestream.yml -if-replica-exists ${target}`) newDb = !fs.existsSync(target) } + + // prepare database await exec('npx prisma migrate deploy') if (newDb) await exec('npx prisma db seed') } diff --git a/test/frameworks/remix-epic/docker-entrypoint.js b/test/frameworks/remix-epic/docker-entrypoint.js index 1216dcc..89c096e 100755 --- a/test/frameworks/remix-epic/docker-entrypoint.js +++ b/test/frameworks/remix-epic/docker-entrypoint.js @@ -4,15 +4,13 @@ import { spawn } from 'node:child_process' const env = { ...process.env } -;(async() => { - // If running the web server then migrate existing database - if (process.argv.slice(2).join(' ') === 'npm run start' && process.env.FLY_REGION === process.env.PRIMARY_REGION) { - await exec('npx prisma migrate deploy') - } +// If running the web server then migrate existing database +if (process.argv.slice(2).join(' ') === 'npm run start' && process.env.FLY_REGION === process.env.PRIMARY_REGION) { + await exec('npx prisma migrate deploy') +} - // launch application - await exec(process.argv.slice(2).join(' ')) -})() +// launch application +await exec(process.argv.slice(2).join(' ')) function exec(command) { const child = spawn(command, { shell: true, stdio: 'inherit', env }) diff --git a/test/frameworks/svelte-prisma/docker-entrypoint.js b/test/frameworks/svelte-prisma/docker-entrypoint.js index 1437720..87844c8 100755 --- a/test/frameworks/svelte-prisma/docker-entrypoint.js +++ b/test/frameworks/svelte-prisma/docker-entrypoint.js @@ -6,20 +6,21 @@ import fs from 'node:fs' const env = { ...process.env } -;(async() => { - // If running the web server then migrate existing database - if (process.argv.slice(2).join(' ') === 'node ./build/index.js') { - const source = path.resolve('./dev.db') - const target = '/data/' + path.basename(source) - if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) - const newDb = !fs.existsSync(target) - await exec('npx prisma migrate deploy') - if (newDb) await exec('npx prisma db seed') - } +// If running the web server then migrate existing database +if (process.argv.slice(2).join(' ') === 'node ./build/index.js') { + // place Sqlite3 database on volume + const source = path.resolve('./dev.db') + const target = '/data/' + path.basename(source) + if (!fs.existsSync(source) && fs.existsSync('/data')) fs.symlinkSync(target, source) + const newDb = !fs.existsSync(target) - // launch application - await exec(process.argv.slice(2).join(' ')) -})() + // prepare database + await exec('npx prisma migrate deploy') + if (newDb) await exec('npx prisma db seed') +} + +// launch application +await exec(process.argv.slice(2).join(' ')) function exec(command) { const child = spawn(command, { shell: true, stdio: 'inherit', env })