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:
@@ -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;
|
||||
}
|
||||
+3
-1
@@ -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
-1
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user