Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code 78c4f9c73c fix: add defensive check for package.json in copyDependenciesInMemory
https://sonarly.com/issue/32080?type=bug

Logic function execution fails with "File not found" error when attempting to build Lambda dependency layers because the code unconditionally downloads package.json from S3 without checking if it exists first. Upgraded workspaces lack this file entirely, causing FileStorageException.

Fix: The fix implements a defensive check for the `package.json` file in `copyDependenciesInMemory()` method, preventing FileStorageException when the file is missing from S3.

**Changes made:**

1. Added import for `SEED_DEPENDENCIES_DIRNAME` constant which points to the seed-dependencies directory containing default dependency files
2. Added `path` to the path module import (previously only importing `dirname` and `join`)
3. Modified `copyDependenciesInMemory()` to check if both `package.json` and `yarn.lock` exist in parallel using `Promise.all()`
4. Added conditional logic for `package.json`: if it exists in S3, download it; otherwise, copy the seed package.json from the local seed-dependencies directory
5. Kept existing conditional logic for `yarn.lock` unchanged

**Why this fixes the issue:**

Workspaces created before the application-system feature, or upgraded from earlier versions, do not have dependency files in S3. When logic functions are executed, the system attempts to retrieve these files. Previously, the code unconditionally tried to download `package.json` without checking if it existed first, causing FileStorageException(FILE_NOT_FOUND) which prevented the entire logic function build process.

By checking file existence first and falling back to the seed package.json (which contains all necessary production dependencies), logic functions can now execute successfully on any workspace, regardless of when it was created or whether it was upgraded.

