Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da67be475f |
@@ -0,0 +1,82 @@
|
||||
# Cal.com Development Guide for AI Agents
|
||||
|
||||
This directory contains comprehensive documentation for AI agents working on the Cal.com codebase.
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
- **[Commands](commands.md)** - Build, test, and development commands
|
||||
- **[Knowledge Base](knowledge-base.md)** - Knowledge base & best practices
|
||||
- **[Architecture Overview](#architecture-overview)** - System structure and patterns
|
||||
|
||||
## Getting Started
|
||||
|
||||
Cal.com is a monorepo using Yarn workspaces and Turbo for build orchestration. The main application is in `apps/web/` with shared packages in `packages/`.
|
||||
|
||||
### Key Directories
|
||||
|
||||
- `apps/web/` - Main Next.js application
|
||||
- `packages/prisma/` - Database schema and migrations
|
||||
- `packages/trpc/` - API layer using tRPC
|
||||
- `packages/ui/` - Shared UI components
|
||||
- `packages/features/` - Feature-specific code
|
||||
- `packages/app-store/` - Third-party app integrations
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Database Layer
|
||||
|
||||
- **Prisma ORM** with PostgreSQL
|
||||
- Schema in `packages/prisma/schema.prisma`
|
||||
- Always use `select` instead of `include` for better performance
|
||||
- Never expose `credential.key` field in API responses
|
||||
|
||||
### API Layer
|
||||
|
||||
- **tRPC** for type-safe APIs
|
||||
- Routers in `packages/trpc/server/routers/`
|
||||
- Authentication handled via NextAuth.js
|
||||
|
||||
### Frontend
|
||||
|
||||
- **Next.js 13+** with App Router in some areas
|
||||
- **React 18** with TypeScript
|
||||
- **Tailwind CSS** for styling
|
||||
- Internationalization with `next-i18next`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use early returns to reduce nesting
|
||||
- Throw descriptive errors with proper error codes
|
||||
- Prefer composition over prop drilling
|
||||
|
||||
### Performance
|
||||
|
||||
- Avoid O(n²) logic in backend code
|
||||
- Minimize Day.js usage in performance-critical paths
|
||||
- Use `select` queries to only fetch needed data
|
||||
- Consider using `.utc()` for Day.js operations
|
||||
|
||||
### Security
|
||||
|
||||
- Never commit secrets or API keys
|
||||
- Always validate input data
|
||||
- Use proper authentication checks
|
||||
- Never expose sensitive credential fields
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Unit tests** with Vitest
|
||||
- **Integration tests** for complex workflows
|
||||
- **E2E tests** with Playwright
|
||||
- Test files use `.test.ts` or `.spec.ts` extensions
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
For large PRs (>500 lines or >10 files):
|
||||
|
||||
- Split by feature boundaries
|
||||
- Separate database migrations, backend logic, frontend components
|
||||
- Create dependency chains that can be merged sequentially
|
||||
- Pattern: Database → Backend → Frontend → Tests
|
||||
@@ -0,0 +1,56 @@
|
||||
# Coding Standards & Best Practices
|
||||
|
||||
|
||||
|
||||
## Import Guidelines
|
||||
|
||||
### Type Imports
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Use type imports for TypeScript types
|
||||
import type { User } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
// ❌ Bad - Regular import for types
|
||||
import { User } from "@prisma/client";
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Early Returns
|
||||
|
||||
- Prefer early returns to reduce nesting: `if (!booking) return null;`
|
||||
|
||||
### Composition Over Prop Drilling
|
||||
|
||||
- Use React children and context instead of passing props through multiple components
|
||||
|
||||
### Security Rules
|
||||
|
||||
```typescript
|
||||
// ❌ NEVER expose credential keys
|
||||
const user = await prisma.user.findFirst({
|
||||
select: {
|
||||
credentials: {
|
||||
select: {
|
||||
key: true, // ❌ NEVER do this
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Good - Never select credential.key field
|
||||
const user = await prisma.user.findFirst({
|
||||
select: {
|
||||
credentials: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
// key field is excluded for security
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
## Lint & Type Check
|
||||
|
||||
- `yarn lint` - Run Biome across the codebase
|
||||
- `yarn lint:fix` - Run Biome and apply safe fixes
|
||||
- `yarn lint:report` - Generate Biome lint report
|
||||
- `yarn lint` - Run ESLint on codebase
|
||||
- `yarn lint:fix` - Run ESLint and fix issues
|
||||
- `yarn lint:report` - Generate lint report
|
||||
- `yarn type-check` - Run TypeScript type checking
|
||||
- `yarn format` - Format code with Biome
|
||||
- `yarn format` - Format code with Prettier
|
||||
|
||||
## Testing Commands
|
||||
|
||||
@@ -28,17 +28,11 @@
|
||||
|
||||
- `yarn test` - Run unit tests (vitest)
|
||||
- `yarn test <filename>` - Run tests for specific file
|
||||
- `yarn test <filename> -t "<testName>"` - Run specific test by name for specific file
|
||||
- `yarn test <filename> -- --integrationTestsOnly` - Run integration tests for specific file
|
||||
- `yarn test <filename> -t "<testName>" -- --integrationTestsOnly` - Run specific test by name
|
||||
- `yarn tdd` - Run tests in watch mode
|
||||
- `yarn test:ui` - Run tests with UI interface
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- `VITEST_MODE=integration yarn test` - Run integration tests (vitest)
|
||||
- `VITEST_MODE=integration yarn test <filename>` - Run integration tests for specific file
|
||||
- `VITEST_MODE=integration yarn test <filename> -t "<testName>"` - Run specific integration test by name for specific file
|
||||
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
- `yarn e2e` - Run end-to-end tests (Playwright)
|
||||
@@ -73,7 +67,7 @@
|
||||
yarn vitest run packages/lib/some-file.test.ts
|
||||
|
||||
# Integration test specific file
|
||||
VITEST_MODE=integration yarn test routing-form-response-denormalized.integration-test.ts
|
||||
yarn test routing-form-response-denormalized.integration-test.ts -- --integrationTestsOnly
|
||||
|
||||
# E2E test specific file
|
||||
yarn e2e tests/booking-flow.e2e.ts
|
||||
@@ -0,0 +1,596 @@
|
||||
# Knowledge Base - Product & Domain-Specific Information
|
||||
|
||||
## Repo note for calcom/cal.com
|
||||
|
||||
The whole thing is a monorepo. You need to be working in the apps/web folder.
|
||||
|
||||
Linting and Formatting
|
||||
- Run lint with report generation: `yarn lint:report`
|
||||
- Run type checking: `yarn type-check:ci --force`
|
||||
- Run auto-fix: `yarn lint -- --fix`
|
||||
|
||||
Development
|
||||
- Install dependencies: `yarn`
|
||||
- Set up environment:
|
||||
- Copy `.env.example` to `.env`
|
||||
- Generate keys with `openssl rand -base64 32` for both `NEXTAUTH_SECRET` and `CALENDSO_ENCRYPTION_KEY`
|
||||
- Configure Postgres database URL in `.env`
|
||||
- Set DATABASE_DIRECT_URL to the same value as DATABASE_URL
|
||||
|
||||
Database Setup
|
||||
- Development: `yarn workspace @calcom/prisma db-migrate`
|
||||
- Production: `yarn workspace @calcom/prisma db-deploy`
|
||||
|
||||
When setting up local development database, it'll create a bunch of users for you. The passwords are the same as the username. e.g. 'free:free' and 'pro:pro'
|
||||
|
||||
PR Requirements
|
||||
- PR title must follow Conventional Commits specification
|
||||
- For most PRs, you only need to run linting and type checking
|
||||
- E2E tests will only run if PR has "ready-for-e2e" label
|
||||
|
||||
Logging
|
||||
- Control logging verbosity by setting `NEXT_PUBLIC_LOGGER_LEVEL` in .env:
|
||||
- 0: silly
|
||||
- 1: trace
|
||||
- 2: debug
|
||||
- 3: info
|
||||
- 4: warn
|
||||
- 5: error
|
||||
- 6: fatal
|
||||
|
||||
## When addressing issues in the Cal.com repository
|
||||
|
||||
When working on the Cal.com repository, prioritize fixing type issues before addressing failing tests. Running `yarn type-check:ci --force` to identify and fix TypeScript errors should be done first, as these errors are often the root cause of test failures. Only after resolving type issues should you move on to fixing failing tests with `TZ=UTC yarn test`.
|
||||
|
||||
## When creating pull requests
|
||||
|
||||
Create pull requests in draft mode by default, so that actual human can mark it as ready for review only when it is.
|
||||
|
||||
## When developing Playwright tests in the Cal.com repository
|
||||
|
||||
Always ensure Playwright tests pass locally before pushing code. The user requires fast local e2e feedback loops instead of relying on CI, which is too slow for development iteration. Never push test code until those tests are passing locally first.
|
||||
|
||||
## When fixing failing tests in the Cal.com repository
|
||||
|
||||
When fixing failing tests in the Cal.com repository, take an incremental approach by addressing one file at a time rather than attempting to fix all issues simultaneously. This methodical approach makes it easier to identify and resolve specific issues without getting overwhelmed by the complexity of multiple failing tests across different files. Focus on getting each file's tests passing completely before moving on to the next file.
|
||||
|
||||
To identify and fix issues in the Cal.com codebase:
|
||||
|
||||
1. Run `yarn type-check:ci --force` to identify TypeScript type errors and get fresh results always, bypassing any caching issues
|
||||
2. Run `yarn test` to identify failing unit tests
|
||||
3. Address both type errors and failing tests before considering the task complete
|
||||
4. Type errors often need to be fixed first as they may be causing the test failures
|
||||
|
||||
## When implementing mock services in the Cal.com test files
|
||||
|
||||
When mocking calendar services in Cal.com test files, implement the Calendar interface rather than adding individual properties from each specific calendar service type (like FeishuCalendarService). Since all calendar services implement the Calendar interface and are stored in a map, the mock service should also implement this interface to ensure type compatibility. This approach is more maintainable than trying to add all properties from each specific calendar service implementation. For complex mocks that cause type compatibility issues with deep mocks, consider using simpler fake implementations that directly implement the required interfaces instead of trying to match all properties of the original service classes. When needed, you can modify other mock files to support your implementation rather than trying to force compatibility with existing mocks.
|
||||
|
||||
## When implementing mocks for Cal.com app-store integrations
|
||||
|
||||
When mocking app-store resources in Cal.com tests, prefer implementing simpler mock designs that directly implement the required interfaces rather than trying to match complex deep mock structures created with mockDeep. This approach is more maintainable and helps resolve type compatibility issues. The user encourages creative solutions and refactoring to better designs when the standard mocking approach causes persistent type errors.
|
||||
|
||||
## When implementing workflow triggers in the Cal.com repository
|
||||
|
||||
To trigger workflows in Cal.com, use the `scheduleWorkflowReminders` function. This is the standard approach used throughout the codebase for triggering any workflow. Before implementing new workflow triggers, examine existing implementations in the codebase to understand the pattern. The function filters workflows by trigger type and processes each workflow step. Key locations where this is used include booking handlers, confirmation processes, and other booking-related events.
|
||||
|
||||
## When making changes to Prisma schema in Cal.com
|
||||
|
||||
After making changes to the Prisma schema in Cal.com and creating migrations, you need to run `yarn prisma generate` to update the TypeScript types. This is especially important when switching Node.js versions, after adding new fields to models, or after pulling changes that include Prisma schema updates, as it ensures the TypeScript compiler recognizes the updated schema structure. If you encounter errors related to missing enum values (like CreationSource.WEBAPP), running `yarn prisma generate` will typically resolve these issues by regenerating the TypeScript types from the schema.
|
||||
|
||||
If you encounter enum generator errors during the Prisma generate step (like "Cannot find module './enum-generator.ts'"), run `yarn install` first before trying to generate. This ensures all dependencies are properly installed before the generation process.
|
||||
|
||||
Whenever you change the schema.prisma file, remember to always consolidate migrations by squashing them as declared in the Prisma docs: https://www.prisma.io/docs/orm/prisma-migrate/workflows/squashing-migrations. This helps maintain a clean migration history and prevents accumulation of multiple migration files.
|
||||
|
||||
## When reviewing PRs in the Cal.com repository
|
||||
|
||||
When asked to review a PR, focus on providing a clear summary of what the PR is doing and its core functionality. Avoid getting sidetracked by CI failures, testing issues, or technical implementation details unless specifically requested. The user prefers concise, focused reviews that prioritize understanding the main purpose and changes of the PR.
|
||||
|
||||
## When running Playwright tests in the Cal.com repository
|
||||
|
||||
Use the command format `PLAYWRIGHT_HEADLESS=1 yarn e2e [test-file.e2e.ts]` to run Playwright tests instead of the standard `yarn playwright test` command. This format includes the proper timezone setting, virtual display server, and uses the repository's e2e runner.
|
||||
|
||||
## When running tests in the Cal.com repository
|
||||
|
||||
When running tests in the Cal.com repository, use the TZ=UTC environment variable (e.g., `TZ=UTC yarn test`) to ensure consistent timezone handling. This prevents timezone-related test failures that might occur when tests are run in different environments or by different developers with varying local timezone settings.
|
||||
|
||||
## When working on calendar cache features in the Cal.com codebase
|
||||
|
||||
The calendar cache system follows specific patterns in `packages/features/calendar-cache-sql`. When implementing provider-specific calendar cache services (like for Outlook/Office365), the provider-specific code should be placed in the corresponding provider directory (e.g., `packages/app-store/office365calendar`).
|
||||
|
||||
## When working on PRs in the Cal.com repository
|
||||
|
||||
When making changes to the Cal.com codebase, always run type checks locally using `yarn type-check:ci` before concluding that CI failures are unrelated to your changes. Even if errors appear in files you haven't directly modified, your changes might still be causing type issues through dependencies or type inference. Compare type check results between the main branch and your feature branch to confirm whether you've introduced new type errors.
|
||||
|
||||
## When working on type issues in the Cal.com repository
|
||||
|
||||
Type casting with "as any" is strictly forbidden in the Cal.com codebase. When encountering Prisma type incompatibilities or other TypeScript type issues, proper type-safe solutions must be used instead, such as Prisma extensions system, type parameter constraints, repository pattern isolation, explicit type definitions, and extension composition patterns that are already established in the codebase.
|
||||
|
||||
## When working on UI layout changes in the Cal.com web application
|
||||
|
||||
The event types page UI components are located in `apps/web/modules/event-types/views/event-types-listing-view.tsx`. This file contains the layout implementation for the search bar and team tabs components on the event types listing page.
|
||||
|
||||
Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency:
|
||||
- Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
|
||||
- Bookings page layout: `apps/web/modules/bookings/views/bookings-view.tsx`
|
||||
- Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views
|
||||
|
||||
## When working on workflow triggers or similar enum-based features in the Cal.com codebase
|
||||
|
||||
To add new workflow triggers:
|
||||
|
||||
1. Check packages/prisma/schema.prisma for existing webhooks and workflow trigger enums as reference
|
||||
2. Add the same enums to workflows (only when asked by user specifically, or else focus on users requirement)
|
||||
3. Add enums to packages/features/ee/workflows/lib/constants.ts for UI display
|
||||
4. Add translations to en/locale.json using the format {enum}_trigger (all lowercase). Webhook triggers serve as the reference implementation pattern for workflow triggers.
|
||||
|
||||
## When working with app-store integrations in Cal.com
|
||||
|
||||
The Cal.com repository uses generated files (*.generated.ts) for app-store integrations. These files are created by the app-store-cli tool. When making structural changes to how integrations are imported or used, you need to update CLI code that generates these files. Typically manual changes to the *.generated.ts files are not made. That was only for PR 19771 for a proof-of-concept. Recent changes have moved from dynamic imports to static map-based imports for better performance. When working with browser components in the app-store, static imports should be used rather than dynamic imports (using Next.js' dynamic function) to maintain consistency with the performance improvements.
|
||||
|
||||
When modifying the app-store-cli build.ts file, you must ensure it correctly handles all types of generated files:
|
||||
1. Regular service files (calendar.services.generated.ts, crm.services.generated.ts, etc.) need default imports
|
||||
2. Browser component files (apps.browser-addon.generated.tsx, etc.) may require dynamic imports with Next.js
|
||||
3. After making changes to build.ts, always verify all generated files have the correct imports by checking each file type
|
||||
|
||||
The lazyImport parameter in getExportedObject() determines whether to use dynamic imports (for browser components) or static imports (for server-side services).
|
||||
|
||||
## When working with branches in the Cal.com repository
|
||||
|
||||
When asked to move changes to a different branch in the Cal.com repository, use git commands to commit existing changes to the specified branch rather than redoing the work. This is more efficient and prevents duplication of effort. The user prefers direct branch operations over reimplementing the same changes multiple times.
|
||||
|
||||
## When working with calendar events in the Cal.com codebase
|
||||
|
||||
Cal.com events in Google Calendar can be identified by checking if the iCalUID ends with "@Cal.com" (e.g., "2GBXSdEixretciJfKVmYN8@Cal.com"). This identifier is used to distinguish Cal.com bookings from other calendar events for data storage and privacy purposes.
|
||||
|
||||
## When working with CI/CD in the Cal.com repository
|
||||
|
||||
When reviewing CI check failures in Cal.com:
|
||||
1. E2E tests can be flaky and may fail intermittently
|
||||
2. Focus only on CI failures that are directly related to your code changes
|
||||
3. Infrastructure-related failures (like dependency installation issues) can be disregarded if all code-specific checks (type checking, linting, unit tests) are passing
|
||||
|
||||
## When working with database models in Cal.com
|
||||
|
||||
Database models in Cal.com are defined in `packages/prisma/schema.prisma`. When adding new fields to models:
|
||||
|
||||
1. For timestamp fields like `createdAt` and `updatedAt`:
|
||||
- Do not set default values if you want existing records to have null values
|
||||
- Only new records should get timestamps automatically
|
||||
- For `updatedAt` fields, ensure they're updated when records are modified
|
||||
|
||||
2. Create a migration using `npx prisma migrate dev --name migration_name` to update the database schema
|
||||
|
||||
3. When implementing cache-related features that require timestamp tracking, always update the database schema first before modifying application code that references those fields. The schema changes must be completed and migrated before the application can successfully query or use the new fields.
|
||||
|
||||
## When working with generated files in the Cal.com repository
|
||||
|
||||
When fixing imports in Cal.com's generated files (like packages/app-store/apps.browser-*.generated.tsx), always check the actual exports in the source files first. For EventTypeAppCardInterface components, they likely use named exports rather than default exports, requiring imports like `import * as ComponentName from "./path"` instead of `import ComponentName from "./path"`. This verification step is crucial when working with browser-addon, browser-appsettings, browser-eventtypesettings, and browser-install generated files. For these files, if you're seeing import errors, check whether the components are exported as default exports in their source files and adjust your import statements accordingly.
|
||||
|
||||
## When working with git and CI systems
|
||||
|
||||
Always push committed changes to the remote repository before waiting for or checking CI status. Waiting for CI checks on unpushed local commits is backwards - the CI runs on the remote repository state, not local commits. The proper sequence is: commit locally, run local checks, push to remote, then monitor CI status.
|
||||
|
||||
## When working with imports/exports in the Cal.com codebase
|
||||
|
||||
When working with imports in the Cal.com codebase, particularly in app-store integrations, pay attention to whether modules use named exports or default exports. Many services like VideoApiAdapter, CalendarService, and PaymentService are exported as named exports, but the actual export name may differ from the generic service type (e.g., `export class AppleCalendarService` instead of `export class CalendarService`). When importing these services, verify the actual export name in the source file and use the appropriate named import syntax (e.g., `import { AppleCalendarService } from "./applecalendar/lib/CalendarService"` or with renaming `import { AppleCalendarService as ApplecalendarCalendarService } from "./applecalendar/lib/CalendarService"`). Always check the actual export pattern and name in the source file to determine the correct import syntax.
|
||||
|
||||
## When working with tRPC components in the Cal.com repository
|
||||
|
||||
When making changes that affect tRPC components or after pulling updates that modify tRPC-related files, you need to follow a specific build order:
|
||||
1. First run `yarn prisma generate` to ensure all database types are up-to-date
|
||||
2. Then run `cd packages/trpc && yarn build` to rebuild the tRPC package
|
||||
This sequence ensures that type definitions are properly generated before building the tRPC components that depend on them, preventing type errors during compilation.
|
||||
|
||||
## When working with workflows or webhooks in the Cal.com repository
|
||||
|
||||
Workflows and webhooks are two completely separate features in Cal.com with different implementations and file structures. The workflow constants file is located at `packages/features/ee/workflows/lib/constants.ts`, not in the webhooks directory. When working on workflow triggers, do not reference or use webhook trigger implementations - they are distinct systems that should not be confused or mixed.
|
||||
|
||||
## When adding new feature flags to the Cal.com repository
|
||||
|
||||
To seed new feature flags in Cal.com, create a Prisma migration using the command `yarn prisma migrate dev --create-only --name seed_[feature_name]_feature`. The migration file should be placed in `packages/prisma/migrations/` with a timestamp prefix format like `20250724210733_seed_calendar_cache_sql_features/migration.sql`. Follow the pattern from existing feature seeding migrations like `packages/prisma/migrations/20241216000000_add_calendar_cache_serve/migration.sql` for the SQL structure. The migration should INSERT the new features into the `Feature` table with appropriate type (like `OPERATIONAL`) and default `enabled` status for manual team enablement.
|
||||
|
||||
## When adding new UI elements or text strings to Cal.com
|
||||
|
||||
All UI strings in Cal.com must be properly translated using the i18n system. This includes:
|
||||
- Labels for new UI elements (like dropdown labels, settings headers)
|
||||
- Option values that are displayed to users
|
||||
- Any text that appears in the interface
|
||||
|
||||
Even if some related strings are already translated (like "Bookings" and "Insights"), new strings must be explicitly added to the translation system.
|
||||
|
||||
## When creating a pull request
|
||||
|
||||
### Title
|
||||
|
||||
- Use conventional commits: `feat:`, `fix:`, `refactor:`
|
||||
- Be specific: `fix: handle timezone edge case in booking creation`
|
||||
- Not generic: `fix: booking bug`
|
||||
|
||||
### Size Limits
|
||||
|
||||
- **Large PRs** (>500 lines or >10 files) are not recommended.
|
||||
- Guide the user how to split large PRs into smaller ones.
|
||||
|
||||
## When handling errors
|
||||
|
||||
### Descriptive Errors
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Descriptive error with context
|
||||
throw new Error(`Unable to create booking: User ${userId} has no available time slots for ${date}`);
|
||||
|
||||
// ❌ Bad - Generic error
|
||||
throw new Error("Booking failed");
|
||||
```
|
||||
|
||||
### Error Types
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Use proper error classes
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Invalid booking time slot",
|
||||
});
|
||||
```
|
||||
|
||||
## Basic Performance Guidelines
|
||||
|
||||
- Aim for O(n) or O(n log n) complexity, avoid O(n²)
|
||||
- Use database-level filtering instead of JavaScript filtering
|
||||
- Consider pagination for large datasets
|
||||
- Use database transactions for related operations
|
||||
|
||||
## When querying database
|
||||
|
||||
Prefer Select over Include:
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Use select for performance and security
|
||||
const booking = await prisma.booking.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ❌ Bad - Include fetches all fields
|
||||
const booking = await prisma.booking.findFirst({
|
||||
include: {
|
||||
user: true, // This gets ALL user fields
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Avoid barrel imports
|
||||
|
||||
```typescript
|
||||
// ❌ Bad - Avoid importing from index.ts barrel files
|
||||
import { BookingService, UserService } from "./services";
|
||||
|
||||
// ✅ Good - Import directly from source files
|
||||
import { BookingService } from "./services/BookingService";
|
||||
import { UserService } from "./services/UserService";
|
||||
|
||||
|
||||
// ❌ Bad
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
// ✅ Good - Import directly from source files
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
```
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
### Repository Files
|
||||
|
||||
- **Must** include `Repository` suffix, PascalCase matching class: `PrismaBookingRepository.ts`
|
||||
|
||||
### Service Files
|
||||
|
||||
- **Must** include `Service` suffix, PascalCase matching class, avoid generic names: `MembershipService.ts`
|
||||
|
||||
### General Files
|
||||
|
||||
- **Components**: PascalCase (e.g., `BookingForm.tsx`)
|
||||
- **Utilities**: kebab-case (e.g., `date-utils.ts`)
|
||||
- **Types**: PascalCase with `.types.ts` suffix (e.g., `Booking.types.ts`)
|
||||
- **Tests**: Same as source file + `.test.ts` or `.spec.ts`
|
||||
- **Avoid**: Dot-suffixes like `.service.ts`, `.repository.ts` (except for tests, types, specs)
|
||||
|
||||
## Repository Method Conventions
|
||||
|
||||
Repositories should follow consistent naming and design patterns to promote reusability and maintainability.
|
||||
|
||||
### Method Naming Rules
|
||||
|
||||
**1. Don't include the repository's entity name in method names**
|
||||
|
||||
Method names should be concise and avoid redundancy since the repository class name already indicates the entity type.
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Concise method names
|
||||
class BookingRepository {
|
||||
findById(id: string) { ... }
|
||||
findByUserId(userId: string) { ... }
|
||||
create(data: BookingCreateInput) { ... }
|
||||
delete(id: string) { ... }
|
||||
}
|
||||
|
||||
// ❌ Bad - Redundant entity name in methods
|
||||
class BookingRepository {
|
||||
findBookingById(id: string) { ... }
|
||||
findBookingByUserId(userId: string) { ... }
|
||||
createBooking(data: BookingCreateInput) { ... }
|
||||
deleteBooking(id: string) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**2. Use `include` or similar keywords for methods that fetch relational data**
|
||||
|
||||
When a method retrieves additional related entities, make this explicit in the method name using keywords like `include`, `with`, or `andRelations`.
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Clear indication of included relations
|
||||
class EventTypeRepository {
|
||||
findById(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
findByIdIncludeHosts(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
hosts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findByIdIncludeHostsAndSchedule(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
hosts: true,
|
||||
schedule: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad - Unclear what data is included
|
||||
class EventTypeRepository {
|
||||
findById(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
hosts: true,
|
||||
schedule: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findByIdForReporting(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
hosts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Keep methods generic and reusable - avoid use-case-specific names**
|
||||
|
||||
Repository methods should be general-purpose and describe what data they return, not how or where it's used. This promotes code reuse across different features.
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Generic, reusable methods
|
||||
class BookingRepository {
|
||||
findByUserIdIncludeAttendees(userId: string) {
|
||||
return prisma.booking.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
attendees: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findByDateRangeIncludeEventType(startDate: Date, endDate: Date) {
|
||||
return prisma.booking.findMany({
|
||||
where: {
|
||||
startTime: { gte: startDate },
|
||||
endTime: { lte: endDate },
|
||||
},
|
||||
include: {
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad - Use-case-specific method names
|
||||
class BookingRepository {
|
||||
findBookingsForReporting(userId: string) {
|
||||
return prisma.booking.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
attendees: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findBookingsForDashboard(startDate: Date, endDate: Date) {
|
||||
return prisma.booking.findMany({
|
||||
where: {
|
||||
startTime: { gte: startDate },
|
||||
endTime: { lte: endDate },
|
||||
},
|
||||
include: {
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. No business logic in repositories**
|
||||
|
||||
Repositories should only handle data access. Business logic, validations, and complex transformations belong in the Service layer.
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Repository only handles data access
|
||||
class BookingRepository {
|
||||
findByIdIncludeAttendees(id: string) {
|
||||
return prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
attendees: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus(id: string, status: BookingStatus) {
|
||||
return prisma.booking.update({
|
||||
where: { id },
|
||||
data: { status },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class BookingService {
|
||||
async confirmBooking(bookingId: string) {
|
||||
const booking = await this.bookingRepository.findByIdIncludeAttendees(bookingId);
|
||||
|
||||
if (!booking) {
|
||||
throw new Error("Booking not found");
|
||||
}
|
||||
|
||||
if (booking.status !== "PENDING") {
|
||||
throw new Error("Only pending bookings can be confirmed");
|
||||
}
|
||||
|
||||
// Business logic: send confirmation emails
|
||||
await this.emailService.sendConfirmationToAttendees(booking.attendees);
|
||||
|
||||
// Update status through repository
|
||||
return this.bookingRepository.updateStatus(bookingId, "CONFIRMED");
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad - Business logic in repository
|
||||
class BookingRepository {
|
||||
async confirmBooking(bookingId: string) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
attendees: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new Error("Booking not found");
|
||||
}
|
||||
|
||||
if (booking.status !== "PENDING") {
|
||||
throw new Error("Only pending bookings can be confirmed");
|
||||
}
|
||||
|
||||
// ❌ Business logic shouldn't be here
|
||||
await sendEmailToAttendees(booking.attendees);
|
||||
|
||||
return prisma.booking.update({
|
||||
where: { id: bookingId },
|
||||
data: { status: "CONFIRMED" },
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Summary
|
||||
|
||||
- Method names should be concise: `findById` not `findBookingById`
|
||||
- Use `include`/`with` keywords when fetching relations: `findByIdIncludeHosts`
|
||||
- Keep methods generic and reusable: `findByUserIdIncludeAttendees` not `findBookingsForReporting`
|
||||
- No business logic in repositories - that belongs in Services
|
||||
|
||||
## When using Day.js
|
||||
|
||||
```typescript
|
||||
// ⚠️ Slow in performance-critical code (loops)
|
||||
dates.map((date) => dayjs(date).add(1, "day").format());
|
||||
|
||||
// ✅ Better - Use .utc() for performance
|
||||
dates.map((date) => dayjs.utc(date).add(1, "day").format());
|
||||
|
||||
// ✅ Best - Use native Date when possible
|
||||
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
|
||||
```
|
||||
|
||||
## Next.js App Directory: Authorisation Checks in Pages
|
||||
|
||||
This can include checking session.user exists or session.org etc.
|
||||
|
||||
### TL;DR:
|
||||
|
||||
Don’t put permission checks in layout.tsx! Always put them directly inside your page.tsx or relevant server components for every restricted route.
|
||||
|
||||
|
||||
### Why Not to Use Layouts for Permission Checks
|
||||
|
||||
* Layouts don’t intercept all requests: If a user navigates directly or refreshes a protected route, layout checks might be skipped, exposing sensitive content.
|
||||
* APIs and server actions bypass layouts: Sensitive operations running on the server can’t be guarded by checks in the layout.
|
||||
* Risk of data leaks: Only page/server-level checks ensure that unauthorized users never get protected data.
|
||||
|
||||
### ✅ How To Secure Routes (The Right Way)
|
||||
|
||||
* Check permissions inside page.tsx or the actual server component.
|
||||
* Perform all session/user/role validation before querying or rendering sensitive content.
|
||||
* Redirect or return nothing to unauthorized users, before running restricted code.
|
||||
|
||||
### 🛠️ Example: Page-Level Permission Check
|
||||
|
||||
|
||||
```tsx
|
||||
// app/admin/page.tsx
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserSession } from "@/lib/auth";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await getUserSession();
|
||||
|
||||
if (!session || session.user.role !== "admin") {
|
||||
redirect("/"); // Or show an error
|
||||
}
|
||||
|
||||
// Protected content here
|
||||
return <div>Welcome, Admin!</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 🧠 Key Reminders
|
||||
|
||||
* Put permission guards in every restricted page.tsx.
|
||||
* Never assume layouts are secure for guarding data.
|
||||
* Validate users before any sensitive queries or rendering.
|
||||
|
||||
|
||||
## Avoid using Dayjs if you don’t need to be strictly tz aware.
|
||||
|
||||
When doing logic like Dayjs.startOf(".."), you can instead use date-fns' `startOfMonth(dateObj)` / `endOfDay(dateObj)`;
|
||||
When doing logic that depends on Browser locale, use i18n.language (prefer to deconstruct) like: `const { i18n: { language } } = useLocale();`, in combination with built-in Intl.
|
||||
|
||||
Note that with Date, you’re dealing with System time, so it’s not suited to everywhere (such as in the Booker, where instead we’ll likely migrate to Temporal) - but in most cases the above are suitable.
|
||||
|
||||
The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It’s a non-ideal solution to a problem that unfortunately exists due to Dayjs.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "calcom/cal.diy" }],
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "calcom/cal.com" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@calcom/atoms": patch
|
||||
---
|
||||
|
||||
fix: unlocked fields not saved for managed event type
|
||||
@@ -1 +0,0 @@
|
||||
../agents/rules
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../agents/skills
|
||||
@@ -0,0 +1,187 @@
|
||||
language: en-US
|
||||
tone_instructions: ''
|
||||
early_access: false
|
||||
enable_free_tier: true
|
||||
reviews:
|
||||
profile: chill
|
||||
request_changes_workflow: false
|
||||
high_level_summary: false
|
||||
high_level_summary_placeholder: '@coderabbitai summary'
|
||||
high_level_summary_in_walkthrough: false
|
||||
auto_title_placeholder: '@coderabbitai'
|
||||
auto_title_instructions: ''
|
||||
review_status: true
|
||||
commit_status: true
|
||||
fail_commit_status: false
|
||||
collapse_walkthrough: false
|
||||
changed_files_summary: false
|
||||
sequence_diagrams: false
|
||||
estimate_code_review_effort: false
|
||||
assess_linked_issues: true
|
||||
related_issues: false
|
||||
related_prs: true
|
||||
suggested_labels: false
|
||||
auto_apply_labels: false
|
||||
suggested_reviewers: false
|
||||
auto_assign_reviewers: false
|
||||
poem: false
|
||||
labeling_instructions: []
|
||||
path_filters:
|
||||
- '!**/node_modules/**'
|
||||
- '!**/dist/**'
|
||||
- '!**/build/**'
|
||||
- '!**/.next/**'
|
||||
path_instructions:
|
||||
- path: "**/*.{ts,tsx,js,jsx}"
|
||||
instructions: "Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module."
|
||||
abort_on_close: true
|
||||
disable_cache: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
auto_incremental_review: true
|
||||
ignore_title_keywords: []
|
||||
labels: []
|
||||
drafts: false
|
||||
base_branches: []
|
||||
finishing_touches:
|
||||
docstrings:
|
||||
enabled: true
|
||||
unit_tests:
|
||||
enabled: true
|
||||
pre_merge_checks:
|
||||
docstrings:
|
||||
mode: warning
|
||||
threshold: 80
|
||||
title:
|
||||
mode: warning
|
||||
requirements: ''
|
||||
description:
|
||||
mode: warning
|
||||
issue_assessment:
|
||||
mode: warning
|
||||
tools:
|
||||
ast-grep:
|
||||
rule_dirs: []
|
||||
util_dirs: []
|
||||
essential_rules: true
|
||||
packages: []
|
||||
shellcheck:
|
||||
enabled: true
|
||||
ruff:
|
||||
enabled: true
|
||||
markdownlint:
|
||||
enabled: true
|
||||
github-checks:
|
||||
enabled: true
|
||||
timeout_ms: 180000
|
||||
languagetool:
|
||||
enabled: true
|
||||
enabled_rules: []
|
||||
disabled_rules: []
|
||||
enabled_categories: []
|
||||
disabled_categories: []
|
||||
enabled_only: false
|
||||
level: default
|
||||
biome:
|
||||
enabled: true
|
||||
hadolint:
|
||||
enabled: true
|
||||
swiftlint:
|
||||
enabled: true
|
||||
phpstan:
|
||||
enabled: true
|
||||
level: default
|
||||
phpmd:
|
||||
enabled: true
|
||||
phpcs:
|
||||
enabled: true
|
||||
golangci-lint:
|
||||
enabled: true
|
||||
yamllint:
|
||||
enabled: true
|
||||
gitleaks:
|
||||
enabled: true
|
||||
checkov:
|
||||
enabled: true
|
||||
detekt:
|
||||
enabled: true
|
||||
eslint:
|
||||
enabled: true
|
||||
flake8:
|
||||
enabled: true
|
||||
rubocop:
|
||||
enabled: true
|
||||
buf:
|
||||
enabled: true
|
||||
regal:
|
||||
enabled: true
|
||||
actionlint:
|
||||
enabled: true
|
||||
pmd:
|
||||
enabled: true
|
||||
cppcheck:
|
||||
enabled: true
|
||||
semgrep:
|
||||
enabled: true
|
||||
circleci:
|
||||
enabled: true
|
||||
clippy:
|
||||
enabled: true
|
||||
sqlfluff:
|
||||
enabled: true
|
||||
prismaLint:
|
||||
enabled: true
|
||||
pylint:
|
||||
enabled: true
|
||||
oxc:
|
||||
enabled: true
|
||||
shopifyThemeCheck:
|
||||
enabled: true
|
||||
luacheck:
|
||||
enabled: true
|
||||
brakeman:
|
||||
enabled: true
|
||||
dotenvLint:
|
||||
enabled: true
|
||||
htmlhint:
|
||||
enabled: true
|
||||
checkmake:
|
||||
enabled: true
|
||||
osvScanner:
|
||||
enabled: true
|
||||
chat:
|
||||
art: true
|
||||
auto_reply: true
|
||||
integrations:
|
||||
jira:
|
||||
usage: auto
|
||||
linear:
|
||||
usage: auto
|
||||
knowledge_base:
|
||||
opt_out: false
|
||||
web_search:
|
||||
enabled: true
|
||||
code_guidelines:
|
||||
enabled: true
|
||||
filePatterns: []
|
||||
learnings:
|
||||
scope: auto
|
||||
issues:
|
||||
scope: auto
|
||||
jira:
|
||||
usage: auto
|
||||
project_keys: []
|
||||
linear:
|
||||
usage: auto
|
||||
team_keys: []
|
||||
pull_requests:
|
||||
scope: auto
|
||||
mcp:
|
||||
usage: auto
|
||||
disabled_servers: []
|
||||
code_generation:
|
||||
docstrings:
|
||||
language: en-US
|
||||
path_instructions: []
|
||||
unit_tests:
|
||||
path_instructions: []
|
||||
@@ -1 +0,0 @@
|
||||
../agents/rules
|
||||
@@ -0,0 +1,36 @@
|
||||
# Review Instructions
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
- **Repository Files**: Must include `Repository` suffix, prefix with technology if applicable (e.g., `PrismaAppRepository.ts`), use PascalCase matching exported class
|
||||
- **Service Files**: Must include `Service` suffix, use PascalCase matching exported class, avoid generic names (e.g., `MembershipService.ts`)
|
||||
- **New files**: Avoid dot-suffixes like `.service.ts` or `.repository.ts`; reserve `.test.ts`, `.spec.ts`, `.types.ts` for their specific purposes
|
||||
|
||||
## Code Quality
|
||||
|
||||
- Prefer early returns. It is recommended to throw/return early so we can ensure null-checks and prevent further nesting.
|
||||
- Prefer Composition over Prop Drilling. Instead of relying on prop drilling, let's try to take advantage of react children feature.
|
||||
- For Prisma queries:
|
||||
- Only select data you need
|
||||
1) Increased performance overhead: When you select all columns from a table, Prisma retrieves all the data associated with each row, which can result in a significant performance impact. This includes reading unnecessary columns and transferring a large volume of data between your application and the database.
|
||||
2) Unnecessary data exposure: Selecting all columns exposes sensitive data that might not be required by your application. This can pose security risks, especially if the table contains sensitive user information or proprietary data. (In our case app credentials and booking information)
|
||||
- TLDR: Never use Includes - use select
|
||||
- Select selects only the fields you specify explicitly
|
||||
- Include selects the relationship AND all the fields of the table the relationship belongs to
|
||||
- Make sure the credential.key field is never returned back from tRPC endpoints or APIs
|
||||
- Always use t() for text localization in frontend code; direct text embedding should trigger a warning.
|
||||
- Check if there's any O(n^2) logic in backend code; we should aim for O(n log n) or O(n) ideally.
|
||||
- Check if there are circular references introduced. We should never allow these.
|
||||
- Flag excessive Day.js use in performance-critical code. Functions like `.add`, `.diff`, `.isBefore`, and `.isAfter` are slow, especially in timezone mode. Prefer `.utc()` for better performance. Where possible, replace with native Date and direct `.valueOf()` comparisons for faster execution. Recommend using native methods or Day.js `.utc()` consistently in hot paths like loops.
|
||||
- When docs changes are made in /docs/api-reference/v2/openapi.json, ensure the following
|
||||
- "summary" fields are short and concise
|
||||
- "summary" fields do not end with a period
|
||||
- "summary" fields are written in proper American english
|
||||
- When changes to API v2 or v1 are made, ensure there are no breaking changes on existing endpoints. Instead, there needs to be a newly versioned endpoint with the updated functionality but the old one must remain functional
|
||||
- For large pull requests (>500 lines changed or >10 files touched), advise splitting into smaller, focused PRs:
|
||||
- Split by feature boundaries: separate different features or user stories
|
||||
- Split by layer/component: frontend changes, backend changes, database migrations, and tests in separate PRs
|
||||
- Split by dependency chain: create PRs that can be merged sequentially
|
||||
- Split by file/module: group related file changes together
|
||||
- Suggested pattern: Database migrations → Backend logic → Frontend components → Integration tests
|
||||
- Benefits: easier review, faster feedback, reduced conflicts, easier rollback, better history
|
||||
@@ -1 +0,0 @@
|
||||
../agents/skills
|
||||
@@ -1,36 +0,0 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Build outputs
|
||||
.next
|
||||
**/.next
|
||||
dist
|
||||
**/dist
|
||||
build
|
||||
**/build
|
||||
|
||||
# Git
|
||||
.git
|
||||
.github
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
**/coverage
|
||||
test-results
|
||||
**/test-results
|
||||
.turbo
|
||||
**/.turbo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Examples and docs
|
||||
docs
|
||||
+18
-10
@@ -24,8 +24,8 @@
|
||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||
|
||||
# - BASECAMP
|
||||
# Used to enable Basecamp integration with Cal.diy
|
||||
# @see https://github.com/calcom/cal.diy#obtaining-basecamp-client-id-and-secret
|
||||
# Used to enable Basecamp integration with Cal.com
|
||||
# @see https://github.com/calcom/cal.com#obtaining-basecamp-client-id-and-secret
|
||||
BASECAMP3_CLIENT_ID=
|
||||
BASECAMP3_CLIENT_SECRET=
|
||||
BASECAMP3_USER_AGENT=
|
||||
@@ -34,7 +34,7 @@ BASECAMP3_USER_AGENT=
|
||||
# Enables Cal Video. to get your key
|
||||
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||
# 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||
# @see https://github.com/calcom/cal.diy#obtaining-daily-api-credentials
|
||||
# @see https://github.com/calcom/cal.com#obtaining-daily-api-credentials
|
||||
|
||||
DAILY_API_KEY=
|
||||
DAILY_SCALE_PLAN=''
|
||||
@@ -43,7 +43,7 @@ DAILY_MEETING_ENDED_WEBHOOK_SECRET=''
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
# @see https://github.com/calcom/cal.diy#obtaining-the-google-api-credentials
|
||||
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||
GOOGLE_API_CREDENTIALS=
|
||||
|
||||
# To enable Login with Google you need to:
|
||||
@@ -55,18 +55,18 @@ GOOGLE_LOGIN_ENABLED=false
|
||||
|
||||
# - HUBSPOT
|
||||
# Used for the HubSpot integration
|
||||
# @see https://github.com/calcom/cal.diy/#obtaining-hubspot-client-id-and-secret
|
||||
# @see https://github.com/calcom/cal.com/#obtaining-hubspot-client-id-and-secret
|
||||
HUBSPOT_CLIENT_ID=""
|
||||
HUBSPOT_CLIENT_SECRET=""
|
||||
|
||||
# - OFFICE 365
|
||||
# Used for the Office 365 / Outlook.com Calendar / MS Teams integration
|
||||
# @see https://github.com/calcom/cal.diy/#Obtaining-Microsoft-Graph-Client-ID-and-Secret
|
||||
# @see https://github.com/calcom/cal.com/#Obtaining-Microsoft-Graph-Client-ID-and-Secret
|
||||
MS_GRAPH_CLIENT_ID=
|
||||
MS_GRAPH_CLIENT_SECRET=
|
||||
|
||||
# - SLACK
|
||||
# @see https://github.com/calcom/cal.diy/#obtaining-slack-client-id-and-secret-and-signing-secret
|
||||
# @see https://github.com/calcom/cal.com/#obtaining-slack-client-id-and-secret-and-signing-secret
|
||||
SLACK_SIGNING_SECRET=
|
||||
SLACK_CLIENT_ID=
|
||||
SLACK_CLIENT_SECRET=
|
||||
@@ -87,7 +87,7 @@ TANDEM_BASE_URL="https://tandem.chat"
|
||||
|
||||
# - ZOOM
|
||||
# Used for the Zoom integration
|
||||
# @see https://github.com/calcom/cal.diy/#obtaining-zoom-client-id-and-secret
|
||||
# @see https://github.com/calcom/cal.com/#obtaining-zoom-client-id-and-secret
|
||||
ZOOM_CLIENT_ID=
|
||||
ZOOM_CLIENT_SECRET=
|
||||
|
||||
@@ -98,7 +98,7 @@ GIPHY_API_KEY=
|
||||
|
||||
# - VITAL
|
||||
# Used for the vital integration
|
||||
# @see https://github.com/calcom/cal.diy/#obtaining-vital-api-keys
|
||||
# @see https://github.com/calcom/cal.com/#obtaining-vital-api-keys
|
||||
VITAL_API_KEY=
|
||||
VITAL_WEBHOOK_SECRET=
|
||||
# "sandbox" | "prod" | "production" | "development"
|
||||
@@ -108,7 +108,7 @@ VITAL_REGION="us"
|
||||
|
||||
# - ZAPIER
|
||||
# Used for the Zapier integration
|
||||
# @see https://github.com/calcom/cal.diy/blob/main/packages/app-store/zapier/README.md
|
||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md
|
||||
ZAPIER_INVITE_LINK=""
|
||||
|
||||
# - LARK
|
||||
@@ -129,6 +129,14 @@ ZOHOCRM_CLIENT_ID=""
|
||||
ZOHOCRM_CLIENT_SECRET=""
|
||||
|
||||
|
||||
# - REVERT
|
||||
# Used for the Pipedrive integration (via/ Revert (https://revert.dev))
|
||||
# @see https://github.com/calcom/cal.com/#obtaining-revert-api-keys
|
||||
REVERT_API_KEY=
|
||||
REVERT_PUBLIC_TOKEN=
|
||||
|
||||
# NOTE: If you're self hosting Revert, update this URL to point to your own instance.
|
||||
REVERT_API_URL=https://api.revert.dev/
|
||||
# *********************************************************************************************************
|
||||
|
||||
# - Huddle01
|
||||
|
||||
+73
-68
@@ -1,16 +1,29 @@
|
||||
# ********** INDEX **********
|
||||
#
|
||||
# - LICENSE
|
||||
# - LICENSE (DEPRECATED)
|
||||
# - DATABASE
|
||||
# - SHARED
|
||||
# - NEXTAUTH
|
||||
# - E-MAIL SETTINGS
|
||||
# - ORGANIZATIONS
|
||||
|
||||
# - LICENSE ************************************************************************************
|
||||
# This project is licensed under the MIT License.
|
||||
# @see https://github.com/calcom/cal.diy/blob/main/LICENSE
|
||||
# Cal.diy is fully open source — no license key is required.
|
||||
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||
#
|
||||
# Summary of terms:
|
||||
# - The codebase has to stay open source, whether it was modified or not
|
||||
# - You can not repackage or sell the codebase
|
||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||
#
|
||||
|
||||
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
|
||||
|
||||
CALCOM_LICENSE_KEY=
|
||||
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
||||
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
||||
CAL_SIGNATURE_TOKEN=
|
||||
# The route to the Cal.com License API
|
||||
CALCOM_PRIVATE_API_ROUTE="https://goblin.cal.com"
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - DATABASE ************************************************************************************************
|
||||
@@ -20,6 +33,12 @@ DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
DATABASE_DIRECT_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
INSIGHTS_DATABASE_URL=
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
# PRISMA_GENERATE_DATAPROXY=true
|
||||
PRISMA_GENERATE_DATAPROXY=
|
||||
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - SHARED **************************************************************************************************
|
||||
@@ -28,16 +47,19 @@ INSIGHTS_DATABASE_URL=
|
||||
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||
# Change to 'http://localhost:3001' if running the website simultaneously
|
||||
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
||||
NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
|
||||
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
|
||||
|
||||
# To enable SAML login, set both these variables
|
||||
# @see https://github.com/calcom/cal.diy/tree/main/packages/features/ee#setting-up-saml-login
|
||||
# @see https://github.com/calcom/cal.com/tree/main/packages/features/ee#setting-up-saml-login
|
||||
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
|
||||
SAML_DATABASE_URL=
|
||||
# SAML_ADMINS='pro@example.com'
|
||||
SAML_ADMINS=
|
||||
# NEXT_PUBLIC_HOSTED_CAL_FEATURES=1
|
||||
NEXT_PUBLIC_HOSTED_CAL_FEATURES=
|
||||
# For additional security set to a random secret and use that value as the client_secret during the OAuth 2.0 flow.
|
||||
SAML_CLIENT_SECRET_VERIFIER=
|
||||
|
||||
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
|
||||
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
|
||||
@@ -45,7 +67,7 @@ NEXT_PUBLIC_HOSTED_CAL_FEATURES=
|
||||
PGSSLMODE=
|
||||
|
||||
# Define which hostnames are expected for the app to work on
|
||||
ALLOWED_HOSTNAMES='"cal.local:3000","localhost:3000"'
|
||||
ALLOWED_HOSTNAMES='"cal.com","cal.dev","cal-staging.com","cal.community","cal.local:3000","localhost:3000"'
|
||||
# Reserved orgs subdomains for our own usage
|
||||
RESERVED_SUBDOMAINS='"app","auth","docs","design","console","go","status","api","saml","www","matrix","developer","cal","my","team","support","security","blog","learn","admin"'
|
||||
|
||||
@@ -72,7 +94,7 @@ CRON_ENABLE_APP_SYNC=false
|
||||
|
||||
# Application Key for symmetric encryption and decryption
|
||||
# must be 32 bytes for AES256 encryption algorithm
|
||||
# You can use: `openssl rand -base64 24` to generate a 32-character key
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
CALENDSO_ENCRYPTION_KEY=
|
||||
|
||||
# Intercom Config
|
||||
@@ -94,15 +116,6 @@ NEXT_PUBLIC_POSTHOG_HOST=
|
||||
# Dub Config
|
||||
DUB_API_KEY=
|
||||
NEXT_PUBLIC_DUB_PROGRAM_ID=
|
||||
# Optional: Domain for SMS workflow shortened links
|
||||
DUB_SMS_DOMAIN=
|
||||
# Optional: Folder ID to organize SMS workflow shortened links in Dub
|
||||
DUB_SMS_FOLDER_ID=
|
||||
|
||||
# Sink URL Shortener Config
|
||||
SINK_API_URL=
|
||||
# API key for Sink authentication
|
||||
SINK_API_KEY=
|
||||
|
||||
# Zendesk Config
|
||||
NEXT_PUBLIC_ZENDESK_KEY=
|
||||
@@ -114,17 +127,6 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
|
||||
NEXT_PUBLIC_FRESHCHAT_TOKEN=
|
||||
NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||
|
||||
# Microsoft OAuth credentials
|
||||
OUTLOOK_LOGIN_ENABLED=false
|
||||
|
||||
|
||||
# For holiday feature:
|
||||
# Step-by-step: Get a Google Calendar API Key
|
||||
# 1. Go to Google Cloud Console: https://console.cloud.google.com/
|
||||
# 2. Select or Create a Project
|
||||
# 3. Enable Google Calendar API (APIs & Services → Library , Search for Google Calendar API)
|
||||
# 4. Create the API Key (APIs & Services → Credentials)
|
||||
GOOGLE_CALENDAR_API_KEY=
|
||||
# Google OAuth credentials
|
||||
# To enable Login with Google you need to:
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` below
|
||||
@@ -135,7 +137,7 @@ GOOGLE_LOGIN_ENABLED=false
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
# @see https://github.com/calcom/cal.diy#obtaining-the-google-api-credentials
|
||||
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||
GOOGLE_API_CREDENTIALS=
|
||||
# Token to verify incoming webhooks from Google Calendar
|
||||
GOOGLE_WEBHOOK_TOKEN=
|
||||
@@ -181,6 +183,17 @@ FORMBRICKS_FEEDBACK_SURVEY_ID=
|
||||
AVATARAPI_USERNAME=
|
||||
AVATARAPI_PASSWORD=
|
||||
|
||||
# Twilio
|
||||
# Used to send SMS reminders in workflows
|
||||
TWILIO_SID=
|
||||
TWILIO_TOKEN=
|
||||
TWILIO_MESSAGING_SID=
|
||||
TWILIO_PHONE_NUMBER=
|
||||
TWILIO_WHATSAPP_PHONE_NUMBER=
|
||||
TWILIO_WHATSAPP_REMINDER_CONTENT_SID=
|
||||
TWILIO_WHATSAPP_CANCELLED_CONTENT_SID=
|
||||
TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID=
|
||||
TWILIO_WHATSAPP_COMPLETED_CONTENT_SID=
|
||||
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
|
||||
NEXT_PUBLIC_SENDER_ID=
|
||||
TWILIO_VERIFY_SID=
|
||||
@@ -195,16 +208,12 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_TEAM_ANNUAL_PRICE_ID=
|
||||
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
|
||||
ORG_MONTHLY_CREDITS=
|
||||
STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_ANNUAL_PRICE_ID=
|
||||
STRIPE_ORG_PRODUCT_ID=
|
||||
# Used to pass trial days for orgs during the Stripe checkout session
|
||||
STRIPE_ORG_TRIAL_DAYS=
|
||||
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
@@ -220,7 +229,7 @@ API_KEY_PREFIX=cal_
|
||||
# allow access to the nodemailer transports from the .env file. E-mail templates are accessible within lib/emails/
|
||||
# Configures the global From: header whilst sending emails.
|
||||
EMAIL_FROM='notifications@yourselfhostedcal.com'
|
||||
EMAIL_FROM_NAME='Cal.diy'
|
||||
EMAIL_FROM_NAME='Cal.com'
|
||||
|
||||
# Configure SMTP settings (@see https://nodemailer.com/smtp/).
|
||||
# Configuration to receive emails locally (mailhog)
|
||||
@@ -242,9 +251,6 @@ EMAIL_SERVER_PORT=1025
|
||||
## @see https://support.google.com/accounts/answer/185833
|
||||
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
|
||||
|
||||
# queue or cancel payment reminder email/flow
|
||||
AWAITING_PAYMENT_EMAIL_DELAY_MINUTES=
|
||||
|
||||
# Used for E2E for email testing
|
||||
# Set it to "1" if you need to email checks in E2E tests locally
|
||||
# Make sure to run mailhog container manually or with `yarn dx`
|
||||
@@ -264,6 +270,9 @@ CLOUDFLARE_TURNSTILE_SECRET=
|
||||
# 0 = false, 1=true
|
||||
NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER=
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||
|
||||
# Close.com internal CRM
|
||||
CLOSECOM_CLIENT_ID=
|
||||
CLOSECOM_CLIENT_SECRET=
|
||||
@@ -272,8 +281,8 @@ CLOSECOM_CLIENT_SECRET=
|
||||
SENDGRID_SYNC_API_KEY=
|
||||
|
||||
# Change your Brand
|
||||
NEXT_PUBLIC_APP_NAME="Cal.diy"
|
||||
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.diy"
|
||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
|
||||
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
||||
# Set this to true in to disable new signups
|
||||
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
||||
@@ -333,7 +342,7 @@ E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||
|
||||
# - CALCOM QA ACCOUNT
|
||||
# Used for E2E tests on Cal.diy that require 3rd party integrations
|
||||
# Used for E2E tests on Cal.com that require 3rd party integrations
|
||||
E2E_TEST_CALCOM_QA_EMAIL="qa@example.com"
|
||||
# Replace with your own password
|
||||
E2E_TEST_CALCOM_QA_PASSWORD="password"
|
||||
@@ -341,7 +350,7 @@ E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS=
|
||||
E2E_TEST_CALCOM_GCAL_KEYS=
|
||||
|
||||
# - APP CREDENTIAL SYNC ***********************************************************************************
|
||||
# Used for self-hosters that are implementing Cal.diy into their applications that already have certain integrations
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
CALCOM_CREDENTIAL_SYNC_SECRET=""
|
||||
@@ -349,11 +358,24 @@ CALCOM_CREDENTIAL_SYNC_SECRET=""
|
||||
CALCOM_CREDENTIAL_SYNC_HEADER_NAME="calcom-credential-sync-secret"
|
||||
# This the endpoint from which the token is fetched
|
||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||
# Key should match on Cal.diy and your application
|
||||
# Key should match on Cal.com and your application
|
||||
# must be 24 bytes for AES256 encryption algorithm
|
||||
# You can use: `openssl rand -base64 24` to generate one
|
||||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
|
||||
|
||||
# - OIDC E2E TEST *******************************************************************************************
|
||||
|
||||
# Ensure this ADMIN EMAIL is present in the SAML_ADMINS list
|
||||
E2E_TEST_SAML_ADMIN_EMAIL=
|
||||
E2E_TEST_SAML_ADMIN_PASSWORD=
|
||||
|
||||
E2E_TEST_OIDC_CLIENT_ID=
|
||||
E2E_TEST_OIDC_CLIENT_SECRET=
|
||||
E2E_TEST_OIDC_PROVIDER_DOMAIN=
|
||||
|
||||
E2E_TEST_OIDC_USER_EMAIL=
|
||||
E2E_TEST_OIDC_USER_PASSWORD=
|
||||
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# api v2
|
||||
@@ -363,10 +385,7 @@ NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
||||
TASKER_ENABLE_WEBHOOKS=0
|
||||
TASKER_ENABLE_EMAILS=0
|
||||
|
||||
# Ratelimiting via Unkey (https://unkey.com)
|
||||
# Optional: Only required if you want to enable rate limiting
|
||||
# Not needed for testing or self-hosting unless you specifically want rate limiting features
|
||||
# Sign up at https://unkey.com to get your Root key
|
||||
# Ratelimiting via unkey
|
||||
UNKEY_ROOT_KEY=
|
||||
|
||||
# Used for Cal.ai Voice AI Agents
|
||||
@@ -402,7 +421,13 @@ BLACKLISTED_GUEST_EMAILS=
|
||||
NEXT_PUBLIC_VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
|
||||
# Custom privacy policy/ terms URLs (for self-hosters: change to your privacy policy / terms URLs)
|
||||
# Mintlify chat api
|
||||
# Power AI chat in for docs
|
||||
NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY=
|
||||
NEXT_PUBLIC_CHAT_API_URL=
|
||||
NEXT_PUBLIC_DOCS_URL=
|
||||
|
||||
# Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs)
|
||||
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=
|
||||
NEXT_PUBLIC_WEBSITE_TERMS_URL=
|
||||
|
||||
@@ -426,7 +451,7 @@ LINGO_DOT_DEV_API_KEY=
|
||||
DIRECTORY_IDS_TO_LOG=
|
||||
|
||||
|
||||
# Set this when Cal.diy is used to serve only one organization's booking pages
|
||||
# Set this when Cal.com is used to serve only one organization's booking pages
|
||||
# Read more about it in the README.md
|
||||
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
||||
|
||||
@@ -461,23 +486,3 @@ CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
|
||||
SALESFORCE_GRAPHQL_DELAY_MS=500
|
||||
SALESFORCE_GRAPHQL_MAX_DELAY_MS=2000
|
||||
SALESFORCE_GRAPHQL_MAX_RETRIES=3
|
||||
|
||||
# Force Node.js to use UTC as process timezone
|
||||
TZ=UTC
|
||||
|
||||
# test oauth client for `packages/platform/examples/base` (optional)
|
||||
SEED_PLATFORM_OAUTH_CLIENT_ID=
|
||||
SEED_PLATFORM_OAUTH_CLIENT_SECRET=
|
||||
|
||||
# test oauth2 client for `packages/platform/examples/base` (optional)
|
||||
SEED_OAUTH2_CLIENT_ID=
|
||||
SEED_OAUTH2_CLIENT_SECRET_HASHED=
|
||||
|
||||
# Trigger.dev
|
||||
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
|
||||
TRIGGER_SECRET_KEY=
|
||||
TRIGGER_API_URL=https://api.trigger.dev
|
||||
TRIGGER_DEV_PROJECT_REF=
|
||||
|
||||
GOOGLE_ADS_ENABLED=1 # To enable Google Ads tracking (gclid)
|
||||
LINKEDIN_ADS_ENABLED=1 # To enable LinkedIn Ads tracking (li_fat_id)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
||||
+37
-1
@@ -1 +1,37 @@
|
||||
|
||||
/**/package.json @calcom/Foundation
|
||||
/apps/api/**/* @calcom/API
|
||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||
/apps/web/lib/csp.ts @calcom/Foundation
|
||||
/apps/web/lib/buildNonce.ts @calcom/Foundation
|
||||
/apps/web/lib/daily-webhook/**/* @calcom/Foundation
|
||||
/apps/web/lib/core/**/* @calcom/Foundation
|
||||
/apps/web/lib/booking.ts @calcom/Foundation
|
||||
/apps/web/lib/handleOrgRedirect.ts @calcom/Foundation
|
||||
/apps/web/middleware.ts @calcom/Foundation
|
||||
/deploy/**/* @calcom/Foundation
|
||||
/packages/app-store/applecalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/exchange2013calendar/**/* @calcom/Foundation
|
||||
/packages/app-store/exchange2016calendar/**/* @calcom/Foundation
|
||||
/packages/app-store/exchangecalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/feishucalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/googlecalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/ics-feedcalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/larkcalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
||||
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
||||
/packages/embeds/**/* @calcom/Foundation
|
||||
/packages/features/auth/lib/next-auth-options.ts @calcom/Foundation
|
||||
/packages/features/bookings/lib/**/* @calcom/Foundation
|
||||
/packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.ts @calcom/Foundation
|
||||
/packages/features/availability/lib/getUserAvailability.ts @calcom/Foundation
|
||||
/packages/features/bookings/lib/getLuckyUser.ts @calcom/Foundation
|
||||
/packages/features/schedules/lib/slots.ts @calcom/Foundation
|
||||
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||
/packages/prisma/**/* @calcom/Foundation
|
||||
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
||||
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
||||
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
||||
/packages/ui/**/* @calcom/Consumer @calcom/Foundation
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions
|
||||
url: https://github.com/calcom/cal.diy/discussions
|
||||
about: Need help self-hosting or have a general question about Cal.diy? Open a discussion
|
||||
url: https://github.com/calcom/cal.com/discussions
|
||||
about: Need help selfhosting or ask a general question about the project? Open a discussion
|
||||
|
||||
@@ -55,4 +55,4 @@ assignees: ""
|
||||
- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves feature request by removing this label, then you can start coding.
|
||||
- For clarity: Non-core member issues automatically get the `🚨 needs approval` label.
|
||||
- Your feature ideas are invaluable to us! However, they undergo review to ensure alignment with the product's direction.
|
||||
- Follow Best Practices lined out in our [Contributor Docs](https://github.com/calcom/cal.diy/blob/main/CONTRIBUTING.md)
|
||||
- Follow Best Practices lined out in our [Contributor Docs](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: Cal.com/platform request
|
||||
about: Suggest a feature or bug report for cal.com/platform
|
||||
title: ""
|
||||
labels: ["platform", "🚨 needs approval"]
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
### Is your proposal related to a problem?
|
||||
|
||||
<!--
|
||||
Provide a clear and concise description of what the problem is.
|
||||
For example, "I'm always frustrated when..."
|
||||
-->
|
||||
|
||||
(Write your answer here.)
|
||||
|
||||
### Describe the solution you'd like
|
||||
|
||||
<!--
|
||||
Provide a clear and concise description of what you want to happen.
|
||||
-->
|
||||
|
||||
(Describe your proposed solution here.)
|
||||
|
||||
### Describe alternatives you've considered
|
||||
|
||||
<!--
|
||||
Let us know about other solutions you've tried or researched.
|
||||
-->
|
||||
|
||||
(Write your answer here.)
|
||||
|
||||
### Additional context
|
||||
|
||||
<!--
|
||||
Is there anything else you can add about the proposal?
|
||||
You might want to link to related issues here, if you haven't already.
|
||||
-->
|
||||
|
||||
(Write your answer here.)
|
||||
|
||||
### Requirement/Document
|
||||
|
||||
<!--
|
||||
Is there any type of document that could support that feature?
|
||||
-->
|
||||
|
||||
(Share it here.)
|
||||
|
||||
---
|
||||
|
||||
##### House rules
|
||||
|
||||
- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves feature request by removing this label, then you can start coding.
|
||||
- For clarity: Non-core member issues automatically get the `🚨 needs approval` label.
|
||||
- Your feature ideas are invaluable to us! However, they undergo review to ensure alignment with the product's direction.
|
||||
- Follow Best Practices lined out in our [Contributor Docs](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
@@ -1,10 +1,9 @@
|
||||
## What does this PR do?
|
||||
|
||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
Note: Cal.diy is a community-maintained open-source project. Contributions here do NOT flow to Cal.com's production service. -->
|
||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
|
||||
- Fixes #XXXX (GitHub issue number)
|
||||
- Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)
|
||||
|
||||
## Visual Demo (For contributors especially)
|
||||
|
||||
@@ -23,7 +22,7 @@ A visual demonstration is strongly recommended, for both the original and new ch
|
||||
## Mandatory Tasks (DO NOT REMOVE)
|
||||
|
||||
- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
|
||||
- [ ] I have updated the developer docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
|
||||
- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
|
||||
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.
|
||||
|
||||
## How should this be tested?
|
||||
@@ -39,8 +38,7 @@ A visual demonstration is strongly recommended, for both the original and new ch
|
||||
|
||||
<!-- Remove bullet points below that don't apply to you -->
|
||||
|
||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.diy/blob/main/CONTRIBUTING.md)
|
||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
- My code doesn't follow the style guidelines of this project
|
||||
- I haven't commented my code, particularly in hard-to-understand areas
|
||||
- I haven't checked if my changes generate no new warnings
|
||||
- My PR is too large (>500 lines or >10 files) and should be split into smaller PRs
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Generate cache-build key
|
||||
description: "Generate the cache key for production build caching"
|
||||
inputs:
|
||||
branch_key:
|
||||
required: true
|
||||
description: "Branch key for cache scoping (e.g., github.head_ref for PRs)"
|
||||
outputs:
|
||||
key:
|
||||
description: "The generated cache key"
|
||||
value: ${{ steps.generate-key.outputs.key }}
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Generate cache key
|
||||
id: generate-key
|
||||
shell: bash
|
||||
env:
|
||||
CACHE_NAME: prod-build
|
||||
BRANCH_KEY: ${{ inputs.branch_key }}
|
||||
LOCKFILE_HASH: ${{ hashFiles('yarn.lock') }}
|
||||
SOURCE_HASH: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'apps/**/*.json', 'apps/**/*.css', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', 'packages/prisma/schema.prisma', 'packages/prisma/migrations/**/*.sql', '!**/node_modules/**', '!packages/prisma/generated/**', '!packages/prisma/client/**', '!packages/prisma/zod/**', '!packages/kysely/**') }}
|
||||
run: |
|
||||
echo "key=${CACHE_NAME}-${BRANCH_KEY}-${LOCKFILE_HASH}-${SOURCE_HASH}" >> $GITHUB_OUTPUT
|
||||
@@ -1,23 +1,33 @@
|
||||
name: Cache production build binaries
|
||||
description: "Cache or restore if necessary"
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v18.x
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Generate cache key
|
||||
id: cache-key
|
||||
uses: ./.github/actions/cache-build-key
|
||||
with:
|
||||
branch_key: ${{ github.head_ref || github.ref_name }}
|
||||
- name: Cache production build
|
||||
uses: actions/cache@v4
|
||||
uses: buildjet/cache@v4
|
||||
id: cache-build
|
||||
env:
|
||||
# WARN: Don't touch this cache key. Currently github.sha refers to the latest commit in main
|
||||
# and not the branch that's attempting to merge to main, which causes CI errors when merged
|
||||
# to main.
|
||||
# TODO: Fix this problem if intending to modify this cache key.
|
||||
cache-name: prod-build
|
||||
key-1: ${{ inputs.node_version }}-${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
key-4: ${{ github.sha }}
|
||||
key-5: ${{ github.event.pull_request.head.sha }}
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/web/.next
|
||||
${{ github.workspace }}/apps/web/public/embed
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ steps.cache-key.outputs.key }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}-${{ env.key-5 }}
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn build
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Cache Git Checkout
|
||||
description: "Cache or restore git checkout to speed up CI workflows"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache git checkout
|
||||
uses: actions/cache@v4
|
||||
id: cache-checkout
|
||||
with:
|
||||
path: |
|
||||
.
|
||||
!.git
|
||||
!node_modules
|
||||
!**/node_modules
|
||||
!apps/web/public/app-store
|
||||
key: git-checkout-${{ github.head_ref || github.ref_name }}-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout PR code (on cache miss)
|
||||
if: steps.cache-checkout.outputs.cache-hit != 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 2
|
||||
sparse-checkout-cone-mode: false
|
||||
sparse-checkout: |
|
||||
/*
|
||||
!/example-apps/
|
||||
!**/*.mp4
|
||||
!/docs/images/
|
||||
!packages/app-store/*/static/*.png
|
||||
!packages/app-store/*/static/*.jpg
|
||||
!packages/app-store/*/static/*.jpeg
|
||||
!packages/app-store/*/static/*.gif
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Generate cache-db key
|
||||
description: "Generate the cache key for database caching"
|
||||
inputs:
|
||||
path:
|
||||
required: false
|
||||
default: "backups/backup.sql"
|
||||
description: "Path to the backup file"
|
||||
outputs:
|
||||
key:
|
||||
description: "The generated cache key"
|
||||
value: ${{ steps.generate-key.outputs.key }}
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Generate cache key
|
||||
id: generate-key
|
||||
shell: bash
|
||||
env:
|
||||
CACHE_NAME: cache-db
|
||||
PATH_KEY: ${{ inputs.path }}
|
||||
PRISMA_HASH: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts', 'scripts/seed*.ts') }}
|
||||
run: |
|
||||
echo "key=${CACHE_NAME}-${PATH_KEY}-${PRISMA_HASH}" >> $GITHUB_OUTPUT
|
||||
@@ -10,15 +10,14 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Generate cache key
|
||||
id: cache-key
|
||||
uses: ./.github/actions/cache-db-key
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
- name: Cache database
|
||||
id: cache-db
|
||||
uses: actions/cache@v4
|
||||
uses: buildjet/cache@v4
|
||||
env:
|
||||
cache-name: cache-db
|
||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||
key-2: ${{ github.event.pull_request.number || github.ref }}
|
||||
key-3: ${{ github.event.pull_request.head.sha }}
|
||||
DATABASE_URL: ${{ inputs.DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ inputs.DATABASE_URL }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ inputs.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
@@ -26,22 +25,20 @@ runs:
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ inputs.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ steps.cache-key.outputs.key }}
|
||||
- run: yarn db-seed
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
||||
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- name: Postgres Dump Backup
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p $(dirname "${{ inputs.path }}")
|
||||
docker run --rm --network host \
|
||||
-v ${{ github.workspace }}:${{ github.workspace }} -w ${{ github.workspace }} \
|
||||
postgres:18 pg_dump "${{ inputs.DATABASE_URL }}" -O -f ${{ inputs.path }}
|
||||
shell: bash
|
||||
uses: tj-actions/pg-dump@v2.3
|
||||
with:
|
||||
database_url: ${{ inputs.DATABASE_URL }}
|
||||
path: ${{ inputs.path }}
|
||||
options: "-O"
|
||||
- name: Postgres Backup Restore
|
||||
if: steps.cache-db.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
docker run --rm -i --network host \
|
||||
-v ${{ github.workspace }}:${{ github.workspace }} -w ${{ github.workspace }} \
|
||||
postgres:18 psql "${{ inputs.DATABASE_URL }}" -f ${{ inputs.path }}
|
||||
shell: bash
|
||||
uses: tj-actions/pg-restore@v4.5
|
||||
with:
|
||||
database_url: ${{ inputs.DATABASE_URL }}
|
||||
backup_file: ${{ inputs.path }}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Dangerous git Checkout
|
||||
description: "Git Checkout from PR code so we can run checks from forks"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
@@ -1,186 +0,0 @@
|
||||
name: "Docker Build and Test"
|
||||
description: "Reusable action to build and test Docker images for Cal.com"
|
||||
|
||||
inputs:
|
||||
platform:
|
||||
description: "Target platform (linux/amd64 or arm64)"
|
||||
required: true
|
||||
platform-suffix:
|
||||
description: "Suffix to add to image tags (e.g., -arm)"
|
||||
required: false
|
||||
default: ""
|
||||
dockerhub-username:
|
||||
description: "Docker Hub username"
|
||||
required: true
|
||||
dockerhub-token:
|
||||
description: "Docker Hub token"
|
||||
required: true
|
||||
github-token:
|
||||
description: "GitHub token for GHCR"
|
||||
required: true
|
||||
postgres-user:
|
||||
description: "PostgreSQL user"
|
||||
required: true
|
||||
postgres-password:
|
||||
description: "PostgreSQL password"
|
||||
required: true
|
||||
postgres-db:
|
||||
description: "PostgreSQL database name"
|
||||
required: true
|
||||
database-host:
|
||||
description: "Database host"
|
||||
required: true
|
||||
push-image:
|
||||
description: "Whether to push the built image"
|
||||
required: false
|
||||
default: "false"
|
||||
use-as-latest:
|
||||
description: "Publish as latest"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Log in to the Docker Hub registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ inputs.dockerhub-username }}
|
||||
password: ${{ inputs.dockerhub-token }}
|
||||
logout: true
|
||||
|
||||
- name: Log in to the Github Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ inputs.github-token }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
docker.io/calendso/calendso
|
||||
docker.io/calcom/cal.diy
|
||||
ghcr.io/calcom/cal.diy
|
||||
flavor: |
|
||||
latest=${{ !github.event.release.prerelease && inputs.use-as-latest == 'true' }}
|
||||
suffix=${{ inputs.platform-suffix }}
|
||||
|
||||
- name: Copy env
|
||||
shell: bash
|
||||
run: |
|
||||
grep -o '^[^#]*' .env.example > .env
|
||||
cat .env >> $GITHUB_ENV
|
||||
echo "DATABASE_HOST=localhost:5432" >> $GITHUB_ENV
|
||||
eval $(sed -e '/^#/d' -e 's/^/export /' -e 's/$/;/' .env) ;
|
||||
|
||||
- name: Start database
|
||||
shell: bash
|
||||
run: |
|
||||
docker compose up -d database
|
||||
|
||||
- name: Show database logs and container status
|
||||
shell: bash
|
||||
run: |
|
||||
echo "--- Container Status ---"
|
||||
docker compose ps database
|
||||
|
||||
echo "--- Container Logs ---"
|
||||
docker compose logs database
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: |
|
||||
network=container:database
|
||||
buildkitd-flags: |
|
||||
--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host
|
||||
|
||||
- name: Build image
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
load: true
|
||||
push: false
|
||||
platforms: ${{ inputs.platform }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_V2_URL=http://localhost:5555/api/v2
|
||||
NEXT_PUBLIC_LICENSE_CONSENT=agree
|
||||
DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||
DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||
|
||||
- name: Test runtime
|
||||
shell: bash
|
||||
run: |
|
||||
tags="${{ steps.meta.outputs.tags }}"
|
||||
IFS=',' read -ra ADDR <<< "$tags"
|
||||
tag=${ADDR[0]}
|
||||
|
||||
docker run --rm --network stack \
|
||||
-p 3000:3000 \
|
||||
-e DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@database/${{ inputs.postgres-db }} \
|
||||
-e DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@database/${{ inputs.postgres-db }} \
|
||||
-e NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }} \
|
||||
-e CALENDSO_ENCRYPTION_KEY=${{ env.CALENDSO_ENCRYPTION_KEY }} \
|
||||
$tag &
|
||||
|
||||
server_pid=$!
|
||||
|
||||
echo "Waiting for the server to start..."
|
||||
sleep 120
|
||||
|
||||
echo http://localhost:3000/auth/login
|
||||
|
||||
for i in {1..60}; do
|
||||
echo "Checking server health ($i/60)..."
|
||||
response=$(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/auth/login)
|
||||
echo "HTTP Status Code: $response"
|
||||
if [[ "$response" == "200" ]] || [[ "$response" == "307" ]]; then
|
||||
echo "Server is healthy"
|
||||
kill $server_pid
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Server health check failed"
|
||||
kill $server_pid
|
||||
exit 1
|
||||
env:
|
||||
NEXTAUTH_SECRET: "EI4qqDpcfdvf4A+0aQEEx8JjHxHSy4uWiZw/F32K+pA="
|
||||
CALENDSO_ENCRYPTION_KEY: "0zfLtY99wjeLnsM7qsa8xsT+Q0oSgnOL"
|
||||
|
||||
- name: Push image
|
||||
id: docker_push
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ inputs.push-image == 'true' && !github.event.release.prerelease }}
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: ${{ inputs.platform }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_V2_URL=http://localhost:5555/api/v2
|
||||
NEXT_PUBLIC_LICENSE_CONSENT=agree
|
||||
DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||
DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||
|
||||
- name: Image digest
|
||||
shell: bash
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
- name: Cleanup
|
||||
shell: bash
|
||||
if: always()
|
||||
run: |
|
||||
docker compose down
|
||||
@@ -15,16 +15,12 @@ inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: v20.x
|
||||
skip-install-if-cache-hit:
|
||||
description: "Skip yarn install if node_modules cache is hit. Use this for jobs that only need to check that cache exists."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Use Node ${{ inputs.node_version }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
- name: Expose yarn config as "$GITHUB_OUTPUT"
|
||||
@@ -33,57 +29,10 @@ runs:
|
||||
run: |
|
||||
echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
# When skip-install-if-cache-hit is true, first check if all caches exist without downloading (lookup-only mode)
|
||||
# This avoids downloading ~1.2GB of cache data when we just want to verify caches exist
|
||||
- name: Check yarn cache (lookup-only)
|
||||
if: ${{ inputs.skip-install-if-cache-hit == 'true' }}
|
||||
uses: actions/cache/restore@v4
|
||||
id: yarn-download-cache-check
|
||||
with:
|
||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||
key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Check node_modules cache (lookup-only)
|
||||
if: ${{ inputs.skip-install-if-cache-hit == 'true' }}
|
||||
uses: actions/cache/restore@v4
|
||||
id: yarn-nm-cache-check
|
||||
with:
|
||||
path: |
|
||||
**/node_modules/
|
||||
!**/.next/node_modules/
|
||||
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Check yarn install state cache (lookup-only)
|
||||
if: ${{ inputs.skip-install-if-cache-hit == 'true' }}
|
||||
uses: actions/cache/restore@v4
|
||||
id: yarn-install-state-cache-check
|
||||
with:
|
||||
path: .yarn/ci-cache/
|
||||
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
lookup-only: true
|
||||
|
||||
# Compute whether all caches hit (only in skip mode)
|
||||
- name: Check if all caches hit
|
||||
if: ${{ inputs.skip-install-if-cache-hit == 'true' }}
|
||||
id: all-caches-check
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ steps.yarn-download-cache-check.outputs.cache-hit }}" == "true" && \
|
||||
"${{ steps.yarn-nm-cache-check.outputs.cache-hit }}" == "true" && \
|
||||
"${{ steps.yarn-install-state-cache-check.outputs.cache-hit }}" == "true" ]]; then
|
||||
echo "all-hit=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "all-hit=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Yarn rotates the downloaded cache archives, @see https://github.com/actions/setup-node/issues/325
|
||||
# Yarn cache is also reusable between arch and os.
|
||||
# Only restore if not in skip mode, or if skip mode but any cache check missed
|
||||
- name: Restore yarn cache
|
||||
if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }}
|
||||
uses: actions/cache@v4
|
||||
uses: buildjet/cache@v4
|
||||
id: yarn-download-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
|
||||
@@ -91,28 +40,25 @@ runs:
|
||||
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore node_modules
|
||||
if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }}
|
||||
id: yarn-nm-cache
|
||||
uses: actions/cache@v4
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: |
|
||||
**/node_modules/
|
||||
!**/.next/node_modules/
|
||||
path: "**/node_modules/"
|
||||
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore yarn install state
|
||||
if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }}
|
||||
id: yarn-install-state-cache
|
||||
uses: actions/cache@v4
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: .yarn/ci-cache/
|
||||
key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }}
|
||||
shell: bash
|
||||
run: yarn install --inline-builds
|
||||
run: |
|
||||
yarn install --inline-builds
|
||||
yarn prisma generate
|
||||
env:
|
||||
# CI optimizations. Overrides yarnrc.yml options (or their defaults) in the CI action.
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
name: Install playwright binaries
|
||||
description: "Install playwright, cache and restore if necessary"
|
||||
inputs:
|
||||
skip-install-if-cache-hit:
|
||||
description: "Skip playwright install if cache is hit. Use this for jobs that only need to check that cache exists."
|
||||
required: false
|
||||
default: "false"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -12,35 +7,15 @@ runs:
|
||||
shell: bash
|
||||
id: playwright-version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
|
||||
|
||||
# When skip-install-if-cache-hit is true, first check if cache exists without downloading (lookup-only mode)
|
||||
- name: Check playwright cache (lookup-only)
|
||||
if: ${{ inputs.skip-install-if-cache-hit == 'true' }}
|
||||
id: playwright-cache-check
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
lookup-only: true
|
||||
|
||||
# Only restore cache if not in skip mode, or if skip mode but cache check missed
|
||||
- name: Cache playwright binaries
|
||||
if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.playwright-cache-check.outputs.cache-hit != 'true' }}
|
||||
id: playwright-cache
|
||||
uses: actions/cache@v4
|
||||
uses: buildjet/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
|
||||
# In default mode: run if playwright-cache missed
|
||||
# In skip mode: run if playwright-cache-check missed (but cache restore step was also skipped)
|
||||
- name: Yarn playwright install
|
||||
if: ${{ (inputs.skip-install-if-cache-hit != 'true' && steps.playwright-cache.outputs.cache-hit != 'true') || (inputs.skip-install-if-cache-hit == 'true' && steps.playwright-cache-check.outputs.cache-hit != 'true') }}
|
||||
shell: bash
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,9 @@ jobs:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
api-v2-unit-test:
|
||||
name: Tests
|
||||
uses: ./.github/workflows/api-v2-unit-tests.yml
|
||||
build-api-v1:
|
||||
name: Production builds
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v2:
|
||||
@@ -46,38 +46,38 @@ jobs:
|
||||
|
||||
integration-test:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v2]
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/integration-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v2]
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v2]
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v2]
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: Tests
|
||||
needs: [lint, build, build-api-v2]
|
||||
needs: [lint, build, build-api-v1, build-api-v2]
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, type-check, unit-test, api-v2-unit-test, integration-test, build, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
name: Production Builds
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build API v1
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- name: Cache API v1 production build
|
||||
uses: buildjet/cache@v4
|
||||
id: cache-api-v1-build
|
||||
env:
|
||||
cache-name: api-v1-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('apps/api/v1/**.[jt]s', 'apps/api/v1/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
# Ensures production-build.yml will always be fresh
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/api/v1/.next
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-api-v1-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for API v1 build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-api-v1-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
shell: bash
|
||||
@@ -6,19 +6,17 @@ on:
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build API v2
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -35,15 +33,31 @@ jobs:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Generate Prisma schemas
|
||||
working-directory: apps/api/v2
|
||||
run: yarn workspace @calcom/api-v2 run generate-schemas
|
||||
- name: Build API v2 with Turbo
|
||||
- name: Cache API v2 production build
|
||||
uses: buildjet/cache@v4
|
||||
id: cache-api-v2-build
|
||||
env:
|
||||
cache-name: api-v2-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('apps/api/v2/**.[jt]s', 'apps/api/v2/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
# Ensures production-build.yml will always be fresh
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for API v2 build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn turbo run build --filter=@calcom/api-v2
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
shell: bash
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: API v2 Unit
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
LINGO_DOT_DEV_API_KEY: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
name: API v2 Unit
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- run: yarn prisma generate
|
||||
- name: Run API v2 unit tests
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn workspace @calcom/platform-libraries build && yarn test
|
||||
@@ -8,30 +8,34 @@ jobs:
|
||||
name: Build Atoms
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Cache atoms production build
|
||||
uses: actions/cache@v4
|
||||
uses: buildjet/cache@v4
|
||||
id: cache-atoms-build
|
||||
env:
|
||||
cache-name: atoms-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', 'packages/platform/atoms/package.json', '!**/node_modules') }}
|
||||
key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
# Ensures production-build.yml will always be fresh
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
**/dist/**
|
||||
key: ${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-atoms-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for Atoms build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-atoms-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
rm -rf packages/platform/atoms/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/atoms run build-npm
|
||||
yarn workspace @calcom/atoms run build
|
||||
shell: bash
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -6,31 +6,23 @@ on:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
permissions:
|
||||
id-token: write # for NPM provenance - developers can verify where and how it was built
|
||||
contents: write # to create release by changesets/action below
|
||||
pull-requests: write # to create pull request by changesets/action below
|
||||
|
||||
permissions: {} # remove permissions and set explicitly under "permissions:" below
|
||||
jobs:
|
||||
release:
|
||||
if: github.repository == 'calcom/cal.diy' # prevent this action from running on forks
|
||||
if: github.repository == 'calcom/cal.com' # prevent this action from running on forks
|
||||
permissions:
|
||||
id-token: write # for NPM provenance - developers can verify where and how it was built
|
||||
contents: write # to create release by changesets/action below
|
||||
pull-requests: write # to create pull request by changesets/action below
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node and registry for OIDC
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.17.0"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
always-auth: false
|
||||
- name: Setup node, yarn and install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Install npm at version supporting OIDC
|
||||
run: npm install -g npm@11.5.1
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
@@ -41,7 +33,8 @@ jobs:
|
||||
title: "chore: version packages" # PR title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
VITE_BOOKER_EMBED_OAUTH_CLIENT_ID: ${{ secrets.VITE_BOOKER_EMBED_OAUTH_CLIENT_ID }}
|
||||
VITE_BOOKER_EMBED_API_URL: ${{ secrets.VITE_BOOKER_EMBED_API_URL }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Check Prisma Migrations
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
|
||||
jobs:
|
||||
check-prisma-migrations:
|
||||
name: Check migrations match schema
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Check if migrations match schema
|
||||
working-directory: packages/prisma
|
||||
run: |
|
||||
npx prisma migrate diff --exit-code \
|
||||
--from-migrations ./migrations \
|
||||
--to-schema-datamodel ./schema.prisma \
|
||||
--shadow-database-url ${{ env.DATABASE_URL }}
|
||||
@@ -2,19 +2,15 @@ name: Check types
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=12288
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
check-types:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Show info
|
||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||
|
||||
@@ -11,20 +11,12 @@ jobs:
|
||||
cleanup-report:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94
|
||||
with:
|
||||
app-id: ${{ secrets.CI_CAL_APP_ID }}
|
||||
private-key: ${{ secrets.CI_CAL_APP_PRIVATE_KEY }}
|
||||
repositories: 'test-results-2'
|
||||
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: calcom/test-results-2
|
||||
repository: calcom/test-results
|
||||
ref: gh-pages
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
- name: Set Git User
|
||||
run: |
|
||||
@@ -34,13 +26,12 @@ jobs:
|
||||
- name: Cleanup HTML reports on PR close/merge
|
||||
env:
|
||||
BRANCH_REPORTS_DIR: reports/${{ github.event.pull_request.head.ref }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
if [ -d "$BRANCH_REPORTS_DIR" ]; then
|
||||
rm -rf "$BRANCH_REPORTS_DIR"
|
||||
git add .
|
||||
git commit -m "workflow: remove all reports for branch $HEAD_REF (PR closed/merged)"
|
||||
git commit -m "workflow: remove all reports for branch ${{ github.event.pull_request.head.ref }} (PR closed/merged)"
|
||||
git push
|
||||
else
|
||||
echo "No reports found for branch $HEAD_REF. Nothing to clean up."
|
||||
echo "No reports found for branch ${{ github.event.pull_request.head.ref }}. Nothing to clean up."
|
||||
fi
|
||||
|
||||
@@ -22,20 +22,12 @@ jobs:
|
||||
# Contains all reports for deleted branch
|
||||
BRANCH_REPORTS_DIR: reports/${{ github.event.ref }}
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94
|
||||
with:
|
||||
app-id: ${{ secrets.CI_CAL_APP_ID }}
|
||||
private-key: ${{ secrets.CI_CAL_APP_PRIVATE_KEY }}
|
||||
repositories: 'test-results-2'
|
||||
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: calcom/test-results-2
|
||||
repository: calcom/test-results
|
||||
ref: gh-pages
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Set Git User
|
||||
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||
run: |
|
||||
|
||||
@@ -9,25 +9,15 @@ on:
|
||||
jobs:
|
||||
cron-scheduleSMSReminders:
|
||||
env:
|
||||
APP_URL_US: ${{ vars.APP_URL_US }}
|
||||
APP_URL_EU: ${{ vars.APP_URL_EU }}
|
||||
CRON_API_KEY_US: ${{ secrets.CRON_API_KEY }}
|
||||
CRON_API_KEY_EU: ${{ secrets.CRON_API_KEY_EU }}
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request EU
|
||||
if: ${{ env.APP_URL_EU && env.CRON_API_KEY_EU }}
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_API_KEY }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL_EU }}/api/cron/workflows/scheduleSMSReminders \
|
||||
curl ${{ secrets.APP_URL }}/api/cron/workflows/scheduleSMSReminders \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ env.CRON_API_KEY_EU }}' \
|
||||
-sSf
|
||||
- name: cURL request US
|
||||
if: ${{ env.APP_URL_US && env.CRON_API_KEY_US }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL_US }}/api/cron/workflows/scheduleSMSReminders \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ env.CRON_API_KEY_US }}' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
-sSf
|
||||
|
||||
@@ -9,25 +9,15 @@ on:
|
||||
jobs:
|
||||
cron-scheduleWhatsappReminders:
|
||||
env:
|
||||
APP_URL_EU: ${{ vars.APP_URL_EU }}
|
||||
APP_URL_US: ${{ vars.APP_URL_US }}
|
||||
CRON_API_KEY_EU: ${{ secrets.CRON_API_KEY_EU }}
|
||||
CRON_API_KEY_US: ${{ secrets.CRON_API_KEY }}
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request EU
|
||||
if: ${{ env.APP_URL_EU && env.CRON_API_KEY_EU }}
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_API_KEY }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL_EU }}/api/cron/workflows/scheduleWhatsappReminders \
|
||||
curl ${{ secrets.APP_URL }}/api/cron/workflows/scheduleWhatsappReminders \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ env.CRON_API_KEY_EU }}' \
|
||||
-sSf
|
||||
- name: cURL request US
|
||||
if: ${{ env.APP_URL_US && env.CRON_API_KEY_US }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL_US }}/api/cron/workflows/scheduleWhatsappReminders \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ env.CRON_API_KEY_US }}' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
-sSf
|
||||
|
||||
@@ -20,8 +20,8 @@ jobs:
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 7
|
||||
days-before-pr-close: 14
|
||||
stale-pr-message: "This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active."
|
||||
close-pr-message: "This PR has been closed due to inactivity. Please feel free to reopen it if you'd like to continue the work."
|
||||
days-before-pr-stale: 14
|
||||
days-before-pr-close: -1
|
||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||
operations-per-run: 100
|
||||
|
||||
@@ -9,25 +9,15 @@ on:
|
||||
jobs:
|
||||
cron-webhookTriggers:
|
||||
env:
|
||||
APP_URL_US: ${{ vars.APP_URL_US }}
|
||||
APP_URL_EU: ${{ vars.APP_URL_EU }}
|
||||
CRON_API_KEY_US: ${{ secrets.CRON_API_KEY }}
|
||||
CRON_API_KEY_EU: ${{ secrets.CRON_API_KEY_EU }}
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request US
|
||||
if: ${{ env.APP_URL_US && env.CRON_API_KEY_US }}
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_API_KEY }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL_US }}/api/cron/webhookTriggers \
|
||||
curl ${{ secrets.APP_URL }}/api/cron/webhookTriggers \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ env.CRON_API_KEY_US }}' \
|
||||
-sSf
|
||||
- name: cURL request EU
|
||||
if: ${{ env.APP_URL_EU && env.CRON_API_KEY_EU }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL_EU }}/api/cron/webhookTriggers \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ env.CRON_API_KEY_EU }}' \
|
||||
-sSf
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Manually Delete BuildJet Cache
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
cache_key:
|
||||
description: "BuildJet Cache Key to Delete"
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
manually-delete-buildjet-cache:
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: buildjet/cache-delete@v1
|
||||
with:
|
||||
cache_key: ${{ inputs.cache_key }}
|
||||
@@ -0,0 +1,42 @@
|
||||
# This is just to test this file
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docs
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- name: Cache Docs build
|
||||
uses: buildjet/cache@v4
|
||||
id: cache-docs-build
|
||||
env:
|
||||
cache-name: docs-build
|
||||
key-1: ${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('docs/**.*', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
key-4: ${{ github.sha }}
|
||||
with:
|
||||
path: |
|
||||
**/docs/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
# Log cache hit
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-docs-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for Docs build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-docs-build.outputs.cache-hit != 'true'
|
||||
working-directory: docs
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
npm install -g mintlify@4.2.87
|
||||
mintlify dev &
|
||||
sleep 5 # Let it run for 5 seconds
|
||||
kill $!
|
||||
shell: bash
|
||||
@@ -17,20 +17,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94
|
||||
with:
|
||||
app-id: ${{ secrets.CI_CAL_APP_ID }}
|
||||
private-key: ${{ secrets.CI_CAL_APP_PRIVATE_KEY }}
|
||||
repositories: 'cal.com'
|
||||
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
@@ -55,59 +48,19 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
# 1. Generate Prisma Client
|
||||
# This is required so Trigger.dev can find the database types during bundling
|
||||
- name: Generate Prisma Client
|
||||
run: yarn prisma generate
|
||||
|
||||
# 2. Deploy Trigger.dev tasks
|
||||
# We use --skip-promotion to get a version without making it live yet
|
||||
- name: Deploy Trigger.dev
|
||||
id: deploy-trigger
|
||||
env:
|
||||
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
|
||||
TRIGGER_DEV_PROJECT_REF: ${{ secrets.TRIGGER_DEV_PROJECT_REF }}
|
||||
run: |
|
||||
# The CLI outputs the version; id allows us to access outputs.deploymentVersion
|
||||
cd packages/features
|
||||
yarn deploy:trigger:ci
|
||||
|
||||
- name: Bump version
|
||||
env:
|
||||
VERSION: ${{ inputs.next_version }}
|
||||
run: |
|
||||
cd apps/web
|
||||
yarn version "$VERSION"
|
||||
yarn version ${{ inputs.next_version }}
|
||||
|
||||
# 3. Update .env.production files
|
||||
# We inject the Trigger version into the apps so they stay in sync
|
||||
- name: Update Env Files
|
||||
run: |
|
||||
VERSION="${{ steps.deploy-trigger.outputs.deploymentVersion }}"
|
||||
|
||||
# Create the TypeScript content
|
||||
CONTENT="export const TRIGGER_VERSION = '$VERSION';"
|
||||
|
||||
# Write to the specific paths
|
||||
echo "$CONTENT" > apps/web/trigger.version.ts
|
||||
echo "$CONTENT" > apps/api/v2/trigger.version.ts
|
||||
|
||||
# Force add to git since these are likely in .gitignore
|
||||
git add -f apps/web/trigger.version.ts apps/api/v2/trigger.version.ts
|
||||
|
||||
# 4. Commit everything together
|
||||
- name: Commit changes
|
||||
env:
|
||||
VERSION: ${{ inputs.next_version }}
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
run: |
|
||||
gh auth setup-git
|
||||
git add .
|
||||
git commit -m "chore: release v$VERSION"
|
||||
git commit -m "chore: release v${{ inputs.next_version }}"
|
||||
git push
|
||||
|
||||
- name: Draft release
|
||||
run: gh release create v$VERSION --generate-notes --draft
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ inputs.next_version }}
|
||||
|
||||
@@ -27,16 +27,14 @@ env:
|
||||
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
|
||||
JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
|
||||
NODE_ENV: ${{ vars.CI_NODE_ENV }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
name: E2E API v2 (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E API v2
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -45,11 +43,10 @@ jobs:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd "pg_isready -h 127.0.0.1 -U postgres"
|
||||
--health-interval 2s
|
||||
--health-timeout 3s
|
||||
--health-retries 15
|
||||
--health-start-period 2s
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
redis:
|
||||
@@ -66,42 +63,38 @@ jobs:
|
||||
--health-retries 5
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- name: Build platform packages with Turbo
|
||||
run: |
|
||||
yarn turbo run build \
|
||||
--filter=@calcom/platform-constants \
|
||||
--filter=@calcom/platform-enums \
|
||||
--filter=@calcom/platform-utils \
|
||||
--filter=@calcom/platform-types \
|
||||
--filter=@calcom/platform-libraries \
|
||||
--filter=@calcom/trpc
|
||||
|
||||
- name: Generate Swagger
|
||||
working-directory: apps/api/v2
|
||||
run: yarn generate-swagger
|
||||
|
||||
- name: Check breaking changes
|
||||
uses: oasdiff/oasdiff-action/breaking@main
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json
|
||||
revision: docs/api-reference/v2/openapi.json
|
||||
fail-on: ERR
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
yarn test:e2e:ci --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
yarn workspace @calcom/platform-libraries build && yarn test:e2e
|
||||
EXIT_CODE=$?
|
||||
echo "yarn test:e2e:ci --shard=${{ matrix.shard }}/${{ strategy.job-total }} command exit code: $EXIT_CODE"
|
||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
|
||||
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-api-v2-shard-${{ matrix.shard }}
|
||||
path: apps/api/v2/test-results
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
name: test-results-api-v2
|
||||
path: test-results
|
||||
|
||||
@@ -45,10 +45,10 @@ jobs:
|
||||
e2e-app-store:
|
||||
timeout-minutes: 20
|
||||
name: E2E App Store
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -57,11 +57,10 @@ jobs:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd "pg_isready -h 127.0.0.1 -U postgres"
|
||||
--health-interval 2s
|
||||
--health-timeout 3s
|
||||
--health-retries 15
|
||||
--health-start-period 2s
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
@@ -81,12 +80,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- run: yarn prisma generate
|
||||
- uses: ./.github/actions/cache-db
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
@@ -97,7 +93,7 @@ jobs:
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn e2e:app-store --workers=4
|
||||
run: yarn e2e:app-store
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -22,16 +22,14 @@ jobs:
|
||||
e2e-atoms:
|
||||
timeout-minutes: 15
|
||||
name: E2E Atoms
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- name: Run E2E Atoms Tests
|
||||
|
||||
@@ -45,10 +45,10 @@ jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
name: E2E Embed React
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -57,11 +57,10 @@ jobs:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd "pg_isready -h 127.0.0.1 -U postgres"
|
||||
--health-interval 2s
|
||||
--health-timeout 3s
|
||||
--health-retries 15
|
||||
--health-start-period 2s
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
@@ -73,17 +72,14 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- run: yarn prisma generate
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: |
|
||||
yarn e2e:embed-react --workers=4
|
||||
yarn e2e:embed-react
|
||||
yarn workspace @calcom/embed-react packaged:tests
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
|
||||
@@ -45,10 +45,10 @@ jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
name: E2E Embed Core
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -57,11 +57,10 @@ jobs:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd "pg_isready -h 127.0.0.1 -U postgres"
|
||||
--health-interval 2s
|
||||
--health-timeout 3s
|
||||
--health-retries 15
|
||||
--health-start-period 2s
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
@@ -81,16 +80,13 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- run: yarn prisma generate
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn e2e:embed --workers=4
|
||||
run: yarn e2e:embed
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
name: E2E Report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR Update"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'The PR number to generate report for'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
get-pr-info:
|
||||
name: Get PR Info
|
||||
runs-on: ubuntu-latest
|
||||
# For workflow_run: only run if the triggering workflow failed
|
||||
# For workflow_dispatch: always run (inputs are provided manually)
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'failure' }}
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
pr-number: ${{ steps.get-info.outputs.pr-number }}
|
||||
head-branch: ${{ steps.get-info.outputs.head-branch }}
|
||||
has-blob-reports: ${{ steps.get-info.outputs.has-blob-reports }}
|
||||
source-run-id: ${{ steps.get-info.outputs.source-run-id }}
|
||||
source-run-number: ${{ steps.get-info.outputs.source-run-number }}
|
||||
source-run-attempt: ${{ steps.get-info.outputs.source-run-attempt }}
|
||||
source-head-sha: ${{ steps.get-info.outputs.source-head-sha }}
|
||||
steps:
|
||||
- name: Get PR and run info
|
||||
id: get-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const isDispatch = context.eventName === 'workflow_dispatch';
|
||||
|
||||
if (isDispatch) {
|
||||
// For workflow_dispatch: look up everything from PR number
|
||||
const prNumber = context.payload.inputs.pr_number;
|
||||
console.log('Looking up PR:', prNumber);
|
||||
|
||||
// Get PR details
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: parseInt(prNumber)
|
||||
});
|
||||
|
||||
const headSha = pr.head.sha;
|
||||
const headBranch = pr.head.ref;
|
||||
console.log('PR head SHA:', headSha);
|
||||
console.log('PR head branch:', headBranch);
|
||||
|
||||
// Find the "PR Update" workflow
|
||||
const { data: workflows } = await github.rest.actions.listRepoWorkflows({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const prUpdateWorkflow = workflows.workflows.find(w => w.name === 'PR Update');
|
||||
if (!prUpdateWorkflow) {
|
||||
core.setFailed('Could not find "PR Update" workflow');
|
||||
return;
|
||||
}
|
||||
console.log('Found PR Update workflow ID:', prUpdateWorkflow.id);
|
||||
|
||||
// Find the most recent completed run for this branch with blob-report artifacts
|
||||
// We search by branch instead of SHA to handle cases where:
|
||||
// 1. The PR was updated after E2E tests ran (artifacts exist for older SHA)
|
||||
// 2. The current run for the SHA is still queued/in-progress
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: prUpdateWorkflow.id,
|
||||
branch: headBranch,
|
||||
status: 'completed',
|
||||
per_page: 20
|
||||
});
|
||||
|
||||
console.log('Found', runs.workflow_runs.length, 'completed runs for branch', headBranch);
|
||||
|
||||
// Find a run with blob-report artifacts
|
||||
let selectedRun = null;
|
||||
let hasBlobReports = false;
|
||||
|
||||
for (const run of runs.workflow_runs) {
|
||||
console.log('Checking run', run.id, 'conclusion:', run.conclusion);
|
||||
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: run.id
|
||||
});
|
||||
|
||||
const blobReports = artifacts.artifacts.filter(a => a.name.startsWith('blob-report-'));
|
||||
if (blobReports.length > 0) {
|
||||
console.log('Found run with blob reports:', run.id, 'artifacts:', blobReports.map(a => a.name));
|
||||
selectedRun = run;
|
||||
hasBlobReports = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedRun) {
|
||||
console.log('No runs found with blob-report artifacts');
|
||||
core.setOutput('pr-number', prNumber);
|
||||
core.setOutput('head-branch', headBranch);
|
||||
core.setOutput('source-head-sha', headSha);
|
||||
core.setOutput('has-blob-reports', 'false');
|
||||
core.setOutput('source-run-id', '');
|
||||
core.setOutput('source-run-number', '');
|
||||
core.setOutput('source-run-attempt', '');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('pr-number', prNumber);
|
||||
core.setOutput('head-branch', headBranch);
|
||||
core.setOutput('source-head-sha', headSha);
|
||||
core.setOutput('source-run-id', selectedRun.id.toString());
|
||||
core.setOutput('source-run-number', selectedRun.run_number.toString());
|
||||
core.setOutput('source-run-attempt', selectedRun.run_attempt.toString());
|
||||
core.setOutput('has-blob-reports', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
// For workflow_run: extract from payload
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
const headSha = workflowRun.head_sha;
|
||||
const headBranch = workflowRun.head_branch;
|
||||
|
||||
console.log('Head SHA:', headSha);
|
||||
console.log('Head Branch:', headBranch);
|
||||
|
||||
// Set source run info
|
||||
core.setOutput('source-run-id', workflowRun.id.toString());
|
||||
core.setOutput('source-run-number', workflowRun.run_number.toString());
|
||||
core.setOutput('source-run-attempt', workflowRun.run_attempt.toString());
|
||||
core.setOutput('source-head-sha', headSha);
|
||||
|
||||
// Check for blob report artifacts
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: workflowRun.id
|
||||
});
|
||||
|
||||
const blobReports = artifacts.artifacts.filter(a => a.name.startsWith('blob-report-'));
|
||||
console.log('Found blob reports:', blobReports.map(a => a.name));
|
||||
core.setOutput('has-blob-reports', blobReports.length > 0 ? 'true' : 'false');
|
||||
|
||||
// Try to get PR number from workflow_run payload first
|
||||
if (workflowRun.pull_requests && workflowRun.pull_requests.length > 0) {
|
||||
const prNumber = workflowRun.pull_requests[0].number;
|
||||
console.log('PR number from payload:', prNumber);
|
||||
core.setOutput('pr-number', prNumber.toString());
|
||||
core.setOutput('head-branch', headBranch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to API lookup
|
||||
try {
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: headSha
|
||||
});
|
||||
|
||||
if (prs.length === 0) {
|
||||
console.log('No PRs found for commit');
|
||||
core.setOutput('pr-number', '');
|
||||
core.setOutput('head-branch', headBranch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find open PR or use first one
|
||||
const openPr = prs.find(pr => pr.state === 'open') || prs[0];
|
||||
console.log('PR number from API:', openPr.number);
|
||||
core.setOutput('pr-number', openPr.number.toString());
|
||||
core.setOutput('head-branch', openPr.head.ref);
|
||||
} catch (e) {
|
||||
console.log('Error fetching PR:', e);
|
||||
core.setOutput('pr-number', '');
|
||||
core.setOutput('head-branch', headBranch);
|
||||
}
|
||||
|
||||
merge-reports:
|
||||
name: Merge reports
|
||||
needs: [get-pr-info]
|
||||
if: ${{ needs.get-pr-info.outputs.has-blob-reports == 'true' && needs.get-pr-info.outputs.pr-number != '' }}
|
||||
uses: ./.github/workflows/merge-reports.yml
|
||||
with:
|
||||
source_run_id: ${{ needs.get-pr-info.outputs.source-run-id }}
|
||||
source_run_number: ${{ needs.get-pr-info.outputs.source-run-number }}
|
||||
source_run_attempt: ${{ needs.get-pr-info.outputs.source-run-attempt }}
|
||||
source_head_sha: ${{ needs.get-pr-info.outputs.source-head-sha }}
|
||||
# No secrets: inherit - merge-reports only needs the default GITHUB_TOKEN for artifact download
|
||||
|
||||
publish-report:
|
||||
name: Publish HTML report
|
||||
needs: [get-pr-info, merge-reports]
|
||||
if: ${{ needs.get-pr-info.outputs.pr-number != '' }}
|
||||
uses: ./.github/workflows/publish-report.yml
|
||||
with:
|
||||
head_branch: ${{ needs.get-pr-info.outputs.head-branch }}
|
||||
source_run_id: ${{ needs.get-pr-info.outputs.source-run-id }}
|
||||
source_run_number: ${{ needs.get-pr-info.outputs.source-run-number }}
|
||||
source_run_attempt: ${{ needs.get-pr-info.outputs.source-run-attempt }}
|
||||
pr_number: ${{ needs.get-pr-info.outputs.pr-number }}
|
||||
secrets: inherit
|
||||
+11
-15
@@ -5,6 +5,7 @@ permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
@@ -14,11 +15,11 @@ env:
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
@@ -29,7 +30,6 @@ env:
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
||||
@@ -45,10 +45,10 @@ jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
name: E2E (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -57,11 +57,10 @@ jobs:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd "pg_isready -h 127.0.0.1 -U postgres"
|
||||
--health-interval 2s
|
||||
--health-timeout 3s
|
||||
--health-retries 15
|
||||
--health-start-period 2s
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
@@ -75,23 +74,20 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- run: yarn prisma generate
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }} --workers=4
|
||||
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
|
||||
@@ -10,30 +10,20 @@ concurrency:
|
||||
jobs:
|
||||
i18n:
|
||||
name: Run i18n
|
||||
if: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94
|
||||
with:
|
||||
app-id: ${{ secrets.CI_CAL_APP_ID }}
|
||||
private-key: ${{ secrets.CI_CAL_APP_PRIVATE_KEY }}
|
||||
repositories: 'cal.com'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
- uses: lingodotdev/lingo.dev@cee3eb86550ca4905fc99fe90786fb7523288a61 # main
|
||||
- uses: lingodotdev/lingo.dev@main
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
with:
|
||||
api-key: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}
|
||||
pull-request: true
|
||||
parallel: true
|
||||
|
||||
@@ -5,7 +5,6 @@ permissions:
|
||||
contents: read
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
INTEGRATION_TESTS: "true"
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
@@ -45,10 +44,10 @@ jobs:
|
||||
integration:
|
||||
timeout-minutes: 20
|
||||
name: Integration
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -79,14 +78,11 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- run: yarn prisma generate
|
||||
- uses: ./.github/actions/cache-db
|
||||
- name: Run Tests
|
||||
run: VITEST_MODE=integration yarn test
|
||||
run: yarn test -- --integrationTestsOnly
|
||||
# TODO: Generate test results so we can upload them
|
||||
# - name: Upload Test Results
|
||||
# if: ${{ always() }}
|
||||
|
||||
@@ -15,6 +15,18 @@ jobs:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
team-labels:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: equitybee/team-label-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "admin, app-store, ai, api, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, self-hosted, sre, teams, webhooks, workflows, zapier"
|
||||
apply-labels-from-issue:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Run Lint
|
||||
run: yarn lint
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,37 +1,17 @@
|
||||
# https://playwright.dev/docs/test-sharding#merging-reports-from-multiple-shards
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
source_run_id:
|
||||
description: 'The run ID to download artifacts from (for workflow_run triggered calls)'
|
||||
required: true
|
||||
type: string
|
||||
source_run_number:
|
||||
description: 'The run number for artifact naming'
|
||||
required: true
|
||||
type: string
|
||||
source_run_attempt:
|
||||
description: 'The run attempt for artifact naming'
|
||||
required: true
|
||||
type: string
|
||||
source_head_sha:
|
||||
description: 'The head SHA to checkout for Playwright version consistency'
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
merge-reports:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.source_head_sha }}
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
@@ -40,6 +20,6 @@ jobs:
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ inputs.source_run_number }}-${{ inputs.source_run_attempt }}
|
||||
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
|
||||
@@ -12,13 +12,13 @@ permissions:
|
||||
jobs:
|
||||
annotate:
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Download base branch bundle stats
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
if: success()
|
||||
with:
|
||||
workflow: nextjs-bundle-analysis.yml
|
||||
|
||||
@@ -16,6 +16,7 @@ env:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
@@ -49,15 +50,12 @@ env:
|
||||
jobs:
|
||||
analyze:
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- run: yarn prisma generate
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Analyze bundle
|
||||
run: |
|
||||
@@ -70,13 +68,3 @@ jobs:
|
||||
with:
|
||||
name: bundle
|
||||
path: apps/web/.next/analyze/__bundle_analysis.json
|
||||
|
||||
- name: Delete cached build
|
||||
continue-on-error: true
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
for key in $(gh actions-cache list -R ${{ github.repository }} --key "prod-build-main-" --limit 100 | cut -f1); do
|
||||
gh actions-cache delete "$key" -R ${{ github.repository }} --confirm
|
||||
done
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -6,27 +6,16 @@ on:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94
|
||||
with:
|
||||
app-id: ${{ secrets.CI_CAL_APP_ID }}
|
||||
private-key: ${{ secrets.CI_CAL_APP_PRIVATE_KEY }}
|
||||
repositories: 'cal.com'
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
name: "PR Welcome Bot"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
name: Welcome new contributors
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Skip PRs from org members and bots
|
||||
if (
|
||||
["MEMBER", "OWNER", "COLLABORATOR"].includes(pr.author_association) ||
|
||||
pr.user.type === "Bot"
|
||||
) {
|
||||
console.log(`Skipping — author association: ${pr.author_association}, type: ${pr.user.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = [
|
||||
`Welcome to **Cal.diy**, @${pr.user.login}! Thanks for opening this pull request.`,
|
||||
"",
|
||||
"A few things to keep in mind:",
|
||||
"",
|
||||
"- **This is Cal.diy, not Cal.com.** Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will **not** be deployed to the Cal.com production app.",
|
||||
"- Please review our [Contributing Guidelines](https://github.com/calcom/cal.diy/blob/main/CONTRIBUTING.md) if you haven't already.",
|
||||
"- Make sure your PR title follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format.",
|
||||
"",
|
||||
"A maintainer will review your PR soon. Thanks for contributing!",
|
||||
].join("\n");
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body,
|
||||
});
|
||||
+108
-325
@@ -1,13 +1,11 @@
|
||||
# ⚠️ SECURITY: Do not add steps that checkout PR code or run local actions before trust-check job completes.
|
||||
name: PR Update
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
branches:
|
||||
- main
|
||||
- gh-actions-test-branch
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -19,224 +17,49 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Security gate: Check if PR is from a trusted contributor or approved via run-ci label
|
||||
# This MUST run before any job that checks out PR code and executes it with secrets
|
||||
trust-check:
|
||||
name: Trust Check
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
actions: read
|
||||
issues: read
|
||||
outputs:
|
||||
is-trusted: ${{ steps.check-trust.outputs.is-trusted }}
|
||||
steps:
|
||||
- name: Check if PR is trusted
|
||||
id: check-trust
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR'];
|
||||
|
||||
if (!context.payload.pull_request) {
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
console.log('workflow_dispatch event - assuming trusted (manual trigger)');
|
||||
core.setOutput('is-trusted', true);
|
||||
return;
|
||||
}
|
||||
console.log('No pull request context found');
|
||||
core.setOutput('is-trusted', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Fetch fresh PR data - payload labels may be stale on re-runs
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
});
|
||||
|
||||
const prNumber = pr.number;
|
||||
const headSha = pr.head.sha;
|
||||
|
||||
async function hasWriteAccess(username) {
|
||||
try {
|
||||
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username,
|
||||
});
|
||||
return ['admin', 'maintain', 'write'].includes(permission.permission);
|
||||
} catch (e) {
|
||||
console.log(`Permission check failed for ${username}: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`PR #${prNumber} by ${pr.user.login} (${pr.author_association})`);
|
||||
|
||||
// Check 1: Is the author a trusted contributor?
|
||||
if (trustedAssociations.includes(pr.author_association)) {
|
||||
console.log(`Author has trusted association: ${pr.author_association}`);
|
||||
core.setOutput('is-trusted', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check 2: Verify write access via API (author_association can be unreliable)
|
||||
if (await hasWriteAccess(pr.user.login)) {
|
||||
console.log(`Author has write access`);
|
||||
core.setOutput('is-trusted', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check 3: Was 'run-ci' label added AFTER this SHA was pushed by someone with write access?
|
||||
// This enables re-runs triggered by the run-ci.yml workflow
|
||||
// NOTE: We use workflow run created_at instead of commit timestamp because
|
||||
// git commit timestamps can be arbitrarily backdated by attackers
|
||||
if (pr.labels?.some(l => l.name === 'run-ci')) {
|
||||
// Skip stale check if this is a re-run (run_attempt > 1)
|
||||
// Re-runs are explicitly triggered by maintainers
|
||||
const runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT || '1', 10);
|
||||
if (runAttempt > 1) {
|
||||
console.log(`Re-run detected (attempt ${runAttempt}), trusting existing 'run-ci' label`);
|
||||
core.setOutput('is-trusted', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const events = await github.paginate(github.rest.issues.listEvents, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const labelEvent = events
|
||||
.filter(e => e.event === 'labeled' && e.label?.name === 'run-ci')
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
|
||||
|
||||
if (labelEvent) {
|
||||
// Get workflow runs to find when this SHA was first pushed
|
||||
const runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'pr.yml',
|
||||
head_sha: headSha,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
// Filter runs to this PR (in case same SHA exists in multiple PRs)
|
||||
const matchingRuns = runs.filter(run =>
|
||||
!run.pull_requests?.length || run.pull_requests.some(p => p.number === prNumber)
|
||||
);
|
||||
|
||||
if (matchingRuns.length > 0) {
|
||||
const labelTime = new Date(labelEvent.created_at);
|
||||
// Use the oldest run's created_at as the push time
|
||||
const originalRun = matchingRuns[matchingRuns.length - 1];
|
||||
const pushTime = new Date(originalRun.created_at);
|
||||
|
||||
if (labelTime > pushTime) {
|
||||
const adder = labelEvent.actor.login;
|
||||
if (await hasWriteAccess(adder)) {
|
||||
console.log(`Approved via 'run-ci' label added by ${adder} after push (label: ${labelTime.toISOString()}, push: ${pushTime.toISOString()})`);
|
||||
core.setOutput('is-trusted', true);
|
||||
return;
|
||||
}
|
||||
console.log(`Label 'run-ci' added by ${adder} (no write access)`);
|
||||
} else {
|
||||
console.log(`Label 'run-ci' is stale (label: ${labelTime.toISOString()}, push: ${pushTime.toISOString()})`);
|
||||
}
|
||||
} else {
|
||||
console.log('No workflow runs found for this SHA - cannot validate label timing');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('External contribution requires "run-ci" label from a maintainer');
|
||||
core.setOutput('is-trusted', false);
|
||||
|
||||
prepare:
|
||||
name: Prepare
|
||||
needs: [trust-check]
|
||||
if: needs.trust-check.outputs.is-trusted == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter-exclusions.outputs.has-files-requiring-all-checks }}
|
||||
has-api-v2-changes: ${{ steps.filter-inclusions.outputs.has-api-v2-changes }}
|
||||
has-prisma-changes: ${{ steps.filter-inclusions.outputs.has-prisma-changes }}
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
commit-sha: ${{ steps.get_sha.outputs.commit-sha }}
|
||||
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' }}
|
||||
db-cache-hit: ${{ steps.cache-db-check.outputs.cache-hit }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- name: Generate DB cache key
|
||||
id: cache-db-key
|
||||
uses: ./.github/actions/cache-db-key
|
||||
- name: Check DB cache (lookup-only)
|
||||
id: cache-db-check
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: backups/backup.sql
|
||||
key: ${{ steps.cache-db-key.outputs.key }}
|
||||
lookup-only: true
|
||||
- name: Check for files requiring all checks (with exclusions)
|
||||
uses: dorny/paths-filter@v3
|
||||
id: filter-exclusions
|
||||
with:
|
||||
predicate-quantifier: "every"
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- '**'
|
||||
- '!.vscode/**'
|
||||
- '!**/*.md'
|
||||
- '!**/*.mdx'
|
||||
- '!.github/CODEOWNERS'
|
||||
- '!docs/**'
|
||||
- '!help/**'
|
||||
- '!packages/i18n/locales/**/common.json'
|
||||
- '!i18n.lock'
|
||||
- name: Check for specific path changes
|
||||
uses: dorny/paths-filter@v3
|
||||
id: filter-inclusions
|
||||
with:
|
||||
filters: |
|
||||
has-api-v2-changes:
|
||||
- "apps/api/v2/**"
|
||||
- "packages/platform-constants/**"
|
||||
- "packages/platform-enums/**"
|
||||
- "packages/platform-utils/**"
|
||||
- "packages/platform-types/**"
|
||||
- "packages/platform-libraries/**"
|
||||
- "packages/trpc/**"
|
||||
- "packages/prisma/schema.prisma"
|
||||
has-prisma-changes:
|
||||
- "packages/prisma/schema.prisma"
|
||||
- "packages/prisma/migrations/**"
|
||||
- "!(**.md|**.mdx|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json|i18n.lock)"
|
||||
- name: Get Latest Commit SHA
|
||||
id: get_sha
|
||||
run: |
|
||||
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
check-label:
|
||||
needs: [changes]
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
name: Check for E2E label
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' && (github.event.action != 'labeled' || github.event.label.name == 'ready-for-e2e') }}
|
||||
steps:
|
||||
- name: Check if PR exists with ready-for-e2e label for this SHA
|
||||
id: check-if-pr-has-label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let labels = [];
|
||||
let prNumber = null;
|
||||
|
||||
if (context.payload.pull_request) {
|
||||
prNumber = context.payload.pull_request.number;
|
||||
labels = context.payload.pull_request.labels;
|
||||
} else {
|
||||
try {
|
||||
const sha = '${{ steps.get_sha.outputs.commit-sha }}';
|
||||
const sha = '${{ needs.changes.outputs.commit-sha }}';
|
||||
console.log('sha', sha);
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
@@ -250,160 +73,125 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
prNumber = prs[0].number;
|
||||
const pr = prs[0];
|
||||
console.log(`PR number: ${pr.number}`);
|
||||
console.log(`PR title: ${pr.title}`);
|
||||
console.log(`PR state: ${pr.state}`);
|
||||
console.log(`PR URL: ${pr.html_url}`);
|
||||
|
||||
labels = pr.labels;
|
||||
}
|
||||
catch (e) {
|
||||
core.setOutput('run-e2e', false);
|
||||
console.log(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Always fetch fresh PR data to get current labels
|
||||
// This avoids stale label data from event payloads
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
console.log(`PR number: ${pr.number}`);
|
||||
console.log(`PR title: ${pr.title}`);
|
||||
console.log(`PR state: ${pr.state}`);
|
||||
console.log(`PR URL: ${pr.html_url}`);
|
||||
|
||||
labels = pr.labels;
|
||||
}
|
||||
catch (e) {
|
||||
core.setOutput('run-e2e', false);
|
||||
console.log(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const labelFound = labels.map(l => l.name).includes('ready-for-e2e');
|
||||
console.log('Found the label?', labelFound);
|
||||
core.setOutput('run-e2e', labelFound);
|
||||
- uses: ./.github/actions/yarn-install
|
||||
if: ${{ steps.filter-exclusions.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
with:
|
||||
skip-install-if-cache-hit: "true"
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
if: ${{ steps.filter-exclusions.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
with:
|
||||
skip-install-if-cache-hit: "true"
|
||||
|
||||
deps:
|
||||
name: Install dependencies
|
||||
needs: [changes, check-label]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/yarn-install.yml
|
||||
|
||||
type-check:
|
||||
name: Type Checks
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
name: Type check
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
unit-test:
|
||||
name: Tests
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
api-v2-unit-test:
|
||||
name: Tests
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v2-unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
security-audit:
|
||||
name: Security Audit
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/security-audit.yml
|
||||
secrets: inherit
|
||||
|
||||
check-prisma-migrations:
|
||||
name: Check Prisma Migrations
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.has-prisma-changes == 'true' }}
|
||||
uses: ./.github/workflows/check-prisma-migrations.yml
|
||||
secrets: inherit
|
||||
|
||||
setup-db:
|
||||
name: Setup Database
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/setup-db.yml
|
||||
with:
|
||||
DB_CACHE_HIT: ${{ needs.prepare.outputs.db-cache-hit }}
|
||||
build-api-v1:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v2:
|
||||
name: Production builds
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v2-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-atoms:
|
||||
name: Production builds
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/atoms-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-docs:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/docs-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
needs: [prepare]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/production-build-without-database.yml
|
||||
secrets: inherit
|
||||
|
||||
integration-test:
|
||||
name: Tests
|
||||
needs: [prepare, build, setup-db]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/integration-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: Tests
|
||||
needs: [prepare, build, setup-db]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-api-v2:
|
||||
name: Tests
|
||||
needs: [prepare, setup-db]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-api-v2.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: Tests
|
||||
needs: [prepare, build, setup-db]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: Tests
|
||||
needs: [prepare, build, setup-db]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed-react:
|
||||
name: Tests
|
||||
needs: [prepare, build, setup-db]
|
||||
if: ${{ needs.prepare.outputs.run-e2e == 'true' && needs.prepare.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, build, build-api-v1, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -413,63 +201,58 @@ jobs:
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
merge-reports:
|
||||
name: Merge reports
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
uses: ./.github/workflows/merge-reports.yml
|
||||
secrets: inherit
|
||||
|
||||
publish-report:
|
||||
name: Publish HTML report
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
needs: [changes, check-label, merge-reports]
|
||||
uses: ./.github/workflows/publish-report.yml
|
||||
secrets: inherit
|
||||
|
||||
cleanup-report:
|
||||
name: Cleanup HTML report
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && (github.event.pull_request.merged == true || github.event.pull_request.state == 'closed') }}
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
needs: [changes, check-label, publish-report]
|
||||
uses: ./.github/workflows/cleanup-report.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs:
|
||||
[
|
||||
trust-check,
|
||||
prepare,
|
||||
changes,
|
||||
lint,
|
||||
type-check,
|
||||
unit-test,
|
||||
api-v2-unit-test,
|
||||
security-audit,
|
||||
check-prisma-migrations,
|
||||
integration-test,
|
||||
check-label,
|
||||
build,
|
||||
build-api-v1,
|
||||
build-api-v2,
|
||||
build-atoms,
|
||||
setup-db,
|
||||
build-docs,
|
||||
e2e,
|
||||
e2e-api-v2,
|
||||
e2e-embed,
|
||||
e2e-embed-react,
|
||||
e2e-app-store,
|
||||
e2e-app-store
|
||||
]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Fail if trust-check did not succeed
|
||||
run: |
|
||||
echo "::error::Trust check did not complete successfully (result: ${{ needs.trust-check.result }}). Please re-run the workflow."
|
||||
exit 1
|
||||
if: needs.trust-check.result != 'success'
|
||||
- name: Fail if PR is not trusted (external contributor without run-ci label)
|
||||
run: |
|
||||
echo "::error::This PR is from an external contributor and requires the 'run-ci' label before CI can run."
|
||||
echo "A maintainer must review the code and add the 'run-ci' label to trigger CI checks."
|
||||
exit 1
|
||||
if: needs.trust-check.outputs.is-trusted != 'true' && needs.trust-check.result == 'success'
|
||||
- name: Fail if conditional jobs failed
|
||||
- name: fail if conditional jobs failed
|
||||
if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
|
||||
run: exit 1
|
||||
if: |
|
||||
(
|
||||
needs.prepare.outputs.has-files-requiring-all-checks == 'true' &&
|
||||
(
|
||||
needs.lint.result != 'success' ||
|
||||
needs.type-check.result != 'success' ||
|
||||
needs.unit-test.result != 'success' ||
|
||||
needs.api-v2-unit-test.result != 'success' ||
|
||||
needs.security-audit.result != 'success' ||
|
||||
(needs.prepare.outputs.has-prisma-changes == 'true' && needs.check-prisma-migrations.result != 'success') ||
|
||||
needs.build.result != 'success' ||
|
||||
needs.build-api-v2.result != 'success' ||
|
||||
needs.build-atoms.result != 'success' ||
|
||||
needs.setup-db.result != 'success' ||
|
||||
needs.integration-test.result != 'success' ||
|
||||
needs.e2e.result != 'success' ||
|
||||
needs.e2e-api-v2.result != 'success' ||
|
||||
needs.e2e-embed.result != 'success' ||
|
||||
needs.e2e-embed-react.result != 'success' ||
|
||||
needs.e2e-app-store.result != 'success'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,13 +4,13 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
@@ -36,42 +36,14 @@ env:
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Web App
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- run: yarn prisma generate
|
||||
- name: Generate cache key
|
||||
id: cache-key
|
||||
uses: ./.github/actions/cache-build-key
|
||||
with:
|
||||
branch_key: ${{ github.head_ref || github.ref_name }}
|
||||
- name: Check if production build cache exists
|
||||
uses: actions/cache@v4
|
||||
id: cache-check
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/web/.next
|
||||
${{ github.workspace }}/apps/web/public/embed
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ steps.cache-key.outputs.key }}
|
||||
lookup-only: true
|
||||
- name: Delete old production build caches for this branch
|
||||
if: steps.cache-check.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
for key in $(gh actions-cache list -R ${{ github.repository }} --key "prod-build-${{ github.head_ref || github.ref_name }}" --limit 100 | cut -f1); do
|
||||
gh actions-cache delete "$key" -R ${{ github.repository }} --confirm
|
||||
done
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
- uses: ./.github/actions/cache-build
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
head_branch:
|
||||
description: 'The head branch name for report path'
|
||||
required: true
|
||||
type: string
|
||||
source_run_id:
|
||||
description: 'The source run ID for report path'
|
||||
required: true
|
||||
type: string
|
||||
source_run_number:
|
||||
description: 'The source run number for artifact naming and comment'
|
||||
required: true
|
||||
type: string
|
||||
source_run_attempt:
|
||||
description: 'The source run attempt for artifact naming and comment'
|
||||
required: true
|
||||
type: string
|
||||
pr_number:
|
||||
description: 'The PR number to post comment on'
|
||||
required: true
|
||||
type: string
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
@@ -31,23 +10,15 @@ jobs:
|
||||
continue-on-error: true
|
||||
env:
|
||||
# Unique URL path for each workflow run attempt
|
||||
HTML_REPORT_URL_PATH: reports/${{ inputs.head_branch }}/${{ inputs.source_run_id }}/${{ inputs.source_run_attempt }}
|
||||
BRANCH_REPORTS_DIR: reports/${{ inputs.head_branch }}
|
||||
HTML_REPORT_URL_PATH: reports/${{ github.head_ref }}/${{ github.run_id }}/${{ github.run_attempt }}
|
||||
BRANCH_REPORTS_DIR: reports/${{ github.head_ref }}
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94
|
||||
with:
|
||||
app-id: ${{ secrets.CI_CAL_APP_ID }}
|
||||
private-key: ${{ secrets.CI_CAL_APP_PRIVATE_KEY }}
|
||||
repositories: 'test-results-2'
|
||||
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: calcom/test-results-2
|
||||
repository: calcom/test-results
|
||||
ref: gh-pages
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Set Git User
|
||||
# see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||
run: |
|
||||
@@ -63,27 +34,22 @@ jobs:
|
||||
- name: Cleanup old HTML reports
|
||||
if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
|
||||
timeout-minutes: 3
|
||||
env:
|
||||
HEAD_BRANCH: ${{ inputs.head_branch }}
|
||||
run: |
|
||||
rm -rf "$BRANCH_REPORTS_DIR"
|
||||
rm -rf $BRANCH_REPORTS_DIR
|
||||
git add .
|
||||
git commit -m "workflow: remove all reports for branch $HEAD_BRANCH"
|
||||
git commit -m "workflow: remove all reports for branch ${{ github.head_ref }}"
|
||||
- name: Download zipped HTML report
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ inputs.source_run_number }}-${{ inputs.source_run_attempt }}
|
||||
name: html-report--attempt-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
path: ${{ env.HTML_REPORT_URL_PATH }}
|
||||
- name: Push HTML Report
|
||||
timeout-minutes: 3
|
||||
# commit report, then try push-rebase-loop until it's able to merge the HTML report to the gh-pages branch
|
||||
# this is necessary when this job running at least twice at the same time (e.g. through two pushes at the same time)
|
||||
env:
|
||||
RUN_ID: ${{ inputs.source_run_id }}
|
||||
RUN_ATTEMPT: ${{ inputs.source_run_attempt }}
|
||||
run: |
|
||||
git add .
|
||||
git commit -m "workflow: add HTML report for run-id $RUN_ID (attempt: $RUN_ATTEMPT)"
|
||||
git commit -m "workflow: add HTML report for run-id ${{ github.run_id }} (attempt: ${{ github.run_attempt }})"
|
||||
|
||||
while true; do
|
||||
git pull --rebase
|
||||
@@ -101,25 +67,25 @@ jobs:
|
||||
- name: Output Report URL as Workflow Annotation
|
||||
id: url
|
||||
run: |
|
||||
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results-2/$HTML_REPORT_URL_PATH
|
||||
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
|
||||
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
|
||||
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ inputs.pr_number }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: <!-- GENERATED-E2E-RESULTS -->
|
||||
- name: Create comment
|
||||
if: steps.fc.outputs.comment-id == ''
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ inputs.pr_number }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
<!-- GENERATED-E2E-RESULTS -->
|
||||
## E2E results are ready!
|
||||
- [Workflow #${{ inputs.source_run_number }}.${{ inputs.source_run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
- name: Update comment
|
||||
if: steps.fc.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
@@ -129,4 +95,4 @@ jobs:
|
||||
body: |
|
||||
<!-- GENERATED-E2E-RESULTS -->
|
||||
## E2E results are ready!
|
||||
- [Workflow #${{ inputs.source_run_number }}.${{ inputs.source_run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
- [Workflow #${{ github.run_number }}.${{ github.run_attempt }} latest results](${{ steps.url.outputs.link }})
|
||||
|
||||
@@ -1,176 +1,48 @@
|
||||
name: "Release Docker"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
POSTGRES_USER: "unicorn_user"
|
||||
POSTGRES_PASSWORD: "magical_password"
|
||||
POSTGRES_DB: "calendso"
|
||||
DATABASE_HOST: "database:5432"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
- published
|
||||
# in case manual trigger is needed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
BUILD_FROM_BRANCH:
|
||||
description: "Build from the selected branch instead of a tag"
|
||||
type: boolean
|
||||
default: false
|
||||
RELEASE_TAG:
|
||||
description: "Tag to build (e.g., v6.0.5) - required if BUILD_FROM_BRANCH is false"
|
||||
required: false
|
||||
PUSH_IMAGE:
|
||||
description: "Push the Docker image to DockerHub"
|
||||
type: boolean
|
||||
default: true
|
||||
description: "v{Major}.{Minor}.{Patch}"
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: "Prepare Release"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_tag: ${{ steps.determine-tag.outputs.release_tag }}
|
||||
checkout_ref: ${{ steps.determine-tag.outputs.checkout_ref }}
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.BUILD_FROM_BRANCH == false && inputs.RELEASE_TAG == ''
|
||||
run: |
|
||||
echo "::error::RELEASE_TAG is required when BUILD_FROM_BRANCH is false"
|
||||
exit 1
|
||||
release:
|
||||
name: "Remote Release"
|
||||
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ (github.event_name == 'workflow_dispatch' && inputs.BUILD_FROM_BRANCH) && github.ref || inputs.RELEASE_TAG || github.ref }}
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
- name: "Determine tag and ref"
|
||||
id: determine-tag
|
||||
run: |
|
||||
# Determine checkout ref
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.BUILD_FROM_BRANCH }}" = "true" ]; then
|
||||
echo "checkout_ref=${{ github.ref }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "checkout_ref=${{ inputs.RELEASE_TAG }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "checkout_ref=${{ github.ref }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Determine release tag
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
if [ "${{ inputs.BUILD_FROM_BRANCH }}" = "true" ]; then
|
||||
# Extract branch name and short SHA for the tag
|
||||
BRANCH_NAME="${GITHUB_REF#refs/heads/}"
|
||||
# Sanitize branch name: replace all invalid Docker tag characters with dashes
|
||||
# Docker tags can only contain lowercase/uppercase letters, digits, underscores, periods, and dashes
|
||||
# Also remove any leading dashes or periods
|
||||
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9_.-]/-/g' | sed 's/^[-.]*//')
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
echo "release_tag=${SANITIZED_BRANCH}-${SHORT_SHA}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "release_tag=${{ inputs.RELEASE_TAG }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "release_tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
release-amd64:
|
||||
name: "Release AMD64"
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.checkout_ref }}
|
||||
|
||||
- name: Build and test Docker image
|
||||
uses: ./.github/actions/docker-build-and-test
|
||||
with:
|
||||
platform: "linux/amd64"
|
||||
platform-suffix: ""
|
||||
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
postgres-user: ${{ env.POSTGRES_USER }}
|
||||
postgres-password: ${{ env.POSTGRES_PASSWORD }}
|
||||
postgres-db: ${{ env.POSTGRES_DB }}
|
||||
database-host: ${{ env.DATABASE_HOST }}
|
||||
push-image: ${{ (github.event_name == 'push' || inputs.PUSH_IMAGE) && 'true' || 'false' }}
|
||||
use-as-latest: "true"
|
||||
- name: "Determine tag"
|
||||
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||
|
||||
- name: Notify Slack on Success
|
||||
if: success() && secrets.CI_SLACK_WEBHOOK_URL != ''
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
- name: "Run remote release workflow"
|
||||
uses: "actions/github-script@v6"
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": ":large_green_circle: Workflow *${{ github.workflow }}* (AMD64) succeeded in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
# Requires a personal access token with Actions Read and write permissions on calcom/docker.
|
||||
github-token: "${{ secrets.DOCKER_REPO_ACCESS_TOKEN }}"
|
||||
script: |
|
||||
try {
|
||||
const response = await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: 'docker',
|
||||
workflow_id: 'create-release.yaml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
"RELEASE_TAG": process.env.RELEASE_TAG
|
||||
},
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Notify Slack on Failure
|
||||
if: failure() && secrets.CI_SLACK_WEBHOOK_URL != ''
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": ":red_circle: Workflow *${{ github.workflow }}* (AMD64) failed in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||
|
||||
release-arm:
|
||||
name: "Release ARM"
|
||||
needs: prepare
|
||||
runs-on: ubuntu-24.04-arm
|
||||
env:
|
||||
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.checkout_ref }}
|
||||
|
||||
- name: Build and test Docker image
|
||||
uses: ./.github/actions/docker-build-and-test
|
||||
with:
|
||||
platform: "arm64"
|
||||
platform-suffix: "-arm"
|
||||
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
postgres-user: ${{ env.POSTGRES_USER }}
|
||||
postgres-password: ${{ env.POSTGRES_PASSWORD }}
|
||||
postgres-db: ${{ env.POSTGRES_DB }}
|
||||
database-host: ${{ env.DATABASE_HOST }}
|
||||
push-image: ${{ (github.event_name == 'push' || inputs.PUSH_IMAGE) && 'true' || 'false' }}
|
||||
|
||||
- name: Notify Slack on Success
|
||||
if: success() && secrets.CI_SLACK_WEBHOOK_URL != ''
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": ":large_green_circle: Workflow *${{ github.workflow }}* (ARM) succeeded in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Notify Slack on Failure
|
||||
if: failure() && secrets.CI_SLACK_WEBHOOK_URL != ''
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": ":red_circle: Workflow *${{ github.workflow }}* (ARM) failed in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
name: Run CI
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
name: Trigger CI
|
||||
if: github.event.label.name == 'run-ci' || github.event.label.name == 'ready-for-e2e'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Verify and trigger CI
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const adder = context.payload.sender.login;
|
||||
const senderType = context.payload.sender.type;
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
console.log(`Sender: ${adder} (type: ${senderType})`);
|
||||
|
||||
const trustedBotLogins = ['graphite-app[bot]'];
|
||||
let isAuthorized = false;
|
||||
|
||||
if (senderType === 'Bot' && trustedBotLogins.includes(adder)) {
|
||||
console.log(`Authorized: trusted GitHub App`);
|
||||
isAuthorized = true;
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: adder,
|
||||
});
|
||||
|
||||
if (!['admin', 'maintain', 'write'].includes(perm.permission)) {
|
||||
core.setFailed(`${adder} does not have write access`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Label added by ${adder}`);
|
||||
}
|
||||
|
||||
// Find the latest pr.yml run for this PR's head SHA
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'pr.yml',
|
||||
head_sha: pr.head.sha,
|
||||
per_page: 5,
|
||||
});
|
||||
|
||||
// Filter runs to this PR (in case same SHA exists in multiple PRs)
|
||||
const matchingRuns = runs.workflow_runs.filter(run =>
|
||||
!run.pull_requests?.length || run.pull_requests.some(p => p.number === pr.number)
|
||||
);
|
||||
|
||||
if (matchingRuns.length === 0) {
|
||||
core.setFailed(`No PR workflow found for SHA ${pr.head.sha}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestRun = matchingRuns[0];
|
||||
|
||||
// If workflow is still running, cancel it first then re-run
|
||||
if (latestRun.status === 'in_progress' || latestRun.status === 'queued') {
|
||||
console.log(`Workflow is running (status: ${latestRun.status}). Cancelling it first...`);
|
||||
|
||||
const cancelMaxRetries = 10;
|
||||
const cancelRetryDelay = 3000;
|
||||
for (let attempt = 1; attempt <= cancelMaxRetries; attempt++) {
|
||||
try {
|
||||
await github.rest.actions.cancelWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestRun.id,
|
||||
});
|
||||
console.log(`Cancel request accepted on attempt ${attempt}`);
|
||||
break;
|
||||
} catch (err) {
|
||||
if (err.status === 409 && attempt < cancelMaxRetries) {
|
||||
console.log(`Cancel attempt ${attempt} got 409 (not yet cancellable), retrying in ${cancelRetryDelay / 1000}s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, cancelRetryDelay));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the workflow to be cancelled (poll with timeout)
|
||||
const maxWaitTime = 60000; // 60 seconds
|
||||
const pollInterval = 2000; // 2 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < maxWaitTime) {
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
|
||||
const { data: updatedRun } = await github.rest.actions.getWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestRun.id,
|
||||
});
|
||||
|
||||
if (updatedRun.status === 'completed') {
|
||||
console.log(`Workflow cancelled successfully (conclusion: ${updatedRun.conclusion})`);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`Waiting for workflow to cancel... (status: ${updatedRun.status})`);
|
||||
}
|
||||
|
||||
// Check final status
|
||||
const { data: finalRun } = await github.rest.actions.getWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestRun.id,
|
||||
});
|
||||
|
||||
if (finalRun.status !== 'completed') {
|
||||
core.setFailed(`Timed out waiting for workflow to cancel (status: ${finalRun.status})`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Re-running workflow ${latestRun.id} (was: ${latestRun.conclusion || latestRun.status})`);
|
||||
|
||||
// Re-run preserves original context (PR, SHA, etc.)
|
||||
await github.rest.actions.reRunWorkflow({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestRun.id,
|
||||
});
|
||||
|
||||
console.log(`Triggered: ${latestRun.html_url}`);
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Security Audit
|
||||
on:
|
||||
workflow_call:
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Report all vulnerabilities
|
||||
run: yarn npm audit --all --recursive || true
|
||||
- name: Fail on critical vulnerabilities
|
||||
run: yarn npm audit --all --recursive --severity critical
|
||||
@@ -1,58 +0,0 @@
|
||||
name: Setup Database
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
DB_CACHE_HIT:
|
||||
required: false
|
||||
type: string
|
||||
default: "false"
|
||||
description: "Whether the DB cache was hit (skip yarn-install and cache-db if true)"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
jobs:
|
||||
setup-db:
|
||||
name: Setup Database
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
if: inputs.DB_CACHE_HIT != 'true'
|
||||
- run: yarn prisma generate
|
||||
if: inputs.DB_CACHE_HIT != 'true'
|
||||
- uses: ./.github/actions/cache-db
|
||||
if: inputs.DB_CACHE_HIT != 'true'
|
||||
@@ -9,14 +9,16 @@ jobs:
|
||||
test:
|
||||
name: Unit
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- run: yarn prisma generate
|
||||
- run: yarn test -- --no-isolate
|
||||
# We could add different timezones here that we need to run our tests in
|
||||
- run: TZ=America/Los_Angeles VITEST_MODE=timezone yarn test -- --no-isolate
|
||||
- run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly --no-isolate
|
||||
- name: Run API v2 tests
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn workspace @calcom/platform-libraries build && yarn test
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Yarn install
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Yarn install & cache
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
+3
-18
@@ -60,22 +60,18 @@ yarn-error.log*
|
||||
.history/
|
||||
|
||||
# Typescript
|
||||
*.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# turbo
|
||||
.turbo
|
||||
|
||||
# Prisma generated files
|
||||
packages/prisma/client/*
|
||||
!packages/prisma/client/index.ts
|
||||
apps/api/v2/generated/prisma/*
|
||||
packages/prisma/generated/prisma/*
|
||||
packages/prisma/zod/**/*.ts
|
||||
packages/prisma/enums
|
||||
|
||||
# Builds
|
||||
dist
|
||||
packages/app-store/types
|
||||
|
||||
# Linting
|
||||
lint-results
|
||||
@@ -104,16 +100,5 @@ apps/auth
|
||||
i18n.cache
|
||||
packages/**/.yarn/ci-cache/
|
||||
|
||||
# AI - session/user specific files (keep CLAUDE.md and .cursor/skills tracked)
|
||||
.claude/plans/
|
||||
.claude/settings.local.json
|
||||
|
||||
# Spec drafts (rough work, not ready for review)
|
||||
specs/**/_drafts/
|
||||
|
||||
# trigger.dev
|
||||
.trigger
|
||||
agents/devin-knowledge.json
|
||||
|
||||
# pagefind
|
||||
_pagefind/
|
||||
# AI
|
||||
.claude
|
||||
|
||||
+4
-5
@@ -1,7 +1,6 @@
|
||||
if [ -f .git/MERGE_HEAD ]; then
|
||||
echo "Merge detected. Skipping lint-staged and generators."
|
||||
exit 0
|
||||
fi
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
||||
|
||||
yarn lint-staged --verbose
|
||||
yarn app-store:build && git add packages/app-store/*.generated.*
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,125 +0,0 @@
|
||||
---
|
||||
name: vercel-react-best-practices
|
||||
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: vercel
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Vercel React Best Practices
|
||||
|
||||
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
- Writing new React components or Next.js pages
|
||||
- Implementing data fetching (client or server-side)
|
||||
- Reviewing code for performance issues
|
||||
- Refactoring existing React/Next.js code
|
||||
- Optimizing bundle size or load times
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Eliminating Waterfalls (CRITICAL)
|
||||
|
||||
- `async-defer-await` - Move await into branches where actually used
|
||||
- `async-parallel` - Use Promise.all() for independent operations
|
||||
- `async-dependencies` - Use better-all for partial dependencies
|
||||
- `async-api-routes` - Start promises early, await late in API routes
|
||||
- `async-suspense-boundaries` - Use Suspense to stream content
|
||||
|
||||
### 2. Bundle Size Optimization (CRITICAL)
|
||||
|
||||
- `bundle-barrel-imports` - Import directly, avoid barrel files
|
||||
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
|
||||
- `bundle-defer-third-party` - Load analytics/logging after hydration
|
||||
- `bundle-conditional` - Load modules only when feature is activated
|
||||
- `bundle-preload` - Preload on hover/focus for perceived speed
|
||||
|
||||
### 3. Server-Side Performance (HIGH)
|
||||
|
||||
- `server-cache-react` - Use React.cache() for per-request deduplication
|
||||
- `server-cache-lru` - Use LRU cache for cross-request caching
|
||||
- `server-serialization` - Minimize data passed to client components
|
||||
- `server-parallel-fetching` - Restructure components to parallelize fetches
|
||||
- `server-after-nonblocking` - Use after() for non-blocking operations
|
||||
|
||||
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
||||
|
||||
- `client-swr-dedup` - Use SWR for automatic request deduplication
|
||||
- `client-event-listeners` - Deduplicate global event listeners
|
||||
|
||||
### 5. Re-render Optimization (MEDIUM)
|
||||
|
||||
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
|
||||
- `rerender-memo` - Extract expensive work into memoized components
|
||||
- `rerender-dependencies` - Use primitive dependencies in effects
|
||||
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
|
||||
- `rerender-functional-setstate` - Use functional setState for stable callbacks
|
||||
- `rerender-lazy-state-init` - Pass function to useState for expensive values
|
||||
- `rerender-transitions` - Use startTransition for non-urgent updates
|
||||
|
||||
### 6. Rendering Performance (MEDIUM)
|
||||
|
||||
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
|
||||
- `rendering-content-visibility` - Use content-visibility for long lists
|
||||
- `rendering-hoist-jsx` - Extract static JSX outside components
|
||||
- `rendering-svg-precision` - Reduce SVG coordinate precision
|
||||
- `rendering-hydration-no-flicker` - Use inline script for client-only data
|
||||
- `rendering-activity` - Use Activity component for show/hide
|
||||
- `rendering-conditional-render` - Use ternary, not && for conditionals
|
||||
|
||||
### 7. JavaScript Performance (LOW-MEDIUM)
|
||||
|
||||
- `js-batch-dom-css` - Group CSS changes via classes or cssText
|
||||
- `js-index-maps` - Build Map for repeated lookups
|
||||
- `js-cache-property-access` - Cache object properties in loops
|
||||
- `js-cache-function-results` - Cache function results in module-level Map
|
||||
- `js-cache-storage` - Cache localStorage/sessionStorage reads
|
||||
- `js-combine-iterations` - Combine multiple filter/map into one loop
|
||||
- `js-length-check-first` - Check array length before expensive comparison
|
||||
- `js-early-exit` - Return early from functions
|
||||
- `js-hoist-regexp` - Hoist RegExp creation outside loops
|
||||
- `js-min-max-loop` - Use loop for min/max instead of sort
|
||||
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
|
||||
- `js-tosorted-immutable` - Use toSorted() for immutability
|
||||
|
||||
### 8. Advanced Patterns (LOW)
|
||||
|
||||
- `advanced-event-handler-refs` - Store event handlers in refs
|
||||
- `advanced-use-latest` - useLatest for stable callback refs
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual rule files for detailed explanations and code examples:
|
||||
|
||||
```
|
||||
rules/async-parallel.md
|
||||
rules/bundle-barrel-imports.md
|
||||
rules/_sections.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
- Additional context and references
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
title: Store Event Handlers in Refs
|
||||
impact: LOW
|
||||
impactDescription: stable subscriptions
|
||||
tags: advanced, hooks, refs, event-handlers, optimization
|
||||
---
|
||||
|
||||
## Store Event Handlers in Refs
|
||||
|
||||
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
|
||||
|
||||
**Incorrect (re-subscribes on every render):**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: () => void) {
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, handler)
|
||||
return () => window.removeEventListener(event, handler)
|
||||
}, [event, handler])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (stable subscription):**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: () => void) {
|
||||
const handlerRef = useRef(handler)
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler
|
||||
}, [handler])
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => handlerRef.current()
|
||||
window.addEventListener(event, listener)
|
||||
return () => window.removeEventListener(event, listener)
|
||||
}, [event])
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative: use `useEffectEvent` if you're on latest React:**
|
||||
|
||||
```tsx
|
||||
import { useEffectEvent } from 'react'
|
||||
|
||||
function useWindowEvent(event: string, handler: () => void) {
|
||||
const onEvent = useEffectEvent(handler)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, onEvent)
|
||||
return () => window.removeEventListener(event, onEvent)
|
||||
}, [event])
|
||||
}
|
||||
```
|
||||
|
||||
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: useLatest for Stable Callback Refs
|
||||
impact: LOW
|
||||
impactDescription: prevents effect re-runs
|
||||
tags: advanced, hooks, useLatest, refs, optimization
|
||||
---
|
||||
|
||||
## useLatest for Stable Callback Refs
|
||||
|
||||
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
function useLatest<T>(value: T) {
|
||||
const ref = useRef(value)
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
return ref
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (effect re-runs on every callback change):**
|
||||
|
||||
```tsx
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearch(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query, onSearch])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (stable effect, fresh callback):**
|
||||
|
||||
```tsx
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const onSearchRef = useLatest(onSearch)
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearchRef.current(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query])
|
||||
}
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
title: Prevent Waterfall Chains in API Routes
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: api-routes, server-actions, waterfalls, parallelization
|
||||
---
|
||||
|
||||
## Prevent Waterfall Chains in API Routes
|
||||
|
||||
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
|
||||
|
||||
**Incorrect (config waits for auth, data waits for both):**
|
||||
|
||||
```typescript
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth()
|
||||
const config = await fetchConfig()
|
||||
const data = await fetchData(session.user.id)
|
||||
return Response.json({ data, config })
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (auth and config start immediately):**
|
||||
|
||||
```typescript
|
||||
export async function GET(request: Request) {
|
||||
const sessionPromise = auth()
|
||||
const configPromise = fetchConfig()
|
||||
const session = await sessionPromise
|
||||
const [config, data] = await Promise.all([
|
||||
configPromise,
|
||||
fetchData(session.user.id)
|
||||
])
|
||||
return Response.json({ data, config })
|
||||
}
|
||||
```
|
||||
|
||||
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
title: Defer Await Until Needed
|
||||
impact: HIGH
|
||||
impactDescription: avoids blocking unused code paths
|
||||
tags: async, await, conditional, optimization
|
||||
---
|
||||
|
||||
## Defer Await Until Needed
|
||||
|
||||
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
|
||||
|
||||
**Incorrect (blocks both branches):**
|
||||
|
||||
```typescript
|
||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
const userData = await fetchUserData(userId)
|
||||
|
||||
if (skipProcessing) {
|
||||
// Returns immediately but still waited for userData
|
||||
return { skipped: true }
|
||||
}
|
||||
|
||||
// Only this branch uses userData
|
||||
return processUserData(userData)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (only blocks when needed):**
|
||||
|
||||
```typescript
|
||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
if (skipProcessing) {
|
||||
// Returns immediately without waiting
|
||||
return { skipped: true }
|
||||
}
|
||||
|
||||
// Fetch only when needed
|
||||
const userData = await fetchUserData(userId)
|
||||
return processUserData(userData)
|
||||
}
|
||||
```
|
||||
|
||||
**Another example (early return optimization):**
|
||||
|
||||
```typescript
|
||||
// Incorrect: always fetches permissions
|
||||
async function updateResource(resourceId: string, userId: string) {
|
||||
const permissions = await fetchPermissions(userId)
|
||||
const resource = await getResource(resourceId)
|
||||
|
||||
if (!resource) {
|
||||
return { error: 'Not found' }
|
||||
}
|
||||
|
||||
if (!permissions.canEdit) {
|
||||
return { error: 'Forbidden' }
|
||||
}
|
||||
|
||||
return await updateResourceData(resource, permissions)
|
||||
}
|
||||
|
||||
// Correct: fetches only when needed
|
||||
async function updateResource(resourceId: string, userId: string) {
|
||||
const resource = await getResource(resourceId)
|
||||
|
||||
if (!resource) {
|
||||
return { error: 'Not found' }
|
||||
}
|
||||
|
||||
const permissions = await fetchPermissions(userId)
|
||||
|
||||
if (!permissions.canEdit) {
|
||||
return { error: 'Forbidden' }
|
||||
}
|
||||
|
||||
return await updateResourceData(resource, permissions)
|
||||
}
|
||||
```
|
||||
|
||||
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
title: Dependency-Based Parallelization
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: async, parallelization, dependencies, better-all
|
||||
---
|
||||
|
||||
## Dependency-Based Parallelization
|
||||
|
||||
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
|
||||
|
||||
**Incorrect (profile waits for config unnecessarily):**
|
||||
|
||||
```typescript
|
||||
const [user, config] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchConfig()
|
||||
])
|
||||
const profile = await fetchProfile(user.id)
|
||||
```
|
||||
|
||||
**Correct (config and profile run in parallel):**
|
||||
|
||||
```typescript
|
||||
import { all } from 'better-all'
|
||||
|
||||
const { user, config, profile } = await all({
|
||||
async user() { return fetchUser() },
|
||||
async config() { return fetchConfig() },
|
||||
async profile() {
|
||||
return fetchProfile((await this.$.user).id)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
title: Promise.all() for Independent Operations
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: async, parallelization, promises, waterfalls
|
||||
---
|
||||
|
||||
## Promise.all() for Independent Operations
|
||||
|
||||
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
|
||||
|
||||
**Incorrect (sequential execution, 3 round trips):**
|
||||
|
||||
```typescript
|
||||
const user = await fetchUser()
|
||||
const posts = await fetchPosts()
|
||||
const comments = await fetchComments()
|
||||
```
|
||||
|
||||
**Correct (parallel execution, 1 round trip):**
|
||||
|
||||
```typescript
|
||||
const [user, posts, comments] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchPosts(),
|
||||
fetchComments()
|
||||
])
|
||||
```
|
||||
@@ -1,99 +0,0 @@
|
||||
---
|
||||
title: Strategic Suspense Boundaries
|
||||
impact: HIGH
|
||||
impactDescription: faster initial paint
|
||||
tags: async, suspense, streaming, layout-shift
|
||||
---
|
||||
|
||||
## Strategic Suspense Boundaries
|
||||
|
||||
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
|
||||
|
||||
**Incorrect (wrapper blocked by data fetching):**
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const data = await fetchData() // Blocks entire page
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<div>
|
||||
<DataDisplay data={data} />
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The entire layout waits for data even though only the middle section needs it.
|
||||
|
||||
**Correct (wrapper shows immediately, data streams in):**
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<div>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DataDisplay />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function DataDisplay() {
|
||||
const data = await fetchData() // Only blocks this component
|
||||
return <div>{data.content}</div>
|
||||
}
|
||||
```
|
||||
|
||||
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
|
||||
|
||||
**Alternative (share promise across components):**
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
// Start fetch immediately, but don't await
|
||||
const dataPromise = fetchData()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DataDisplay dataPromise={dataPromise} />
|
||||
<DataSummary dataPromise={dataPromise} />
|
||||
</Suspense>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise) // Unwraps the promise
|
||||
return <div>{data.content}</div>
|
||||
}
|
||||
|
||||
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise) // Reuses the same promise
|
||||
return <div>{data.summary}</div>
|
||||
}
|
||||
```
|
||||
|
||||
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
|
||||
|
||||
**When NOT to use this pattern:**
|
||||
|
||||
- Critical data needed for layout decisions (affects positioning)
|
||||
- SEO-critical content above the fold
|
||||
- Small, fast queries where suspense overhead isn't worth it
|
||||
- When you want to avoid layout shift (loading → content jump)
|
||||
|
||||
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
title: Avoid Barrel File Imports
|
||||
impact: CRITICAL
|
||||
impactDescription: 200-800ms import cost, slow builds
|
||||
tags: bundle, imports, tree-shaking, barrel-files, performance
|
||||
---
|
||||
|
||||
## Avoid Barrel File Imports
|
||||
|
||||
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
|
||||
|
||||
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
|
||||
|
||||
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
|
||||
|
||||
**Incorrect (imports entire library):**
|
||||
|
||||
```tsx
|
||||
import { Check, X, Menu } from 'lucide-react'
|
||||
// Loads 1,583 modules, takes ~2.8s extra in dev
|
||||
// Runtime cost: 200-800ms on every cold start
|
||||
|
||||
import { Button, TextField } from '@mui/material'
|
||||
// Loads 2,225 modules, takes ~4.2s extra in dev
|
||||
```
|
||||
|
||||
**Correct (imports only what you need):**
|
||||
|
||||
```tsx
|
||||
import Check from 'lucide-react/dist/esm/icons/check'
|
||||
import X from 'lucide-react/dist/esm/icons/x'
|
||||
import Menu from 'lucide-react/dist/esm/icons/menu'
|
||||
// Loads only 3 modules (~2KB vs ~1MB)
|
||||
|
||||
import Button from '@mui/material/Button'
|
||||
import TextField from '@mui/material/TextField'
|
||||
// Loads only what you use
|
||||
```
|
||||
|
||||
**Alternative (Next.js 13.5+):**
|
||||
|
||||
```js
|
||||
// next.config.js - use optimizePackageImports
|
||||
module.exports = {
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react', '@mui/material']
|
||||
}
|
||||
}
|
||||
|
||||
// Then you can keep the ergonomic barrel imports:
|
||||
import { Check, X, Menu } from 'lucide-react'
|
||||
// Automatically transformed to direct imports at build time
|
||||
```
|
||||
|
||||
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
|
||||
|
||||
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
|
||||
|
||||
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
title: Conditional Module Loading
|
||||
impact: HIGH
|
||||
impactDescription: loads large data only when needed
|
||||
tags: bundle, conditional-loading, lazy-loading
|
||||
---
|
||||
|
||||
## Conditional Module Loading
|
||||
|
||||
Load large data or modules only when a feature is activated.
|
||||
|
||||
**Example (lazy-load animation frames):**
|
||||
|
||||
```tsx
|
||||
function AnimationPlayer({ enabled }: { enabled: boolean }) {
|
||||
const [frames, setFrames] = useState<Frame[] | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && !frames && typeof window !== 'undefined') {
|
||||
import('./animation-frames.js')
|
||||
.then(mod => setFrames(mod.frames))
|
||||
.catch(() => setEnabled(false))
|
||||
}
|
||||
}, [enabled, frames])
|
||||
|
||||
if (!frames) return <Skeleton />
|
||||
return <Canvas frames={frames} />
|
||||
}
|
||||
```
|
||||
|
||||
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: Defer Non-Critical Third-Party Libraries
|
||||
impact: MEDIUM
|
||||
impactDescription: loads after hydration
|
||||
tags: bundle, third-party, analytics, defer
|
||||
---
|
||||
|
||||
## Defer Non-Critical Third-Party Libraries
|
||||
|
||||
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
|
||||
|
||||
**Incorrect (blocks initial bundle):**
|
||||
|
||||
```tsx
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (loads after hydration):**
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Analytics = dynamic(
|
||||
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
title: Dynamic Imports for Heavy Components
|
||||
impact: CRITICAL
|
||||
impactDescription: directly affects TTI and LCP
|
||||
tags: bundle, dynamic-import, code-splitting, next-dynamic
|
||||
---
|
||||
|
||||
## Dynamic Imports for Heavy Components
|
||||
|
||||
Use `next/dynamic` to lazy-load large components not needed on initial render.
|
||||
|
||||
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
||||
|
||||
```tsx
|
||||
import { MonacoEditor } from './monaco-editor'
|
||||
|
||||
function CodePanel({ code }: { code: string }) {
|
||||
return <MonacoEditor value={code} />
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Monaco loads on demand):**
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const MonacoEditor = dynamic(
|
||||
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
function CodePanel({ code }: { code: string }) {
|
||||
return <MonacoEditor value={code} />
|
||||
}
|
||||
```
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
title: Preload Based on User Intent
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces perceived latency
|
||||
tags: bundle, preload, user-intent, hover
|
||||
---
|
||||
|
||||
## Preload Based on User Intent
|
||||
|
||||
Preload heavy bundles before they're needed to reduce perceived latency.
|
||||
|
||||
**Example (preload on hover/focus):**
|
||||
|
||||
```tsx
|
||||
function EditorButton({ onClick }: { onClick: () => void }) {
|
||||
const preload = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
void import('./monaco-editor')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={preload}
|
||||
onFocus={preload}
|
||||
onClick={onClick}
|
||||
>
|
||||
Open Editor
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Example (preload when feature flag is enabled):**
|
||||
|
||||
```tsx
|
||||
function FlagsProvider({ children, flags }: Props) {
|
||||
useEffect(() => {
|
||||
if (flags.editorEnabled && typeof window !== 'undefined') {
|
||||
void import('./monaco-editor').then(mod => mod.init())
|
||||
}
|
||||
}, [flags.editorEnabled])
|
||||
|
||||
return <FlagsContext.Provider value={flags}>
|
||||
{children}
|
||||
</FlagsContext.Provider>
|
||||
}
|
||||
```
|
||||
|
||||
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
title: Deduplicate Global Event Listeners
|
||||
impact: LOW
|
||||
impactDescription: single listener for N components
|
||||
tags: client, swr, event-listeners, subscription
|
||||
---
|
||||
|
||||
## Deduplicate Global Event Listeners
|
||||
|
||||
Use `useSWRSubscription()` to share global event listeners across component instances.
|
||||
|
||||
**Incorrect (N instances = N listeners):**
|
||||
|
||||
```tsx
|
||||
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && e.key === key) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [key, callback])
|
||||
}
|
||||
```
|
||||
|
||||
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
|
||||
|
||||
**Correct (N instances = 1 listener):**
|
||||
|
||||
```tsx
|
||||
import useSWRSubscription from 'swr/subscription'
|
||||
|
||||
// Module-level Map to track callbacks per key
|
||||
const keyCallbacks = new Map<string, Set<() => void>>()
|
||||
|
||||
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||
// Register this callback in the Map
|
||||
useEffect(() => {
|
||||
if (!keyCallbacks.has(key)) {
|
||||
keyCallbacks.set(key, new Set())
|
||||
}
|
||||
keyCallbacks.get(key)!.add(callback)
|
||||
|
||||
return () => {
|
||||
const set = keyCallbacks.get(key)
|
||||
if (set) {
|
||||
set.delete(callback)
|
||||
if (set.size === 0) {
|
||||
keyCallbacks.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [key, callback])
|
||||
|
||||
useSWRSubscription('global-keydown', () => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && keyCallbacks.has(e.key)) {
|
||||
keyCallbacks.get(e.key)!.forEach(cb => cb())
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
})
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
// Multiple shortcuts will share the same listener
|
||||
useKeyboardShortcut('p', () => { /* ... */ })
|
||||
useKeyboardShortcut('k', () => { /* ... */ })
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
title: Use SWR for Automatic Deduplication
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: automatic deduplication
|
||||
tags: client, swr, deduplication, data-fetching
|
||||
---
|
||||
|
||||
## Use SWR for Automatic Deduplication
|
||||
|
||||
SWR enables request deduplication, caching, and revalidation across component instances.
|
||||
|
||||
**Incorrect (no deduplication, each instance fetches):**
|
||||
|
||||
```tsx
|
||||
function UserList() {
|
||||
const [users, setUsers] = useState([])
|
||||
useEffect(() => {
|
||||
fetch('/api/users')
|
||||
.then(r => r.json())
|
||||
.then(setUsers)
|
||||
}, [])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (multiple instances share one request):**
|
||||
|
||||
```tsx
|
||||
import useSWR from 'swr'
|
||||
|
||||
function UserList() {
|
||||
const { data: users } = useSWR('/api/users', fetcher)
|
||||
}
|
||||
```
|
||||
|
||||
**For immutable data:**
|
||||
|
||||
```tsx
|
||||
import { useImmutableSWR } from '@/lib/swr'
|
||||
|
||||
function StaticContent() {
|
||||
const { data } = useImmutableSWR('/api/config', fetcher)
|
||||
}
|
||||
```
|
||||
|
||||
**For mutations:**
|
||||
|
||||
```tsx
|
||||
import { useSWRMutation } from 'swr/mutation'
|
||||
|
||||
function UpdateButton() {
|
||||
const { trigger } = useSWRMutation('/api/user', updateUser)
|
||||
return <button onClick={() => trigger()}>Update</button>
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [https://swr.vercel.app](https://swr.vercel.app)
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
title: Batch DOM CSS Changes
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces reflows/repaints
|
||||
tags: javascript, dom, css, performance, reflow
|
||||
---
|
||||
|
||||
## Batch DOM CSS Changes
|
||||
|
||||
Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
|
||||
|
||||
**Incorrect (multiple reflows):**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
// Each line triggers a reflow
|
||||
element.style.width = '100px'
|
||||
element.style.height = '200px'
|
||||
element.style.backgroundColor = 'blue'
|
||||
element.style.border = '1px solid black'
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (add class - single reflow):**
|
||||
|
||||
```typescript
|
||||
// CSS file
|
||||
.highlighted-box {
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
// JavaScript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.classList.add('highlighted-box')
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (change cssText - single reflow):**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.style.cssText = `
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
`
|
||||
}
|
||||
```
|
||||
|
||||
**React example:**
|
||||
|
||||
```tsx
|
||||
// Incorrect: changing styles one by one
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isHighlighted) {
|
||||
ref.current.style.width = '100px'
|
||||
ref.current.style.height = '200px'
|
||||
ref.current.style.backgroundColor = 'blue'
|
||||
}
|
||||
}, [isHighlighted])
|
||||
|
||||
return <div ref={ref}>Content</div>
|
||||
}
|
||||
|
||||
// Correct: toggle class
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
return (
|
||||
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
||||
Content
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
title: Cache Repeated Function Calls
|
||||
impact: MEDIUM
|
||||
impactDescription: avoid redundant computation
|
||||
tags: javascript, cache, memoization, performance
|
||||
---
|
||||
|
||||
## Cache Repeated Function Calls
|
||||
|
||||
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
|
||||
|
||||
**Incorrect (redundant computation):**
|
||||
|
||||
```typescript
|
||||
function ProjectList({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
{projects.map(project => {
|
||||
// slugify() called 100+ times for same project names
|
||||
const slug = slugify(project.name)
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (cached results):**
|
||||
|
||||
```typescript
|
||||
// Module-level cache
|
||||
const slugifyCache = new Map<string, string>()
|
||||
|
||||
function cachedSlugify(text: string): string {
|
||||
if (slugifyCache.has(text)) {
|
||||
return slugifyCache.get(text)!
|
||||
}
|
||||
const result = slugify(text)
|
||||
slugifyCache.set(text, result)
|
||||
return result
|
||||
}
|
||||
|
||||
function ProjectList({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
{projects.map(project => {
|
||||
// Computed only once per unique project name
|
||||
const slug = cachedSlugify(project.name)
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Simpler pattern for single-value functions:**
|
||||
|
||||
```typescript
|
||||
let isLoggedInCache: boolean | null = null
|
||||
|
||||
function isLoggedIn(): boolean {
|
||||
if (isLoggedInCache !== null) {
|
||||
return isLoggedInCache
|
||||
}
|
||||
|
||||
isLoggedInCache = document.cookie.includes('auth=')
|
||||
return isLoggedInCache
|
||||
}
|
||||
|
||||
// Clear cache when auth changes
|
||||
function onAuthChange() {
|
||||
isLoggedInCache = null
|
||||
}
|
||||
```
|
||||
|
||||
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
|
||||
|
||||
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
title: Cache Property Access in Loops
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces lookups
|
||||
tags: javascript, loops, optimization, caching
|
||||
---
|
||||
|
||||
## Cache Property Access in Loops
|
||||
|
||||
Cache object property lookups in hot paths.
|
||||
|
||||
**Incorrect (3 lookups × N iterations):**
|
||||
|
||||
```typescript
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
process(obj.config.settings.value)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (1 lookup total):**
|
||||
|
||||
```typescript
|
||||
const value = obj.config.settings.value
|
||||
const len = arr.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
process(value)
|
||||
}
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
title: Cache Storage API Calls
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces expensive I/O
|
||||
tags: javascript, localStorage, storage, caching, performance
|
||||
---
|
||||
|
||||
## Cache Storage API Calls
|
||||
|
||||
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
|
||||
|
||||
**Incorrect (reads storage on every call):**
|
||||
|
||||
```typescript
|
||||
function getTheme() {
|
||||
return localStorage.getItem('theme') ?? 'light'
|
||||
}
|
||||
// Called 10 times = 10 storage reads
|
||||
```
|
||||
|
||||
**Correct (Map cache):**
|
||||
|
||||
```typescript
|
||||
const storageCache = new Map<string, string | null>()
|
||||
|
||||
function getLocalStorage(key: string) {
|
||||
if (!storageCache.has(key)) {
|
||||
storageCache.set(key, localStorage.getItem(key))
|
||||
}
|
||||
return storageCache.get(key)
|
||||
}
|
||||
|
||||
function setLocalStorage(key: string, value: string) {
|
||||
localStorage.setItem(key, value)
|
||||
storageCache.set(key, value) // keep cache in sync
|
||||
}
|
||||
```
|
||||
|
||||
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
|
||||
|
||||
**Cookie caching:**
|
||||
|
||||
```typescript
|
||||
let cookieCache: Record<string, string> | null = null
|
||||
|
||||
function getCookie(name: string) {
|
||||
if (!cookieCache) {
|
||||
cookieCache = Object.fromEntries(
|
||||
document.cookie.split('; ').map(c => c.split('='))
|
||||
)
|
||||
}
|
||||
return cookieCache[name]
|
||||
}
|
||||
```
|
||||
|
||||
**Important (invalidate on external changes):**
|
||||
|
||||
If storage can change externally (another tab, server-set cookies), invalidate cache:
|
||||
|
||||
```typescript
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key) storageCache.delete(e.key)
|
||||
})
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
storageCache.clear()
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
title: Combine Multiple Array Iterations
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces iterations
|
||||
tags: javascript, arrays, loops, performance
|
||||
---
|
||||
|
||||
## Combine Multiple Array Iterations
|
||||
|
||||
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
|
||||
|
||||
**Incorrect (3 iterations):**
|
||||
|
||||
```typescript
|
||||
const admins = users.filter(u => u.isAdmin)
|
||||
const testers = users.filter(u => u.isTester)
|
||||
const inactive = users.filter(u => !u.isActive)
|
||||
```
|
||||
|
||||
**Correct (1 iteration):**
|
||||
|
||||
```typescript
|
||||
const admins: User[] = []
|
||||
const testers: User[] = []
|
||||
const inactive: User[] = []
|
||||
|
||||
for (const user of users) {
|
||||
if (user.isAdmin) admins.push(user)
|
||||
if (user.isTester) testers.push(user)
|
||||
if (!user.isActive) inactive.push(user)
|
||||
}
|
||||
```
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
title: Early Return from Functions
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids unnecessary computation
|
||||
tags: javascript, functions, optimization, early-return
|
||||
---
|
||||
|
||||
## Early Return from Functions
|
||||
|
||||
Return early when result is determined to skip unnecessary processing.
|
||||
|
||||
**Incorrect (processes all items even after finding answer):**
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
let hasError = false
|
||||
let errorMessage = ''
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
hasError = true
|
||||
errorMessage = 'Email required'
|
||||
}
|
||||
if (!user.name) {
|
||||
hasError = true
|
||||
errorMessage = 'Name required'
|
||||
}
|
||||
// Continues checking all users even after error found
|
||||
}
|
||||
|
||||
return hasError ? { valid: false, error: errorMessage } : { valid: true }
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (returns immediately on first error):**
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
return { valid: false, error: 'Email required' }
|
||||
}
|
||||
if (!user.name) {
|
||||
return { valid: false, error: 'Name required' }
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
```
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: Hoist RegExp Creation
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids recreation
|
||||
tags: javascript, regexp, optimization, memoization
|
||||
---
|
||||
|
||||
## Hoist RegExp Creation
|
||||
|
||||
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
|
||||
|
||||
**Incorrect (new RegExp every render):**
|
||||
|
||||
```tsx
|
||||
function Highlighter({ text, query }: Props) {
|
||||
const regex = new RegExp(`(${query})`, 'gi')
|
||||
const parts = text.split(regex)
|
||||
return <>{parts.map((part, i) => ...)}</>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (memoize or hoist):**
|
||||
|
||||
```tsx
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
function Highlighter({ text, query }: Props) {
|
||||
const regex = useMemo(
|
||||
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
|
||||
[query]
|
||||
)
|
||||
const parts = text.split(regex)
|
||||
return <>{parts.map((part, i) => ...)}</>
|
||||
}
|
||||
```
|
||||
|
||||
**Warning (global regex has mutable state):**
|
||||
|
||||
Global regex (`/g`) has mutable `lastIndex` state:
|
||||
|
||||
```typescript
|
||||
const regex = /foo/g
|
||||
regex.test('foo') // true, lastIndex = 3
|
||||
regex.test('foo') // false, lastIndex = 0
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user