fix: support containerized release runtimes
This commit is contained in:
@@ -59,6 +59,8 @@ TRPC_PATH=/trpc
|
||||
# Frontend public URLs
|
||||
# These values are public and become part of the browser bundle.
|
||||
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.
|
||||
VITE_GATEWAY_USER_ICON_BASE_URL=/gateway/api/user-icons
|
||||
VITE_BOARD_COMMUNITY_URL=/xe/community
|
||||
|
||||
@@ -164,6 +164,7 @@ docs/.vitepress/dist/
|
||||
docker-compose.override.yml
|
||||
|
||||
.turbo/
|
||||
.pnpm-store/
|
||||
*_errors.txt
|
||||
docs/image-storage.md
|
||||
playwright-report/
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { runGameApiServer } from './server.js';
|
||||
import { runBattleSimWorker } from './battleSim/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 { JsonObject, JsonArray } from './context.js';
|
||||
|
||||
const isMain = (): boolean => {
|
||||
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]);
|
||||
};
|
||||
const GAME_API_ROLES = ['server', 'battle-sim-worker', 'auction-worker', 'tournament-worker'] as const;
|
||||
export const shouldRunGameApi = (role: string | undefined): boolean =>
|
||||
typeof role === 'string' && GAME_API_ROLES.includes(role as (typeof GAME_API_ROLES)[number]);
|
||||
|
||||
if (isMain()) {
|
||||
const role = process.env.GAME_API_ROLE ?? 'server';
|
||||
if (shouldRunGameApi(process.env.GAME_API_ROLE)) {
|
||||
const role = process.env.GAME_API_ROLE;
|
||||
const run =
|
||||
role === 'battle-sim-worker'
|
||||
? 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": {
|
||||
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/game-engine",
|
||||
"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:fix": "eslint . --fix",
|
||||
"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';
|
||||
|
||||
export * from './lifecycle/types.js';
|
||||
@@ -29,17 +26,9 @@ export * from './turn/selectPoolService.js';
|
||||
export * from './turn/turnDaemon.js';
|
||||
export * from './turn/cli.js';
|
||||
|
||||
const isMain = (): boolean => {
|
||||
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]);
|
||||
};
|
||||
export const shouldRunTurnDaemon = (role: string | undefined): boolean => role === 'turn-daemon';
|
||||
|
||||
if (isMain()) {
|
||||
if (shouldRunTurnDaemon(process.env.GAME_ENGINE_ROLE)) {
|
||||
runTurnDaemonCli().catch((error) => {
|
||||
console.error('[turn-daemon] failed to start', error);
|
||||
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, '')}/`;
|
||||
};
|
||||
|
||||
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/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
@@ -28,7 +40,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
preview: {
|
||||
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 { runGatewayOrchestrator } from './orchestrator/orchestratorServer.js';
|
||||
import { runProfileSeedCli } from './orchestrator/profileSeedCli.js';
|
||||
@@ -33,15 +30,12 @@ export * from './auth/kakaoClient.js';
|
||||
export * from './auth/oauthSessionStore.js';
|
||||
export * from './auth/postgresUserRepository.js';
|
||||
|
||||
const isMain = (): boolean => {
|
||||
if (!process.argv[1]) {
|
||||
return false;
|
||||
}
|
||||
return fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
||||
};
|
||||
const GATEWAY_ROLES = ['api', 'orchestrator', 'profile-seed'] as const;
|
||||
export const shouldRunGateway = (role: string | undefined): boolean =>
|
||||
typeof role === 'string' && GATEWAY_ROLES.includes(role as (typeof GATEWAY_ROLES)[number]);
|
||||
|
||||
if (isMain()) {
|
||||
const role = process.env.GATEWAY_ROLE ?? 'api';
|
||||
if (shouldRunGateway(process.env.GATEWAY_ROLE)) {
|
||||
const role = process.env.GATEWAY_ROLE;
|
||||
const run =
|
||||
role === 'orchestrator'
|
||||
? runGatewayOrchestrator
|
||||
|
||||
@@ -378,13 +378,14 @@ export const buildProcessDefinitions = (
|
||||
const runtimeWorkspace = profile.buildWorkspace ?? config.workspaceRoot;
|
||||
const frontendCwd = path.join(runtimeWorkspace, 'app', 'game-frontend');
|
||||
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 daemonCwd = path.join(runtimeWorkspace, 'app', 'game-engine');
|
||||
const apiScript = path.join(apiCwd, 'dist', 'index.js');
|
||||
const daemonScript = path.join(daemonCwd, 'dist', 'index.js');
|
||||
const apiEnv = {
|
||||
...baseEnv,
|
||||
GAME_API_ROLE: 'server',
|
||||
PROFILE: profile.profile,
|
||||
SCENARIO: profile.scenario,
|
||||
GAME_PROFILE_NAME: profile.profileName,
|
||||
@@ -398,6 +399,7 @@ export const buildProcessDefinitions = (
|
||||
};
|
||||
const daemonEnv = {
|
||||
...baseEnv,
|
||||
GAME_ENGINE_ROLE: 'turn-daemon',
|
||||
TURN_PROFILE: profile.profile,
|
||||
PROFILE: profile.profile,
|
||||
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({
|
||||
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: [
|
||||
'preview',
|
||||
'--host',
|
||||
|
||||
@@ -11,6 +11,18 @@ const normalizeBasePath = (value: string | undefined): string => {
|
||||
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/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
@@ -28,7 +40,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
preview: {
|
||||
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 frontendCwd = path.join(workspaceRoot, 'app', 'gateway-frontend');
|
||||
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 = {
|
||||
...config.baseEnv,
|
||||
GATEWAY_API_HOST: '0.0.0.0',
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
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';
|
||||
|
||||
const SHA = '1111111111111111111111111111111111111111';
|
||||
@@ -116,6 +116,14 @@ const createRepository = () => {
|
||||
|
||||
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', () => {
|
||||
it('builds, migrates, switches all gateway roles, verifies readiness, and publishes atomically', async () => {
|
||||
const workspace = await createReleaseWorkspace();
|
||||
|
||||
@@ -52,6 +52,7 @@ VITE_APP_BASE_PATH=/gateway \
|
||||
VITE_GATEWAY_API_URL=/gateway/api/trpc \
|
||||
VITE_GAME_API_URL_TEMPLATE='/{profile}/api/trpc' \
|
||||
VITE_GAME_WEB_URL_TEMPLATE='/{profile}/' \
|
||||
VITE_PREVIEW_ALLOWED_HOSTS=dev-sam-e2e.hided.net \
|
||||
pnpm --filter @sammo-ts/gateway-frontend build
|
||||
```
|
||||
|
||||
@@ -61,6 +62,7 @@ Game frontend는 profile별 값으로 build합니다.
|
||||
VITE_APP_BASE_PATH=/che \
|
||||
VITE_GAME_API_URL=/che/api/trpc \
|
||||
VITE_GAME_SSE_URL=/che/api/events \
|
||||
VITE_PREVIEW_ALLOWED_HOSTS=dev-sam-e2e.hided.net \
|
||||
pnpm --filter @sammo-ts/game-frontend build
|
||||
```
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ profile 범위 권한과 별개인 전역 `admin.releases.manage` 권한이 필
|
||||
- Gateway PostgreSQL, profile PostgreSQL, Redis와 PM2가 준비되어 있습니다.
|
||||
- controller가 `GATEWAY_DATABASE_URL`, `GATEWAY_DB_SCHEMA`, workspace와
|
||||
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 하위 호환성이
|
||||
유지됩니다.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user