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:
Charles Bochet
2026-04-08 06:49:10 +02:00
committed by GitHub
parent af3423dc6f
commit 15eb3e7edc
31 changed files with 523 additions and 313 deletions
@@ -1,11 +1,17 @@
import axios from 'axios';
import { SERVER_URL } from '@/cli/__tests__/constants/server-url.constant';
import { getServerUrl } from '@/cli/__tests__/constants/server-url.constant';
describe('Twenty Server Health Check (E2E)', () => {
const HEALTH_ENDPOINT = `${SERVER_URL}/healthz`;
let healthEndpoint: string;
beforeAll(async () => {
const serverUrl = await getServerUrl();
healthEndpoint = `${serverUrl}/healthz`;
});
it('should return 200 for health', async () => {
const response = await axios.get(HEALTH_ENDPOINT);
const response = await axios.get(healthEndpoint);
expect(response.status).toBe(200);
expect(response.data).toBeDefined();
});
@@ -1 +1,7 @@
export const SERVER_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
import { ConfigService } from '@/cli/utilities/config/config-service';
export const getServerUrl = async (): Promise<string> => {
const config = await new ConfigService().getConfig();
return config.apiUrl;
};
@@ -5,20 +5,44 @@ import { beforeAll } from 'vitest';
import { ensureDir } from '@/cli/utilities/file/fs-utils';
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
const testConfigPath = getConfigPath();
const testConfigPath = getConfigPath(true);
beforeAll(async () => {
const apiUrl = process.env.TWENTY_API_URL;
const token = process.env.TWENTY_API_KEY;
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Run: twenty server start --test\n' +
'Or set them in vitest env config.',
);
}
const response = await fetch(`${apiUrl}/healthz`).catch(() => null);
if (!response?.ok) {
throw new Error(
`Twenty server not reachable at ${apiUrl}. ` +
'Run: twenty server start --test',
);
}
await ensureDir(path.dirname(testConfigPath));
const configFile = {
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
await writeFile(
testConfigPath,
JSON.stringify(
{
remotes: {
local: { apiUrl, apiKey: token },
},
defaultRemote: 'local',
},
},
defaultRemote: 'local',
};
null,
2,
),
);
await writeFile(testConfigPath, JSON.stringify(configFile, null, 2));
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
});