@zaaxch/tailframe 2.0.0 → 2.2.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/package.json +1 -1
- package/src/new.mjs +429 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaaxch/tailframe",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/new.mjs
CHANGED
|
@@ -63,6 +63,9 @@ if (fs.existsSync(root) && fs.readdirSync(root).length) fail(`Refusing to overwr
|
|
|
63
63
|
const title = options.name.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
64
64
|
const svc = `${options.name}-svc`;
|
|
65
65
|
const ui = `${options.name}-ui`;
|
|
66
|
+
const databaseName = options.name.replaceAll("-", "_");
|
|
67
|
+
const mongoApplicationUser = `${options.name}-app`;
|
|
68
|
+
const mongoBackupUser = `${options.name}-backup`;
|
|
66
69
|
const files = {};
|
|
67
70
|
const add = (relative, content) => { files[relative] = content.endsWith("\n") ? content : `${content}\n`; };
|
|
68
71
|
// Generated repositories depend on the published toolkit rather than copying a validator, and they
|
|
@@ -77,9 +80,7 @@ const prettierConfig = {
|
|
|
77
80
|
endOfLine: "lf",
|
|
78
81
|
singleQuote: false
|
|
79
82
|
};
|
|
80
|
-
|
|
81
|
-
add(".prettierrc.json", JSON.stringify(prettierConfig, null, "\t"));
|
|
82
|
-
add(".gitattributes", `* text=auto eol=lf
|
|
83
|
+
const gitAttributes = `* text=auto eol=lf
|
|
83
84
|
|
|
84
85
|
*.png binary
|
|
85
86
|
*.jpg binary
|
|
@@ -87,7 +88,10 @@ add(".gitattributes", `* text=auto eol=lf
|
|
|
87
88
|
*.gif binary
|
|
88
89
|
*.ico binary
|
|
89
90
|
*.pdf binary
|
|
90
|
-
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
add(".prettierrc.json", JSON.stringify(prettierConfig, null, "\t"));
|
|
94
|
+
add(".gitattributes", gitAttributes);
|
|
91
95
|
|
|
92
96
|
const repos = [[svc, "Express/TypeScript API and background runtime."]];
|
|
93
97
|
if (options.ui) repos.push([ui, "Vue/Vite customer interface."]);
|
|
@@ -203,7 +207,7 @@ add(`${svc}/package.json`, JSON.stringify({
|
|
|
203
207
|
...(options.worker ? { "dev:worker": "nodemon --legacy-watch --exec ts-node -r tsconfig-paths/register src/worker.ts" } : {}),
|
|
204
208
|
build: "tsc && tsc-alias", start: "node dist/server.js",
|
|
205
209
|
...(options.worker ? { worker: "node dist/worker.js" } : {}),
|
|
206
|
-
test: "npm run test:unit && npm run test:integration", "test:unit": "jest --selectProjects unit --runInBand", "test:integration": "jest --selectProjects integration --runInBand", "validate:architecture": "tailframe validate --kind service .", format: "prettier --write src/",
|
|
210
|
+
test: "npm run test:unit && npm run test:integration", "test:unit": "jest --selectProjects unit --runInBand", "test:integration": "jest --selectProjects integration --runInBand", "validate:architecture": "tailframe validate --kind service .", format: "prettier --write src/", "format:check": "prettier --check src/",
|
|
207
211
|
"test:db:up": "docker compose -f docker-compose.test.yml up -d", "test:db:down": "docker compose -f docker-compose.test.yml down -v",
|
|
208
212
|
"docker:dev": "docker compose -f docker-compose.dev.yml up",
|
|
209
213
|
"docker:prod": "docker compose -f docker-compose.yml up -d",
|
|
@@ -220,11 +224,25 @@ module.exports = { projects: [
|
|
|
220
224
|
{ ...base, displayName: "integration", testMatch: ["**/*.integration.test.ts"] }
|
|
221
225
|
] };`);
|
|
222
226
|
add(`${svc}/.prettierrc.json`, JSON.stringify(prettierConfig, null, "\t"));
|
|
223
|
-
add(`${svc}/.
|
|
227
|
+
add(`${svc}/.gitattributes`, gitAttributes);
|
|
228
|
+
add(`${svc}/.gitignore`, `node_modules/\ndist/\n.env*\n!.env.example\n!.env.infrastructure.example\n*.log\nfirebase-service-account*.json\n`);
|
|
224
229
|
add(`${svc}/.dockerignore`, `node_modules\ndist\n.git\n.env*\n!.env.example\n*.log\ndata\nfirebase-service-account*.json\n`);
|
|
225
230
|
if (options.ui) add(".dockerignore", `**/node_modules\n**/dist\n**/.git\n**/.env*\n**/*.log\n**/data\n**/firebase-service-account*.json\n`);
|
|
226
|
-
const dbEnv = "MONGODB_URI=mongodb://localhost:27017/?replicaSet=rs0&directConnection=true\nMONGODB_DB_NAME=" +
|
|
231
|
+
const dbEnv = "MONGODB_URI=mongodb://localhost:27017/?replicaSet=rs0&directConnection=true\nMONGODB_DB_NAME=" + databaseName;
|
|
227
232
|
add(`${svc}/.env.example`, `NODE_ENV=development\nPORT=3000\nCORS_ORIGIN=https://localhost:5173\n${dbEnv}\n${options.auth === "firebase" ? "FIREBASE_PROJECT_ID=\n" : ""}${options.redis ? "REDIS_URL=redis://localhost:6379\n" : ""}`);
|
|
233
|
+
add(`${svc}/.env.infrastructure.example`, `ECR_IMAGE=<account>.dkr.ecr.<region>.amazonaws.com/${options.name}
|
|
234
|
+
IMAGE_TAG=<service-sha>${options.ui ? "_<ui-sha>" : ""}
|
|
235
|
+
APP_ENV_FILE=/opt/${options.name}/shared/.env.production
|
|
236
|
+
MONGO_DATA_DIR=/opt/${options.name}/shared/mongo/data
|
|
237
|
+
MONGO_KEYFILE=/opt/${options.name}/shared/mongo/config/keyfile
|
|
238
|
+
MONGO_INIT_SCRIPT=/opt/${options.name}/current/deploy/mongo/10-create-users.js
|
|
239
|
+
MONGODB_ROOT_PASSWORD=<uri-safe-random-value>
|
|
240
|
+
MONGODB_APP_PASSWORD=<different-uri-safe-random-value>
|
|
241
|
+
MONGODB_BACKUP_PASSWORD=<different-uri-safe-random-value>
|
|
242
|
+
${options.auth === "firebase" ? `SECRETS_DIR=/opt/${options.name}/shared/secrets
|
|
243
|
+
` : ""}${options.redis ? `REDIS_DATA_DIR=/opt/${options.name}/shared/redis/data
|
|
244
|
+
REDIS_PASSWORD=<different-uri-safe-random-value>
|
|
245
|
+
` : ""}`);
|
|
228
246
|
add(`${svc}/src/platform/config/env.ts`, `import "@dotenvx/dotenvx/config";
|
|
229
247
|
export const env = {
|
|
230
248
|
nodeEnv: process.env.NODE_ENV ?? "development",
|
|
@@ -232,7 +250,7 @@ export const env = {
|
|
|
232
250
|
corsOrigin: process.env.CORS_ORIGIN ?? "https://localhost:5173",
|
|
233
251
|
${options.auth === "firebase" ? `firebaseProjectId: process.env.FIREBASE_PROJECT_ID ?? "",` : ""}
|
|
234
252
|
mongodbUri: process.env.MONGODB_URI ?? "mongodb://localhost:27017/?replicaSet=rs0&directConnection=true",
|
|
235
|
-
mongodbDbName: process.env.MONGODB_DB_NAME ?? "${
|
|
253
|
+
mongodbDbName: process.env.MONGODB_DB_NAME ?? "${databaseName}",
|
|
236
254
|
${options.redis ? `redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",` : ""}
|
|
237
255
|
};`);
|
|
238
256
|
const databaseSource = `import { MongoClient } from "mongodb";
|
|
@@ -442,17 +460,25 @@ This service uses MongoDB. Module-owned MongoDB adapters own collection access,
|
|
|
442
460
|
- Keep optional entry-point assembly under \`src/app/<entry-point-kind>\`; root files such as \`src/server.ts\` and \`src/worker.ts\` may only bootstrap that assembly.
|
|
443
461
|
- Initialize infrastructure first, then call \`registerDependencies(...)\` from every process entry point. The composition root resets the container, explicitly constructs dependencies, and registers class-token instances. Keep tsyringe, injection tokens, decorators, and container resolution out of modules and platform adapters.
|
|
444
462
|
- Register routes, workers, and jobs explicitly. Shutdown paths are awaitable and close resources without forcing \`process.exit\`.
|
|
463
|
+
- Scheduled work prevents unintended overlap, stops intake before shutdown, awaits the active run, destroys its scheduler or consumer, and only then closes Redis and database clients.
|
|
445
464
|
${options.ui ? "- In production, serve the compiled UI from `dist/public` with a non-API SPA fallback. Never return the SPA for `/api` requests." : ""}
|
|
446
465
|
|
|
466
|
+
## Production image
|
|
467
|
+
The ECR build targets \`linux/amd64\`. ${options.ui ? "Its default immutable tag is `<service-sha>_<ui-sha>` and production Compose requires that exact tag." : "Its default immutable tag is `<service-sha>` and production Compose requires that exact tag."} The runtime image contains production dependencies only and runs as the Node user.${options.ui && options.auth === "firebase" ? ` The build requires \`${ui}/.env.production\` and mounts it as a BuildKit secret only while Vite compiles the browser bundle; it is not copied into the final image.` : ""}
|
|
468
|
+
|
|
469
|
+
## Production deployment
|
|
470
|
+
Only service \`main\` pushes or manual workflow dispatch deploy; UI pushes never trigger deployment. GitHub Actions may replace files under \`/opt/${options.name}/current\` and MUST NOT replace persistent configuration, credentials, keyfiles, or data under \`/opt/${options.name}/shared\`. Keep Node runtime behavior in the shared service \`.env.production\`, Compose interpolation and infrastructure credentials in \`.env.infrastructure\`, and fixed topology wiring in Compose \`environment:\`. The optional UI build-time \`.env.production\` is a separate ephemeral file. Do not run deployment until the host and GitHub production environment have been bootstrapped according to \`docs/production-deployment.md\`.
|
|
471
|
+
|
|
447
472
|
## Tests and validation
|
|
448
|
-
Run \`npm run validate:architecture\` after changing files or imports. The generated service also includes unit tests and Mongo-backed integration tests. Run \`npm run test:db:up\`, \`npm test\`, and \`npm run test:db:down\` for the full service check. Test use cases, policies, Mongo repository adapters, and HTTP boundaries where behavior lives. Every public operation needs success and error-envelope coverage; authenticated operations need trusted-identity coverage; persisted capabilities need Mongo ownership/query coverage; UI-enabled services must verify that \`/api\` paths never fall through to the SPA. All AWS/ECR and Docker commands, including \`npm run docker:push\`, must run outside the sandbox from the first attempt.
|
|
473
|
+
Run \`npm run validate:architecture\` and \`npm run format:check\` after changing files or imports. The generated service also includes unit tests and Mongo-backed integration tests. Run \`npm run test:db:up\`, \`npm test\`, and \`npm run test:db:down\` for the full service check. Test use cases, policies, Mongo repository adapters, and HTTP boundaries where behavior lives. Every public operation needs success and error-envelope coverage; authenticated operations need trusted-identity coverage; persisted capabilities need Mongo ownership/query coverage; UI-enabled services must verify that \`/api\` paths never fall through to the SPA. All AWS/ECR and Docker commands, including \`npm run docker:push\`, must run outside the sandbox from the first attempt.
|
|
449
474
|
`);
|
|
450
475
|
|
|
451
476
|
if (options.ui) {
|
|
452
477
|
const uiDeps = { "@tailwindcss/vite": "^4.1.18", "@vueuse/core": "^14.3.0", axios: "^1.9.0", pinia: "^3.0.1", primevue: "^4.2.5", vue: "^3.5.13", "vue-router": "^4.5.0", ...(options.auth === "firebase" ? { firebase: "^11.0.0" } : {}) };
|
|
453
478
|
const uiDevDeps = { "@tsconfig/node22": "^22.0.1", "@zaaxch/tailframe": contractVersion, "@types/node": "^22.13.14", "@vitejs/plugin-vue": "^5.2.3", "@vue/test-utils": "^2.4.10", "@vue/tsconfig": "^0.7.0", jsdom: "^29.1.1", "npm-run-all2": "^7.0.2", prettier: "3.5.3", typescript: "~5.8.0", vite: "^6.2.4", vitest: "^4.1.7", "vue-tsc": "^2.2.8" };
|
|
454
|
-
add(`${ui}/package.json`, JSON.stringify({ name: ui, version: "0.1.0", private: true, type: "module", scripts: { dev: "vite --host 0.0.0.0", build: "run-p type-check \"build-only {@}\" --", "build-only": "vite build", "type-check": "vue-tsc --build", test: "vitest", "validate:architecture": "tailframe validate --kind ui .", format: "prettier --write src/" }, dependencies: uiDeps, devDependencies: uiDevDeps }, null, "\t"));
|
|
479
|
+
add(`${ui}/package.json`, JSON.stringify({ name: ui, version: "0.1.0", private: true, type: "module", scripts: { dev: "vite --host 0.0.0.0", build: "run-p type-check \"build-only {@}\" --", "build-only": "vite build", "type-check": "vue-tsc --build", test: "vitest run", "test:watch": "vitest", "validate:architecture": "tailframe validate --kind ui .", format: "prettier --write src/", "format:check": "prettier --check src/" }, dependencies: uiDeps, devDependencies: uiDevDeps }, null, "\t"));
|
|
455
480
|
add(`${ui}/.prettierrc.json`, JSON.stringify(prettierConfig, null, "\t"));
|
|
481
|
+
add(`${ui}/.gitattributes`, gitAttributes);
|
|
456
482
|
add(`${ui}/tsconfig.json`, JSON.stringify({ files: [], references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }] }, null, "\t"));
|
|
457
483
|
add(`${ui}/tsconfig.app.json`, JSON.stringify({ extends: "@vue/tsconfig/tsconfig.dom.json", include: ["env.d.ts", "src/**/*", "src/**/*.vue"], compilerOptions: { composite: true, baseUrl: ".", paths: { "@/*": ["./src/*"] } } }, null, "\t"));
|
|
458
484
|
add(`${ui}/tsconfig.node.json`, JSON.stringify({ extends: "@tsconfig/node22/tsconfig.json", include: ["vite.config.*"], compilerOptions: { composite: true, types: ["node"] } }, null, "\t"));
|
|
@@ -587,13 +613,13 @@ Reuse Vue, PrimeVue, Tailwind, loading, error, and accessibility conventions. Pr
|
|
|
587
613
|
- Keep \`src/app/styles.css\` limited to Tailwind imports, theme tokens, and true global base behavior.
|
|
588
614
|
|
|
589
615
|
## Production packaging
|
|
590
|
-
The UI runs through Vite in development but has no production container. The service's multi-stage Dockerfile builds this repository and copies its output into \`dist/public\`; Express serves it with SPA fallback. AWS ECR stores the combined production image.
|
|
616
|
+
The UI runs through Vite in development but has no production container. The service's multi-stage Dockerfile builds this repository and copies its output into \`dist/public\`; Express serves it with SPA fallback. AWS ECR stores the combined \`linux/amd64\` production image under the default immutable \`<service-sha>_<ui-sha>\` tag, and production Compose requires that exact tag.${options.auth === "firebase" ? ` Provide \`${ui}/.env.production\` before a production build; the service build mounts it as a required BuildKit secret only while Vite compiles the browser bundle and does not copy it into the final image.` : ""}
|
|
591
617
|
|
|
592
618
|
## Product language
|
|
593
619
|
The product domain is undefined. Do not invent entities, workflows, roles, claims, navigation, or customer-facing promises.
|
|
594
620
|
|
|
595
621
|
## Validation
|
|
596
|
-
Run \`npm run validate:architecture\`, then add focused Vitest coverage and run type-check, build, and
|
|
622
|
+
Run \`npm run validate:architecture\` and \`npm run format:check\`, then add focused Vitest coverage and run type-check, build, and \`npm test\` as applicable. Use \`npm run test:watch\` only for interactive development.
|
|
597
623
|
`);
|
|
598
624
|
}
|
|
599
625
|
|
|
@@ -604,20 +630,26 @@ RUN npm ci
|
|
|
604
630
|
COPY ${svc}/ ./
|
|
605
631
|
RUN npm run build
|
|
606
632
|
|
|
633
|
+
FROM node:22.13-bookworm AS service-production-dependencies
|
|
634
|
+
WORKDIR /app/service
|
|
635
|
+
COPY ${svc}/package*.json ./
|
|
636
|
+
RUN npm ci --omit=dev && npm cache clean --force
|
|
637
|
+
|
|
607
638
|
FROM node:22.13-bookworm AS ui-build
|
|
608
639
|
WORKDIR /app/ui
|
|
609
640
|
COPY ${ui}/package*.json ./
|
|
610
641
|
RUN npm ci
|
|
611
642
|
COPY ${ui}/ ./
|
|
612
|
-
RUN npm run build
|
|
643
|
+
${options.auth === "firebase" ? "RUN --mount=type=secret,id=ui_env,target=/app/ui/.env.production,required=true npm run build" : "RUN npm run build"}
|
|
613
644
|
|
|
614
645
|
FROM node:22.13-bookworm-slim
|
|
615
646
|
WORKDIR /app
|
|
616
647
|
ENV NODE_ENV=production
|
|
617
648
|
COPY --from=service-build /app/service/dist ./dist
|
|
618
649
|
COPY --from=service-build /app/service/package*.json ./
|
|
619
|
-
COPY --from=service-
|
|
650
|
+
COPY --from=service-production-dependencies /app/service/node_modules ./node_modules
|
|
620
651
|
COPY --from=ui-build /app/ui/dist ./dist/public
|
|
652
|
+
USER node
|
|
621
653
|
EXPOSE 3000
|
|
622
654
|
CMD ["node", "dist/server.js"]
|
|
623
655
|
` : `FROM node:22.13-bookworm AS build
|
|
@@ -627,12 +659,18 @@ RUN npm ci
|
|
|
627
659
|
COPY . .
|
|
628
660
|
RUN npm run build
|
|
629
661
|
|
|
662
|
+
FROM node:22.13-bookworm AS production-dependencies
|
|
663
|
+
WORKDIR /app
|
|
664
|
+
COPY package*.json ./
|
|
665
|
+
RUN npm ci --omit=dev && npm cache clean --force
|
|
666
|
+
|
|
630
667
|
FROM node:22.13-bookworm-slim
|
|
631
668
|
WORKDIR /app
|
|
632
669
|
ENV NODE_ENV=production
|
|
633
670
|
COPY --from=build /app/dist ./dist
|
|
634
671
|
COPY --from=build /app/package*.json ./
|
|
635
|
-
COPY --from=
|
|
672
|
+
COPY --from=production-dependencies /app/node_modules ./node_modules
|
|
673
|
+
USER node
|
|
636
674
|
EXPOSE 3000
|
|
637
675
|
CMD ["node", "dist/server.js"]
|
|
638
676
|
`);
|
|
@@ -650,92 +688,431 @@ set -euo pipefail
|
|
|
650
688
|
: "\${AWS_ACCOUNT_ID:?Set AWS_ACCOUNT_ID}"
|
|
651
689
|
: "\${AWS_REGION:?Set AWS_REGION}"
|
|
652
690
|
|
|
691
|
+
SVC_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
692
|
+
BUILD_CONTEXT="${options.ui ? `$(cd "\${SVC_DIR}/.." && pwd)` : `\${SVC_DIR}`}"
|
|
693
|
+
${options.ui ? `UI_DIR="\${BUILD_CONTEXT}/${ui}"
|
|
694
|
+
SVC_SHA="$(git -C "\${SVC_DIR}" rev-parse --short HEAD)"
|
|
695
|
+
UI_SHA="$(git -C "\${UI_DIR}" rev-parse --short HEAD)"` : `SVC_SHA="$(git -C "\${SVC_DIR}" rev-parse --short HEAD)"`}
|
|
696
|
+
|
|
653
697
|
ECR_REPOSITORY="\${ECR_REPOSITORY:-${options.name}}"
|
|
654
|
-
IMAGE_TAG="\${IMAGE_TAG
|
|
698
|
+
IMAGE_TAG="\${IMAGE_TAG:-${options.ui ? `\${SVC_SHA}_\${UI_SHA}` : `\${SVC_SHA}`}}"
|
|
655
699
|
REGISTRY="\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com"
|
|
656
700
|
IMAGE="\${REGISTRY}/\${ECR_REPOSITORY}:\${IMAGE_TAG}"
|
|
657
|
-
SVC_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
658
|
-
BUILD_CONTEXT="${options.ui ? `$(cd "\${SVC_DIR}/.." && pwd)` : `\${SVC_DIR}`}"
|
|
659
701
|
DOCKERFILE="\${SVC_DIR}/Dockerfile"
|
|
702
|
+
${options.ui && options.auth === "firebase" ? `UI_ENV_FILE="\${UI_ENV_FILE:-\${UI_DIR}/.env.production}"` : ""}
|
|
660
703
|
|
|
661
704
|
aws ecr get-login-password --region "\${AWS_REGION}" |
|
|
662
705
|
docker login --username AWS --password-stdin "\${REGISTRY}"
|
|
663
706
|
|
|
664
|
-
docker build --file "\${DOCKERFILE}" --tag "\${IMAGE}" "\${BUILD_CONTEXT}"
|
|
707
|
+
docker build --platform linux/amd64${options.ui && options.auth === "firebase" ? ` --secret "id=ui_env,src=\${UI_ENV_FILE}"` : ""} --file "\${DOCKERFILE}" --tag "\${IMAGE}" "\${BUILD_CONTEXT}"
|
|
665
708
|
docker push "\${IMAGE}"
|
|
666
709
|
|
|
667
710
|
echo "Pushed \${IMAGE}"
|
|
668
711
|
`);
|
|
669
712
|
|
|
713
|
+
const workflowUiCheckout = options.ui ? `
|
|
714
|
+
- name: Checkout UI dependency
|
|
715
|
+
uses: actions/checkout@v6
|
|
716
|
+
with:
|
|
717
|
+
repository: \${{ github.repository_owner }}/${ui}
|
|
718
|
+
ref: main
|
|
719
|
+
ssh-key: \${{ secrets.UI_REPO_SSH_KEY }}
|
|
720
|
+
path: ${ui}
|
|
721
|
+
` : "";
|
|
722
|
+
const workflowUiEnvironment = options.ui && options.auth === "firebase" ? `
|
|
723
|
+
- name: Write UI production environment
|
|
724
|
+
env:
|
|
725
|
+
UI_ENV_PRODUCTION: \${{ secrets.UI_ENV_PRODUCTION }}
|
|
726
|
+
run: |
|
|
727
|
+
printf '%s\\n' "$UI_ENV_PRODUCTION" > ${ui}/.env.production
|
|
728
|
+
` : "";
|
|
729
|
+
const workflowTagResolution = options.ui ? `
|
|
730
|
+
ui_sha="$(git -C ${ui} rev-parse --short HEAD)"
|
|
731
|
+
echo "tag=\${svc_sha}_\${ui_sha}" >> "$GITHUB_OUTPUT"` : `
|
|
732
|
+
echo "tag=\${svc_sha}" >> "$GITHUB_OUTPUT"`;
|
|
733
|
+
add(`${svc}/.github/workflows/deploy.yml`, `name: Build and deploy
|
|
734
|
+
|
|
735
|
+
on:
|
|
736
|
+
push:
|
|
737
|
+
branches:
|
|
738
|
+
- main
|
|
739
|
+
workflow_dispatch:
|
|
740
|
+
|
|
741
|
+
permissions:
|
|
742
|
+
contents: read
|
|
743
|
+
id-token: write
|
|
744
|
+
|
|
745
|
+
concurrency:
|
|
746
|
+
group: ${options.name}-production
|
|
747
|
+
cancel-in-progress: false
|
|
748
|
+
|
|
749
|
+
jobs:
|
|
750
|
+
deploy:
|
|
751
|
+
name: Deploy production
|
|
752
|
+
if: \${{ vars.DEPLOY_HOST != '' && vars.DEPLOY_USER != '' }}
|
|
753
|
+
runs-on: ubuntu-latest
|
|
754
|
+
environment: production
|
|
755
|
+
env:
|
|
756
|
+
AWS_ACCOUNT_ID: \${{ vars.AWS_ACCOUNT_ID }}
|
|
757
|
+
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
758
|
+
ECR_REPOSITORY: \${{ vars.ECR_REPOSITORY || '${options.name}' }}
|
|
759
|
+
DEPLOY_HOST: \${{ vars.DEPLOY_HOST }}
|
|
760
|
+
DEPLOY_USER: \${{ vars.DEPLOY_USER }}
|
|
761
|
+
|
|
762
|
+
steps:
|
|
763
|
+
- name: Checkout service
|
|
764
|
+
uses: actions/checkout@v6
|
|
765
|
+
with:
|
|
766
|
+
path: ${svc}
|
|
767
|
+
${workflowUiCheckout}${workflowUiEnvironment}
|
|
768
|
+
- name: Resolve immutable image
|
|
769
|
+
id: image
|
|
770
|
+
run: |
|
|
771
|
+
svc_sha="$(git -C ${svc} rev-parse --short HEAD)"${workflowTagResolution}
|
|
772
|
+
echo "registry=\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com" >> "$GITHUB_OUTPUT"
|
|
773
|
+
echo "image=\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com/\${ECR_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
|
774
|
+
|
|
775
|
+
- name: Configure AWS credentials
|
|
776
|
+
uses: aws-actions/configure-aws-credentials@v6
|
|
777
|
+
with:
|
|
778
|
+
role-to-assume: \${{ vars.AWS_ROLE_ARN }}
|
|
779
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
780
|
+
|
|
781
|
+
- name: Build and push image
|
|
782
|
+
working-directory: ${svc}
|
|
783
|
+
env:
|
|
784
|
+
IMAGE_TAG: \${{ steps.image.outputs.tag }}
|
|
785
|
+
run: ./scripts/build_and_push.sh
|
|
786
|
+
|
|
787
|
+
- name: Configure deployment SSH
|
|
788
|
+
env:
|
|
789
|
+
DEPLOY_SSH_PRIVATE_KEY: \${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
|
|
790
|
+
DEPLOY_SSH_KNOWN_HOSTS: \${{ secrets.DEPLOY_SSH_KNOWN_HOSTS }}
|
|
791
|
+
run: |
|
|
792
|
+
install -m 700 -d ~/.ssh
|
|
793
|
+
printf '%s\\n' "$DEPLOY_SSH_PRIVATE_KEY" > ~/.ssh/${options.name}-deploy
|
|
794
|
+
chmod 600 ~/.ssh/${options.name}-deploy
|
|
795
|
+
printf '%s\\n' "$DEPLOY_SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
|
796
|
+
|
|
797
|
+
- name: Authenticate server with ECR
|
|
798
|
+
run: |
|
|
799
|
+
aws ecr get-login-password --region "$AWS_REGION" |
|
|
800
|
+
ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
|
|
801
|
+
"docker login --username AWS --password-stdin '\${{ steps.image.outputs.registry }}'"
|
|
802
|
+
|
|
803
|
+
- name: Install deployment files
|
|
804
|
+
run: |
|
|
805
|
+
ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
|
|
806
|
+
"install -m 755 -d /opt/${options.name}/current/deploy/mongo"
|
|
807
|
+
scp -i ~/.ssh/${options.name}-deploy \\
|
|
808
|
+
${svc}/docker-compose.yml \\
|
|
809
|
+
${svc}/scripts/deploy_remote.sh \\
|
|
810
|
+
"\${DEPLOY_USER}@\${DEPLOY_HOST}:/opt/${options.name}/current/"
|
|
811
|
+
scp -i ~/.ssh/${options.name}-deploy \\
|
|
812
|
+
${svc}/deploy/mongo/10-create-users.js \\
|
|
813
|
+
"\${DEPLOY_USER}@\${DEPLOY_HOST}:/opt/${options.name}/current/deploy/mongo/"
|
|
814
|
+
ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
|
|
815
|
+
"chmod 755 /opt/${options.name}/current/deploy_remote.sh"
|
|
816
|
+
|
|
817
|
+
- name: Deploy image
|
|
818
|
+
run: |
|
|
819
|
+
ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
|
|
820
|
+
"/opt/${options.name}/current/deploy_remote.sh '\${{ steps.image.outputs.tag }}' '\${{ steps.image.outputs.image }}'"
|
|
821
|
+
`);
|
|
822
|
+
|
|
823
|
+
add(`${svc}/scripts/deploy_remote.sh`, `#!/usr/bin/env bash
|
|
824
|
+
set -euo pipefail
|
|
825
|
+
|
|
826
|
+
IMAGE_TAG="\${1:?Usage: deploy_remote.sh IMAGE_TAG ECR_IMAGE}"
|
|
827
|
+
ECR_IMAGE="\${2:?Usage: deploy_remote.sh IMAGE_TAG ECR_IMAGE}"
|
|
828
|
+
DEPLOY_DIR="\${DEPLOY_DIR:-/opt/${options.name}/current}"
|
|
829
|
+
SHARED_DIR="\${SHARED_DIR:-/opt/${options.name}/shared}"
|
|
830
|
+
COMPOSE_FILE="\${DEPLOY_DIR}/docker-compose.yml"
|
|
831
|
+
INFRA_ENV="\${SHARED_DIR}/.env.infrastructure"
|
|
832
|
+
|
|
833
|
+
if [[ ! -f "\${INFRA_ENV}" ]]; then
|
|
834
|
+
echo "Missing provisioned infrastructure environment: \${INFRA_ENV}" >&2
|
|
835
|
+
exit 1
|
|
836
|
+
fi
|
|
837
|
+
|
|
838
|
+
NEXT_ENV="$(mktemp "\${SHARED_DIR}/.env.infrastructure.XXXXXX")"
|
|
839
|
+
cleanup() {
|
|
840
|
+
rm -f "\${NEXT_ENV}"
|
|
841
|
+
}
|
|
842
|
+
trap cleanup EXIT
|
|
843
|
+
|
|
844
|
+
grep -Ev '^(ECR_IMAGE|IMAGE_TAG)=' "\${INFRA_ENV}" > "\${NEXT_ENV}" || true
|
|
845
|
+
printf 'ECR_IMAGE=%s\\nIMAGE_TAG=%s\\n' "\${ECR_IMAGE}" "\${IMAGE_TAG}" >> "\${NEXT_ENV}"
|
|
846
|
+
chmod 600 "\${NEXT_ENV}"
|
|
847
|
+
mv "\${NEXT_ENV}" "\${INFRA_ENV}"
|
|
848
|
+
trap - EXIT
|
|
849
|
+
|
|
850
|
+
docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" config --quiet
|
|
851
|
+
docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" pull
|
|
852
|
+
docker compose --env-file "\${INFRA_ENV}" --file "\${COMPOSE_FILE}" up -d --wait
|
|
853
|
+
`);
|
|
854
|
+
|
|
855
|
+
add(`${svc}/deploy/mongo/10-create-users.js`, `const applicationPassword = process.env.MONGODB_APP_PASSWORD;
|
|
856
|
+
const backupPassword = process.env.MONGODB_BACKUP_PASSWORD;
|
|
857
|
+
|
|
858
|
+
if (!applicationPassword || !backupPassword) {
|
|
859
|
+
throw new Error("MONGODB_APP_PASSWORD and MONGODB_BACKUP_PASSWORD are required");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
db.getSiblingDB("${databaseName}").createUser({
|
|
863
|
+
user: "${mongoApplicationUser}",
|
|
864
|
+
pwd: applicationPassword,
|
|
865
|
+
roles: [{ role: "readWrite", db: "${databaseName}" }]
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
db.getSiblingDB("admin").createUser({
|
|
869
|
+
user: "${mongoBackupUser}",
|
|
870
|
+
pwd: backupPassword,
|
|
871
|
+
roles: [{ role: "backup", db: "admin" }]
|
|
872
|
+
});
|
|
873
|
+
`);
|
|
874
|
+
|
|
875
|
+
add(`${svc}/docs/production-deployment.md`, `# Production deployment
|
|
876
|
+
|
|
877
|
+
## Delivery model
|
|
878
|
+
|
|
879
|
+
GitHub Actions checks out ${svc}${options.ui ? ` and ${ui}` : ""}${options.ui ? " as sibling build inputs" : ""}, builds one
|
|
880
|
+
non-root production image, tags it ${options.ui ? "`<service-sha>_<ui-sha>`" : "`<service-sha>`"}, pushes it to ECR, and installs only deployment artifacts on the host.
|
|
881
|
+
Only service \`main\` pushes and manual dispatch trigger this workflow; UI pushes never deploy.
|
|
882
|
+
|
|
883
|
+
The host separates replaceable artifacts from persistent state:
|
|
884
|
+
|
|
885
|
+
\`\`\`text
|
|
886
|
+
/opt/${options.name}/
|
|
887
|
+
├── current/
|
|
888
|
+
│ ├── docker-compose.yml
|
|
889
|
+
│ ├── deploy_remote.sh
|
|
890
|
+
│ └── deploy/mongo/10-create-users.js
|
|
891
|
+
└── shared/
|
|
892
|
+
├── .env.production
|
|
893
|
+
├── .env.infrastructure
|
|
894
|
+
${options.auth === "firebase" ? ` ├── secrets/firebase-service-account.json
|
|
895
|
+
` : ""} ├── mongo/config/keyfile
|
|
896
|
+
├── mongo/data/
|
|
897
|
+
${options.redis ? ` └── redis/data/
|
|
898
|
+
` : ""}\`\`\`
|
|
899
|
+
|
|
900
|
+
Actions may replace \`current/\` and must never replace \`shared/\`. The server never needs a source checkout.
|
|
901
|
+
|
|
902
|
+
## Configuration ownership
|
|
903
|
+
|
|
904
|
+
- \`.env.production\` contains Node application behavior and provider credentials.
|
|
905
|
+
- \`.env.infrastructure\` contains Compose paths, image identity, and infrastructure passwords. Start from
|
|
906
|
+
\`.env.infrastructure.example\` and replace every placeholder.
|
|
907
|
+
- Compose \`environment:\` owns fixed production wiring: \`NODE_ENV\`, internal database/cache URLs, and mounted
|
|
908
|
+
credential paths.
|
|
909
|
+
${options.ui && options.auth === "firebase" ? `- The UI repository's build-time \`.env.production\` comes from the GitHub \`UI_ENV_PRODUCTION\` secret and is unrelated to the service runtime file above.
|
|
910
|
+
` : ""}
|
|
911
|
+
Do not inject \`.env.infrastructure\` into Node. Make both host environment files root-owned and mode \`0600\`.
|
|
912
|
+
|
|
913
|
+
## One-time host bootstrap
|
|
914
|
+
|
|
915
|
+
1. Install Docker Engine and Docker Compose.
|
|
916
|
+
2. Create \`/opt/${options.name}/current/deploy/mongo\`, \`/opt/${options.name}/shared/mongo/config\`, and
|
|
917
|
+
\`/opt/${options.name}/shared/mongo/data\`${options.redis ? `, plus \`/opt/${options.name}/shared/redis/data\`` : ""}.
|
|
918
|
+
3. Install \`.env.production\` and a completed \`.env.infrastructure\` under \`shared/\`.
|
|
919
|
+
4. Generate the MongoDB replica key with \`openssl rand -base64 756\`, make it owned by UID/GID \`999:999\`, and
|
|
920
|
+
mode \`0400\`.
|
|
921
|
+
${options.auth === "firebase" ? `5. Install the Firebase service account at \`shared/secrets/firebase-service-account.json\`, owned by \`root:1000\` and mode \`0640\`.
|
|
922
|
+
6. ` : "5. "}Give the deployment user Docker access and write access to \`current/\`; keep \`shared/\` and its files protected.
|
|
923
|
+
${options.auth === "firebase" ? "7" : "6"}. Authorize the dedicated deployment SSH key and record the host key for GitHub Actions.
|
|
924
|
+
|
|
925
|
+
Use a fresh Mongo data directory. Migrating an existing unauthenticated database requires a separately reviewed plan.
|
|
926
|
+
|
|
927
|
+
## GitHub production environment
|
|
928
|
+
|
|
929
|
+
Configure these variables:
|
|
930
|
+
|
|
931
|
+
- \`AWS_ACCOUNT_ID\`, \`AWS_REGION\`, and \`AWS_ROLE_ARN\` for the repository-restricted OIDC role.
|
|
932
|
+
- \`DEPLOY_HOST\` and \`DEPLOY_USER\`; the deployment job remains skipped until both exist.
|
|
933
|
+
- Optional \`ECR_REPOSITORY\`; the default is \`${options.name}\`.
|
|
934
|
+
|
|
935
|
+
Configure these secrets:
|
|
936
|
+
|
|
937
|
+
${options.ui ? `- \`UI_REPO_SSH_KEY\`: read-only deploy key for \`${ui}\`.
|
|
938
|
+
` : ""}${options.ui && options.auth === "firebase" ? `- \`UI_ENV_PRODUCTION\`: Vite/Firebase build-time environment.
|
|
939
|
+
` : ""}- \`DEPLOY_SSH_PRIVATE_KEY\`: dedicated host deployment key.
|
|
940
|
+
- \`DEPLOY_SSH_KNOWN_HOSTS\`: pinned host key.
|
|
941
|
+
|
|
942
|
+
The ECR repository and AWS OIDC provider/role are provisioned separately. The workflow never stores long-lived AWS
|
|
943
|
+
keys or creates cloud infrastructure.
|
|
944
|
+
|
|
945
|
+
## Deploy and rollback
|
|
946
|
+
|
|
947
|
+
The remote script requires the provisioned infrastructure file, atomically replaces only \`ECR_IMAGE\` and
|
|
948
|
+
\`IMAGE_TAG\`, validates Compose, pulls, and starts with health waiting. Roll back by selecting a previous immutable
|
|
949
|
+
tag and running the same script. Never use \`docker compose down -v\` in production; image rollback does not restore
|
|
950
|
+
database state.
|
|
951
|
+
|
|
952
|
+
MongoDB and optional Redis expose no host ports and require authentication. Verify API readiness, container health,
|
|
953
|
+
database/cache authentication, file permissions, and off-host backup/restore before treating the deployment as
|
|
954
|
+
production-ready. Product-specific TLS, capacity limits, migrations, and scheduled-work grace periods must be added
|
|
955
|
+
from measured application requirements rather than copied from another product.
|
|
956
|
+
`);
|
|
957
|
+
|
|
670
958
|
const serviceVolumes = [
|
|
671
|
-
...(options.auth === "firebase" ? [" -
|
|
959
|
+
...(options.auth === "firebase" ? [" - ${SECRETS_DIR:?Set SECRETS_DIR}/firebase-service-account.json:/app/secrets/firebase-service-account.json:ro"] : [])
|
|
672
960
|
];
|
|
673
961
|
const devVolumes = [
|
|
674
962
|
" - .:/app",
|
|
675
963
|
" - /app/node_modules",
|
|
676
964
|
...(options.auth === "firebase" ? [" - ./firebase-service-account.development.json:/app/firebase-service-account.development.json:ro"] : [])
|
|
677
965
|
];
|
|
678
|
-
const
|
|
966
|
+
const productionDependencies = [
|
|
967
|
+
["mongo-init", "service_completed_successfully"],
|
|
968
|
+
...(options.redis ? [["redis", "service_healthy"]] : [])
|
|
969
|
+
];
|
|
970
|
+
const developmentDependencies = [
|
|
679
971
|
["mongo-init", "service_completed_successfully"],
|
|
680
972
|
...(options.redis ? [["redis", "service_started"]] : [])
|
|
681
973
|
];
|
|
682
|
-
const dependsBlock = `\n depends_on:\n${
|
|
974
|
+
const dependsBlock = `\n depends_on:\n${productionDependencies.map(([dependency, condition]) => ` ${dependency}:\n condition: ${condition}`).join("\n")}`;
|
|
975
|
+
const devDependsBlock = `\n depends_on:\n${developmentDependencies.map(([dependency, condition]) => ` ${dependency}:\n condition: ${condition}`).join("\n")}`;
|
|
976
|
+
const productionEnvironment = [
|
|
977
|
+
" NODE_ENV: production",
|
|
978
|
+
` MONGODB_URI: "mongodb://${mongoApplicationUser}:\${MONGODB_APP_PASSWORD:?Set MONGODB_APP_PASSWORD}@mongodb:27017/${databaseName}?replicaSet=rs0&directConnection=true&authSource=${databaseName}"`,
|
|
979
|
+
` MONGODB_DB_NAME: "${databaseName}"`,
|
|
980
|
+
...(options.auth === "firebase" ? [" GOOGLE_APPLICATION_CREDENTIALS: /app/secrets/firebase-service-account.json"] : []),
|
|
981
|
+
...(options.redis ? [" REDIS_URL: \"redis://:\${REDIS_PASSWORD:?Set REDIS_PASSWORD}@redis:6379\""] : [])
|
|
982
|
+
];
|
|
983
|
+
const productionEnvironmentBlock = `\n environment:\n${productionEnvironment.join("\n")}`;
|
|
683
984
|
const databaseEnvironmentBlock = `\n environment:\n MONGODB_URI: "mongodb://mongodb:27017/?replicaSet=rs0&directConnection=true"`;
|
|
684
985
|
const volumeBlock = serviceVolumes.length ? `\n volumes:\n${serviceVolumes.join("\n")}` : "";
|
|
685
986
|
const workerService = options.worker ? `\n worker:
|
|
686
|
-
image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG
|
|
987
|
+
image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG:?Set IMAGE_TAG}
|
|
687
988
|
env_file:
|
|
688
|
-
-
|
|
689
|
-
command: ["node", "dist/worker.js"]${
|
|
989
|
+
- \${APP_ENV_FILE:-.env.production}
|
|
990
|
+
command: ["node", "dist/worker.js"]${productionEnvironmentBlock}${volumeBlock}${dependsBlock}
|
|
991
|
+
healthcheck:
|
|
992
|
+
test: ["CMD", "node", "-e", "process.kill(1, 0)"]
|
|
993
|
+
interval: 60s
|
|
994
|
+
timeout: 5s
|
|
995
|
+
retries: 3
|
|
996
|
+
start_period: 30s
|
|
997
|
+
stop_grace_period: 2m
|
|
998
|
+
networks:
|
|
999
|
+
- backend
|
|
690
1000
|
restart: unless-stopped` : "";
|
|
691
1001
|
const databaseService = `\n mongodb:
|
|
692
1002
|
image: mongo:8
|
|
693
|
-
command:
|
|
1003
|
+
command:
|
|
1004
|
+
- mongod
|
|
1005
|
+
- --replSet
|
|
1006
|
+
- rs0
|
|
1007
|
+
- --bind_ip_all
|
|
1008
|
+
- --keyFile
|
|
1009
|
+
- /etc/mongo-keyfile/keyfile
|
|
1010
|
+
environment:
|
|
1011
|
+
MONGO_INITDB_DATABASE: "${databaseName}"
|
|
1012
|
+
MONGO_INITDB_ROOT_USERNAME: "${options.name}-root"
|
|
1013
|
+
MONGO_INITDB_ROOT_PASSWORD: \${MONGODB_ROOT_PASSWORD:?Set MONGODB_ROOT_PASSWORD}
|
|
1014
|
+
MONGODB_APP_PASSWORD: \${MONGODB_APP_PASSWORD:?Set MONGODB_APP_PASSWORD}
|
|
1015
|
+
MONGODB_BACKUP_PASSWORD: \${MONGODB_BACKUP_PASSWORD:?Set MONGODB_BACKUP_PASSWORD}
|
|
694
1016
|
volumes:
|
|
695
|
-
-
|
|
1017
|
+
- \${MONGO_DATA_DIR:?Set MONGO_DATA_DIR}:/data/db
|
|
1018
|
+
- \${MONGO_KEYFILE:?Set MONGO_KEYFILE}:/etc/mongo-keyfile/keyfile:ro
|
|
1019
|
+
- \${MONGO_INIT_SCRIPT:?Set MONGO_INIT_SCRIPT}:/docker-entrypoint-initdb.d/10-create-users.js:ro
|
|
696
1020
|
healthcheck:
|
|
697
|
-
test:
|
|
698
|
-
|
|
699
|
-
|
|
1021
|
+
test:
|
|
1022
|
+
- CMD-SHELL
|
|
1023
|
+
- >-
|
|
1024
|
+
mongosh --quiet --host localhost
|
|
1025
|
+
--username "$\${MONGO_INITDB_ROOT_USERNAME}"
|
|
1026
|
+
--password "$\${MONGO_INITDB_ROOT_PASSWORD}"
|
|
1027
|
+
--authenticationDatabase admin
|
|
1028
|
+
--eval "quit(db.adminCommand({ping:1}).ok ? 0 : 1)"
|
|
1029
|
+
interval: 5s
|
|
1030
|
+
timeout: 5s
|
|
700
1031
|
retries: 30
|
|
1032
|
+
start_period: 20s
|
|
1033
|
+
networks:
|
|
1034
|
+
- backend
|
|
701
1035
|
restart: unless-stopped
|
|
702
1036
|
mongo-init:
|
|
703
1037
|
image: mongo:8
|
|
1038
|
+
environment:
|
|
1039
|
+
MONGO_INITDB_ROOT_USERNAME: "${options.name}-root"
|
|
1040
|
+
MONGO_INITDB_ROOT_PASSWORD: \${MONGODB_ROOT_PASSWORD:?Set MONGODB_ROOT_PASSWORD}
|
|
704
1041
|
depends_on:
|
|
705
1042
|
mongodb:
|
|
706
1043
|
condition: service_healthy
|
|
707
1044
|
restart: "no"
|
|
708
|
-
entrypoint:
|
|
709
|
-
|
|
710
|
-
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1045
|
+
entrypoint: ["bash", "-ec"]
|
|
1046
|
+
command:
|
|
1047
|
+
- >-
|
|
1048
|
+
mongosh --quiet --host mongodb:27017
|
|
1049
|
+
--username "$\${MONGO_INITDB_ROOT_USERNAME}"
|
|
1050
|
+
--password "$\${MONGO_INITDB_ROOT_PASSWORD}"
|
|
1051
|
+
--authenticationDatabase admin
|
|
1052
|
+
--eval "try { const status = rs.status(); quit(status.ok === 1 ? 0 : 1); }
|
|
1053
|
+
catch (error) {
|
|
1054
|
+
if (error.code === 94 || error.codeName === 'NotYetInitialized') {
|
|
1055
|
+
const result = rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]});
|
|
1056
|
+
quit(result.ok === 1 ? 0 : 1);
|
|
717
1057
|
}
|
|
718
|
-
|
|
1058
|
+
throw error;
|
|
1059
|
+
}"
|
|
1060
|
+
networks:
|
|
1061
|
+
- backend`;
|
|
719
1062
|
const redisService = options.redis ? `\n redis:
|
|
720
1063
|
image: redis:7-alpine
|
|
1064
|
+
command:
|
|
1065
|
+
- redis-server
|
|
1066
|
+
- --appendonly
|
|
1067
|
+
- "yes"
|
|
1068
|
+
- --appendfsync
|
|
1069
|
+
- everysec
|
|
1070
|
+
- --maxmemory-policy
|
|
1071
|
+
- noeviction
|
|
1072
|
+
- --requirepass
|
|
1073
|
+
- \${REDIS_PASSWORD:?Set REDIS_PASSWORD}
|
|
1074
|
+
environment:
|
|
1075
|
+
REDIS_PASSWORD: \${REDIS_PASSWORD:?Set REDIS_PASSWORD}
|
|
721
1076
|
volumes:
|
|
722
|
-
-
|
|
1077
|
+
- \${REDIS_DATA_DIR:?Set REDIS_DATA_DIR}:/data
|
|
1078
|
+
healthcheck:
|
|
1079
|
+
test:
|
|
1080
|
+
- CMD-SHELL
|
|
1081
|
+
- REDISCLI_AUTH="$\${REDIS_PASSWORD}" redis-cli ping | grep -q PONG
|
|
1082
|
+
interval: 10s
|
|
1083
|
+
timeout: 3s
|
|
1084
|
+
retries: 10
|
|
1085
|
+
start_period: 10s
|
|
1086
|
+
networks:
|
|
1087
|
+
- backend
|
|
723
1088
|
restart: unless-stopped` : "";
|
|
724
|
-
const namedVolumes = [
|
|
725
|
-
" mongodb-data:",
|
|
726
|
-
...(options.redis ? [" redis-data:"] : [])
|
|
727
|
-
];
|
|
728
1089
|
add(`${svc}/docker-compose.yml`, `name: ${options.name}-prod
|
|
729
1090
|
services:
|
|
730
1091
|
${options.name}:
|
|
731
|
-
image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG
|
|
1092
|
+
image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG:?Set IMAGE_TAG}
|
|
732
1093
|
env_file:
|
|
733
|
-
-
|
|
1094
|
+
- \${APP_ENV_FILE:-.env.production}
|
|
734
1095
|
ports:
|
|
735
|
-
- "3000:3000"${
|
|
1096
|
+
- "3000:3000"${productionEnvironmentBlock}${volumeBlock}${dependsBlock}
|
|
1097
|
+
healthcheck:
|
|
1098
|
+
test:
|
|
1099
|
+
- CMD-SHELL
|
|
1100
|
+
- >-
|
|
1101
|
+
node -e
|
|
1102
|
+
"fetch('http://127.0.0.1:3000/api/v1/health.readiness',
|
|
1103
|
+
{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:'{}'})
|
|
1104
|
+
.then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
1105
|
+
interval: 30s
|
|
1106
|
+
timeout: 10s
|
|
1107
|
+
retries: 3
|
|
1108
|
+
start_period: 30s
|
|
1109
|
+
stop_grace_period: 2m
|
|
1110
|
+
networks:
|
|
1111
|
+
- backend
|
|
736
1112
|
restart: unless-stopped${workerService}${databaseService}${redisService}
|
|
737
|
-
|
|
738
|
-
|
|
1113
|
+
networks:
|
|
1114
|
+
backend:
|
|
1115
|
+
driver: bridge
|
|
739
1116
|
`);
|
|
740
1117
|
|
|
741
1118
|
const devWorker = options.worker ? `\n worker:
|
|
@@ -746,7 +1123,7 @@ const devWorker = options.worker ? `\n worker:
|
|
|
746
1123
|
- .env.development
|
|
747
1124
|
command: ["npm", "run", "dev:worker"]${databaseEnvironmentBlock}
|
|
748
1125
|
volumes:
|
|
749
|
-
${devVolumes.join("\n")}${
|
|
1126
|
+
${devVolumes.join("\n")}${devDependsBlock}` : "";
|
|
750
1127
|
const devDatabase = `\n mongodb:
|
|
751
1128
|
image: mongo:8
|
|
752
1129
|
command: mongod --replSet rs0 --bind_ip_all
|
|
@@ -797,7 +1174,7 @@ services:
|
|
|
797
1174
|
ports:
|
|
798
1175
|
- "3000:3000"${databaseEnvironmentBlock}
|
|
799
1176
|
volumes:
|
|
800
|
-
${devVolumes.join("\n")}${
|
|
1177
|
+
${devVolumes.join("\n")}${devDependsBlock}${devWorker}${devDatabase}${devRedis}
|
|
801
1178
|
volumes:
|
|
802
1179
|
${devNamedVolumes.join("\n")}
|
|
803
1180
|
`);
|