Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24b074f436 | ||
|
|
181b48105e |
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git stash:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Setting up Docker Compose environment...' && cd packages/twenty-docker && cp -n docker-compose.yml docker-compose.dev.yml || true && echo 'Dependencies installed and docker-compose prepared'",
|
||||
"start": "sudo service docker start && echo 'Docker service started' && cd packages/twenty-docker && echo 'Installing yq for YAML processing...' && sudo apt-get update -qq && sudo apt-get install -y wget && wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq && echo 'Patching docker-compose for local development...' && yq eval 'del(.services.server.image)' -i docker-compose.dev.yml && yq eval '.services.server.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.server.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && yq eval 'del(.services.worker.image)' -i docker-compose.dev.yml && yq eval '.services.worker.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.worker.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && echo 'Setting up .env file with database configuration...' && echo 'SERVER_URL=http://localhost:3000' > .env && echo 'APP_SECRET='$(openssl rand -base64 32) >> .env && echo 'PG_DATABASE_PASSWORD='$(openssl rand -hex 16) >> .env && echo 'PG_DATABASE_URL=postgres://postgres:password@localhost:5432/postgres' >> .env && echo 'SIGN_IN_PREFILLED=true' >> .env && echo 'Building and starting services...' && docker-compose -f docker-compose.dev.yml up -d --build && echo 'Waiting for services to initialize...' && sleep 30 && echo 'Checking service health...' && docker-compose -f docker-compose.dev.yml ps && echo 'Environment setup complete!'",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Database Setup & Seed",
|
||||
"command": "sleep 40 && cd packages/twenty-docker && echo 'Waiting for PostgreSQL to be ready...' && until docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres; do echo 'Waiting for PostgreSQL...'; sleep 5; done && echo 'PostgreSQL is ready!' && echo 'Waiting for Twenty server to be healthy...' && until docker-compose -f docker-compose.dev.yml exec -T server curl --fail http://localhost:3000/healthz 2>/dev/null; do echo 'Waiting for server...'; sleep 5; done && echo 'Server is healthy!' && echo 'Running database setup and seeding...' && docker-compose -f docker-compose.dev.yml exec -T server npx nx database:reset twenty-server && echo 'Database seeded successfully!' && bash"
|
||||
},
|
||||
{
|
||||
"name": "Application Logs",
|
||||
"command": "sleep 35 && cd packages/twenty-docker && echo 'Following application logs...' && docker-compose -f docker-compose.dev.yml logs -f server worker"
|
||||
},
|
||||
{
|
||||
"name": "Service Monitor",
|
||||
"command": "sleep 15 && cd packages/twenty-docker && echo '=== Service Status Monitor ===' && while true; do clear; echo '=== Service Status at $(date) ===' && docker-compose -f docker-compose.dev.yml ps && echo '\\n=== Health Status ===' && (docker-compose -f docker-compose.dev.yml exec -T server curl -s http://localhost:3000/healthz 2>/dev/null && echo '✅ Twenty Server: Healthy') || echo '❌ Twenty Server: Not Ready' && (docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres 2>/dev/null && echo '✅ PostgreSQL: Ready') || echo '❌ PostgreSQL: Not Ready' && echo '\\n=== Database Connection Test ===' && docker-compose -f docker-compose.dev.yml exec -T server node -e \"const { Client } = require('pg'); const client = new Client({connectionString: process.env.PG_DATABASE_URL}); client.connect().then(() => {console.log('✅ Database Connection: OK'); client.end();}).catch(e => console.log('❌ Database Connection: Failed -', e.message));\" || echo 'Connection test failed' && sleep 45; done"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"install": "yarn install",
|
||||
"start": "(sudo service docker start || service docker start || true) && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Development Server",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.git
|
||||
.env
|
||||
**/node_modules
|
||||
node_modules
|
||||
.nx/cache
|
||||
packages/twenty-server/.env
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
storage: /tmp/verdaccio-storage
|
||||
auth:
|
||||
htpasswd:
|
||||
file: /tmp/verdaccio-htpasswd
|
||||
max_users: 100
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
packages:
|
||||
'twenty-sdk':
|
||||
access: $all
|
||||
publish: $all
|
||||
'create-twenty-app':
|
||||
access: $all
|
||||
publish: $all
|
||||
'**':
|
||||
access: $all
|
||||
proxy: npmjs
|
||||
log: { type: stdout, format: pretty, level: warn }
|
||||
@@ -1,68 +0,0 @@
|
||||
name: AI Catalog Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Daily at 6 AM UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sync-catalog:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Run catalog sync
|
||||
run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/ai-sync-models-dev.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
if git diff --quiet packages/twenty-server/src/engine/metadata-modules/ai/ai-models/ai-providers.json; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync AI model catalog from models.dev'
|
||||
title: 'chore: sync AI model catalog from models.dev'
|
||||
body: |
|
||||
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev).
|
||||
|
||||
This PR updates pricing, context windows, and model availability based on the latest data.
|
||||
New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically.
|
||||
Deprecated models are detected based on cost-efficiency within the same model family.
|
||||
|
||||
**Please review before merging** — verify no critical models were incorrectly deprecated.
|
||||
branch: chore/ai-catalog-sync
|
||||
base: main
|
||||
labels: ai, automated
|
||||
delete-branch: true
|
||||
|
||||
- name: Trigger automerge
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: automated-pr-ready
|
||||
@@ -1,184 +0,0 @@
|
||||
name: CI Create App E2E
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
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-shared/**
|
||||
packages/twenty-server/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
!packages/twenty-server/package.json
|
||||
create-app-e2e:
|
||||
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: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- 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 twenty-sdk create-twenty-app --releaseVersion=$CI_VERSION
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
npx nx build twenty-sdk
|
||||
npx nx build create-twenty-app
|
||||
|
||||
- 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: |
|
||||
npm set //localhost:4873/:_authToken "ci-auth-token"
|
||||
|
||||
for pkg in twenty-sdk create-twenty-app; do
|
||||
cd packages/$pkg
|
||||
npm publish --registry http://localhost:4873 --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 --exhaustive --display-name "Test App" --description "E2E test 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
|
||||
|
||||
- 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.devDependencies['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: Build server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Create and setup database
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npx nx start twenty-server &
|
||||
echo "Waiting for server to be ready..."
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
|
||||
|
||||
- name: Authenticate with twenty-server
|
||||
env:
|
||||
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --token $SEED_API_KEY --url http://localhost:3000
|
||||
|
||||
- name: Build scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty build
|
||||
test -d .twenty/output
|
||||
|
||||
- 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: Run scaffolded app integration test
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
ci-create-app-e2e-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, create-app-e2e]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -151,40 +151,6 @@ jobs:
|
||||
# npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
|
||||
# - name: Checking coverage
|
||||
# run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
|
||||
visual-regression-dispatch:
|
||||
needs: front-sb-build
|
||||
if: github.event_name == 'pull_request'
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static
|
||||
- name: Package storybook
|
||||
run: tar -czf /tmp/storybook-twenty-front.tar.gz -C storybook-static .
|
||||
- name: Upload storybook tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-front-tarball
|
||||
path: /tmp/storybook-twenty-front.tar.gz
|
||||
retention-days: 1
|
||||
- name: Dispatch to ci-privileged
|
||||
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": "${{ github.event.pull_request.number }}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"project": "twenty-front",
|
||||
"branch": "${{ github.head_ref }}",
|
||||
"commit": "${{ github.event.pull_request.head.sha }}"
|
||||
}
|
||||
front-task:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
|
||||
@@ -10,8 +10,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CI Docker
|
||||
name: CI Docker Compose
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
files: |
|
||||
packages/twenty-docker/**
|
||||
docker-compose.yml
|
||||
test-compose:
|
||||
test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
@@ -30,10 +30,10 @@ jobs:
|
||||
- name: Run compose
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
# change image to localbuild using yq
|
||||
yq eval 'del(.services.server.image)' -i docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
|
||||
yq eval '.services.server.build.target = "twenty"' -i docker-compose.yml
|
||||
yq eval '.services.server.restart = "no"' -i docker-compose.yml
|
||||
|
||||
echo "Setting up .env file..."
|
||||
@@ -89,64 +89,11 @@ jobs:
|
||||
echo "Still waiting for server... (${count}/300s)"
|
||||
done
|
||||
working-directory: ./packages/twenty-docker/
|
||||
test-app-dev:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Create frontend placeholder
|
||||
run: |
|
||||
mkdir -p packages/twenty-front/build
|
||||
echo '<html><body>CI placeholder</body></html>' > packages/twenty-front/build/index.html
|
||||
- name: Build app-dev image
|
||||
run: |
|
||||
docker build \
|
||||
--target twenty-app-dev \
|
||||
-f packages/twenty-docker/twenty/Dockerfile \
|
||||
-t twenty-app-dev-ci \
|
||||
.
|
||||
- name: Start container
|
||||
run: |
|
||||
docker run -d --name twenty-app-dev \
|
||||
-p 3000:3000 \
|
||||
twenty-app-dev-ci
|
||||
docker logs twenty-app-dev -f &
|
||||
- name: Wait for server health
|
||||
run: |
|
||||
echo "Waiting for twenty-app-dev to become healthy..."
|
||||
count=0
|
||||
while true; do
|
||||
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/healthz 2>/dev/null || echo "000")
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "Server is healthy!"
|
||||
curl -s http://localhost:3000/healthz
|
||||
break
|
||||
fi
|
||||
|
||||
container_status=$(docker inspect --format='{{.State.Status}}' twenty-app-dev 2>/dev/null || echo "unknown")
|
||||
if [ "$container_status" = "exited" ]; then
|
||||
echo "Container exited unexpectedly"
|
||||
docker logs twenty-app-dev
|
||||
exit 1
|
||||
fi
|
||||
|
||||
count=$((count+1))
|
||||
if [ $count -gt 300 ]; then
|
||||
echo "Server did not become healthy within 5 minutes"
|
||||
docker logs twenty-app-dev
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting... (${count}/300s) [HTTP ${status}]"
|
||||
sleep 1
|
||||
done
|
||||
ci-test-docker-status-check:
|
||||
ci-test-docker-compose-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, test-compose, test-app-dev]
|
||||
needs: [changed-files-check, test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
name: CI UI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
ui-task:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }}
|
||||
run: npx nx ${{ matrix.task }} twenty-ui
|
||||
ui-sb-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
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@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build storybook
|
||||
run: npx nx storybook:build twenty-ui
|
||||
- name: Upload storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui
|
||||
path: packages/twenty-ui/storybook-static
|
||||
retention-days: 1
|
||||
ui-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: ui-sb-build
|
||||
env:
|
||||
STORYBOOK_URL: http://localhost:6007
|
||||
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: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui
|
||||
path: packages/twenty-ui/storybook-static
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd packages/twenty-ui
|
||||
npx playwright install
|
||||
- name: Serve storybook & run tests
|
||||
run: |
|
||||
npx http-server packages/twenty-ui/storybook-static --port 6007 --silent &
|
||||
timeout 30 bash -c 'until curl -sf http://localhost:6007 > /dev/null 2>&1; do sleep 1; done'
|
||||
npx nx storybook:test twenty-ui
|
||||
visual-regression-dispatch:
|
||||
needs: ui-sb-build
|
||||
if: github.event_name == 'pull_request'
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui
|
||||
path: storybook-static
|
||||
- name: Package storybook
|
||||
run: tar -czf /tmp/storybook-twenty-ui.tar.gz -C storybook-static .
|
||||
- name: Upload storybook tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui-tarball
|
||||
path: /tmp/storybook-twenty-ui.tar.gz
|
||||
retention-days: 1
|
||||
- name: Dispatch to ci-privileged
|
||||
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": "${{ github.event.pull_request.number }}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"project": "twenty-ui",
|
||||
"branch": "${{ github.head_ref }}",
|
||||
"commit": "${{ github.event.pull_request.head.sha }}"
|
||||
}
|
||||
ci-ui-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
ui-task,
|
||||
ui-sb-build,
|
||||
ui-sb-test,
|
||||
]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -40,8 +40,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
@@ -112,7 +111,7 @@ jobs:
|
||||
run: yarn docs:generate-paths
|
||||
|
||||
- name: Commit artifacts to pull request branch
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git add packages/twenty-docs/docs.json packages/twenty-docs/navigation/navigation.template.json packages/twenty-shared/src/constants/DocumentationPaths.ts
|
||||
if git diff --staged --quiet --exit-code; then
|
||||
@@ -151,10 +150,3 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -138,11 +138,3 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -102,11 +102,3 @@ jobs:
|
||||
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'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -24,12 +24,10 @@ jobs:
|
||||
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
|
||||
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-postgres", "${PG_DATABASE_URL}"],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
|
||||
@@ -80,17 +80,6 @@ npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/mi
|
||||
npx nx run twenty-server:command workspace:sync-metadata
|
||||
```
|
||||
|
||||
### Database Inspection (Postgres MCP)
|
||||
|
||||
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
|
||||
- Inspect workspace data, metadata, and object definitions while developing
|
||||
- Verify migration results (columns, types, constraints) after running migrations
|
||||
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
|
||||
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
|
||||
- Inspect metadata tables to debug GraphQL schema generation or `workspace:sync-metadata` issues
|
||||
|
||||
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
|
||||
|
||||
### GraphQL
|
||||
```bash
|
||||
# Generate GraphQL types (run after schema changes)
|
||||
@@ -199,22 +188,13 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
||||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- Clear mocks between tests with `jest.clearAllMocks()`
|
||||
|
||||
## Dev Environment Setup
|
||||
## CI Environment (GitHub Actions)
|
||||
|
||||
All dev environments (Claude Code web, Cursor, local) use one script:
|
||||
When running in CI, the dev environment is **not** pre-configured. Dependencies are installed but builds, env files, and databases are not set up.
|
||||
|
||||
```bash
|
||||
bash packages/twenty-utils/setup-dev-env.sh
|
||||
```
|
||||
|
||||
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
|
||||
|
||||
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
|
||||
- `--down` — stop services
|
||||
- `--reset` — wipe data and restart fresh
|
||||
- **Before running tests, builds, lint, type checks, or DB operations**, run: `bash packages/twenty-utils/setup-dev-env.sh`
|
||||
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
|
||||
|
||||
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
|
||||
- The script is idempotent and safe to run multiple times.
|
||||
|
||||
## Important Files
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
# Installation
|
||||
|
||||
See:
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-host/capabilities/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/contribute/capabilities/local-setup)
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
|
||||
|
||||
# Why Twenty
|
||||
|
||||
@@ -36,7 +36,7 @@ We built Twenty for three reasons:
|
||||
|
||||
**A fresh start is required to build a better experience.** We can learn from past mistakes and craft a cohesive experience inspired by new UX patterns from tools like Notion, Airtable or Linear.
|
||||
|
||||
**We believe in open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
**We believe in Open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
@@ -136,14 +136,6 @@
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"set-local-version": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npm pkg set version={args.releaseVersion}"
|
||||
}
|
||||
},
|
||||
"storybook:build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^4.0.0",
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
@@ -164,7 +164,6 @@
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"verdaccio": "^6.3.1",
|
||||
"vite": "^7.0.0",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
@@ -207,6 +206,7 @@
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"packages/twenty-sdk",
|
||||
"packages/twenty-standard-application",
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/create-twenty-app",
|
||||
|
||||
@@ -25,47 +25,41 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ (recommended) and Yarn 4
|
||||
- Docker (for the local Twenty dev server)
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Scaffold a new app — the CLI will offer to start a local Twenty server
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# The scaffolder can automatically:
|
||||
# 1. Start a local Twenty server (Docker)
|
||||
# 2. Open the browser to log in (tim@apple.dev / tim@apple.dev)
|
||||
# 3. Authenticate your app via OAuth
|
||||
# Get help and list all available commands
|
||||
yarn twenty help
|
||||
|
||||
# Or do it manually:
|
||||
yarn twenty server start # Start local Twenty server
|
||||
yarn twenty remote add --local # Authenticate via OAuth
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
|
||||
# Add a new entity to your application (guided)
|
||||
yarn twenty entity:add
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
|
||||
yarn twenty app:dev
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn twenty logs
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function with a JSON payload
|
||||
yarn twenty exec -n my-function -p '{"key": "value"}'
|
||||
yarn twenty function:execute -n my-function -p '{"key": "value"}'
|
||||
|
||||
# Execute the pre-install function
|
||||
yarn twenty exec --preInstall
|
||||
yarn twenty function:execute --preInstall
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
|
||||
# Build the app for distribution
|
||||
yarn twenty build
|
||||
|
||||
# Publish the app to npm or directly to a Twenty server
|
||||
yarn twenty publish
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty uninstall
|
||||
yarn twenty app:uninstall
|
||||
```
|
||||
|
||||
## Scaffolding modes
|
||||
@@ -107,77 +101,43 @@ npx create-twenty-app@latest my-app -m
|
||||
- `skills/example-skill.ts` — Example AI agent skill definition
|
||||
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
|
||||
|
||||
## Local server
|
||||
|
||||
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker). You can also manage it manually:
|
||||
|
||||
```bash
|
||||
yarn twenty server start # Start (pulls image if needed)
|
||||
yarn twenty server status # Check if it's healthy
|
||||
yarn twenty server logs # Stream logs
|
||||
yarn twenty server stop # Stop (data is preserved)
|
||||
yarn twenty server reset # Wipe all data and start fresh
|
||||
```
|
||||
|
||||
The server is pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
|
||||
|
||||
### How to use a local Twenty instance
|
||||
|
||||
If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`):
|
||||
|
||||
```bash
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- Run `yarn twenty help` to see all available commands.
|
||||
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace via OAuth.
|
||||
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty app:dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
|
||||
## Build and publish your application
|
||||
## Publish your application
|
||||
|
||||
Once your app is ready, build and publish it using the CLI:
|
||||
Applications are currently stored in `twenty/packages/twenty-apps`.
|
||||
|
||||
```bash
|
||||
# Build the app (output goes to .twenty/output/)
|
||||
yarn twenty build
|
||||
|
||||
# Build and create a tarball (.tgz) for distribution
|
||||
yarn twenty build --tarball
|
||||
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty publish --tag beta
|
||||
|
||||
# Deploy directly to a Twenty server (builds, uploads, and installs in one step)
|
||||
yarn twenty deploy
|
||||
```
|
||||
|
||||
### Publish to the Twenty marketplace
|
||||
|
||||
You can also contribute your application to the curated marketplace:
|
||||
You can share your application with all Twenty users:
|
||||
|
||||
```bash
|
||||
# pull the Twenty project
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
cd twenty
|
||||
|
||||
# create a new branch
|
||||
git checkout -b feature/my-awesome-app
|
||||
```
|
||||
|
||||
- Copy your app folder into `twenty/packages/twenty-apps`.
|
||||
- Commit your changes and open a pull request on https://github.com/twentyhq/twenty
|
||||
|
||||
```bash
|
||||
git commit -m "Add new application"
|
||||
git push
|
||||
```
|
||||
|
||||
Our team reviews contributions for quality, security, and reusability before merging.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
|
||||
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add --local`.
|
||||
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
|
||||
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn twenty app:dev` is running — it auto‑generates the typed client.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.8.0-canary.2",
|
||||
"version": "0.7.0-canary.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"command": "node dist/cli.cjs"
|
||||
}
|
||||
},
|
||||
"set-local-version": {},
|
||||
"typecheck": {},
|
||||
"lint": {},
|
||||
"test": {
|
||||
|
||||
@@ -18,23 +18,6 @@ const program = new Command(packageJson.name)
|
||||
'-m, --minimal',
|
||||
'Create only core entities (application-config and default-role)',
|
||||
)
|
||||
.option('-n, --name <name>', 'Application name (skips prompt)')
|
||||
.option(
|
||||
'-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',
|
||||
)
|
||||
.option(
|
||||
'-p, --port <port>',
|
||||
'Port of an existing Twenty server (skips Docker setup)',
|
||||
)
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(
|
||||
async (
|
||||
@@ -42,11 +25,6 @@ const program = new Command(packageJson.name)
|
||||
options?: {
|
||||
exhaustive?: boolean;
|
||||
minimal?: boolean;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
port?: string;
|
||||
},
|
||||
) => {
|
||||
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
|
||||
@@ -69,24 +47,9 @@ const program = new Command(packageJson.name)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options?.name !== undefined && options.name.trim().length === 0) {
|
||||
console.error(chalk.red('Error: --name cannot be empty.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
|
||||
|
||||
const port = options?.port ? parseInt(options.port, 10) : undefined;
|
||||
|
||||
await new CreateAppCommand().execute({
|
||||
directory,
|
||||
mode,
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
port,
|
||||
});
|
||||
await new CreateAppCommand().execute(directory, mode);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -11,4 +11,3 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -1,11 +1,62 @@
|
||||
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
Run `yarn twenty help` to list all available commands.
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Available Commands
|
||||
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
|
||||
|
||||
```bash
|
||||
# Make sure a Twenty server is running at http://localhost:3000
|
||||
yarn test
|
||||
```
|
||||
|
||||
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
Main docs and pitfalls are available in LLMS.md file.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/capabilities/apps)
|
||||
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
|
||||
- [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
To learn more about Twenty applications, take a look at the following resources:
|
||||
|
||||
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
|
||||
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { basename } from 'path';
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
import { install } from '@/utils/install';
|
||||
import {
|
||||
type LocalInstanceResult,
|
||||
setupLocalInstance,
|
||||
} from '@/utils/setup-local-instance';
|
||||
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 { execSync } from 'node:child_process';
|
||||
import * as path from 'path';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type ExampleOptions,
|
||||
@@ -22,25 +15,16 @@ import {
|
||||
|
||||
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
|
||||
|
||||
type CreateAppOptions = {
|
||||
directory?: string;
|
||||
mode?: ScaffoldingMode;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
port?: number;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
async execute(options: CreateAppOptions = {}): Promise<void> {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(options);
|
||||
|
||||
async execute(
|
||||
directory?: string,
|
||||
mode: ScaffoldingMode = 'exhaustive',
|
||||
): Promise<void> {
|
||||
try {
|
||||
const exampleOptions = this.resolveExampleOptions(
|
||||
options.mode ?? 'exhaustive',
|
||||
);
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(directory);
|
||||
|
||||
const exampleOptions = this.resolveExampleOptions(mode);
|
||||
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
@@ -60,45 +44,29 @@ export class CreateAppCommand {
|
||||
|
||||
await tryGitInit(appDirectory);
|
||||
|
||||
let localResult: LocalInstanceResult = { running: false };
|
||||
|
||||
if (!options.skipLocalInstance) {
|
||||
localResult = await setupLocalInstance(appDirectory, options.port);
|
||||
|
||||
if (localResult.running && localResult.serverUrl) {
|
||||
await this.connectToLocal(appDirectory, localResult.serverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, localResult);
|
||||
this.logSuccess(appDirectory);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('\nCreate application failed:'),
|
||||
chalk.red('Initialization failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAppInfos(options: CreateAppOptions): Promise<{
|
||||
private async getAppInfos(directory?: string): 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 { name, displayName, description } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Application name:',
|
||||
when: () => !hasName,
|
||||
default: 'my-twenty-app',
|
||||
when: () => !directory,
|
||||
default: 'my-awesome-app',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) return 'Application name is required';
|
||||
return true;
|
||||
@@ -108,33 +76,25 @@ export class CreateAppCommand {
|
||||
type: 'input',
|
||||
name: 'displayName',
|
||||
message: 'Application display name:',
|
||||
when: () => !hasDisplayName,
|
||||
default: (answers: { name?: string }) => {
|
||||
return convertToLabel(
|
||||
answers?.name ?? options.name ?? directory ?? '',
|
||||
);
|
||||
default: (answers: any) => {
|
||||
return convertToLabel(answers?.name ?? directory);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: 'Application description (optional):',
|
||||
when: () => !hasDescription,
|
||||
default: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const appName = (
|
||||
options.name ??
|
||||
name ??
|
||||
directory ??
|
||||
'my-twenty-app'
|
||||
).trim();
|
||||
const computedName = name ?? directory;
|
||||
|
||||
const appDisplayName =
|
||||
(options.displayName ?? displayName)?.trim() || convertToLabel(appName);
|
||||
const appName = computedName.trim();
|
||||
|
||||
const appDescription = (options.description ?? description ?? '').trim();
|
||||
const appDisplayName = displayName.trim();
|
||||
|
||||
const appDescription = description.trim();
|
||||
|
||||
const appDirectory = directory
|
||||
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
|
||||
@@ -191,49 +151,22 @@ export class CreateAppCommand {
|
||||
appDirectory: string;
|
||||
appName: string;
|
||||
}): void {
|
||||
console.log(
|
||||
chalk.blue('\n', 'Creating Twenty Application\n'),
|
||||
chalk.gray(`- Directory: ${appDirectory}\n`, `- Name: ${appName}\n`),
|
||||
);
|
||||
console.log(chalk.blue('🎯 Creating Twenty Application'));
|
||||
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
|
||||
console.log(chalk.gray(`📝 Name: ${appName}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private async connectToLocal(
|
||||
appDirectory: string,
|
||||
serverUrl: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
execSync(`yarn twenty remote add ${serverUrl} --as local`, {
|
||||
cwd: appDirectory,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
localResult: LocalInstanceResult,
|
||||
): void {
|
||||
const dirName = basename(appDirectory);
|
||||
|
||||
console.log(chalk.blue('\nApplication created. Next steps:'));
|
||||
console.log(chalk.gray(`- cd ${dirName}`));
|
||||
|
||||
if (!localResult.running) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
'- yarn twenty remote add --local # Authenticate with Twenty',
|
||||
),
|
||||
);
|
||||
}
|
||||
private logSuccess(appDirectory: string): void {
|
||||
const dirName = appDirectory.split('/').reverse()[0] ?? '';
|
||||
|
||||
console.log(chalk.green('✅ Application created!'));
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(chalk.gray(` cd ${dirName}`));
|
||||
console.log(
|
||||
chalk.gray('- yarn twenty dev # Start dev mode'),
|
||||
chalk.gray(' yarn twenty auth:login # Authenticate with Twenty'),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ describe('scaffoldIntegrationTest', () => {
|
||||
|
||||
expect(content).toContain('TWENTY_API_KEY');
|
||||
expect(content).not.toContain('TWENTY_TEST_API_KEY');
|
||||
expect(content).toContain('TWENTY_API_URL');
|
||||
expect(content).toContain('setup-test.ts');
|
||||
expect(content).toContain('tsconfig.spec.json');
|
||||
expect(content).toContain('integration-test.ts');
|
||||
|
||||
@@ -30,12 +30,12 @@ export const copyBaseApplicationProject = async ({
|
||||
includeExampleIntegrationTest: exampleOptions.includeExampleIntegrationTest,
|
||||
});
|
||||
|
||||
await createYarnLock(appDirectory);
|
||||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
await createPublicAssetDirectory(appDirectory);
|
||||
|
||||
await createYarnLock(appDirectory);
|
||||
|
||||
const sourceFolderPath = join(appDirectory, SRC_FOLDER);
|
||||
|
||||
await fs.ensureDir(sourceFolderPath);
|
||||
@@ -142,6 +142,13 @@ const createPublicAssetDirectory = async (appDirectory: string) => {
|
||||
await fs.ensureDir(join(appDirectory, ASSETS_DIR));
|
||||
};
|
||||
|
||||
const createYarnLock = async (appDirectory: string) => {
|
||||
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
|
||||
};
|
||||
const createGitignore = async (appDirectory: string) => {
|
||||
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
@@ -474,7 +481,7 @@ const createExampleNavigationMenuItem = async ({
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
@@ -482,7 +489,6 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: 'VIEW',
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
`;
|
||||
@@ -584,14 +590,6 @@ export default defineApplication({
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createYarnLock = async (appDirectory: string) => {
|
||||
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
|
||||
};
|
||||
|
||||
const createPackageJson = async ({
|
||||
appName,
|
||||
appDirectory,
|
||||
@@ -610,9 +608,8 @@ const createPackageJson = async ({
|
||||
const devDependencies: Record<string, string> = {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^19.0.0',
|
||||
react: '^19.0.0',
|
||||
'react-dom': '^19.0.0',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
oxlint: '^0.16.0',
|
||||
'twenty-sdk': createTwentyAppPackageJson.version,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import { exec } from 'child_process';
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
export const install = async (root: string) => {
|
||||
console.log(chalk.gray('Installing yarn dependencies...'));
|
||||
try {
|
||||
await execPromise('corepack enable', { cwd: root });
|
||||
} catch (error: any) {
|
||||
@@ -15,6 +14,6 @@ export const install = async (root: string) => {
|
||||
try {
|
||||
await execPromise('yarn install', { cwd: root });
|
||||
} catch (error: any) {
|
||||
console.warn(chalk.yellow('yarn install failed:'), error.stdout);
|
||||
console.error(chalk.red('yarn install failed:'), error.stdout);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const LOCAL_PORTS = [2020, 3000];
|
||||
|
||||
// Minimal health check — the full implementation lives in twenty-sdk
|
||||
const isServerReady = async (port: number): Promise<boolean> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/healthz`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
return body.status === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
const detectRunningServer = async (
|
||||
preferredPort?: number,
|
||||
): Promise<number | null> => {
|
||||
const ports = preferredPort ? [preferredPort] : LOCAL_PORTS;
|
||||
|
||||
for (const port of ports) {
|
||||
if (await isServerReady(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export type LocalInstanceResult = {
|
||||
running: boolean;
|
||||
serverUrl?: string;
|
||||
};
|
||||
|
||||
export const setupLocalInstance = async (
|
||||
appDirectory: string,
|
||||
preferredPort?: number,
|
||||
): Promise<LocalInstanceResult> => {
|
||||
const detectedPort = await detectRunningServer(preferredPort);
|
||||
|
||||
if (detectedPort) {
|
||||
const serverUrl = `http://localhost:${detectedPort}`;
|
||||
|
||||
console.log(chalk.green(`Twenty server detected on ${serverUrl}.\n`));
|
||||
|
||||
return { running: true, serverUrl };
|
||||
}
|
||||
|
||||
if (preferredPort) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`No Twenty server found on port ${preferredPort}.\n` +
|
||||
'Start your server and run `yarn twenty remote add --local` manually.\n',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
console.log(chalk.blue('Setting up local Twenty instance...\n'));
|
||||
|
||||
try {
|
||||
execSync('yarn twenty server start', {
|
||||
cwd: appDirectory,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
console.log(chalk.gray('Waiting for Twenty to be ready...\n'));
|
||||
|
||||
const startTime = Date.now();
|
||||
const timeoutMs = 180 * 1000;
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
if (await isServerReady(LOCAL_PORTS[0])) {
|
||||
const serverUrl = `http://localhost:${LOCAL_PORTS[0]}`;
|
||||
|
||||
console.log(chalk.green(`Server running on '${serverUrl}'\n`));
|
||||
|
||||
return { running: true, serverUrl };
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Twenty server did not become healthy in time.\n',
|
||||
"Check: 'yarn twenty server logs'\n",
|
||||
),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
};
|
||||
@@ -45,6 +45,7 @@ export default defineConfig({
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
TWENTY_API_KEY:
|
||||
'${SEED_API_KEY}',
|
||||
},
|
||||
@@ -93,7 +94,7 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
@@ -119,13 +120,12 @@ beforeAll(async () => {
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
profiles: {
|
||||
default: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
+3
-3
@@ -30,7 +30,7 @@ type AnalysisResult = {
|
||||
commitments: Commitment[];
|
||||
};
|
||||
|
||||
type RichTextData = {
|
||||
type RichTextV2Data = {
|
||||
markdown: string;
|
||||
blocknote: null;
|
||||
};
|
||||
@@ -123,7 +123,7 @@ const createNoteInTwenty = async (
|
||||
bodyV2: {
|
||||
markdown: noteBodyMarkdown,
|
||||
blocknote: null,
|
||||
} satisfies RichTextData,
|
||||
} satisfies RichTextV2Data,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -159,7 +159,7 @@ const createTaskInTwenty = async (
|
||||
|
||||
const taskData: {
|
||||
title: string;
|
||||
bodyV2: RichTextData;
|
||||
bodyV2: RichTextV2Data;
|
||||
dueAt?: string;
|
||||
} = {
|
||||
title: actionItem.title,
|
||||
|
||||
@@ -10,4 +10,3 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --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
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
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
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@ type AnalysisResult = {
|
||||
commitments: Commitment[];
|
||||
};
|
||||
|
||||
type RichTextData = {
|
||||
type RichTextV2Data = {
|
||||
markdown: string;
|
||||
blocknote: null;
|
||||
};
|
||||
@@ -362,7 +362,7 @@ const createNoteInTwenty = async (
|
||||
bodyV2: {
|
||||
markdown: noteBodyMarkdown,
|
||||
blocknote: null,
|
||||
} satisfies RichTextData,
|
||||
} satisfies RichTextV2Data,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -451,7 +451,7 @@ const createTaskInTwenty = async (
|
||||
|
||||
const taskData: {
|
||||
title: string;
|
||||
bodyV2: RichTextData;
|
||||
bodyV2: RichTextV2Data;
|
||||
dueAt?: string;
|
||||
assigneeId?: string;
|
||||
} = {
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"dev": "twenty dev",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"app:dev": "twenty app:dev",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '../views/all-post-card-recipients.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 2,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_POST_CARD_RECIPIENTS_VIEW_ID,
|
||||
});
|
||||
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
|
||||
});
|
||||
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/recipient.object';
|
||||
import { ALL_RECIPIENTS_VIEW_ID } from '../views/all-recipients.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_RECIPIENTS_VIEW_ID,
|
||||
});
|
||||
|
||||
@@ -10,4 +10,3 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --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
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
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
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
@@ -29,13 +29,12 @@ beforeAll(async () => {
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
profiles: {
|
||||
default: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
+1
-3
@@ -1,6 +1,5 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
|
||||
@@ -8,6 +7,5 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -7,4 +7,3 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --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
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
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
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
@@ -236,7 +236,7 @@ const handler = async (event: any) => {
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: remove `as any` after running `yarn twenty dev` to regenerate the typed client
|
||||
// TODO: remove `as any` after running `yarn twenty app:dev` to regenerate the typed client
|
||||
const updateSummary = async (markdown: string) => {
|
||||
await client.mutation({
|
||||
updateCallRecording: {
|
||||
|
||||
-2
@@ -1,12 +1,10 @@
|
||||
import { CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/call-recording-view';
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '5248a62d-7d2e-43a7-ba45-6e8f61876a71',
|
||||
name: 'Call recordings',
|
||||
icon: 'IconPhone',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ export default defineObject({
|
||||
},
|
||||
{
|
||||
universalIdentifier: TRANSCRIPT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RICH_TEXT,
|
||||
type: FieldType.RICH_TEXT_V2,
|
||||
name: 'transcript',
|
||||
label: 'Transcript',
|
||||
description: 'Human-readable transcript of the call',
|
||||
@@ -114,7 +114,7 @@ export default defineObject({
|
||||
},
|
||||
{
|
||||
universalIdentifier: SUMMARY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RICH_TEXT,
|
||||
type: FieldType.RICH_TEXT_V2,
|
||||
name: 'summary',
|
||||
label: 'Summary',
|
||||
description: 'AI-generated summary of the call',
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ Use this skill when a user asks you to summarize, analyze, or extract insights f
|
||||
|
||||
## How to Access the Data
|
||||
1. Use \`find_one_callRecording\` to fetch the call recording by its ID.
|
||||
2. Read the \`transcript\` field (RICH_TEXT, markdown format) which contains the full conversation.
|
||||
2. Read the \`transcript\` field (RICH_TEXT_V2, markdown format) which contains the full conversation.
|
||||
3. The transcript uses the format: **Speaker Name:** spoken text
|
||||
|
||||
## What to Produce
|
||||
|
||||
-2
@@ -1,5 +1,4 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
@@ -7,7 +6,6 @@ export default defineNavigationMenuItem({
|
||||
name: 'Self host user',
|
||||
icon: 'IconList',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ DOCKER_NETWORK=twenty_network
|
||||
# =============================================================================
|
||||
|
||||
prod-build:
|
||||
@cd ../.. && docker build --target twenty -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
|
||||
|
||||
prod-run:
|
||||
@docker run -d -p 3000:3000 --name twenty twenty:$(TAG)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# Development infrastructure services only (Postgres + Redis).
|
||||
# Use this when developing locally against the source code.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.dev.yml up -d
|
||||
# docker compose -f docker-compose.dev.yml down # stop
|
||||
# docker compose -f docker-compose.dev.yml down -v # stop + wipe data
|
||||
|
||||
name: twenty-dev
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- dev-db-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: default
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["--maxmemory-policy", "noeviction"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
dev-db-data:
|
||||
@@ -38,6 +38,7 @@ services:
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
@@ -91,6 +92,7 @@ services:
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
|
||||
@@ -89,15 +89,6 @@ password
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Check if using external secret for redis password */}}
|
||||
{{- define "twenty.redis.useExternalSecret" -}}
|
||||
{{- if and (not .Values.redisInternal.enabled) .Values.redis.external.secretName .Values.redis.external.passwordKey -}}
|
||||
true
|
||||
{{- else -}}
|
||||
false
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Compose Redis URL */}}
|
||||
{{- define "twenty.redisUrl" -}}
|
||||
{{- if .Values.server.env.REDIS_URL -}}
|
||||
@@ -108,14 +99,9 @@ false
|
||||
{{- else -}}
|
||||
{{- $host := .Values.redis.external.host | default "redis" -}}
|
||||
{{- $port := .Values.redis.external.port | default 6379 -}}
|
||||
{{- if or (eq (include "twenty.redis.useExternalSecret" .) "true") (.Values.redis.external.password) -}}
|
||||
{{- $auth := ":$(REDIS_PASSWORD)@" -}}
|
||||
{{- printf "redis://%s%s:%v" $auth $host $port -}}
|
||||
{{- else -}}
|
||||
{{- printf "redis://%s:%v" $host $port -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Compose Server URL from override, ingress, or service */}}
|
||||
{{- define "twenty.serverUrl" -}}
|
||||
|
||||
@@ -46,12 +46,12 @@ spec:
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: redis-data
|
||||
{{- if .Values.redisInternal.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.redisInternal.persistence.existingClaim | default (printf "%s-redis" (include "twenty.fullname" .)) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: redis-data
|
||||
{{- if .Values.redisInternal.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.redisInternal.persistence.existingClaim | default (printf "%s-redis" (include "twenty.fullname" .)) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -83,16 +83,16 @@ spec:
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v db="${DBNAME}" -Atc "SELECT 1 FROM pg_database WHERE datname = :'db'" | grep -q 1 || \
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v db="${DBNAME}" -c 'CREATE DATABASE :"db";'
|
||||
echo "Creating app user ${APP_USER} if it doesn't exist..."
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v app_user="${APP_USER}" -v app_password="${APP_PASSWORD}" <<'EOSQL'
|
||||
DO
|
||||
$do$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
|
||||
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
|
||||
END IF;
|
||||
END
|
||||
$do$;
|
||||
EOSQL
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v app_user="${APP_USER}" -v app_password="${APP_PASSWORD}" <<'EOSQL'
|
||||
DO
|
||||
$do$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
|
||||
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
|
||||
END IF;
|
||||
END
|
||||
$do$;
|
||||
EOSQL
|
||||
echo "Creating core schema and granting permissions..."
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'CREATE SCHEMA IF NOT EXISTS core'
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v db="${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON DATABASE :"db" TO :"app_user";'
|
||||
@@ -106,6 +106,31 @@ spec:
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO :"app_user";'
|
||||
echo "Database ${DBNAME} is ready."
|
||||
{{- end }}
|
||||
- name: run-migrations
|
||||
{{- $img := include "twenty.server.image" . }}
|
||||
image: {{ include "twenty.image.repository" $img }}:{{ include "twenty.image.tag" $img }}
|
||||
imagePullPolicy: {{ include "twenty.image.pullPolicy" $img }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
npx -y typeorm migration:run -d dist/database/typeorm/core/core.datasource
|
||||
env:
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbPassword.secretName" . }}
|
||||
key: {{ include "twenty.dbPassword.secretKey" . }}
|
||||
- name: PG_DATABASE_URL
|
||||
value: {{ include "twenty.dbUrl.template" . | quote }}
|
||||
{{- else }}
|
||||
- name: PG_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: server
|
||||
{{- $img := include "twenty.server.image" . }}
|
||||
@@ -129,16 +154,6 @@ spec:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
{{- if eq (include "twenty.redis.useExternalSecret" .) "true" }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.redis.external.secretName }}
|
||||
key: {{ .Values.redis.external.passwordKey }}
|
||||
{{- else if .Values.redis.external.password }}
|
||||
- name: REDIS_PASSWORD
|
||||
value: {{ .Values.redis.external.password | quote }}
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "twenty.redisUrl" . | quote }}
|
||||
- name: SIGN_IN_PREFILLED
|
||||
@@ -156,10 +171,7 @@ spec:
|
||||
key: accessToken
|
||||
{{- $storageEnv := (include "twenty.storageEnv" .) }}
|
||||
{{- if $storageEnv }}
|
||||
{{- $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{ $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http-tcp
|
||||
|
||||
@@ -67,16 +67,6 @@ spec:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
{{- if eq (include "twenty.redis.useExternalSecret" .) "true" }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.redis.external.secretName }}
|
||||
key: {{ .Values.redis.external.passwordKey }}
|
||||
{{- else if .Values.redis.external.password }}
|
||||
- name: REDIS_PASSWORD
|
||||
value: {{ .Values.redis.external.password | quote }}
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "twenty.redisUrl" . | quote }}
|
||||
- name: STORAGE_TYPE
|
||||
@@ -86,12 +76,9 @@ spec:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.secret.tokens.name" . }}
|
||||
key: accessToken
|
||||
{{- with .Values.worker.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- $storageEnv := (include "twenty.storageEnv" .) }}
|
||||
{{- if $storageEnv }}
|
||||
{{- $storageEnv | nindent 12 }}
|
||||
{{ $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.worker.resources | nindent 12 }}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{{- if and .Values.redisInternal.enabled .Values.redisInternal.persistence.enabled (not .Values.redisInternal.persistence.existingClaim) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "twenty.fullname" . }}-redis
|
||||
namespace: {{ include "twenty.namespace" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "twenty.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: redis
|
||||
spec:
|
||||
accessModes:
|
||||
{{ toYaml .Values.redisInternal.persistence.accessModes | nindent 4 }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.redisInternal.persistence.size }}
|
||||
{{- if .Values.redisInternal.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.redisInternal.persistence.storageClass }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,58 +0,0 @@
|
||||
suite: env mapping
|
||||
templates:
|
||||
- templates/deployment-server.yaml
|
||||
- templates/deployment-worker.yaml
|
||||
release:
|
||||
name: my-twenty
|
||||
namespace: default
|
||||
tests:
|
||||
- it: renders server extraEnv with plain value
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
server.extraEnv:
|
||||
- name: FEATURE_X_ENABLED
|
||||
value: "true"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: FEATURE_X_ENABLED
|
||||
value: "true"
|
||||
|
||||
- it: renders server extraEnv with valueFrom
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
server.extraEnv:
|
||||
- name: SMTP_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: smtp-creds
|
||||
key: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: SMTP_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: smtp-creds
|
||||
key: password
|
||||
|
||||
- it: renders worker extraEnv with valueFrom
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
worker.extraEnv:
|
||||
- name: CUSTOM_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: my-secret
|
||||
key: custom
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: CUSTOM_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: my-secret
|
||||
key: custom
|
||||
@@ -1,89 +0,0 @@
|
||||
suite: redis external authentication
|
||||
templates:
|
||||
- templates/deployment-server.yaml
|
||||
- templates/deployment-worker.yaml
|
||||
release:
|
||||
name: my-twenty
|
||||
namespace: default
|
||||
tests:
|
||||
- it: injects REDIS_PASSWORD from external secret into server
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.secretName: redis-creds
|
||||
redis.external.passwordKey: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis-creds
|
||||
key: password
|
||||
|
||||
- it: injects REDIS_PASSWORD from external secret into worker
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.secretName: redis-creds
|
||||
redis.external.passwordKey: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis-creds
|
||||
key: password
|
||||
|
||||
- it: injects plaintext REDIS_PASSWORD when password set directly in server
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.password: "s3cr3t"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
value: "s3cr3t"
|
||||
|
||||
- it: injects plaintext REDIS_PASSWORD when password set directly in worker
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.password: "s3cr3t"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
value: "s3cr3t"
|
||||
|
||||
- it: does not inject REDIS_PASSWORD into server when using internal redis
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: true
|
||||
asserts:
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
any: true
|
||||
|
||||
- it: does not inject REDIS_PASSWORD into worker when using internal redis
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: true
|
||||
asserts:
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
any: true
|
||||
@@ -105,3 +105,18 @@ tests:
|
||||
path: spec.template.spec.initContainers[?(@.name=="ensure-database-exists")].command[2]
|
||||
pattern: ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES
|
||||
|
||||
# TypeORM Migration Tests
|
||||
# ======================
|
||||
# TypeORM migrations are configured to use the core.datasource which targets the
|
||||
# 'core' schema. This ensures the _typeorm_migrations table and all application
|
||||
# tables use the dedicated core schema.
|
||||
|
||||
- it: migrations run against core datasource
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
db.enabled: true
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: spec.template.spec.initContainers[?(@.name=="run-migrations")].command[2]
|
||||
pattern: core\.datasource
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ tests:
|
||||
- crm.example.com
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].env[?(@.name=="SERVER_URL")].value
|
||||
path: spec.template.spec.containers[0].env[0].value
|
||||
value: "https://crm.example.com:443"
|
||||
- it: falls back to service when ingress disabled
|
||||
set:
|
||||
@@ -28,7 +28,7 @@ tests:
|
||||
server.env.SERVER_URL: ""
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: spec.template.spec.containers[0].env[?(@.name=="SERVER_URL")].value
|
||||
path: spec.template.spec.containers[0].env[0].value
|
||||
pattern: ^http://my-twenty-twenty-server\.default\.svc\.cluster\.local:3000$
|
||||
---
|
||||
suite: ingress configuration
|
||||
@@ -66,7 +66,6 @@ tests:
|
||||
set:
|
||||
server.ingress.acme: true
|
||||
asserts:
|
||||
- isSubset:
|
||||
path: metadata.annotations
|
||||
content:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
- equal:
|
||||
path: metadata.annotations[cert-manager.io/cluster-issuer]
|
||||
value: letsencrypt-prod
|
||||
|
||||
@@ -61,27 +61,12 @@
|
||||
"EMAIL_SMTP_NO_TLS": { "type": "boolean" },
|
||||
"EMAIL_FROM_ADDRESS": { "type": "string" },
|
||||
"EMAIL_FROM_NAME": { "type": "string" },
|
||||
"EMAIL_SYSTEM_ADDRESS": { "type": "string" },
|
||||
"IS_EMAIL_VERIFICATION_REQUIRED": { "type": "boolean" },
|
||||
"EMAIL_VERIFICATION_TOKEN_EXPIRES_IN": { "type": "string" },
|
||||
"PASSWORD_RESET_TOKEN_EXPIRES_IN": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"value": { "type": "string" },
|
||||
"valueFrom": { "type": "object" }
|
||||
},
|
||||
"required": ["name"],
|
||||
"oneOf": [
|
||||
{ "required": ["value"] },
|
||||
{ "required": ["valueFrom"] }
|
||||
]
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -157,22 +142,6 @@
|
||||
"STORAGE_TYPE": { "type": "string" },
|
||||
"DISABLE_DB_MIGRATIONS": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"value": { "type": "string" },
|
||||
"valueFrom": { "type": "object" }
|
||||
},
|
||||
"required": ["name"],
|
||||
"oneOf": [
|
||||
{ "required": ["value"] },
|
||||
{ "required": ["valueFrom"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,14 +52,6 @@ server:
|
||||
SIGN_IN_PREFILLED: "false"
|
||||
ACCESS_TOKEN_EXPIRES_IN: "7d"
|
||||
LOGIN_TOKEN_EXPIRES_IN: "1h"
|
||||
extraEnv: []
|
||||
# - name: EMAIL_DRIVER
|
||||
# value: smtp
|
||||
# - name: SMTP_PASSWORD
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: smtp-creds
|
||||
# key: password
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
@@ -113,8 +105,6 @@ worker:
|
||||
cpu: 1000m
|
||||
memory: 2048Mi
|
||||
|
||||
extraEnv: []
|
||||
|
||||
# PostgreSQL
|
||||
db:
|
||||
enabled: true
|
||||
@@ -184,6 +174,3 @@ redis:
|
||||
external:
|
||||
host: ""
|
||||
port: 6379
|
||||
password: ""
|
||||
secretName: ""
|
||||
passwordKey: ""
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/bin/sh /etc/s6-overlay/scripts/init-db.sh
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Initialize PostgreSQL data directory if empty
|
||||
if [ ! -f /data/postgres/PG_VERSION ]; then
|
||||
echo "Initializing PostgreSQL data directory..."
|
||||
su-exec postgres initdb -D /data/postgres --auth=trust --encoding=UTF8
|
||||
# Allow local connections without password
|
||||
echo "host all all 127.0.0.1/32 trust" >> /data/postgres/pg_hba.conf
|
||||
echo "host all all ::1/128 trust" >> /data/postgres/pg_hba.conf
|
||||
fi
|
||||
|
||||
exec su-exec postgres postgres -D /data/postgres \
|
||||
-c listen_addresses=localhost \
|
||||
-c unix_socket_directories=/tmp
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
exec redis-server \
|
||||
--dir /data/redis \
|
||||
--maxmemory-policy noeviction \
|
||||
--bind 127.0.0.1 \
|
||||
--protected-mode yes
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /app/packages/twenty-server
|
||||
exec yarn start:prod
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /app/packages/twenty-server
|
||||
exec yarn worker:prod
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Wait for PostgreSQL to be ready (timeout after 60s)
|
||||
echo "Waiting for PostgreSQL..."
|
||||
TRIES=0
|
||||
until su-exec postgres pg_isready -h localhost; do
|
||||
TRIES=$((TRIES + 1))
|
||||
if [ "$TRIES" -ge 120 ]; then
|
||||
echo "ERROR: PostgreSQL did not become ready within 60s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
echo "PostgreSQL is ready."
|
||||
|
||||
# Create role if it doesn't exist
|
||||
su-exec postgres psql -h localhost -tc \
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='twenty'" | grep -q 1 \
|
||||
|| su-exec postgres psql -h localhost -c "CREATE ROLE twenty WITH LOGIN PASSWORD 'twenty' SUPERUSER"
|
||||
|
||||
# Create database if it doesn't exist
|
||||
su-exec postgres psql -h localhost -tc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='default'" | grep -q 1 \
|
||||
|| su-exec postgres createdb -h localhost -O twenty default
|
||||
|
||||
# Run Twenty database setup and migrations
|
||||
cd /app/packages/twenty-server
|
||||
|
||||
has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
|
||||
|
||||
if [ "$has_schema" = "f" ]; then
|
||||
echo "Database appears to be empty, running initial setup..."
|
||||
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
|
||||
fi
|
||||
|
||||
# Always run migrations (idempotent — skips already-applied ones)
|
||||
yarn database:migrate:prod
|
||||
|
||||
yarn command:prod cache:flush
|
||||
yarn command:prod upgrade
|
||||
yarn command:prod cache:flush
|
||||
|
||||
# Only seed on first boot — check if the dev workspace already exists
|
||||
has_workspace=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
|
||||
"SELECT EXISTS (SELECT 1 FROM core.workspace WHERE id = '20202020-1c25-4d02-bf25-6aeccf7ea419')")
|
||||
|
||||
if [ "$has_workspace" = "f" ]; then
|
||||
echo "Seeding app dev data..."
|
||||
yarn command:prod workspace:seed:dev --light || true
|
||||
else
|
||||
echo "Dev workspace already seeded, skipping."
|
||||
fi
|
||||
|
||||
echo "Database initialization complete."
|
||||
@@ -1,11 +1,9 @@
|
||||
# ===========================================================================
|
||||
# Shared build stages (used by both targets)
|
||||
# ===========================================================================
|
||||
|
||||
# Base image for common dependencies
|
||||
FROM node:24-alpine AS common-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only the necessary files for dependency resolution
|
||||
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./.yarn/patches /app/.yarn/patches
|
||||
@@ -17,39 +15,29 @@ COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY ./packages/twenty-standard-application/package.json /app/packages/twenty-standard-application/
|
||||
|
||||
# Install all dependencies
|
||||
RUN yarn && yarn cache clean && npx nx reset
|
||||
|
||||
|
||||
# Build the back
|
||||
FROM common-deps AS twenty-server-build
|
||||
|
||||
# Copy sourcecode after installing dependences to accelerate subsequents builds
|
||||
COPY ./packages/twenty-emails /app/packages/twenty-emails
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
COPY ./packages/twenty-standard-application /app/packages/twenty-standard-application
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
RUN npx nx run twenty-server:lingui:extract && \
|
||||
npx nx run twenty-server:lingui:compile && \
|
||||
npx nx run twenty-emails:lingui:extract && \
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
|
||||
RUN npx nx build twenty-standard-application
|
||||
RUN npx nx run twenty-server:build
|
||||
|
||||
# Bundle setup-db script into a standalone JS file so the final image
|
||||
# doesn't need tsx or the TypeScript source tree at runtime.
|
||||
RUN npx esbuild packages/twenty-server/scripts/setup-db.ts \
|
||||
--bundle --platform=node --outfile=packages/twenty-server/dist/scripts/setup-db.js \
|
||||
--external:typeorm --external:dotenv --external:pg
|
||||
|
||||
# Clean server build output (type declarations and compiled tests are not needed at runtime;
|
||||
# source maps are kept because twenty-infra extracts them from the image for Sentry uploads)
|
||||
RUN find /app/packages/twenty-server/dist -name '*.d.ts' -delete \
|
||||
&& rm -rf /app/packages/twenty-server/dist/packages/twenty-server/test
|
||||
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
|
||||
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-standard-application twenty-server
|
||||
|
||||
# Build the front
|
||||
FROM common-deps AS twenty-front-build
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
@@ -58,26 +46,18 @@ COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
RUN npx nx run twenty-front:lingui:extract && \
|
||||
npx nx run twenty-front:lingui:compile
|
||||
# To skip the memory-intensive frontend build, pre-build on the host:
|
||||
# npx nx build twenty-front
|
||||
# The check below will use packages/twenty-front/build/ if it already exists.
|
||||
RUN if [ -d /app/packages/twenty-front/build ]; then \
|
||||
echo "Using pre-built frontend from host"; \
|
||||
else \
|
||||
NODE_OPTIONS="--max-old-space-size=8192" npx nx build twenty-front; \
|
||||
fi
|
||||
RUN npx nx build twenty-front
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Target: twenty (production)
|
||||
# docker build --target twenty -f packages/twenty-docker/twenty/Dockerfile .
|
||||
# ===========================================================================
|
||||
|
||||
# Final stage: Run the application
|
||||
FROM node:24-alpine AS twenty
|
||||
|
||||
RUN apk add --no-cache curl jq postgresql-client
|
||||
# Used to run healthcheck in docker
|
||||
RUN apk add --no-cache curl jq
|
||||
|
||||
RUN npm install -g tsx
|
||||
|
||||
RUN apk add --no-cache postgresql-client
|
||||
|
||||
COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
@@ -89,135 +69,20 @@ ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL
|
||||
ARG APP_VERSION
|
||||
ENV APP_VERSION=$APP_VERSION
|
||||
|
||||
# Workspace root config
|
||||
COPY --chown=1000 --from=twenty-server-build /app/package.json /app/yarn.lock /app/.yarnrc.yml /app/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/tsconfig.base.json /app/nx.json /app/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/.yarn /app/.yarn
|
||||
COPY --chown=1000 --from=twenty-server-build /app/node_modules /app/node_modules
|
||||
|
||||
# Server package (compiled dist + package.json only, no src/)
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/package.json /app/packages/twenty-server/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/dist /app/packages/twenty-server/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/patches /app/packages/twenty-server/patches
|
||||
|
||||
# Workspace packages (dist + package.json; node_modules symlinks resolve to these)
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
|
||||
# Frontend static build
|
||||
# Copy built applications from previous stages
|
||||
COPY --chown=1000 --from=twenty-server-build /app /app
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server
|
||||
COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
|
||||
|
||||
# Set metadata and labels
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="Production Twenty image with backend and frontend."
|
||||
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment."
|
||||
|
||||
RUN mkdir -p /app/.local-storage /app/packages/twenty-server/.local-storage && \
|
||||
chown 1000:1000 /app/.local-storage /app/packages/twenty-server/.local-storage
|
||||
chown -R 1000:1000 /app
|
||||
|
||||
# Use non root user with uid 1000
|
||||
USER 1000
|
||||
|
||||
CMD ["node", "dist/main"]
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Target: twenty-app-dev (all-in-one with Postgres + Redis)
|
||||
# docker build --target twenty-app-dev -f packages/twenty-docker/twenty/Dockerfile .
|
||||
# ===========================================================================
|
||||
|
||||
FROM alpine:3.20 AS s6-fetch
|
||||
ARG S6_OVERLAY_VERSION=3.2.0.2
|
||||
ARG TARGETARCH
|
||||
RUN if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64" > /tmp/s6arch; \
|
||||
else echo "x86_64" > /tmp/s6arch; fi
|
||||
RUN S6_ARCH=$(cat /tmp/s6arch) && \
|
||||
wget -O /tmp/s6-overlay-noarch.tar.xz \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
|
||||
wget -O /tmp/s6-overlay-noarch.tar.xz.sha256 \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" && \
|
||||
wget -O /tmp/s6-overlay-arch.tar.xz \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
|
||||
wget -O /tmp/s6-overlay-arch.tar.xz.sha256 \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" && \
|
||||
cd /tmp && \
|
||||
NOARCH_SUM=$(awk '{print $1}' s6-overlay-noarch.tar.xz.sha256) && \
|
||||
ARCH_SUM=$(awk '{print $1}' s6-overlay-arch.tar.xz.sha256) && \
|
||||
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
|
||||
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
|
||||
|
||||
FROM node:24-alpine AS twenty-app-dev
|
||||
|
||||
# s6-overlay
|
||||
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
|
||||
COPY --from=s6-fetch /tmp/s6-overlay-arch.tar.xz /tmp/
|
||||
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
|
||||
&& tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \
|
||||
&& rm /tmp/s6-overlay-*.tar.xz
|
||||
|
||||
RUN apk add --no-cache \
|
||||
postgresql16 postgresql16-contrib \
|
||||
redis \
|
||||
curl jq su-exec
|
||||
|
||||
# Workspace root config
|
||||
COPY --from=twenty-server-build /app/package.json /app/yarn.lock /app/.yarnrc.yml /app/
|
||||
COPY --from=twenty-server-build /app/tsconfig.base.json /app/nx.json /app/
|
||||
COPY --from=twenty-server-build /app/.yarn /app/.yarn
|
||||
COPY --from=twenty-server-build /app/node_modules /app/node_modules
|
||||
|
||||
# Server package (compiled dist + package.json only, no src/)
|
||||
COPY --from=twenty-server-build /app/packages/twenty-server/package.json /app/packages/twenty-server/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-server/dist /app/packages/twenty-server/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-server/patches /app/packages/twenty-server/patches
|
||||
|
||||
# Workspace packages (dist + package.json; node_modules symlinks resolve to these)
|
||||
COPY --from=twenty-server-build /app/packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
|
||||
# Frontend static build
|
||||
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
|
||||
|
||||
# Source maps are not needed in the dev image (no Sentry)
|
||||
RUN find /app/packages/twenty-server/dist -name '*.js.map' -delete
|
||||
|
||||
# s6 service definitions
|
||||
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
|
||||
|
||||
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
|
||||
&& chown -R postgres:postgres /data/postgres \
|
||||
&& chown 1000:1000 /data/redis /app/.local-storage
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
ARG APP_VERSION=0.0.0
|
||||
|
||||
ENV S6_KEEP_ENV=1
|
||||
ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
|
||||
SERVER_URL=http://localhost:2020 \
|
||||
REDIS_URL=redis://localhost:6379 \
|
||||
STORAGE_TYPE=local \
|
||||
APP_SECRET=twenty-app-dev-secret-not-for-production \
|
||||
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
|
||||
APP_VERSION=$APP_VERSION \
|
||||
NODE_ENV=development \
|
||||
NODE_PORT=3000 \
|
||||
DISABLE_DB_MIGRATIONS=true \
|
||||
DISABLE_CRON_JOBS_REGISTRATION=true \
|
||||
IS_BILLING_ENABLED=false \
|
||||
SIGN_IN_PREFILLED=true
|
||||
|
||||
EXPOSE 3000
|
||||
VOLUME ["/data/postgres", "/app/.local-storage"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
|
||||
@@ -13,7 +13,7 @@ setup_and_migrate_db() {
|
||||
has_schema=$(psql -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')" ${PG_DATABASE_URL})
|
||||
if [ "$has_schema" = "f" ]; then
|
||||
echo "Database appears to be empty, running migrations."
|
||||
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
|
||||
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
|
||||
yarn database:migrate:prod
|
||||
fi
|
||||
|
||||
|
||||
+3
-2
@@ -9,7 +9,7 @@ The goal here is to have a consistent codebase, which is easy to read and easy t
|
||||
|
||||
For this, it's better to be a bit more verbose than to be too concise.
|
||||
|
||||
Always keep in mind that people read code more often than they write it, especially on an open source project, where anyone can contribute.
|
||||
Always keep in mind that people read code more often than they write it, specially on an open source project, where anyone can contribute.
|
||||
|
||||
There are a lot of rules that are not defined here, but that are automatically checked by linters.
|
||||
|
||||
@@ -150,7 +150,7 @@ type MyType = {
|
||||
|
||||
### Use string literals instead of enums
|
||||
|
||||
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, especially with code completion.
|
||||
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, specially with code completion.
|
||||
|
||||
You can see why TypeScript recommends avoiding enums [here](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
|
||||
|
||||
@@ -288,3 +288,4 @@ An Oxlint rule, `typescript/consistent-type-imports`, enforces the no-type impor
|
||||
Please note that this rule specifically addresses rare edge cases where unintentional type imports occur. TypeScript itself discourages this practice, as mentioned in the [TypeScript 3.8 release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). In most situations, you should not need to use type-only imports.
|
||||
|
||||
To ensure your code complies with this rule, make sure to run Oxlint as part of your development workflow.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "The guide for contributors (or curious developers) who want to run
|
||||
## Prerequisites
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux and macOS">
|
||||
<Tab title="Linux and MacOS">
|
||||
|
||||
Before you can install and use Twenty, make sure you install the following on your computer:
|
||||
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
@@ -31,7 +31,7 @@ wsl --install
|
||||
```
|
||||
You should now see a prompt to restart your computer. If not, restart it manually.
|
||||
|
||||
Upon restart, a PowerShell window will open and install Ubuntu. This may take up some time.
|
||||
Upon restart, a powershell window will open and install Ubuntu. This may take up some time.
|
||||
You'll see a prompt to create a username and password for your Ubuntu installation.
|
||||
|
||||
2. Install and configure git
|
||||
@@ -104,7 +104,7 @@ You should run all commands in the following steps from the root of the project.
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
**Option 1 (preferred):** To provision your database locally:
|
||||
Use the following link to install PostgreSQL on your Linux machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/)
|
||||
Use the following link to install Postgresql on your Linux machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -131,7 +131,7 @@ You should run all commands in the following steps from the root of the project.
|
||||
```
|
||||
|
||||
The installer might not create the `postgres` user by default when installing
|
||||
via Homebrew on macOS. Instead, it creates a PostgreSQL role that matches your macOS
|
||||
via Homebrew on MacOS. Instead, it creates a PostgreSQL role that matches your macOS
|
||||
username (e.g., "john").
|
||||
To check and create the `postgres` user if necessary, follow these steps:
|
||||
```bash
|
||||
@@ -174,8 +174,8 @@ You should run all commands in the following steps from the root of the project.
|
||||
<Tab title="Windows (WSL)">
|
||||
All the following steps are to be run in the WSL terminal (within your virtual machine)
|
||||
|
||||
**Option 1:** To provision your PostgreSQL locally:
|
||||
Use the following link to install PostgreSQL on your Linux virtual machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/)
|
||||
**Option 1:** To provision your Postgresql locally:
|
||||
Use the following link to install Postgresql on your Linux virtual machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -190,12 +190,10 @@ You should run all commands in the following steps from the root of the project.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
You can now access the database at `localhost:5432`.
|
||||
|
||||
If you used the Docker option above, the default credentials are user `postgres` and password `postgres`. For native PostgreSQL installations, use the credentials and roles configured on your machine.
|
||||
You can now access the database at [localhost:5432](localhost:5432), with user `postgres` and password `postgres` .
|
||||
|
||||
## Step 4: Set up a Redis Database (cache)
|
||||
Twenty requires a Redis cache to provide the best performance.
|
||||
Twenty requires a redis cache to provide the best performance
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
@@ -212,10 +210,8 @@ Twenty requires a Redis cache to provide the best performance.
|
||||
```bash
|
||||
brew install redis
|
||||
```
|
||||
Start your Redis server:
|
||||
```bash
|
||||
brew services start redis
|
||||
```
|
||||
Start your redis server:
|
||||
```brew services start redis```
|
||||
|
||||
**Option 2:** If you have docker installed:
|
||||
```bash
|
||||
@@ -233,11 +229,11 @@ Twenty requires a Redis cache to provide the best performance.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
If you need a client GUI, we recommend [Redis Insight](https://redis.io/insight/) (free version available).
|
||||
If you need a Client GUI, we recommend [redis insight](https://redis.io/insight/) (free version available)
|
||||
|
||||
## Step 5: Set up environment variables
|
||||
## Step 5: Setup environment variables
|
||||
|
||||
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup).
|
||||
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup)
|
||||
|
||||
Copy the `.env.example` files in `/front` and `/server`:
|
||||
```bash
|
||||
|
||||
@@ -19,7 +19,7 @@ Apps let you extend Twenty with custom objects, fields, logic functions, AI skil
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ and Yarn 4
|
||||
- Docker (for the local Twenty dev server)
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -37,7 +37,7 @@ yarn twenty app:dev
|
||||
The scaffolder supports two modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
@@ -221,18 +221,6 @@ Then add a `twenty` script:
|
||||
|
||||
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty app:dev`, `yarn twenty help`, etc.
|
||||
|
||||
## How to use a local Twenty instance
|
||||
|
||||
If you're already running a Twenty instance locally (e.g. via `npx nx start twenty-server`), you can connect to it instead of using Docker:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding — skip Docker, connect to your running instance
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding — add a remote pointing to your instance
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
|
||||
|
||||
@@ -20,51 +20,19 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ and Yarn 4
|
||||
- Docker (for the local Twenty dev server)
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Create a new app using the official scaffolder. It can automatically start a local Twenty instance for you:
|
||||
Create a new app using the official scaffolder, then authenticate and start developing:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app — the CLI will offer to start a local Twenty server
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
### Local Server Management
|
||||
|
||||
The SDK includes commands to manage a local Twenty dev server (all-in-one Docker image with PostgreSQL, Redis, server, and worker):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Start the local server (pulls the image if needed)
|
||||
yarn twenty server start
|
||||
|
||||
# Check server status
|
||||
yarn twenty server status
|
||||
|
||||
# Stream server logs
|
||||
yarn twenty server logs
|
||||
|
||||
# Stop the server
|
||||
yarn twenty server stop
|
||||
|
||||
# Reset all data and start fresh
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
The local server comes pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`), so you can start developing immediately without any manual setup.
|
||||
|
||||
### Authentication
|
||||
|
||||
Connect your app to the local server using OAuth:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Authenticate via OAuth (opens browser)
|
||||
yarn twenty remote add --local
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports two modes for controlling which example files are included:
|
||||
@@ -77,18 +45,6 @@ npx create-twenty-app@latest my-app
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
|
||||
### How to use a local Twenty instance
|
||||
|
||||
If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
From here you can:
|
||||
|
||||
```bash filename="Terminal"
|
||||
@@ -107,12 +63,6 @@ yarn twenty function:execute --preInstall
|
||||
# Execute the post-install function
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Build the app for distribution
|
||||
yarn twenty app:build
|
||||
|
||||
# Publish the app to npm or a Twenty server
|
||||
yarn twenty app:publish
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty app:uninstall
|
||||
|
||||
@@ -1274,113 +1224,6 @@ Key points:
|
||||
|
||||
Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Building your app
|
||||
|
||||
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Build the app (output goes to .twenty/output/)
|
||||
yarn twenty app:build
|
||||
|
||||
# Build and create a tarball (.tgz) for distribution
|
||||
yarn twenty app:build --tarball
|
||||
```
|
||||
|
||||
The build process:
|
||||
|
||||
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
|
||||
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
|
||||
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
|
||||
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
|
||||
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
|
||||
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
|
||||
7. **Optionally creates a tarball** — if `--tarball` is passed, runs `npm pack` to create a `.tgz` file ready for distribution.
|
||||
|
||||
The build output in `.twenty/output/` contains:
|
||||
|
||||
```text
|
||||
.twenty/output/
|
||||
├── manifest.json # Manifest with checksums for all built files
|
||||
├── package.json # Copied from app root
|
||||
├── yarn.lock # Copied from app root
|
||||
├── src/
|
||||
│ ├── logic-functions/ # Compiled .mjs logic function files
|
||||
│ └── front-components/ # Compiled .mjs front component files
|
||||
├── public/ # Static assets (if any)
|
||||
└── my-app-1.0.0.tgz # Only with --tarball flag
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `[appPath]` | Path to the app directory (defaults to current directory) |
|
||||
| `--tarball` | Also pack the output into a `.tgz` tarball |
|
||||
|
||||
## Publishing your app
|
||||
|
||||
Use `app:publish` to distribute your app — either to the npm registry or directly to a Twenty server.
|
||||
|
||||
### Publish to npm (default)
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty app:publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty app:publish --tag beta
|
||||
```
|
||||
|
||||
This builds the app and runs `npm publish` from the `.twenty/output/` directory. The published package can then be installed from the Twenty marketplace by any workspace.
|
||||
|
||||
### Publish to a Twenty server
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish directly to a Twenty server
|
||||
yarn twenty app:publish --server https://app.twenty.com
|
||||
```
|
||||
|
||||
This builds the app with a tarball, uploads it to the server via the `uploadAppTarball` GraphQL mutation, and triggers installation in one step. This is useful for private deployments or testing against a specific server.
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `[appPath]` | Path to the app directory (defaults to current directory) |
|
||||
| `--server <url>` | Publish to a Twenty server instead of npm |
|
||||
| `--token <token>` | Authentication token for the target server |
|
||||
| `--tag <tag>` | npm dist-tag (e.g. `beta`, `next`) — only for npm publish |
|
||||
|
||||
## Application registration
|
||||
|
||||
Before an app can be installed in a workspace, it must be **registered**. A registration is a metadata record that describes where the app comes from and how to authenticate it. This is handled automatically by the CLI in most cases.
|
||||
|
||||
### Source types
|
||||
|
||||
Each registration has a **source type** that determines how the app's files are resolved during installation:
|
||||
|
||||
| Source type | How files are resolved | Typical use case |
|
||||
|-------------|----------------------|------------------|
|
||||
| `LOCAL` | Files are synced in real-time by the CLI watcher — installation is skipped | Development with `app:dev` |
|
||||
| `NPM` | Fetched from the npm registry via the `sourcePackage` field | Published apps on npm |
|
||||
| `TARBALL` | Extracted from an uploaded `.tgz` file stored on the server | Private apps published with `--server` |
|
||||
|
||||
### How registration happens
|
||||
|
||||
- **`app:dev`** — automatically creates a `LOCAL` registration the first time you run dev mode against a workspace.
|
||||
- **`app:publish --server`** — uploads a tarball and creates (or updates) a `TARBALL` registration, then installs the app.
|
||||
- **npm marketplace** — `NPM` registrations are created when apps are synced from the npm registry into the Twenty marketplace catalog.
|
||||
- **GraphQL API** — you can also create registrations programmatically via the `createApplicationRegistration` mutation.
|
||||
|
||||
### Registration vs installation
|
||||
|
||||
**Registration** and **installation** are separate concepts:
|
||||
|
||||
- A **registration** (`ApplicationRegistration`) is a global metadata record describing the app: its name, source type, OAuth credentials, and marketplace listing status. It exists independently of any workspace.
|
||||
- An **installation** (`Application`) is a per-workspace instance. When a user installs an app, Twenty resolves the package from the registration's source, writes the built files to storage, and synchronizes the manifest (creating objects, fields, logic functions, etc.) in that workspace.
|
||||
|
||||
One registration can be installed in many workspaces. Each workspace gets its own copy of the app's files and data model.
|
||||
|
||||
### OAuth credentials
|
||||
|
||||
Each registration includes OAuth credentials (`oAuthClientId` and `oAuthClientSecret`) generated at creation time. These are used by the app to authenticate API requests on behalf of users. The client secret is returned **once** at creation — store it securely. You can rotate it later via the `rotateApplicationRegistrationClientSecret` mutation.
|
||||
|
||||
## Manual setup (without the scaffolder)
|
||||
|
||||
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire a single script in your package.json:
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 1-Click w/ Docker Compose
|
||||
|
||||
|
||||
<Warning>
|
||||
Docker containers are for production hosting or self-hosting. For contributing, please check the [Local Setup](/developers/contribute/capabilities/local-setup).
|
||||
Docker containers are for production hosting or self-hosting, for the contribution please check the [Local Setup](/developers/contribute/capabilities/local-setup).
|
||||
</Warning>
|
||||
|
||||
## Overview
|
||||
@@ -13,7 +13,7 @@ This guide provides step-by-step instructions to install and configure the Twent
|
||||
|
||||
**Important:** Only modify settings explicitly mentioned in this guide. Altering other configurations may lead to issues.
|
||||
|
||||
See [Setup Environment Variables](/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the `docker-compose.yml` file at the server and/or worker level, depending on the variable.
|
||||
See docs [Setup Environment Variables](/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the docker-compose.yml file at the server and / or worker level depending on the variable.
|
||||
|
||||
## System Requirements
|
||||
|
||||
@@ -237,3 +237,4 @@ docker compose up -d
|
||||
|
||||
If you encounter any problem, check [Troubleshooting](/developers/self-host/capabilities/troubleshooting) for solutions.
|
||||
|
||||
|
||||
|
||||
@@ -289,57 +289,43 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</Warning>
|
||||
|
||||
## Logic Functions & Code Interpreter
|
||||
## Logic Functions
|
||||
|
||||
Twenty supports logic functions for workflows and the code interpreter for AI data analysis. Both run user-provided code and require explicit configuration for security.
|
||||
|
||||
### Security Defaults
|
||||
|
||||
**In production (NODE_ENV=production):** Both logic functions and code interpreter default to **Disabled**. You must explicitly enable them with `LOGIC_FUNCTION_TYPE` and `CODE_INTERPRETER_TYPE` if you need these features.
|
||||
|
||||
**In development (NODE_ENV=development):** Both default to **LOCAL** for convenience when running locally.
|
||||
Twenty supports logic functions for workflows and custom logic. The execution environment is configured via the `SERVERLESS_TYPE` environment variable.
|
||||
|
||||
<Warning>
|
||||
**Security Notice:** The local driver (`LOGIC_FUNCTION_TYPE=LOCAL` or `CODE_INTERPRETER_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, use `LOGIC_FUNCTION_TYPE=LAMBDA` or `CODE_INTERPRETER_TYPE=E2B` (with sandboxing), or keep them disabled.
|
||||
**Security Notice:** The local driver (`SERVERLESS_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, we highly recommend using `SERVERLESS_TYPE=LAMBDA` or `SERVERLESS_TYPE=DISABLED`.
|
||||
</Warning>
|
||||
|
||||
### Logic Functions - Available Drivers
|
||||
### Available Drivers
|
||||
|
||||
| Driver | Environment Variable | Use Case | Security Level |
|
||||
|--------|---------------------|----------|----------------|
|
||||
| Disabled | `LOGIC_FUNCTION_TYPE=DISABLED` | Disable logic functions entirely | N/A |
|
||||
| Local | `LOGIC_FUNCTION_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
|
||||
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
|
||||
| Disabled | `SERVERLESS_TYPE=DISABLED` | Disable logic functions entirely | N/A |
|
||||
| Local | `SERVERLESS_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
|
||||
|
||||
### Logic Functions - Recommended Configuration
|
||||
### Recommended Configuration
|
||||
|
||||
**For development:**
|
||||
```bash
|
||||
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
|
||||
SERVERLESS_TYPE=LOCAL # default
|
||||
```
|
||||
|
||||
**For production (AWS):**
|
||||
```bash
|
||||
LOGIC_FUNCTION_TYPE=LAMBDA
|
||||
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
|
||||
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
SERVERLESS_TYPE=LAMBDA
|
||||
SERVERLESS_LAMBDA_REGION=us-east-1
|
||||
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**To disable logic functions:**
|
||||
```bash
|
||||
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
```
|
||||
|
||||
### Code Interpreter - Available Drivers
|
||||
|
||||
| Driver | Environment Variable | Use Case | Security Level |
|
||||
|--------|---------------------|----------|----------------|
|
||||
| Disabled | `CODE_INTERPRETER_TYPE=DISABLED` | Disable AI code execution | N/A |
|
||||
| Local | `CODE_INTERPRETER_TYPE=LOCAL` | Development only | Low (no sandboxing) |
|
||||
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Production with sandboxed execution | High (isolated sandbox) |
|
||||
|
||||
<Note>
|
||||
When using `LOGIC_FUNCTION_TYPE=DISABLED` or `CODE_INTERPRETER_TYPE=DISABLED`, any attempt to execute will return an error. This is useful if you want to run Twenty without these capabilities.
|
||||
When using `SERVERLESS_TYPE=DISABLED`, any attempt to execute a logic function will return an error. This is useful if you want to run Twenty without logic function capabilities.
|
||||
</Note>
|
||||
|
||||
@@ -157,9 +157,7 @@ plugins: [
|
||||
|
||||
Run `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` in database container to get access to admin panel.
|
||||
|
||||
#### When running workflow, workflow run fails with "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
|
||||
|
||||
In production, logic functions are disabled by default. Set the `LOGIC_FUNCTION_TYPE` environment variable to `LOCAL` or `LAMBDA` to enable them. This can be configured via environment variables or through the admin panel database variables. See the [Logic Functions setup guide](/developers/self-host/capabilities/setup#logic-functions-available-drivers) for details.
|
||||
|
||||
### 1-click Docker compose
|
||||
|
||||
|
||||
@@ -175,8 +175,7 @@
|
||||
"user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -622,8 +621,7 @@
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1069,8 +1067,7 @@
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1516,8 +1513,7 @@
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1963,8 +1959,7 @@
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2410,8 +2405,7 @@
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2857,8 +2851,7 @@
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3304,8 +3297,7 @@
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3751,8 +3743,7 @@
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4198,8 +4189,7 @@
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4645,8 +4635,7 @@
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5092,8 +5081,7 @@
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5539,8 +5527,7 @@
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5986,8 +5973,7 @@
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -6316,10 +6302,6 @@
|
||||
"source": "/developers/extend/capabilities/apps",
|
||||
"destination": "/developers/extend/apps/getting-started"
|
||||
},
|
||||
{
|
||||
"source": "/developers/extend/mcp",
|
||||
"destination": "/user-guide/ai/capabilities/mcp"
|
||||
},
|
||||
{
|
||||
"source": "/developers/local-setup",
|
||||
"destination": "/developers/contribute/capabilities/local-setup"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 269 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 248 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user