Add twenty-partners on internal ci apps (#21975)

as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21975?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
martmull
2026-06-22 21:08:40 +02:00
committed by GitHub
parent d2083e7a1b
commit 1646bdf35e
8 changed files with 114 additions and 31 deletions
+3 -8
View File
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
application:
description: 'Internal app folder name to test (e.g. "twenty-linear"). Leave empty to test all non-excluded apps. Runs even if in CI_EXCLUDED_APPLICATIONS.'
description: 'Internal app folder name to test (e.g. "twenty-linear"). Leave empty to test all apps.'
required: false
default: ''
type: string
@@ -62,11 +62,6 @@ jobs:
const fs = require('fs');
const path = require('path');
const root = 'packages/twenty-apps/internal';
// CI_EXCLUDED_APPLICATIONS is temporary: it skips applications from CI to
// keep this PR small while the remaining apps are made CI-ready. Remove an
// app from this list once its checks pass, and delete the list entirely
// once every application is covered.
const CI_EXCLUDED_APPLICATIONS = ['twenty-meeting-bot', 'twenty-partners'];
const eventName = process.env.EVENT_NAME;
const changedFiles = JSON.parse(process.env.CHANGED_FILES || '[]');
const changedApps = new Set();
@@ -81,9 +76,9 @@ jobs:
.filter((name) => fs.existsSync(path.join(root, name, 'package.json')))
.filter((name) => {
if (eventName === 'workflow_dispatch') {
return requestedApp ? name === requestedApp : !CI_EXCLUDED_APPLICATIONS.includes(name);
return requestedApp ? name === requestedApp : true;
}
return !CI_EXCLUDED_APPLICATIONS.includes(name) && changedApps.has(name);
return changedApps.has(name);
})
.map((name) => {
const appPath = path.join(root, name);
@@ -1,20 +1,58 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["../../.oxlintrc.base.json"],
"plugins": ["typescript"],
"plugins": ["react", "typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": "off",
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/jsx-key": "off",
"react/display-name": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-useless-fragment": "off",
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
@@ -0,0 +1,56 @@
import { CoreApiClient } from 'twenty-client-sdk/core';
// Integration tests authenticate with the workspace API key, which the server
// rate-limits per workspace (API_RATE_LIMITING_LONG_LIMIT requests per window).
// The full suite issues more API calls than one window allows, so later calls
// would fail with "Limit reached". The server refuses a throttled request
// without consuming a token and its bucket refills continuously, so waiting
// briefly frees a token. Transparently retry rate-limited SDK calls after a
// short delay — patching the client prototype once covers every test.
const RATE_LIMIT_MAX_RETRIES = 30;
const RATE_LIMIT_RETRY_DELAY_MS = 700;
const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
const isRateLimitError = (error: unknown): boolean => {
const message = error instanceof Error ? error.message : String(error);
return /limit reached|tokens per/i.test(message);
};
type AsyncMethod = (...args: unknown[]) => Promise<unknown>;
const RATE_LIMIT_PATCHED = Symbol.for('twenty-partners.rateLimitRetryPatched');
const patchMethod = (methodName: 'query' | 'mutation' | 'uploadFile'): void => {
const prototype = CoreApiClient.prototype as unknown as Record<
string,
AsyncMethod
>;
const original = prototype[methodName];
if (typeof original !== 'function') return;
prototype[methodName] = async function rateLimitRetry(...args: unknown[]) {
let attempt = 0;
for (;;) {
try {
return await original.apply(this, args);
} catch (error) {
if (!isRateLimitError(error) || attempt >= RATE_LIMIT_MAX_RETRIES) {
throw error;
}
attempt += 1;
await sleep(RATE_LIMIT_RETRY_DELAY_MS);
}
}
};
};
const clientConstructor = CoreApiClient as unknown as Record<symbol, boolean>;
if (!clientConstructor[RATE_LIMIT_PATCHED]) {
patchMethod('query');
patchMethod('mutation');
patchMethod('uploadFile');
clientConstructor[RATE_LIMIT_PATCHED] = true;
}
@@ -4,7 +4,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// factory reference the mock fn safely despite hoisting.
const { queryMock } = vi.hoisted(() => ({ queryMock: vi.fn() }));
vi.mock('twenty-client-sdk/core', () => ({
CoreApiClient: vi.fn(() => ({ query: queryMock })),
CoreApiClient: vi.fn(function () {
return { query: queryMock };
}),
}));
import {
@@ -1,4 +1,4 @@
import { DatabaseEventPayload, defineLogicFunction } from 'twenty-sdk/define';
import { type DatabaseEventPayload, defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { ON_OPP_AUTO_MATCH_FN_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
@@ -1,4 +1,4 @@
import { InstallPayload, definePostInstallLogicFunction } from 'twenty-sdk/define';
import { type InstallPayload, definePostInstallLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: InstallPayload) => {
@@ -6,7 +6,7 @@
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "node",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
@@ -1,23 +1,14 @@
import { loadEnv } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
// Integration tests authenticate to a local Twenty server. Credentials are
// resolved from the shell env, then a gitignored .env.local in this directory
// (see .env.example). No API key is committed; if none is found, global-setup
// fails with a clear message.
const fileEnv = loadEnv('test', process.cwd(), 'TWENTY_');
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY =
process.env.TWENTY_API_KEY ??
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc';
const TWENTY_API_URL =
process.env.TWENTY_API_URL ?? fileEnv.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? fileEnv.TWENTY_API_KEY;
// Make env available to globalSetup (runs in the main process); test.env below
// covers the worker processes.
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
if (TWENTY_API_KEY) {
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
}
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
@@ -32,9 +23,10 @@ export default defineConfig({
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
setupFiles: ['src/__tests__/rate-limit-retry.setup.ts'],
env: {
TWENTY_API_URL,
...(TWENTY_API_KEY ? { TWENTY_API_KEY } : {}),
TWENTY_API_KEY,
},
},
});