@cosmicdrift/kumiko-dev-server 0.336.1 → 0.338.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-kumiko-server.d.ts +8 -0
- package/dist/create-kumiko-server.js +61 -13
- package/dist/init-deploy-cli.js +3 -2
- package/dist/setup-test-stack-from-features.js +13 -1
- package/package.json +6 -6
- package/src/changes.json +32 -0
- package/templates/deploy/Dockerfile.template +3 -3
- package/templates/deploy/migrate-step.sh.template +20 -3
|
@@ -48,7 +48,12 @@ export type DevHostDispatchResult = {
|
|
|
48
48
|
readonly to: string;
|
|
49
49
|
readonly status?: 301 | 302;
|
|
50
50
|
} | {
|
|
51
|
+
/** Plain 404 by default. `file` serves a static page with status 404
|
|
52
|
+
* (like `static-html`), `entryName` serves that bundle's shell with 404;
|
|
53
|
+
* `file` wins when both are set. */
|
|
51
54
|
readonly kind: "not-found";
|
|
55
|
+
readonly entryName?: string;
|
|
56
|
+
readonly file?: string;
|
|
52
57
|
};
|
|
53
58
|
/** Picks an entry by inspecting the incoming request. Wird von
|
|
54
59
|
* Multi-Entry-Apps gesetzt; im Single-Entry-Mode irrelevant. */
|
|
@@ -185,6 +190,9 @@ export type KumikoServerHandle = {
|
|
|
185
190
|
readonly stack: TestStack;
|
|
186
191
|
/** Stops the server and tears down the stack (DB + redis). */
|
|
187
192
|
readonly stop: () => Promise<void>;
|
|
193
|
+
/** BullMQ queue-name prefix of this boot: per-boot in ephemeral mode, so
|
|
194
|
+
* its `bull:<prefix>-*` keys belong to this server alone on a shared Redis. */
|
|
195
|
+
readonly jobQueueNamePrefix: string;
|
|
188
196
|
};
|
|
189
197
|
export declare const STYLESHEET_WATCH_ENV = "KUMIKO_DEV_STYLESHEET_WATCH";
|
|
190
198
|
export declare const PROD_BUNDLES_ENV = "KUMIKO_DEV_PROD_BUNDLES";
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// The companion prod entry will land at `@cosmicdrift/kumiko-framework/server`
|
|
16
16
|
// with a different options shape (clientDist, auth config, db url).
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
|
-
import { existsSync, mkdtempSync, statSync } from "node:fs";
|
|
18
|
+
import { existsSync, mkdtempSync, realpathSync, statSync } from "node:fs";
|
|
19
19
|
import { readFile, realpath, watch } from "node:fs/promises";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { join, resolve, sep } from "node:path";
|
|
@@ -228,13 +228,14 @@ async function servePublicFile(pathname, publicDir) {
|
|
|
228
228
|
if (filePath === undefined)
|
|
229
229
|
return undefined;
|
|
230
230
|
try {
|
|
231
|
-
const bytes = await readFile(filePath);
|
|
232
231
|
// Lexical containment (above) only catches "..", not a symlink inside
|
|
233
232
|
// publicDir that points outside it on disk — realpath resolves the
|
|
234
|
-
// actual target and the same containment check runs against it.
|
|
233
|
+
// actual target and the same containment check runs against it. Read
|
|
234
|
+
// from the resolved path so the checked target is the one read.
|
|
235
235
|
const real = await realpath(filePath);
|
|
236
236
|
if (real !== publicDir && !real.startsWith(publicDir + sep))
|
|
237
237
|
return undefined;
|
|
238
|
+
const bytes = await readFile(real);
|
|
238
239
|
return { bytes, mime: publicFileMimeType(filePath) };
|
|
239
240
|
}
|
|
240
241
|
catch (err) {
|
|
@@ -722,7 +723,7 @@ export async function createKumikoServer(options) {
|
|
|
722
723
|
// resolvePageHead goes through the same headless resolveAndInjectPageHead
|
|
723
724
|
// that runProdApp uses, so dev and e2e exercise the one timeout and
|
|
724
725
|
// fallback path instead of a second copy of it (#3026).
|
|
725
|
-
const htmlResponse = async (entryName, req, socketAddress) => {
|
|
726
|
+
const htmlResponse = async (entryName, req, socketAddress, status = 200) => {
|
|
726
727
|
const template = htmlTemplates.get(entryName) ?? defaultTemplate;
|
|
727
728
|
const headers = new Headers();
|
|
728
729
|
headers.set("Content-Type", "text/html; charset=utf-8");
|
|
@@ -744,7 +745,23 @@ export async function createKumikoServer(options) {
|
|
|
744
745
|
systemQuery: buildDevSystemQuery(req, socketAddress),
|
|
745
746
|
});
|
|
746
747
|
}
|
|
747
|
-
return new Response(html, { headers });
|
|
748
|
+
return new Response(html, { status, headers });
|
|
749
|
+
};
|
|
750
|
+
// Raw file serve, no bundle injection — mirrors prod's `{ kind: "html", file }`.
|
|
751
|
+
const staticHtmlResponse = async (file, status) => {
|
|
752
|
+
let content;
|
|
753
|
+
try {
|
|
754
|
+
content = await readFile(file, "utf-8");
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
if (error.code !== "ENOENT")
|
|
758
|
+
throw error;
|
|
759
|
+
return new Response(`hostDispatch: file not found: ${file}`, { status: 500 });
|
|
760
|
+
}
|
|
761
|
+
return new Response(content, {
|
|
762
|
+
status,
|
|
763
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
764
|
+
});
|
|
748
765
|
};
|
|
749
766
|
// --- Fetch handler (runtime-neutral) ---
|
|
750
767
|
// Bundle-Pfad-Lookup: für jede Entry serven wir
|
|
@@ -756,7 +773,12 @@ export async function createKumikoServer(options) {
|
|
|
756
773
|
bundleByAssetPath.set(assetPathFor(e.name), e.name);
|
|
757
774
|
// App-root convention (same as expandWatchPatterns/resolveStylesheet above):
|
|
758
775
|
// process.cwd() is the app workspace, so public/ is its static asset dir.
|
|
759
|
-
|
|
776
|
+
// Symlink-resolved once: servePublicFile compares realpath()s against it, so
|
|
777
|
+
// a symlinked public/ would otherwise 404 every file.
|
|
778
|
+
const lexicalPublicDir = resolve(process.cwd(), "public");
|
|
779
|
+
const publicDir = existsSync(lexicalPublicDir)
|
|
780
|
+
? realpathSync(lexicalPublicDir)
|
|
781
|
+
: lexicalPublicDir;
|
|
760
782
|
// The app's cwd first, this package's own location as fallback (the cwd may
|
|
761
783
|
// not have renderer-web installed, e.g. a fixture dir).
|
|
762
784
|
const rendererWebFontsDir = resolveRendererWebFontsDir([process.cwd(), import.meta.dir]);
|
|
@@ -892,15 +914,15 @@ export async function createKumikoServer(options) {
|
|
|
892
914
|
});
|
|
893
915
|
}
|
|
894
916
|
if (dispatch.kind === "not-found") {
|
|
917
|
+
if (dispatch.file !== undefined)
|
|
918
|
+
return staticHtmlResponse(dispatch.file, 404);
|
|
919
|
+
if (dispatch.entryName !== undefined) {
|
|
920
|
+
return htmlResponse(dispatch.entryName, req, socketAddress, 404);
|
|
921
|
+
}
|
|
895
922
|
return new Response("Not Found", { status: 404 });
|
|
896
923
|
}
|
|
897
924
|
if (dispatch.kind === "static-html") {
|
|
898
|
-
|
|
899
|
-
// `{ kind: "html", file: ... }` static-html branch.
|
|
900
|
-
const file = await readFile(dispatch.file, "utf-8");
|
|
901
|
-
return new Response(file, {
|
|
902
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
903
|
-
});
|
|
925
|
+
return staticHtmlResponse(dispatch.file, 200);
|
|
904
926
|
}
|
|
905
927
|
return htmlResponse(dispatch.entryName, req, socketAddress);
|
|
906
928
|
}
|
|
@@ -984,6 +1006,29 @@ export async function createKumikoServer(options) {
|
|
|
984
1006
|
}, watcherAbort.signal);
|
|
985
1007
|
}
|
|
986
1008
|
}
|
|
1009
|
+
// The per-boot prefix is never reused, so its BullMQ keys (queues,
|
|
1010
|
+
// repeatables, delayed jobs) would pile up in the shared Redis forever.
|
|
1011
|
+
// A prefix-less duplicate: SCAN returns full key names, which the stack
|
|
1012
|
+
// client's keyPrefix would otherwise prepend a second time.
|
|
1013
|
+
// Takes the connection as a parameter: it must be opened before
|
|
1014
|
+
// stack.cleanup() closes the stack client, yet used only after it, because
|
|
1015
|
+
// closing the stack's enqueuer queues re-writes their `meta` keys.
|
|
1016
|
+
const deleteEphemeralJobQueues = async (raw) => {
|
|
1017
|
+
try {
|
|
1018
|
+
const keys = [];
|
|
1019
|
+
for await (const batch of raw.scanStream({
|
|
1020
|
+
match: `bull:${jobQueueNamePrefix}-*`,
|
|
1021
|
+
count: 500,
|
|
1022
|
+
})) {
|
|
1023
|
+
keys.push(...batch);
|
|
1024
|
+
}
|
|
1025
|
+
if (keys.length > 0)
|
|
1026
|
+
await raw.del(...keys);
|
|
1027
|
+
}
|
|
1028
|
+
finally {
|
|
1029
|
+
raw.disconnect();
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
987
1032
|
const stop = async () => {
|
|
988
1033
|
// Watcher zuerst stoppen damit kein onChange während des Teardowns
|
|
989
1034
|
// mehr feuert (sonst können tmpdir-rmSync ein process.exit(75)
|
|
@@ -998,7 +1043,10 @@ export async function createKumikoServer(options) {
|
|
|
998
1043
|
await stack.eventDispatcher.stop();
|
|
999
1044
|
}
|
|
1000
1045
|
await devJobRunners.stop();
|
|
1046
|
+
const rawRedis = persistentDb ? undefined : stack.redis.redis.duplicate({ keyPrefix: "" });
|
|
1001
1047
|
await stack.cleanup();
|
|
1048
|
+
if (rawRedis)
|
|
1049
|
+
await deleteEphemeralJobQueues(rawRedis);
|
|
1002
1050
|
};
|
|
1003
1051
|
// --- graceful shutdown ---
|
|
1004
1052
|
// Signal handlers fire on Ctrl-C / kill. Without them, repeated dev
|
|
@@ -1041,5 +1089,5 @@ export async function createKumikoServer(options) {
|
|
|
1041
1089
|
? ` (hot reload on ${entries.length === 1 ? "client entry" : `${entries.length} entries`})`
|
|
1042
1090
|
: ""));
|
|
1043
1091
|
}
|
|
1044
|
-
return { fetch: handleFetch, server, stack, stop };
|
|
1092
|
+
return { fetch: handleFetch, server, stack, stop, jobQueueNamePrefix };
|
|
1045
1093
|
}
|
package/dist/init-deploy-cli.js
CHANGED
|
@@ -84,7 +84,8 @@ export async function runInitDeployCli({ argv, cwd, out, }) {
|
|
|
84
84
|
out.err("");
|
|
85
85
|
return 2;
|
|
86
86
|
}
|
|
87
|
-
const
|
|
87
|
+
const outDir = getStringFlag(args, "out") ?? cwd;
|
|
88
|
+
const appName = getStringFlag(args, "app") ?? defaultAppName(outDir);
|
|
88
89
|
if (!appName) {
|
|
89
90
|
out.err("");
|
|
90
91
|
out.err(' --app <name> is required (no package.json "name" found to default from).');
|
|
@@ -96,7 +97,7 @@ export async function runInitDeployCli({ argv, cwd, out, }) {
|
|
|
96
97
|
appName,
|
|
97
98
|
port: getNumberFlag(args, "port"),
|
|
98
99
|
githubOrg: getStringFlag(args, "github-org"),
|
|
99
|
-
destination:
|
|
100
|
+
destination: outDir,
|
|
100
101
|
};
|
|
101
102
|
try {
|
|
102
103
|
return check ? reportDrift(target, out) : reportScaffold(target, force, out);
|
|
@@ -6,6 +6,18 @@ import { setupTestStack, } from "@cosmicdrift/kumiko-framework/stack";
|
|
|
6
6
|
import { jobRunLoggerCallbacks } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
|
|
7
7
|
import { composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
8
8
|
import { addConfigAccessorFactory } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
|
|
9
|
+
function isConfigResolver(value) {
|
|
10
|
+
return (typeof value === "object" && value !== null && "get" in value && typeof value.get === "function");
|
|
11
|
+
}
|
|
12
|
+
function resolveBaseConfigResolver(fromBase) {
|
|
13
|
+
const supplied = fromBase["configResolver"];
|
|
14
|
+
if (supplied === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (!isConfigResolver(supplied)) {
|
|
17
|
+
throw new Error("extraContext.configResolver must be a ConfigResolver (an object with get()), e.g. from createConfigResolver() — got a different value; pass the instance, not a factory.");
|
|
18
|
+
}
|
|
19
|
+
return supplied;
|
|
20
|
+
}
|
|
9
21
|
export function mergeExtraContext(base, presets) {
|
|
10
22
|
if (presets.length === 0)
|
|
11
23
|
return base;
|
|
@@ -14,7 +26,7 @@ export function mergeExtraContext(base, presets) {
|
|
|
14
26
|
const merged = { ...fromBase };
|
|
15
27
|
if (presets.includes("config")) {
|
|
16
28
|
// Same precedence as mergeConfigResolverDefault (run-dev-app.ts) — a base-supplied resolver wins, factory always derives from it (fw#3313).
|
|
17
|
-
const configResolver = fromBase
|
|
29
|
+
const configResolver = resolveBaseConfigResolver(fromBase) ?? createConfigResolver();
|
|
18
30
|
Object.assign(merged, addConfigAccessorFactory({ configResolver }, deps.registry));
|
|
19
31
|
}
|
|
20
32
|
if (presets.includes("template-resolver")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.338.0",
|
|
4
4
|
"description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
"kumiko-upgrade": "./bin/kumiko-upgrade.ts"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
68
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
69
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
70
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
71
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
67
|
+
"@cosmicdrift/kumiko-bundled-features": "0.338.0",
|
|
68
|
+
"@cosmicdrift/kumiko-framework": "0.338.0",
|
|
69
|
+
"@cosmicdrift/kumiko-headless": "0.338.0",
|
|
70
|
+
"@cosmicdrift/kumiko-renderer-web": "0.338.0",
|
|
71
|
+
"@cosmicdrift/kumiko-server-runtime": "0.338.0",
|
|
72
72
|
"bun-types": "^1.3.13",
|
|
73
73
|
"ts-morph": "^28.0.0",
|
|
74
74
|
"zod": "^4.4.3"
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.338.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Scaffolded migrate step percent-encodes DB_PASSWORD",
|
|
6
|
+
"detail": "dev-server: the scaffolded `deploy/migrate-step.sh` percent-encodes `DB_PASSWORD` in `DATABASE_URL`, so passwords with `@`, `:`, `/`, `#`, `%` or non-ASCII characters no longer break the URL. Re-run `kumiko init-deploy` to pick it up; apps with a hand-written `urlencode` should take the new version, which also encodes bytes >= 0x80 correctly."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.338.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "dev hostDispatch not-found can serve a custom 404 page",
|
|
12
|
+
"detail": "`DevHostDispatchResult` `{ kind: \"not-found\" }` accepts `file` (served like `static-html`) or `entryName` (that entry's shell), both with status 404. A missing file answers 500 with `hostDispatch: file not found`. Without fields the plain \"Not Found\" 404 stays.",
|
|
13
|
+
"migration": "No code change needed."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.338.0",
|
|
17
|
+
"type": "improvement",
|
|
18
|
+
"title": "New package @cosmicdrift/kumiko-samples with the corpus sources",
|
|
19
|
+
"detail": "New package with the sample recipes, sample apps and bundled-features sources\n`@cosmicdrift/kumiko-samples` ships the sample recipes, sample apps and bundled-features sources in the repository layout. Tools such as the few-shot corpus build can run against a pinned version of it and get the same entry ids and source paths as in the monorepo."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "0.337.1",
|
|
23
|
+
"type": "improvement",
|
|
24
|
+
"title": "createKumikoServer handle exposes the boot's jobQueueNamePrefix",
|
|
25
|
+
"detail": "dev-server: `createKumikoServer()` returns the boot's `jobQueueNamePrefix` on its handle, so callers sharing a Redis can address exactly that boot's `bull:<prefix>-*` keys.",
|
|
26
|
+
"migration": "No action needed: purely additive handle field."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"version": "0.337.0",
|
|
30
|
+
"type": "fix",
|
|
31
|
+
"title": "public/ served via realpath incl. symlinked public/, ephemeral dev queues cleaned on stop, init-deploy --out app name, compose-style migrate-step network name, config preset validates configResolver; guards report correct isolation lines and time out kumiko-upgrade",
|
|
32
|
+
"detail": "The dev server serves `public/` files from the symlink-resolved path (realpath check before the read) and works when `public/` itself is a symlink. Ephemeral dev boots delete their per-boot BullMQ keys from the shared Redis on `stop()`. `kumiko-init-deploy --out <dir>` defaults the app name from that directory's `package.json`. The generated `migrate-step.sh` with `stackNetwork: \"directory\"` derives the network name like Docker Compose (lowercased, sanitised, `COMPOSE_PROJECT_NAME` wins). `setupTestStackFromFeatures` with the `config` preset throws a clear error when `extraContext.configResolver` is not a resolver. The runtime-isolation guard reports each regression at its own import line, and the upgrade-state guard no longer hangs on a stuck `kumiko-upgrade` (stdin ignored, timeout)."
|
|
33
|
+
},
|
|
2
34
|
{
|
|
3
35
|
"version": "0.335.0",
|
|
4
36
|
"type": "fix",
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Production-Image for {{appName}}.
|
|
4
4
|
#
|
|
5
|
-
# Server-Bundle variant — the runtime image doesn't
|
|
6
|
-
#
|
|
5
|
+
# Server-Bundle variant — the runtime image doesn't carry the source tree
|
|
6
|
+
# or its node_modules any more, only:
|
|
7
7
|
#
|
|
8
8
|
# dist/ ← Client build (kumiko-build)
|
|
9
9
|
# dist-server/ ← Server bundle (bun build) + minimal package.json
|
|
10
10
|
# kumiko/migrations/ ← Checked-in SQL migrations applied by `kumiko schema apply`
|
|
11
11
|
#
|
|
12
|
-
# Build context: repository root (
|
|
12
|
+
# Build context: repository root (the full source tree is only used in the build stage).
|
|
13
13
|
|
|
14
14
|
ARG BUN_VERSION=1.4.0
|
|
15
15
|
{{#hasPrivateGhPackages}}
|
|
@@ -33,9 +33,11 @@ if [ -z "$STACK_NETWORK" ]; then
|
|
|
33
33
|
fi
|
|
34
34
|
{{/stackNetworkDiscover}}
|
|
35
35
|
{{#stackNetworkDirectory}}
|
|
36
|
-
|
|
36
|
+
# Compose lowercases the project name and drops characters outside [a-z0-9_-].
|
|
37
|
+
COMPOSE_PROJECT="${COMPOSE_PROJECT_NAME:-$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')}"
|
|
38
|
+
STACK_NETWORK="${COMPOSE_PROJECT}_stack"
|
|
37
39
|
if ! docker network inspect "$STACK_NETWORK" >/dev/null 2>&1; then
|
|
38
|
-
echo "Stack network '$STACK_NETWORK' not found — compose project not running?" >&2
|
|
40
|
+
echo "Stack network '$STACK_NETWORK' not found (derived from COMPOSE_PROJECT_NAME or the directory name '$(basename "$PWD")') — compose project not running?" >&2
|
|
39
41
|
exit 1
|
|
40
42
|
fi
|
|
41
43
|
{{/stackNetworkDirectory}}
|
|
@@ -47,6 +49,21 @@ set -a
|
|
|
47
49
|
. ./.env
|
|
48
50
|
set +a
|
|
49
51
|
|
|
52
|
+
# Percent-encode so '@', ':', '/', '#', '%' in the password cannot change how
|
|
53
|
+
# the URL is parsed. LC_ALL=C makes the loop walk bytes, not characters;
|
|
54
|
+
# bash sign-extends bytes >= 0x80 in "'$c", hence the & 255.
|
|
55
|
+
urlencode() {
|
|
56
|
+
local LC_ALL=C s="$1" out="" c i byte
|
|
57
|
+
for ((i = 0; i < ${#s}; i++)); do
|
|
58
|
+
c="${s:i:1}"
|
|
59
|
+
case "$c" in
|
|
60
|
+
[A-Za-z0-9._~-]) out+="$c" ;;
|
|
61
|
+
*) printf -v byte '%d' "'$c"; printf -v c '%%%02X' $((byte & 255)); out+="$c" ;;
|
|
62
|
+
esac
|
|
63
|
+
done
|
|
64
|
+
printf '%s' "$out"
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
# DATABASE_URL assumes: db user = appName, db name = appName, host "db"
|
|
51
68
|
# (compose-service-name), port 5432. Adjust to your stack if different.
|
|
52
69
|
{{#customDbUser}}
|
|
@@ -58,7 +75,7 @@ set +a
|
|
|
58
75
|
# NAME (`-e DATABASE_URL`, no value) so the expanded password never appears
|
|
59
76
|
# in `docker run`'s argv — otherwise it would be visible in `ps auxe` for
|
|
60
77
|
# the duration of the migrate run.
|
|
61
|
-
export DATABASE_URL="postgresql://{{dbUser}}:$
|
|
78
|
+
export DATABASE_URL="postgresql://{{dbUser}}:$(urlencode "$DB_PASSWORD")@db:5432/{{appName}}"
|
|
62
79
|
docker run --rm \
|
|
63
80
|
--network "$STACK_NETWORK" \
|
|
64
81
|
-e DATABASE_URL \
|