Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e44b085609 |
@@ -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/core/migrations/[name] -d src/database/typeorm/core/core.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,33 +0,0 @@
|
||||
---
|
||||
description: Guidelines and best practices for working with Nx in the Twenty workspace, including workspace architecture understanding, configuration management, and generator usage.
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
// This file is automatically generated by Nx Console
|
||||
|
||||
You are in an nx workspace using Nx 18.3.3 and yarn as the package manager.
|
||||
|
||||
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
|
||||
|
||||
# General Guidelines
|
||||
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
|
||||
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
|
||||
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
|
||||
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
|
||||
|
||||
# Generation Guidelines
|
||||
If the user wants to generate something, use the following flow:
|
||||
|
||||
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
|
||||
- get the available generators using the 'nx_generators' tool
|
||||
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
|
||||
- get generator details using the 'nx_generator_schema' tool
|
||||
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
|
||||
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
|
||||
- open the generator UI using the 'nx_open_generate_ui' tool
|
||||
- wait for the user to finish the generator
|
||||
- read the generator log file using the 'nx_read_generator_log' tool
|
||||
- use the information provided in the log file to answer the user's question or continue with what they were doing
|
||||
|
||||
|
||||
@@ -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: UUID!) {
|
||||
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]>;
|
||||
};
|
||||
```
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
plugins: ['@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn'],
|
||||
rules: {
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
|
||||
'no-control-regex': 0,
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-imports': 'error',
|
||||
'no-undef': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
{
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: [],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:backend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'import/no-relative-packages': 'error',
|
||||
'import/no-useless-path-segments': 'error',
|
||||
'import/no-duplicates': ['error', { considerQueryString: true }],
|
||||
|
||||
'prefer-arrow/prefer-arrow-functions': [
|
||||
'error',
|
||||
{
|
||||
disallowPrototype: true,
|
||||
singleReturnOnly: false,
|
||||
classPropertiesAllowed: false,
|
||||
},
|
||||
],
|
||||
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
extends: ['plugin:@nx/typescript'],
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-comment': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'no-type-imports' },
|
||||
],
|
||||
'@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',
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx'],
|
||||
extends: ['plugin:@nx/javascript'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['*.spec.@(ts|tsx|js|jsx)', '*.test.@(ts|tsx|js|jsx)'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/constants/*.ts', '**/*.constants.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['UPPER_CASE'],
|
||||
},
|
||||
],
|
||||
'unicorn/filename-case': [
|
||||
'warn',
|
||||
{
|
||||
cases: {
|
||||
pascalCase: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@nx/workspace-max-consts-per-file': ['error', { max: 1 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.json'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,149 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:prettier/recommended', 'plugin:lingui/recommended'],
|
||||
ignorePatterns: ['node_modules'],
|
||||
plugins: [
|
||||
'@nx',
|
||||
'prefer-arrow',
|
||||
'import',
|
||||
'unused-imports',
|
||||
'unicorn',
|
||||
'lingui',
|
||||
],
|
||||
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,
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-imports': 'error',
|
||||
'no-undef': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
{
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: [],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:backend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'import/no-relative-packages': 'error',
|
||||
'import/no-useless-path-segments': 'error',
|
||||
'import/no-duplicates': ['error', { considerQueryString: true }],
|
||||
|
||||
'prefer-arrow/prefer-arrow-functions': [
|
||||
'error',
|
||||
{
|
||||
disallowPrototype: true,
|
||||
singleReturnOnly: false,
|
||||
classPropertiesAllowed: false,
|
||||
},
|
||||
],
|
||||
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
extends: ['plugin:@nx/typescript'],
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-comment': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'no-type-imports' },
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx'],
|
||||
extends: ['plugin:@nx/javascript'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'*.spec.@(ts|tsx|js|jsx)',
|
||||
'*.integration-spec.@(ts|tsx|js|jsx)',
|
||||
'*.test.@(ts|tsx|js|jsx)',
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/constants/*.ts', '**/*.constants.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['UPPER_CASE'],
|
||||
},
|
||||
],
|
||||
'unicorn/filename-case': [
|
||||
'warn',
|
||||
{
|
||||
cases: {
|
||||
pascalCase: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@nx/workspace-max-consts-per-file': ['error', { max: 1 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.json'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
},
|
||||
],
|
||||
};
|
||||
+5
-114
@@ -6,74 +6,14 @@ module.exports = {
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:lingui/recommended',
|
||||
'plugin:@nx/typescript'
|
||||
],
|
||||
plugins: ['react-hooks', 'react-refresh', '@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn', 'lingui'],
|
||||
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,
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-imports': 'error',
|
||||
'no-undef': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
{
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: [],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:backend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'import/no-relative-packages': 'error',
|
||||
'import/no-useless-path-segments': 'error',
|
||||
'import/no-duplicates': ['error', { considerQueryString: true }],
|
||||
|
||||
'prefer-arrow/prefer-arrow-functions': [
|
||||
'error',
|
||||
{
|
||||
disallowPrototype: true,
|
||||
singleReturnOnly: false,
|
||||
classPropertiesAllowed: false,
|
||||
},
|
||||
],
|
||||
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['react-hooks', 'react-refresh'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
files: ['*.ts', '*.tsx'],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.base.{json,*.json}'],
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
@@ -97,14 +37,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@nx/workspace-effect-components': 'error',
|
||||
'@nx/workspace-no-hardcoded-colors': 'error',
|
||||
'@nx/workspace-matching-state-variable': 'error',
|
||||
@@ -154,46 +86,5 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.jsx'],
|
||||
extends: ['plugin:@nx/javascript'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'*.test.@(ts|tsx|js|jsx)',
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.constants.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['UPPER_CASE'],
|
||||
},
|
||||
],
|
||||
'unicorn/filename-case': [
|
||||
'warn',
|
||||
{
|
||||
cases: {
|
||||
pascalCase: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@nx/workspace-max-consts-per-file': ['error', { max: 1 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.json'],
|
||||
parser: 'jsonc-eslint-parser',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -13,10 +13,14 @@ Good first issues are a great way to start contributing and get familiar with th
|
||||
|
||||
## Issue assignment
|
||||
|
||||
To avoid conflicts, we follow these guidelines:
|
||||
Having multiple contributors address the same issue can cause frustration.
|
||||
|
||||
1. For `Good First Issue` and `Experienced Contributor` issues without `size: long` labels, we'll merge the first PRs that meet our [code quality standards](https://twenty.com/developers). **We don't assign contributors to these issues**. For `priority: high` issues, our core team will step in within days if no adequate contributions are received.
|
||||
2. For `size: long` Issues, assigned contributors have one week to submit their first draft PR.
|
||||
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.
|
||||
|
||||
Please ensure you're assigned to an issue before starting work.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
@@ -63,4 +67,4 @@ git push origin your-branch-name
|
||||
|
||||
## 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 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.
|
||||
@@ -0,0 +1,33 @@
|
||||
name: oss.gg hack submission 🕹️
|
||||
description: "Submit your contribution for the for the oss.gg hackathon"
|
||||
title: "[🕹️]"
|
||||
labels: 🕹️ oss.gg, player submission, hacktoberfest
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: contribution-name
|
||||
attributes:
|
||||
label: What side quest or challenge are you solving?
|
||||
description: Add the name of the side quest or challenge.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: points
|
||||
attributes:
|
||||
label: Points
|
||||
description: How many points are assigned to this contribution?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: What's the task your performed?
|
||||
validations:
|
||||
- type: textarea
|
||||
id: proof
|
||||
attributes:
|
||||
label: Provide proof that you've completed the task
|
||||
description: Screenshots, loom recordings, links to the content you shared or interacted with.
|
||||
validations:
|
||||
required: true
|
||||
@@ -2,16 +2,15 @@ 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'
|
||||
args:
|
||||
required: false
|
||||
types: [string]
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -19,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 }}' ${{ inputs.args }}
|
||||
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=v3-${{ 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: '22'
|
||||
|
||||
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: v3-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
|
||||
restore-keys: v3-${{ 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'
|
||||
@@ -1,777 +0,0 @@
|
||||
name: GraphQL and OpenAPI Breaking Changes Detection
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, edited]
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
MAIN_SERVER_PORT: 3000
|
||||
CURRENT_SERVER_PORT: 3002
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
packages/twenty-shared/**
|
||||
|
||||
api-breaking-changes:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 45
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
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
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
ports:
|
||||
- 8123:8123
|
||||
- 9000:9000
|
||||
options: >-
|
||||
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Build shared dependencies
|
||||
run: |
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-emails
|
||||
|
||||
- name: Build current branch server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Setup databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "current_branch";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "main_branch";'
|
||||
|
||||
- name: Run ClickHouse migrations
|
||||
run: npx nx clickhouse:migrate twenty-server
|
||||
env:
|
||||
CLICKHOUSE_URL: http://default:clickhousePassword@localhost:8123/twenty
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
|
||||
- name: Setup current branch database
|
||||
run: |
|
||||
npx nx reset:env twenty-server
|
||||
# Function to set or update environment variable
|
||||
set_env_var() {
|
||||
local var_name="$1"
|
||||
local var_value="$2"
|
||||
local env_file="packages/twenty-server/.env"
|
||||
|
||||
if grep -q "^${var_name}=" "$env_file"; then
|
||||
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
|
||||
else
|
||||
echo "${var_name}=${var_value}" >> "$env_file"
|
||||
fi
|
||||
}
|
||||
|
||||
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/current_branch"
|
||||
set_env_var "NODE_PORT" "${{ env.CURRENT_SERVER_PORT }}"
|
||||
set_env_var "REDIS_URL" "redis://localhost:6379"
|
||||
set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
|
||||
|
||||
npx nx run twenty-server:database:init:prod
|
||||
npx nx run twenty-server:database:migrate:prod
|
||||
|
||||
- name: Seed current branch database with test data
|
||||
run: |
|
||||
npx nx command-no-deps twenty-server -- workspace:seed:dev
|
||||
|
||||
- name: Start current branch server in background
|
||||
run: |
|
||||
echo "=== Current branch .env file contents ==="
|
||||
cat packages/twenty-server/.env
|
||||
echo "=== Starting current branch server ==="
|
||||
nohup npx nx run twenty-server:start:prod > /tmp/current-server.log 2>&1 &
|
||||
echo $! > /tmp/current-server.pid
|
||||
echo "Current server PID: $(cat /tmp/current-server.pid)"
|
||||
|
||||
- name: Wait for current branch server to be ready
|
||||
run: |
|
||||
echo "Waiting for current branch server to start..."
|
||||
timeout=300
|
||||
interval=5
|
||||
elapsed=0
|
||||
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
|
||||
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
|
||||
echo "Current branch server is ready!"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Current branch server not ready yet, waiting ${interval}s..."
|
||||
sleep $interval
|
||||
elapsed=$((elapsed + interval))
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "Timeout waiting for current branch server to start"
|
||||
echo "Current server log:"
|
||||
cat /tmp/current-server.log || echo "No current server log found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download GraphQL and REST responses from current branch
|
||||
run: |
|
||||
# Admin token from jest-integration.config.ts
|
||||
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwiaWF0IjoxNzM5NTQ3NjYxLCJleHAiOjMzMjk3MTQ3NjYxfQ.fbOM9yhr3jWDicPZ1n771usUURiPGmNdeFApsgrbxOw"
|
||||
|
||||
# Load introspection query from file
|
||||
INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql)
|
||||
|
||||
# Prepare the query payload
|
||||
QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g')
|
||||
|
||||
echo "Downloading GraphQL schema from current server..."
|
||||
curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
|
||||
-o current-schema-introspection.json \
|
||||
-w "HTTP Status: %{http_code}\n" \
|
||||
-s
|
||||
|
||||
echo "Downloading GraphQL metadata schema from current server..."
|
||||
curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/metadata" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
|
||||
-o current-metadata-schema-introspection.json \
|
||||
-w "HTTP Status: %{http_code}\n" \
|
||||
-s
|
||||
|
||||
# Download current branch OpenAPI specs
|
||||
echo "Downloading OpenAPI specifications from current server..."
|
||||
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-o current-rest-api.json \
|
||||
-w "HTTP Status: %{http_code}\n"
|
||||
|
||||
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/metadata" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-o current-rest-metadata-api.json \
|
||||
-w "HTTP Status: %{http_code}\n"
|
||||
|
||||
# Verify the downloads
|
||||
echo "Current branch files downloaded:"
|
||||
ls -la current-*
|
||||
|
||||
|
||||
- name: Preserve current branch files
|
||||
run: |
|
||||
# Create a temp directory to store current branch files
|
||||
mkdir -p /tmp/current-branch-files
|
||||
|
||||
# Move current branch files to temp directory
|
||||
mv current-* /tmp/current-branch-files/ 2>/dev/null || echo "No current-* files to preserve"
|
||||
|
||||
echo "Preserved current branch files for later restoration"
|
||||
|
||||
- name: Stop current branch server
|
||||
run: |
|
||||
if [ -f /tmp/current-server.pid ]; then
|
||||
echo "Stopping current branch server..."
|
||||
kill $(cat /tmp/current-server.pid) || true
|
||||
# Wait a bit for graceful shutdown
|
||||
sleep 5
|
||||
# Force kill if still running
|
||||
kill -9 $(cat /tmp/current-server.pid) 2>/dev/null || true
|
||||
rm -f /tmp/current-server.pid
|
||||
fi
|
||||
|
||||
- name: Checkout main branch
|
||||
run: |
|
||||
git stash
|
||||
git checkout origin/main
|
||||
git clean -fd
|
||||
|
||||
- name: Install dependencies for main branch
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Build main branch dependencies
|
||||
run: |
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-emails
|
||||
|
||||
- name: Build main branch server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Setup main branch database
|
||||
run: |
|
||||
# Function to set or update environment variable
|
||||
set_env_var() {
|
||||
local var_name="$1"
|
||||
local var_value="$2"
|
||||
local env_file="packages/twenty-server/.env"
|
||||
|
||||
if grep -q "^${var_name}=" "$env_file"; then
|
||||
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
|
||||
else
|
||||
echo "${var_name}=${var_value}" >> "$env_file"
|
||||
fi
|
||||
}
|
||||
|
||||
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/main_branch"
|
||||
set_env_var "NODE_PORT" "${{ env.MAIN_SERVER_PORT }}"
|
||||
|
||||
npx nx run twenty-server:database:init:prod
|
||||
npx nx run twenty-server:database:migrate:prod
|
||||
|
||||
- name: Seed main branch database with test data
|
||||
run: |
|
||||
npx nx command-no-deps twenty-server -- workspace:seed:dev
|
||||
|
||||
- name: Start main branch server in background
|
||||
run: |
|
||||
echo "=== Main branch .env file contents ==="
|
||||
cat packages/twenty-server/.env
|
||||
echo "=== Starting main branch server ==="
|
||||
nohup npx nx run twenty-server:start:prod > /tmp/main-server.log 2>&1 &
|
||||
echo $! > /tmp/main-server.pid
|
||||
echo "Main server PID: $(cat /tmp/main-server.pid)"
|
||||
|
||||
- name: Wait for main branch server to be ready
|
||||
run: |
|
||||
echo "Waiting for main branch server to start..."
|
||||
timeout=300
|
||||
interval=5
|
||||
elapsed=0
|
||||
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
|
||||
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
|
||||
echo "Main branch server is ready!"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Main branch server not ready yet, waiting ${interval}s..."
|
||||
sleep $interval
|
||||
elapsed=$((elapsed + interval))
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "Timeout waiting for main branch server to start"
|
||||
echo "Main server log:"
|
||||
cat /tmp/main-server.log || echo "No main server log found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download GraphQL and REST responses from main branch
|
||||
run: |
|
||||
# Admin token from jest-integration.config.ts
|
||||
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwiaWF0IjoxNzM5NTQ3NjYxLCJleHAiOjMzMjk3MTQ3NjYxfQ.fbOM9yhr3jWDicPZ1n771usUURiPGmNdeFApsgrbxOw"
|
||||
|
||||
# Load introspection query from file
|
||||
INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql)
|
||||
|
||||
# Prepare the query payload
|
||||
QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g')
|
||||
|
||||
echo "Downloading GraphQL schema from main server..."
|
||||
curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
|
||||
-o main-schema-introspection.json \
|
||||
-w "HTTP Status: %{http_code}\n" \
|
||||
-s
|
||||
|
||||
echo "Downloading GraphQL metadata schema from main server..."
|
||||
curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/metadata" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
|
||||
-o main-metadata-schema-introspection.json \
|
||||
-w "HTTP Status: %{http_code}\n" \
|
||||
-s
|
||||
|
||||
# Download main branch OpenAPI specs
|
||||
echo "Downloading OpenAPI specifications from main server..."
|
||||
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-o main-rest-api.json \
|
||||
-w "HTTP Status: %{http_code}\n"
|
||||
|
||||
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/metadata" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-o main-rest-metadata-api.json \
|
||||
-w "HTTP Status: %{http_code}\n"
|
||||
|
||||
# Verify the downloads
|
||||
echo "Main branch files downloaded:"
|
||||
ls -la main-*
|
||||
|
||||
|
||||
- name: Restore current branch files
|
||||
run: |
|
||||
# Move current branch files back to working directory
|
||||
mv /tmp/current-branch-files/* . 2>/dev/null || echo "No files to restore"
|
||||
|
||||
# Verify all files are present
|
||||
echo "All API files restored:"
|
||||
ls -la current-* main-* 2>/dev/null || echo "Some files may be missing"
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf /tmp/current-branch-files
|
||||
|
||||
- name: Install OpenAPI Diff Tool
|
||||
run: |
|
||||
# Using the Java-based OpenAPITools/openapi-diff via Docker
|
||||
echo "Using OpenAPITools/openapi-diff via Docker"
|
||||
|
||||
- name: Generate GraphQL Schema Diff Reports
|
||||
run: |
|
||||
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
|
||||
npm install -g @graphql-inspector/cli
|
||||
|
||||
echo "=== GENERATING GRAPHQL DIFF REPORTS ==="
|
||||
|
||||
# Check if GraphQL schema has changes
|
||||
echo "Checking GraphQL schema for changes..."
|
||||
if graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >/dev/null 2>&1; then
|
||||
echo "✅ No changes in GraphQL schema"
|
||||
# Don't create a diff file for no changes
|
||||
else
|
||||
echo "⚠️ Changes detected in GraphQL schema, generating report..."
|
||||
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
|
||||
echo "" >> graphql-schema-diff.md
|
||||
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || {
|
||||
echo "⚠️ **Breaking changes or errors detected in GraphQL schema**" >> graphql-schema-diff.md
|
||||
echo "" >> graphql-schema-diff.md
|
||||
echo "\`\`\`" >> graphql-schema-diff.md
|
||||
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json 2>&1 >> graphql-schema-diff.md || echo "Error generating diff" >> graphql-schema-diff.md
|
||||
echo "\`\`\`" >> graphql-schema-diff.md
|
||||
}
|
||||
fi
|
||||
|
||||
# Check if GraphQL metadata schema has changes
|
||||
echo "Checking GraphQL metadata schema for changes..."
|
||||
if graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >/dev/null 2>&1; then
|
||||
echo "✅ No changes in GraphQL metadata schema"
|
||||
# Don't create a diff file for no changes
|
||||
else
|
||||
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
|
||||
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
|
||||
echo "" >> graphql-metadata-diff.md
|
||||
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || {
|
||||
echo "⚠️ **Breaking changes or errors detected in GraphQL metadata schema**" >> graphql-metadata-diff.md
|
||||
echo "" >> graphql-metadata-diff.md
|
||||
echo "\`\`\`" >> graphql-metadata-diff.md
|
||||
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json 2>&1 >> graphql-metadata-diff.md || echo "Error generating diff" >> graphql-metadata-diff.md
|
||||
echo "\`\`\`" >> graphql-metadata-diff.md
|
||||
}
|
||||
fi
|
||||
|
||||
# Show summary
|
||||
echo "Generated diff files:"
|
||||
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
|
||||
|
||||
- name: Check REST API Breaking Changes
|
||||
run: |
|
||||
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
|
||||
|
||||
# Use the Java-based openapi-diff via Docker
|
||||
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \
|
||||
--json /specs/rest-api-diff.json \
|
||||
/specs/main-rest-api.json /specs/current-rest-api.json || echo "OpenAPI diff completed with exit code $?"
|
||||
|
||||
# Check if the output file was created and is valid JSON
|
||||
if [ -f "rest-api-diff.json" ] && jq empty rest-api-diff.json 2>/dev/null; then
|
||||
# Check for breaking changes using Java openapi-diff JSON structure
|
||||
incompatible=$(jq -r '.incompatible // false' rest-api-diff.json)
|
||||
different=$(jq -r '.different // false' rest-api-diff.json)
|
||||
|
||||
# Count changes
|
||||
new_endpoints=$(jq -r '.newEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0")
|
||||
missing_endpoints=$(jq -r '.missingEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0")
|
||||
changed_operations=$(jq -r '.changedOperations | length' rest-api-diff.json 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$incompatible" = "true" ]; then
|
||||
echo "❌ Breaking changes detected in REST API"
|
||||
|
||||
# Generate breaking changes report
|
||||
echo "# REST API Breaking Changes" > rest-api-diff.md
|
||||
echo "" >> rest-api-diff.md
|
||||
echo "⚠️ **Breaking changes detected that may affect existing API consumers**" >> rest-api-diff.md
|
||||
echo "" >> rest-api-diff.md
|
||||
|
||||
# Parse and format the changes from Java openapi-diff
|
||||
jq -r '
|
||||
if (.missingEndpoints | length) > 0 then
|
||||
"## 🚨 Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" +
|
||||
(.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n"))
|
||||
else "" end,
|
||||
if (.changedOperations | length) > 0 then
|
||||
"\n## ⚠️ Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
|
||||
(.changedOperations | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n"))
|
||||
else "" end,
|
||||
if (.newEndpoints | length) > 0 then
|
||||
"\n## ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
|
||||
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
|
||||
else "" end
|
||||
' rest-api-diff.json >> rest-api-diff.md
|
||||
|
||||
elif [ "$different" = "true" ]; then
|
||||
echo "📝 Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed)"
|
||||
|
||||
# Generate non-breaking changes report
|
||||
echo "# REST API Changes" > rest-api-diff.md
|
||||
echo "" >> rest-api-diff.md
|
||||
echo "## Summary" >> rest-api-diff.md
|
||||
|
||||
jq -r '
|
||||
if (.newEndpoints | length) > 0 then
|
||||
"### ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
|
||||
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
|
||||
else "" end,
|
||||
if (.changedOperations | length) > 0 then
|
||||
"\n### 🔄 Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
|
||||
(.changedOperations | map("- " + .method + " " + .pathUrl + ": " + (.summary // "Modified operation")) | join("\n"))
|
||||
else "" end
|
||||
' rest-api-diff.json >> rest-api-diff.md
|
||||
else
|
||||
echo "✅ No changes detected in REST API"
|
||||
# Don't create diff file for no changes
|
||||
fi
|
||||
else
|
||||
echo "⚠️ OpenAPI diff tool could not process the files"
|
||||
|
||||
echo "# REST API Analysis Error" > rest-api-diff.md
|
||||
echo "" >> rest-api-diff.md
|
||||
echo "⚠️ **Error occurred while analyzing REST API changes**" >> rest-api-diff.md
|
||||
echo "" >> rest-api-diff.md
|
||||
echo "## Error Output" >> rest-api-diff.md
|
||||
echo "\`\`\`" >> rest-api-diff.md
|
||||
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-api.json /specs/current-rest-api.json 2>&1 >> rest-api-diff.md || echo "Could not capture error output"
|
||||
echo "\`\`\`" >> rest-api-diff.md
|
||||
|
||||
# Don't fail the workflow for tool errors
|
||||
echo "::warning::REST API analysis tool error - continuing workflow"
|
||||
fi
|
||||
|
||||
- name: Check REST Metadata API Breaking Changes
|
||||
run: |
|
||||
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
|
||||
|
||||
# Use the Java-based openapi-diff for metadata API as well
|
||||
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \
|
||||
--json /specs/rest-metadata-api-diff.json \
|
||||
/specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json || echo "OpenAPI diff completed with exit code $?"
|
||||
|
||||
# Check if the output file was created and is valid JSON
|
||||
if [ -f "rest-metadata-api-diff.json" ] && jq empty rest-metadata-api-diff.json 2>/dev/null; then
|
||||
# Check for breaking changes using Java openapi-diff JSON structure
|
||||
incompatible=$(jq -r '.incompatible // false' rest-metadata-api-diff.json)
|
||||
different=$(jq -r '.different // false' rest-metadata-api-diff.json)
|
||||
|
||||
# Count changes
|
||||
new_endpoints=$(jq -r '.newEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
|
||||
missing_endpoints=$(jq -r '.missingEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
|
||||
changed_operations=$(jq -r '.changedOperations | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$incompatible" = "true" ]; then
|
||||
echo "❌ Breaking changes detected in REST Metadata API"
|
||||
|
||||
# Generate breaking changes report
|
||||
echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md
|
||||
echo "" >> rest-metadata-api-diff.md
|
||||
echo "⚠️ **Breaking changes detected that may affect existing API consumers**" >> rest-metadata-api-diff.md
|
||||
echo "" >> rest-metadata-api-diff.md
|
||||
|
||||
# Parse and format the changes from Java openapi-diff
|
||||
jq -r '
|
||||
if (.missingEndpoints | length) > 0 then
|
||||
"## 🚨 Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" +
|
||||
(.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n"))
|
||||
else "" end,
|
||||
if (.changedOperations | length) > 0 then
|
||||
"\n## ⚠️ Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
|
||||
(.changedOperations | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n"))
|
||||
else "" end,
|
||||
if (.newEndpoints | length) > 0 then
|
||||
"\n## ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
|
||||
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
|
||||
else "" end
|
||||
' rest-metadata-api-diff.json >> rest-metadata-api-diff.md
|
||||
|
||||
elif [ "$different" = "true" ]; then
|
||||
echo "📝 Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed)"
|
||||
|
||||
# Generate non-breaking changes report
|
||||
echo "# REST Metadata API Changes" > rest-metadata-api-diff.md
|
||||
echo "" >> rest-metadata-api-diff.md
|
||||
echo "## Summary" >> rest-metadata-api-diff.md
|
||||
|
||||
jq -r '
|
||||
if (.newEndpoints | length) > 0 then
|
||||
"### ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
|
||||
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
|
||||
else "" end,
|
||||
if (.changedOperations | length) > 0 then
|
||||
"\n### 🔄 Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
|
||||
(.changedOperations | map("- " + .method + " " + .pathUrl + ": " + (.summary // "Modified operation")) | join("\n"))
|
||||
else "" end
|
||||
' rest-metadata-api-diff.json >> rest-metadata-api-diff.md
|
||||
else
|
||||
echo "✅ No changes detected in REST Metadata API"
|
||||
# Don't create diff file for no changes
|
||||
fi
|
||||
else
|
||||
echo "⚠️ OpenAPI diff tool could not process the metadata API files"
|
||||
|
||||
echo "# REST Metadata API Analysis Error" > rest-metadata-api-diff.md
|
||||
echo "" >> rest-metadata-api-diff.md
|
||||
echo "⚠️ **Error occurred while analyzing REST Metadata API changes**" >> rest-metadata-api-diff.md
|
||||
echo "" >> rest-metadata-api-diff.md
|
||||
echo "## Error Output" >> rest-metadata-api-diff.md
|
||||
echo "\`\`\`" >> rest-metadata-api-diff.md
|
||||
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json 2>&1 >> rest-metadata-api-diff.md || echo "Could not capture error output"
|
||||
echo "\`\`\`" >> rest-metadata-api-diff.md
|
||||
|
||||
# Don't fail the workflow for tool errors
|
||||
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
|
||||
fi
|
||||
|
||||
- name: Comment API Changes on PR
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
let hasChanges = false;
|
||||
let comment = '';
|
||||
|
||||
try {
|
||||
if (fs.existsSync('graphql-schema-diff.md')) {
|
||||
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
|
||||
if (graphqlDiff.trim()) {
|
||||
if (!hasChanges) {
|
||||
comment = '## 📊 API Changes Report\n\n';
|
||||
hasChanges = true;
|
||||
}
|
||||
comment += '### GraphQL Schema Changes\n' + graphqlDiff + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync('graphql-metadata-diff.md')) {
|
||||
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
|
||||
if (graphqlMetadataDiff.trim()) {
|
||||
if (!hasChanges) {
|
||||
comment = '## 📊 API Changes Report\n\n';
|
||||
hasChanges = true;
|
||||
}
|
||||
comment += '### GraphQL Metadata Schema Changes\n' + graphqlMetadataDiff + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync('rest-api-diff.md')) {
|
||||
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
|
||||
if (restDiff.trim()) {
|
||||
if (!hasChanges) {
|
||||
comment = '## 📊 API Changes Report\n\n';
|
||||
hasChanges = true;
|
||||
}
|
||||
comment += restDiff + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync('rest-metadata-api-diff.md')) {
|
||||
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
|
||||
if (metadataDiff.trim()) {
|
||||
if (!hasChanges) {
|
||||
comment = '## 📊 API Changes Report\n\n';
|
||||
hasChanges = true;
|
||||
}
|
||||
comment += metadataDiff + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Only post comment if there are changes
|
||||
if (hasChanges) {
|
||||
// Check if there are any breaking changes detected
|
||||
let hasBreakingChanges = false;
|
||||
let breakingChangeNote = '';
|
||||
|
||||
// Check for breaking changes in any of the diff files
|
||||
if (fs.existsSync('rest-api-diff.md')) {
|
||||
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
|
||||
if (restDiff.includes('Breaking Changes') || restDiff.includes('🚨') ||
|
||||
restDiff.includes('Removed Endpoints') || restDiff.includes('Changed Operations')) {
|
||||
hasBreakingChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync('rest-metadata-api-diff.md')) {
|
||||
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
|
||||
if (metadataDiff.includes('Breaking Changes') || metadataDiff.includes('🚨') ||
|
||||
metadataDiff.includes('Removed Endpoints') || metadataDiff.includes('Changed Operations')) {
|
||||
hasBreakingChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check GraphQL changes for breaking changes indicators
|
||||
if (fs.existsSync('graphql-schema-diff.md')) {
|
||||
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
|
||||
if (graphqlDiff.includes('Breaking changes') || graphqlDiff.includes('BREAKING')) {
|
||||
hasBreakingChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync('graphql-metadata-diff.md')) {
|
||||
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
|
||||
if (graphqlMetadataDiff.includes('Breaking changes') || graphqlMetadataDiff.includes('BREAKING')) {
|
||||
hasBreakingChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check PR title for "breaking"
|
||||
const prTitle = "${{ github.event.pull_request.title }}";
|
||||
const titleContainsBreaking = prTitle.toLowerCase().includes('breaking');
|
||||
|
||||
if (hasBreakingChanges) {
|
||||
if (titleContainsBreaking) {
|
||||
breakingChangeNote = '\n\n## ✅ Breaking Change Protocol\n\n' +
|
||||
'**This PR title contains "breaking" and breaking changes were detected - the CI will fail as expected.**\n\n' +
|
||||
'📝 **Action Required**: Please add `BREAKING CHANGE:` to your commit message to trigger a major version bump.\n\n' +
|
||||
'Example:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
|
||||
} else {
|
||||
breakingChangeNote = '\n\n## ⚠️ Breaking Change Protocol\n\n' +
|
||||
'**Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.**\n\n' +
|
||||
'🔄 **Options**:\n' +
|
||||
'1. **If this IS a breaking change**: Add "breaking" to your PR title and add `BREAKING CHANGE:` to your commit message\n' +
|
||||
'2. **If this is NOT a breaking change**: The API diff tool may have false positives - please review carefully\n\n' +
|
||||
'For breaking changes, add to commit message:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
|
||||
}
|
||||
}
|
||||
|
||||
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
|
||||
const commentBody = COMMENT_MARKER + '\n' + comment + '⚠️ **Please review these API changes carefully before merging.**' + breakingChangeNote;
|
||||
|
||||
// Get all comments to find existing API changes comment
|
||||
const {data: comments} = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
// Find our existing comment
|
||||
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
console.log('Updated existing API changes comment');
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: commentBody
|
||||
});
|
||||
console.log('Created new API changes comment');
|
||||
}
|
||||
} else {
|
||||
console.log('No API changes detected - skipping PR comment');
|
||||
|
||||
// Check if there's an existing comment to remove
|
||||
const {data: comments} = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
|
||||
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
});
|
||||
console.log('Deleted existing API changes comment (no changes detected)');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not post comment:', error);
|
||||
}
|
||||
|
||||
- name: Cleanup servers
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f /tmp/current-server.pid ]; then
|
||||
kill $(cat /tmp/current-server.pid) || true
|
||||
fi
|
||||
if [ -f /tmp/main-server.pid ]; then
|
||||
kill $(cat /tmp/main-server.pid) || true
|
||||
fi
|
||||
|
||||
- name: Upload API specifications and diffs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-specifications-and-diffs
|
||||
path: |
|
||||
/tmp/main-server.log
|
||||
/tmp/current-server.log
|
||||
*-api.json
|
||||
*-schema-introspection.json
|
||||
*-diff.md
|
||||
*-diff.json
|
||||
|
||||
@@ -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,23 +3,15 @@ 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: |
|
||||
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:
|
||||
@@ -33,16 +25,23 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-chrome-extension/**
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Chrome Extension / Run build
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx build twenty-chrome-extension
|
||||
ci-chrome-extension-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
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
|
||||
|
||||
- name: Mark as Valid if No Changes
|
||||
if: steps.changed-files.outputs.changed != 'true'
|
||||
run: |
|
||||
echo "No relevant changes detected. Marking as valid."
|
||||
|
||||
@@ -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: 5
|
||||
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
|
||||
@@ -0,0 +1,52 @@
|
||||
name: CI E2E Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
packages/**
|
||||
playwright.config.ts
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes detected. Marking as valid."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Install Playwright Browsers
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: yarn test:e2e companies
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
@@ -1,62 +0,0 @@
|
||||
name: CI Emails
|
||||
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: |
|
||||
package.json
|
||||
packages/twenty-emails/**
|
||||
emails-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
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: Build twenty-emails
|
||||
run: npx nx build twenty-emails
|
||||
- name: Run email tests
|
||||
run: |
|
||||
# Start the email server in the background
|
||||
npx nx run twenty-emails:start &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for server to start
|
||||
sleep 20
|
||||
|
||||
# Check if server is running
|
||||
if ! curl -s http://localhost:4001/preview/test.email > /dev/null; then
|
||||
echo "Email server failed to start"
|
||||
kill $SERVER_PID
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Kill the server
|
||||
kill $SERVER_PID
|
||||
ci-emails-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: [changed-files-check, emails-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
+120
-97
@@ -3,30 +3,17 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
|
||||
pull_request:
|
||||
|
||||
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
|
||||
@@ -39,34 +26,46 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-front/**
|
||||
packages/twenty-ui/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Diagnostic disk space issue
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Front / Build storybook
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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: shipfox-8vcpu-ubuntu-2204
|
||||
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:
|
||||
@@ -74,57 +73,73 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-front/**
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Install Playwright
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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:
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }}
|
||||
front-sb-test-performance:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: front-sb-test
|
||||
runs-on: shipfox-8vcpu-ubuntu-2204
|
||||
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
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
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 }}
|
||||
files: |
|
||||
packages/twenty-front/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Install Playwright
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: cd packages/twenty-front && npx playwright install
|
||||
- name: Front / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Run storybook tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx run twenty-front:storybook:serve-and-test:static:performance
|
||||
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 }}
|
||||
@@ -133,27 +148,41 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore storybook build cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
files: |
|
||||
packages/twenty-front/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Front / Restore Storybook Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
cd packages/twenty-front
|
||||
touch .env
|
||||
echo "REACT_APP_SERVER_BASE_URL: $REACT_APP_SERVER_BASE_URL" >> .env
|
||||
- name: Publish to Chromatic
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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]
|
||||
@@ -166,41 +195,35 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.TASK_CACHE_KEY }}
|
||||
files: |
|
||||
packages/twenty-front/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Front / Restore ${{ matrix.task }} task cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: ${{ matrix.task }}
|
||||
- name: Reset .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: reset:env
|
||||
- name: Run ${{ matrix.task }} task
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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: 5
|
||||
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
|
||||
|
||||
@@ -5,28 +5,15 @@ on:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
services:
|
||||
@@ -34,9 +21,9 @@ jobs:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -53,74 +40,48 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Server / Restore Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:backend
|
||||
- name: Server / Run lint & typecheck
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: lint,typecheck
|
||||
- name: Server / Build
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx build twenty-server
|
||||
- name: Server / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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: |
|
||||
CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true)
|
||||
|
||||
CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$CORE_MIGRATION_FILE" ]; then
|
||||
echo "::error::Unexpected migration files were generated. Please create a proper migration manually."
|
||||
echo "$CORE_MIGRATION_OUTPUT"
|
||||
|
||||
rm -f 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 }}
|
||||
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
- name: Worker / Run
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx run twenty-server:worker:ci
|
||||
|
||||
server-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
needs: server-setup
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
@@ -129,13 +90,26 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
- name: Server / Run Tests
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Server / Restore Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:backend
|
||||
- name: Server / Run Tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
@@ -143,20 +117,16 @@ jobs:
|
||||
|
||||
server-integration-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
needs: server-setup
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -168,70 +138,40 @@ jobs:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:latest
|
||||
env:
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
ports:
|
||||
- 8123:8123
|
||||
- 9000:9000
|
||||
options: >-
|
||||
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
NODE_ENV: test
|
||||
ANALYTICS_ENABLED: true
|
||||
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
|
||||
CLICKHOUSE_PASSWORD: clickhousePassword
|
||||
SHARD_COUNTER: 4
|
||||
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 integrations tests
|
||||
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
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
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: Run ClickHouse migrations
|
||||
run: npx nx clickhouse:migrate twenty-server
|
||||
- name: Run ClickHouse seeds
|
||||
run: npx nx clickhouse:seed twenty-server
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Server / Restore Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:backend
|
||||
- name: Server / Run Integration Tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: 'test:integration'
|
||||
configuration: 'with-db-reset'
|
||||
args: --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
|
||||
ci-server-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
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
|
||||
tasks: "test:integration:with-db-reset"
|
||||
- name: Server / Upload reset-logs file
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reset-logs
|
||||
path: reset-logs.log
|
||||
|
||||
@@ -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: 5
|
||||
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,92 @@
|
||||
name: 'Test Docker Compose'
|
||||
on:
|
||||
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-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
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-docker/**
|
||||
docker-compose.yml
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed != 'true'
|
||||
run: echo "No relevant changes detected. Marking as valid."
|
||||
|
||||
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
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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-spilo/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 "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "PGPASSWORD_SUPERUSER=$(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: 5
|
||||
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 "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=$!
|
||||
|
||||
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 "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/
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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
|
||||
@@ -21,7 +21,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
danger-js:
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'closed'
|
||||
steps:
|
||||
|
||||
@@ -5,31 +5,24 @@ on:
|
||||
- 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: |
|
||||
package.json
|
||||
packages/twenty-website/**
|
||||
website-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -41,31 +34,31 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: 'package.json, packages/twenty-website/**'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
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";'
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
|
||||
- name: Website / Run migrations
|
||||
run: npx nx database:migrate twenty-website
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
run: npx nx database:migrate twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
|
||||
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
|
||||
- name: Website / Build Website
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
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: 5
|
||||
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://postgres:twenty@localhost:5432/default
|
||||
|
||||
- name: Mark as VALID
|
||||
if: steps.changed-files.outputs.changed != 'true' # If no changes, mark as valid
|
||||
run: echo "No relevant changes detected. CI is valid."
|
||||
@@ -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,36 +0,0 @@
|
||||
name: 'Preview Environment Dispatch'
|
||||
|
||||
on:
|
||||
# Using pull_request_target instead of pull_request to have access to secrets for external contributors
|
||||
# Security note: This is safe because we're only using the repository-dispatch action with limited scope
|
||||
# and not checking out or running any code from the external contributor's PR
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
paths:
|
||||
- packages/twenty-docker/**
|
||||
- packages/twenty-server/**
|
||||
- packages/twenty-front/**
|
||||
- .github/workflows/preview-env-dispatch.yaml
|
||||
- .github/workflows/preview-env-keepalive.yaml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
trigger-preview:
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
pull-requests: read
|
||||
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || (github.event.action == 'labeled' && github.event.label.name == 'preview-app')
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger preview environment workflow
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
event-type: preview-environment
|
||||
client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}'
|
||||
@@ -1,150 +0,0 @@
|
||||
name: 'Preview Environment Keep Alive'
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [preview-environment]
|
||||
|
||||
jobs:
|
||||
preview-environment:
|
||||
timeout-minutes: 310
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.client_payload.pr_head_sha }}
|
||||
|
||||
- name: Run compose setup
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
# change image to localbuild using yq
|
||||
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
echo "Setting up .env file..."
|
||||
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
|
||||
|
||||
echo "Generating secrets..."
|
||||
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
|
||||
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
|
||||
echo "SIGN_IN_PREFILLED=true" >> packages/twenty-docker/.env
|
||||
echo "Docker compose build..."
|
||||
cd packages/twenty-docker/
|
||||
docker compose build
|
||||
working-directory: ./
|
||||
|
||||
- name: Create Tunnel
|
||||
id: expose-tunnel
|
||||
uses: codetalkio/expose-tunnel@v1.5.0
|
||||
with:
|
||||
service: bore.pub
|
||||
port: 3000
|
||||
|
||||
- name: Start services with correct SERVER_URL
|
||||
run: |
|
||||
cd packages/twenty-docker/
|
||||
|
||||
# Update the SERVER_URL with the tunnel URL
|
||||
echo "Setting SERVER_URL to ${{ steps.expose-tunnel.outputs.tunnel-url }}"
|
||||
sed -i '/SERVER_URL=/d' .env
|
||||
echo "SERVER_URL=${{ steps.expose-tunnel.outputs.tunnel-url }}" >> .env
|
||||
|
||||
# Start the services
|
||||
echo "Docker compose up..."
|
||||
docker compose up -d || {
|
||||
echo "Docker compose failed to start"
|
||||
docker compose logs
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "Waiting for services to be ready..."
|
||||
count=0
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
sleep 5
|
||||
count=$((count+1))
|
||||
if [ $count -gt 60 ]; then
|
||||
echo "Timeout waiting for services to be ready"
|
||||
docker compose logs
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for services... ($count/60)"
|
||||
done
|
||||
|
||||
echo "All services are up and running!"
|
||||
working-directory: ./
|
||||
|
||||
- name: Seed Dev Workspace
|
||||
run: |
|
||||
cd packages/twenty-docker/
|
||||
echo "Seeding full dev workspace..."
|
||||
if ! docker compose exec -T server yarn command:prod -- workspace:seed:dev; then
|
||||
echo "❌ Seeding full dev workspace failed. Dumping server logs..."
|
||||
docker compose logs server
|
||||
exit 1
|
||||
fi
|
||||
working-directory: ./
|
||||
|
||||
- name: Output tunnel URL to logs
|
||||
run: |
|
||||
echo "✅ Preview Environment Ready!"
|
||||
echo "🔗 Preview URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}"
|
||||
echo "⏱️ This environment will be available for 5 hours"
|
||||
|
||||
- name: Post comment on PR
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const COMMENT_MARKER = '<!-- PR_PREVIEW_ENV -->';
|
||||
const commentBody = `${COMMENT_MARKER}
|
||||
🚀 **Preview Environment Ready!**
|
||||
|
||||
Your preview environment is available at: ${{ steps.expose-tunnel.outputs.tunnel-url }}
|
||||
|
||||
This environment will automatically shut down when the PR is closed or after 5 hours.`;
|
||||
|
||||
// Get all comments
|
||||
const {data: comments} = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: ${{ github.event.client_payload.pr_number }},
|
||||
});
|
||||
|
||||
// Find our comment
|
||||
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
console.log('Updated existing comment');
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: ${{ github.event.client_payload.pr_number }},
|
||||
body: commentBody
|
||||
});
|
||||
console.log('Created new comment');
|
||||
}
|
||||
|
||||
- name: Keep tunnel alive for 5 hours
|
||||
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/twenty-docker/
|
||||
docker compose down -v
|
||||
working-directory: ./
|
||||
-16
@@ -18,7 +18,6 @@
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.vercel
|
||||
.swc
|
||||
|
||||
**/**/logs/**
|
||||
|
||||
@@ -31,18 +30,3 @@ storybook-static
|
||||
test-results/
|
||||
dump.rdb
|
||||
.tinyb
|
||||
|
||||
.notes
|
||||
/data/
|
||||
/.devenv/
|
||||
/.direnv/
|
||||
/.pre-commit-config.yaml
|
||||
/.envrc
|
||||
/devenv.nix
|
||||
/flake.lock
|
||||
/flake.nix
|
||||
|
||||
.crowdin.yml
|
||||
.react-email/
|
||||
|
||||
mcp.json
|
||||
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
+8
-38
@@ -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",
|
||||
@@ -37,50 +39,18 @@
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeVersion": "^22.12.0",
|
||||
"runtimeVersion": "18",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"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
-5
@@ -43,11 +43,7 @@
|
||||
],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"search.exclude": {
|
||||
"**/.yarn": true
|
||||
"**/.yarn": true,
|
||||
},
|
||||
"eslint.debug": true,
|
||||
"files.associations": {
|
||||
".cursorrules": "markdown"
|
||||
},
|
||||
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}"
|
||||
}
|
||||
|
||||
Vendored
-8
@@ -24,10 +24,6 @@
|
||||
"name": "packages/twenty-emails",
|
||||
"path": "../packages/twenty-emails"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-shared",
|
||||
"path": "../packages/twenty-shared"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-server",
|
||||
"path": "../packages/twenty-server"
|
||||
@@ -44,10 +40,6 @@
|
||||
"name": "tools/eslint-rules",
|
||||
"path": "../tools/eslint-rules"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-e2e-testing",
|
||||
"path": "../packages/twenty-e2e-testing"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
enableConstraintsChecks: true
|
||||
|
||||
enableHardenedMode: true
|
||||
|
||||
enableInlineHunks: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
postgres-on-docker:
|
||||
docker run -d \
|
||||
--name twenty_pg \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e PGUSER_SUPERUSER=postgres \
|
||||
-e PGPASSWORD_SUPERUSER=twenty \
|
||||
-e ALLOW_NOSSL=true \
|
||||
-v twenty_db_data:/var/lib/postgresql/data \
|
||||
-v twenty_db_data:/home/postgres/pgdata \
|
||||
-p 5432:5432 \
|
||||
postgres:16
|
||||
twentycrm/twenty-postgres-spilo:latest
|
||||
@echo "Waiting for PostgreSQL to be ready..."
|
||||
@until docker exec twenty_pg psql -U postgres -d postgres \
|
||||
@until PGPASSWORD=twenty psql -h localhost -p 5432 -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 \
|
||||
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres \
|
||||
-c "CREATE DATABASE \"default\" WITH OWNER postgres;" \
|
||||
-c "CREATE DATABASE \"test\" WITH OWNER postgres;"
|
||||
|
||||
redis-on-docker:
|
||||
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
|
||||
|
||||
clickhouse-on-docker:
|
||||
docker run -d --name twenty_clickhouse -p 8123:8123 -p 9000:9000 -e CLICKHOUSE_PASSWORD=clickhousePassword clickhouse/clickhouse-server:latest \
|
||||
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
|
||||
@@ -1,53 +1,66 @@
|
||||
|
||||
<br />
|
||||
<br>
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM </h2>
|
||||
<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 />
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/preview-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/preview-light.png" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/preview-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/preview-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/preview-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<br>
|
||||
|
||||
<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.
|
||||
|
||||
# Installation
|
||||
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.
|
||||
|
||||
See:
|
||||
<br>
|
||||
|
||||
# Demo
|
||||
Go to <a href="https://demo.twenty.com/">demo.twenty.com</a> and login with the following credentials:
|
||||
|
||||
```
|
||||
email: tim@apple.dev
|
||||
password: Applecar2025
|
||||
```
|
||||
|
||||
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 />
|
||||
<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)
|
||||
@@ -60,8 +73,8 @@ Below are a few features we have implemented to date:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/index-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/index-light.png" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/index-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/index-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/visualise-customer-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</p>
|
||||
@@ -70,9 +83,9 @@ Below are a few features we have implemented to date:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/kanban-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/kanban-light.png" />
|
||||
<img src="./packages/twenty-docs/static/img/follow-your-deals-light.png" alt="Opportunities view" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/kanban-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/kanban-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/follow-your-deals-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -80,9 +93,9 @@ Below are a few features we have implemented to date:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/emails-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/emails-light.png" />
|
||||
<img src="./packages/twenty-docs/static/img/emails-light.png" alt="Emails" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/emails-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/emails-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/rich-notes-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -90,9 +103,9 @@ Below are a few features we have implemented to date:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/data-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/data-light.png" />
|
||||
<img src="./packages/twenty-docs/static/img/data-light.png" alt="Data model" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/data-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/data-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/rich-notes-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -100,9 +113,9 @@ Below are a few features we have implemented to date:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/notes-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/notes-light.png" />
|
||||
<img src="./packages/twenty-docs/static/img/notes-light.png" alt="Rich notes" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/notes-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/notes-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/rich-notes-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -110,9 +123,9 @@ Below are a few features we have implemented to date:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/tasks-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/tasks-light.png" />
|
||||
<img src="./packages/twenty-docs/static/img/create-tasks-light.png" alt="Tasks" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/tasks-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/tasks-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/create-tasks-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -120,9 +133,9 @@ Below are a few features we have implemented to date:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/keyboard-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/keyboard-light.png" />
|
||||
<img src="./packages/twenty-docs/static/img/keyboard-dark.png" alt="Keyboard shortcuts" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/keyboard-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/keyboard-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/shortcut-navigation-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -130,44 +143,30 @@ Below are a few features we have implemented to date:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/api-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/api-light.png" />
|
||||
<img src="./packages/twenty-docs/static/img/api-light.png" alt="API" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/api-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/twentyhq/twenty/v0.12.0/packages/twenty-docs/static/img/api-light.png">
|
||||
<img src="./packages/twenty-docs/static/img/shortcut-navigation-light.png" alt="Companies view" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
<br />
|
||||
# What's In Store
|
||||
|
||||
# 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/), [Emotion](https://emotion.sh/) and [Lingui](https://lingui.dev/)
|
||||
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.
|
||||
|
||||
# Thanks
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="30" alt="Chromatic" /></a>
|
||||
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="30" alt="Greptile" /></a>
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="30" alt="Sentry" /></a>
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="30" alt="Crowdin" /></a>
|
||||
</p>
|
||||
|
||||
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
|
||||
|
||||
<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",
|
||||
}
|
||||
]
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔧 Checking dependencies..."
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo -e "\t❌ Docker is not installed or not in PATH. Please install Docker first.\n\t\tSee https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
# Check if docker compose plugin is installed
|
||||
if ! docker compose version &>/dev/null; then
|
||||
echo -e "\t❌ Docker Compose is not installed or not in PATH (n.b. docker-compose is deprecated)\n\t\tUpdate docker or install docker-compose-plugin\n\t\tOn Linux: sudo apt-get install docker-compose-plugin\n\t\tSee https://docs.docker.com/compose/install/"
|
||||
exit 1
|
||||
fi
|
||||
# Check if docker is started
|
||||
if ! docker info &>/dev/null; then
|
||||
echo -e "\t❌ Docker is not running.\n\t\tPlease start Docker Desktop, Docker or check documentation at https://docs.docker.com/config/daemon/start/"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v curl &>/dev/null; then
|
||||
echo -e "\t❌ Curl is not installed or not in PATH.\n\t\tOn macOS: brew install curl\n\t\tOn Linux: sudo apt install curl"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if docker compose version is >= 2
|
||||
if [ "$(docker compose version --short | cut -d' ' -f3 | cut -d'.' -f1)" -lt 2 ]; then
|
||||
echo -e "\t❌ Docker Compose is outdated. Please update Docker Compose to version 2 or higher.\n\t\tSee https://docs.docker.com/compose/install/linux/"
|
||||
exit 1
|
||||
fi
|
||||
# Check if docker-compose is installed, if so issue a warning if version is < 2
|
||||
if command -v docker-compose &>/dev/null; then
|
||||
if [ "$(docker-compose version --short | cut -d' ' -f3 | cut -d'.' -f1)" -lt 2 ]; then
|
||||
echo -e "\n\t⚠️ 'docker-compose' is installed but outdated. Make sure to use 'docker compose' or to upgrade 'docker-compose' to version 2.\n\t\tSee https://docs.docker.com/compose/install/standalone/\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Catch errors
|
||||
set -e
|
||||
function on_exit {
|
||||
# $? is the exit status of the last command executed
|
||||
local exit_status=$?
|
||||
if [ $exit_status -ne 0 ]; then
|
||||
echo "❌ Something went wrong, exiting: $exit_status"
|
||||
fi
|
||||
}
|
||||
trap on_exit EXIT
|
||||
|
||||
# Use environment variables VERSION and BRANCH, with defaults if not set
|
||||
version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)}
|
||||
branch=${BRANCH:-$version}
|
||||
|
||||
echo "🚀 Using version $version and branch $branch"
|
||||
|
||||
dir_name="twenty"
|
||||
function ask_directory {
|
||||
read -p "📁 Enter the directory name to setup the project (default: $dir_name): " answer
|
||||
if [ -n "$answer" ]; then
|
||||
dir_name=$answer
|
||||
fi
|
||||
}
|
||||
|
||||
ask_directory
|
||||
|
||||
while [ -d "$dir_name" ]; do
|
||||
read -p "🚫 Directory '$dir_name' already exists. Do you want to overwrite it? (y/N) " answer
|
||||
if [ "$answer" = "y" ]; then
|
||||
break
|
||||
else
|
||||
ask_directory
|
||||
fi
|
||||
done
|
||||
|
||||
# Create a directory named twenty
|
||||
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
|
||||
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
|
||||
|
||||
# Copy twenty/packages/twenty-docker/.env.example to .env
|
||||
echo -e "\t• Setting up .env file"
|
||||
curl -sLo .env https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/.env.example
|
||||
|
||||
# Replace TAG=latest by TAG=<latest_release or version input>
|
||||
if [[ $(uname) == "Darwin" ]]; then
|
||||
# Running on macOS
|
||||
sed -i '' "s/TAG=latest/TAG=$version/g" .env
|
||||
else
|
||||
# Assuming Linux
|
||||
sed -i'' "s/TAG=latest/TAG=$version/g" .env
|
||||
fi
|
||||
|
||||
# Generate random strings for secrets
|
||||
echo "# === Randomly generated secrets ===" >>.env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "" >>.env
|
||||
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
|
||||
echo "" >>.env
|
||||
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env
|
||||
|
||||
echo -e "\t• .env configuration completed"
|
||||
|
||||
port=3000
|
||||
# Check if command nc is available
|
||||
if command -v nc &> /dev/null; then
|
||||
# Check if port 3000 is already in use, propose to change it
|
||||
while nc -zv localhost $port &>/dev/null; do
|
||||
read -p "🚫 Port $port is already in use. Do you want to use another port? (Y/n) " answer
|
||||
if [ "$answer" = "n" ]; then
|
||||
continue
|
||||
fi
|
||||
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
|
||||
fi
|
||||
|
||||
# Ask user if they want to start the project
|
||||
read -p "🚀 Do you want to start the project now? (Y/n) " answer
|
||||
if [ "$answer" = "n" ]; then
|
||||
echo "✅ Project setup completed. Run 'docker compose up -d' to start."
|
||||
exit 0
|
||||
else
|
||||
echo "🐳 Starting Docker containers..."
|
||||
docker compose up -d
|
||||
# Check if port is listening
|
||||
echo "Waiting for server to be healthy, it might take a few minutes while we initialize the database..."
|
||||
# Tail logs of the server until it's ready
|
||||
docker compose logs -f server &
|
||||
pid=$!
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
sleep 1
|
||||
done
|
||||
kill $pid
|
||||
echo ""
|
||||
echo "✅ Server is up and running"
|
||||
fi
|
||||
|
||||
function ask_open_browser {
|
||||
read -p "🌐 Do you want to open the project in your browser? (Y/n) " answer
|
||||
if [ "$answer" = "n" ]; then
|
||||
echo "✅ Setup completed. Access your project at http://localhost:$port"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Ask user if they want to open the project
|
||||
# Running on macOS
|
||||
if [[ $(uname) == "Darwin" ]]; then
|
||||
ask_open_browser
|
||||
|
||||
open "http://localhost:$port"
|
||||
# Assuming Linux
|
||||
else
|
||||
# xdg-open is not installed, we could be running in a non gui environment
|
||||
if command -v xdg-open >/dev/null 2>&1; then
|
||||
ask_open_browser
|
||||
|
||||
xdg-open "http://localhost:$port"
|
||||
else
|
||||
echo "✅ Setup completed. Your project is available at http://localhost:$port"
|
||||
fi
|
||||
fi
|
||||
@@ -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",
|
||||
@@ -116,11 +112,10 @@
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
|
||||
"command": "VITE_DISABLE_ESLINT_CHECKER=true storybook build",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"storybook:serve:dev": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -152,14 +147,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": {
|
||||
@@ -186,10 +179,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 +191,8 @@
|
||||
"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} --configuration={args.performance}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'"
|
||||
],
|
||||
"shard": "1/1",
|
||||
"checkCoverage": true,
|
||||
"port": 6006
|
||||
}
|
||||
},
|
||||
|
||||
+34
-42
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"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.700.0",
|
||||
"@aws-sdk/client-s3": "^3.700.0",
|
||||
"@aws-sdk/client-sts": "^3.700.0",
|
||||
"@aws-sdk/credential-providers": "^3.700.0",
|
||||
"@aws-sdk/client-lambda": "^3.614.0",
|
||||
"@aws-sdk/client-s3": "^3.363.0",
|
||||
"@aws-sdk/credential-providers": "^3.363.0",
|
||||
"@blocknote/mantine": "^0.15.3",
|
||||
"@blocknote/react": "^0.15.3",
|
||||
"@codesandbox/sandpack-react": "^2.13.5",
|
||||
"@dagrejs/dagre": "^1.1.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
@@ -34,21 +35,22 @@
|
||||
"@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",
|
||||
"@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.35",
|
||||
"@react-email/render": "0.0.17",
|
||||
"@sentry/node": "^9.26.0",
|
||||
"@sentry/profiling-node": "^9.26.0",
|
||||
"@sentry/react": "^9.26.0",
|
||||
"@react-email/components": "0.0.12",
|
||||
"@react-email/render": "0.0.10",
|
||||
"@sentry/node": "^8",
|
||||
"@sentry/profiling-node": "^8",
|
||||
"@sentry/react": "^8",
|
||||
"@sniptt/guards": "^0.2.0",
|
||||
"@stoplight/elements": "^8.0.5",
|
||||
"@swc/jest": "^0.2.29",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tabler/icons-react": "^2.44.0",
|
||||
"@tiptap/extension-hard-break": "^2.9.1",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/facepaint": "^1.2.5",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
@@ -58,7 +60,6 @@
|
||||
"@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",
|
||||
@@ -67,13 +68,12 @@
|
||||
"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-parser": "^5.1.1",
|
||||
"cron-validate": "^1.4.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"danger-plugin-todos": "^1.3.1",
|
||||
@@ -88,17 +88,14 @@
|
||||
"esbuild-plugin-svgr": "^2.1.0",
|
||||
"facepaint": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"framer-motion": "^11.18.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"framer-motion": "^10.12.17",
|
||||
"googleapis": "105",
|
||||
"graphiql": "^3.1.1",
|
||||
"graphql": "16.8.0",
|
||||
"graphql-fields": "^2.0.3",
|
||||
"graphql-middleware": "^6.1.35",
|
||||
"graphql-rate-limit": "^3.3.0",
|
||||
"graphql-redis-subscriptions": "^2.7.0",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"graphql-sse": "^2.5.4",
|
||||
"graphql-subscriptions": "2.0.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
@@ -117,7 +114,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",
|
||||
@@ -139,6 +135,8 @@
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"nodemailer": "^6.9.8",
|
||||
"openapi-types": "^12.1.3",
|
||||
"overlayscrollbars": "^2.6.1",
|
||||
"overlayscrollbars-react": "^0.5.4",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
@@ -163,13 +161,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",
|
||||
@@ -180,13 +179,13 @@
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.32.1",
|
||||
"slash": "^5.1.0",
|
||||
"storybook-addon-mock-date": "^0.6.0",
|
||||
"stripe": "^17.3.1",
|
||||
"stripe": "^14.17.0",
|
||||
"ts-key-enum": "^2.0.12",
|
||||
"tslib": "^2.3.0",
|
||||
"tsup": "^8.2.4",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.3.3",
|
||||
"use-context-selector": "^2.0.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
@@ -217,7 +216,7 @@
|
||||
"@nx/vite": "18.3.3",
|
||||
"@nx/web": "18.3.3",
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@sentry/types": "^8",
|
||||
"@sentry/types": "^7.109.0",
|
||||
"@storybook/addon-actions": "^7.6.3",
|
||||
"@storybook/addon-coverage": "^1.0.0",
|
||||
"@storybook/addon-essentials": "^7.6.7",
|
||||
@@ -236,26 +235,26 @@
|
||||
"@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.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@types/addressparser": "^1.0.3",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/bytes": "^3.1.1",
|
||||
"@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",
|
||||
@@ -269,7 +268,7 @@
|
||||
"@types/lodash.upperfirst": "^4.3.7",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node": "18.19.26",
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
@@ -285,7 +284,6 @@
|
||||
"@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",
|
||||
@@ -298,7 +296,6 @@
|
||||
"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",
|
||||
@@ -311,7 +308,7 @@
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "30.0.0-beta.3",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-environment-node": "^29.4.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jsdom": "~22.1.0",
|
||||
@@ -333,15 +330,14 @@
|
||||
"ts-node": "10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-checker": "^0.6.2",
|
||||
"vite-plugin-cjs-interop": "^2.2.0",
|
||||
"vite-plugin-dts": "3.8.1",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.12.0",
|
||||
"node": "^18.17.1",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
@@ -351,10 +347,7 @@
|
||||
"resolutions": {
|
||||
"graphql": "16.8.0",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.3.3",
|
||||
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
||||
"prosemirror-view": "1.40.0",
|
||||
"prosemirror-transform": "1.10.4"
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
@@ -372,7 +365,6 @@
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"tools/eslint-rules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['../../.eslintrc.global.cjs', '../../.eslintrc.react.cjs'],
|
||||
ignorePatterns: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'**/generated/*',
|
||||
],
|
||||
extends: ['../../.eslintrc.cjs', '../../.eslintrc.react.cjs'],
|
||||
ignorePatterns: ['!**/*', 'node_modules', 'dist', '**/generated/*'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
files: ['*.ts', '*.tsx'],
|
||||
parserOptions: {
|
||||
project: ['packages/twenty-chrome-extension/tsconfig.*.json'],
|
||||
project: ['packages/twenty-chrome-extension/tsconfig.{json,*.json}'],
|
||||
},
|
||||
rules: {
|
||||
'@nx/workspace-explicit-boolean-predicates-in-if': 'warn',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="images/icons/android/android-launchericon-48-48.png" />
|
||||
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Twenty</title>
|
||||
</head>
|
||||
|
||||
@@ -6,8 +6,5 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npx vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-shared": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="images/icons/android/android-launchericon-48-48.png" />
|
||||
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Twenty</title>
|
||||
</head>
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "{projectRoot}/dist"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"executor": "nx:run-commands",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="images/icons/android/android-launchericon-48-48.png" />
|
||||
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Twenty</title>
|
||||
<style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// Open options page programmatically in a new tab.
|
||||
// chrome.runtime.onInstalled.addListener((details) => {
|
||||
// if (details.reason === 'install') {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
interface CustomDiv extends HTMLDivElement {
|
||||
onClickHandler: (newHandler: () => void) => void;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLink
|
||||
import extractDomain from '~/contentScript/utils/extractDomain';
|
||||
import { createCompany, fetchCompany } from '~/db/company.db';
|
||||
import { CompanyInput } from '~/db/types/company.types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const checkIfCompanyExists = async () => {
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 'twenty-shared/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const checkIfPersonExists = async () => {
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
|
||||
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
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,5 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const btn = document.getElementById('twenty-settings-btn');
|
||||
if (!isDefined(btn)) {
|
||||
const div = document.createElement('div');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const changeSidePanelUrl = async (url: string) => {
|
||||
if (isDefined(url)) {
|
||||
chrome.storage.local.set({ navigateSidepanel: 'sidepanel' });
|
||||
|
||||
+4
-1
@@ -1,4 +1,7 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
// 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 '~/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) => {
|
||||
// Regular expression to match the company ID
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
Tokens,
|
||||
} from '~/db/types/auth.types';
|
||||
import { EXCHANGE_AUTHORIZATION_CODE } from '~/graphql/auth/mutations';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { callMutation } from '~/utils/requestDb';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const exchangeAuthorizationCode = async (
|
||||
exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
|
||||
@@ -6,9 +6,9 @@ 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';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const fetchCompany = async (
|
||||
companyfilerInput: CompanyFilterInput,
|
||||
|
||||
@@ -6,9 +6,9 @@ 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';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const fetchPerson = async (
|
||||
personFilterData: PersonFilterInput,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { Tokens } from '~/db/types/auth.types';
|
||||
import { RENEW_TOKEN } from '~/graphql/auth/mutations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const renewToken = async (
|
||||
appToken: string,
|
||||
|
||||
@@ -6358,7 +6358,7 @@ export type RelationConnection = {
|
||||
};
|
||||
|
||||
export type RelationDefinition = {
|
||||
direction: RelationType;
|
||||
direction: RelationDefinitionType;
|
||||
relationId: Scalars['UUID'];
|
||||
sourceFieldMetadata: Field;
|
||||
sourceObjectMetadata: Object;
|
||||
@@ -6367,7 +6367,7 @@ export type RelationDefinition = {
|
||||
};
|
||||
|
||||
/** Relation definition type */
|
||||
export enum RelationType {
|
||||
export enum RelationDefinitionType {
|
||||
ManyToMany = 'MANY_TO_MANY',
|
||||
ManyToOne = 'MANY_TO_ONE',
|
||||
OneToMany = 'ONE_TO_MANY',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Settings from '~/options/Settings';
|
||||
import Sidepanel from '~/options/Sidepanel';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const App = () => {
|
||||
const [currentScreen, setCurrentScreen] = useState('');
|
||||
|
||||
@@ -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 { clearStore } from '~/utils/apolloClient';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
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/utils';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
display: block;
|
||||
|
||||
@@ -42,6 +42,7 @@ const StyledInput = styled.input`
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
|
||||
&::placeholder {
|
||||
|
||||
@@ -1,7 +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/utils';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export type ToggleSize = 'small' | 'medium';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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/utils';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const clearStore = () => {
|
||||
chrome.storage.local.remove([
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { OperationVariables } from '@apollo/client';
|
||||
import { DocumentNode } from 'graphql';
|
||||
|
||||
import getApolloClient from '~/utils/apolloClient';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
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,16 +1,18 @@
|
||||
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
|
||||
#PGUSER_SUPERUSER=postgres
|
||||
#PGPASSWORD_SUPERUSER=replace_me_with_a_strong_password
|
||||
|
||||
PG_DATABASE_HOST=db:5432
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
SERVER_URL=http://localhost:3000
|
||||
|
||||
# Use openssl rand -base64 32 for each secret
|
||||
# APP_SECRET=replace_me_with_a_random_string
|
||||
|
||||
SIGN_IN_PREFILLED=true
|
||||
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# STORAGE_S3_REGION=eu-west3
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
# Makefile for building Twenty CRM docker images.
|
||||
# Set the tag and/or target build platform using make command-line variables assignments.
|
||||
#
|
||||
# Optional make variables:
|
||||
# PLATFORM - defaults to 'linux/amd64'
|
||||
# TAG - defaults to 'latest'
|
||||
#
|
||||
# Example: make
|
||||
# Example: make PLATFORM=linux/aarch64 TAG=my-tag
|
||||
|
||||
PLATFORM ?= linux/amd64
|
||||
TAG ?= latest
|
||||
|
||||
prod-build:
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --tag twenty . && cd -
|
||||
|
||||
prod-run:
|
||||
@docker run -d -p 3000:3000 --name twenty twenty:$(TAG)
|
||||
@docker run -d -p 3000:3000 --name twenty twenty
|
||||
|
||||
prod-postgres-run:
|
||||
@docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres:$(TAG)
|
||||
@docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres
|
||||
|
||||
prod-website-build:
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty-website/Dockerfile --platform $(PLATFORM) --tag twenty-website:$(TAG) . && cd -
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty-website/Dockerfile --tag twenty-website . && cd -
|
||||
|
||||
prod-website-run:
|
||||
@docker run -d -p 3000:3000 --name twenty-website twenty-website:$(TAG)
|
||||
@docker run -d -p 3000:3000 --name twenty-website twenty-website
|
||||
|
||||
@@ -1,99 +1,70 @@
|
||||
version: "3.9"
|
||||
name: twenty
|
||||
|
||||
services:
|
||||
server:
|
||||
image: twentycrm/twenty:${TAG:-latest}
|
||||
change-vol-ownership:
|
||||
image: ubuntu
|
||||
user: root
|
||||
volumes:
|
||||
- server-local-data:/app/packages/twenty-server/.local-storage
|
||||
- server-local-data:/tmp/server-local-data
|
||||
- docker-data:/tmp/docker-data
|
||||
command: >
|
||||
bash -c "
|
||||
chown -R 1000:1000 /tmp/server-local-data
|
||||
&& chown -R 1000:1000 /tmp/docker-data"
|
||||
|
||||
server:
|
||||
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://${PGUSER_SUPERUSER:-postgres}:${PGPASSWORD_SUPERUSER:-twenty}@${PG_DATABASE_HOST:-db:5432}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
|
||||
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:-}
|
||||
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
depends_on:
|
||||
change-vol-ownership:
|
||||
condition: service_completed_successfully
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:3000/healthz
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
retries: 10
|
||||
restart: always
|
||||
|
||||
worker:
|
||||
image: twentycrm/twenty:${TAG:-latest}
|
||||
volumes:
|
||||
- server-local-data:/app/packages/twenty-server/${STORAGE_LOCAL_PATH:-.local-storage}
|
||||
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://${PGUSER_SUPERUSER:-postgres}:${PGPASSWORD_SUPERUSER:-twenty}@${PG_DATABASE_HOST:-db:5432}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: "true" # it already runs on the server
|
||||
|
||||
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:-}
|
||||
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -102,14 +73,16 @@ services:
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
image: twentycrm/twenty-postgres-spilo:${TAG}
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
- db-data:/home/postgres/pgdata
|
||||
environment:
|
||||
POSTGRES_USER: ${PG_DATABASE_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${PG_DATABASE_PASSWORD:-postgres}
|
||||
PGUSER_SUPERUSER: ${PGUSER_SUPERUSER:-postgres}
|
||||
PGPASSWORD_SUPERUSER: ${PGPASSWORD_SUPERUSER:-twenty}
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
healthcheck:
|
||||
test: pg_isready -U ${PG_DATABASE_USER:-postgres} -h localhost -d postgres
|
||||
test: pg_isready -U ${PGUSER_SUPERUSER:-postgres} -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -118,8 +91,8 @@ services:
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
command: ["--maxmemory-policy", "noeviction"]
|
||||
|
||||
volumes:
|
||||
docker-data:
|
||||
db-data:
|
||||
server-local-data:
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# README
|
||||
DISCLAIMER: The k8s and podman deployments are not maintained by the core team.
|
||||
These files are provided and maintained by the community. Twenty core team
|
||||
maintains support for docker deployment.
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -27,17 +27,13 @@ spec:
|
||||
claimName: twentycrm-db-pvc
|
||||
containers:
|
||||
- name: twentycrm
|
||||
image: twentycrm/twenty-postgres-spilo:latest
|
||||
image: twentycrm/twenty-postgres:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: PGUSER_SUPERUSER
|
||||
value: "postgres"
|
||||
- name: PGPASSWORD_SUPERUSER
|
||||
value: "postgres"
|
||||
- name: SPILO_PROVIDER
|
||||
value: "local"
|
||||
- name: ALLOW_NOSSL
|
||||
value: "true"
|
||||
value: "twenty"
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: tcp
|
||||
|
||||
@@ -25,10 +25,9 @@ spec:
|
||||
- name: redis
|
||||
image: redis/redis-stack-server:latest
|
||||
imagePullPolicy: Always
|
||||
args: ["--maxmemory-policy", "noeviction"]
|
||||
env:
|
||||
- name: PORT
|
||||
value: "6379"
|
||||
value: 6379
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
name: redis
|
||||
|
||||
@@ -33,18 +33,24 @@ spec:
|
||||
image: twentycrm/twenty:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: NODE_PORT
|
||||
- name: PORT
|
||||
value: 3000
|
||||
- name: SERVER_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: FRONT_BASE_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: "PG_DATABASE_URL"
|
||||
value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default"
|
||||
value: "postgres://postgres:twenty@twenty-db.twentycrm.svc.cluster.local/default"
|
||||
- name: "REDIS_URL"
|
||||
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
||||
- name: ENABLE_DB_MIGRATIONS
|
||||
value: "true"
|
||||
- name: SIGN_IN_PREFILLED
|
||||
value: "false"
|
||||
value: "true"
|
||||
- name: STORAGE_TYPE
|
||||
value: "local"
|
||||
- name: "MESSAGE_QUEUE_TYPE"
|
||||
value: "bull-mq"
|
||||
- name: "ACCESS_TOKEN_EXPIRES_IN"
|
||||
value: "7d"
|
||||
- name: "LOGIN_TOKEN_EXPIRES_IN"
|
||||
|
||||
@@ -28,12 +28,18 @@ spec:
|
||||
env:
|
||||
- name: SERVER_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: FRONT_BASE_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: "postgres://postgres:twenty@twenty-db.twentycrm.svc.cluster.local/default"
|
||||
- name: ENABLE_DB_MIGRATIONS
|
||||
value: "false" # it already runs on the server
|
||||
- name: STORAGE_TYPE
|
||||
value: "local"
|
||||
- name: "MESSAGE_QUEUE_TYPE"
|
||||
value: "bull-mq"
|
||||
- name: "CACHE_STORAGE_TYPE"
|
||||
value: "redis"
|
||||
- name: "REDIS_URL"
|
||||
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
||||
- name: APP_SECRET
|
||||
|
||||
@@ -38,15 +38,24 @@ resource "kubernetes_deployment" "twentycrm_server" {
|
||||
tty = true
|
||||
|
||||
env {
|
||||
name = "NODE_PORT"
|
||||
name = "PORT"
|
||||
value = "3000"
|
||||
}
|
||||
# env {
|
||||
# name = "DEBUG_MODE"
|
||||
# value = false
|
||||
# }
|
||||
|
||||
env {
|
||||
name = "SERVER_URL"
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "FRONT_BASE_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"
|
||||
@@ -56,14 +65,23 @@ resource "kubernetes_deployment" "twentycrm_server" {
|
||||
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"
|
||||
name = "ENABLE_DB_MIGRATIONS"
|
||||
value = "true"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "SIGN_IN_PREFILLED"
|
||||
value = "true"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "STORAGE_TYPE"
|
||||
value = "local"
|
||||
}
|
||||
env {
|
||||
name = "MESSAGE_QUEUE_TYPE"
|
||||
value = "bull-mq"
|
||||
}
|
||||
env {
|
||||
name = "ACCESS_TOKEN_EXPIRES_IN"
|
||||
value = "7d"
|
||||
|
||||
@@ -43,25 +43,39 @@ resource "kubernetes_deployment" "twentycrm_worker" {
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "FRONT_BASE_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 = "CACHE_STORAGE_TYPE"
|
||||
value = "redis"
|
||||
}
|
||||
|
||||
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
|
||||
name = "ENABLE_DB_MIGRATIONS"
|
||||
value = "false" #it already runs on the server
|
||||
}
|
||||
|
||||
env {
|
||||
name = "STORAGE_TYPE"
|
||||
value = "local"
|
||||
}
|
||||
env {
|
||||
name = "MESSAGE_QUEUE_TYPE"
|
||||
value = "bull-mq"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "APP_SECRET"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# How to deploy twenty on podman
|
||||
|
||||
DISCLAIMER: The k8s and podman deployments are not maintained by the core team.
|
||||
These files are provided and maintained by the community. Twenty core team
|
||||
maintains support for docker deployment.
|
||||
|
||||
|
||||
## How to use
|
||||
|
||||
1. Edit `.env` file. At the minimum set `POSTGRES_PASSWORD`, `SERVER_URL`, and `APP_SECRET`.
|
||||
2. Start twenty by running `podman-compose up -d`.
|
||||
|
||||
If you need to stop twenty, you can do so by running `podman-compose down`.
|
||||
|
||||
|
||||
### Install systemd service (optional)
|
||||
|
||||
If you want to install a systemd service to run twenty, you can use the provided systemd service.
|
||||
|
||||
Edit `twentycrm.service` and change these two variables:
|
||||
|
||||
|
||||
WorkingDirectory=/opt/apps/twenty
|
||||
EnvironmentFile=/opt/apps/twenty/.env
|
||||
|
||||
`WorkingDirectory` should be changed to the path in which `podman-compose.yml` is located.
|
||||
|
||||
`EnvironmentFile` should be changed to the path in which your `.env`file is located.
|
||||
|
||||
You can run the script `install-systemd-user-service` to install the systemd service under the current user.
|
||||
|
||||
|
||||
./install-systemd-user-service
|
||||
|
||||
Note: this script will enable the service and also start it. So it will assume that twenty is not currently running.
|
||||
If you started it previously, bring it down using:
|
||||
|
||||
podman-compose down
|
||||
|
||||
|
||||
|
||||
## Compatibility
|
||||
|
||||
These files should be compatible with podman 4.3+.
|
||||
|
||||
I have tested this on Debian GNU/Linux 12 (bookworm) and with the podman that is distributed with the official Debian stable mirrors (podman v4.3.1+ds1-8+deb12u1, podman-compose v1.0.3-3).
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
mkdir -p ~/.config/systemd/user
|
||||
[ -f twentycrm.service ] || { echo "Error: twentycrm.service not found"; exit 1; }
|
||||
cp twentycrm.service ~/.config/systemd/user
|
||||
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable twentycrm.service
|
||||
systemctl --user start twentycrm.service
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
if [ ! -f "$(dirname "$0")/.env" ]; then
|
||||
echo "Error: .env file not found"
|
||||
exit 1
|
||||
fi
|
||||
source "$(dirname "$0")/.env"
|
||||
|
||||
## steps to cleanup and start over from scratch - useful when testing
|
||||
#podman pod stop twenty-pod
|
||||
#podman pod rm twenty-pod
|
||||
#podman volume rm twenty-db-data twenty-server-data twenty-redis-data
|
||||
# end of cleanup
|
||||
|
||||
podman volume create twenty-db-data
|
||||
podman volume create twenty-server-data
|
||||
podman volume create twenty-redis-data
|
||||
|
||||
podman pod create --name twenty-pod -p 127.0.0.1:8080:3000
|
||||
|
||||
podman run -d --pod twenty-pod --name twenty-db \
|
||||
-e POSTGRES_DB=twenty \
|
||||
-e POSTGRES_USER=twenty \
|
||||
-e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
|
||||
-v twenty-db-data:/var/lib/postgresql/data:Z \
|
||||
docker.io/library/postgres:16
|
||||
|
||||
podman run -d --pod twenty-pod --name twenty-redis \
|
||||
-v twenty-redis-data:/data:Z \
|
||||
docker.io/library/redis:latest
|
||||
|
||||
podman run -d --pod twenty-pod --name twenty-server \
|
||||
-e NODE_PORT=3000 \
|
||||
-e SERVER_URL="$SERVER_URL" \
|
||||
-e PG_DATABASE_URL="postgresql://twenty:$POSTGRES_PASSWORD@twenty-db:5432/twenty" \
|
||||
-e REDIS_URL="redis://twenty-redis:6379" \
|
||||
-e APP_SECRET="$APP_SECRET" \
|
||||
-e NODE_ENV=production \
|
||||
-e LOG_LEVEL=info \
|
||||
-v twenty-server-data:/app/docker-data:Z \
|
||||
docker.io/twentycrm/twenty:latest
|
||||
|
||||
podman run -d --pod twenty-pod --name twenty-worker \
|
||||
--init \
|
||||
-e SERVER_URL="$SERVER_URL" \
|
||||
-e PG_DATABASE_URL="postgresql://twenty:$POSTGRES_PASSWORD@twenty-db:5432/twenty" \
|
||||
-e REDIS_URL="redis://twenty-redis:6379" \
|
||||
-e APP_SECRET="$APP_SECRET" \
|
||||
-e DISABLE_DB_MIGRATIONS=true \
|
||||
-e NODE_ENV=production \
|
||||
-e LOG_LEVEL=info \
|
||||
-v twenty-server-data:/app/docker-data:Z \
|
||||
docker.io/twentycrm/twenty:latest \
|
||||
yarn worker:prod
|
||||
|
||||
# wait some time, check status
|
||||
sleep 30
|
||||
podman ps --pod -f name=twenty-pod
|
||||
|
||||
|
||||
mkdir -p ~/.config/systemd/user
|
||||
if [ ! -f "twentycrm.service" ]; then
|
||||
echo "Error: twentycrm.service file not found"
|
||||
exit 1
|
||||
fi
|
||||
cp twentycrm.service ~/.config/systemd/user
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable twentycrm.service
|
||||
systemctl --user start twentycrm.service
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
container_name: twenty-db
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_DB: twenty
|
||||
POSTGRES_USER: twenty
|
||||
POSTGRES_PASSWORD: ${PG_DATABASE_PASSWORD}
|
||||
volumes:
|
||||
- twenty-db-data:/var/lib/postgresql/data:Z
|
||||
|
||||
redis:
|
||||
container_name: twenty-redis
|
||||
image: redis:latest
|
||||
command: ["--maxmemory-policy", "noeviction"]
|
||||
volumes:
|
||||
- twenty-redis-data:/data:Z
|
||||
|
||||
server:
|
||||
container_name: twenty-server
|
||||
image: twentycrm/twenty:latest
|
||||
environment:
|
||||
NODE_PORT: "3000"
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
PG_DATABASE_URL: "postgresql://twenty:${PG_DATABASE_PASSWORD}@twenty-db:5432/twenty"
|
||||
REDIS_URL: "redis://twenty-redis:6379"
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
volumes:
|
||||
- twenty-server-data:/app/docker-data:Z
|
||||
ports:
|
||||
- 127.0.0.1:8080:3000
|
||||
|
||||
worker:
|
||||
container_name: twenty-worker
|
||||
image: twentycrm/twenty:latest
|
||||
command: yarn worker:prod
|
||||
environment:
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
PG_DATABASE_URL: "postgresql://twenty:${PG_DATABASE_PASSWORD}@twenty-db:5432/twenty"
|
||||
REDIS_URL: "redis://twenty-redis:6379"
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
DISABLE_DB_MIGRATIONS: "true"
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
volumes:
|
||||
- twenty-server-data:/app/docker-data:Z
|
||||
init: true
|
||||
|
||||
volumes:
|
||||
twenty-db-data:
|
||||
twenty-server-data:
|
||||
twenty-redis-data:
|
||||
@@ -1,15 +0,0 @@
|
||||
[Unit]
|
||||
Description=TwentyCRM Container Stack via Podman-Compose
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/apps/twenty
|
||||
EnvironmentFile=/opt/apps/twenty/.env
|
||||
ExecStart=/usr/bin/podman-compose -f podman-compose.yml up -d
|
||||
ExecStop=/usr/bin/podman-compose -f podman-compose.yml down
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
pull_version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/tags | grep '"name":' | head -n 1 | cut -d '"' -f 4)}
|
||||
|
||||
if [[ -z "$pull_version" ]]; then
|
||||
echo "Error: Unable to fetch the latest version tag. Please check your network connection or the GitHub API response."
|
||||
exit 1
|
||||
fi
|
||||
pull_version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)}
|
||||
pull_branch=${BRANCH:-$pull_version}
|
||||
|
||||
version_num=${pull_version#v}
|
||||
@@ -11,12 +6,7 @@ target_version="0.32.4"
|
||||
|
||||
# We moved the install script to a different location in v0.32.4
|
||||
if [[ -n "$BRANCH" ]] || [[ "$(printf '%s\n' "$target_version" "$version_num" | sort -V | head -n1)" != "$version_num" ]]; then
|
||||
curl -sL "https://raw.githubusercontent.com/twentyhq/twenty/$pull_branch/packages/twenty-docker/scripts/install.sh" -o twenty_install.sh
|
||||
curl -sL "https://raw.githubusercontent.com/twentyhq/twenty/$pull_branch/packages/twenty-docker/scripts/install.sh" | bash -s -- "$VERSION" "$BRANCH"
|
||||
else
|
||||
curl -sL "https://raw.githubusercontent.com/twentyhq/twenty/$pull_branch/install.sh" -o twenty_install.sh
|
||||
curl -sL "https://raw.githubusercontent.com/twentyhq/twenty/$pull_branch/install.sh" | bash -s -- "$VERSION" "$BRANCH"
|
||||
fi
|
||||
|
||||
chmod +x twenty_install.sh
|
||||
VERSION="$VERSION" BRANCH="$BRANCH" ./twenty_install.sh
|
||||
|
||||
rm twenty_install.sh
|
||||
|
||||
@@ -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:-$version}
|
||||
|
||||
echo "🚀 Using docker version $version and Github branch $branch"
|
||||
echo "🚀 Using version $version and branch $branch"
|
||||
|
||||
dir_name="twenty"
|
||||
function ask_directory {
|
||||
@@ -90,11 +90,10 @@ 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 "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "" >>.env
|
||||
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
|
||||
|
||||
echo -e "\t• .env configuration completed"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22-alpine as twenty-website-build
|
||||
FROM node:18.17.1-alpine as twenty-website-build
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
@@ -8,22 +8,14 @@ COPY ./yarn.lock .
|
||||
COPY ./.yarnrc.yml .
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./tools/eslint-rules /app/tools/eslint-rules
|
||||
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-website/package.json /app/packages/twenty-website/package.json
|
||||
|
||||
RUN yarn
|
||||
|
||||
ENV KEYSTATIC_GITHUB_CLIENT_ID="<fake build value>"
|
||||
ENV KEYSTATIC_GITHUB_CLIENT_SECRET="<fake build value>"
|
||||
ENV KEYSTATIC_SECRET="<fake build value>"
|
||||
ENV NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG="<fake build value>"
|
||||
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-website /app/packages/twenty-website
|
||||
RUN npx nx build twenty-website
|
||||
|
||||
FROM node:22-alpine as twenty-website
|
||||
FROM node:18.17.1-alpine as twenty-website
|
||||
|
||||
WORKDIR /app/packages/twenty-website
|
||||
|
||||
@@ -34,9 +26,4 @@ WORKDIR /app/packages/twenty-website
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website."
|
||||
|
||||
RUN chown -R 1000 /app
|
||||
|
||||
# Use non root user with uid 1000
|
||||
USER 1000
|
||||
|
||||
CMD ["/bin/sh", "-c", "npx nx start"]
|
||||
@@ -1,5 +1,5 @@
|
||||
# Base image for common dependencies
|
||||
FROM node:22-alpine as common-deps
|
||||
FROM node:18.17.1-alpine as common-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -7,12 +7,10 @@ WORKDIR /app
|
||||
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
|
||||
COPY ./.prettierrc /app/
|
||||
COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
|
||||
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
|
||||
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
|
||||
# Install all dependencies
|
||||
@@ -24,7 +22,6 @@ FROM common-deps as twenty-server-build
|
||||
|
||||
# Copy sourcecode after installing dependences to accelerate subsequents builds
|
||||
COPY ./packages/twenty-emails /app/packages/twenty-emails
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
RUN npx nx run twenty-server:build
|
||||
@@ -34,7 +31,7 @@ RUN mv /app/packages/twenty-server/dist/package.json /app/packages/twenty-server
|
||||
RUN rm -rf /app/packages/twenty-server/dist
|
||||
RUN mv /app/packages/twenty-server/build /app/packages/twenty-server/dist
|
||||
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-server
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-server
|
||||
|
||||
|
||||
# Build the front
|
||||
@@ -44,12 +41,11 @@ ARG REACT_APP_SERVER_BASE_URL
|
||||
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
RUN npx nx build twenty-front
|
||||
|
||||
|
||||
# Final stage: Run the application
|
||||
FROM node:22-alpine as twenty
|
||||
FROM node:18.17.1-alpine as twenty
|
||||
|
||||
# Used to run healthcheck in docker
|
||||
RUN apk add --no-cache curl jq
|
||||
@@ -64,9 +60,8 @@ WORKDIR /app/packages/twenty-server
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
ENV REACT_APP_SERVER_BASE_URL $REACT_APP_SERVER_BASE_URL
|
||||
|
||||
ARG APP_VERSION
|
||||
ENV APP_VERSION $APP_VERSION
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE $SENTRY_RELEASE
|
||||
|
||||
# Copy built applications from previous stages
|
||||
COPY --chown=1000 --from=twenty-server-build /app /app
|
||||
@@ -77,8 +72,8 @@ COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /ap
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment."
|
||||
|
||||
RUN mkdir -p /app/.local-storage /app/packages/twenty-server/.local-storage && \
|
||||
chown -R 1000:1000 /app
|
||||
RUN mkdir /app/.local-storage
|
||||
RUN chown -R 1000 /app
|
||||
|
||||
# Use non root user with uid 1000
|
||||
USER 1000
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
setup_and_migrate_db() {
|
||||
if [ "${DISABLE_DB_MIGRATIONS}" = "true" ]; then
|
||||
echo "Database setup and migrations are disabled, skipping..."
|
||||
return
|
||||
fi
|
||||
|
||||
# Check if the initialization has already been done and that we enabled automatic migration
|
||||
if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
||||
echo "Running database setup and migrations..."
|
||||
|
||||
# Creating the database if it doesn't exist
|
||||
PGUSER=$(echo $PG_DATABASE_URL | awk -F '//' '{print $2}' | awk -F ':' '{print $1}')
|
||||
PGPASS=$(echo $PG_DATABASE_URL | awk -F ':' '{print $3}' | awk -F '@' '{print $1}')
|
||||
PGHOST=$(echo $PG_DATABASE_URL | awk -F '@' '{print $2}' | awk -F ':' '{print $1}')
|
||||
PGPORT=$(echo $PG_DATABASE_URL | awk -F ':' '{print $4}' | awk -F '/' '{print $1}')
|
||||
PGDATABASE=$(echo $PG_DATABASE_URL | awk -F ':' '{print $4}' | awk -F '/' '{print $2}')
|
||||
PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'default'" | grep -q 1 || \
|
||||
PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -c "CREATE DATABASE \"default\""
|
||||
|
||||
# Creating the database if it doesn't exist
|
||||
db_count=$(PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -tAc "SELECT COUNT(*) FROM pg_database WHERE datname = '${PGDATABASE}'")
|
||||
if [ "$db_count" = "0" ]; then
|
||||
echo "Database ${PGDATABASE} does not exist, creating..."
|
||||
PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -c "CREATE DATABASE \"${PGDATABASE}\""
|
||||
# Run setup and migration scripts
|
||||
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
|
||||
yarn database:migrate:prod
|
||||
|
||||
# Run setup and migration scripts
|
||||
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
|
||||
yarn database:migrate:prod
|
||||
fi
|
||||
|
||||
yarn command:prod upgrade
|
||||
echo "Successfully migrated DB!"
|
||||
}
|
||||
setup_and_migrate_db
|
||||
# Mark initialization as done
|
||||
echo "Successfuly migrated DB!"
|
||||
touch /app/docker-data/db_status
|
||||
fi
|
||||
|
||||
# Continue with the original Docker command
|
||||
exec "$@"
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
# Note that provide always without trailing forward slash to have expected behaviour
|
||||
FRONTEND_BASE_URL=http://localhost:3001
|
||||
BACKEND_BASE_URL=http://localhost:3000
|
||||
CI_DEFAULT_BASE_URL=https://demo.twenty.com
|
||||
DEFAULT_LOGIN=tim@apple.dev
|
||||
DEFAULT_PASSWORD=tim@apple.dev
|
||||
WEBSITE_URL=https://twenty.com
|
||||
NEW_WORKSPACE_LOGIN=test@apple.dev
|
||||
DEMO_DEFAULT_LOGIN=noah@demo.dev
|
||||
DEFAULT_PASSWORD=Applecar2025
|
||||
WEBSITE_URL=https://twenty.com
|
||||
|
||||
# === DO NOT USE, WORK IN PROGRESS ===
|
||||
# This URL must have trailing forward slash as all REST API endpoints have object after it
|
||||
# Documentation for REST API: https://twenty.com/developers/rest-api/core#/
|
||||
# REST_API_BASE_URL=http://localhost:3000/rest/
|
||||
|
||||
# Documentation for GraphQL API: https://twenty.com/developers/graphql/core
|
||||
# GRAPHQL_BASE_URL=http://localhost:3000/graphql
|
||||
|
||||
# Without this key, all API tests will fail, to generate this key
|
||||
# Log in to Twenty workspace, go to Settings > Developers, generate new key and paste it here
|
||||
# In order to use it, header Authorization: Bearer token must be used
|
||||
# This key works for both REST and GraphQL API
|
||||
# API_DEV_KEY=fill_with_proper_key
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user