feat(sdk): use config file as single source of truth, remove env var fallbacks (#19409)
## Summary - **Config as source of truth**: `~/.twenty/config.json` is now the single source of truth for SDK authentication — env var fallbacks have been removed from the config resolution chain. - **Test instance support**: `twenty server start --test` spins up a dedicated Docker instance on port 2021 with its own config (`config.test.json`), so integration tests don't interfere with the dev environment. - **API key auth for marketplace**: Removed `UserAuthGuard` from `MarketplaceResolver` so API key tokens (workspace-scoped) can call `installMarketplaceApp`. - **CI for example apps**: Added monorepo CI workflows for `hello-world` and `postcard` example apps to catch regressions. - **Simplified CI**: All `ci-create-app-e2e` and example app workflows now use a shared `spawn-twenty-app-dev-test` action (Docker-based) instead of building the server from source. Consolidated auth env vars to `TWENTY_API_URL` + `TWENTY_API_KEY`. - **Template publishing fix**: `create-twenty-app` template now correctly preserves `.github/` and `.gitignore` through npm publish (stored without leading dot, renamed after copy). ## Test plan - [x] CI SDK (lint, typecheck, unit, integration, e2e) — all green - [x] CI Example App Hello World — green - [x] CI Example App Postcard — green - [x] CI Create App E2E minimal — green - [x] CI Front, CI Server, CI Shared — green
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import { SERVER_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import {
|
||||
checkDockerRunning,
|
||||
CONTAINER_NAME,
|
||||
containerExists,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_TEST_PORT,
|
||||
getContainerPort,
|
||||
IMAGE,
|
||||
isContainerRunning,
|
||||
TEST_CONTAINER_NAME,
|
||||
} from '@/cli/utilities/server/docker-container';
|
||||
import {
|
||||
checkServerHealth,
|
||||
@@ -22,14 +25,17 @@ const HEALTH_TIMEOUT_MS = 180 * 1000;
|
||||
const MILESTONE_START = '==> START ';
|
||||
const MILESTONE_DONE = '==> DONE';
|
||||
|
||||
const waitForHealthy = async (port: number): Promise<boolean> => {
|
||||
const waitForHealthy = async (
|
||||
port: number,
|
||||
containerName: string,
|
||||
): Promise<boolean> => {
|
||||
const startTime = Date.now();
|
||||
const onProgress = (message: string) =>
|
||||
process.stdout.write(chalk.gray(message));
|
||||
|
||||
const logStream = spawn(
|
||||
'docker',
|
||||
['logs', '-f', '--since', '1s', CONTAINER_NAME],
|
||||
['logs', '-f', '--since', '1s', containerName],
|
||||
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
||||
);
|
||||
|
||||
@@ -98,6 +104,7 @@ const waitForHealthy = async (port: number): Promise<boolean> => {
|
||||
|
||||
export type ServerStartOptions = {
|
||||
port?: number;
|
||||
test?: boolean;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
@@ -109,12 +116,23 @@ export type ServerStartResult = {
|
||||
const innerServerStart = async (
|
||||
options: ServerStartOptions = {},
|
||||
): Promise<CommandResult<ServerStartResult>> => {
|
||||
const { onProgress } = options;
|
||||
const { onProgress, test: isTest } = options;
|
||||
|
||||
const existingUrl = await detectLocalServer(options.port);
|
||||
const containerName = isTest ? TEST_CONTAINER_NAME : CONTAINER_NAME;
|
||||
const defaultPort = isTest ? DEFAULT_TEST_PORT : DEFAULT_PORT;
|
||||
const volumeData = isTest
|
||||
? 'twenty-app-dev-test-data'
|
||||
: 'twenty-app-dev-data';
|
||||
const volumeStorage = isTest
|
||||
? 'twenty-app-dev-test-storage'
|
||||
: 'twenty-app-dev-storage';
|
||||
|
||||
const existingUrl = await detectLocalServer(options.port ?? defaultPort);
|
||||
|
||||
if (existingUrl) {
|
||||
const configService = new ConfigService();
|
||||
const configService = new ConfigService(
|
||||
isTest ? { configPath: getConfigPath(true) } : undefined,
|
||||
);
|
||||
|
||||
ConfigService.setActiveRemote('local');
|
||||
await configService.setConfig({ apiUrl: existingUrl });
|
||||
@@ -139,12 +157,12 @@ const innerServerStart = async (
|
||||
};
|
||||
}
|
||||
|
||||
if (isContainerRunning()) {
|
||||
const port = getContainerPort();
|
||||
if (isContainerRunning(containerName)) {
|
||||
const port = getContainerPort(containerName);
|
||||
|
||||
onProgress?.('Container is running, waiting for it to become healthy...');
|
||||
|
||||
const healthy = await waitForHealthy(port);
|
||||
const healthy = await waitForHealthy(port, containerName);
|
||||
|
||||
if (!healthy) {
|
||||
return {
|
||||
@@ -159,7 +177,9 @@ const innerServerStart = async (
|
||||
}
|
||||
|
||||
const url = `http://localhost:${port}`;
|
||||
const configService = new ConfigService();
|
||||
const configService = new ConfigService(
|
||||
isTest ? { configPath: getConfigPath(true) } : undefined,
|
||||
);
|
||||
|
||||
ConfigService.setActiveRemote('local');
|
||||
await configService.setConfig({ apiUrl: url });
|
||||
@@ -169,21 +189,21 @@ const innerServerStart = async (
|
||||
return { success: true, data: { port, url } };
|
||||
}
|
||||
|
||||
let port = options.port ?? DEFAULT_PORT;
|
||||
let port = options.port ?? defaultPort;
|
||||
|
||||
if (containerExists()) {
|
||||
const existingPort = getContainerPort();
|
||||
if (containerExists(containerName)) {
|
||||
const existingPort = getContainerPort(containerName);
|
||||
|
||||
if (existingPort !== port) {
|
||||
onProgress?.(
|
||||
`Existing container uses port ${existingPort}. Run 'yarn twenty server reset' first to change ports.`,
|
||||
`Existing container uses port ${existingPort}. Run 'yarn twenty server reset${isTest ? ' --test' : ''}' first to change ports.`,
|
||||
);
|
||||
}
|
||||
|
||||
port = existingPort;
|
||||
|
||||
onProgress?.('Starting existing container...');
|
||||
execSync(`docker start ${CONTAINER_NAME}`, { stdio: 'ignore' });
|
||||
execSync(`docker start ${containerName}`, { stdio: 'ignore' });
|
||||
} else {
|
||||
onProgress?.('Starting Twenty container...');
|
||||
|
||||
@@ -193,7 +213,7 @@ const innerServerStart = async (
|
||||
'run',
|
||||
'-d',
|
||||
'--name',
|
||||
CONTAINER_NAME,
|
||||
containerName,
|
||||
'-p',
|
||||
`${port}:${port}`,
|
||||
'-e',
|
||||
@@ -201,9 +221,9 @@ const innerServerStart = async (
|
||||
'-e',
|
||||
`SERVER_URL=http://localhost:${port}`,
|
||||
'-v',
|
||||
'twenty-app-dev-data:/data/postgres',
|
||||
`${volumeData}:/data/postgres`,
|
||||
'-v',
|
||||
'twenty-app-dev-storage:/app/packages/twenty-server/.local-storage',
|
||||
`${volumeStorage}:/app/packages/twenty-server/.local-storage`,
|
||||
IMAGE,
|
||||
],
|
||||
{ stdio: 'inherit' },
|
||||
@@ -222,7 +242,7 @@ const innerServerStart = async (
|
||||
|
||||
onProgress?.('Waiting for Twenty to be ready...');
|
||||
|
||||
const healthy = await waitForHealthy(port);
|
||||
const healthy = await waitForHealthy(port, containerName);
|
||||
|
||||
if (!healthy) {
|
||||
return {
|
||||
@@ -237,7 +257,9 @@ const innerServerStart = async (
|
||||
}
|
||||
|
||||
const url = `http://localhost:${port}`;
|
||||
const configService = new ConfigService();
|
||||
const configService = new ConfigService(
|
||||
isTest ? { configPath: getConfigPath(true) } : undefined,
|
||||
);
|
||||
|
||||
ConfigService.setActiveRemote('local');
|
||||
await configService.setConfig({ apiUrl: url });
|
||||
|
||||
Reference in New Issue
Block a user