fix: support containerized release runtimes
This commit is contained in:
@@ -59,6 +59,8 @@ TRPC_PATH=/trpc
|
|||||||
# Frontend public URLs
|
# Frontend public URLs
|
||||||
# These values are public and become part of the browser bundle.
|
# These values are public and become part of the browser bundle.
|
||||||
VITE_GATEWAY_WEB_URL=/gateway/
|
VITE_GATEWAY_WEB_URL=/gateway/
|
||||||
|
# Comma-separated public hostnames accepted by Vite preview. Use * only behind a trusted reverse proxy.
|
||||||
|
VITE_PREVIEW_ALLOWED_HOSTS=localhost,dev-sam-e2e.hided.net
|
||||||
# Account-uploaded icons are served by the Gateway and remain a public browser URL.
|
# Account-uploaded icons are served by the Gateway and remain a public browser URL.
|
||||||
VITE_GATEWAY_USER_ICON_BASE_URL=/gateway/api/user-icons
|
VITE_GATEWAY_USER_ICON_BASE_URL=/gateway/api/user-icons
|
||||||
VITE_BOARD_COMMUNITY_URL=/xe/community
|
VITE_BOARD_COMMUNITY_URL=/xe/community
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ docs/.vitepress/dist/
|
|||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
|
||||||
.turbo/
|
.turbo/
|
||||||
|
.pnpm-store/
|
||||||
*_errors.txt
|
*_errors.txt
|
||||||
docs/image-storage.md
|
docs/image-storage.md
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import path from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { runGameApiServer } from './server.js';
|
import { runGameApiServer } from './server.js';
|
||||||
import { runBattleSimWorker } from './battleSim/worker.js';
|
import { runBattleSimWorker } from './battleSim/worker.js';
|
||||||
import { runAuctionWorker } from './auction/worker.js';
|
import { runAuctionWorker } from './auction/worker.js';
|
||||||
@@ -41,18 +38,12 @@ export type { TurnCommandTable } from './turns/commandTable.js';
|
|||||||
export type { ReservedTurnView } from './turns/reservedTurns.js';
|
export type { ReservedTurnView } from './turns/reservedTurns.js';
|
||||||
export type { JsonObject, JsonArray } from './context.js';
|
export type { JsonObject, JsonArray } from './context.js';
|
||||||
|
|
||||||
const isMain = (): boolean => {
|
const GAME_API_ROLES = ['server', 'battle-sim-worker', 'auction-worker', 'tournament-worker'] as const;
|
||||||
if (typeof process.env.NODE_APP_INSTANCE === 'string') {
|
export const shouldRunGameApi = (role: string | undefined): boolean =>
|
||||||
return true;
|
typeof role === 'string' && GAME_API_ROLES.includes(role as (typeof GAME_API_ROLES)[number]);
|
||||||
}
|
|
||||||
if (!process.argv[1]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isMain()) {
|
if (shouldRunGameApi(process.env.GAME_API_ROLE)) {
|
||||||
const role = process.env.GAME_API_ROLE ?? 'server';
|
const role = process.env.GAME_API_ROLE;
|
||||||
const run =
|
const run =
|
||||||
role === 'battle-sim-worker'
|
role === 'battle-sim-worker'
|
||||||
? runBattleSimWorker
|
? runBattleSimWorker
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { shouldRunGameApi } from '../src/index.js';
|
||||||
|
|
||||||
|
describe('game-api entrypoint', () => {
|
||||||
|
it('starts only for an explicitly selected API or worker role', () => {
|
||||||
|
expect(shouldRunGameApi('server')).toBe(true);
|
||||||
|
expect(shouldRunGameApi('auction-worker')).toBe(true);
|
||||||
|
expect(shouldRunGameApi('gateway')).toBe(false);
|
||||||
|
expect(shouldRunGameApi(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/game-engine",
|
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/game-engine",
|
||||||
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/game-engine --watch",
|
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/game-engine --watch",
|
||||||
"start": "pnpm run build && node dist/index.js",
|
"start": "pnpm run build && GAME_ENGINE_ROLE=turn-daemon node dist/index.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"profile:npc-unification-memory": "node scripts/profile-npc-unification-memory.mjs",
|
"profile:npc-unification-memory": "node scripts/profile-npc-unification-memory.mjs",
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import path from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { runTurnDaemonCli } from './turn/cli.js';
|
import { runTurnDaemonCli } from './turn/cli.js';
|
||||||
|
|
||||||
export * from './lifecycle/types.js';
|
export * from './lifecycle/types.js';
|
||||||
@@ -29,17 +26,9 @@ export * from './turn/selectPoolService.js';
|
|||||||
export * from './turn/turnDaemon.js';
|
export * from './turn/turnDaemon.js';
|
||||||
export * from './turn/cli.js';
|
export * from './turn/cli.js';
|
||||||
|
|
||||||
const isMain = (): boolean => {
|
export const shouldRunTurnDaemon = (role: string | undefined): boolean => role === 'turn-daemon';
|
||||||
if (typeof process.env.NODE_APP_INSTANCE === 'string') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!process.argv[1]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isMain()) {
|
if (shouldRunTurnDaemon(process.env.GAME_ENGINE_ROLE)) {
|
||||||
runTurnDaemonCli().catch((error) => {
|
runTurnDaemonCli().catch((error) => {
|
||||||
console.error('[turn-daemon] failed to start', error);
|
console.error('[turn-daemon] failed to start', error);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { shouldRunTurnDaemon } from '../src/index.js';
|
||||||
|
|
||||||
|
describe('game-engine entrypoint', () => {
|
||||||
|
it('starts only for an explicitly selected turn-daemon role', () => {
|
||||||
|
expect(shouldRunTurnDaemon('turn-daemon')).toBe(true);
|
||||||
|
expect(shouldRunTurnDaemon('api')).toBe(false);
|
||||||
|
expect(shouldRunTurnDaemon(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,18 @@ const normalizeBasePath = (value: string | undefined): string => {
|
|||||||
return `/${pathValue.replace(/^\/+|\/+$/g, '')}/`;
|
return `/${pathValue.replace(/^\/+|\/+$/g, '')}/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolvePreviewAllowedHosts = (value: string | undefined): true | string[] => {
|
||||||
|
const normalized = value?.trim();
|
||||||
|
if (normalized === '*') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hosts = (normalized ?? 'dev-sam-e2e.hided.net')
|
||||||
|
.split(',')
|
||||||
|
.map((host) => host.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return hosts;
|
||||||
|
};
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), '');
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
@@ -28,7 +40,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
allowedHosts: ['dev-sam-e2e.hided.net'],
|
allowedHosts: resolvePreviewAllowedHosts(env.VITE_PREVIEW_ALLOWED_HOSTS),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import path from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { runGatewayApiServer } from './server.js';
|
import { runGatewayApiServer } from './server.js';
|
||||||
import { runGatewayOrchestrator } from './orchestrator/orchestratorServer.js';
|
import { runGatewayOrchestrator } from './orchestrator/orchestratorServer.js';
|
||||||
import { runProfileSeedCli } from './orchestrator/profileSeedCli.js';
|
import { runProfileSeedCli } from './orchestrator/profileSeedCli.js';
|
||||||
@@ -33,15 +30,12 @@ export * from './auth/kakaoClient.js';
|
|||||||
export * from './auth/oauthSessionStore.js';
|
export * from './auth/oauthSessionStore.js';
|
||||||
export * from './auth/postgresUserRepository.js';
|
export * from './auth/postgresUserRepository.js';
|
||||||
|
|
||||||
const isMain = (): boolean => {
|
const GATEWAY_ROLES = ['api', 'orchestrator', 'profile-seed'] as const;
|
||||||
if (!process.argv[1]) {
|
export const shouldRunGateway = (role: string | undefined): boolean =>
|
||||||
return false;
|
typeof role === 'string' && GATEWAY_ROLES.includes(role as (typeof GATEWAY_ROLES)[number]);
|
||||||
}
|
|
||||||
return fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isMain()) {
|
if (shouldRunGateway(process.env.GATEWAY_ROLE)) {
|
||||||
const role = process.env.GATEWAY_ROLE ?? 'api';
|
const role = process.env.GATEWAY_ROLE;
|
||||||
const run =
|
const run =
|
||||||
role === 'orchestrator'
|
role === 'orchestrator'
|
||||||
? runGatewayOrchestrator
|
? runGatewayOrchestrator
|
||||||
|
|||||||
@@ -378,13 +378,14 @@ export const buildProcessDefinitions = (
|
|||||||
const runtimeWorkspace = profile.buildWorkspace ?? config.workspaceRoot;
|
const runtimeWorkspace = profile.buildWorkspace ?? config.workspaceRoot;
|
||||||
const frontendCwd = path.join(runtimeWorkspace, 'app', 'game-frontend');
|
const frontendCwd = path.join(runtimeWorkspace, 'app', 'game-frontend');
|
||||||
const frontendOutDir = buildProfileFrontendOutDir(runtimeWorkspace, profile.profileName);
|
const frontendOutDir = buildProfileFrontendOutDir(runtimeWorkspace, profile.profileName);
|
||||||
const frontendScript = path.join(runtimeWorkspace, 'node_modules', 'vite', 'bin', 'vite.js');
|
const frontendScript = path.join(frontendCwd, 'node_modules', 'vite', 'bin', 'vite.js');
|
||||||
const apiCwd = path.join(runtimeWorkspace, 'app', 'game-api');
|
const apiCwd = path.join(runtimeWorkspace, 'app', 'game-api');
|
||||||
const daemonCwd = path.join(runtimeWorkspace, 'app', 'game-engine');
|
const daemonCwd = path.join(runtimeWorkspace, 'app', 'game-engine');
|
||||||
const apiScript = path.join(apiCwd, 'dist', 'index.js');
|
const apiScript = path.join(apiCwd, 'dist', 'index.js');
|
||||||
const daemonScript = path.join(daemonCwd, 'dist', 'index.js');
|
const daemonScript = path.join(daemonCwd, 'dist', 'index.js');
|
||||||
const apiEnv = {
|
const apiEnv = {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
|
GAME_API_ROLE: 'server',
|
||||||
PROFILE: profile.profile,
|
PROFILE: profile.profile,
|
||||||
SCENARIO: profile.scenario,
|
SCENARIO: profile.scenario,
|
||||||
GAME_PROFILE_NAME: profile.profileName,
|
GAME_PROFILE_NAME: profile.profileName,
|
||||||
@@ -398,6 +399,7 @@ export const buildProcessDefinitions = (
|
|||||||
};
|
};
|
||||||
const daemonEnv = {
|
const daemonEnv = {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
|
GAME_ENGINE_ROLE: 'turn-daemon',
|
||||||
TURN_PROFILE: profile.profile,
|
TURN_PROFILE: profile.profile,
|
||||||
PROFILE: profile.profile,
|
PROFILE: profile.profile,
|
||||||
SCENARIO: profile.scenario,
|
SCENARIO: profile.scenario,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { shouldRunGateway } from '../src/index.js';
|
||||||
|
|
||||||
|
describe('gateway entrypoint', () => {
|
||||||
|
it('starts only for an explicitly selected Gateway role', () => {
|
||||||
|
expect(shouldRunGateway('api')).toBe(true);
|
||||||
|
expect(shouldRunGateway('orchestrator')).toBe(true);
|
||||||
|
expect(shouldRunGateway('profile-seed')).toBe(true);
|
||||||
|
expect(shouldRunGateway('server')).toBe(false);
|
||||||
|
expect(shouldRunGateway(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -118,7 +118,7 @@ describe('buildProcessDefinitions', () => {
|
|||||||
|
|
||||||
expect(definitions.frontend).toMatchObject({
|
expect(definitions.frontend).toMatchObject({
|
||||||
cwd: path.join(buildWorkspace, 'app', 'game-frontend'),
|
cwd: path.join(buildWorkspace, 'app', 'game-frontend'),
|
||||||
script: path.join(buildWorkspace, 'node_modules', 'vite', 'bin', 'vite.js'),
|
script: path.join(buildWorkspace, 'app', 'game-frontend', 'node_modules', 'vite', 'bin', 'vite.js'),
|
||||||
args: [
|
args: [
|
||||||
'preview',
|
'preview',
|
||||||
'--host',
|
'--host',
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ const normalizeBasePath = (value: string | undefined): string => {
|
|||||||
return `/${pathValue.replace(/^\/+|\/+$/g, '')}/`;
|
return `/${pathValue.replace(/^\/+|\/+$/g, '')}/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolvePreviewAllowedHosts = (value: string | undefined): true | string[] => {
|
||||||
|
const normalized = value?.trim();
|
||||||
|
if (normalized === '*') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hosts = (normalized ?? 'dev-sam-e2e.hided.net')
|
||||||
|
.split(',')
|
||||||
|
.map((host) => host.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return hosts;
|
||||||
|
};
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), '');
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
@@ -28,7 +40,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
allowedHosts: ['dev-sam-e2e.hided.net'],
|
allowedHosts: resolvePreviewAllowedHosts(env.VITE_PREVIEW_ALLOWED_HOSTS),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const buildGatewayProcessDefinitions = (
|
|||||||
const apiCwd = path.join(workspaceRoot, 'app', 'gateway-api');
|
const apiCwd = path.join(workspaceRoot, 'app', 'gateway-api');
|
||||||
const frontendCwd = path.join(workspaceRoot, 'app', 'gateway-frontend');
|
const frontendCwd = path.join(workspaceRoot, 'app', 'gateway-frontend');
|
||||||
const apiScript = path.join(apiCwd, 'dist', 'index.js');
|
const apiScript = path.join(apiCwd, 'dist', 'index.js');
|
||||||
const frontendScript = path.join(workspaceRoot, 'node_modules', 'vite', 'bin', 'vite.js');
|
const frontendScript = path.join(frontendCwd, 'node_modules', 'vite', 'bin', 'vite.js');
|
||||||
const env = {
|
const env = {
|
||||||
...config.baseEnv,
|
...config.baseEnv,
|
||||||
GATEWAY_API_HOST: '0.0.0.0',
|
GATEWAY_API_HOST: '0.0.0.0',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
import { afterEach, describe, expect, it } from 'vitest';
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { resolveReleaseControllerConfig, type ReleaseControllerConfig } from '../src/config.js';
|
import { resolveReleaseControllerConfig, type ReleaseControllerConfig } from '../src/config.js';
|
||||||
import { GatewayReleaseController } from '../src/releaseController.js';
|
import { buildGatewayProcessDefinitions, GatewayReleaseController } from '../src/releaseController.js';
|
||||||
import { upgradeReleaseController } from '../src/selfUpgrade.js';
|
import { upgradeReleaseController } from '../src/selfUpgrade.js';
|
||||||
|
|
||||||
const SHA = '1111111111111111111111111111111111111111';
|
const SHA = '1111111111111111111111111111111111111111';
|
||||||
@@ -116,6 +116,14 @@ const createRepository = () => {
|
|||||||
|
|
||||||
const gatewayNames = ['sammo:gateway-api', 'sammo:gateway-frontend', 'sammo:gateway-orchestrator'];
|
const gatewayNames = ['sammo:gateway-api', 'sammo:gateway-frontend', 'sammo:gateway-orchestrator'];
|
||||||
|
|
||||||
|
it('runs Gateway preview from the frontend workspace dependency', () => {
|
||||||
|
const definitions = buildGatewayProcessDefinitions('/srv/sammo/release', config);
|
||||||
|
const frontend = definitions.find((definition) => definition.name === 'sammo:gateway-frontend');
|
||||||
|
expect(frontend?.script).toBe(
|
||||||
|
'/srv/sammo/release/app/gateway-frontend/node_modules/vite/bin/vite.js'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('GatewayReleaseController', () => {
|
describe('GatewayReleaseController', () => {
|
||||||
it('builds, migrates, switches all gateway roles, verifies readiness, and publishes atomically', async () => {
|
it('builds, migrates, switches all gateway roles, verifies readiness, and publishes atomically', async () => {
|
||||||
const workspace = await createReleaseWorkspace();
|
const workspace = await createReleaseWorkspace();
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ VITE_APP_BASE_PATH=/gateway \
|
|||||||
VITE_GATEWAY_API_URL=/gateway/api/trpc \
|
VITE_GATEWAY_API_URL=/gateway/api/trpc \
|
||||||
VITE_GAME_API_URL_TEMPLATE='/{profile}/api/trpc' \
|
VITE_GAME_API_URL_TEMPLATE='/{profile}/api/trpc' \
|
||||||
VITE_GAME_WEB_URL_TEMPLATE='/{profile}/' \
|
VITE_GAME_WEB_URL_TEMPLATE='/{profile}/' \
|
||||||
|
VITE_PREVIEW_ALLOWED_HOSTS=dev-sam-e2e.hided.net \
|
||||||
pnpm --filter @sammo-ts/gateway-frontend build
|
pnpm --filter @sammo-ts/gateway-frontend build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ Game frontend는 profile별 값으로 build합니다.
|
|||||||
VITE_APP_BASE_PATH=/che \
|
VITE_APP_BASE_PATH=/che \
|
||||||
VITE_GAME_API_URL=/che/api/trpc \
|
VITE_GAME_API_URL=/che/api/trpc \
|
||||||
VITE_GAME_SSE_URL=/che/api/events \
|
VITE_GAME_SSE_URL=/che/api/events \
|
||||||
|
VITE_PREVIEW_ALLOWED_HOSTS=dev-sam-e2e.hided.net \
|
||||||
pnpm --filter @sammo-ts/game-frontend build
|
pnpm --filter @sammo-ts/game-frontend build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ profile 범위 권한과 별개인 전역 `admin.releases.manage` 권한이 필
|
|||||||
- Gateway PostgreSQL, profile PostgreSQL, Redis와 PM2가 준비되어 있습니다.
|
- Gateway PostgreSQL, profile PostgreSQL, Redis와 PM2가 준비되어 있습니다.
|
||||||
- controller가 `GATEWAY_DATABASE_URL`, `GATEWAY_DB_SCHEMA`, workspace와
|
- controller가 `GATEWAY_DATABASE_URL`, `GATEWAY_DB_SCHEMA`, workspace와
|
||||||
worktree 경로를 올바르게 읽습니다.
|
worktree 경로를 올바르게 읽습니다.
|
||||||
|
- 공개 hostname을 쉼표로 구분한 `VITE_PREVIEW_ALLOWED_HOSTS`가 Gateway와
|
||||||
|
profile frontend build 환경에 전달됩니다. 신뢰된 reverse proxy 뒤가 아니라면
|
||||||
|
`*`를 사용하지 않습니다.
|
||||||
|
- PM2 definition은 Gateway에 `GATEWAY_ROLE=api|orchestrator`, game API에
|
||||||
|
`GAME_API_ROLE=server|*-worker`, turn daemon에
|
||||||
|
`GAME_ENGINE_ROLE=turn-daemon`을 명시합니다. library import나 PM2 wrapper의
|
||||||
|
argv만으로 실행 역할을 추론하지 않습니다.
|
||||||
- migration 이후 이전 애플리케이션으로 돌아갈 때 schema 하위 호환성이
|
- migration 이후 이전 애플리케이션으로 돌아갈 때 schema 하위 호환성이
|
||||||
유지됩니다.
|
유지됩니다.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user