This follows the exact same defensive pattern already implemented for `yarn.lock` handling and matches the reference fix that was previously developed (commit a730cf3f7d).
2026-04-28 12:50:14 +00:00
7667 changed files with 173861 additions and 335082 deletions
+13 -14
View File
@@ -125,16 +125,15 @@ mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
**Naming Convention:** `{VERSION}-descriptive-name.webp`
**Naming Convention:** `{VERSION}-descriptive-name.png`
Examples:
- `1.9.0-feature-name.webp`
- `1.9.0-another-feature.webp`
- `1.9.0-feature-name.png`
- `1.9.0-another-feature.png`
```bash
# Move and rename source files, then convert to webp if needed
# Move and rename files
cp ~/Downloads/🆕/source-file.png packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
cd packages/twenty-website && node scripts/convert-png-to-webp.mjs
```
### 4. Research Features (if needed)
@@ -159,19 +158,19 @@ Date: {YYYY-MM-DD}
Short description explaining what the feature does and why it's useful. Keep it user-focused and concise (1-2 sentences).
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png)
# Feature 2 Name
Another short description of the second feature.
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png)
# Feature 3 Name
Description of the third feature.
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-3.webp)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-3.png)
```
**Style Guidelines:**
@@ -222,8 +221,8 @@ I've created the changelog for version {VERSION}. Here's the content for your re
[Show full MDX content]
Images moved to:
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png
Please review the content. Once you approve, I'll commit the changes and create the pull request.
```
@@ -289,12 +288,12 @@ Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
- **Location**: `packages/twenty-website/public/images/releases/`
### Image Files
- **Format**: `{VERSION}-descriptive-name.webp`
- **Format**: `{VERSION}-descriptive-name.png`
- **Convention**: Kebab-case descriptive names
- **Examples**:
- `1.8.0-workflow-iterator.webp`
- `1.8.0-bulk-select.webp`
- `1.9.0-new-feature.webp`
- `1.8.0-workflow-iterator.png`
- `1.8.0-bulk-select.png`
- `1.9.0-new-feature.png`
## Quick Reference Template
-72
View File
@@ -1,72 +0,0 @@
---
description: GitHub Actions security guidelines for supply chain protection
globs: **/.github/**/*.yml, **/.github/**/*.yaml
alwaysApply: false
---
# GitHub Actions Security
## Pin Third-Party Actions to Commit SHAs
Always reference external actions and reusable workflows by their full commit SHA, never by a mutable tag or branch. Tags can be force-pushed by a compromised maintainer account.
```yaml
# ❌ Mutable tag — vulnerable to supply chain attacks
uses: actions/checkout@v4
uses: actions/setup-node@v4
# ✅ Pinned to commit SHA with tag comment for readability
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
```
## Prefer `gh api` Over Third-Party Dispatch Actions
For repository dispatch calls, use `gh api` directly instead of third-party actions like `peter-evans/repository-dispatch`. This eliminates a supply-chain dependency entirely.
```yaml
# ✅ Use env vars + bracket notation to prevent injection
- name: Dispatch to target repo
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
gh api repos/org/repo/dispatches \
-f event_type=my-event \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[branch]=$BRANCH"
# ✅ Simple dispatch without payload
- name: Trigger workflow
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
run: |
gh api repos/org/repo/dispatches -f event_type=my-event
# ❌ Third-party action dependency
- uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.DISPATCH_TOKEN }}
repository: org/repo
event-type: my-event
# ❌ Inline ${{ }} in shell — vulnerable to injection
- run: |
gh api repos/org/repo/dispatches --input - <<EOF
{"event_type": "x", "client_payload": {"branch": "${{ github.head_ref }}"}}
EOF
```
## Minimal Permissions
Always declare explicit `permissions` at the job level with the least privilege required. Never rely on the default `GITHUB_TOKEN` permissions.
```yaml
# ✅ Explicit minimal permissions
permissions:
contents: read
# ❌ Overly broad or implicit permissions
permissions: write-all
```
-6
View File
@@ -1,6 +0,0 @@
/.github/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/CODEOWNERS @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/workflows/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/actions/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/dependabot.yml @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.yarnrc.yml @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
+2 -2
View File
@@ -21,7 +21,7 @@ runs:
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@v4
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
@@ -50,4 +50,4 @@ runs:
- name: Deploy
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty app:publish --private --remote target
run: yarn twenty deploy --remote target
@@ -21,7 +21,7 @@ runs:
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@v4
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
@@ -50,4 +50,4 @@ runs:
- name: Install
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty app:install --remote target
run: yarn twenty install --remote target
+1 -1
View File
@@ -20,7 +20,7 @@ runs:
run: git fetch origin main --depth=1
- name: Get last successful commit
if: env.NX_BASE == ''
uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4
uses: nrwl/nx-set-shas@v4
- name: Fallback to origin/main if no base found
if: env.NX_BASE == ''
shell: bash
+1 -1
View File
@@ -25,7 +25,7 @@ runs:
run: |
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}"
- name: Restore cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
uses: actions/cache/restore@v4
id: restore-cache
with:
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
+1 -4
View File
@@ -9,11 +9,8 @@ inputs:
runs:
using: "composite"
steps:
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
- name: Save cache
if: ${{ format('{0}', github.event.pull_request.head.repo.fork) != 'true' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
uses: actions/cache/save@v4
with:
key: ${{ inputs.key }}
path: |
@@ -48,7 +48,7 @@ runs:
fi
- name: Checkout docker compose files
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
repository: ${{ inputs.twenty-repository }}
ref: ${{ steps.resolve.outputs.git-ref }}
+4 -18
View File
@@ -7,17 +7,6 @@ inputs:
runs:
using: 'composite'
steps:
- name: Free disk space for install
if: runner.os == 'Linux'
shell: bash
run: |
# Default GitHub images ship large SDKs this repo does not use; removing
# them avoids ENOSPC when restoring or linking a full Yarn node_modules.
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
df -h
- name: Cache primary key builder
id: globals
shell: bash
@@ -29,12 +18,12 @@ runs:
echo "packages/*/node_modules" >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
- name: Setup Node.js and get yarn cache
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Restore node_modules
id: cache-node-modules
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
uses: actions/cache/restore@v4
with:
key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
@@ -44,13 +33,10 @@ runs:
shell: ${{ steps.globals.outputs.ACTION_SHELL }}
run: |
yarn config set enableHardenedMode true
yarn config set enableScripts false
yarn --immutable --check-cache
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
- name: Save cache
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' && format('{0}', github.event.pull_request.head.repo.fork) != 'true' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
uses: actions/cache/save@v4
with:
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
+13 -12
View File
@@ -4,19 +4,20 @@
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
preserve_hierarchy: true
base_path: ..
"preserve_hierarchy": true
"base_path": ".."
files: [
{
#
# Source files filter - PO files for Lingui
#
"source": "**/en.po",
files:
#
# Source files filter - PO files for Lingui
#
- source: packages/twenty-front/src/locales/en.po
#
# Translation files path
#
translation: '%original_path%/%locale%.po'
- source: packages/twenty-server/src/engine/core-modules/i18n/locales/en.po
translation: '%original_path%/%locale%.po'
- source: packages/twenty-emails/src/locales/en.po
translation: '%original_path%/%locale%.po'
"translation": "%original_path%/%locale%.po",
}
]
-23
View File
@@ -1,23 +0,0 @@
#
# Crowdin CLI configuration for Website translations (twenty-website)
# Project ID: 4
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
project_id: 4
preserve_hierarchy: true
base_url: 'https://twenty.api.crowdin.com'
base_path: ..
languages_mapping:
locale:
fr: fr-FR
files:
#
# Source file - PO file for Lingui
#
- source: packages/twenty-website/src/locales/en.po
#
# Translation files path
#
translation: '%original_path%/%locale%.po'
+6 -5
View File
@@ -14,8 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches \
-f event_type=auto-deploy-main
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: auto-deploy-main
client-payload: '{"github": ${{ toJson(github) }}}' # Passes the entire github context to the downstream workflow
+6 -7
View File
@@ -14,10 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
REF_NAME: ${{ github.ref_name }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches \
-f event_type=auto-deploy-tag \
-f "client_payload[github][ref_name]=$REF_NAME"
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: auto-deploy-tag
client-payload: '{"github": ${{ toJson(github) }}}' # Passes the entire github context to the downstream workflow
+2 -2
View File
@@ -21,11 +21,11 @@ jobs:
any_changed: ${{ steps.changed-files.outputs.any_changed }}
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9
uses: tj-actions/changed-files@v45
with:
files: ${{ inputs.files }}
+7 -6
View File
@@ -17,7 +17,7 @@ jobs:
NODE_OPTIONS: '--max-old-space-size=4096'
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
ref: main
@@ -41,7 +41,7 @@ jobs:
- name: Create pull request
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.6
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync AI model catalog from models.dev'
@@ -61,7 +61,8 @@ jobs:
- name: Trigger automerge
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=automated-pr-ready
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: automated-pr-ready
+14 -40
View File
@@ -66,7 +66,7 @@ jobs:
steps:
- name: Checkout current branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
@@ -164,17 +164,9 @@ jobs:
interval=5
elapsed=0
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
while [ $elapsed -lt $timeout ]; do
GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}')
if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \
curl -fsS "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then
if curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
echo "Current branch server is ready!"
break
fi
@@ -185,9 +177,10 @@ jobs:
done
if [ $elapsed -ge $timeout ]; then
echo "::warning::Timed out waiting for current branch server to serve a valid schema. Validation will skip the API diff."
echo "Timeout waiting for current branch server to start"
echo "Current server log:"
cat /tmp/current-server.log || echo "No current server log found"
exit 1
fi
- name: Download GraphQL and REST responses from current branch
@@ -331,17 +324,9 @@ jobs:
interval=5
elapsed=0
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
while [ $elapsed -lt $timeout ]; do
GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}')
if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \
curl -fsS "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then
if curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
echo "Main branch server is ready!"
break
fi
@@ -352,9 +337,10 @@ jobs:
done
if [ $elapsed -ge $timeout ]; then
echo "::warning::Timed out waiting for main branch server to serve a valid schema. Validation will skip the API diff."
echo "Timeout waiting for main branch server to start"
echo "Main server log:"
cat /tmp/main-server.log || echo "No main server log found"
exit 1
fi
- name: Download GraphQL and REST responses from main branch
@@ -421,23 +407,11 @@ jobs:
valid=true
for file in main-schema-introspection.json current-schema-introspection.json \
main-metadata-schema-introspection.json current-metadata-schema-introspection.json; do
if [ ! -f "$file" ]; then
echo "::warning::Missing GraphQL schema file: $file"
valid=false
elif ! jq -e '.data.__schema' "$file" >/dev/null 2>&1; then
echo "::warning::File $file is not a valid GraphQL introspection result. First 200 bytes: $(head -c 200 "$file")"
valid=false
fi
done
for file in main-rest-api.json current-rest-api.json \
main-metadata-schema-introspection.json current-metadata-schema-introspection.json \
main-rest-api.json current-rest-api.json \
main-rest-metadata-api.json current-rest-metadata-api.json; do
if [ ! -f "$file" ]; then
echo "::warning::Missing OpenAPI spec file: $file"
valid=false
elif ! jq -e '.openapi // .swagger' "$file" >/dev/null 2>&1; then
echo "::warning::File $file is not a valid OpenAPI spec. First 200 bytes: $(head -c 200 "$file")"
if [ ! -f "$file" ] || ! jq empty "$file" 2>/dev/null; then
echo "::warning::Invalid or missing schema file: $file"
valid=false
fi
done
@@ -634,7 +608,7 @@ jobs:
- name: Upload breaking changes report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: breaking-changes-report
path: |
@@ -0,0 +1,201 @@
name: CI Hello world App E2E
on:
# Temporarily disabled — will be re-enabled when example apps are published.
# push:
# branches:
# - main
#
# pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx run $pkg:set-local-version --releaseVersion=$CI_VERSION
done
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
echo "--- Installing last SDK versions ---"
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute hello-world logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-hello-world-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-hello-world-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-hello-world-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-hello-world]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
@@ -55,7 +55,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
@@ -105,7 +105,7 @@ jobs:
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --url http://localhost:3000
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
@@ -153,7 +153,7 @@ jobs:
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote:add --api-key ${{ env.TWENTY_API_KEY }} --url ${{ env.TWENTY_API_URL }}
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: |
@@ -0,0 +1,199 @@
name: CI Postcard App E2E
on:
# Temporarily disabled — will be re-enabled when example apps are published.
# push:
# branches:
# - main
#
# pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-postcard:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
echo "--- Installing last SDK versions ---"
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute postcard logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName postcard-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-postcard-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-postcard-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-postcard]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+5 -1
View File
@@ -30,8 +30,12 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
+6 -1
View File
@@ -28,8 +28,13 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -54,7 +54,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
+1 -28
View File
@@ -23,11 +23,9 @@ jobs:
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
packages/twenty-server/**
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
!packages/twenty-server/package.json
example-app-postcard:
needs: changed-files-check
@@ -56,7 +54,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
@@ -85,31 +83,6 @@ jobs:
working-directory: packages/twenty-apps/examples/postcard
run: npx vitest run
- name: Configure remote for SDK CLI
run: |
mkdir -p ~/.twenty
cat > ~/.twenty/config.json <<EOF
{
"version": 1,
"remotes": {
"target": {
"apiUrl": "${TWENTY_API_URL}",
"apiKey": "${TWENTY_API_KEY}",
"accessToken": "${TWENTY_API_KEY}"
}
},
"defaultRemote": "target"
}
EOF
- name: Deploy postcard app (registry install path)
working-directory: packages/twenty-apps/examples/postcard
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs deploy --remote target
- name: Install postcard app (registry install path)
working-directory: packages/twenty-apps/examples/postcard
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs install --remote target
ci-example-app-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
@@ -32,8 +32,12 @@ jobs:
matrix:
task: [build, typecheck, lint]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -46,8 +50,12 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -55,7 +63,7 @@ jobs:
- name: Build storybook
run: npx nx storybook:build twenty-front-component-renderer
- name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
@@ -68,7 +76,7 @@ jobs:
STORYBOOK_URL: http://localhost:6008
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -76,7 +84,7 @@ jobs:
- name: Build dependencies
run: npx nx build twenty-sdk
- name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@v4
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
+22 -10
View File
@@ -38,8 +38,12 @@ jobs:
env:
REACT_APP_SERVER_BASE_URL: http://localhost:3000
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -51,7 +55,7 @@ jobs:
- name: Front / Build storybook
run: npx nx storybook:build twenty-front
- name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: storybook-static
path: packages/twenty-front/storybook-static
@@ -75,7 +79,7 @@ jobs:
STORYBOOK_URL: http://localhost:6006
steps:
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -92,7 +96,7 @@ jobs:
npx nx build twenty-ui
npx nx build twenty-front-component-renderer
- name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@v4
with:
name: storybook-static
path: packages/twenty-front/storybook-static
@@ -117,7 +121,7 @@ jobs:
# exit 1
# fi
# - name: Upload coverage artifact
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# uses: actions/upload-artifact@v4
# with:
# retention-days: 1
# name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
@@ -132,12 +136,12 @@ jobs:
# matrix:
# storybook_scope: [modules, pages, performance]
# steps:
# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# - uses: actions/checkout@v4
# with:
# fetch-depth: 10
# - name: Install dependencies
# uses: ./.github/actions/yarn-install
# - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
# - uses: actions/download-artifact@v4
# with:
# pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
# merge-multiple: true
@@ -160,8 +164,12 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -195,8 +203,12 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=10240"
ANALYZE: "true"
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -206,7 +218,7 @@ jobs:
- name: Build frontend
run: npx nx build twenty-front
# - name: Upload frontend build artifact
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# uses: actions/upload-artifact@v4
# with:
# name: frontend-build
# path: packages/twenty-front/build
+5 -5
View File
@@ -41,11 +41,11 @@ jobs:
ports:
- 6379:6379
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@v4
with:
fetch-depth: 10
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@v4
with:
node-version: lts/*
@@ -53,7 +53,7 @@ jobs:
uses: ./.github/actions/yarn-install
- name: Restore Nx build cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
uses: actions/cache/restore@v4
with:
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
@@ -83,7 +83,7 @@ jobs:
- name: Save Nx build cache
if: always()
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
uses: actions/cache/save@v4
with:
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
path: |
@@ -119,7 +119,7 @@ jobs:
- name: Upload Playwright results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: |
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref }}
@@ -47,7 +47,7 @@ jobs:
printf '%s\n' "$VERSION" > version.txt
- name: Create Pull Request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
uses: peter-evans/create-pull-request@v6
with:
branch: release/${{ steps.sanitize.outputs.version }}
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
fi
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
ref: main
@@ -55,7 +55,7 @@ jobs:
git tag "v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}"
- uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5
- uses: release-drafter/release-drafter@v5
if: contains(github.event.pull_request.labels.*.name, 'create_release')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6 -2
View File
@@ -30,8 +30,12 @@ jobs:
matrix:
task: [lint, typecheck, test:unit, test:integration]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -70,7 +74,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
+7 -7
View File
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -65,7 +65,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -83,12 +83,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Get changed upgrade-version-command files
id: changed-files
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9
uses: tj-actions/changed-files@v45
with:
files: |
packages/twenty-server/src/database/commands/upgrade-version-command/**
@@ -178,7 +178,7 @@ jobs:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -278,7 +278,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -341,7 +341,7 @@ jobs:
SHARD_COUNTER: 10
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
+5 -1
View File
@@ -29,8 +29,12 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -26,9 +26,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
@@ -101,9 +101,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
+13 -5
View File
@@ -30,8 +30,12 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -44,8 +48,12 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -53,7 +61,7 @@ jobs:
- name: Build storybook
run: npx nx storybook:build twenty-ui
- name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
@@ -66,7 +74,7 @@ jobs:
STORYBOOK_URL: http://localhost:6007
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -74,7 +82,7 @@ jobs:
- name: Build dependencies
run: npx nx build twenty-shared
- name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@v4
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.action != 'closed'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Utils / Run Danger.js
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.action == 'closed' && github.event.pull_request.merged == true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run congratulate-dangerfile.js
+37 -20
View File
@@ -1,12 +1,12 @@
name: CI Website
permissions:
contents: read
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -18,36 +18,53 @@ jobs:
with:
files: |
package.json
yarn.lock
packages/twenty-website/**
packages/twenty-shared/**
website-task:
website-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
timeout-minutes: 10
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=6144'
strategy:
matrix:
task: [lint, typecheck, test]
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:website
tasks: ${{ matrix.task }}
- name: Server / Create DB
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
- name: Website / Run migrations
run: npx nx database:migrate twenty-website
env:
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
- name: Website / Build Website
run: npx nx build twenty-website
env:
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
KEYSTATIC_GITHUB_CLIENT_ID: xxx
KEYSTATIC_GITHUB_CLIENT_SECRET: xxx
KEYSTATIC_SECRET: xxx
NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG: xxx
ci-website-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, website-task]
needs: [changed-files-check, website-build]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+6 -2
View File
@@ -46,7 +46,7 @@ jobs:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
@@ -97,8 +97,12 @@ jobs:
matrix:
task: [lint, typecheck, validate]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
+16 -45
View File
@@ -8,12 +8,10 @@ on:
pull_request_review:
types: [submitted]
issues:
types: [opened]
types: [opened, assigned]
repository_dispatch:
types: [claude-core-team-issues]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.client_payload.issue_number }}
cancel-in-progress: false
@@ -21,30 +19,10 @@ concurrency:
jobs:
claude:
if: |
(
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@claude') &&
github.event.comment.user.type != 'Bot' &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
) ||
(
github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
github.event.comment.user.type != 'Bot' &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
) ||
(
github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
github.event.review.user.type != 'Bot' &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)
) ||
(
github.event_name == 'issues' &&
github.event.action == 'opened' &&
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
)
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && github.event.review.user.type != 'Bot') ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
@@ -72,14 +50,14 @@ jobs:
- 6379:6379
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run Claude Code
id: claude-code
uses: anthropics/claude-code-action@dde2242db6af13460b916652159b6ba19a598f30 # v1
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: |
@@ -144,14 +122,14 @@ jobs:
ports:
- 6379:6379
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build prompt from dispatch payload
id: prompt
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@v7
with:
script: |
const p = context.payload.client_payload;
@@ -166,7 +144,7 @@ jobs:
core.setOutput('issue_number', p.issue_number);
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@dde2242db6af13460b916652159b6ba19a598f30 # v1
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: ${{ steps.prompt.outputs.prompt }}
@@ -181,16 +159,9 @@ jobs:
}
- name: Dispatch response to ci-privileged
if: always()
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
REPO: ${{ steps.prompt.outputs.repo }}
ISSUE_NUMBER: ${{ steps.prompt.outputs.issue_number }}
RUN_ID: ${{ github.run_id }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=claude-cross-repo-response \
-f "client_payload[repo]=$REPO" \
-f "client_payload[issue_number]=$ISSUE_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[run_url]=$RUN_URL"
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: claude-cross-repo-response
client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
+6 -5
View File
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
token: ${{ github.token }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
@@ -153,7 +153,8 @@ jobs:
- name: Trigger i18n automerge
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
token: ${{ github.token }}
ref: ${{ github.ref }}
@@ -36,7 +36,7 @@ jobs:
run: yarn docs:generate-navigation-template
- name: Upload docs to Crowdin
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
@@ -1,28 +0,0 @@
name: Auto-Draft External PRs
on:
pull_request_target:
types: [opened]
permissions: {}
jobs:
dispatch:
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.author_association != 'MEMBER' &&
github.event.pull_request.author_association != 'OWNER' &&
github.event.pull_request.author_association != 'COLLABORATOR'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_NODE_ID: ${{ github.event.pull_request.node_id }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=convert-pr-to-draft \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_node_id]=$PR_NODE_ID"
+11 -6
View File
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }}
@@ -58,6 +58,7 @@ jobs:
npx nx run twenty-server:lingui:compile --strict
npx nx run twenty-emails:lingui:compile --strict
npx nx run twenty-front:lingui:compile --strict
npx nx run twenty-website-new:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
@@ -69,11 +70,13 @@ jobs:
- name: Pull translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
source: '**/en.po'
translation: '%original_path%/%locale%.po'
export_only_approved: false
localization_branch_name: i18n
base_url: 'https://twenty.api.crowdin.com'
@@ -113,6 +116,7 @@ jobs:
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
npx nx run twenty-website-new:lingui:compile
git status
git add .
if ! git diff --staged --quiet --exit-code; then
@@ -139,7 +143,8 @@ jobs:
- name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true'
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+9 -6
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
token: ${{ github.token }}
ref: main
@@ -41,6 +41,7 @@ jobs:
npx nx run twenty-server:lingui:extract
npx nx run twenty-emails:lingui:extract
npx nx run twenty-front:lingui:extract
npx nx run twenty-website-new:lingui:extract
- name: Check and commit extracted files
id: check_extract_changes
@@ -60,6 +61,7 @@ jobs:
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
npx nx run twenty-website-new:lingui:compile
- name: Check and commit compiled files
id: check_compile_changes
@@ -80,7 +82,7 @@ jobs:
- name: Upload missing translations
if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: true
@@ -105,7 +107,8 @@ jobs:
- name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+7 -14
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Get PR number from workflow run
id: pr-info
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@v7
with:
script: |
const runId = context.payload.workflow_run.id;
@@ -63,16 +63,9 @@ jobs:
- name: Dispatch to ci-privileged
if: steps.pr-info.outputs.has_pr == 'true'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
RUN_ID: ${{ steps.pr-info.outputs.run_id }}
REPOSITORY: ${{ github.repository }}
BRANCH_STATE: ${{ github.event.workflow_run.head_branch }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=breaking-changes-report \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[branch_state]=$BRANCH_STATE"
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: breaking-changes-report
client-payload: '{"pr_number": ${{ toJSON(steps.pr-info.outputs.pr_number) }}, "run_id": ${{ toJSON(steps.pr-info.outputs.run_id) }}, "repo": ${{ toJSON(github.repository) }}, "branch_state": ${{ toJSON(github.event.workflow_run.head_branch) }}}'
@@ -1,26 +0,0 @@
name: PR Review Dispatch
on:
pull_request_target:
types: [ready_for_review, synchronize]
permissions: {}
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
dispatch:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=pr-review \
-f "client_payload[pr_number]=$PR_NUMBER"
+12 -22
View File
@@ -36,27 +36,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger preview environment workflow
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/"$REPOSITORY"/dispatches \
-f event_type=preview-environment \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[repo_full_name]=$REPOSITORY"
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
event-type: preview-environment
client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}'
- name: Dispatch to ci-privileged for PR comment
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
KEEPALIVE_DISPATCH_TIME: ${{ github.event.pull_request.updated_at }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-env-url \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[keepalive_dispatch_time]=$KEEPALIVE_DISPATCH_TIME" \
-f "client_payload[repo]=$REPOSITORY"
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: preview-env-url
client-payload: '{"pr_number": ${{ toJSON(github.event.pull_request.number) }}, "keepalive_dispatch_time": ${{ toJSON(github.event.pull_request.updated_at) }}, "repo": ${{ toJSON(github.repository) }}}'
+10 -57
View File
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
@@ -56,54 +56,10 @@ jobs:
- name: Create Tunnel
id: expose-tunnel
env:
CLOUDFLARED_VERSION: '2026.3.0'
run: |
set -euo pipefail
# Install cloudflared (pinned for reproducibility)
sudo curl -fsSL -o /usr/local/bin/cloudflared \
"https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64"
sudo chmod +x /usr/local/bin/cloudflared
cloudflared --version
# Start an account-less "quick tunnel" pointing at the server container.
# Cloudflare prints the assigned https://*.trycloudflare.com URL into the log.
log_file="$RUNNER_TEMP/cloudflared.log"
: > "$log_file"
cloudflared tunnel \
--url http://localhost:3000 \
--no-autoupdate \
--logfile "$log_file" \
--loglevel info \
> "$RUNNER_TEMP/cloudflared.stdout" 2>&1 &
pid=$!
echo "$pid" > "$RUNNER_TEMP/cloudflared.pid"
echo "cloudflared PID: $pid"
# Wait up to 2 minutes for the URL to appear; fail fast if cloudflared exits.
url=''
for _ in $(seq 1 60); do
url=$(grep -oE 'https://[a-zA-Z0-9-]+\.trycloudflare\.com' "$log_file" 2>/dev/null | head -n1 || true)
[ -n "$url" ] && break
if ! kill -0 "$pid" 2>/dev/null; then
echo "cloudflared exited before producing a URL"
cat "$log_file" || true
exit 1
fi
sleep 2
done
if [ -z "$url" ]; then
echo "Timed out waiting for tunnel URL"
cat "$log_file" || true
exit 1
fi
echo "Tunnel URL: $url"
echo "tunnel-url=$url" >> "$GITHUB_OUTPUT"
uses: codetalkio/expose-tunnel@v1.5.0
with:
service: bore.pub
port: 3000
- name: Start services with correct SERVER_URL
env:
@@ -143,9 +99,9 @@ jobs:
- name: Seed Dev Workspace
run: |
cd packages/twenty-docker/
echo "Seeding light dev workspace (Apple only)..."
if ! docker compose exec -T server yarn command:prod workspace:seed:dev --light; then
echo "❌ Seeding light dev workspace failed. Dumping server logs..."
echo "Seeding full dev workspace..."
if ! docker compose exec -T server yarn command:prod -- workspace:seed:dev; then
echo "❌ Seeding full dev workspace failed. Dumping server logs..."
docker compose logs server
exit 1
fi
@@ -166,7 +122,7 @@ jobs:
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: tunnel-url
path: tunnel-url.txt
@@ -178,9 +134,6 @@ jobs:
- name: Cleanup
if: always()
run: |
if [ -f "$RUNNER_TEMP/cloudflared.pid" ]; then
kill "$(cat "$RUNNER_TEMP/cloudflared.pid")" 2>/dev/null || true
fi
cd packages/twenty-docker/
docker compose down -v
working-directory: ./
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Determine project and artifact name
id: project
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@v7
with:
script: |
const workflowName = context.payload.workflow_run.name;
@@ -43,7 +43,7 @@ jobs:
- name: Check if storybook artifact exists
id: check-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@v7
with:
script: |
const artifactName = '${{ steps.project.outputs.artifact_name }}';
@@ -65,7 +65,7 @@ jobs:
- name: Get PR number
if: steps.check-artifact.outputs.exists == 'true'
id: pr-info
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/github-script@v7
with:
script: |
const headBranch = context.payload.workflow_run.head_branch;
@@ -107,7 +107,7 @@ jobs:
- name: Download storybook artifact from triggering run
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@v4
with:
name: ${{ steps.project.outputs.artifact_name }}
path: storybook-static
@@ -120,7 +120,7 @@ jobs:
- name: Upload storybook tarball
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: ${{ steps.project.outputs.tarball_name }}
path: /tmp/${{ steps.project.outputs.tarball_file }}
@@ -128,20 +128,17 @@ jobs:
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
RUN_ID: ${{ github.run_id }}
REPOSITORY: ${{ github.repository }}
PROJECT: ${{ steps.project.outputs.project }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=visual-regression \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[project]=$PROJECT" \
-f "client_payload[branch]=$BRANCH" \
-f "client_payload[commit]=$COMMIT"
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: visual-regression
client-payload: >-
{
"pr_number": "${{ steps.pr-info.outputs.pr_number }}",
"run_id": "${{ github.run_id }}",
"repo": "${{ github.repository }}",
"project": "${{ steps.project.outputs.project }}",
"branch": "${{ github.event.workflow_run.head_branch }}",
"commit": "${{ github.event.workflow_run.head_sha }}"
}
-134
View File
@@ -1,134 +0,0 @@
# Pull down website translations from Crowdin every two hours or when triggered manually.
# When force_pull input is true, translations will be pulled regardless of compilation status.
name: 'Pull website translations from Crowdin'
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 */2 * * *' # Every two hours.
workflow_dispatch:
inputs:
force_pull:
description: 'Force pull translations regardless of compilation status'
required: false
type: boolean
default: false
workflow_call:
inputs:
force_pull:
description: 'Force pull translations regardless of compilation status'
required: false
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
pull_website_translations:
name: Pull website translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }}
- name: Setup website i18n branch
run: |
git fetch origin i18n-website || true
git checkout -B i18n-website origin/i18n-website || git checkout -b i18n-website
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
# Strict mode fails if there are missing website translations.
- name: Compile website translations
id: compile_translations_strict
run: npx nx run twenty-website:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add .
git stash
- name: Pull website translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: false
upload_translations: false
download_translations: true
source: 'packages/twenty-website/src/locales/en.po'
translation: 'packages/twenty-website/src/locales/%locale%.po'
export_only_approved: false
localization_branch_name: i18n-website
base_url: 'https://twenty.api.crowdin.com'
auto_approve_imported: false
import_eq_suggestions: false
download_sources: false
push_sources: false
skip_untranslated_strings: false
skip_untranslated_files: false
push_translations: false
create_pull_request: false
skip_ref_checkout: true
dryrun_action: false
config: '.github/crowdin-website.yml'
env:
GITHUB_TOKEN: ${{ github.token }}
# Website translations project
CROWDIN_PROJECT_ID: '4'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# As the files are extracted from a Docker container, they belong to root:root.
# We need to fix this before the next steps.
- name: Fix file permissions
run: sudo chown -R runner:docker .
- name: Compile website translations
id: compile_translations
run: |
npx nx run twenty-website:lingui:compile
git status
git add packages/twenty-website/src/locales
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Push changes
if: steps.compile_translations.outputs.changes_detected == 'true'
run: git push origin HEAD:i18n-website
- name: Create pull request
if: steps.compile_translations.outputs.changes_detected == 'true'
run: |
if git diff --name-only origin/main..HEAD | grep -q .; then
gh pr create -B main -H i18n-website --title 'i18n - website translations' --body 'Created by Github action' || true
else
echo "No file differences between branches, skipping PR creation"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true'
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
-110
View File
@@ -1,110 +0,0 @@
name: 'Push website translations to Crowdin'
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
workflow_call:
push:
branches: ['main']
paths:
- 'packages/twenty-website/**'
- '.github/crowdin-website.yml'
- '.github/workflows/website-i18n-push.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
extract_website_translations:
name: Extract and upload website translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: main
- name: Setup website i18n branch
run: |
git fetch origin i18n-website || true
git checkout -B i18n-website origin/i18n-website || git checkout -b i18n-website
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Extract website translations
run: npx nx run twenty-website:lingui:extract
- name: Check and commit extracted files
id: check_extract_changes
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add packages/twenty-website/src/locales
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: extract website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Compile website translations
run: npx nx run twenty-website:lingui:compile
- name: Check and commit compiled files
id: check_compile_changes
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add packages/twenty-website/src/locales/generated
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Push changes and create remote branch if needed
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
run: git push origin HEAD:i18n-website
- name: Upload missing website translations
if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: true
upload_translations: true
download_translations: false
localization_branch_name: i18n-website
base_url: 'https://twenty.api.crowdin.com'
config: '.github/crowdin-website.yml'
env:
# Website translations project
CROWDIN_PROJECT_ID: '4'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Create a pull request
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
run: |
if git diff --name-only origin/main..HEAD | grep -q .; then
gh pr create -B main -H i18n-website --title 'i18n - website translations' --body 'Created by Github action' || true
else
echo "No file differences between branches, skipping PR creation"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
@@ -1,69 +0,0 @@
name: 'Website Preview Dispatch'
permissions:
contents: read
on:
pull_request:
types: [opened, synchronize, reopened, closed, labeled]
paths:
- packages/twenty-website/**
- .github/workflows/website-preview-dispatch.yaml
concurrency:
# Keyed on PR number so independent PRs don't cancel each other. `github.ref`
# would resolve to the base branch under pull_request and collide.
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
trigger-build:
# Same fork PRs from outside the org don't have `secrets.*` so the dispatch
# call would fail anyway — skip explicitly to avoid noise.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action != 'closed' && (
(github.event.action == 'labeled' && github.event.label.name == 'preview-website') ||
(
(
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
) && contains(fromJSON('["opened","synchronize","reopened"]'), github.event.action)
)
)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch website-preview-build to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=website-preview-build \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[pr_head_ref]=$PR_HEAD_REF"
trigger-cleanup:
# Covers both merge and close-without-merge — pull_request `closed` fires
# for both. PRs left open forever are covered by OpenNext's
# `maxVersionAgeDays: 14` + `maxNumberOfVersions: 50` auto-pruning in
# open-next.config.ts, so nothing leaks even if cleanup never runs.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action == 'closed'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch website-preview-cleanup to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=website-preview-cleanup \
-f "client_payload[pr_number]=$PR_NUMBER"
-26
View File
@@ -1,26 +0,0 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": [
"**/dist/**",
"**/build/**",
"**/lib/**",
"**/.next/**",
"**/coverage/**",
"**/generated/**",
"**/generated-admin/**",
"**/generated-metadata/**",
"**/.cache/**",
"**/node_modules/**",
"**/*.min.js",
"**/*.snap",
"**/*.md",
"**/*.mdx",
"**/seed-project/**/*.mjs",
"packages/twenty-zapier/build/**"
]
}
-40
View File
@@ -43,46 +43,6 @@
{
"name": "packages/twenty-e2e-testing",
"path": "../packages/twenty-e2e-testing"
},
{
"name": "packages/twenty-docs",
"path": "../packages/twenty-docs"
},
{
"name": "packages/create-twenty-app",
"path": "../packages/create-twenty-app"
},
{
"name": "packages/twenty-apps",
"path": "../packages/twenty-apps"
},
{
"name": "packages/twenty-claude-skills",
"path": "../packages/twenty-claude-skills"
},
{
"name": "packages/twenty-cli",
"path": "../packages/twenty-cli"
},
{
"name": "packages/twenty-client-sdk",
"path": "../packages/twenty-client-sdk"
},
{
"name": "packages/twenty-companion",
"path": "../packages/twenty-companion"
},
{
"name": "packages/twenty-front-component-renderer",
"path": "../packages/twenty-front-component-renderer"
},
{
"name": "packages/twenty-sdk",
"path": "../packages/twenty-sdk"
},
{
"name": "packages/twenty-website",
"path": "../packages/twenty-website"
}
],
"settings": {
-4
View File
@@ -4,10 +4,6 @@ enableHardenedMode: true
enableInlineHunks: true
enableScripts: false
nodeLinker: node-modules
npmMinimalAgeGate: 3d
yarnPath: .yarn/releases/yarn-4.13.0.cjs
+1 -2
View File
@@ -110,8 +110,7 @@ packages/
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js marketing website
├── twenty-docs/ # Documentation website
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
+28 -28
View File
@@ -6,14 +6,14 @@
<h2 align="center" >The #1 Open-Source CRM</h2>
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.webp" width="12" height="12"/> Figma</a></p>
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
<p align="center">
<a href="https://www.twenty.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.webp" />
<img src="./packages/twenty-website/public/images/readme/github-cover-light.webp" alt="Twenty banner" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website/public/images/readme/github-cover-light.png" alt="Twenty banner" />
</picture>
</a>
</p>
@@ -24,7 +24,7 @@
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
<a href="https://twenty.com/resources/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
<a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
<br />
@@ -63,7 +63,7 @@ export default defineObject({
Then ship it to your workspace:
```bash
npx twenty app:publish --private
npx twenty deploy
```
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
@@ -85,17 +85,17 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" alt="Create your apps" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" alt="Stay on top with version control" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
</td>
@@ -103,17 +103,17 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" alt="All the tools you need to build anything" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.webp" alt="Customize your layouts" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
</td>
@@ -121,17 +121,17 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" alt="AI agents and chats" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" alt="Plus all the tools of a good CRM" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
</td>
@@ -152,13 +152,13 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
# Thanks
<p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.webp" height="28" alt="Chromatic" /></a>
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.webp" height="28" alt="Greptile" /></a>
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.webp" height="28" alt="Sentry" /></a>
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.webp" height="28" alt="Crowdin" /></a>
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
</p>
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
+13 -10
View File
@@ -44,12 +44,12 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "npx oxlint -c .oxlintrc.json . && (npx oxfmt --check . || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
"command": "npx oxlint -c .oxlintrc.json . && (prettier . --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
},
"configurations": {
"ci": {},
"fix": {
"command": "npx oxlint --fix -c .oxlintrc.json . && npx oxfmt ."
"command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
}
},
"dependsOn": ["^build", "twenty-oxlint-rules:build"]
@@ -57,14 +57,13 @@
"lint:diff-with-main": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": ["twenty-oxlint-rules:build"],
"options": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (npx oxfmt --check $FILES || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"pattern": "\\.(ts|tsx|js|jsx)$"
},
"configurations": {
"fix": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && npx oxfmt $FILES)"
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && prettier --write $FILES)"
}
}
},
@@ -73,14 +72,18 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "npx oxfmt --check {args.files} {args.write}",
"files": ".",
"write": ""
"command": "prettier {args.files} --check --cache {args.cache} --cache-location {args.cacheLocation} --write {args.write} --cache-strategy {args.cacheStrategy}",
"cache": true,
"cacheLocation": "../../.cache/prettier/{projectRoot}",
"cacheStrategy": "metadata",
"write": false
},
"configurations": {
"ci": {},
"ci": {
"cacheStrategy": "content"
},
"fix": {
"command": "npx oxfmt {args.files}"
"write": true
}
},
"dependsOn": ["^build"]
+157 -7
View File
@@ -1,21 +1,172 @@
{
"private": true,
"dependencies": {
"@apollo/client": "^4.0.0",
"@floating-ui/react": "^0.24.3",
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
"@radix-ui/colors": "^3.0.0",
"@sniptt/guards": "^0.2.0",
"@tabler/icons-react": "^3.31.0",
"@wyw-in-js/babel-preset": "^1.0.6",
"@wyw-in-js/vite": "^0.7.0",
"archiver": "^7.0.1",
"danger-plugin-todos": "^1.3.1",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"deep-equal": "^2.2.2",
"file-type": "16.5.4",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"googleapis": "105",
"hex-rgb": "^5.0.0",
"immer": "^10.1.1",
"jotai": "^2.17.1",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.chunk": "^4.2.0",
"lodash.compact": "^3.0.1",
"lodash.escaperegexp": "^4.1.2",
"lodash.groupby": "^4.6.0",
"lodash.identity": "^3.0.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isobject": "^3.0.2",
"lodash.kebabcase": "^4.1.1",
"lodash.mapvalues": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
"microdiff": "^1.3.2",
"next-with-linaria": "^1.3.0",
"planer": "^1.2.0",
"pluralize": "^8.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.30.3",
"react-tooltip": "^5.13.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
"semver": "^7.5.4",
"slash": "^5.1.0",
"temporal-polyfill": "^0.3.0",
"ts-key-enum": "^2.0.12",
"tslib": "^2.8.1",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.1",
"xlsx-ugnis": "^0.19.3",
"zod": "^4.1.11"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.24.6",
"@chromatic-com/storybook": "^4.1.3",
"@graphql-codegen/cli": "^3.3.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@nx/jest": "22.5.4",
"@nx/js": "22.5.4",
"@nx/react": "22.5.4",
"@nx/storybook": "22.5.4",
"@nx/vite": "22.5.4",
"@nx/web": "22.5.4",
"@oxlint/plugins": "^1.51.0",
"@sentry/types": "^8",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-vitest": "^10.3.3",
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.3.3",
"@storybook/test-runner": "^0.24.2",
"@swc-node/register": "^1.11.1",
"@swc/cli": "^0.7.10",
"@swc/core": "^1.15.11",
"@swc/helpers": "~0.5.19",
"@swc/jest": "^0.2.39",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/addressparser": "^1.0.3",
"@types/bcrypt": "^5.0.0",
"@types/bytes": "^3.1.1",
"@types/chrome": "^0.0.267",
"@types/deep-equal": "^1.0.1",
"@types/fs-extra": "^11.0.4",
"@types/graphql-fields": "^1.3.6",
"@types/inquirer": "^9.0.9",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.compact": "^3.0.9",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/lodash.groupby": "^4.6.9",
"@types/lodash.identity": "^3.0.9",
"@types/lodash.isempty": "^4.4.7",
"@types/lodash.isequal": "^4.5.7",
"@types/lodash.isobject": "^3.0.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.mapvalues": "^4.6.9",
"@types/lodash.omit": "^4.5.9",
"@types/lodash.pickby": "^4.6.9",
"@types/lodash.snakecase": "^4.1.7",
"@types/lodash.upperfirst": "^4.3.7",
"@types/ms": "^0.7.31",
"@types/node": "^24.0.0",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-microsoft": "^2.1.0",
"@types/pluralize": "^0.0.33",
"@types/react": "^18.2.39",
"@types/react-datepicker": "^6.2.0",
"@types/react-dom": "^18.2.15",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.2",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitejs/plugin-react-swc": "4.2.3",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-istanbul": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"@yarnpkg/types": "^4.0.0",
"chromatic": "^6.18.0",
"concurrently": "^8.2.2",
"danger": "^13.0.4",
"dotenv-cli": "^7.4.4",
"esbuild": "^0.25.10",
"http-server": "^14.1.1",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"jest-environment-node": "^29.4.1",
"jest-fetch-mock": "^3.0.3",
"jsdom": "~22.1.0",
"msw": "^2.12.7",
"msw-storybook-addon": "^2.0.6",
"nx": "22.5.4",
"oxfmt": "0.50.0",
"prettier": "^3.1.1",
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.20",
"storybook": "^10.3.3",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.3.3",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
"ts-node": "10.9.1",
"tsc-alias": "^1.8.16",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"verdaccio": "^6.3.1"
"verdaccio": "^6.3.1",
"vite": "^7.0.0",
"vitest": "^4.0.18"
},
"engines": {
"node": "^24.5.0",
@@ -34,9 +185,7 @@
"@lingui/core": "5.1.2",
"@types/qs": "6.9.16",
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@opentelemetry/api": "1.9.1",
"chokidar": "^3.6.0"
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch"
},
"version": "0.2.1",
"nx": {},
@@ -55,17 +204,18 @@
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website",
"packages/twenty-website-new",
"packages/twenty-docs",
"packages/twenty-e2e-testing",
"packages/twenty-shared",
"packages/twenty-sdk",
"packages/twenty-front-component-renderer",
"packages/twenty-client-sdk",
"packages/twenty-apps",
"packages/twenty-cli",
"packages/create-twenty-app",
"packages/twenty-oxlint-rules",
"packages/twenty-companion",
"packages/twenty-claude-skills"
"packages/twenty-companion"
]
},
"prettier": {
+24 -16
View File
@@ -1,7 +1,7 @@
<div align="center">
<a href="https://twenty.com">
<picture>
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website/public/images/core/logo.svg" height="128">
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/2f25922f4cd5bd61e1427c57c4f8ea224e1d552c/packages/twenty-website/public/images/core/logo.svg" height="128">
</picture>
</a>
<h1>Create Twenty App</h1>
@@ -25,31 +25,39 @@ yarn twenty dev
The scaffolder will:
1. Create a new project with TypeScript, linting, tests, and a preconfigured `twenty` CLI
2. Start a local Twenty server via Docker (pulls the latest image automatically)
3. Authenticate with the development API key
2. Optionally start a local Twenty server (Docker)
3. Open the browser for OAuth authentication
## Options
| Flag | Description |
| ---------------------------------- | --------------------------------------------------------------------- |
| `--name <name>` | Set the app name |
| `--display-name <displayName>` | Set the display name |
| `--description <description>` | Set the description |
| `--url <url>` | Twenty workspace URL (default: `http://localhost:2020`) |
| `--authentication-method <method>` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) |
| Flag | Description |
| ------------------------------ | --------------------------------------- |
| `--example <name>` | Initialize from an example |
| `--name <name>` | Set the app name (skips the prompt) |
| `--display-name <displayName>` | Set the display name (skips the prompt) |
| `--description <description>` | Set the description (skips the prompt) |
| `--skip-local-instance` | Skip the local server setup prompt |
By default (no flags), a minimal app is generated with core files and an integration test. Use `--example` to start from a richer example:
```bash
npx create-twenty-app@latest my-twenty-app --example hello-world
```
Examples are sourced from [twentyhq/twenty/packages/twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples).
## Documentation
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)**:
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started)**:
- [Quick Start](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start) — scaffold, run a local server, sync your code
- [Concepts](https://docs.twenty.com/developers/extend/apps/getting-started/concepts) — how apps work: entity model, sandboxing, lifecycle
- [Operations](https://docs.twenty.com/developers/extend/apps/operations/overview) — CLI, testing, CI, deploy and publish
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started) — step-by-step setup, project structure, server management, CI
- [Building Apps](https://docs.twenty.com/developers/extend/apps/building) — entity definitions, API clients, testing
- [Publishing](https://docs.twenty.com/developers/extend/apps/publishing) — deploy, npm publish, marketplace
## Troubleshooting
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty docker:logs`.
- Auth not working: run `yarn twenty remote:add --local` to re-authenticate.
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: make sure you are logged in to Twenty in the browser, then run `yarn twenty remote add`.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing
+3 -6
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.7.0",
"version": "2.1.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -32,6 +32,7 @@
"chalk": "^5.3.0",
"commander": "^12.0.0",
"fs-extra": "^11.2.0",
"inquirer": "^10.0.0",
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"lodash.startcase": "^4.4.0",
@@ -39,16 +40,12 @@
"uuid": "^13.0.0"
},
"devDependencies": {
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@types/fs-extra": "^11.0.0",
"@types/jest": "^30.0.0",
"@types/inquirer": "^9.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"jest": "29.7.0",
"jest-environment-node": "^29.4.1",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"vite": "^7.0.0",
+17 -36
View File
@@ -1,10 +1,7 @@
#!/usr/bin/env node
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
import {
type AuthenticationMethod,
CreateAppCommand,
} from '@/create-app.command';
import { CreateAppCommand } from '@/create-app.command';
import packageJson from '../package.json';
const program = new Command(packageJson.name)
@@ -15,26 +12,30 @@ const program = new Command(packageJson.name)
'Output the current version of create-twenty-app.',
)
.argument('[directory]')
.option('-n, --name <name>', 'Application name')
.option('-d, --display-name <displayName>', 'Application display name')
.option('--description <description>', 'Application description')
.option('--url <url>', 'Twenty server URL (default: http://localhost:2020)')
.option('--api-url <apiUrl>', '[deprecated: use --url]')
.option('--example <name>', 'Initialize from an example')
.option('-n, --name <name>', 'Application name (skips prompt)')
.option(
'--authentication-method <method>',
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
'-d, --display-name <displayName>',
'Application display name (skips prompt)',
)
.option(
'--description <description>',
'Application description (skips prompt)',
)
.option(
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
)
.helpOption('-h, --help', 'Display this help message.')
.action(
async (
directory?: string,
options?: {
example?: string;
name?: string;
displayName?: string;
description?: string;
url?: string;
apiUrl?: string;
authenticationMethod?: AuthenticationMethod;
skipLocalInstance?: boolean;
},
) => {
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
@@ -51,33 +52,13 @@ const program = new Command(packageJson.name)
process.exit(1);
}
if (
options?.authenticationMethod &&
!['oauth', 'apiKey'].includes(options.authenticationMethod)
) {
console.error(
chalk.red(
'Error: --authentication-method must be "oauth" or "apiKey".',
),
);
process.exit(1);
}
if (options?.apiUrl) {
console.warn(
chalk.yellow('Warning: --api-url is deprecated. Use --url instead.'),
);
}
const serverUrl = (options?.url ?? options?.apiUrl)?.replace(/\/+$/, '');
await new CreateAppCommand().execute({
directory,
example: options?.example,
name: options?.name,
displayName: options?.displayName,
description: options?.description,
serverUrl,
authenticationMethod: options?.authenticationMethod,
skipLocalInstance: options?.skipLocalInstance,
});
},
);
@@ -1,67 +0,0 @@
## Base documentation
- Getting started:
- https://docs.twenty.com/developers/extend/apps/getting-started/quick-start.md
- https://docs.twenty.com/developers/extend/apps/getting-started/concepts.md
- https://docs.twenty.com/developers/extend/apps/getting-started/project-structure.md
- https://docs.twenty.com/developers/extend/apps/getting-started/local-server.md
- https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding.md
- https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting.md
- Config:
- https://docs.twenty.com/developers/extend/apps/config/overview.md
- https://docs.twenty.com/developers/extend/apps/config/application.md
- https://docs.twenty.com/developers/extend/apps/config/roles.md
- https://docs.twenty.com/developers/extend/apps/config/install-hooks.md
- https://docs.twenty.com/developers/extend/apps/config/public-assets.md
- Data:
- https://docs.twenty.com/developers/extend/apps/data/overview.md
- https://docs.twenty.com/developers/extend/apps/data/objects.md
- https://docs.twenty.com/developers/extend/apps/data/extending-objects.md
- https://docs.twenty.com/developers/extend/apps/data/relations.md
- Logic:
- https://docs.twenty.com/developers/extend/apps/logic/overview.md
- https://docs.twenty.com/developers/extend/apps/logic/logic-functions.md
- https://docs.twenty.com/developers/extend/apps/logic/skills-and-agents.md
- https://docs.twenty.com/developers/extend/apps/logic/connections.md
- Layout:
- https://docs.twenty.com/developers/extend/apps/layout/overview.md
- https://docs.twenty.com/developers/extend/apps/layout/views.md
- https://docs.twenty.com/developers/extend/apps/layout/navigation-menu-items.md
- https://docs.twenty.com/developers/extend/apps/layout/page-layouts.md
- https://docs.twenty.com/developers/extend/apps/layout/front-components.md
- https://docs.twenty.com/developers/extend/apps/layout/command-menu-items.md
- Operations:
- https://docs.twenty.com/developers/extend/apps/operations/overview.md
- https://docs.twenty.com/developers/extend/apps/operations/cli.md
- https://docs.twenty.com/developers/extend/apps/operations/testing.md
- https://docs.twenty.com/developers/extend/apps/operations/publishing.md
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
## Best practice
It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options:
| Entity type | Command | Generated file |
| -------------------- | ---------------------------------------- | ------------------------------------- |
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
This helps automatically generate required IDs etc.
@@ -2,21 +2,10 @@ This is a [Twenty](https://twenty.com) application bootstrapped with [`create-tw
## Getting Started
This app was scaffolded with a local Twenty server running at [http://localhost:2020](http://localhost:2020).
Login with the default development credentials: `tim@apple.dev` / `tim@apple.dev`.
Run `yarn twenty help` to list all available commands.
## Useful Commands
- `yarn twenty dev` - Start the development server and sync your app
- `yarn twenty docker:status` - Check the local Twenty server status
- `yarn twenty docker:start` - Start the local Twenty server
- `yarn test` - Run integration tests
## Learn More
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57)
@@ -11,7 +11,7 @@ permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:2020
TWENTY_DEPLOY_URL: http://localhost:3000
concurrency:
group: cd-${{ github.ref }}
@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none">
<rect width="80" height="80" rx="16" fill="#141414"/>
<rect x="20" y="20" width="16" height="16" rx="4" fill="#FAFAFA"/>
<rect x="44" y="20" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
<rect x="20" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
<rect x="44" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.3"/>
</svg>

Before

Width:  |  Height:  |  Size: 454 B

@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
@@ -4,10 +4,12 @@ import {
APP_DESCRIPTION,
APP_DISPLAY_NAME,
APPLICATION_UNIVERSAL_IDENTIFIER,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: APP_DISPLAY_NAME,
description: APP_DESCRIPTION,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
@@ -2,10 +2,3 @@ export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED';
export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
'UUID-TO-BE-GENERATED';
@@ -1,11 +1,11 @@
import { defineApplicationRole } from 'twenty-sdk/define';
import { defineRole } from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineApplicationRole({
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: `${APP_DISPLAY_NAME} default function role`,
description: `${APP_DISPLAY_NAME} default function role`,
@@ -1,291 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
Avatar,
IconBox,
IconHierarchy,
IconLayout,
IconSettingsAutomation,
} from 'twenty-sdk/ui';
import { useState } from 'react';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
const DOCS_BASE_URL = 'https://docs.twenty.com/developers/extend/apps';
const CATEGORIES = [
{
title: 'Data model',
color: '#73D08D',
items: [
{ label: 'CUSTOM OBJECT', href: `${DOCS_BASE_URL}/data/objects` },
{
label: 'CUSTOM FIELDS',
href: `${DOCS_BASE_URL}/data/extending-objects`,
},
],
rotation: '2.4deg',
},
{
title: 'Logic',
color: '#F4D345',
items: [
{
label: 'TOOLS',
href: `${DOCS_BASE_URL}/logic/logic-functions`,
},
{
label: 'LOGIC FUNCTION',
href: `${DOCS_BASE_URL}/logic/logic-functions`,
},
{
label: 'SKILLS',
href: `${DOCS_BASE_URL}/logic/skills-and-agents`,
},
],
rotation: '0deg',
},
{
title: 'Layout',
color: '#C4A2E0',
items: [
{ label: 'VIEWS', href: `${DOCS_BASE_URL}/layout/views` },
{ label: 'WIDGETS', href: `${DOCS_BASE_URL}/layout/page-layouts` },
{
label: 'LAYOUT PAGES',
href: `${DOCS_BASE_URL}/layout/page-layouts`,
},
{
label: 'COMMANDS',
href: `${DOCS_BASE_URL}/layout/command-menu-items`,
},
],
rotation: '-2.8deg',
},
] as const;
const ArrowUpRight = ({ color = '#999' }: { color?: string }) => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path
d="M4.5 3.5H10.5V9.5M10.5 3.5L3.5 10.5"
stroke={color}
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const CategoryCard = ({
title,
color,
items,
rotation,
}: {
title: string;
color: string;
items: ReadonlyArray<{ label: string; href: string }>;
rotation: string;
}) => {
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const CategoryIcon = () => {
if (title === 'Data model') {
return <IconHierarchy color={color} size={'20px'} />;
}
if (title === 'Logic') {
return <IconSettingsAutomation color={color} size={'20px'} />;
}
if (title === 'Layout') {
return <IconLayout color={color} size={'20px'} />;
}
};
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
border: `1px solid ${color}80`,
borderRadius: '12px',
overflow: 'hidden',
width: '240px',
background: '#FFFFFF',
transform: `rotate(${rotation})`,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
}}
>
<div
style={{
padding: '16px 20px',
background: `${color}22`,
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<CategoryIcon />
<span
style={{
fontSize: '16px',
fontWeight: 600,
color: color,
}}
>
{title}
</span>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: '8px',
gap: '4px',
}}
>
{items.map((item) => {
const isHovered = hoveredItem === item.label;
return (
<a
key={item.label}
href={item.href}
target="_blank"
rel="noopener noreferrer"
onMouseEnter={() => setHoveredItem(item.label)}
onMouseLeave={() => setHoveredItem(null)}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
textDecoration: 'none',
cursor: 'pointer',
padding: '10px 12px',
borderRadius: '8px',
background: isHovered ? '#0000000A' : 'transparent',
transition: 'background 0.15s',
}}
>
<IconBox color={color} size={'20px'} />
<span
style={{
fontSize: '13px',
fontWeight: 300,
color: '#333',
letterSpacing: '0.5px',
flex: 1,
}}
>
{item.label}
</span>
{isHovered && <ArrowUpRight />}
</a>
);
})}
</div>
</div>
);
};
const MainPage = () => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
fontFamily:
'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
gap: '8px',
padding: '40px',
}}
>
<Avatar
placeholder={APP_DISPLAY_NAME}
placeholderColorSeed={APP_DISPLAY_NAME}
size="xl"
/>
<span
style={{
fontSize: '24px',
fontWeight: 600,
color: '#333',
marginTop: '8px',
}}
>
{APP_DISPLAY_NAME}
</span>
<span
style={{
fontSize: '13px',
color: '#888',
textAlign: 'center',
lineHeight: '1.5',
}}
>
Was installed successfully.
<br />
You can now add content to your app.
</span>
<a
href="/settings/applications#installed"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
marginTop: '16px',
fontSize: '13px',
color: '#333',
textDecoration: 'none',
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
background: '#fafafa',
transition: 'background 0.15s, border-color 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f0f0f0';
e.currentTarget.style.borderColor = '#ccc';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#fafafa';
e.currentTarget.style.borderColor = '#e0e0e0';
}}
>
Open app settings
<ArrowUpRight color="#333" />
</a>
<div
style={{
display: 'flex',
gap: '16px',
marginTop: '32px',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'flex-start',
}}
>
{CATEGORIES.map((category) => (
<CategoryCard
key={category.title}
title={category.title}
color={category.color}
items={category.items}
rotation={category.rotation}
/>
))}
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
description: `${APP_DISPLAY_NAME} front component displaying the app logo and name`,
component: MainPage,
});
@@ -1,19 +0,0 @@
import {
defineNavigationMenuItem,
NavigationMenuItemType,
} from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineNavigationMenuItem({
universalIdentifier: MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
icon: 'IconFile',
position: -1,
type: NavigationMenuItemType.PAGE_LAYOUT,
pageLayoutUniversalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
});
@@ -1,37 +0,0 @@
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default definePageLayout({
universalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
type: 'STANDALONE_PAGE',
tabs: [
{
universalIdentifier: MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Overview',
position: 0,
icon: 'IconApps',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
title: ' ',
type: 'FRONT_COMPONENT',
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
@@ -1,172 +1,93 @@
import { copyBaseApplicationProject } from '@/utils/app-template';
import { downloadExample } from '@/utils/download-example';
import { convertToLabel } from '@/utils/convert-to-label';
import { install } from '@/utils/install';
import { tryGitInit } from '@/utils/try-git-init';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import kebabCase from 'lodash.kebabcase';
import * as path from 'path';
import { basename } from 'path';
import { spawn } from 'node:child_process';
import {
authLogin,
authLoginOAuth,
checkDockerRunning,
ConfigService,
DEV_API_KEY,
DEV_API_URL,
detectLocalServer,
serverStart,
type ServerStartResult,
} from 'twenty-sdk/cli';
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
import {
getDockerInstallInstructions,
isDockerInstalled,
} from '@/utils/docker-install';
import { isDefined } from 'twenty-shared/utils';
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
const IMAGE = 'twentycrm/twenty-app-dev:latest';
export type AuthenticationMethod = 'oauth' | 'apiKey';
type CreateAppOptions = {
directory?: string;
example?: string;
name?: string;
displayName?: string;
description?: string;
serverUrl?: string;
authenticationMethod?: AuthenticationMethod;
skipLocalInstance?: boolean;
};
export class CreateAppCommand {
private stepCounter = 0;
private totalSteps = 0;
async execute(options: CreateAppOptions = {}): Promise<void> {
const { appName, appDisplayName, appDirectory, appDescription } =
this.getAppInfos(options);
const serverUrl = options.serverUrl ?? DEV_API_URL;
const skipLocalInstance = serverUrl !== DEV_API_URL;
if (!skipLocalInstance && !isDockerInstalled()) {
console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n'));
process.exit(1);
}
if (skipLocalInstance && options.authenticationMethod === 'apiKey') {
console.log(
chalk.yellow(
'API key authentication is only supported on a local Docker instance. Ignoring and switching to OAuth authentication.',
),
);
}
const authenticationMethod = skipLocalInstance
? 'oauth'
: (options.authenticationMethod ?? 'apiKey');
await this.getAppInfos(options);
try {
await this.validateDirectory(appDirectory);
this.totalSteps = this.computeTotalSteps({
skipLocalInstance,
});
this.stepCounter = 0;
const dockerPullPromise =
!skipLocalInstance && checkDockerRunning()
? this.pullImageInBackground()
: Promise.resolve(false);
this.logPlan({ appName, appDisplayName, appDescription, appDirectory });
this.logNextStep('Creating project directory');
this.logCreationInfo({ appDirectory, appName });
await fs.ensureDir(appDirectory);
this.logDetail(appDirectory);
this.logNextStep('Scaffolding project files');
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
onProgress: (message) => this.logDetail(message),
});
this.logNextStep('Installing dependencies');
await install(appDirectory, (message) => this.logDetail(message));
this.logNextStep('Initializing Git repository');
const gitInitialized = await tryGitInit(appDirectory);
if (gitInitialized) {
this.logDetail('Initialized on branch main');
this.logDetail('Created initial commit');
} else {
this.logDetail(
'Skipped (Git unavailable, initialization failed, or already in a repository)',
if (options.example) {
const exampleSucceeded = await this.tryDownloadExample(
options.example,
appDirectory,
);
}
console.log('');
let authSucceeded = false;
let resolvedServerUrl = serverUrl;
let serverReady = skipLocalInstance;
if (!skipLocalInstance) {
this.logNextStep('Starting Twenty server');
const serverResult = await this.ensureDockerServer(dockerPullPromise);
if (isDefined(serverResult.url)) {
resolvedServerUrl = serverResult.url;
serverReady = true;
}
}
if (serverReady) {
this.logNextStep('Authenticating');
authSucceeded = await this.tryExistingAuth(resolvedServerUrl);
if (authSucceeded) {
this.logDetail('Reusing existing credentials');
} else if (authenticationMethod === 'oauth') {
this.logDetail('Starting OAuth flow');
authSucceeded = await this.authenticateWithOAuth(resolvedServerUrl);
} else {
this.logDetail('Using development API key');
authSucceeded = await this.authenticateWithDevKey(resolvedServerUrl);
}
}
this.logNextStep('Installing application');
let syncSucceeded = false;
if (serverReady && authSucceeded) {
syncSucceeded = await this.syncApplication(appDirectory);
if (!syncSucceeded) {
this.logDetail('Sync failed. Run `yarn twenty dev --once` manually.');
return;
if (!exampleSucceeded) {
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
});
}
} else {
this.logDetail('Skipped (server or authentication not available)');
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
});
}
if (syncSucceeded) {
await this.openMainPage(appDirectory, resolvedServerUrl);
await install(appDirectory);
await tryGitInit(appDirectory);
let serverResult: ServerStartResult | undefined;
if (!options.skipLocalInstance) {
const shouldStartServer = await this.shouldStartServer();
if (shouldStartServer) {
const startResult = await serverStart({
onProgress: (message: string) => console.log(chalk.gray(message)),
});
if (startResult.success) {
serverResult = startResult.data;
await this.promptConnectToLocal(serverResult.url);
} else {
console.log(chalk.yellow(`\n${startResult.error.message}`));
}
}
}
this.logSuccess(appDirectory, resolvedServerUrl, authSucceeded);
this.logSuccess(appDirectory, serverResult);
} catch (error) {
console.error(
chalk.red('\nCreate application failed:'),
@@ -176,42 +97,66 @@ export class CreateAppCommand {
}
}
private computeTotalSteps({
skipLocalInstance,
}: {
skipLocalInstance: boolean;
}): number {
let steps = 4; // directory, scaffold, install, git
if (!skipLocalInstance) {
steps += 1; // start server
}
steps += 1; // authenticate (oauth or apiKey)
steps += 1; // sync application
return steps;
}
private getAppInfos(options: CreateAppOptions): {
private async getAppInfos(options: CreateAppOptions): Promise<{
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
} {
}> {
const { directory } = options;
const hasName = isDefined(options.name) || isDefined(directory);
const hasDisplayName = isDefined(options.displayName);
const hasDescription = isDefined(options.description);
const hasExample = isDefined(options.example);
const { name, displayName, description } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Application name:',
when: () => !hasName && !hasExample,
default: 'my-twenty-app',
validate: (input) => {
if (input.length === 0) return 'Application name is required';
return true;
},
},
{
type: 'input',
name: 'displayName',
message: 'Application display name:',
when: () => !hasDisplayName && !hasExample,
default: (answers: { name?: string }) => {
return convertToLabel(
answers?.name ?? options.name ?? directory ?? '',
);
},
},
{
type: 'input',
name: 'description',
message: 'Application description (optional):',
when: () => !hasDescription && !hasExample,
default: '',
},
]);
const appName = (
options.name ??
options.directory ??
name ??
directory ??
options.example ??
'my-twenty-app'
).trim();
const appDisplayName =
options.displayName?.trim() || convertToLabel(appName);
(options.displayName ?? displayName)?.trim() || convertToLabel(appName);
const appDescription = (options.description ?? '').trim();
const appDescription = (options.description ?? description ?? '').trim();
const appDirectory = options.directory
? path.join(CURRENT_EXECUTION_DIRECTORY, options.directory)
const appDirectory = directory
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
return { appName, appDisplayName, appDirectory, appDescription };
@@ -230,333 +175,96 @@ export class CreateAppCommand {
}
}
private logPlan({
appName,
appDisplayName,
appDescription,
appDirectory,
}: {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
}): void {
console.log(chalk.blue('\nCreating Twenty Application\n'));
console.log(chalk.white(` Name: ${appName}`));
console.log(chalk.white(` Display name: ${appDisplayName}`));
private async tryDownloadExample(
example: string,
appDirectory: string,
): Promise<boolean> {
try {
await downloadExample(example, appDirectory);
if (appDescription) {
console.log(chalk.white(` Description: ${appDescription}`));
}
console.log(chalk.white(` Directory: ${appDirectory}`));
console.log('');
}
private logNextStep(title: string): void {
this.stepCounter++;
console.log(
chalk.blue(`\n[${this.stepCounter}/${this.totalSteps}]`) +
chalk.white(` ${title}...`),
);
}
private logDetail(message: string): void {
console.log(chalk.gray(`${message}`));
}
private pullImageInBackground(): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn('docker', ['pull', IMAGE], { stdio: 'ignore' });
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
});
}
private async ensureDockerServer(
dockerPullPromise: Promise<boolean>,
): Promise<{ url?: string }> {
if (!checkDockerRunning()) {
console.log(
chalk.yellow(
'\n Docker is installed but not running.\n' +
' Please start Docker and run this command again.\n',
return true;
} catch (error) {
console.error(
chalk.red(
`\n${error instanceof Error ? error.message : 'Failed to download example.'}`,
),
);
return {};
}
this.logDetail('Ensuring latest Twenty server image...');
const pullSucceeded = await dockerPullPromise;
if (!pullSucceeded) {
this.logDetail(
'Image pull failed, continuing with cached image if available...',
);
}
const startResult = await serverStart({
onProgress: (message: string) => this.logDetail(message),
});
if (startResult.success) {
return { url: startResult.data.url };
}
console.log(chalk.yellow(`\n ${startResult.error.message}`));
return {};
}
private async openMainPage(
appDirectory: string,
serverUrl: string,
): Promise<void> {
try {
const configService = new ConfigService();
const config = await configService.getConfig();
const token = config.twentyCLIAccessToken ?? config.apiKey;
if (!token) {
return;
}
const [universalIdentifier, frontUrl] = await Promise.all([
this.readMainPageLayoutUniversalIdentifier(appDirectory),
this.resolveWorkspaceFrontUrl(serverUrl, token),
const { useTemplate } = await inquirer.prompt([
{
type: 'confirm',
name: 'useTemplate',
message: 'Would you like to create a default template app instead?',
default: true,
},
]);
if (!universalIdentifier || !frontUrl) {
return;
if (!useTemplate) {
process.exit(1);
}
const pageLayoutId = await this.resolvePageLayoutId(
serverUrl,
universalIdentifier,
token,
// Clean up any partial files from the failed download
await fs.emptyDir(appDirectory);
return false;
}
}
private logCreationInfo({
appDirectory,
appName,
}: {
appDirectory: string;
appName: string;
}): void {
console.log(
chalk.blue('\n', 'Creating Twenty Application\n'),
chalk.gray(`- Directory: ${appDirectory}\n`, `- Name: ${appName}\n`),
);
}
private async shouldStartServer(): Promise<boolean> {
const existingServerUrl = await detectLocalServer();
if (existingServerUrl) {
return true;
}
const { startDocker } = await inquirer.prompt([
{
type: 'confirm',
name: 'startDocker',
message:
'No running Twenty instance found. Would you like to start one using Docker?',
default: true,
},
]);
return startDocker;
}
private async promptConnectToLocal(serverUrl: string): Promise<void> {
const { shouldAuthenticate } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldAuthenticate',
message: `Would you like to authenticate to the local Twenty instance (${serverUrl})?`,
default: true,
},
]);
if (!shouldAuthenticate) {
console.log(
chalk.gray(
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
),
);
if (!pageLayoutId) {
return;
}
const url = `${frontUrl}/page/${pageLayoutId}`;
this.logDetail(`Opening app welcome page: ${url}`);
this.openInBrowser(url);
} catch {
// Best-effort — don't fail the scaffold if browser open fails
}
}
private async resolveWorkspaceFrontUrl(
serverUrl: string,
token: string,
): Promise<string | null> {
const query = `{ currentWorkspace { workspaceUrls { subdomainUrl customUrl } } }`;
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query }),
});
if (!response.ok) {
return null;
}
const body = (await response.json()) as {
data?: {
currentWorkspace?: {
workspaceUrls?: { subdomainUrl?: string; customUrl?: string };
};
};
};
const urls = body.data?.currentWorkspace?.workspaceUrls;
if (!urls) {
return null;
}
const frontUrl = urls.customUrl ?? urls.subdomainUrl;
return frontUrl ? normalizeUrl(frontUrl) : null;
}
private async readMainPageLayoutUniversalIdentifier(
appDirectory: string,
): Promise<string | null> {
const filePath = path.join(
appDirectory,
'src',
'constants',
'universal-identifiers.ts',
);
const content = await fs.readFile(filePath, 'utf-8');
const match = content.match(
/MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER\s*=\s*'([^']+)'/,
);
return match?.[1] ?? null;
}
private async resolvePageLayoutId(
serverUrl: string,
universalIdentifier: string,
token: string,
): Promise<string | null> {
const query = `{ getPageLayouts { id universalIdentifier } }`;
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query }),
});
if (!response.ok) {
return null;
}
const body = (await response.json()) as {
data?: {
getPageLayouts?: { id: string; universalIdentifier: string }[];
};
};
const matching = body.data?.getPageLayouts?.find(
(layout) => layout.universalIdentifier === universalIdentifier,
);
return matching?.id ?? null;
}
private sanitizeBrowserUrl(url: string): string | null {
if (/[^\u0020-\u007E]/.test(url)) {
return null;
}
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null;
}
return parsed.toString();
} catch {
return null;
}
}
private openInBrowser(url: string): void {
const safeUrl = this.sanitizeBrowserUrl(url);
if (!safeUrl) {
return;
}
const isWindows = process.platform === 'win32';
const command = isWindows
? 'rundll32'
: process.platform === 'darwin'
? 'open'
: 'xdg-open';
const args = isWindows
? ['url.dll,FileProtocolHandler', safeUrl]
: [safeUrl];
const child = spawn(command, args, {
stdio: 'ignore',
detached: !isWindows,
});
child.on('error', () => undefined);
if (!isWindows) {
child.unref();
}
}
private async syncApplication(appDirectory: string): Promise<boolean> {
this.logDetail('Running `yarn twenty dev --once`...');
return new Promise((resolve) => {
const child = spawn('yarn', ['twenty', 'dev', '--once'], {
cwd: appDirectory,
stdio: ['inherit', 'pipe', 'pipe'],
});
child.stdout?.resume();
child.stderr?.resume();
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
});
}
private async tryExistingAuth(serverUrl: string): Promise<boolean> {
try {
const configService = new ConfigService();
const remoteNames = await configService.getRemotes();
for (const remoteName of remoteNames) {
const remoteConfig = await configService.getConfigForRemote(remoteName);
if (remoteConfig.apiUrl !== serverUrl) {
continue;
}
const token = remoteConfig.twentyCLIAccessToken ?? remoteConfig.apiKey;
if (!token) {
continue;
}
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: '{ currentWorkspace { id } }',
}),
});
if (!response.ok) {
continue;
}
const body = (await response.json()) as {
data?: { currentWorkspace?: { id: string } };
errors?: unknown[];
};
if (isDefined(body.data?.currentWorkspace) && !body.errors) {
ConfigService.setActiveRemote(remoteName);
await configService.setDefaultRemote(remoteName);
return true;
}
}
return false;
} catch {
return false;
}
}
private async authenticateWithDevKey(serverUrl: string): Promise<boolean> {
try {
const result = await authLogin({
apiKey: DEV_API_KEY,
const result = await authLoginOAuth({
apiUrl: serverUrl,
remote: 'local',
});
@@ -565,111 +273,41 @@ export class CreateAppCommand {
const configService = new ConfigService();
await configService.setDefaultRemote('local');
this.logDetail('Authenticated as tim@apple.dev (development API key)');
return true;
} else {
console.log(
chalk.yellow(
'Authentication failed. Run `yarn twenty remote add --local` manually.',
),
);
}
console.log(
chalk.yellow(
' Authentication failed. Run `yarn twenty remote:add --local` manually.',
),
);
return false;
} catch {
console.log(
chalk.yellow(
' Authentication failed. Run `yarn twenty remote:add --local` manually.',
'Authentication failed. Run `yarn twenty remote add` manually.',
),
);
return false;
}
}
private deriveRemoteName(url: string): string {
try {
return new URL(url).hostname.replace(/\./g, '-');
} catch {
return 'remote';
}
}
private async authenticateWithOAuth(serverUrl: string): Promise<boolean> {
try {
const remoteName = this.deriveRemoteName(serverUrl);
ConfigService.setActiveRemote(remoteName);
this.logDetail('Opening browser for OAuth...');
const result = await authLoginOAuth({ apiUrl: serverUrl });
if (result.success) {
const configService = new ConfigService();
await configService.setDefaultRemote(remoteName);
this.logDetail(`Authenticated via OAuth to ${serverUrl}`);
return true;
}
console.log(
chalk.yellow(
` OAuth failed: ${result.error.message}\n` +
` Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
),
);
return false;
} catch {
console.log(
chalk.yellow(
` Authentication failed. Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
),
);
return false;
}
}
private logSuccess(
appDirectory: string,
serverUrl: string,
authSucceeded: boolean,
serverResult?: ServerStartResult,
): void {
const dirName = basename(appDirectory);
console.log(chalk.green('\nApplication created successfully!\n'));
console.log(chalk.blue('\nApplication created. Next steps:'));
console.log(chalk.gray(`- cd ${dirName}`));
console.log(chalk.white(' Next steps:\n'));
let stepNumber = 1;
console.log(chalk.white(` ${stepNumber}. Navigate to your project`));
console.log(chalk.cyan(` cd ${dirName}\n`));
stepNumber++;
if (!authSucceeded) {
console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`));
if (!serverResult) {
console.log(
chalk.cyan(' yarn twenty remote:add --url <your-instance-url>\n'),
chalk.gray(
'- yarn twenty remote add # Authenticate with Twenty',
),
);
stepNumber++;
}
console.log(chalk.white(` ${stepNumber}. Start developing`));
console.log(chalk.cyan(' yarn twenty dev\n'));
stepNumber++;
console.log(chalk.white(` ${stepNumber}. Open your twenty instance`));
console.log(chalk.cyan(` ${serverUrl}\n`));
console.log(
chalk.gray(
' Documentation: https://docs.twenty.com/developers/extend/capabilities/apps',
),
chalk.gray('- yarn twenty dev # Start dev mode'),
);
}
}
@@ -17,7 +17,6 @@ const UNIVERSAL_IDENTIFIERS_PATH = join(
'constants',
'universal-identifiers.ts',
);
const YARNRC_PATH = 'yarnrc.yml';
// Template content matching template/src/constants/universal-identifiers.ts
const TEMPLATE_UNIVERSAL_IDENTIFIERS = `export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
@@ -77,16 +76,11 @@ describe('copyBaseApplicationProject', () => {
appDirectory: testAppDirectory,
});
// Two fs.copy calls: (1) the template directory, (2) AGENTS.md → CLAUDE.md
expect(fs.copy).toHaveBeenCalledTimes(2);
expect(fs.copy).toHaveBeenCalledTimes(1);
expect(fs.copy).toHaveBeenCalledWith(
expect.stringContaining('template'),
testAppDirectory,
);
expect(fs.copy).toHaveBeenCalledWith(
join(testAppDirectory, 'AGENTS.md'),
join(testAppDirectory, 'CLAUDE.md'),
);
});
it('should replace placeholders in universal-identifiers.ts with real values', async () => {
@@ -174,27 +168,6 @@ describe('copyBaseApplicationProject', () => {
expect(publicDirectoryContents).toHaveLength(0);
});
it('should rename yarnrc.yml to .yarnrc.yml in the scaffolded project', async () => {
await fs.writeFile(
join(testAppDirectory, YARNRC_PATH),
'nodeLinker: node-modules',
);
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
expect(await fs.pathExists(join(testAppDirectory, YARNRC_PATH))).toBe(
false,
);
expect(await fs.pathExists(join(testAppDirectory, '.yarnrc.yml'))).toBe(
true,
);
});
it('should handle empty description', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
@@ -3,6 +3,7 @@ import { join } from 'path';
import { v4 } from 'uuid';
import createTwentyAppPackageJson from 'package.json';
import chalk from 'chalk';
const SRC_FOLDER = 'src';
@@ -11,33 +12,25 @@ export const copyBaseApplicationProject = async ({
appDisplayName,
appDescription,
appDirectory,
onProgress,
}: {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
onProgress?: (message: string) => void;
}) => {
onProgress?.('Copying base template');
console.log(chalk.gray('Generating application project...'));
await fs.copy(join(__dirname, './constants/template'), appDirectory);
onProgress?.('Configuring dotfiles (.gitignore, .github, .yarnrc.yml)');
await renameDotfiles({ appDirectory });
onProgress?.('Mirroring AGENTS.md to CLAUDE.md');
await mirrorAgentsToClaude({ appDirectory });
await addEmptyPublicDirectory({ appDirectory });
onProgress?.('Generating unique application identifiers');
await generateUniversalIdentifiers({
appDisplayName,
appDescription,
appDirectory,
});
onProgress?.('Updating package.json');
await updatePackageJson({ appName, appDirectory });
};
@@ -47,7 +40,6 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
const renames = [
{ from: 'gitignore', to: '.gitignore' },
{ from: 'github', to: '.github' },
{ from: 'yarnrc.yml', to: '.yarnrc.yml' },
];
for (const { from, to } of renames) {
@@ -59,19 +51,6 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
}
};
// AGENTS.md is the cross-tool standard; Claude Code prefers CLAUDE.md and only
// falls back to AGENTS.md, so we mirror the file to keep a single source of truth.
const mirrorAgentsToClaude = async ({
appDirectory,
}: {
appDirectory: string;
}) => {
await fs.copy(
join(appDirectory, 'AGENTS.md'),
join(appDirectory, 'CLAUDE.md'),
);
};
const addEmptyPublicDirectory = async ({
appDirectory,
}: {
@@ -1,33 +0,0 @@
import { execSync } from 'node:child_process';
const DOCKER_DOWNLOAD_URLS: Record<string, string> = {
darwin: 'https://docs.docker.com/desktop/setup/install/mac-install/',
win32: 'https://docs.docker.com/desktop/setup/install/windows-install/',
linux: 'https://docs.docker.com/engine/install/',
};
export const isDockerInstalled = (): boolean => {
try {
execSync('docker --version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
export const getDockerInstallInstructions = (): string => {
const url =
DOCKER_DOWNLOAD_URLS[process.platform] ?? DOCKER_DOWNLOAD_URLS.linux;
return [
' Docker is required but not installed.',
'',
` Install Docker: ${url}`,
'',
' Then run this command again.',
'',
' Alternatively, connect to an existing Twenty instance:',
' npx create-twenty-app@latest my-twenty-app --url <your-instance-url>',
].join('\n');
};
@@ -0,0 +1,179 @@
import { execSync } from 'node:child_process';
import * as fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'node:os';
import chalk from 'chalk';
const TWENTY_REPO_OWNER = 'twentyhq';
const TWENTY_REPO_NAME = 'twenty';
const TWENTY_FALLBACK_REF = 'main';
const TWENTY_EXAMPLES_PATH = 'packages/twenty-apps/examples';
const TWENTY_EXAMPLES_URL = `https://github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tree/${TWENTY_FALLBACK_REF}/${TWENTY_EXAMPLES_PATH}`;
// Fetches the latest release tag from the repo, or falls back to main
const resolveRef = async (): Promise<string> => {
const response = await fetch(
`https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/releases/latest`,
{ headers: { Accept: 'application/vnd.github.v3+json' } },
);
if (response.ok) {
const release = (await response.json()) as { tag_name: string };
return release.tag_name;
}
return TWENTY_FALLBACK_REF;
};
// Uses the GitHub Contents API to list directories — fast and doesn't download the repo
const fetchGitHubDirectoryContents = async (
path: string,
ref: string,
): Promise<{ name: string; type: string }[] | null> => {
const apiUrl = `https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/contents/${path}?ref=${ref}`;
const response = await fetch(apiUrl, {
headers: { Accept: 'application/vnd.github.v3+json' },
});
if (!response.ok) {
return null;
}
const data = await response.json();
if (!Array.isArray(data)) {
return null;
}
return data as { name: string; type: string }[];
};
const listAvailableExamples = async (ref: string): Promise<string[]> => {
const contents = await fetchGitHubDirectoryContents(
TWENTY_EXAMPLES_PATH,
ref,
);
if (!contents) {
return [];
}
return contents
.filter((entry) => entry.type === 'dir')
.map((entry) => entry.name);
};
const validateExampleExists = async (
exampleName: string,
ref: string,
): Promise<void> => {
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
const contents = await fetchGitHubDirectoryContents(examplePath, ref);
if (contents !== null) {
return;
}
const availableExamples = await listAvailableExamples(ref);
throw new Error(
`Example "${exampleName}" not found.\n\n` +
(availableExamples.length > 0
? `Available examples:\n${availableExamples.map((name) => ` - ${name}`).join('\n')}\n\n`
: '') +
`Browse all examples: ${TWENTY_EXAMPLES_URL}`,
);
};
export const downloadExample = async (
exampleName: string,
targetDirectory: string,
): Promise<void> => {
if (
exampleName.includes('/') ||
exampleName.includes('\\') ||
exampleName.includes('..')
) {
throw new Error(
`Invalid example name: "${exampleName}". Example names must be simple directory names (e.g., "hello-world").`,
);
}
const ref = await resolveRef();
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
console.log(chalk.gray(`Resolving examples from ref '${ref}'...`));
await validateExampleExists(exampleName, ref);
console.log(chalk.gray(`Example '${examplePath}' validated successfully.`));
const tarballUrl = `https://codeload.github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tar.gz/${ref}`;
const tempDir = join(
tmpdir(),
`create-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
try {
await fs.ensureDir(tempDir);
console.log(chalk.gray(`Downloading tarball from ${tarballUrl}...`));
const response = await fetch(tarballUrl);
if (!response.ok) {
if (response.status === 404) {
throw new Error(
`Could not find repository: ${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME} (ref: ${ref})`,
);
}
throw new Error(
`Failed to download from GitHub: ${response.status} ${response.statusText}`,
);
}
console.log(chalk.gray('Tarball downloaded. Writing to disk...'));
const tarballPath = join(tempDir, 'archive.tar.gz');
const buffer = Buffer.from(await response.arrayBuffer());
await fs.writeFile(tarballPath, buffer);
console.log(
chalk.gray(
`Tarball saved (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Extracting...`,
),
);
execSync(`tar xzf "${tarballPath}" -C "${tempDir}"`, {
stdio: 'pipe',
});
// GitHub tarballs extract to a directory named {repo}-{ref}/
const extractedEntries = await fs.readdir(tempDir);
const extractedDir = extractedEntries.find(
(entry) => entry !== 'archive.tar.gz',
);
if (!extractedDir) {
throw new Error('Failed to extract archive: no directory found');
}
const sourcePath = join(tempDir, extractedDir, examplePath);
if (!(await fs.pathExists(sourcePath))) {
throw new Error(
`Example directory not found in archive: "${examplePath}"`,
);
}
await fs.copy(sourcePath, targetDirectory);
} finally {
await fs.remove(tempDir);
}
};
@@ -4,18 +4,14 @@ import { exec } from 'child_process';
const execPromise = promisify(exec);
export const install = async (
root: string,
onProgress?: (message: string) => void,
) => {
onProgress?.('Enabling corepack');
export const install = async (root: string) => {
console.log(chalk.gray('Installing yarn dependencies...'));
try {
await execPromise('corepack enable', { cwd: root });
} catch (error: any) {
console.warn(chalk.yellow('corepack enable failed:'), error.stderr);
console.warn(chalk.yellow('corepack enabled failed:'), error.stderr);
}
onProgress?.('Running yarn install');
try {
await execPromise('yarn install', { cwd: root });
} catch (error: any) {
@@ -93,7 +93,7 @@ yarn install
# Register your local Twenty server as a remote (interactive prompt).
# When asked for the URL use http://localhost:2021 and paste an API key
# from Settings -> Developers in the Twenty UI.
yarn twenty remote:add
yarn twenty remote add
# Build, install, and watch for changes.
yarn twenty dev
@@ -107,8 +107,8 @@ watching `src/`. Edit any file and the change is re-synced within seconds.
```bash
cd packages/twenty-apps/community/github-connector
yarn install
yarn twenty remote:add # same prompts as above
yarn twenty app:install # builds and installs once
yarn twenty remote add # same prompts as above
yarn twenty install # builds and installs once
```
## Configure authentication
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote:add --api-url http://localhost:2020 --as local
yarn twenty remote add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status
yarn twenty remote:use # Set default remote
yarn twenty remote:list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
# Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
```
## Integration Tests
@@ -13,7 +13,7 @@ beforeAll(async () => {
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
@@ -28,6 +28,12 @@ yarn install
yarn twenty dev
```
Or use it as a template via create-twenty-app:
```bash
npx create-twenty-app@latest my-app --example postcard
```
## Learn more
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started)
@@ -19,8 +19,8 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "2.2.0",
"twenty-sdk": "2.2.0"
"twenty-client-sdk": "0.9.0",
"twenty-sdk": "0.9.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
@@ -8,6 +8,7 @@ export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Postcard App',
description: 'Send postcards easily with Twenty',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -1,12 +1,8 @@
import { useEffect } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
unmountFrontComponent,
updateProgress,
useRecordId,
} from 'twenty-sdk/front-component';
import { useRecordId, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { isDefined } from 'twenty-shared/utils';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
const SYSTEM_PROMPT =
@@ -18,9 +14,9 @@ const GeneratePostCardEffect = () => {
const recordId = useRecordId();
useEffect(() => {
if (recordId === null) {
if (!isDefined(recordId)) {
enqueueSnackbar({
message: 'Please select exactly one record',
message: 'No record selected',
variant: 'error',
});
unmountFrontComponent();
@@ -1,12 +1,8 @@
import { useEffect } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
unmountFrontComponent,
updateProgress,
useRecordId,
} from 'twenty-sdk/front-component';
import { useRecordId, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { isDefined } from 'twenty-shared/utils';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
const SendPostCardsEffect = () => {
@@ -18,25 +14,55 @@ const SendPostCardsEffect = () => {
await updateProgress(0.1);
const client = new CoreApiClient();
let idsToSend: string[] = [];
if (isDefined(recordId)) {
idsToSend = [recordId];
} else {
const { postCards } = await client.query({
postCards: {
__args: {
filter: { status: { eq: 'DRAFT' } },
},
edges: { node: { id: true } },
},
});
idsToSend =
postCards?.edges?.map(
(edge: { node: { id: string; status: true } }) => edge.node.id,
) ?? [];
}
if (idsToSend.length === 0) {
await updateProgress(1);
await unmountFrontComponent();
return;
}
await updateProgress(0.3);
if (recordId) {
for (let i = 0; i < idsToSend.length; i++) {
await client.mutation({
updatePostCard: {
__args: {
id: recordId,
id: idsToSend[i],
data: { status: 'SENT' },
},
id: true,
},
});
await enqueueSnackbar({
message: `Postcard sent`,
variant: 'success',
});
await updateProgress(0.3 + (0.7 * (i + 1)) / idsToSend.length);
}
const count = idsToSend.length;
await enqueueSnackbar({
message: `${count} postcard${count > 1 ? 's' : ''} sent`,
variant: 'success',
});
await unmountFrontComponent();
} catch (error) {
const message =
@@ -3317,8 +3317,8 @@ __metadata:
oxlint: "npm:^0.16.0"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
twenty-client-sdk: "npm:2.2.0"
twenty-sdk: "npm:2.2.0"
twenty-client-sdk: "npm:0.9.0"
twenty-sdk: "npm:0.9.0"
typescript: "npm:^5.9.3"
vite-tsconfig-paths: "npm:^4.2.1"
vitest: "npm:^3.1.1"
@@ -4059,21 +4059,21 @@ __metadata:
languageName: node
linkType: hard
"twenty-client-sdk@npm:2.2.0":
version: 2.2.0
resolution: "twenty-client-sdk@npm:2.2.0"
"twenty-client-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-client-sdk@npm:0.9.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
esbuild: "npm:^0.25.0"
graphql: "npm:^16.8.1"
checksum: 10c0/90122593efa53440ae386960211a2c274b13a410942bf6f9bc35cf952ae55fe83280e29074677fd284fa3c79069fc578980db06a92a143c08e043f865dbbbd3c
checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54
languageName: node
linkType: hard
"twenty-sdk@npm:2.2.0":
version: 2.2.0
resolution: "twenty-sdk@npm:2.2.0"
"twenty-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-sdk@npm:0.9.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
@@ -4093,7 +4093,7 @@ __metadata:
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
tinyglobby: "npm:^0.2.15"
twenty-client-sdk: "npm:2.2.0"
twenty-client-sdk: "npm:0.9.0"
typescript: "npm:^5.9.2"
uuid: "npm:^13.0.0"
vite: "npm:^7.0.0"
@@ -4101,7 +4101,7 @@ __metadata:
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.cjs
checksum: 10c0/8978b4b0aa5ea282c8f76799347d9d1812901ec609bc2bd9270261a6bc5589ad361f6102a06900a4511a29a8378cd3811c2c0bad9f01cb8adadabca6d96dfd0b
checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3
languageName: node
linkType: hard
@@ -10,8 +10,8 @@
"packageManager": "yarn@4.9.2",
"scripts": {
"dev": "twenty dev",
"exec": "twenty dev:fn-exec",
"uninstall": "twenty app:uninstall",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
@@ -4,5 +4,6 @@ export default defineApplication({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
displayName: 'Root App',
description: 'An app with all entities at root level',
icon: 'IconFolder',
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
});
@@ -5,6 +5,7 @@ export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Rich App',
description: 'A simple rich app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -10,13 +10,5 @@ export default defineLogicFunction({
description: 'Look up a recipient by name to find their details',
timeoutSeconds: 5,
handler,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
recipientName: { type: 'string' },
},
required: ['recipientName'],
},
},
isTool: true,
});
@@ -30,31 +30,31 @@ export default defineView({
],
groups: [
{
universalIdentifier: 'e9ed34f1-3c3d-41b1-869b-00aae0033d9c',
universalIdentifier: 'bg1a2b3c-0001-4a7b-8c9d-0e1f2a3b4c5d',
fieldValue: 'DRAFT',
position: 0,
isVisible: true,
},
{
universalIdentifier: '19b1a3c1-53f0-4d32-b072-d645dac98e38',
universalIdentifier: 'bg1a2b3c-0002-4a7b-8c9d-0e1f2a3b4c5d',
fieldValue: 'SENT',
position: 1,
isVisible: true,
},
{
universalIdentifier: 'f545cb5a-370d-423f-9b4e-278a9a465bdf',
universalIdentifier: 'bg1a2b3c-0003-4a7b-8c9d-0e1f2a3b4c5d',
fieldValue: 'DELIVERED',
position: 2,
isVisible: true,
},
{
universalIdentifier: '5d4c6d5f-af53-4cd0-a843-df38915561b2',
universalIdentifier: 'bg1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d',
fieldValue: 'RETURNED',
position: 3,
isVisible: true,
},
{
universalIdentifier: '5ebbd7dc-9939-4594-b2a0-519269b4531f',
universalIdentifier: 'bg1a2b3c-0005-4a7b-8c9d-0e1f2a3b4c5d',
fieldValue: 'LOST',
position: 4,
isVisible: true,
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote:add --api-url http://localhost:2020 --as local
yarn twenty remote add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status
yarn twenty remote:use # Set default remote
yarn twenty remote:list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
# Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
```
## LLMs instructions
@@ -111,8 +111,7 @@ export default defineLogicFunction({
description:
'Structured web search powered by Exa. Returns entity-aware results with category filtering (companies, people, research papers, news, and other content types). Prefer this when the query benefits from structured data or a specific category. For general real-time web browsing, prefer the native `web_search` tool when it is available.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: exaWebSearchInputSchema,
},
isTool: true,
toolInputSchema: exaWebSearchInputSchema,
handler,
});

Some files were not shown because too many files have changed in this diff Show More