Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f12fa68fb0 | ||
|
|
132a484f67 | ||
|
|
bfb84018c0 | ||
|
|
2f40d93e42 |
@@ -1,65 +0,0 @@
|
||||
# Twenty Development Rules
|
||||
|
||||
This directory contains Twenty's development guidelines and best practices. The rules are organized into several key categories:
|
||||
|
||||
## Guidelines Structure
|
||||
|
||||
### 1. Architecture and Structure
|
||||
- `architecture.md`: Project overview, technology stack, and infrastructure setup
|
||||
- `file-structure-guidelines.md`: File and directory organization patterns
|
||||
|
||||
### 2. Code Style and Development
|
||||
- `typescript-guidelines.md`: TypeScript best practices and conventions
|
||||
- `code-style-guidelines.md`: General coding standards and style guide
|
||||
|
||||
### 3. React Development
|
||||
- `react-general-guidelines.md`: Core React development principles and patterns
|
||||
- `react-state-management-guidelines.md`: State management approaches and best practices
|
||||
|
||||
### 4. Testing
|
||||
- `testing-guidelines.md`: Testing strategies, patterns, and best practices
|
||||
|
||||
### 5. Internationalization
|
||||
- `translations.md`: Translation workflow, i18n setup, and string management
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Frontend Commands
|
||||
```bash
|
||||
# Testing
|
||||
npx nx test twenty-front # Run unit tests
|
||||
npx nx storybook:build twenty-front # Build Storybook
|
||||
npx nx storybook:serve-and-test:static # Run Storybook tests
|
||||
|
||||
# Development
|
||||
npx nx lint twenty-front # Run linter
|
||||
npx nx typecheck twenty-front # Type checking
|
||||
npx nx run twenty-front:graphql:generate # Generate GraphQL types
|
||||
```
|
||||
|
||||
### Backend Commands
|
||||
```bash
|
||||
# Database
|
||||
npx nx database:reset twenty-server # Reset database
|
||||
npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
|
||||
# Development
|
||||
npx nx run twenty-server:start # Start the server
|
||||
npx nx run twenty-server:lint # Run linter (add --fix to auto-fix)
|
||||
npx nx run twenty-server:typecheck # Type checking
|
||||
npx nx run twenty-server:test # Run unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
|
||||
|
||||
# Migrations
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/metadata/migrations/[name] -d src/database/typeorm/metadata/metadata.datasource.ts
|
||||
|
||||
# Workspace
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f # Sync metadata
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
These rules are automatically attached to relevant files in your workspace through Cursor's context system. They help maintain consistency and quality across the Twenty codebase.
|
||||
|
||||
For the most up-to-date version of these guidelines, always refer to the files in this directory.
|
||||
@@ -1,97 +0,0 @@
|
||||
# Twenty Project Architecture
|
||||
|
||||
## Overview
|
||||
Twenty is an open-source CRM built with modern technologies, using TypeScript for both frontend and backend development. This document outlines the core architectural decisions and structure of the project.
|
||||
|
||||
## Monorepo Structure
|
||||
The project is organized as a monorepo using nx, with the following main packages:
|
||||
|
||||
### Main Packages
|
||||
- `packages/twenty-front`: Main Frontend application
|
||||
- Technology: React
|
||||
- Purpose: Provides the main user interface for the CRM
|
||||
- Key responsibilities: User interactions, state management, data display
|
||||
|
||||
- `packages/twenty-server`: Main Backend application
|
||||
- Technology: NestJS
|
||||
- Purpose: Handles business logic, data persistence, and API
|
||||
- Key responsibilities: Data processing, authentication, API endpoints
|
||||
|
||||
- `packages/twenty-website`: Marketing Website and Documentation
|
||||
- Technology: NextJS
|
||||
- Purpose: Public-facing website and documentation
|
||||
- Key responsibilities: Marketing content, documentation, SEO
|
||||
|
||||
- `packages/twenty-ui`: UI Component Library
|
||||
- Technology: React
|
||||
- Purpose: Shared UI components and design system
|
||||
- Key responsibilities: Reusable components, design consistency
|
||||
|
||||
- `packages/twenty-shared`: Shared Utilities
|
||||
- Purpose: Cross-package shared code between frontend and backend
|
||||
- Contents: Utils, constants, types, interfaces
|
||||
|
||||
## Core Technology Stack
|
||||
|
||||
### Package Management
|
||||
- Package Manager: yarn
|
||||
- Monorepo Tool: nx
|
||||
- Benefits: Consistent dependency management, shared configurations
|
||||
|
||||
### Database Layer
|
||||
- Primary Database: PostgreSQL
|
||||
- Schema Structure:
|
||||
- Core schema: Main application data
|
||||
- Metadata schema: Configuration and customization data
|
||||
- Workspace schemas: One schema per tenant, containing tenant-specific data
|
||||
- ORM Layer:
|
||||
- TypeORM: For core and metadata schemas
|
||||
- Purpose: Type-safe database operations for system data
|
||||
- Benefits: Strong typing, migration support
|
||||
- TwentyORM: For workspace schemas
|
||||
- Purpose: Manages tenant-specific entities and customizations
|
||||
- Benefits: Dynamic entity management, per-tenant customization
|
||||
- Example: Entities like CompanyWorkspaceEntity are managed per workspace
|
||||
|
||||
### State Management
|
||||
- Frontend State: Recoil
|
||||
- Purpose: Global state management
|
||||
- Use cases: User preferences, UI state, cached data
|
||||
|
||||
### Data Layer
|
||||
- API Technology: GraphQL
|
||||
- Client: Apollo Client
|
||||
- Purpose: Data fetching and caching
|
||||
- Benefits: Type safety, efficient data loading
|
||||
|
||||
### Infrastructure
|
||||
- Cache: Redis
|
||||
- Purpose: High-performance caching layer
|
||||
- Use cases: Session data, frequent queries
|
||||
|
||||
- Authentication: JWT
|
||||
- Purpose: Secure user authentication
|
||||
- Implementation: Token-based auth flow
|
||||
|
||||
- Queue System: BullMQ
|
||||
- Purpose: Background job processing
|
||||
- Use cases: Emails, exports, imports
|
||||
|
||||
- Storage: S3/Local Filesystem
|
||||
- Purpose: File storage and management
|
||||
- Flexibility: Configurable for cloud or local storage
|
||||
|
||||
### Testing Infrastructure
|
||||
- Backend Testing:
|
||||
- Framework: Jest
|
||||
- API Testing: Supertest
|
||||
- Coverage: Unit tests, integration tests
|
||||
|
||||
- Frontend Testing:
|
||||
- Framework: Jest
|
||||
- Component Testing: Storybook
|
||||
- API Mocking: MSW (Mock Service Worker)
|
||||
|
||||
- End-to-End Testing:
|
||||
- Framework: Playwright
|
||||
- Coverage: Critical user journeys
|
||||
@@ -1,259 +0,0 @@
|
||||
# Code Style Guidelines
|
||||
|
||||
## Core Code Style Principles
|
||||
Twenty emphasizes clean, readable, and maintainable code. This document outlines our code style conventions and best practices.
|
||||
|
||||
## Control Flow
|
||||
|
||||
### Early Returns
|
||||
- Use early returns to reduce nesting
|
||||
- Handle edge cases first
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const processUser = (user: User | null) => {
|
||||
if (!user) return null;
|
||||
if (!user.isActive) return null;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
};
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const processUser = (user: User | null) => {
|
||||
if (user) {
|
||||
if (user.isActive) {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
```
|
||||
|
||||
### No Nested Ternaries
|
||||
- Avoid nested ternary operators
|
||||
- Use if statements or early returns
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const getUserDisplay = (user: User) => {
|
||||
if (!user.name) return 'Anonymous';
|
||||
if (!user.isActive) return 'Inactive User';
|
||||
return user.name;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const getUserDisplay = (user: User) =>
|
||||
user.name
|
||||
? user.isActive
|
||||
? user.name
|
||||
: 'Inactive User'
|
||||
: 'Anonymous';
|
||||
```
|
||||
|
||||
### No Else-If Chains
|
||||
- Use switch statements or lookup objects
|
||||
- Keep conditions flat
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const getStatusColor = (status: Status): string => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'green';
|
||||
case 'warning':
|
||||
return 'yellow';
|
||||
case 'error':
|
||||
return 'red';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
// Or using a lookup object
|
||||
const statusColors: Record<Status, string> = {
|
||||
success: 'green',
|
||||
warning: 'yellow',
|
||||
error: 'red',
|
||||
default: 'gray',
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const getStatusColor = (status: Status): string => {
|
||||
if (status === 'success') {
|
||||
return 'green';
|
||||
} else if (status === 'warning') {
|
||||
return 'yellow';
|
||||
} else if (status === 'error') {
|
||||
return 'red';
|
||||
} else {
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Operators and Expressions
|
||||
|
||||
### Optional Chaining Over &&
|
||||
- Use optional chaining for null/undefined checks
|
||||
- Clearer intent and better type safety
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const userName = user?.name;
|
||||
const userAddress = user?.address?.street;
|
||||
|
||||
// ❌ Incorrect
|
||||
const userName = user && user.name;
|
||||
const userAddress = user && user.address && user.address.street;
|
||||
```
|
||||
|
||||
## Function Design
|
||||
|
||||
### Small Focused Functions
|
||||
- Keep functions small and single-purpose
|
||||
- Extract complex logic into helper functions
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const validateUser = (user: User) => {
|
||||
if (!isValidName(user.name)) return false;
|
||||
if (!isValidEmail(user.email)) return false;
|
||||
if (!isValidAge(user.age)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const isValidName = (name: string) => {
|
||||
return name.length >= 2 && /^[a-zA-Z\s]*$/.test(name);
|
||||
};
|
||||
|
||||
const isValidEmail = (email: string) => {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
};
|
||||
|
||||
const isValidAge = (age: number) => {
|
||||
return age >= 18 && age <= 120;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const validateUser = (user: User) => {
|
||||
if (user.name.length < 2 || !/^[a-zA-Z\s]*$/.test(user.name)) return false;
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(user.email)) return false;
|
||||
if (user.age < 18 || user.age > 120) return false;
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
## Naming and Documentation
|
||||
|
||||
### Clear Variable Names
|
||||
- Use descriptive, intention-revealing names
|
||||
- Avoid abbreviations unless common
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const isUserActive = user.status === 'active';
|
||||
const hasRequiredPermissions = user.permissions.includes('admin');
|
||||
const userDisplayName = user.name || 'Anonymous';
|
||||
|
||||
// ❌ Incorrect
|
||||
const active = user.status === 'active';
|
||||
const hasPerm = user.permissions.includes('admin');
|
||||
const udn = user.name || 'Anonymous';
|
||||
```
|
||||
|
||||
### No Console.logs in Commits
|
||||
- Remove all console.logs before committing
|
||||
- Use proper logging/error tracking in production
|
||||
```typescript
|
||||
// ❌ Incorrect - Don't commit these
|
||||
console.log('user:', user);
|
||||
console.log('debug:', someValue);
|
||||
|
||||
// ✅ Correct - Use proper logging
|
||||
logger.info('User action completed', { userId: user.id });
|
||||
logger.error('Operation failed', { error });
|
||||
```
|
||||
|
||||
### Minimal Comments
|
||||
- Write self-documenting code
|
||||
- Use comments only for complex business logic
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
// Calculate pro-rated amount based on billing cycle
|
||||
const calculateProRatedAmount = (amount: number, daysLeft: number, totalDays: number) => {
|
||||
return (amount * daysLeft) / totalDays;
|
||||
};
|
||||
|
||||
// ❌ Incorrect - Unnecessary comments
|
||||
// Get the user's name
|
||||
const getUserName = (user: User) => user.name;
|
||||
|
||||
// Check if user is active
|
||||
const isUserActive = (user: User) => user.status === 'active';
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Proper Error Handling
|
||||
- Use try-catch blocks appropriately
|
||||
- Provide meaningful error messages
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const fetchUserData = async (userId: string) => {
|
||||
try {
|
||||
const response = await api.get(`/users/${userId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch user data', {
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
throw new UserFetchError('Failed to fetch user data');
|
||||
}
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const fetchUserData = async (userId: string) => {
|
||||
try {
|
||||
const response = await api.get(`/users/${userId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log('error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Code Organization
|
||||
|
||||
### Logical Grouping
|
||||
- Group related code together
|
||||
- Maintain consistent organization
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
class UserService {
|
||||
// Properties
|
||||
private readonly api: Api;
|
||||
private readonly logger: Logger;
|
||||
|
||||
// Constructor
|
||||
constructor(api: Api, logger: Logger) {
|
||||
this.api = api;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
// Public methods
|
||||
public async getUser(id: string): Promise<User> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
public async updateUser(user: User): Promise<User> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Private helpers
|
||||
private validateUser(user: User): boolean {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,207 +0,0 @@
|
||||
# File Structure Guidelines
|
||||
|
||||
## Core File Structure Principles
|
||||
Twenty follows a modular and organized file structure that promotes maintainability and scalability. This document outlines our file organization conventions and best practices.
|
||||
|
||||
## Component Organization
|
||||
|
||||
### One Component Per File
|
||||
- Each component should have its own file
|
||||
- File name should match component name
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
// UserProfile.tsx
|
||||
export const UserProfile = () => {
|
||||
return <div>...</div>;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
// users.tsx
|
||||
export const UserProfile = () => {
|
||||
return <div>...</div>;
|
||||
};
|
||||
|
||||
export const UserList = () => {
|
||||
return <div>...</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Feature Modules
|
||||
- Place features in `modules/` directory
|
||||
- Group related components and logic
|
||||
```
|
||||
modules/
|
||||
├── users/
|
||||
│ ├── components/
|
||||
│ │ ├── UserList.tsx
|
||||
│ │ ├── UserCard.tsx
|
||||
│ │ └── UserProfile.tsx
|
||||
│ ├── hooks/
|
||||
│ │ └── useUser.ts
|
||||
│ ├── states/
|
||||
│ │ └── userStates.ts
|
||||
│ └── types/
|
||||
│ └── user.ts
|
||||
├── workspace/
|
||||
│ ├── components/
|
||||
│ ├── hooks/
|
||||
│ └── states/
|
||||
└── settings/
|
||||
├── components/
|
||||
├── hooks/
|
||||
└── states/
|
||||
```
|
||||
|
||||
### Hooks Organization
|
||||
- Place hooks in `hooks/` directory
|
||||
- Group by feature or global usage
|
||||
```
|
||||
hooks/
|
||||
├── useClickOutside.ts
|
||||
├── useDebounce.ts
|
||||
└── features/
|
||||
├── users/
|
||||
│ ├── useUserActions.ts
|
||||
│ └── useUserData.ts
|
||||
└── workspace/
|
||||
└── useWorkspaceSettings.ts
|
||||
```
|
||||
|
||||
### State Management
|
||||
- Place state definitions in `states/` directory
|
||||
- Organize by feature
|
||||
```
|
||||
states/
|
||||
├── global/
|
||||
│ ├── theme.ts
|
||||
│ └── navigation.ts
|
||||
├── users/
|
||||
│ ├── atoms.ts
|
||||
│ └── selectors.ts
|
||||
└── workspace/
|
||||
├── atoms.ts
|
||||
└── selectors.ts
|
||||
```
|
||||
|
||||
### Types Organization
|
||||
- Place types in `types/` directory
|
||||
- Group by domain or feature
|
||||
```
|
||||
types/
|
||||
├── common.ts
|
||||
├── api.ts
|
||||
└── features/
|
||||
├── user.ts
|
||||
├── workspace.ts
|
||||
└── settings.ts
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Component Files
|
||||
- Use PascalCase for component files
|
||||
- Use descriptive, feature-specific names
|
||||
```
|
||||
components/
|
||||
├── UserProfile.tsx
|
||||
├── UserProfileHeader.tsx
|
||||
└── UserProfileContent.tsx
|
||||
```
|
||||
|
||||
### Non-Component Files
|
||||
- Use camelCase for non-component files
|
||||
- Use clear, descriptive names
|
||||
```
|
||||
hooks/
|
||||
├── useClickOutside.ts
|
||||
└── useDebounce.ts
|
||||
|
||||
utils/
|
||||
├── dateFormatter.ts
|
||||
└── stringUtils.ts
|
||||
```
|
||||
|
||||
## Module Structure
|
||||
|
||||
### Feature Module Organization
|
||||
- Consistent structure across features
|
||||
- Clear separation of concerns
|
||||
```
|
||||
modules/users/
|
||||
├── components/
|
||||
│ ├── UserList/
|
||||
│ │ ├── UserList.tsx
|
||||
│ │ ├── UserListItem.tsx
|
||||
│ │ └── UserListHeader.tsx
|
||||
│ └── UserProfile/
|
||||
│ ├── UserProfile.tsx
|
||||
│ └── UserProfileHeader.tsx
|
||||
├── hooks/
|
||||
│ ├── useUserList.ts
|
||||
│ └── useUserProfile.ts
|
||||
├── states/
|
||||
│ ├── atoms.ts
|
||||
│ └── selectors.ts
|
||||
├── types/
|
||||
│ └── user.ts
|
||||
└── utils/
|
||||
└── userFormatter.ts
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Import Organization
|
||||
- Group imports by type
|
||||
- Maintain consistent order
|
||||
```typescript
|
||||
// External dependencies
|
||||
import { useState } from 'react';
|
||||
import { styled } from '@emotion/styled';
|
||||
|
||||
// Internal modules
|
||||
import { useUser } from '~/modules/users/hooks';
|
||||
import { userState } from '~/modules/users/states';
|
||||
|
||||
// Local imports
|
||||
import { UserAvatar } from './UserAvatar';
|
||||
import { type UserProfileProps } from './types';
|
||||
```
|
||||
|
||||
### Path Aliases
|
||||
- Use path aliases for better imports
|
||||
- Avoid deep relative paths
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
import { Button } from '~/components/Button';
|
||||
import { useUser } from '~/modules/users/hooks';
|
||||
|
||||
// ❌ Incorrect
|
||||
import { Button } from '../../../components/Button';
|
||||
import { useUser } from '../../../modules/users/hooks';
|
||||
```
|
||||
|
||||
### Component Co-location
|
||||
- Keep related files close together
|
||||
- Use index files for public APIs
|
||||
```
|
||||
components/UserProfile/
|
||||
├── UserProfile.tsx
|
||||
├── UserProfileHeader.tsx
|
||||
├── UserProfileContent.tsx
|
||||
├── styles.ts
|
||||
├── types.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Test File Location
|
||||
- Place test files next to implementation
|
||||
- Use `.test.ts` or `.spec.ts` extension
|
||||
```
|
||||
components/
|
||||
├── UserProfile.tsx
|
||||
├── UserProfile.test.tsx
|
||||
├── UserProfile.stories.tsx
|
||||
└── types.ts
|
||||
```
|
||||
@@ -1,220 +0,0 @@
|
||||
# React Guidelines
|
||||
|
||||
## Core React Principles
|
||||
Twenty follows modern React best practices with a focus on functional components and clean, maintainable code. This document outlines our React conventions and best practices.
|
||||
|
||||
## Component Structure
|
||||
|
||||
### Functional Components Only
|
||||
- Use functional components exclusively
|
||||
- No class components allowed
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
export const UserProfile = ({ user }: UserProfileProps) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<h1>{user.name}</h1>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
export class UserProfile extends React.Component<UserProfileProps> {
|
||||
render() {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<h1>{this.props.user.name}</h1>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Named Exports
|
||||
- Use named exports exclusively
|
||||
- No default exports
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
export const Button = ({ label }: ButtonProps) => {
|
||||
return <button>{label}</button>;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
export default function Button({ label }: ButtonProps) {
|
||||
return <button>{label}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## State and Effects
|
||||
|
||||
### Event Handlers Over useEffect
|
||||
- Prefer event handlers for state updates
|
||||
- Avoid useEffect for state synchronization
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const UserForm = () => {
|
||||
const handleSubmit = async (data: FormData) => {
|
||||
await updateUser(data);
|
||||
refreshUserList();
|
||||
};
|
||||
|
||||
return <Form onSubmit={handleSubmit} />;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const UserForm = () => {
|
||||
useEffect(() => {
|
||||
if (formData) {
|
||||
updateUser(formData);
|
||||
}
|
||||
}, [formData]);
|
||||
|
||||
return <Form />;
|
||||
};
|
||||
```
|
||||
|
||||
## Component Design
|
||||
|
||||
### Small, Focused Components
|
||||
- Keep components small and single-purpose
|
||||
- Extract reusable logic into custom hooks
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const UserCard = ({ user }: UserCardProps) => {
|
||||
return (
|
||||
<StyledCard>
|
||||
<UserAvatar user={user} />
|
||||
<UserInfo user={user} />
|
||||
<UserActions user={user} />
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const UserCard = ({ user }: UserCardProps) => {
|
||||
return (
|
||||
<StyledCard>
|
||||
{/* Too much logic in one component */}
|
||||
<img src={user.avatar} />
|
||||
<div>{user.name}</div>
|
||||
<div>{user.email}</div>
|
||||
<button onClick={() => handleEdit(user)}>Edit</button>
|
||||
<button onClick={() => handleDelete(user)}>Delete</button>
|
||||
{/* More complex logic... */}
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### Prop Naming
|
||||
- Use clear, descriptive prop names
|
||||
- Follow React conventions (onClick, isActive, etc.)
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
type ButtonProps = {
|
||||
onClick: () => void;
|
||||
isDisabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
type ButtonProps = {
|
||||
clickHandler: () => void;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
### Prop Destructuring
|
||||
- Destructure props with proper typing
|
||||
- Use TypeScript for prop types
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const Button = ({ onClick, isDisabled, children }: ButtonProps) => {
|
||||
return (
|
||||
<button onClick={onClick} disabled={isDisabled}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const Button = (props: ButtonProps) => {
|
||||
return (
|
||||
<button onClick={props.onClick} disabled={props.isDisabled}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memoization
|
||||
- Use memo for expensive computations
|
||||
- Avoid premature optimization
|
||||
```typescript
|
||||
// ✅ Correct - Complex computation
|
||||
const MemoizedChart = memo(({ data }: ChartProps) => {
|
||||
// Complex rendering logic
|
||||
return <ComplexChart data={data} />;
|
||||
});
|
||||
|
||||
// ❌ Incorrect - Unnecessary memoization
|
||||
const MemoizedText = memo(({ text }: { text: string }) => {
|
||||
return <span>{text}</span>;
|
||||
});
|
||||
```
|
||||
|
||||
### Event Handlers
|
||||
- Use callback refs for DOM manipulation
|
||||
- Memoize callbacks when needed
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const UserList = () => {
|
||||
const handleScroll = useCallback((event: UIEvent) => {
|
||||
// Complex scroll handling
|
||||
}, []);
|
||||
|
||||
return <div onScroll={handleScroll}>{/* content */}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Boundaries
|
||||
- Use error boundaries for component error handling
|
||||
- Provide meaningful fallback UIs
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const ErrorFallback = ({ error }: { error: Error }) => (
|
||||
<StyledError>
|
||||
<h2>Something went wrong</h2>
|
||||
<pre>{error.message}</pre>
|
||||
</StyledError>
|
||||
);
|
||||
|
||||
const SafeComponent = () => (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<ComponentThatMightError />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
```
|
||||
|
||||
### Loading States
|
||||
- Handle loading states gracefully
|
||||
- Provide meaningful loading indicators
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const UserProfile = () => {
|
||||
const { data: user, isLoading, error } = useUser();
|
||||
|
||||
if (isLoading) return <LoadingSpinner />;
|
||||
if (error) return <ErrorMessage error={error} />;
|
||||
if (!user) return <NotFound />;
|
||||
|
||||
return <UserProfileContent user={user} />;
|
||||
};
|
||||
```
|
||||
@@ -1,219 +0,0 @@
|
||||
# State Management Guidelines
|
||||
|
||||
## Core State Management Principles
|
||||
Twenty uses a combination of Recoil for global state and Apollo Client for server state management. This document outlines our state management conventions and best practices.
|
||||
|
||||
## Global State Management
|
||||
|
||||
### Recoil Usage
|
||||
- Use Recoil for global application state
|
||||
- Keep atoms small and focused
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
// states/theme.ts
|
||||
export const themeState = atom<'light' | 'dark'>({
|
||||
key: 'themeState',
|
||||
default: 'light',
|
||||
});
|
||||
|
||||
// states/user.ts
|
||||
export const userState = atom<User | null>({
|
||||
key: 'userState',
|
||||
default: null,
|
||||
});
|
||||
|
||||
// ❌ Incorrect
|
||||
// states/globalState.ts
|
||||
export const globalState = atom({
|
||||
key: 'globalState',
|
||||
default: {
|
||||
theme: 'light',
|
||||
user: null,
|
||||
settings: {},
|
||||
// ... many other unrelated pieces of state
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Atom Organization
|
||||
- Place atoms in the `states/` directory
|
||||
- Group related atoms in feature-specific files
|
||||
```typescript
|
||||
// states/workspace/atoms.ts
|
||||
export const workspaceIdState = atom<string>({
|
||||
key: 'workspaceIdState',
|
||||
default: '',
|
||||
});
|
||||
|
||||
export const workspaceSettingsState = atom<WorkspaceSettings>({
|
||||
key: 'workspaceSettingsState',
|
||||
default: defaultSettings,
|
||||
});
|
||||
```
|
||||
|
||||
## Server State Management
|
||||
|
||||
### Apollo Client Usage
|
||||
- Use Apollo Client for all GraphQL operations
|
||||
- Leverage Apollo's caching capabilities
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const { data, loading } = useQuery(GET_USER_QUERY, {
|
||||
variables: { id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
// ❌ Incorrect
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
fetch('/api/user/' + id).then(setUser);
|
||||
}, [id]);
|
||||
```
|
||||
|
||||
### Query Organization
|
||||
- Separate operation files
|
||||
- Use fragments for shared fields
|
||||
```typescript
|
||||
// queries/user.ts
|
||||
export const UserFragment = gql`
|
||||
fragment UserFields on User {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_USER = gql`
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
...UserFields
|
||||
}
|
||||
}
|
||||
${UserFragment}
|
||||
`;
|
||||
```
|
||||
|
||||
## State Management Best Practices
|
||||
|
||||
### Multiple Small Atoms
|
||||
- Prefer multiple small atoms over prop drilling
|
||||
- Keep atoms focused on specific features
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
export const selectedViewState = atom<string>({
|
||||
key: 'selectedViewState',
|
||||
default: '',
|
||||
});
|
||||
|
||||
export const viewFiltersState = atom<ViewFilters>({
|
||||
key: 'viewFiltersState',
|
||||
default: {},
|
||||
});
|
||||
|
||||
// ❌ Incorrect - Prop drilling
|
||||
const ViewContainer = ({ selectedView, filters, onViewChange }) => {
|
||||
return (
|
||||
<ViewHeader view={selectedView} onViewChange={onViewChange}>
|
||||
<ViewContent>
|
||||
<ViewFilters filters={filters} />
|
||||
</ViewContent>
|
||||
</ViewHeader>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### No useRef for State
|
||||
- Never use useRef for state management
|
||||
- Use proper state management tools
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const [count, setCount] = useState(0);
|
||||
// or
|
||||
const [count, setCount] = useRecoilState(countState);
|
||||
|
||||
// ❌ Incorrect
|
||||
const countRef = useRef(0);
|
||||
```
|
||||
|
||||
### Data Fetching
|
||||
- Extract data fetching to sibling components
|
||||
- Keep components focused on presentation
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const UserProfileContainer = () => {
|
||||
const { data, loading } = useQuery(GET_USER);
|
||||
if (loading) return <LoadingSpinner />;
|
||||
return <UserProfile user={data.user} />;
|
||||
};
|
||||
|
||||
const UserProfile = ({ user }: UserProfileProps) => {
|
||||
return <div>{user.name}</div>;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
const UserProfile = () => {
|
||||
const { data, loading } = useQuery(GET_USER);
|
||||
if (loading) return <LoadingSpinner />;
|
||||
return <div>{data.user.name}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Hook Usage
|
||||
- Use appropriate hooks for state access
|
||||
- Choose between useRecoilValue and useRecoilState based on needs
|
||||
```typescript
|
||||
// ✅ Correct - Read-only access
|
||||
const theme = useRecoilValue(themeState);
|
||||
|
||||
// ✅ Correct - Read-write access
|
||||
const [theme, setTheme] = useRecoilState(themeState);
|
||||
|
||||
// ❌ Incorrect - Using state setter when only reading
|
||||
const [theme, _] = useRecoilState(themeState);
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Selector Usage
|
||||
- Use selectors for derived state
|
||||
- Memoize complex calculations
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const filteredUsersState = selector({
|
||||
key: 'filteredUsersState',
|
||||
get: ({ get }) => {
|
||||
const users = get(usersState);
|
||||
const filter = get(userFilterState);
|
||||
return users.filter(user =>
|
||||
user.name.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// ❌ Incorrect - Calculating in component
|
||||
const UserList = () => {
|
||||
const users = useRecoilValue(usersState);
|
||||
const filter = useRecoilValue(userFilterState);
|
||||
const filteredUsers = users.filter(user =>
|
||||
user.name.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
return <List users={filteredUsers} />;
|
||||
};
|
||||
```
|
||||
|
||||
### Cache Management
|
||||
- Configure appropriate cache policies
|
||||
- Handle cache invalidation properly
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const [updateUser] = useMutation(UPDATE_USER, {
|
||||
update: (cache, { data }) => {
|
||||
cache.modify({
|
||||
id: cache.identify(data.updateUser),
|
||||
fields: {
|
||||
name: () => data.updateUser.name,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -1,253 +0,0 @@
|
||||
# Testing Guidelines
|
||||
|
||||
## Core Testing Principles
|
||||
Twenty follows a comprehensive testing strategy across all packages, ensuring high-quality, maintainable code. This document outlines our testing conventions and best practices.
|
||||
|
||||
## Testing Stack
|
||||
|
||||
### Backend Testing
|
||||
- Primary Framework: Jest
|
||||
- API Testing: Supertest
|
||||
- Coverage Requirements: 80% minimum
|
||||
|
||||
### Frontend Testing
|
||||
- Component Testing: Jest + React Testing Library
|
||||
- Visual Testing: Storybook
|
||||
- API Mocking: MSW (Mock Service Worker)
|
||||
|
||||
### End-to-End Testing
|
||||
- Framework: Playwright
|
||||
- Coverage: Critical user journeys
|
||||
- Cross-browser testing
|
||||
|
||||
## Test Organization
|
||||
|
||||
### Test File Location
|
||||
- Co-locate tests with implementation files
|
||||
- Use consistent naming patterns
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── UserProfile.tsx
|
||||
│ ├── UserProfile.test.tsx
|
||||
│ └── UserProfile.stories.tsx
|
||||
```
|
||||
|
||||
### Test File Naming
|
||||
- Use `.test.ts(x)` for unit/integration tests
|
||||
- Use `.spec.ts(x)` for E2E tests
|
||||
- Use `.stories.tsx` for Storybook stories
|
||||
|
||||
## Unit Testing
|
||||
|
||||
### Component Testing
|
||||
- Test behavior, not implementation
|
||||
- Use React Testing Library best practices
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
test('displays user name when provided', () => {
|
||||
render(<UserProfile user={{ name: 'John Doe' }} />);
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ❌ Incorrect - Testing implementation details
|
||||
test('sets the text content', () => {
|
||||
const { container } = render(<UserProfile user={{ name: 'John Doe' }} />);
|
||||
expect(container.querySelector('h1').textContent).toBe('John Doe');
|
||||
});
|
||||
```
|
||||
|
||||
### Hook Testing
|
||||
- Use `renderHook` from @testing-library/react-hooks
|
||||
- Test all possible states
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
test('useUser hook manages user state', () => {
|
||||
const { result } = renderHook(() => useUser());
|
||||
|
||||
act(() => {
|
||||
result.current.setUser({ id: '1', name: 'John' });
|
||||
});
|
||||
|
||||
expect(result.current.user).toEqual({ id: '1', name: 'John' });
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking
|
||||
- Mock external dependencies
|
||||
- Use jest.mock for module mocking
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
jest.mock('~/services/api', () => ({
|
||||
fetchUser: jest.fn().mockResolvedValue({ id: '1', name: 'John' }),
|
||||
}));
|
||||
|
||||
test('fetches and displays user', async () => {
|
||||
render(<UserProfile userId="1" />);
|
||||
expect(await screen.findByText('John')).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
### API Testing
|
||||
- Test complete request/response cycles
|
||||
- Use Supertest for backend API testing
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
describe('GET /api/users/:id', () => {
|
||||
it('returns user when found', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/users/1')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 404 when user not found', async () => {
|
||||
await request(app)
|
||||
.get('/api/users/999')
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## E2E Testing
|
||||
|
||||
### Test Structure
|
||||
- Organize by user journey
|
||||
- Use page objects for reusability
|
||||
```typescript
|
||||
// pages/login.ts
|
||||
export class LoginPage {
|
||||
async login(email: string, password: string) {
|
||||
await this.page.fill('[data-testid="email-input"]', email);
|
||||
await this.page.fill('[data-testid="password-input"]', password);
|
||||
await this.page.click('[data-testid="login-button"]');
|
||||
}
|
||||
}
|
||||
|
||||
// tests/auth.spec.ts
|
||||
test('user can login successfully', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.login('user@example.com', 'password');
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
||||
```
|
||||
|
||||
### Test Data
|
||||
- Use dedicated test environments
|
||||
- Reset state between tests
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
beforeEach(async () => {
|
||||
await resetDatabase();
|
||||
await seedTestData();
|
||||
});
|
||||
|
||||
test('user workflow', async ({ page }) => {
|
||||
// Test with clean, predictable state
|
||||
});
|
||||
```
|
||||
|
||||
## Visual Testing
|
||||
|
||||
### Storybook Guidelines
|
||||
- Create stories for all components
|
||||
- Document component variants
|
||||
```typescript
|
||||
// Button.stories.tsx
|
||||
export default {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
} as Meta;
|
||||
|
||||
export const Primary = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
label: 'Primary Button',
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary = {
|
||||
args: {
|
||||
variant: 'secondary',
|
||||
label: 'Secondary Button',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Visual Regression
|
||||
- Use Storybook's visual regression testing
|
||||
- Review changes carefully
|
||||
```typescript
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
preset: 'jest-image-snapshot',
|
||||
setupFilesAfterEnv: ['<rootDir>/setup-tests.ts'],
|
||||
};
|
||||
|
||||
// Button.visual.test.tsx
|
||||
describe('Button', () => {
|
||||
it('matches visual snapshot', async () => {
|
||||
const image = await page.screenshot();
|
||||
expect(image).toMatchImageSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Quality
|
||||
|
||||
### Test Data Attributes
|
||||
- Use data-testid for test selectors
|
||||
- Avoid selecting by CSS classes
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
<button data-testid="submit-button">Submit</button>
|
||||
|
||||
// In tests
|
||||
const button = screen.getByTestId('submit-button');
|
||||
|
||||
// ❌ Incorrect
|
||||
const button = container.querySelector('.submit-btn');
|
||||
```
|
||||
|
||||
### Assertion Best Practices
|
||||
- Use explicit assertions
|
||||
- Test both positive and negative cases
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
test('form validation', async () => {
|
||||
render(<UserForm />);
|
||||
|
||||
// Negative case
|
||||
await userEvent.click(screen.getByText('Submit'));
|
||||
expect(screen.getByText('Name is required')).toBeInTheDocument();
|
||||
|
||||
// Positive case
|
||||
await userEvent.type(screen.getByLabelText('Name'), 'John Doe');
|
||||
await userEvent.click(screen.getByText('Submit'));
|
||||
expect(screen.queryByText('Name is required')).not.toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
### Coverage Requirements
|
||||
- A new feature should have at least 80% coverage
|
||||
- Focus on critical paths
|
||||
- Run coverage reports in CI
|
||||
```typescript
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 80,
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -1,162 +0,0 @@
|
||||
# Translation Guidelines
|
||||
|
||||
## Core Translation Principles
|
||||
Twenty uses Lingui for internationalization (i18n) and Crowdin for translation management. This document outlines our translation workflow and best practices.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Translation Tools
|
||||
- **Framework**: @lingui/react
|
||||
- **Translation Management**: Crowdin
|
||||
- **Workflow**: GitHub Actions for automation
|
||||
|
||||
### Package Structure
|
||||
Translation files are managed in multiple packages:
|
||||
- `twenty-front`: Frontend translations
|
||||
- `twenty-server`: Backend translations
|
||||
- `twenty-emails`: Email template translations
|
||||
|
||||
## Translation Process
|
||||
|
||||
### Adding New Strings
|
||||
|
||||
#### Using Lingui Macros
|
||||
- Use `<Trans>` for components
|
||||
- Use `t` macro for strings outside JSX
|
||||
```typescript
|
||||
// ✅ Correct - In JSX
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
const WelcomeMessage = () => (
|
||||
<h1>
|
||||
<Trans>Welcome to Twenty</Trans>
|
||||
</h1>
|
||||
);
|
||||
|
||||
// ✅ Correct - Outside JSX
|
||||
import { t } from '@lingui/react/macro';
|
||||
|
||||
const getMessage = () => {
|
||||
return t`Welcome to Twenty`;
|
||||
};
|
||||
|
||||
// ❌ Incorrect - Don't use raw strings
|
||||
const WelcomeMessage = () => (
|
||||
<h1>Welcome to Twenty</h1>
|
||||
);
|
||||
```
|
||||
|
||||
### String Guidelines
|
||||
|
||||
#### What to Translate
|
||||
- User interface text
|
||||
- Error messages
|
||||
- Notifications
|
||||
- Email content
|
||||
|
||||
#### What Not to Translate
|
||||
- Variables
|
||||
- Test data/mocks
|
||||
|
||||
### Translation Workflow
|
||||
|
||||
#### 1. Extracting Translations
|
||||
- Automatically triggered on main branch changes
|
||||
- Can be manually triggered in GitHub Actions
|
||||
- Process:
|
||||
```bash
|
||||
# Extract new strings
|
||||
nx run twenty-front:lingui:extract
|
||||
nx run twenty-server:lingui:extract
|
||||
nx run twenty-emails:lingui:extract
|
||||
```
|
||||
|
||||
#### 2. Translation Management
|
||||
- Translations are managed in Crowdin
|
||||
- Changes are synced every 2 hours
|
||||
- Process:
|
||||
1. New strings are uploaded to Crowdin
|
||||
2. Translators work on translations
|
||||
3. Translations are pulled back to the repository
|
||||
|
||||
#### 3. Compiling Translations
|
||||
- Happens automatically in CI/CD
|
||||
- Required before running the application
|
||||
```bash
|
||||
# Compile translations
|
||||
nx run twenty-front:lingui:compile
|
||||
nx run twenty-server:lingui:compile
|
||||
nx run twenty-emails:lingui:compile
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### String Management
|
||||
|
||||
#### Use Placeholders
|
||||
- Use placeholders for dynamic content
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
<Trans>Hello {userName},</Trans>
|
||||
|
||||
// ❌ Incorrect - String concatenation
|
||||
<Trans>Hello </Trans>{userName},
|
||||
```
|
||||
|
||||
#### Provide Context
|
||||
- Lingui provides powerfulway to add context for translators but we don't use them as of today.
|
||||
|
||||
### Code Organization
|
||||
|
||||
#### Translation Files
|
||||
- Keep translation files organized by feature
|
||||
- Use consistent naming patterns
|
||||
```
|
||||
src/
|
||||
├── locales/
|
||||
│ ├── en/
|
||||
│ │ ├── messages.po
|
||||
│ │ └── messages.js
|
||||
│ └── fr/
|
||||
│ ├── messages.po
|
||||
│ └── messages.js
|
||||
```
|
||||
|
||||
### Quality Assurance
|
||||
|
||||
#### Strict Mode
|
||||
- Use --strict mode when compiling to identify missing translations
|
||||
|
||||
|
||||
#### Testing Translations
|
||||
- Test with different locales
|
||||
- Verify string interpolation
|
||||
- Check layout with different language lengths
|
||||
|
||||
## Automation
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
#### Pull Workflow
|
||||
- Runs every 2 hours
|
||||
- Downloads new translations from Crowdin
|
||||
- Creates PR if changes detected
|
||||
- Can be manually triggered with force pull option
|
||||
|
||||
#### Push Workflow
|
||||
- Runs on main branch changes
|
||||
- Extracts and uploads new strings
|
||||
- Compiles translations
|
||||
- Creates PR with changes
|
||||
|
||||
### Error Handling
|
||||
|
||||
#### Missing Translations
|
||||
- Development: Shown in original language
|
||||
- Production: Falls back to default language
|
||||
- Strict mode in CI catches missing translations
|
||||
|
||||
#### Compilation Errors
|
||||
- Addressed before merging
|
||||
- PR created for fixing missing translations
|
||||
- Automated testing in CI pipeline
|
||||
@@ -1,172 +0,0 @@
|
||||
# TypeScript Guidelines
|
||||
|
||||
## Core TypeScript Principles
|
||||
Twenty enforces strict TypeScript usage to ensure type safety and maintainable code. This document outlines our TypeScript conventions and best practices.
|
||||
|
||||
## Type Safety
|
||||
|
||||
### Strict Typing
|
||||
- **No 'any' type allowed**
|
||||
- TypeScript strict mode enabled
|
||||
- noImplicitAny enabled
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
function processUser(user: User) {
|
||||
return user.name;
|
||||
}
|
||||
|
||||
// ❌ Incorrect
|
||||
function processUser(user: any) {
|
||||
return user.name;
|
||||
}
|
||||
```
|
||||
|
||||
### Type Definitions
|
||||
|
||||
#### Types over Interfaces
|
||||
- Use `type` for all type definitions
|
||||
- Exception: When extending third-party interfaces
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
```
|
||||
|
||||
### String Literals over Enums
|
||||
- Use string literal unions instead of enums
|
||||
- Exception: GraphQL enums
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
type UserRole = 'admin' | 'user' | 'guest';
|
||||
|
||||
// ❌ Incorrect
|
||||
enum UserRole {
|
||||
Admin = 'admin',
|
||||
User = 'user',
|
||||
Guest = 'guest',
|
||||
}
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Component Props
|
||||
- Suffix component prop types with 'Props'
|
||||
- Keep props focused and single-purpose
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
type ButtonProps = {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: 'primary' | 'secondary';
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
type ButtonParameters = {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: 'primary' | 'secondary';
|
||||
};
|
||||
```
|
||||
|
||||
## Type Inference
|
||||
|
||||
### Leverage TypeScript Inference
|
||||
- Use type inference when types are clear
|
||||
- Explicitly type when inference is ambiguous
|
||||
```typescript
|
||||
// ✅ Correct - Clear inference
|
||||
const users = ['John', 'Jane']; // inferred as string[]
|
||||
|
||||
// ✅ Correct - Explicit typing needed
|
||||
const processUser = (user: User): UserResponse => {
|
||||
// Complex processing
|
||||
return response;
|
||||
};
|
||||
|
||||
// ❌ Incorrect - Unnecessary explicit typing
|
||||
const users: string[] = ['John', 'Jane'];
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Type Guards
|
||||
- Use type guards for runtime type checking
|
||||
- Prefer discriminated unions
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
type Success = {
|
||||
type: 'success';
|
||||
data: User;
|
||||
};
|
||||
|
||||
type Error = {
|
||||
type: 'error';
|
||||
message: string;
|
||||
};
|
||||
|
||||
type Result = Success | Error;
|
||||
|
||||
function handleResult(result: Result) {
|
||||
if (result.type === 'success') {
|
||||
// TypeScript knows result.data exists
|
||||
console.log(result.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generics
|
||||
- Use generics for reusable type patterns
|
||||
- Keep generic names descriptive
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
type ApiResponse<TData> = {
|
||||
data: TData;
|
||||
status: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
// ❌ Incorrect
|
||||
type ApiResponse<T> = {
|
||||
data: T;
|
||||
status: number;
|
||||
message: string;
|
||||
};
|
||||
```
|
||||
|
||||
### Type Exports
|
||||
- Export types when they're used across files
|
||||
- Keep type definitions close to their usage
|
||||
```typescript
|
||||
// types.ts
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
// UserComponent.tsx
|
||||
import { type User } from './types';
|
||||
```
|
||||
|
||||
### Utility Types
|
||||
- Leverage TypeScript utility types
|
||||
- Create custom utility types for repeated patterns
|
||||
```typescript
|
||||
// Built-in utility types
|
||||
type UserPartial = Partial<User>;
|
||||
type UserReadonly = Readonly<User>;
|
||||
|
||||
// Custom utility types
|
||||
type NonNullableProperties<T> = {
|
||||
[P in keyof T]: NonNullable<T[P]>;
|
||||
};
|
||||
```
|
||||
+4
-19
@@ -1,16 +1,8 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:prettier/recommended', 'plugin:lingui/recommended'],
|
||||
plugins: [
|
||||
'@nx',
|
||||
'prefer-arrow',
|
||||
'import',
|
||||
'unused-imports',
|
||||
'unicorn',
|
||||
'lingui',
|
||||
],
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
plugins: ['@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn'],
|
||||
rules: {
|
||||
'lingui/no-single-variables-to-translate': 'off',
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
|
||||
'no-control-regex': 0,
|
||||
@@ -37,10 +29,6 @@ module.exports = {
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -82,6 +70,7 @@ module.exports = {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
@@ -107,11 +96,7 @@ module.exports = {
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'*.spec.@(ts|tsx|js|jsx)',
|
||||
'*.integration-spec.@(ts|tsx|js|jsx)',
|
||||
'*.test.@(ts|tsx|js|jsx)',
|
||||
],
|
||||
files: ['*.spec.@(ts|tsx|js|jsx)', '*.test.@(ts|tsx|js|jsx)'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Twenty's Pledge
|
||||
|
||||
The contributors and maintainers of this project pledge to ensure a harassment-free experience for everyone in the community.
|
||||
The contributors and maintainers of this project pledge to ensure a harassment-free experience for everyone in the community. This commitment applies to individuals of all backgrounds, including age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity, experience, education, socio-economic status, nationality, appearance, race, religion. It also applies to individuals of all sexual identities and orientations.
|
||||
|
||||
The focus of both contributors and maintainers is on acting and interacting in ways that promote an open, welcoming, friendly, diverse, inclusive, and healthy community.
|
||||
|
||||
|
||||
+48
-27
@@ -1,70 +1,91 @@
|
||||
# Contributing to Twenty
|
||||
|
||||
Thanks for considering contributing to Twenty!
|
||||
|
||||
Please make sure to go through the [documentation](https://docs.twenty.com) before.
|
||||
Thank you for considering contributing to Twenty! All community contributions are welcome.
|
||||
|
||||
This guide outlines the process for contributing to this project. Please make sure to go through the [documentation](https://docs.twenty.com) before making your contribution.
|
||||
|
||||
|
||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation:
|
||||
> - Star the project
|
||||
> - Tweet about it
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
## Good first issues
|
||||
## Getting Started
|
||||
|
||||
Good first issues are a great way to start contributing and get familiar with the codebase. You can find them on by filtering on the [good first issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue) label.
|
||||
Good first issues are a great way to start contributing to the project and get familiar with the codebase. Here's how to find them:
|
||||
|
||||
## Issue assignment
|
||||
1. Visit the "[Issues](https://github.com/twentyhq/twenty/issues)" tab on the main [repository](https://github.com/twentyhq/twenty).
|
||||
2. Use the "Labels" filter and select "[Good First Issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue)" to see a list of beginner-friendly tasks.
|
||||
3. Choose an issue that interests you, fork the project, and start working on it. Once you solve and test the issue, open a PR for review.
|
||||
|
||||
Having multiple contributors address the same issue can cause frustration.
|
||||
Note: We are aware that having multiple contributors address the same issue can cause frustration. To prevent this, we adhere to a specific guideline: if a core team member has assigned an issue to a contributor, either as the issue assignee or through explicit assignment in the issue comments within the past three days, that contributor's pull request takes precedence. Otherwise, the first PR submitted will be given priority. This delay is reduced to one day for PR tagged with "size: minutes" and extended to a week for PR tagged "size: days".
|
||||
|
||||
To avoid conflicts, we follow these guidelines:
|
||||
1. If a core team member assigned you the issue within the last three days, your PR takes priority.
|
||||
2. Otherwise, the first submitted PR is prioritized.
|
||||
3. For "size: long" PRs, the assignment period extends to one week.
|
||||
Therefore, ensure you are assigned to an issue before beginning work on it.
|
||||
|
||||
Please ensure you're assigned to an issue before starting work.
|
||||
<br>
|
||||
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Fork the Repository:** Click on the 'Fork' button in the upper right corner of the repository's GitHub page. This will create a copy of the repository in your GitHub account.
|
||||
|
||||
|
||||
2. **Clone the Repository:** Clone your forked repository to your local machine using `git clone`.
|
||||
|
||||
|
||||
```shell
|
||||
git clone https://github.com/yourusername/twenty.git
|
||||
cd twenty
|
||||
```
|
||||
|
||||
3. **Create a New Branch:** Create a new branch for your changes instead of using the main branch.
|
||||
|
||||
```shell
|
||||
git checkout -b your-branch-name
|
||||
```
|
||||
|
||||
4. **Make Changes:** Make your desired changes and ensure that your code adheres to Twenty's coding standards.
|
||||
3. **Make Changes:** Make your desired changes and ensure that your code adheres to Twenty's coding standards.
|
||||
|
||||
|
||||
5. **Test Locally:** Test your changes locally to ensure they work as expected.
|
||||
4. **Test Locally:** Test your changes locally to ensure they work as expected.
|
||||
|
||||
|
||||
6. **Commit Changes:** Commit your changes with a clear and concise commit message.
|
||||
5. **Commit Changes:** Commit your changes with a clear and concise commit message.
|
||||
|
||||
|
||||
```shell
|
||||
git commit -m "Add your detailed description here"
|
||||
```
|
||||
6. **Push Changes:** Push your changes to your forked repository.
|
||||
|
||||
7. **Push Changes:** Push your changes to your forked repository.
|
||||
|
||||
```shell
|
||||
git push origin your-branch-name
|
||||
git push origin branch-name
|
||||
```
|
||||
|
||||
8. **Create a Pull Request:** Go to the original Twenty repository and create a pull request. Please provide a detailed description of your changes. Submitting a PR means you agree to the CLA.
|
||||
|
||||
9. **Code Review:** Your pull request will undergo a code review.
|
||||
|
||||
10. **Merge:** Once approved, maintainers will merge your pull request into the main repository.
|
||||
7. **Create a Pull Request:** Go to the original Twenty repository and create a pull request. Please provide a detailed description of your changes. To have your pull request accepted, you must sign a CLA.
|
||||
|
||||
|
||||
8. **Code Review:** Your pull request will undergo a code review. Note that you might need to make any necessary adjustments based on feedback.
|
||||
|
||||
|
||||
9. **Merge:** Once approved, maintainers will merge your pull request into the main repository.
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please note that by contributing to this project, you're expected to follow Twenty's [Code of Conduct](./CODE_OF_CONDUCT.md). All maintainers strive to maintain a welcoming, friendly, and inclusive community for all contributors.
|
||||
|
||||
<br>
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you face any issues or have suggestions, please feel free to (create an issue on Twenty's GitHub repository)[https://github.com/twentyhq/twenty/issues/new]. Please provide as much detail as possible.
|
||||
If you encounter any issues or have suggestions for improvements, please feel free to (create an issue on Twenty's GitHub repository)[https://github.com/twentyhq/twenty/issues/new]. When reporting issues, please provide as much detail as possible to help in understanding and addressing the problem effectively.
|
||||
|
||||
---
|
||||
|
||||
Thank you for considering contributing to Twenty. Your contributions help make Twenty's CRM platform even better!
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ name: Nx Affected CI
|
||||
inputs:
|
||||
parallel:
|
||||
required: false
|
||||
default: '3'
|
||||
types: [number]
|
||||
default: 3
|
||||
tag:
|
||||
required: false
|
||||
types: [string]
|
||||
tasks:
|
||||
required: true
|
||||
configuration:
|
||||
required: false
|
||||
default: 'ci'
|
||||
types: [string]
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -18,4 +18,4 @@ runs:
|
||||
uses: nrwl/nx-set-shas@v4
|
||||
- name: Run affected command
|
||||
shell: bash
|
||||
run: npx nx affected --nxBail --configuration=${{ inputs.configuration }} -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}'
|
||||
run: npx nx affected --nxBail --configuration=ci -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}'
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Restore cache
|
||||
inputs:
|
||||
key:
|
||||
required: true
|
||||
description: Prefix to the cache key
|
||||
additional-paths:
|
||||
required: false
|
||||
outputs:
|
||||
cache-primary-key:
|
||||
description: actions/cache/restore cache-primary-key outputs proxy
|
||||
value: ${{ steps.restore-cache.outputs.cache-primary-key }}
|
||||
cache-hit:
|
||||
description: String bool indicating whether cache has been directly or indirectly hit
|
||||
value: ${{ steps.restore-cache.outputs.cache-hit == 'true' || steps.restore-cache.outputs.cache-matched-key != '' }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Cache primary key builder
|
||||
id: cache-primary-key-builder
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CACHE_PRIMARY_KEY_PREFIX=${{ inputs.key }}-${{ github.ref_name }}" >> "${GITHUB_OUTPUT}"
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-cache
|
||||
with:
|
||||
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
|
||||
restore-keys: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-
|
||||
path: |
|
||||
.cache
|
||||
.nx/cache
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
${{ inputs.additional-paths }}
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Save cache
|
||||
inputs:
|
||||
key:
|
||||
required: true
|
||||
description: Primary key to the cache, should be retrieved from `cache-restore` composite action outputs.
|
||||
additional-paths:
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Save cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: ${{ inputs.key }}
|
||||
path: |
|
||||
.cache
|
||||
.nx/cache
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
${{ inputs.additional-paths }}
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Restore Tasks Cache CI
|
||||
inputs:
|
||||
tag:
|
||||
required: false
|
||||
types: [string]
|
||||
tasks:
|
||||
required: false
|
||||
types: [string]
|
||||
default: all
|
||||
suffix:
|
||||
required: false
|
||||
types: [string]
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Compute tasks key
|
||||
id: tasks-key
|
||||
shell: bash
|
||||
run: echo "key=${{ inputs.tasks }}" | tr , - >> $GITHUB_OUTPUT
|
||||
- name: Restore tasks cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
.nx/cache
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
key: tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-
|
||||
@@ -1,43 +1,23 @@
|
||||
name: Yarn Install
|
||||
inputs:
|
||||
node-version:
|
||||
required: false
|
||||
default: '18'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache primary key builder
|
||||
id: globals
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ACTION_SHELL=bash" >> "${GITHUB_OUTPUT}"
|
||||
echo "CACHE_KEY_PREFIX=node_modules-cache-node-${{ inputs.node-version }}-${{ hashFiles('yarn.lock') }}" >> "${GITHUB_OUTPUT}"
|
||||
echo 'PATH_TO_CACHE<<EOF' >> $GITHUB_OUTPUT
|
||||
echo "node_modules" >> $GITHUB_OUTPUT
|
||||
echo "packages/*/node_modules" >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
- name: Setup Node.js and get yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
- name: Restore node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache/restore@v4
|
||||
node-version: "18"
|
||||
cache: yarn
|
||||
- name: Cache node modules
|
||||
id: node-modules-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
|
||||
restore-keys: ${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
|
||||
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
|
||||
path: |
|
||||
node_modules
|
||||
packages/*/node_modules
|
||||
key: root-node_modules-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: root-node_modules-
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
|
||||
shell: ${{ steps.globals.outputs.ACTION_SHELL }}
|
||||
run: |
|
||||
yarn config set enableHardenedMode true
|
||||
yarn --immutable --check-cache
|
||||
- name: Save cache
|
||||
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
|
||||
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
|
||||
|
||||
shell: bash
|
||||
run: yarn --immutable --check-cache
|
||||
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||
@@ -5,7 +5,6 @@ on:
|
||||
- main
|
||||
jobs:
|
||||
deploy-main:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
|
||||
@@ -5,7 +5,6 @@ on:
|
||||
- 'v*'
|
||||
jobs:
|
||||
deploy-tag:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Changed files reusable workflow
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
files:
|
||||
required: true
|
||||
type: string
|
||||
outputs:
|
||||
any_changed:
|
||||
value: ${{ jobs.changed-files.outputs.any_changed }}
|
||||
|
||||
jobs:
|
||||
changed-files:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
any_changed: ${{ steps.changed-files.outputs.any_changed }}
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
@@ -3,24 +3,19 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-chrome-extension/**'
|
||||
pull_request:
|
||||
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-chrome-extension/**'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-chrome-extension/**
|
||||
chrome-extension-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VITE_SERVER_BASE_URL: http://localhost:3000
|
||||
@@ -31,18 +26,7 @@ jobs:
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Chrome Extension / Run build
|
||||
run: npx nx build twenty-chrome-extension
|
||||
ci-chrome-extension-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, chrome-extension-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
name: CI Demo check
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 7,19 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/twenty-e2e-testing
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
id: test
|
||||
run: yarn playwright test --grep "@demo-only"
|
||||
|
||||
- name: Upload report after tests
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 90
|
||||
|
||||
- name: Send Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: 'Demo check ${{ steps.test.outcome }} - check ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
|
||||
- name: Send email if demo is not working
|
||||
if: steps.test.outcome == 'failure'
|
||||
uses: dawidd6/action-send-mail@v3.12.0
|
||||
with:
|
||||
connection_url: ${{ secrets.MAIL_CONNECTION }}
|
||||
server_address: smtp.gmail.com
|
||||
server_port: 465
|
||||
secure: true
|
||||
username: ${{ secrets.MAIL_USERNAME }}
|
||||
subject: 'Demo is not working'
|
||||
from: 'Github CI Demo check'
|
||||
to: ${{ secrets.RECIPIENTS }}
|
||||
body: '<a href="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}">Link</a>'
|
||||
priority: high
|
||||
@@ -1,133 +0,0 @@
|
||||
name: CI E2E Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/**
|
||||
playwright.config.ts
|
||||
.github/workflows/ci-e2e.yaml
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true' && ( github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-e2e')))
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
# https://github.com/actions/runner-images/issues/70#issuecomment-589562148
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Check system resources
|
||||
run: |
|
||||
echo "Available memory:"
|
||||
free -h
|
||||
echo "Available disk space:"
|
||||
df -h
|
||||
echo "CPU info:"
|
||||
lscpu
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx nx setup twenty-e2e-testing
|
||||
|
||||
- name: Setup environment files
|
||||
run: |
|
||||
cp packages/twenty-front/.env.example packages/twenty-front/.env
|
||||
npx nx reset:env twenty-server
|
||||
|
||||
- name: Build frontend
|
||||
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
|
||||
|
||||
- name: Build server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Create and setup database
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npx nx start twenty-server &
|
||||
echo "Waiting for server to be ready..."
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
|
||||
|
||||
- name: Start frontend
|
||||
run: |
|
||||
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
|
||||
echo "Waiting for frontend to be ready..."
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
|
||||
|
||||
- name: Start worker
|
||||
run: |
|
||||
npx nx run twenty-server:worker &
|
||||
echo "Worker started"
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npx nx test twenty-e2e-testing
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: packages/twenty-e2e-testing/run_results/
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: packages/twenty-e2e-testing/playwright-report/
|
||||
retention-days: 30
|
||||
ci-e2e-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
+43
-107
@@ -3,30 +3,22 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-front/**'
|
||||
- 'packages/twenty-ui/**'
|
||||
pull_request:
|
||||
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-front/**'
|
||||
- 'packages/twenty-ui/**'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
STORYBOOK_BUILD_CACHE_KEY: storybook-build-depot-ubuntu-24.04-8-runner
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-front/**
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
front-sb-build:
|
||||
needs: [changed-files-check]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
@@ -37,94 +29,62 @@ jobs:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Diagnostic disk space issue
|
||||
run: df -h
|
||||
- name: Restore storybook build cache
|
||||
id: restore-storybook-build-cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
- name: Front / Restore Storybook Task Cache
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Front / Build storybook
|
||||
run: npx nx storybook:build twenty-front
|
||||
- name: Save storybook build cache
|
||||
uses: ./.github/workflows/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-storybook-build-cache.outputs.cache-primary-key }}
|
||||
front-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ci-8-cores
|
||||
needs: front-sb-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
storybook_scope: [modules, pages, performance]
|
||||
storybook_scope: [pages, modules]
|
||||
env:
|
||||
SHARD_COUNTER: 4
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
steps:
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Install Playwright
|
||||
run: cd packages/twenty-front && npx playwright install
|
||||
- name: Restore storybook build cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
- name: Front / Restore Storybook Task Cache
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Run storybook tests
|
||||
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }} --checkCoverage=false
|
||||
- name: Rename coverage file
|
||||
run: mv packages/twenty-front/coverage/storybook/coverage-storybook.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 1
|
||||
name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
|
||||
path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
merge-reports-and-check-coverage:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: front-sb-test
|
||||
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }}
|
||||
front-sb-test-performance:
|
||||
runs-on: ci-8-cores
|
||||
env:
|
||||
PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
|
||||
strategy:
|
||||
matrix:
|
||||
storybook_scope: [modules, pages, performance]
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
|
||||
merge-multiple: true
|
||||
path: coverage-artifacts
|
||||
- name: Merge coverage reports
|
||||
run: |
|
||||
mkdir -p ${{ env.PATH_TO_COVERAGE }}
|
||||
npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
|
||||
- name: Checking coverage
|
||||
run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
|
||||
- name: Install Playwright
|
||||
run: cd packages/twenty-front && npx playwright install
|
||||
- name: Front / Write .env
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Run storybook tests
|
||||
run: npx nx storybook:serve-and-test:static:performance twenty-front
|
||||
front-chromatic-deployment:
|
||||
timeout-minutes: 30
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push'
|
||||
needs: front-sb-build
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
@@ -135,10 +95,11 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore storybook build cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
- name: Front / Restore Storybook Task Cache
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
run: |
|
||||
cd packages/twenty-front
|
||||
@@ -147,13 +108,9 @@ jobs:
|
||||
- name: Publish to Chromatic
|
||||
run: npx nx run twenty-front:chromatic:ci
|
||||
front-task:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
TASK_CACHE_KEY: front-task-${{ matrix.task }}
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
@@ -168,11 +125,11 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore ${{ matrix.task }} cache
|
||||
id: restore-task-cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
- name: Front / Restore ${{ matrix.task }} task cache
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.TASK_CACHE_KEY }}
|
||||
tag: scope:frontend
|
||||
tasks: ${{ matrix.task }}
|
||||
- name: Reset .env
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
@@ -182,25 +139,4 @@ jobs:
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: ${{ matrix.task }}
|
||||
- name: Save ${{ matrix.task }} cache
|
||||
uses: ./.github/workflows/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-task-cache.outputs.cache-primary-key }}
|
||||
ci-front-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
front-task,
|
||||
front-chromatic-deployment,
|
||||
merge-reports-and-check-coverage,
|
||||
front-sb-test,
|
||||
front-sb-build,
|
||||
]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
tasks: ${{ matrix.task }}
|
||||
@@ -15,7 +15,6 @@ on:
|
||||
|
||||
jobs:
|
||||
create_pr:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -6,7 +6,6 @@ on:
|
||||
|
||||
jobs:
|
||||
tag_and_release:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
|
||||
steps:
|
||||
|
||||
@@ -3,51 +3,32 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-server/**'
|
||||
- 'packages/twenty-emails/**'
|
||||
pull_request:
|
||||
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-server/**'
|
||||
- 'packages/twenty-emails/**'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
SERVER_SETUP_CACHE_KEY: server-setup
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
packages/twenty-shared/**
|
||||
server-setup:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
server-test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
image: twentycrm/twenty-postgres
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -55,167 +36,23 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore server setup
|
||||
id: restore-server-setup-cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
- name: Server / Restore Tasks Cache
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
tag: scope:backend
|
||||
- name: Server / Run lint & typecheck
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: lint,typecheck
|
||||
- name: Server / Build
|
||||
run: npx nx build twenty-server
|
||||
- name: Server / Write .env
|
||||
run: npx nx reset:env twenty-server
|
||||
- name: Server / Create DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:init:prod
|
||||
npx nx run twenty-server:database:migrate:prod
|
||||
- name: Worker / Run
|
||||
run: |
|
||||
timeout 30s npx nx run twenty-server:worker || exit_code=$?
|
||||
if [ $exit_code -eq 124 ]; then
|
||||
# If timeout was reached (exit code 124), consider it a success
|
||||
exit 0
|
||||
elif [ $exit_code -ne 0 ]; then
|
||||
# If worker failed for other reasons, fail the build
|
||||
exit $exit_code
|
||||
fi
|
||||
- name: Server / Check for Pending Migrations
|
||||
run: |
|
||||
METADATA_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate metadata-migration-check -d src/database/typeorm/metadata/metadata.datasource.ts || true)
|
||||
|
||||
CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true)
|
||||
|
||||
METADATA_MIGRATION_FILE=$(ls packages/twenty-server/*metadata-migration-check.ts 2>/dev/null || echo "")
|
||||
CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$METADATA_MIGRATION_FILE" ] || [ -n "$CORE_MIGRATION_FILE" ]; then
|
||||
echo "::error::Unexpected migration files were generated. Please create a proper migration manually."
|
||||
echo "$METADATA_MIGRATION_OUTPUT"
|
||||
echo "$CORE_MIGRATION_OUTPUT"
|
||||
|
||||
rm -f packages/twenty-server/*metadata-migration-check.ts packages/twenty-server/*core-migration-check.ts
|
||||
|
||||
exit 1
|
||||
fi
|
||||
- name: GraphQL / Check for Pending Generation
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
# Run GraphQL generation commands
|
||||
npx nx run twenty-front:graphql:generate
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
|
||||
# Check if any files were modified
|
||||
if ! git diff --quiet; then
|
||||
echo "::error::GraphQL schema changes detected. Please run 'npx nx run twenty-front:graphql:generate' and 'npx nx run twenty-front:graphql:generate --configuration=metadata' and commit the changes."
|
||||
exit 1
|
||||
fi
|
||||
- name: Save server setup
|
||||
uses: ./.github/workflows/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-server-setup-cache.outputs.cache-primary-key }}
|
||||
|
||||
server-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: server-setup
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore server setup
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
- name: Server / Run Tests
|
||||
- name: Server / Run tests
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: test
|
||||
|
||||
server-integration-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: server-setup
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
NODE_ENV: test
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Update .env.test for billing
|
||||
run: |
|
||||
echo "IS_BILLING_ENABLED=true" >> .env.test
|
||||
echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
|
||||
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
|
||||
echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
|
||||
echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
|
||||
- name: Restore server setup
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
- name: Server / Build
|
||||
run: npx nx build twenty-server
|
||||
- name: Build dependencies
|
||||
run: |
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-emails
|
||||
- name: Server / Create Test DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
- name: Server / Run Integration Tests
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: 'test:integration'
|
||||
configuration: 'with-db-reset'
|
||||
- name: Server / Upload reset-logs file
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reset-logs
|
||||
path: reset-logs.log
|
||||
ci-server-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: [changed-files-check, server-setup, server-test, server-integration-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
- name: Server / Write .env
|
||||
run: npx nx reset:env twenty-server
|
||||
- name: Worker / Run
|
||||
run: MESSAGE_QUEUE_TYPE=sync npx nx worker twenty-server
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
name: CI Shared
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-shared/**
|
||||
shared-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: ${{ matrix.task }}
|
||||
ci-shared-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, shared-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -1,93 +1,59 @@
|
||||
name: 'Test Docker Compose'
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
paths:
|
||||
- 'packages/twenty-docker/**'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-docker/**
|
||||
docker-compose.yml
|
||||
test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run compose
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
# change image to localbuild using yq
|
||||
yq eval 'del(.services.server.image)' -i docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
|
||||
yq eval '.services.server.restart = "no"' -i docker-compose.yml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
echo "Setting up .env file..."
|
||||
cp .env.example .env
|
||||
echo "Generating secrets..."
|
||||
echo "# === Randomly generated secrets ===" >>.env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
|
||||
- name: Run compose
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
# change image to localbuild using yq
|
||||
yq eval 'del(.services.server.image)' -i docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
|
||||
yq eval '.services.server.restart = "no"' -i docker-compose.yml
|
||||
|
||||
echo "Docker compose up..."
|
||||
docker compose up -d || {
|
||||
echo "Docker compose failed to start"
|
||||
docker compose logs
|
||||
exit 1
|
||||
}
|
||||
docker compose logs db server -f &
|
||||
pid=$!
|
||||
yq eval 'del(.services.db.image)' -i docker-compose.yml
|
||||
yq eval '.services.db.build.context = "../../"' -i docker-compose.yml
|
||||
yq eval '.services.db.build.dockerfile = "./packages/twenty-docker/twenty-postgres/Dockerfile"' -i docker-compose.yml
|
||||
|
||||
echo "Waiting for database to start..."
|
||||
count=0
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ]; do
|
||||
sleep 1;
|
||||
count=$((count+1));
|
||||
if [ $(docker inspect --format='{{.State.Status}}' twenty-db-1) = "exited" ]; then
|
||||
echo "Database exited"
|
||||
docker compose logs db
|
||||
exit 1
|
||||
fi
|
||||
if [ $count -gt 300 ]; then
|
||||
echo "Failed to start database after 5 minutes"
|
||||
docker compose logs db
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for database... (${count}/60)"
|
||||
done
|
||||
echo "Setting up .env file..."
|
||||
cp .env.example .env
|
||||
echo "Generating secrets..."
|
||||
echo "# === Randomly generated secrets ===" >>.env
|
||||
echo "ACCESS_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "LOGIN_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "REFRESH_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "FILE_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env
|
||||
|
||||
echo "Waiting for server to start..."
|
||||
count=0
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
sleep 1;
|
||||
count=$((count+1));
|
||||
if [ $(docker inspect --format='{{.State.Status}}' twenty-server-1) = "exited" ]; then
|
||||
echo "Server exited"
|
||||
docker compose logs server
|
||||
exit 1
|
||||
fi
|
||||
if [ $count -gt 300 ]; then
|
||||
echo "Failed to start server after 5 minutes"
|
||||
docker compose logs server
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for server... (${count}/300s)"
|
||||
done
|
||||
working-directory: ./packages/twenty-docker/
|
||||
ci-test-docker-compose-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
echo "Starting server..."
|
||||
docker compose up -d
|
||||
docker compose logs db server -f &
|
||||
pid=$!
|
||||
|
||||
echo "Waiting for server to start..."
|
||||
count=0
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
sleep 1;
|
||||
count=$((count+1));
|
||||
if [ $(docker inspect --format='{{.State.Status}}' twenty-server-1) = "exited" ]; then
|
||||
echo "Server exited"
|
||||
exit 1
|
||||
fi
|
||||
if [ $count -gt 300 ]; then
|
||||
echo "Failed to start server"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
working-directory: ./packages/twenty-docker/
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: CI Tinybird
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-tinybird/**'
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-tinybird/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: tinybirdco/ci/.github/workflows/ci.yml@main
|
||||
with:
|
||||
data_project_dir: packages/twenty-tinybird
|
||||
secrets:
|
||||
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
|
||||
tb_host: https://api.eu-central-1.aws.tinybird.co
|
||||
@@ -15,13 +15,10 @@ permissions:
|
||||
statuses: write
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
# We don't cancel in-progress because this workflow is triggered on
|
||||
# pull_request_target, which means the ref can be the same accross two PRs.
|
||||
# cancel-in-progress: true
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
danger-js:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'closed'
|
||||
steps:
|
||||
@@ -34,7 +31,6 @@ jobs:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
|
||||
|
||||
congratulate:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'closed' && github.event.pull_request.merged == true
|
||||
steps:
|
||||
|
||||
@@ -3,69 +3,37 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-website/**'
|
||||
pull_request:
|
||||
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-website/**'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-website/**
|
||||
website-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
image: twentycrm/twenty-postgres
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
POSTGRES_PASSWORD: twenty
|
||||
POSTGRES_USER: twenty
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Server / Create DB
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
|
||||
- name: Website / Run migrations
|
||||
run: npx nx database:migrate twenty-website
|
||||
run: npx nx database:migrate twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
|
||||
DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default
|
||||
- name: Website / Build Website
|
||||
run: npx nx build twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
|
||||
KEYSTATIC_GITHUB_CLIENT_ID: xxx
|
||||
KEYSTATIC_GITHUB_CLIENT_SECRET: xxx
|
||||
KEYSTATIC_SECRET: xxx
|
||||
NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG: xxx
|
||||
ci-website-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, website-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default
|
||||
@@ -1,131 +0,0 @@
|
||||
# Pull down translations from Crowdin every two hours or when triggered manually.
|
||||
# When force_pull input is true, translations will be pulled regardless of compilation status.
|
||||
|
||||
name: 'Pull translations from Crowdin'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *' # Every two hours.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of compilation status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of compilation status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pull_translations:
|
||||
name: Pull translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
- name: Setup i18n branch
|
||||
run: |
|
||||
git fetch origin i18n || true
|
||||
git checkout -B i18n origin/i18n || git checkout -b i18n
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
# Strict mode fails if there are missing translations.
|
||||
- name: Compile translations
|
||||
id: compile_translations_strict
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile --strict
|
||||
npx nx run twenty-emails:lingui:compile --strict
|
||||
npx nx run twenty-front:lingui:compile --strict
|
||||
continue-on-error: true
|
||||
|
||||
- name: Stash any changes before pulling translations
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add .
|
||||
git stash
|
||||
|
||||
- name: Pull translations from Crowdin
|
||||
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
auto_approve_imported: false
|
||||
import_eq_suggestions: false
|
||||
download_sources: false
|
||||
push_sources: false
|
||||
skip_untranslated_strings: false
|
||||
skip_untranslated_files: false
|
||||
push_translations: true
|
||||
create_pull_request: false
|
||||
skip_ref_checkout: true
|
||||
dryrun_action: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CROWDIN_PROJECT_ID: '1'
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
# As the files are extracted from a Docker container, they belong to root:root
|
||||
# We need to fix this before the next steps
|
||||
- name: Fix file permissions
|
||||
run: sudo chown -R runner:docker .
|
||||
|
||||
- name: Compile translations
|
||||
id: compile_translations
|
||||
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
git status
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add .
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: compile translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
if git diff --name-only origin/main..HEAD | grep -q .; then
|
||||
gh pr create -B main -H i18n --title 'i18n - translations' --body 'Created by Github action' || true
|
||||
else
|
||||
echo "No file differences between branches, skipping PR creation"
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,104 +0,0 @@
|
||||
name: 'Push translations to Crowdin'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
extract_translations:
|
||||
name: Extract and upload translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: main
|
||||
|
||||
- name: Setup i18n branch
|
||||
run: |
|
||||
git fetch origin i18n || true
|
||||
git checkout -B i18n origin/i18n || git checkout -b i18n
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Extract translations
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:extract
|
||||
npx nx run twenty-emails:lingui:extract
|
||||
npx nx run twenty-front:lingui:extract
|
||||
|
||||
- name: Check and commit extracted files
|
||||
id: check_extract_changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add .
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: extract translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Compile translations
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
|
||||
- name: Check and commit compiled files
|
||||
id: check_compile_changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add .
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: compile translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Push changes and create remote branch if needed
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n
|
||||
|
||||
- name: Upload missing translations
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
env:
|
||||
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
|
||||
CROWDIN_PROJECT_ID: 1
|
||||
|
||||
# Visit https://crowdin.com/settings#api-key to create this token
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
- name: Create a pull request
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
if git diff --name-only origin/main..HEAD | grep -q .; then
|
||||
gh pr create -B main -H i18n --title 'i18n - translations' --body 'Created by Github action' || true
|
||||
else
|
||||
echo "No file differences between branches, skipping PR creation"
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+1
-16
@@ -18,7 +18,6 @@
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.vercel
|
||||
.swc
|
||||
|
||||
**/**/logs/**
|
||||
|
||||
@@ -27,19 +26,5 @@ dist
|
||||
storybook-static
|
||||
*.tsbuildinfo
|
||||
.eslintcache
|
||||
.cache
|
||||
.nyc_output
|
||||
test-results/
|
||||
dump.rdb
|
||||
.tinyb
|
||||
|
||||
.notes
|
||||
/data/
|
||||
/.devenv/
|
||||
/.direnv/
|
||||
/.pre-commit-config.yaml
|
||||
/.envrc
|
||||
/devenv.nix
|
||||
/flake.lock
|
||||
/flake.nix
|
||||
|
||||
.crowdin.yml
|
||||
Vendored
+1
-2
@@ -14,7 +14,6 @@
|
||||
"styled-components.vscode-styled-components",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"xyc.vscode-mdx-preview",
|
||||
"yoavbls.pretty-ts-errors",
|
||||
"ms-playwright.playwright"
|
||||
"yoavbls.pretty-ts-errors"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+7
-37
@@ -6,11 +6,12 @@
|
||||
"name": "twenty-server - start debug",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeVersion": "18",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:start"
|
||||
"twenty-server:start",
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
@@ -21,11 +22,12 @@
|
||||
"name": "twenty-server - worker debug",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeVersion": "18",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:worker"
|
||||
"twenty-server:worker",
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
@@ -43,44 +45,12 @@
|
||||
"run",
|
||||
"twenty-server:command",
|
||||
"my-command",
|
||||
"--my-parameter value"
|
||||
"--my-parameter value",
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"console": "internalConsole",
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server/"
|
||||
},
|
||||
{
|
||||
"name": "Playwright Test current file",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["nx", "test", "twenty-e2e-testing", "${file}"],
|
||||
"console": "integratedTerminal",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"envFile": "${workspaceFolder}/packages/twenty-e2e-testing/.env"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "twenty-server - debug integration test file (to launch with test file open)",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:jest",
|
||||
"--",
|
||||
"--config",
|
||||
"./jest-integration.config.ts",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+1
-6
@@ -45,10 +45,5 @@
|
||||
"search.exclude": {
|
||||
"**/.yarn": true,
|
||||
},
|
||||
"eslint.debug": true,
|
||||
"files.associations": {
|
||||
".cursorrules": "markdown"
|
||||
},
|
||||
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}"
|
||||
}
|
||||
"eslint.debug": true
|
||||
}
|
||||
|
||||
Vendored
+2
-14
@@ -17,16 +17,8 @@
|
||||
"path": "../packages/twenty-front"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-ui",
|
||||
"path": "../packages/twenty-ui"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-emails",
|
||||
"path": "../packages/twenty-emails"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-shared",
|
||||
"path": "../packages/twenty-shared"
|
||||
"name": "packages/twenty-postgres",
|
||||
"path": "../packages/twenty-postgres"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-server",
|
||||
@@ -44,10 +36,6 @@
|
||||
"name": "tools/eslint-rules",
|
||||
"path": "../tools/eslint-rules"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-e2e-testing",
|
||||
"path": "../packages/twenty-e2e-testing"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
+893
File diff suppressed because one or more lines are too long
Vendored
-925
File diff suppressed because one or more lines are too long
+1
-5
@@ -1,9 +1,5 @@
|
||||
enableConstraintsChecks: true
|
||||
|
||||
enableHardenedMode: true
|
||||
|
||||
enableInlineHunks: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.4.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
|
||||
This project is mostly licensed under the GNU General Public License (GPL) as described below. However, certain files within this project are licensed under a different commercial license. These files are clearly marked with the following comment at the top of the file: /* @license Enterprise */
|
||||
Files with this comment are not licensed under the aGPL v3, but instead are subject to the commercial license terms defined later in this file.
|
||||
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
@@ -664,47 +659,3 @@ specific requirements.
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
|
||||
------------------------------------
|
||||
|
||||
|
||||
|
||||
The Twenty.com Commercial License (the “Commercial License”)
|
||||
Copyright (c) 2023-present Twenty.com, PBC
|
||||
|
||||
With regard to Twenty's Software:
|
||||
|
||||
This part of the software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Terms available
|
||||
at https://twenty.com/legal/terms, or other agreements governing
|
||||
the use of the Software, as mutually agreed by you and Twenty.com, PBC ("Twenty"),
|
||||
and otherwise have a valid Twenty Enterprise Edition subscription
|
||||
for the correct number of hosts and seats as defined in the Commercial Terms.
|
||||
Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Twenty and/or its licensors (as applicable) retain all right, title and interest in
|
||||
and to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Commercial Subscription for the correct number of hosts and seats.
|
||||
Notwithstanding the foregoing, you may copy and modify the Software for development
|
||||
and testing purposes, without requiring a subscription. You agree that Twenty.Com and/or
|
||||
its licensors (as applicable) retain all right, title and interest in and to all such
|
||||
modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Twenty Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
@@ -1,20 +1,26 @@
|
||||
postgres-on-docker:
|
||||
docker run -d \
|
||||
--name twenty_pg \
|
||||
-e PGUSER_SUPERUSER=postgres \
|
||||
-e PGPASSWORD_SUPERUSER=postgres \
|
||||
-e ALLOW_NOSSL=true \
|
||||
-v twenty_db_data:/home/postgres/pgdata \
|
||||
-p 5432:5432 \
|
||||
twentycrm/twenty-postgres-spilo:latest
|
||||
@echo "Waiting for PostgreSQL to be ready..."
|
||||
@until docker exec twenty_pg psql -U postgres -d postgres \
|
||||
-c 'SELECT pg_is_in_recovery();' 2>/dev/null | grep -q 'f'; do \
|
||||
sleep 1; \
|
||||
done
|
||||
docker exec twenty_pg psql -U postgres -d postgres \
|
||||
-c "CREATE DATABASE \"default\" WITH OWNER postgres;" \
|
||||
-c "CREATE DATABASE \"test\" WITH OWNER postgres;"
|
||||
docker-dev-build:
|
||||
make -C packages/twenty-docker dev-build
|
||||
|
||||
redis-on-docker:
|
||||
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
|
||||
docker-dev-up:
|
||||
make -C packages/twenty-docker dev-up
|
||||
|
||||
docker-dev-start:
|
||||
make -C packages/twenty-docker dev-start
|
||||
|
||||
docker-dev-stop:
|
||||
make -C packages/twenty-docker dev-stop
|
||||
|
||||
docker-dev-sh:
|
||||
make -C packages/twenty-docker dev-sh
|
||||
|
||||
postgres-on-docker:
|
||||
make -C packages/twenty-postgres provision-on-docker
|
||||
|
||||
postgres-on-macos-arm:
|
||||
make -C packages/twenty-postgres provision-on-macos-arm
|
||||
|
||||
postgres-on-macos-intel:
|
||||
make -C packages/twenty-postgres provision-on-macos-intel
|
||||
|
||||
postgres-on-linux:
|
||||
make -C packages/twenty-postgres provision-on-linux
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
</p>
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM </h3>
|
||||
<p align="center">Tailored to your unique business needs</p>
|
||||
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://twenty.com/developers">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a><p>
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://twenty.com/developers">📚 Documentation</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a><p>
|
||||
<br />
|
||||
|
||||
|
||||
@@ -21,33 +22,44 @@
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<br>
|
||||
|
||||
We’ve spent thousands of hours grappling with traditional CRMs like Pipedrive and Salesforce to align them with our business needs, only to end up frustrated — customizations are complex and the closed ecosystems of these platforms can feel restrictive.
|
||||
|
||||
We felt the need for a CRM platform that empowers rather than constrains. We believe the next great CRM will come from the open-source community. We’ve packed Twenty with powerful features to give you full control and help you run your business efficiently.
|
||||
|
||||
<br>
|
||||
|
||||
# Installation
|
||||
# Demo
|
||||
Go to <a href="https://demo.twenty.com/">demo.twenty.com</a> and login with the following credentials:
|
||||
```
|
||||
email: noah@demo.dev
|
||||
password: Applecar2025
|
||||
```
|
||||
|
||||
See:
|
||||
See also:
|
||||
🚀 [Self-hosting](https://twenty.com/developers/section/self-hosting)
|
||||
🖥️ [Local Setup](https://twenty.com/developers/local-setup)
|
||||
|
||||
# Does the world need another CRM?
|
||||
# Why Choose Twenty?
|
||||
We understand that the CRM landscape is vast. So why should you choose us?
|
||||
|
||||
We built Twenty for three reasons:
|
||||
⛓️ **Full control, Full Freedom:** Contribute, self-host, fork. Break free from vendor lock-in and join us in shaping the open future of CRM.
|
||||
|
||||
**CRMs are too expensive, and users are trapped.** Companies use locked-in customer data to hike prices. It shouldn't be that way.
|
||||
📊 **Data, Your Way:** The days when the role of CRM platforms was to shift manual data entries to a database are over. Now, the data is already there. CRM 2.0 should be built around your data, allowing you to access and visualize any existing sources, not forcing you to retrofit your data into predefined objects on a remote cloud.
|
||||
|
||||
**A fresh start is required to build a better experience.** We can learn from past mistakes and craft a cohesive experience inspired by new UX patterns from tools like Notion, Airtable or Linear.
|
||||
🎨 **Effortlessly Intuitive:** We set out to create something that we ourselves would always enjoy using. The main application draws inspiration from Notion, a tool known for its user-friendly interface and customization capabilities.
|
||||
<br>
|
||||
|
||||
**We believe in Open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
|
||||
<br>
|
||||
|
||||
# What You Can Do With Twenty
|
||||
We're currently developing Twenty's beta version.
|
||||
We're currently in the development phase of Twenty's alpha version.
|
||||
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
Please feel free to flag any specific need you have need by creating an issue.
|
||||
|
||||
Below are a few features we have implemented to date:
|
||||
Below are some features we have implemented to date:
|
||||
|
||||
+ [Add, filter, sort, edit, and track customers](#add-filter-sort-edit-and-track-customers)
|
||||
+ [Create one or several opportunities for each company](#create-one-or-several-opportunities-for-each-company)
|
||||
@@ -55,7 +67,6 @@ Below are a few features we have implemented to date:
|
||||
+ [Create tasks on records](#create-tasks-on-records)
|
||||
+ [Navigate quickly through the app using keyboard shortcuts and search](#navigate-quickly-through-the-app-using-keyboard-shortcuts-and-search)
|
||||
|
||||
|
||||
## Add, filter, sort, edit, and track customers:
|
||||
|
||||
<p align="center">
|
||||
@@ -136,27 +147,24 @@ Below are a few features we have implemented to date:
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
# Stack
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Nx](https://nx.dev/)
|
||||
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
|
||||
- [React](https://reactjs.org/), with [Recoil](https://recoiljs.org/) and [Emotion](https://emotion.sh/)
|
||||
- [Greptile](https://greptile.com) for code reviews.
|
||||
- [Lingui](https://lingui.dev/) and [Crowdin](https://twenty.crowdin.com/twenty) for translations.
|
||||
# What's In Store
|
||||
|
||||
Here’s what you can look forward to:
|
||||
|
||||
⏳ **Frequent updates:** We’re shipping fast! Expect regular updates and new features that enhance your experience.
|
||||
|
||||
🔗 **Extensibility:** We’re putting the power in your hands. Soon, you’ll have the tools to extend and customize Twenty with plugins and more.
|
||||
|
||||
<br>
|
||||
|
||||
# Join the Community
|
||||
|
||||
- Star the repo
|
||||
- Subscribe to releases (watch -> custom -> releases)
|
||||
- Join [discussions](https://github.com/twentyhq/twenty/discussions) and track [issues](https://github.com/twentyhq/twenty/issues)
|
||||
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
|
||||
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
#
|
||||
# Basic Crowdin CLI configuration
|
||||
# See https://crowdin.github.io/crowdin-cli/configuration for more information
|
||||
# See https://support.crowdin.com/developer/configuration-file/ for all available options
|
||||
#
|
||||
|
||||
#
|
||||
# Defines whether to preserve the original directory structure in the Crowdin project
|
||||
# Recommended to set to true
|
||||
#
|
||||
"preserve_hierarchy": true
|
||||
|
||||
#
|
||||
# Files configuration.
|
||||
# See https://support.crowdin.com/developer/configuration-file/ for all available options
|
||||
#
|
||||
files: [
|
||||
{
|
||||
#
|
||||
# Source files filter
|
||||
# e.g. "/resources/en/*.json"
|
||||
#
|
||||
"source": "**/en.po",
|
||||
|
||||
#
|
||||
# Translation files filter
|
||||
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
||||
#
|
||||
"translation": "%original_path%/%locale%.po",
|
||||
}
|
||||
]
|
||||
@@ -44,10 +44,10 @@ function on_exit {
|
||||
trap on_exit EXIT
|
||||
|
||||
# Use environment variables VERSION and BRANCH, with defaults if not set
|
||||
version=${VERSION:-$(curl -s "https://hub.docker.com/v2/repositories/twentycrm/twenty/tags" | grep -o '"name":"[^"]*"' | grep -v 'latest' | cut -d'"' -f4 | sort -V | tail -n1)}
|
||||
branch=${BRANCH:-$(curl -s https://api.github.com/repos/twentyhq/twenty/tags | grep '"name":' | head -n 1 | cut -d '"' -f 4)}
|
||||
version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)}
|
||||
branch=${BRANCH:-main}
|
||||
|
||||
echo "🚀 Using docker version $version and Github branch $branch"
|
||||
echo "🚀 Using version $version and branch $branch"
|
||||
|
||||
dir_name="twenty"
|
||||
function ask_directory {
|
||||
@@ -72,7 +72,7 @@ done
|
||||
echo "📁 Creating directory '$dir_name'"
|
||||
mkdir -p "$dir_name" && cd "$dir_name" || { echo "❌ Failed to create/access directory '$dir_name'"; exit 1; }
|
||||
|
||||
# Copy twenty/packages/twenty-docker/docker-compose.yml in it
|
||||
# Copy the twenty/packages/twenty-docker/docker-compose.yml file in it
|
||||
echo -e "\t• Copying docker-compose.yml"
|
||||
curl -sLo docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/docker-compose.yml
|
||||
|
||||
@@ -90,11 +90,13 @@ else
|
||||
fi
|
||||
|
||||
# Generate random strings for secrets
|
||||
echo "# === Randomly generated secret ===" >> .env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
|
||||
echo "" >> .env
|
||||
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> .env
|
||||
echo "# === Randomly generated secrets ===" >>.env
|
||||
echo "ACCESS_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "LOGIN_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "REFRESH_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "FILE_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "" >>.env
|
||||
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env
|
||||
|
||||
echo -e "\t• .env configuration completed"
|
||||
|
||||
@@ -110,10 +112,8 @@ if command -v nc &> /dev/null; then
|
||||
read -p "Enter a new port number: " new_port
|
||||
if [[ $(uname) == "Darwin" ]]; then
|
||||
sed -i '' "s/$port:$port/$new_port:$port/g" docker-compose.yml
|
||||
sed -E -i '' "s|^SERVER_URL=http://localhost:[0-9]+|SERVER_URL=http://localhost:$new_port|g" .env
|
||||
else
|
||||
sed -i'' "s/$port:$port/$new_port:$port/g" docker-compose.yml
|
||||
sed -E -i'' "s|^SERVER_URL=http://localhost:[0-9]+|SERVER_URL=http://localhost:$new_port|g" .env
|
||||
fi
|
||||
port=$new_port
|
||||
done
|
||||
@@ -14,7 +14,6 @@
|
||||
"!{projectRoot}/**/tsconfig.spec.json",
|
||||
"!{projectRoot}/**/*.test.(ts|tsx)",
|
||||
"!{projectRoot}/**/*.spec.(ts|tsx)",
|
||||
"!{projectRoot}/**/*.integration-spec.ts",
|
||||
"!{projectRoot}/**/__tests__/*"
|
||||
],
|
||||
"production": [
|
||||
@@ -48,8 +47,7 @@
|
||||
"configurations": {
|
||||
"ci": { "cacheStrategy": "content" },
|
||||
"fix": { "fix": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -65,8 +63,7 @@
|
||||
"configurations": {
|
||||
"ci": { "cacheStrategy": "content" },
|
||||
"fix": { "write": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"typecheck": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -77,8 +74,7 @@
|
||||
},
|
||||
"configurations": {
|
||||
"watch": { "watch": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
@@ -112,15 +108,15 @@
|
||||
"storybook:build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["^default", "excludeTests"],
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
|
||||
"command": "storybook build",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"storybook:serve:dev": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -152,14 +148,12 @@
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"commands": [
|
||||
"test-storybook --url http://localhost:{args.port} --maxWorkers=3 --coverage --coverageDirectory={args.coverageDir} --shard={args.shard}",
|
||||
"nx storybook:coverage {projectName} --coverageDir={args.coverageDir} --checkCoverage={args.checkCoverage}"
|
||||
"test-storybook --url http://localhost:{args.port} --maxWorkers=3 --coverage --coverageDirectory={args.coverageDir}",
|
||||
"nx storybook:coverage {projectName} --coverageDir={args.coverageDir}"
|
||||
],
|
||||
"shard": "1/1",
|
||||
"parallel": false,
|
||||
"coverageDir": "coverage/storybook",
|
||||
"port": 6006,
|
||||
"checkCoverage": true
|
||||
"port": 6006
|
||||
}
|
||||
},
|
||||
"storybook:test:no-coverage": {
|
||||
@@ -168,7 +162,7 @@
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"commands": [
|
||||
"test-storybook --url http://localhost:{args.port} --maxWorkers=2"
|
||||
"test-storybook --url http://localhost:{args.port} --maxWorkers=3"
|
||||
],
|
||||
"port": 6006
|
||||
}
|
||||
@@ -186,10 +180,9 @@
|
||||
"!{projectRoot}/coverage/storybook/coverage-storybook.json"
|
||||
],
|
||||
"options": {
|
||||
"command": "npx nyc report --reporter={args.reporter} --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage={args.checkCoverage} --cwd={projectRoot}",
|
||||
"command": "npx nyc report --reporter={args.reporter} --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage --cwd={projectRoot}",
|
||||
"coverageDir": "coverage/storybook",
|
||||
"reporter": "lcov",
|
||||
"checkCoverage": true
|
||||
"reporter": "lcov"
|
||||
},
|
||||
"configurations": {
|
||||
"text": { "reporter": "text" }
|
||||
@@ -199,10 +192,17 @@
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": [
|
||||
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --shard={args.shard} --checkCoverage={args.checkCoverage} --port={args.port} --configuration={args.scope}'"
|
||||
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port}'"
|
||||
],
|
||||
"port": 6006
|
||||
}
|
||||
},
|
||||
"storybook:serve-and-test:static:performance": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": [
|
||||
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:dev {projectName} --configuration=performance --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test:no-coverage {projectName} --port={args.port} --configuration=performance'"
|
||||
],
|
||||
"shard": "1/1",
|
||||
"checkCoverage": true,
|
||||
"port": 6006
|
||||
}
|
||||
},
|
||||
|
||||
+38
-49
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@air/react-drag-to-select": "^5.0.8",
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@apollo/server": "^4.7.3",
|
||||
"@aws-sdk/client-lambda": "^3.614.0",
|
||||
"@aws-sdk/client-s3": "^3.363.0",
|
||||
"@aws-sdk/client-sts": "^3.744.0",
|
||||
"@aws-sdk/credential-providers": "^3.363.0",
|
||||
"@blocknote/mantine": "^0.22.0",
|
||||
"@blocknote/react": "^0.22.0",
|
||||
"@blocknote/server-util": "0.17.1",
|
||||
"@blocknote/core": "^0.12.1",
|
||||
"@blocknote/react": "^0.12.2",
|
||||
"@chakra-ui/accordion": "^2.3.0",
|
||||
"@chakra-ui/system": "^2.6.0",
|
||||
"@codesandbox/sandpack-react": "^2.13.5",
|
||||
"@dagrejs/dagre": "^1.1.2",
|
||||
"@docusaurus/core": "^3.1.0",
|
||||
"@docusaurus/preset-classic": "^3.1.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@envelop/on-resolve": "^4.1.0",
|
||||
@@ -26,7 +26,6 @@
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
"@nestjs/apollo": "^11.0.5",
|
||||
"@nestjs/axios": "^3.0.1",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
@@ -38,21 +37,25 @@
|
||||
"@nestjs/passport": "^9.0.3",
|
||||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/terminus": "^9.2.2",
|
||||
"@nestjs/typeorm": "^10.0.0",
|
||||
"@nivo/calendar": "^0.84.0",
|
||||
"@nivo/core": "^0.84.0",
|
||||
"@nx/eslint-plugin": "^17.2.8",
|
||||
"@octokit/graphql": "^7.0.2",
|
||||
"@ptc-org/nestjs-query-core": "^4.2.0",
|
||||
"@ptc-org/nestjs-query-typeorm": "4.2.1-alpha.2",
|
||||
"@react-email/components": "0.0.32",
|
||||
"@react-email/render": "0.0.17",
|
||||
"@sentry/node": "^8",
|
||||
"@sentry/profiling-node": "^8",
|
||||
"@sentry/react": "^8",
|
||||
"@react-email/components": "0.0.12",
|
||||
"@react-email/render": "0.0.10",
|
||||
"@sentry/node": "^7.99.0",
|
||||
"@sentry/profiling-node": "^1.3.4",
|
||||
"@sentry/react": "^7.88.0",
|
||||
"@sentry/tracing": "^7.99.0",
|
||||
"@sniptt/guards": "^0.2.0",
|
||||
"@stoplight/elements": "^8.0.5",
|
||||
"@storybook/icons": "^1.2.9",
|
||||
"@swc/jest": "^0.2.29",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tabler/icons-react": "^2.44.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/facepaint": "^1.2.5",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
@@ -62,37 +65,34 @@
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-microsoft": "^1.0.3",
|
||||
"@wyw-in-js/vite": "^0.5.3",
|
||||
"@xyflow/react": "^12.4.2",
|
||||
"add": "^2.0.6",
|
||||
"addressparser": "^1.0.1",
|
||||
"afterframe": "^1.0.2",
|
||||
"apollo-server-express": "^3.12.0",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.6.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"body-parser": "^1.20.2",
|
||||
"bullmq": "^5.40.0",
|
||||
"bullmq": "^4.14.0",
|
||||
"bytes": "^3.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cron-validate": "^1.4.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"danger-plugin-todos": "^1.3.1",
|
||||
"dataloader": "^2.2.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
"debounce": "^2.0.0",
|
||||
"deep-equal": "^2.2.2",
|
||||
"docusaurus-node-polyfills": "^1.0.0",
|
||||
"dompurify": "^3.0.11",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"drizzle-orm": "^0.29.3",
|
||||
"esbuild-plugin-svgr": "^2.1.0",
|
||||
"facepaint": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"framer-motion": "^11.18.0",
|
||||
"framer-motion": "^10.12.17",
|
||||
"googleapis": "105",
|
||||
"graphiql": "^3.1.1",
|
||||
"graphql": "16.8.0",
|
||||
@@ -118,7 +118,6 @@
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"lodash.compact": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.identity": "^3.0.0",
|
||||
"lodash.isempty": "^4.4.0",
|
||||
@@ -166,13 +165,14 @@
|
||||
"react-hotkeys-hook": "^4.4.4",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-imask": "^7.6.0",
|
||||
"react-intersection-observer": "^9.15.1",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-phone-number-input": "^3.3.4",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.4.4",
|
||||
"react-textarea-autosize": "^8.4.1",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"reactflow": "^11.11.3",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-behead": "^3.1.0",
|
||||
@@ -182,13 +182,12 @@
|
||||
"scroll-into-view": "^1.16.2",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.32.1",
|
||||
"slash": "^5.1.0",
|
||||
"stripe": "^17.3.1",
|
||||
"stripe": "^14.17.0",
|
||||
"ts-key-enum": "^2.0.12",
|
||||
"tslib": "^2.3.0",
|
||||
"tsup": "^8.2.4",
|
||||
"tsup": "^8.0.1",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.3.3",
|
||||
"typeorm": "^0.3.20",
|
||||
"use-context-selector": "^2.0.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
@@ -201,12 +200,13 @@
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@babel/preset-typescript": "^7.24.6",
|
||||
"@crxjs/vite-plugin": "^1.0.14",
|
||||
"@docusaurus/module-type-aliases": "^3.1.0",
|
||||
"@docusaurus/tsconfig": "3.1.0",
|
||||
"@graphql-codegen/cli": "^3.3.1",
|
||||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
"@nestjs/testing": "^9.0.0",
|
||||
@@ -219,7 +219,6 @@
|
||||
"@nx/storybook": "18.3.3",
|
||||
"@nx/vite": "18.3.3",
|
||||
"@nx/web": "18.3.3",
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@sentry/types": "^7.109.0",
|
||||
"@storybook/addon-actions": "^7.6.3",
|
||||
"@storybook/addon-coverage": "^1.0.0",
|
||||
@@ -229,7 +228,6 @@
|
||||
"@storybook/addon-onboarding": "^1.0.10",
|
||||
"@storybook/blocks": "^7.6.3",
|
||||
"@storybook/core-server": "7.6.3",
|
||||
"@storybook/icons": "^1.2.9",
|
||||
"@storybook/jest": "^0.2.3",
|
||||
"@storybook/react": "^7.6.3",
|
||||
"@storybook/react-vite": "^7.6.3",
|
||||
@@ -239,7 +237,7 @@
|
||||
"@stylistic/eslint-plugin": "^1.5.0",
|
||||
"@swc-node/register": "1.8.0",
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "1.7.42",
|
||||
"@swc/core": "~1.3.100",
|
||||
"@swc/helpers": "~0.5.2",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "14.0.0",
|
||||
@@ -251,15 +249,13 @@
|
||||
"@types/chrome": "^0.0.267",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/graphql-fields": "^1.3.6",
|
||||
"@types/graphql-upload": "^8.0.12",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.compact": "^3.0.9",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.escaperegexp": "^4.1.9",
|
||||
"@types/lodash.groupby": "^4.6.9",
|
||||
"@types/lodash.identity": "^3.0.9",
|
||||
"@types/lodash.isempty": "^4.4.7",
|
||||
@@ -276,7 +272,6 @@
|
||||
"@types/node": "18.19.26",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-datepicker": "^6.2.0",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
@@ -289,23 +284,20 @@
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"@vitest/ui": "1.4.0",
|
||||
"@yarnpkg/types": "^4.0.0",
|
||||
"chromatic": "^6.18.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-var": "^1.1.0",
|
||||
"danger": "^11.3.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"drizzle-kit": "^0.20.14",
|
||||
"esbuild": "^0.23.0",
|
||||
"esbuild": "^0.20.2",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-next": "14.0.4",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-lingui": "^0.9.0",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-project-structure": "^3.9.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
@@ -322,7 +314,7 @@
|
||||
"msw": "^2.0.11",
|
||||
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
|
||||
"nx": "18.3.3",
|
||||
"playwright": "^1.46.0",
|
||||
"playwright": "^1.40.1",
|
||||
"prettier": "^3.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^5.0.5",
|
||||
@@ -330,14 +322,15 @@
|
||||
"storybook": "^7.6.3",
|
||||
"storybook-addon-cookie": "^3.2.0",
|
||||
"storybook-addon-pseudo-states": "^2.1.2",
|
||||
"storybook-dark-mode": "^3.0.3",
|
||||
"storybook-dark-mode": "^4.0.1",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"vite": "^5.4.0",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-checker": "^0.6.2",
|
||||
"vite-plugin-dts": "3.8.1",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
@@ -350,18 +343,16 @@
|
||||
},
|
||||
"license": "AGPL-3.0",
|
||||
"name": "twenty",
|
||||
"packageManager": "yarn@4.4.0",
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"resolutions": {
|
||||
"graphql": "16.8.0",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.3.3",
|
||||
"prosemirror-model": "1.23.0",
|
||||
"yjs": "13.6.18"
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
"scripts": {
|
||||
"start": "npx concurrently --kill-others 'npx nx run-many -t start -p twenty-server twenty-front' 'npx wait-on tcp:3000 && npx nx run twenty-server:worker'"
|
||||
"start": "npx nx run-many -t start -p twenty-server twenty-front"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -373,8 +364,6 @@
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"tools/eslint-rules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,5 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npx vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-shared": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "{projectRoot}/dist"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"executor": "nx:run-commands",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// Open options page programmatically in a new tab.
|
||||
// chrome.runtime.onInstalled.addListener((details) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
interface CustomDiv extends HTMLDivElement {
|
||||
onClickHandler: (newHandler: () => void) => void;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { createDefaultButton } from '~/contentScript/createButton';
|
||||
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
|
||||
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
|
||||
import extractDomain from '~/contentScript/utils/extractDomain';
|
||||
import { createCompany, fetchCompany } from '~/db/company.db';
|
||||
import { CompanyInput } from '~/db/types/company.types';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const checkIfCompanyExists = async () => {
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { createDefaultButton } from '~/contentScript/createButton';
|
||||
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
|
||||
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
|
||||
import { createPerson, fetchPerson } from '~/db/person.db';
|
||||
import { PersonInput } from '~/db/types/person.types';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const checkIfPersonExists = async () => {
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
|
||||
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// Inject buttons into the DOM when SPA is reloaded on the resource url.
|
||||
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const btn = document.getElementById('twenty-settings-btn');
|
||||
if (!isDefined(btn)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const changeSidePanelUrl = async (url: string) => {
|
||||
if (isDefined(url)) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Extract "https://www.linkedin.com/company/twenty/" from any of the following urls, which the user can visit while on the company page.
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// "https://www.linkedin.com/company/twenty/" "https://www.linkedin.com/company/twenty/about/" "https://www.linkedin.com/company/twenty/people/".
|
||||
const extractCompanyLinkedinLink = (activeTabUrl: string) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
ExchangeAuthCodeInput,
|
||||
ExchangeAuthCodeResponse,
|
||||
Tokens,
|
||||
} from '~/db/types/auth.types';
|
||||
import { EXCHANGE_AUTHORIZATION_CODE } from '~/graphql/auth/mutations';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { callMutation } from '~/utils/requestDb';
|
||||
|
||||
export const exchangeAuthorizationCode = async (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
CompanyInput,
|
||||
CreateCompanyResponse,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
import { Company, CompanyFilterInput } from '~/generated/graphql';
|
||||
import { CREATE_COMPANY } from '~/graphql/company/mutations';
|
||||
import { FIND_COMPANY } from '~/graphql/company/queries';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { callMutation, callQuery } from '../utils/requestDb';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
CreatePersonResponse,
|
||||
FindPersonResponse,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
import { Person, PersonFilterInput } from '~/generated/graphql';
|
||||
import { CREATE_PERSON } from '~/graphql/person/mutations';
|
||||
import { FIND_PERSON } from '~/graphql/person/queries';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { callMutation, callQuery } from '../utils/requestDb';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { Tokens } from '~/db/types/auth.types';
|
||||
import { RENEW_TOKEN } from '~/graphql/auth/mutations';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const renewToken = async (
|
||||
appToken: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Apollo from '@apollo/client';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
@@ -2527,8 +2527,8 @@ export enum FieldMetadataType {
|
||||
Number = 'NUMBER',
|
||||
Numeric = 'NUMERIC',
|
||||
Phone = 'PHONE',
|
||||
Phones = 'PHONES',
|
||||
Position = 'POSITION',
|
||||
Probability = 'PROBABILITY',
|
||||
Rating = 'RATING',
|
||||
RawJson = 'RAW_JSON',
|
||||
Relation = 'RELATION',
|
||||
@@ -3078,8 +3078,8 @@ export type MessageChannelSyncStageEnumFilter = {
|
||||
|
||||
/** Sync status */
|
||||
export enum MessageChannelSyncStatusEnum {
|
||||
/** Active */
|
||||
Active = 'ACTIVE',
|
||||
/** Completed */
|
||||
Completed = 'COMPLETED',
|
||||
/** Failed */
|
||||
Failed = 'FAILED',
|
||||
/** Failed Insufficient Permissions */
|
||||
@@ -7456,7 +7456,7 @@ export type Workspace = {
|
||||
billingSubscriptions?: Maybe<Array<BillingSubscription>>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
currentBillingSubscription?: Maybe<BillingSubscription>;
|
||||
metadataVersion?: Maybe<Scalars['String']>;
|
||||
currentCacheVersion?: Maybe<Scalars['String']>;
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
domainName?: Maybe<Scalars['String']>;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import Settings from '~/options/Settings';
|
||||
import Sidepanel from '~/options/Sidepanel';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const App = () => {
|
||||
const [currentScreen, setCurrentScreen] = useState('');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Loader } from '@/ui/display/loader/components/Loader';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Loader } from '@/ui/display/loader/components/Loader';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MainButton } from '@/ui/input/button/MainButton';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { clearStore } from '~/utils/apolloClient';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MainButton } from '@/ui/input/button/MainButton';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
display: block;
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
||||
type H2TitleProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
adornment?: React.ReactNode;
|
||||
addornment?: React.ReactNode;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -33,11 +33,11 @@ const StyledDescription = styled.h3`
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const H2Title = ({ title, description, adornment }: H2TitleProps) => (
|
||||
export const H2Title = ({ title, description, addornment }: H2TitleProps) => (
|
||||
<StyledContainer>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{adornment}
|
||||
{addornment}
|
||||
</StyledTitleContainer>
|
||||
{description && <StyledDescription>{description}</StyledDescription>}
|
||||
</StyledContainer>
|
||||
|
||||
+4
-6
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import React, { useId } from 'react';
|
||||
|
||||
interface TextInputProps {
|
||||
label?: string;
|
||||
@@ -18,7 +18,7 @@ const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.label`
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
@@ -42,6 +42,7 @@ const StyledInput = styled.input`
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
|
||||
&::placeholder {
|
||||
@@ -64,15 +65,12 @@ const TextInput: React.FC<TextInputProps> = ({
|
||||
placeholder,
|
||||
icon,
|
||||
}) => {
|
||||
const inputId = useId();
|
||||
|
||||
return (
|
||||
<StyledContainer fullWidth={fullWidth}>
|
||||
{label && <StyledLabel htmlFor={inputId}>{label}</StyledLabel>}
|
||||
{label && <StyledLabel>{label}</StyledLabel>}
|
||||
<StyledInputContainer>
|
||||
{icon && <StyledIcon>{icon}</StyledIcon>}
|
||||
<StyledInput
|
||||
id={inputId}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export type ToggleSize = 'small' | 'medium';
|
||||
|
||||
@@ -15,7 +15,7 @@ type ContainerProps = {
|
||||
const StyledContainer = styled.div<ContainerProps>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme, isOn, color }) =>
|
||||
isOn ? (color ?? theme.color.blue) : theme.background.quaternary};
|
||||
isOn ? color ?? theme.color.blue : theme.background.quaternary};
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client';
|
||||
import { setContext } from '@apollo/client/link/context';
|
||||
import { onError } from '@apollo/client/link/error';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const clearStore = () => {
|
||||
chrome.storage.local.remove([
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { OperationVariables } from '@apollo/client';
|
||||
import { DocumentNode } from 'graphql';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import getApolloClient from '~/utils/apolloClient';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const callQuery = async <T>(
|
||||
query: DocumentNode,
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/options/modules/*"],
|
||||
"~/*": ["./src/*"]
|
||||
"@/*": ["packages/twenty-chrome-extension/src/options/modules/*"],
|
||||
"~/*": ["packages/twenty-chrome-extension/src/*"]
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
TAG=latest
|
||||
|
||||
#PG_DATABASE_USER=postgres
|
||||
#PG_DATABASE_PASSWORD=replace_me_with_a_strong_password_without_special_characters
|
||||
#PG_DATABASE_HOST=db
|
||||
#PG_DATABASE_PORT=5432
|
||||
#REDIS_URL=redis://redis:6379
|
||||
# POSTGRES_ADMIN_PASSWORD=replace_me_with_a_strong_password
|
||||
|
||||
PG_DATABASE_HOST=db:5432
|
||||
|
||||
SERVER_URL=http://localhost:3000
|
||||
SIGN_IN_PREFILLED=false
|
||||
# Uncoment if you are serving your front on another server than the API (eg. bucket)
|
||||
# FRONT_BASE_URL=http://localhost:3000
|
||||
|
||||
# Use openssl rand -base64 32 for each secret
|
||||
# APP_SECRET=replace_me_with_a_random_string
|
||||
# ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
|
||||
# LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login
|
||||
# REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
|
||||
# FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh
|
||||
|
||||
SIGN_IN_PREFILLED=true
|
||||
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# STORAGE_S3_REGION=eu-west3
|
||||
# STORAGE_S3_NAME=my-bucket
|
||||
# STORAGE_S3_ENDPOINT=
|
||||
|
||||
MESSAGE_QUEUE_TYPE=pg-boss
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
dev-build:
|
||||
@docker compose -f dev/docker-compose.yml down -v
|
||||
@docker compose -f dev/docker-compose.yml build
|
||||
|
||||
dev-up:
|
||||
@docker compose -f dev/docker-compose.yml up -d
|
||||
|
||||
dev-start:
|
||||
@docker compose -f dev/docker-compose.yml start
|
||||
|
||||
dev-stop:
|
||||
@docker compose -f dev/docker-compose.yml stop
|
||||
|
||||
dev-down:
|
||||
@docker compose -f dev/docker-compose.yml down -v
|
||||
|
||||
dev-sh:
|
||||
@docker compose -f dev/docker-compose.yml exec twenty-dev sh
|
||||
|
||||
dev-postgres-build:
|
||||
@docker stop twenty_postgres || true
|
||||
@docker rm twenty_postgres || true
|
||||
@docker volume rm twenty_db_data || true
|
||||
@docker compose -f dev/docker-compose.yml up --build postgres -d
|
||||
|
||||
prod-build:
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --tag twenty . && cd -
|
||||
|
||||
prod-run:
|
||||
@docker run -d -p 3000:3000 --name twenty twenty
|
||||
|
||||
prod-postgres-build:
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty-postgres/Dockerfile --tag twenty-postgres . && cd -
|
||||
|
||||
prod-postgres-run:
|
||||
@docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres
|
||||
|
||||
@@ -12,3 +40,11 @@ prod-website-build:
|
||||
|
||||
prod-website-run:
|
||||
@docker run -d -p 3000:3000 --name twenty-website twenty-website
|
||||
|
||||
release-postgres:
|
||||
@cd ../.. && docker buildx build \
|
||||
--push \
|
||||
--no-cache \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-f ./packages/twenty-docker/twenty-postgres/Dockerfile -t twentycrm/twenty-postgres:$(version) -t twentycrm/twenty-postgres:latest . \
|
||||
&& cd -
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "3.9"
|
||||
name: twenty
|
||||
|
||||
services:
|
||||
@@ -13,49 +14,30 @@ services:
|
||||
&& chown -R 1000:1000 /tmp/docker-data"
|
||||
|
||||
server:
|
||||
image: twentycrm/twenty:${TAG:-latest}
|
||||
image: twentycrm/twenty:${TAG}
|
||||
volumes:
|
||||
- server-local-data:/app/packages/twenty-server/${STORAGE_LOCAL_PATH:-.local-storage}
|
||||
- docker-data:/app/docker-data
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
NODE_PORT: 3000
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
PORT: 3000
|
||||
PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
||||
MESSAGE_QUEUE_TYPE: ${MESSAGE_QUEUE_TYPE}
|
||||
|
||||
ENABLE_DB_MIGRATIONS: "true"
|
||||
|
||||
SIGN_IN_PREFILLED: ${SIGN_IN_PREFILLED}
|
||||
STORAGE_TYPE: ${STORAGE_TYPE}
|
||||
STORAGE_S3_REGION: ${STORAGE_S3_REGION}
|
||||
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
|
||||
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT}
|
||||
|
||||
APP_SECRET: ${APP_SECRET:-replace_me_with_a_random_string}
|
||||
|
||||
# MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED}
|
||||
# CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED}
|
||||
# AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID}
|
||||
# AUTH_GOOGLE_CLIENT_SECRET: ${AUTH_GOOGLE_CLIENT_SECRET}
|
||||
# AUTH_GOOGLE_CALLBACK_URL: ${AUTH_GOOGLE_CALLBACK_URL}
|
||||
# AUTH_GOOGLE_APIS_CALLBACK_URL: ${AUTH_GOOGLE_APIS_CALLBACK_URL}
|
||||
|
||||
# CALENDAR_PROVIDER_MICROSOFT_ENABLED: ${CALENDAR_PROVIDER_MICROSOFT_ENABLED}
|
||||
# MESSAGING_PROVIDER_MICROSOFT_ENABLED: ${MESSAGING_PROVIDER_MICROSOFT_ENABLED}
|
||||
# AUTH_MICROSOFT_ENABLED: ${AUTH_MICROSOFT_ENABLED}
|
||||
# AUTH_MICROSOFT_CLIENT_ID: ${AUTH_MICROSOFT_CLIENT_ID}
|
||||
# AUTH_MICROSOFT_CLIENT_SECRET: ${AUTH_MICROSOFT_CLIENT_SECRET}
|
||||
# AUTH_MICROSOFT_CALLBACK_URL: ${AUTH_MICROSOFT_CALLBACK_URL}
|
||||
# AUTH_MICROSOFT_APIS_CALLBACK_URL: ${AUTH_MICROSOFT_APIS_CALLBACK_URL}
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
# EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||
# EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||
|
||||
ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET}
|
||||
LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET}
|
||||
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
|
||||
FILE_TOKEN_SECRET: ${FILE_TOKEN_SECRET}
|
||||
depends_on:
|
||||
change-vol-ownership:
|
||||
condition: service_completed_successfully
|
||||
@@ -69,45 +51,24 @@ services:
|
||||
restart: always
|
||||
|
||||
worker:
|
||||
image: twentycrm/twenty:${TAG:-latest}
|
||||
image: twentycrm/twenty:${TAG}
|
||||
command: ["yarn", "worker:prod"]
|
||||
environment:
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: "true" # it already runs on the server
|
||||
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
||||
MESSAGE_QUEUE_TYPE: ${MESSAGE_QUEUE_TYPE}
|
||||
|
||||
ENABLE_DB_MIGRATIONS: "false" # it already runs on the server
|
||||
|
||||
STORAGE_TYPE: ${STORAGE_TYPE}
|
||||
STORAGE_S3_REGION: ${STORAGE_S3_REGION}
|
||||
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
|
||||
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT}
|
||||
|
||||
APP_SECRET: ${APP_SECRET:-replace_me_with_a_random_string}
|
||||
|
||||
# MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED}
|
||||
# CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED}
|
||||
# AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID}
|
||||
# AUTH_GOOGLE_CLIENT_SECRET: ${AUTH_GOOGLE_CLIENT_SECRET}
|
||||
# AUTH_GOOGLE_CALLBACK_URL: ${AUTH_GOOGLE_CALLBACK_URL}
|
||||
# AUTH_GOOGLE_APIS_CALLBACK_URL: ${AUTH_GOOGLE_APIS_CALLBACK_URL}
|
||||
|
||||
# CALENDAR_PROVIDER_MICROSOFT_ENABLED: ${CALENDAR_PROVIDER_MICROSOFT_ENABLED}
|
||||
# MESSAGING_PROVIDER_MICROSOFT_ENABLED: ${MESSAGING_PROVIDER_MICROSOFT_ENABLED}
|
||||
# AUTH_MICROSOFT_ENABLED: ${AUTH_MICROSOFT_ENABLED}
|
||||
# AUTH_MICROSOFT_CLIENT_ID: ${AUTH_MICROSOFT_CLIENT_ID}
|
||||
# AUTH_MICROSOFT_CLIENT_SECRET: ${AUTH_MICROSOFT_CLIENT_SECRET}
|
||||
# AUTH_MICROSOFT_CALLBACK_URL: ${AUTH_MICROSOFT_CALLBACK_URL}
|
||||
# AUTH_MICROSOFT_APIS_CALLBACK_URL: ${AUTH_MICROSOFT_APIS_CALLBACK_URL}
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
# EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||
# EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||
|
||||
ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET}
|
||||
LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET}
|
||||
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
|
||||
FILE_TOKEN_SECRET: ${FILE_TOKEN_SECRET}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -116,23 +77,18 @@ services:
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
image: twentycrm/twenty-postgres:${TAG}
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
- db-data:/bitnami/postgresql
|
||||
environment:
|
||||
POSTGRES_USER: ${PG_DATABASE_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${PG_DATABASE_PASSWORD:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD}
|
||||
healthcheck:
|
||||
test: pg_isready -U ${PG_DATABASE_USER:-postgres} -h localhost -d postgres
|
||||
test: pg_isready -U twenty -d default
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
docker-data:
|
||||
db-data:
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
# README
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains Kubernetes manifests and Terraform files to help you deploy and manage the TwentyCRM application. The files are located in the `packages/twenty-docker/k8s` directory.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using these files, ensure you have the following installed and configured on your system:
|
||||
|
||||
- Kubernetes cluster (e.g., Minikube, EKS, GKE)
|
||||
- kubectl
|
||||
- Terraform
|
||||
- Docker
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### Step 1: Clone the Repository
|
||||
|
||||
Clone the repository to your local machine:
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
cd twentycrm/packages/twenty-docker/k8s
|
||||
```
|
||||
|
||||
### Step 2: Customize the Manifests and Terraform Files
|
||||
|
||||
**Important:** These files require customization for your specific implementation. Update the placeholders and configurations according to your environment and requirements.
|
||||
|
||||
### Step 3: Deploy with Terraform
|
||||
|
||||
1. Navigate to the Terraform directory:
|
||||
|
||||
```bash
|
||||
cd terraform
|
||||
```
|
||||
|
||||
2. Initialize Terraform:
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
```
|
||||
|
||||
3. Plan the deployment:
|
||||
|
||||
```bash
|
||||
terraform plan
|
||||
```
|
||||
|
||||
4. Apply the deployment:
|
||||
|
||||
```bash
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## OR
|
||||
|
||||
### Step 3: Deploy with Kubernetes Manifests
|
||||
|
||||
1. Navigate to the Kubernetes manifests directory:
|
||||
|
||||
```bash
|
||||
cd ../k8s
|
||||
```
|
||||
|
||||
2. Create Server Secret
|
||||
|
||||
``` bash
|
||||
kubectl create secret generic -n twentycrm tokens --from-literal accessToken=changeme --from-literal loginToken="changeme" --from-literal refreshToken="changeme" --from-literal fileToken="changeme"
|
||||
```
|
||||
|
||||
3. Apply the manifests:
|
||||
|
||||
```bash
|
||||
kubectl apply -f .
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Kubernetes Manifests
|
||||
|
||||
- **Namespace:** Update the `namespace` in the manifests as needed.
|
||||
- **Resource Limits:** Adjust the resource limits and requests according to your application's requirements.
|
||||
- **Environment Variables:** Configure server tokens in the `Secret` command above.
|
||||
|
||||
### Terraform Files
|
||||
|
||||
- **Variables:** Update the variables in the `variables.tf` file to match your environment.
|
||||
- **Locals:** Update the locals in the `main.tf` file to match your environment.
|
||||
- **Providers:** Ensure the provider configurations (e.g., AWS, GCP) are correct for your setup.
|
||||
- **Resources:** Modify the resource definitions as needed to fit your infrastructure.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
- **Connectivity:** Ensure your Kubernetes cluster is accessible and configured correctly.
|
||||
- **Permissions:** Verify that you have the necessary permissions to deploy resources in your cloud provider.
|
||||
- **Resource Limits:** Adjust resource limits if you encounter issues related to insufficient resources.
|
||||
|
||||
### Logs and Debugging
|
||||
|
||||
- Use `kubectl logs` to check the logs of your Kubernetes pods.
|
||||
- Use `terraform show` and `terraform state` to inspect your Terraform state and configurations.
|
||||
|
||||
## Conclusion
|
||||
|
||||
This setup provides a basic structure for deploying the TwentyCRM application using Kubernetes and Terraform. Ensure you thoroughly customize the manifests and Terraform files to suit your specific needs. For any issues or questions, please refer to the official documentation of Kubernetes and Terraform or seek support from your cloud provider.
|
||||
|
||||
---
|
||||
|
||||
Feel free to contribute and improve this repository by submitting pull requests or opening issues. Happy deploying!
|
||||
@@ -1,58 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: twentycrm-db
|
||||
name: twentycrm-db
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 1
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: twentycrm-db
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: twentycrm-db
|
||||
spec:
|
||||
volumes:
|
||||
- name: twentycrm-db-data
|
||||
persistentVolumeClaim:
|
||||
claimName: twentycrm-db-pvc
|
||||
containers:
|
||||
- name: twentycrm
|
||||
image: twentycrm/twenty-postgres-spilo:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: PGUSER_SUPERUSER
|
||||
value: "postgres"
|
||||
- name: PGPASSWORD_SUPERUSER
|
||||
value: "postgres"
|
||||
- name: SPILO_PROVIDER
|
||||
value: "local"
|
||||
- name: ALLOW_NOSSL
|
||||
value: "true"
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: tcp
|
||||
protocol: TCP
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "1024Mi"
|
||||
cpu: "1000m"
|
||||
stdin: true
|
||||
tty: true
|
||||
volumeMounts:
|
||||
- mountPath: /home/postgres/pgdata
|
||||
name: twentycrm-db-data
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
@@ -1,44 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: twentycrm-redis
|
||||
name: twentycrm-redis
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 1
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: twentycrm-redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: twentycrm-redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis/redis-stack-server:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: PORT
|
||||
value: 6379
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
name: redis
|
||||
protocol: TCP
|
||||
resources:
|
||||
requests:
|
||||
memory: "1024Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "2048Mi"
|
||||
cpu: "500m"
|
||||
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
@@ -1,76 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: twentycrm-server
|
||||
name: twentycrm-server
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 1
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: twentycrm-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: twentycrm-server
|
||||
spec:
|
||||
volumes:
|
||||
- name: twentycrm-server-data
|
||||
persistentVolumeClaim:
|
||||
claimName: twentycrm-server-pvc
|
||||
- name: twentycrm-docker-data
|
||||
persistentVolumeClaim:
|
||||
claimName: twentycrm-docker-data-pvc
|
||||
containers:
|
||||
- name: twentycrm
|
||||
image: twentycrm/twenty:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: NODE_PORT
|
||||
value: 3000
|
||||
- name: SERVER_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: "PG_DATABASE_URL"
|
||||
value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default"
|
||||
- name: "REDIS_URL"
|
||||
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
||||
- name: SIGN_IN_PREFILLED
|
||||
value: "false"
|
||||
- name: STORAGE_TYPE
|
||||
value: "local"
|
||||
- name: "ACCESS_TOKEN_EXPIRES_IN"
|
||||
value: "7d"
|
||||
- name: "LOGIN_TOKEN_EXPIRES_IN"
|
||||
value: "1h"
|
||||
- name: APP_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tokens
|
||||
key: accessToken
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: http-tcp
|
||||
protocol: TCP
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "1024Mi"
|
||||
cpu: "1000m"
|
||||
stdin: true
|
||||
tty: true
|
||||
volumeMounts:
|
||||
- mountPath: /app/docker-data
|
||||
name: twentycrm-docker-data
|
||||
- mountPath: /app/packages/twenty-server/.local-storage
|
||||
name: twentycrm-server-data
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
@@ -1,57 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: twentycrm-worker
|
||||
name: twentycrm-worker
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 1
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: twentycrm-worker
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: twentycrm-worker
|
||||
spec:
|
||||
containers:
|
||||
- name: twentycrm
|
||||
image: twentycrm/twenty:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: SERVER_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: PG_DATABASE_URL
|
||||
value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default"
|
||||
- name: DISABLE_DB_MIGRATIONS
|
||||
value: "false" # it already runs on the server
|
||||
- name: STORAGE_TYPE
|
||||
value: "local"
|
||||
- name: "REDIS_URL"
|
||||
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
||||
- name: APP_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tokens
|
||||
key: accessToken
|
||||
command:
|
||||
- yarn
|
||||
- worker:prod
|
||||
resources:
|
||||
requests:
|
||||
memory: "1024Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "2048Mi"
|
||||
cpu: "1000m"
|
||||
stdin: true
|
||||
tty: true
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
@@ -1,24 +0,0 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: twentycrm
|
||||
namespace: twentycrm
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
more_set_headers "X-Forwarded-For $http_x_forwarded_for";
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "false"
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: crm.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: twentycrm-server
|
||||
port:
|
||||
name: http-tcp
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: twentycrm-db-pv
|
||||
spec:
|
||||
storageClassName: default
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: twentycrm-docker-data-pv
|
||||
spec:
|
||||
storageClassName: default
|
||||
capacity:
|
||||
storage: 100Mi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: twentycrm-server-pv
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
storageClassName: default
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: twentycrm-db-pvc
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
storageClassName: default
|
||||
volumeName: twentycrm-db-pv
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: twentycrm-docker-data-pvc
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
storageClassName: default
|
||||
volumeName: twentycrm-docker-data-pv
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 100Mi
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: twentycrm-server-pvc
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
storageClassName: default
|
||||
volumeName: twentycrm-server-pv
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: twentycrm-db
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
internalTrafficPolicy: Cluster
|
||||
ports:
|
||||
- port: 5432
|
||||
protocol: TCP
|
||||
targetPort: 5432
|
||||
selector:
|
||||
app: twentycrm-db
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
clientIP:
|
||||
timeoutSeconds: 10800
|
||||
type: ClusterIP
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: twentycrm-redis
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
internalTrafficPolicy: Cluster
|
||||
ports:
|
||||
- port: 6379
|
||||
protocol: TCP
|
||||
targetPort: 6379
|
||||
selector:
|
||||
app: twentycrm-redis
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
clientIP:
|
||||
timeoutSeconds: 10800
|
||||
type: ClusterIP
|
||||
@@ -1,19 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: twentycrm-server
|
||||
namespace: twentycrm
|
||||
spec:
|
||||
internalTrafficPolicy: Cluster
|
||||
ports:
|
||||
- name: http-tcp
|
||||
port: 3000
|
||||
protocol: TCP
|
||||
targetPort: 3000
|
||||
selector:
|
||||
app: twentycrm-server
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
clientIP:
|
||||
timeoutSeconds: 10800
|
||||
type: ClusterIP
|
||||
@@ -1,48 +0,0 @@
|
||||
formatter: "markdown table" # this is required
|
||||
|
||||
version: ""
|
||||
|
||||
header-from: main.tf
|
||||
|
||||
recursive:
|
||||
enabled: false
|
||||
path: modules
|
||||
|
||||
output:
|
||||
file: "README.md"
|
||||
mode: inject
|
||||
template: |-
|
||||
<!-- BEGIN_TF_DOCS -->
|
||||
# TwentyCRM Terraform Docs
|
||||
|
||||
This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure, and use visit their website.
|
||||
|
||||
To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs -c `./.terraform-docs.yml .`
|
||||
|
||||
To make configuration changes to how this doc is generated, see `./.terraform-docs.yml`
|
||||
|
||||
{{ .Content }}
|
||||
<!-- END_TF_DOCS -->
|
||||
|
||||
output-values:
|
||||
enabled: false
|
||||
from: "outputs.tf"
|
||||
|
||||
sort:
|
||||
enabled: true
|
||||
by: required
|
||||
|
||||
settings:
|
||||
anchor: true
|
||||
color: true
|
||||
default: true
|
||||
description: true
|
||||
escape: true
|
||||
hide-empty: true
|
||||
html: true
|
||||
indent: 2
|
||||
lockfile: true
|
||||
read-comments: true
|
||||
required: true
|
||||
sensitive: true
|
||||
type: true
|
||||
@@ -1,73 +0,0 @@
|
||||
<!-- BEGIN_TF_DOCS -->
|
||||
# TwentyCRM Terraform Docs
|
||||
|
||||
This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure, and use visit their website.
|
||||
|
||||
To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs -c `./.terraform-docs.yml .`
|
||||
|
||||
To make configuration changes to how this doc is generated, see `./.terraform-docs.yml`
|
||||
|
||||
## Requirements
|
||||
|
||||
| Name | Version |
|
||||
|------|---------|
|
||||
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.9.2 |
|
||||
| <a name="requirement_kubernetes"></a> [kubernetes](#requirement\_kubernetes) | >= 2.32.0 |
|
||||
| <a name="requirement_random"></a> [random](#requirement\_random) | >= 3.6.3 |
|
||||
|
||||
## Providers
|
||||
|
||||
| Name | Version |
|
||||
|------|---------|
|
||||
| <a name="provider_kubernetes"></a> [kubernetes](#provider\_kubernetes) | >= 2.32.0 |
|
||||
| <a name="provider_random"></a> [random](#provider\_random) | >= 3.6.3 |
|
||||
|
||||
## Resources
|
||||
|
||||
| Name | Type |
|
||||
|------|------|
|
||||
| [kubernetes_deployment.twentycrm_db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource |
|
||||
| [kubernetes_deployment.twentycrm_redis](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource |
|
||||
| [kubernetes_deployment.twentycrm_server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource |
|
||||
| [kubernetes_deployment.twentycrm_worker](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource |
|
||||
| [kubernetes_ingress.twentycrm](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/ingress) | resource |
|
||||
| [kubernetes_namespace.twentycrm](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource |
|
||||
| [kubernetes_persistent_volume.db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource |
|
||||
| [kubernetes_persistent_volume.docker_data](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource |
|
||||
| [kubernetes_persistent_volume.server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource |
|
||||
| [kubernetes_persistent_volume_claim.db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource |
|
||||
| [kubernetes_persistent_volume_claim.docker_data](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource |
|
||||
| [kubernetes_persistent_volume_claim.server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource |
|
||||
| [kubernetes_secret.twentycrm_tokens](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource |
|
||||
| [kubernetes_service.twentycrm_db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource |
|
||||
| [kubernetes_service.twentycrm_redis](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource |
|
||||
| [kubernetes_service.twentycrm_server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource |
|
||||
| [random_bytes.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/bytes) | resource |
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Description | Type | Default | Required |
|
||||
|------|-------------|------|---------|:--------:|
|
||||
| <a name="input_twentycrm_app_hostname"></a> [twentycrm\_app\_hostname](#input\_twentycrm\_app\_hostname) | The protocol, DNS fully qualified hostname, and port used to access TwentyCRM in your environment. Ex: https://crm.example.com:443 | `string` | n/a | yes |
|
||||
| <a name="input_twentycrm_pgdb_admin_password"></a> [twentycrm\_pgdb\_admin\_password](#input\_twentycrm\_pgdb\_admin\_password) | TwentyCRM password for postgres database. | `string` | n/a | yes |
|
||||
| <a name="input_twentycrm_app_name"></a> [twentycrm\_app\_name](#input\_twentycrm\_app\_name) | A friendly name prefix to use for every component deployed. | `string` | `"twentycrm"` | no |
|
||||
| <a name="input_twentycrm_db_image"></a> [twentycrm\_db\_image](#input\_twentycrm\_db\_image) | TwentyCRM image for database deployment. This defaults to latest. | `string` | `"twentycrm/twenty-postgres-spilo:latest"` | no |
|
||||
| <a name="input_twentycrm_db_pv_capacity"></a> [twentycrm\_db\_pv\_capacity](#input\_twentycrm\_db\_pv\_capacity) | Storage capacity provisioned for database persistent volume. | `string` | `"10Gi"` | no |
|
||||
| <a name="input_twentycrm_db_pv_path"></a> [twentycrm\_db\_pv\_path](#input\_twentycrm\_db\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no |
|
||||
| <a name="input_twentycrm_db_pvc_requests"></a> [twentycrm\_db\_pvc\_requests](#input\_twentycrm\_db\_pvc\_requests) | Storage capacity reservation for database persistent volume claim. | `string` | `"10Gi"` | no |
|
||||
| <a name="input_twentycrm_db_replicas"></a> [twentycrm\_db\_replicas](#input\_twentycrm\_db\_replicas) | Number of replicas for the TwentyCRM database deployment. This defaults to 1. | `number` | `1` | no |
|
||||
| <a name="input_twentycrm_docker_data_mount_path"></a> [twentycrm\_docker\_data\_mount\_path](#input\_twentycrm\_docker\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/docker-data'. | `string` | `"/app/docker-data"` | no |
|
||||
| <a name="input_twentycrm_docker_data_pv_capacity"></a> [twentycrm\_docker\_data\_pv\_capacity](#input\_twentycrm\_docker\_data\_pv\_capacity) | Storage capacity provisioned for server persistent volume. | `string` | `"10Gi"` | no |
|
||||
| <a name="input_twentycrm_docker_data_pv_path"></a> [twentycrm\_docker\_data\_pv\_path](#input\_twentycrm\_docker\_data\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no |
|
||||
| <a name="input_twentycrm_docker_data_pvc_requests"></a> [twentycrm\_docker\_data\_pvc\_requests](#input\_twentycrm\_docker\_data\_pvc\_requests) | Storage capacity reservation for server persistent volume claim. | `string` | `"10Gi"` | no |
|
||||
| <a name="input_twentycrm_namespace"></a> [twentycrm\_namespace](#input\_twentycrm\_namespace) | Namespace for all TwentyCRM resources | `string` | `"twentycrm"` | no |
|
||||
| <a name="input_twentycrm_redis_image"></a> [twentycrm\_redis\_image](#input\_twentycrm\_redis\_image) | TwentyCRM image for Redis deployment. This defaults to latest. | `string` | `"redis/redis-stack-server:latest"` | no |
|
||||
| <a name="input_twentycrm_redis_replicas"></a> [twentycrm\_redis\_replicas](#input\_twentycrm\_redis\_replicas) | Number of replicas for the TwentyCRM Redis deployment. This defaults to 1. | `number` | `1` | no |
|
||||
| <a name="input_twentycrm_server_data_mount_path"></a> [twentycrm\_server\_data\_mount\_path](#input\_twentycrm\_server\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/packages/twenty-server/.local-storage'. | `string` | `"/app/packages/twenty-server/.local-storage"` | no |
|
||||
| <a name="input_twentycrm_server_image"></a> [twentycrm\_server\_image](#input\_twentycrm\_server\_image) | TwentyCRM server image for the server deployment. This defaults to latest. This value is also used for the workers image. | `string` | `"twentycrm/twenty:latest"` | no |
|
||||
| <a name="input_twentycrm_server_pv_capacity"></a> [twentycrm\_server\_pv\_capacity](#input\_twentycrm\_server\_pv\_capacity) | Storage capacity provisioned for server persistent volume. | `string` | `"10Gi"` | no |
|
||||
| <a name="input_twentycrm_server_pv_path"></a> [twentycrm\_server\_pv\_path](#input\_twentycrm\_server\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no |
|
||||
| <a name="input_twentycrm_server_pvc_requests"></a> [twentycrm\_server\_pvc\_requests](#input\_twentycrm\_server\_pvc\_requests) | Storage capacity reservation for server persistent volume claim. | `string` | `"10Gi"` | no |
|
||||
| <a name="input_twentycrm_server_replicas"></a> [twentycrm\_server\_replicas](#input\_twentycrm\_server\_replicas) | Number of replicas for the TwentyCRM server deployment. This defaults to 1. | `number` | `1` | no |
|
||||
| <a name="input_twentycrm_worker_replicas"></a> [twentycrm\_worker\_replicas](#input\_twentycrm\_worker\_replicas) | Number of replicas for the TwentyCRM worker deployment. This defaults to 1. | `number` | `1` | no |
|
||||
<!-- END_TF_DOCS -->
|
||||
@@ -1,87 +0,0 @@
|
||||
resource "kubernetes_deployment" "twentycrm_db" {
|
||||
metadata {
|
||||
name = "${var.twentycrm_app_name}-db"
|
||||
namespace = kubernetes_namespace.twentycrm.metadata.0.name
|
||||
labels = {
|
||||
app = "${var.twentycrm_app_name}-db"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = var.twentycrm_db_replicas
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "${var.twentycrm_app_name}-db"
|
||||
}
|
||||
}
|
||||
|
||||
strategy {
|
||||
type = "RollingUpdate"
|
||||
rolling_update {
|
||||
max_surge = "1"
|
||||
max_unavailable = "1"
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "${var.twentycrm_app_name}-db"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
container {
|
||||
image = var.twentycrm_db_image
|
||||
name = var.twentycrm_app_name
|
||||
stdin = true
|
||||
tty = true
|
||||
security_context {
|
||||
allow_privilege_escalation = true
|
||||
}
|
||||
|
||||
env {
|
||||
name = "POSTGRES_PASSWORD"
|
||||
value = var.twentycrm_pgdb_admin_password
|
||||
}
|
||||
env {
|
||||
name = "BITNAMI_DEBUG"
|
||||
value = true
|
||||
}
|
||||
|
||||
port {
|
||||
container_port = 5432
|
||||
protocol = "TCP"
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "250m"
|
||||
memory = "256Mi"
|
||||
}
|
||||
limits = {
|
||||
cpu = "1000m"
|
||||
memory = "1024Mi"
|
||||
}
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "db-data"
|
||||
mount_path = "/bitnami/postgresql"
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "db-data"
|
||||
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.db.metadata.0.name
|
||||
}
|
||||
}
|
||||
|
||||
dns_policy = "ClusterFirst"
|
||||
restart_policy = "Always"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
resource "kubernetes_deployment" "twentycrm_redis" {
|
||||
metadata {
|
||||
name = "${var.twentycrm_app_name}-redis"
|
||||
namespace = kubernetes_namespace.twentycrm.metadata.0.name
|
||||
|
||||
labels = {
|
||||
app = "${var.twentycrm_app_name}-redis"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = var.twentycrm_redis_replicas
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "${var.twentycrm_app_name}-redis"
|
||||
}
|
||||
}
|
||||
|
||||
strategy {
|
||||
type = "RollingUpdate"
|
||||
rolling_update {
|
||||
max_surge = "1"
|
||||
max_unavailable = "1"
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "${var.twentycrm_app_name}-redis"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
container {
|
||||
image = var.twentycrm_redis_image
|
||||
name = "redis"
|
||||
|
||||
port {
|
||||
container_port = 6379
|
||||
protocol = "TCP"
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "250m"
|
||||
memory = "1024Mi"
|
||||
}
|
||||
limits = {
|
||||
cpu = "500m"
|
||||
memory = "2048Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
dns_policy = "ClusterFirst"
|
||||
restart_policy = "Always"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
resource "kubernetes_deployment" "twentycrm_server" {
|
||||
metadata {
|
||||
name = "${var.twentycrm_app_name}-server"
|
||||
namespace = kubernetes_namespace.twentycrm.metadata.0.name
|
||||
labels = {
|
||||
app = "${var.twentycrm_app_name}-server"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = var.twentycrm_server_replicas
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "${var.twentycrm_app_name}-server"
|
||||
}
|
||||
}
|
||||
|
||||
strategy {
|
||||
type = "RollingUpdate"
|
||||
rolling_update {
|
||||
max_surge = "1"
|
||||
max_unavailable = "1"
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "${var.twentycrm_app_name}-server"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
container {
|
||||
image = var.twentycrm_server_image
|
||||
name = var.twentycrm_app_name
|
||||
stdin = true
|
||||
tty = true
|
||||
|
||||
env {
|
||||
name = "NODE_PORT"
|
||||
value = "3000"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "SERVER_URL"
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "PG_DATABASE_URL"
|
||||
value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default"
|
||||
}
|
||||
env {
|
||||
name = "REDIS_URL"
|
||||
value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379"
|
||||
}
|
||||
env {
|
||||
name = "DISABLE_DB_MIGRATIONS"
|
||||
value = "false"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "STORAGE_TYPE"
|
||||
value = "local"
|
||||
}
|
||||
env {
|
||||
name = "ACCESS_TOKEN_EXPIRES_IN"
|
||||
value = "7d"
|
||||
}
|
||||
env {
|
||||
name = "LOGIN_TOKEN_EXPIRES_IN"
|
||||
value = "1h"
|
||||
}
|
||||
env {
|
||||
name = "APP_SECRET"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = "tokens"
|
||||
key = "accessToken"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
port {
|
||||
container_port = 3000
|
||||
protocol = "TCP"
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "250m"
|
||||
memory = "256Mi"
|
||||
}
|
||||
limits = {
|
||||
cpu = "1000m"
|
||||
memory = "1024Mi"
|
||||
}
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "server-data"
|
||||
mount_path = var.twentycrm_server_data_mount_path
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "docker-data"
|
||||
mount_path = var.twentycrm_docker_data_mount_path
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "server-data"
|
||||
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.server.metadata.0.name
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "docker-data"
|
||||
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.docker_data.metadata.0.name
|
||||
}
|
||||
}
|
||||
|
||||
dns_policy = "ClusterFirst"
|
||||
restart_policy = "Always"
|
||||
}
|
||||
}
|
||||
}
|
||||
depends_on = [
|
||||
kubernetes_deployment.twentycrm_db,
|
||||
kubernetes_deployment.twentycrm_redis,
|
||||
kubernetes_secret.twentycrm_tokens
|
||||
]
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
resource "kubernetes_deployment" "twentycrm_worker" {
|
||||
metadata {
|
||||
name = "${var.twentycrm_app_name}-worker"
|
||||
namespace = kubernetes_namespace.twentycrm.metadata.0.name
|
||||
labels = {
|
||||
app = "${var.twentycrm_app_name}-worker"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = var.twentycrm_worker_replicas
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "${var.twentycrm_app_name}-worker"
|
||||
}
|
||||
}
|
||||
|
||||
strategy {
|
||||
type = "RollingUpdate"
|
||||
rolling_update {
|
||||
max_surge = "1"
|
||||
max_unavailable = "1"
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "${var.twentycrm_app_name}-worker"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
container {
|
||||
image = var.twentycrm_server_image
|
||||
name = var.twentycrm_app_name
|
||||
stdin = true
|
||||
tty = true
|
||||
command = ["yarn", "worker:prod"]
|
||||
|
||||
env {
|
||||
name = "SERVER_URL"
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "PG_DATABASE_URL"
|
||||
value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "REDIS_URL"
|
||||
value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "DISABLE_DB_MIGRATIONS"
|
||||
value = "true" #it already runs on the server
|
||||
}
|
||||
|
||||
env {
|
||||
name = "STORAGE_TYPE"
|
||||
value = "local"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "APP_SECRET"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = "tokens"
|
||||
key = "accessToken"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "250m"
|
||||
memory = "1024Mi"
|
||||
}
|
||||
limits = {
|
||||
cpu = "1000m"
|
||||
memory = "2048Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dns_policy = "ClusterFirst"
|
||||
restart_policy = "Always"
|
||||
}
|
||||
}
|
||||
}
|
||||
depends_on = [
|
||||
kubernetes_deployment.twentycrm_db,
|
||||
kubernetes_deployment.twentycrm_redis,
|
||||
kubernetes_deployment.twentycrm_server,
|
||||
kubernetes_secret.twentycrm_tokens,
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user