Compare commits

..
Author SHA1 Message Date
Abdul Rahman 9e55fc5f6c Update enqueueLogicFunctionExecution to use isNonEmptyString for universalIdentifier validation and add test for empty universalIdentifier case 2026-05-07 15:09:57 +05:30
Abdul Rahman 8a5d4cf812 Refactor jobId assignment in AppLogicFunctionService for improved readability 2026-05-07 15:09:41 +05:30
Abdul Rahman 204986f99d Refactor AppLogicFunctionModule imports to include TokenModule and WorkspaceCacheStorageModule 2026-05-07 15:00:39 +05:30
Abdul Rahman b5d4d9af63 Merge branch 'main' into logic-function-enqueue-execution 2026-05-07 14:04:31 +05:30
Abdul Rahman 374d64fc61 Add documentation for enqueueLogicFunctionExecution usage
- Documented the `enqueueLogicFunctionExecution` function in the logic-functions.mdx file.
- Provided an example of how to use the function within a logic function handler.
- Clarified the requirement to pass either `name` or `universalIdentifier` for the function to work correctly.
2026-05-07 07:04:12 +05:30
Abdul Rahman e31fa038fe Add enqueueLogicFunctionExecution for handling logic function executions
- Introduced `enqueueLogicFunctionExecution` function to enqueue logic function executions with either a name or a universal identifier.
- Implemented error handling for missing environment variables and validation for input parameters.
- Added tests to verify the functionality and error cases for the new function.
- Updated the logic-function index to export the new function and its types.
2026-05-07 06:35:33 +05:30
Abdul Rahman 957ac1ff94 Add AppLogicFunction module and controller for enqueueing logic function executions
- Introduced `AppLogicFunctionModule`, `AppLogicFunctionController`, and `AppLogicFunctionService` to handle enqueueing logic function executions.
- Added DTO `EnqueueLogicFunctionExecutionDto` for request validation.
- Integrated the new module into the existing `LogicFunctionModule`.
- Implemented guards and validation for secure and structured request handling.
- Enhanced message queue interaction for processing logic function jobs.
2026-05-07 06:34:09 +05:30
Abdul Rahman 70be1712ea Refactor message queue driver methods to return job IDs
Updated the `add` method in `BullMQDriver` and `SyncDriver` to return a job ID instead of void. Adjusted the `MessageQueueDriver` interface accordingly. This change enhances the ability to track jobs by their IDs across the message queue system.
2026-05-06 23:12:54 +05:30
5228 changed files with 119590 additions and 251346 deletions
+21 -21
View File
@@ -106,24 +106,24 @@ Replace `{VERSION}` with the actual version number (e.g., `1.9.0`)
### 2. Create File Structure ### 2. Create File Structure
**Create changelog file:** **Create changelog file:**
- Path: `packages/twenty-website/src/content/releases/{VERSION}.mdx` - Path: `packages/twenty-website-new/src/content/releases/{VERSION}.mdx`
- Example: `packages/twenty-website/src/content/releases/1.9.0.mdx` - Example: `packages/twenty-website-new/src/content/releases/1.9.0.mdx`
**Create image folder:** **Create image folder:**
- Path: `packages/twenty-website/public/images/releases/{MINOR_VERSION}/` - Path: `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
- Example for version 1.9.0: `packages/twenty-website/public/images/releases/1.9/` - Example for version 1.9.0: `packages/twenty-website-new/public/images/releases/1.9/`
- Example for version 2.0.0: `packages/twenty-website/public/images/releases/2.0/` - Example for version 2.0.0: `packages/twenty-website-new/public/images/releases/2.0/`
```bash ```bash
# Create the image folder # Create the image folder
mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION} mkdir -p packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
``` ```
### 3. Move Illustration Files ### 3. Move Illustration Files
**Source:** `/Users/thomascolasdesfrancs/Downloads/🆕` **Source:** `/Users/thomascolasdesfrancs/Downloads/🆕`
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/` **Destination:** `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
**Naming Convention:** `{VERSION}-descriptive-name.webp` **Naming Convention:** `{VERSION}-descriptive-name.webp`
@@ -133,8 +133,8 @@ Examples:
```bash ```bash
# Move and rename source files, then convert to webp if needed # Move and rename source files, then convert to webp if needed
cp ~/Downloads/🆕/source-file.png packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png cp ~/Downloads/🆕/source-file.png packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
cd packages/twenty-website && node scripts/convert-png-to-webp.mjs cd packages/twenty-website-new && node scripts/convert-png-to-webp.mjs
``` ```
### 4. Research Features (if needed) ### 4. Research Features (if needed)
@@ -183,7 +183,7 @@ Description of the third feature.
- **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead - **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead
**Reference Previous Changelogs:** **Reference Previous Changelogs:**
- Check `packages/twenty-website/src/content/releases/` for examples - Check `packages/twenty-website-new/src/content/releases/` for examples
- Recent releases: 1.7.0.mdx, 1.6.0.mdx, 1.5.0.mdx - Recent releases: 1.7.0.mdx, 1.6.0.mdx, 1.5.0.mdx
### 6. Review ### 6. Review
@@ -191,10 +191,10 @@ Description of the third feature.
Open the changelog file for review: Open the changelog file for review:
```bash ```bash
# Open in Cursor # Open in Cursor
cursor packages/twenty-website/src/content/releases/{VERSION}.mdx cursor packages/twenty-website-new/src/content/releases/{VERSION}.mdx
# Open image folder to verify illustrations # Open image folder to verify illustrations
open packages/twenty-website/public/images/releases/{MINOR_VERSION} open packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
``` ```
Review checklist: Review checklist:
@@ -222,8 +222,8 @@ I've created the changelog for version {VERSION}. Here's the content for your re
[Show full MDX content] [Show full MDX content]
Images moved to: Images moved to:
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp - packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp - packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
Please review the content. Once you approve, I'll commit the changes and create the pull request. Please review the content. Once you approve, I'll commit the changes and create the pull request.
``` ```
@@ -242,8 +242,8 @@ Possible user responses:
git status git status
# Add files # Add files
git add packages/twenty-website/src/content/releases/{VERSION}.mdx git add packages/twenty-website-new/src/content/releases/{VERSION}.mdx
git add packages/twenty-website/public/images/releases/{MINOR_VERSION}/ git add packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
# Commit # Commit
git commit -m "Add {VERSION} release changelog" git commit -m "Add {VERSION} release changelog"
@@ -266,7 +266,7 @@ This release includes:
- Feature 2 - Feature 2
- Feature 3 - Feature 3
Changelog file: \`packages/twenty-website/src/content/releases/{VERSION}.mdx\` Changelog file: \`packages/twenty-website-new/src/content/releases/{VERSION}.mdx\`
Release date: {DATE}" \ Release date: {DATE}" \
--base main \ --base main \
--head {VERSION} --head {VERSION}
@@ -280,13 +280,13 @@ Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
- **Format**: `{MAJOR}.{MINOR}.{PATCH}.mdx` - **Format**: `{MAJOR}.{MINOR}.{PATCH}.mdx`
- **Convention**: One file per complete version - **Convention**: One file per complete version
- **Examples**: `1.6.0.mdx`, `1.7.0.mdx`, `2.0.0.mdx` - **Examples**: `1.6.0.mdx`, `1.7.0.mdx`, `2.0.0.mdx`
- **Location**: `packages/twenty-website/src/content/releases/` - **Location**: `packages/twenty-website-new/src/content/releases/`
### Image Folders ### Image Folders
- **Format**: `{MAJOR}.{MINOR}/` - **Format**: `{MAJOR}.{MINOR}/`
- **Convention**: One folder per minor version (shared across patches) - **Convention**: One folder per minor version (shared across patches)
- **Examples**: `1.6/`, `1.7/`, `2.0/` - **Examples**: `1.6/`, `1.7/`, `2.0/`
- **Location**: `packages/twenty-website/public/images/releases/` - **Location**: `packages/twenty-website-new/public/images/releases/`
### Image Files ### Image Files
- **Format**: `{VERSION}-descriptive-name.webp` - **Format**: `{VERSION}-descriptive-name.webp`
@@ -311,8 +311,8 @@ Features to document:
3. ___________________________ 3. ___________________________
Branch name: {VERSION} Branch name: {VERSION}
Changelog path: packages/twenty-website/src/content/releases/{VERSION}.mdx Changelog path: packages/twenty-website-new/src/content/releases/{VERSION}.mdx
Images path: packages/twenty-website/public/images/releases/{MINOR_VERSION}/ Images path: packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
``` ```
## Tips ## Tips
-72
View File
@@ -1,72 +0,0 @@
---
description: GitHub Actions security guidelines for supply chain protection
globs: **/.github/**/*.yml, **/.github/**/*.yaml
alwaysApply: false
---
# GitHub Actions Security
## Pin Third-Party Actions to Commit SHAs
Always reference external actions and reusable workflows by their full commit SHA, never by a mutable tag or branch. Tags can be force-pushed by a compromised maintainer account.
```yaml
# ❌ Mutable tag — vulnerable to supply chain attacks
uses: actions/checkout@v4
uses: actions/setup-node@v4
# ✅ Pinned to commit SHA with tag comment for readability
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
```
## Prefer `gh api` Over Third-Party Dispatch Actions
For repository dispatch calls, use `gh api` directly instead of third-party actions like `peter-evans/repository-dispatch`. This eliminates a supply-chain dependency entirely.
```yaml
# ✅ Use env vars + bracket notation to prevent injection
- name: Dispatch to target repo
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
gh api repos/org/repo/dispatches \
-f event_type=my-event \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[branch]=$BRANCH"
# ✅ Simple dispatch without payload
- name: Trigger workflow
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
run: |
gh api repos/org/repo/dispatches -f event_type=my-event
# ❌ Third-party action dependency
- uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.DISPATCH_TOKEN }}
repository: org/repo
event-type: my-event
# ❌ Inline ${{ }} in shell — vulnerable to injection
- run: |
gh api repos/org/repo/dispatches --input - <<EOF
{"event_type": "x", "client_payload": {"branch": "${{ github.head_ref }}"}}
EOF
```
## Minimal Permissions
Always declare explicit `permissions` at the job level with the least privilege required. Never rely on the default `GITHUB_TOKEN` permissions.
```yaml
# ✅ Explicit minimal permissions
permissions:
contents: read
# ❌ Overly broad or implicit permissions
permissions: write-all
```
-6
View File
@@ -1,6 +0,0 @@
/.github/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/CODEOWNERS @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/workflows/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/actions/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/dependabot.yml @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.yarnrc.yml @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
+2 -2
View File
@@ -21,7 +21,7 @@ runs:
run: corepack enable run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: '${{ inputs.app-path }}/.nvmrc' node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn cache: yarn
@@ -50,4 +50,4 @@ runs:
- name: Deploy - name: Deploy
shell: bash shell: bash
working-directory: ${{ inputs.app-path }} working-directory: ${{ inputs.app-path }}
run: yarn twenty app:publish --private --remote target run: yarn twenty deploy --remote target
@@ -21,7 +21,7 @@ runs:
run: corepack enable run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: '${{ inputs.app-path }}/.nvmrc' node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn cache: yarn
@@ -50,4 +50,4 @@ runs:
- name: Install - name: Install
shell: bash shell: bash
working-directory: ${{ inputs.app-path }} working-directory: ${{ inputs.app-path }}
run: yarn twenty app:install --remote target run: yarn twenty install --remote target
+1 -1
View File
@@ -20,7 +20,7 @@ runs:
run: git fetch origin main --depth=1 run: git fetch origin main --depth=1
- name: Get last successful commit - name: Get last successful commit
if: env.NX_BASE == '' if: env.NX_BASE == ''
uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4 uses: nrwl/nx-set-shas@v4
- name: Fallback to origin/main if no base found - name: Fallback to origin/main if no base found
if: env.NX_BASE == '' if: env.NX_BASE == ''
shell: bash shell: bash
+1 -1
View File
@@ -25,7 +25,7 @@ runs:
run: | run: |
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}" echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}"
- name: Restore cache - name: Restore cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore) uses: actions/cache/restore@v4
id: restore-cache id: restore-cache
with: with:
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }} key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
+1 -4
View File
@@ -9,11 +9,8 @@ inputs:
runs: runs:
using: "composite" using: "composite"
steps: steps:
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
- name: Save cache - name: Save cache
if: ${{ format('{0}', github.event.pull_request.head.repo.fork) != 'true' }} uses: actions/cache/save@v4
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
with: with:
key: ${{ inputs.key }} key: ${{ inputs.key }}
path: | path: |
@@ -48,7 +48,7 @@ runs:
fi fi
- name: Checkout docker compose files - name: Checkout docker compose files
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
repository: ${{ inputs.twenty-repository }} repository: ${{ inputs.twenty-repository }}
ref: ${{ steps.resolve.outputs.git-ref }} ref: ${{ steps.resolve.outputs.git-ref }}
+4 -7
View File
@@ -29,12 +29,12 @@ runs:
echo "packages/*/node_modules" >> $GITHUB_OUTPUT echo "packages/*/node_modules" >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT
- name: Setup Node.js and get yarn cache - name: Setup Node.js and get yarn cache
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version: ${{ inputs.node-version }} node-version: ${{ inputs.node-version }}
- name: Restore node_modules - name: Restore node_modules
id: cache-node-modules id: cache-node-modules
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore) uses: actions/cache/restore@v4
with: with:
key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}} key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}- restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
@@ -44,13 +44,10 @@ runs:
shell: ${{ steps.globals.outputs.ACTION_SHELL }} shell: ${{ steps.globals.outputs.ACTION_SHELL }}
run: | run: |
yarn config set enableHardenedMode true yarn config set enableHardenedMode true
yarn config set enableScripts false
yarn --immutable --check-cache yarn --immutable --check-cache
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
- name: Save cache - name: Save cache
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' && format('{0}', github.event.pull_request.head.repo.fork) != 'true' }} if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save) uses: actions/cache/save@v4
with: with:
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }} key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
path: ${{ steps.globals.outputs.PATH_TO_CACHE }} path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
+2 -2
View File
@@ -1,5 +1,5 @@
# #
# Crowdin CLI configuration for Website translations (twenty-website) # Crowdin CLI configuration for Website translations (twenty-website-new)
# Project ID: 4 # Project ID: 4
# See https://crowdin.github.io/crowdin-cli/configuration for more information # See https://crowdin.github.io/crowdin-cli/configuration for more information
# #
@@ -16,7 +16,7 @@ files:
# #
# Source file - PO file for Lingui # Source file - PO file for Lingui
# #
- source: packages/twenty-website/src/locales/en.po - source: packages/twenty-website-new/src/locales/en.po
# #
# Translation files path # Translation files path
# #
+6 -5
View File
@@ -14,8 +14,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Repository Dispatch - name: Repository Dispatch
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }} with:
run: | token: ${{ secrets.TWENTY_INFRA_TOKEN }}
gh api repos/twentyhq/twenty-infra/dispatches \ repository: twentyhq/twenty-infra
-f event_type=auto-deploy-main event-type: auto-deploy-main
client-payload: '{"github": ${{ toJson(github) }}}' # Passes the entire github context to the downstream workflow
+6 -7
View File
@@ -14,10 +14,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Repository Dispatch - name: Repository Dispatch
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }} with:
REF_NAME: ${{ github.ref_name }} token: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: | repository: twentyhq/twenty-infra
gh api repos/twentyhq/twenty-infra/dispatches \ event-type: auto-deploy-tag
-f event_type=auto-deploy-tag \ client-payload: '{"github": ${{ toJson(github) }}}' # Passes the entire github context to the downstream workflow
-f "client_payload[github][ref_name]=$REF_NAME"
+2 -2
View File
@@ -21,11 +21,11 @@ jobs:
any_changed: ${{ steps.changed-files.outputs.any_changed }} any_changed: ${{ steps.changed-files.outputs.any_changed }}
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Check for changed files - name: Check for changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9 uses: tj-actions/changed-files@v45
with: with:
files: ${{ inputs.files }} files: ${{ inputs.files }}
+7 -6
View File
@@ -17,7 +17,7 @@ jobs:
NODE_OPTIONS: '--max-old-space-size=4096' NODE_OPTIONS: '--max-old-space-size=4096'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
ref: main ref: main
@@ -41,7 +41,7 @@ jobs:
- name: Create pull request - name: Create pull request
if: steps.changes.outputs.changed == 'true' if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.6 uses: peter-evans/create-pull-request@v7
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync AI model catalog from models.dev' commit-message: 'chore: sync AI model catalog from models.dev'
@@ -61,7 +61,8 @@ jobs:
- name: Trigger automerge - name: Trigger automerge
if: steps.changes.outputs.changed == 'true' if: steps.changes.outputs.changed == 'true'
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }} with:
run: | token: ${{ secrets.TWENTY_INFRA_TOKEN }}
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=automated-pr-ready repository: twentyhq/twenty-infra
event-type: automated-pr-ready
+15 -127
View File
@@ -66,7 +66,7 @@ jobs:
steps: steps:
- name: Checkout current branch - name: Checkout current branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
@@ -144,9 +144,6 @@ jobs:
npx nx run twenty-server:database:init:prod npx nx run twenty-server:database:init:prod
- name: Flush cache before seeding current branch
run: npx nx command-no-deps twenty-server -- cache:flush
- name: Seed current branch database with test data - name: Seed current branch database with test data
run: | run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev npx nx command-no-deps twenty-server -- workspace:seed:dev
@@ -163,21 +160,13 @@ jobs:
- name: Wait for current branch server to be ready - name: Wait for current branch server to be ready
run: | run: |
echo "Waiting for current branch server to start..." echo "Waiting for current branch server to start..."
timeout=60 timeout=300
interval=5 interval=5
elapsed=0 elapsed=0
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
while [ $elapsed -lt $timeout ]; do while [ $elapsed -lt $timeout ]; do
GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \ if curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
-H "Content-Type: application/json" \ curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}')
if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \
curl -fsS "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then
echo "Current branch server is ready!" echo "Current branch server is ready!"
break break
fi fi
@@ -188,7 +177,7 @@ jobs:
done done
if [ $elapsed -ge $timeout ]; then if [ $elapsed -ge $timeout ]; then
echo "Timed out waiting for current branch server to serve a valid schema." echo "Timeout waiting for current branch server to start"
echo "Current server log:" echo "Current server log:"
cat /tmp/current-server.log || echo "No current server log found" cat /tmp/current-server.log || echo "No current server log found"
exit 1 exit 1
@@ -315,9 +304,6 @@ jobs:
npx nx run twenty-server:database:init:prod npx nx run twenty-server:database:init:prod
- name: Flush cache before seeding main branch
run: npx nx command-no-deps twenty-server -- cache:flush
- name: Seed main branch database with test data - name: Seed main branch database with test data
run: | run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev npx nx command-no-deps twenty-server -- workspace:seed:dev
@@ -338,17 +324,9 @@ jobs:
interval=5 interval=5
elapsed=0 elapsed=0
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
while [ $elapsed -lt $timeout ]; do while [ $elapsed -lt $timeout ]; do
GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \ if curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
-H "Content-Type: application/json" \ curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}')
if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \
curl -fsS "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then
echo "Main branch server is ready!" echo "Main branch server is ready!"
break break
fi fi
@@ -359,7 +337,7 @@ jobs:
done done
if [ $elapsed -ge $timeout ]; then if [ $elapsed -ge $timeout ]; then
echo "Timed out waiting for main branch server to serve a valid schema." echo "Timeout waiting for main branch server to start"
echo "Main server log:" echo "Main server log:"
cat /tmp/main-server.log || echo "No main server log found" cat /tmp/main-server.log || echo "No main server log found"
exit 1 exit 1
@@ -429,23 +407,11 @@ jobs:
valid=true valid=true
for file in main-schema-introspection.json current-schema-introspection.json \ for file in main-schema-introspection.json current-schema-introspection.json \
main-metadata-schema-introspection.json current-metadata-schema-introspection.json; do main-metadata-schema-introspection.json current-metadata-schema-introspection.json \
if [ ! -f "$file" ]; then main-rest-api.json current-rest-api.json \
echo "::warning::Missing GraphQL schema file: $file"
valid=false
elif ! jq -e '.data.__schema' "$file" >/dev/null 2>&1; then
echo "::warning::File $file is not a valid GraphQL introspection result. First 200 bytes: $(head -c 200 "$file")"
valid=false
fi
done
for file in main-rest-api.json current-rest-api.json \
main-rest-metadata-api.json current-rest-metadata-api.json; do main-rest-metadata-api.json current-rest-metadata-api.json; do
if [ ! -f "$file" ]; then if [ ! -f "$file" ] || ! jq empty "$file" 2>/dev/null; then
echo "::warning::Missing OpenAPI spec file: $file" echo "::warning::Invalid or missing schema file: $file"
valid=false
elif ! jq -e '.openapi // .swagger' "$file" >/dev/null 2>&1; then
echo "::warning::File $file is not a valid OpenAPI spec. First 200 bytes: $(head -c 200 "$file")"
valid=false valid=false
fi fi
done done
@@ -458,7 +424,6 @@ jobs:
echo "Using OpenAPITools/openapi-diff via Docker" echo "Using OpenAPITools/openapi-diff via Docker"
- name: Generate GraphQL Schema Diff Reports - name: Generate GraphQL Schema Diff Reports
id: graphql-diff
if: steps.validate-schemas.outputs.valid == 'true' if: steps.validate-schemas.outputs.valid == 'true'
run: | run: |
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ===" echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
@@ -472,7 +437,6 @@ jobs:
echo "✅ No changes in GraphQL schema" echo "✅ No changes in GraphQL schema"
else else
echo "⚠️ Changes detected in GraphQL schema, generating report..." echo "⚠️ Changes detected in GraphQL schema, generating report..."
echo "core_breaking=true" >> $GITHUB_OUTPUT
echo "# GraphQL Schema Changes" > graphql-schema-diff.md echo "# GraphQL Schema Changes" > graphql-schema-diff.md
echo "" >> graphql-schema-diff.md echo "" >> graphql-schema-diff.md
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || { graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || {
@@ -490,7 +454,6 @@ jobs:
echo "✅ No changes in GraphQL metadata schema" echo "✅ No changes in GraphQL metadata schema"
else else
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..." echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
echo "metadata_breaking=true" >> $GITHUB_OUTPUT
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
echo "" >> graphql-metadata-diff.md echo "" >> graphql-metadata-diff.md
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || { graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || {
@@ -507,7 +470,6 @@ jobs:
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)" ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
- name: Check REST API Breaking Changes - name: Check REST API Breaking Changes
id: rest-diff
if: steps.validate-schemas.outputs.valid == 'true' if: steps.validate-schemas.outputs.valid == 'true'
run: | run: |
echo "=== CHECKING REST API FOR BREAKING CHANGES ===" echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
@@ -530,7 +492,6 @@ jobs:
if [ "$incompatible" = "true" ]; then if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST API" echo "❌ Breaking changes detected in REST API"
echo "breaking=true" >> $GITHUB_OUTPUT
# Generate breaking changes report # Generate breaking changes report
echo "# REST API Breaking Changes" > rest-api-diff.md echo "# REST API Breaking Changes" > rest-api-diff.md
@@ -578,7 +539,6 @@ jobs:
fi fi
- name: Check REST Metadata API Breaking Changes - name: Check REST Metadata API Breaking Changes
id: rest-metadata-diff
if: steps.validate-schemas.outputs.valid == 'true' if: steps.validate-schemas.outputs.valid == 'true'
run: | run: |
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ===" echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
@@ -601,7 +561,6 @@ jobs:
if [ "$incompatible" = "true" ]; then if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST Metadata API" echo "❌ Breaking changes detected in REST Metadata API"
echo "breaking=true" >> $GITHUB_OUTPUT
# Generate breaking changes report (only for breaking changes) # Generate breaking changes report (only for breaking changes)
echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md
@@ -647,82 +606,9 @@ jobs:
echo "::warning::REST Metadata API analysis tool error - continuing workflow" echo "::warning::REST Metadata API analysis tool error - continuing workflow"
fi fi
- name: Fail on breaking changes
if: steps.validate-schemas.outputs.valid == 'true'
run: |
breaking=false
if [ "${{ steps.graphql-diff.outputs.core_breaking }}" = "true" ]; then
echo "❌ GraphQL core schema has breaking changes"
breaking=true
if [ -f graphql-schema-diff.md ]; then
echo ""
cat graphql-schema-diff.md
echo ""
fi
fi
if [ "${{ steps.graphql-diff.outputs.metadata_breaking }}" = "true" ]; then
echo "❌ GraphQL metadata schema has breaking changes"
breaking=true
if [ -f graphql-metadata-diff.md ]; then
echo ""
cat graphql-metadata-diff.md
echo ""
fi
fi
if [ "${{ steps.rest-diff.outputs.breaking }}" = "true" ]; then
echo "❌ REST core API has breaking changes"
breaking=true
if [ -f rest-api-diff.json ]; then
echo ""
jq -r '
(if (.missingEndpoints | length) > 0 then
" Removed endpoints:\n" +
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end),
(if (.changedOperations | length) > 0 then
" Changed operations:\n" +
(.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end)
' rest-api-diff.json | sed '/^$/d'
echo ""
fi
fi
if [ "${{ steps.rest-metadata-diff.outputs.breaking }}" = "true" ]; then
echo "❌ REST metadata API has breaking changes"
breaking=true
if [ -f rest-metadata-api-diff.json ]; then
echo ""
jq -r '
(if (.missingEndpoints | length) > 0 then
" Removed endpoints:\n" +
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end),
(if (.changedOperations | length) > 0 then
" Changed operations:\n" +
(.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end)
' rest-metadata-api-diff.json | sed '/^$/d'
echo ""
fi
fi
if [ "$breaking" = "true" ]; then
echo ""
echo "This PR introduces breaking changes to the public API."
echo "If intentional, deprecate the old endpoint and introduce a new one."
echo "See the breaking changes report artifact and PR comment for details."
exit 1
fi
echo "✅ No breaking API changes detected"
- name: Upload breaking changes report - name: Upload breaking changes report
if: always() if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: breaking-changes-report name: breaking-changes-report
path: | path: |
@@ -740,3 +626,5 @@ jobs:
if [ -f /tmp/main-server.pid ]; then if [ -f /tmp/main-server.pid ]; then
kill $(cat /tmp/main-server.pid) || true kill $(cat /tmp/main-server.pid) || true
fi fi
@@ -0,0 +1,201 @@
name: CI Hello world App E2E
on:
# Temporarily disabled — will be re-enabled when example apps are published.
# push:
# branches:
# - main
#
# pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx run $pkg:set-local-version --releaseVersion=$CI_VERSION
done
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
echo "--- Installing last SDK versions ---"
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute hello-world logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-hello-world-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-hello-world-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-hello-world-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-hello-world]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
@@ -55,7 +55,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
@@ -105,7 +105,7 @@ jobs:
create-twenty-app --version create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace cd /tmp/e2e-test-workspace
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --url http://localhost:3000 create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --skip-local-instance
- name: Install scaffolded app dependencies - name: Install scaffolded app dependencies
run: | run: |
@@ -153,7 +153,7 @@ jobs:
- name: Authenticate with twenty-server - name: Authenticate with twenty-server
run: | run: |
cd /tmp/e2e-test-workspace/test-app cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote:add --api-key ${{ env.TWENTY_API_KEY }} --url ${{ env.TWENTY_API_URL }} npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Run scaffolded app integration test (deploys, installs, and verifies the app) - name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: | run: |
@@ -0,0 +1,199 @@
name: CI Postcard App E2E
on:
# Temporarily disabled — will be re-enabled when example apps are published.
# push:
# branches:
# - main
#
# pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-postcard:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
echo "--- Installing last SDK versions ---"
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute postcard logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName postcard-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-postcard-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-postcard-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-postcard]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+5 -1
View File
@@ -30,8 +30,12 @@ jobs:
matrix: matrix:
task: [lint, typecheck, test] task: [lint, typecheck, test]
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
+6 -1
View File
@@ -28,8 +28,13 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch local actions - name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -54,7 +54,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
uses: ./.github/actions/yarn-install uses: ./.github/actions/yarn-install
+1 -28
View File
@@ -23,11 +23,9 @@ jobs:
packages/twenty-sdk/** packages/twenty-sdk/**
packages/twenty-client-sdk/** packages/twenty-client-sdk/**
packages/twenty-shared/** packages/twenty-shared/**
packages/twenty-server/**
!packages/twenty-sdk/package.json !packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json !packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json !packages/twenty-shared/package.json
!packages/twenty-server/package.json
example-app-postcard: example-app-postcard:
needs: changed-files-check needs: changed-files-check
@@ -56,7 +54,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
uses: ./.github/actions/yarn-install uses: ./.github/actions/yarn-install
@@ -85,31 +83,6 @@ jobs:
working-directory: packages/twenty-apps/examples/postcard working-directory: packages/twenty-apps/examples/postcard
run: npx vitest run run: npx vitest run
- name: Configure remote for SDK CLI
run: |
mkdir -p ~/.twenty
cat > ~/.twenty/config.json <<EOF
{
"version": 1,
"remotes": {
"target": {
"apiUrl": "${TWENTY_API_URL}",
"apiKey": "${TWENTY_API_KEY}",
"accessToken": "${TWENTY_API_KEY}"
}
},
"defaultRemote": "target"
}
EOF
- name: Deploy postcard app (registry install path)
working-directory: packages/twenty-apps/examples/postcard
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs deploy --remote target
- name: Install postcard app (registry install path)
working-directory: packages/twenty-apps/examples/postcard
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs install --remote target
ci-example-app-postcard-status-check: ci-example-app-postcard-status-check:
if: always() && !cancelled() if: always() && !cancelled()
timeout-minutes: 5 timeout-minutes: 5
@@ -32,8 +32,12 @@ jobs:
matrix: matrix:
task: [build, typecheck, lint] task: [build, typecheck, lint]
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -46,8 +50,12 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -55,7 +63,7 @@ jobs:
- name: Build storybook - name: Build storybook
run: npx nx storybook:build twenty-front-component-renderer run: npx nx storybook:build twenty-front-component-renderer
- name: Upload storybook build - name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: storybook-twenty-front-component-renderer name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static path: packages/twenty-front-component-renderer/storybook-static
@@ -68,7 +76,7 @@ jobs:
STORYBOOK_URL: http://localhost:6008 STORYBOOK_URL: http://localhost:6008
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -76,7 +84,7 @@ jobs:
- name: Build dependencies - name: Build dependencies
run: npx nx build twenty-sdk run: npx nx build twenty-sdk
- name: Download storybook build - name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@v4
with: with:
name: storybook-twenty-front-component-renderer name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static path: packages/twenty-front-component-renderer/storybook-static
+22 -10
View File
@@ -38,8 +38,12 @@ jobs:
env: env:
REACT_APP_SERVER_BASE_URL: http://localhost:3000 REACT_APP_SERVER_BASE_URL: http://localhost:3000
steps: steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch local actions - name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -51,7 +55,7 @@ jobs:
- name: Front / Build storybook - name: Front / Build storybook
run: npx nx storybook:build twenty-front run: npx nx storybook:build twenty-front
- name: Upload storybook build - name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: storybook-static name: storybook-static
path: packages/twenty-front/storybook-static path: packages/twenty-front/storybook-static
@@ -75,7 +79,7 @@ jobs:
STORYBOOK_URL: http://localhost:6006 STORYBOOK_URL: http://localhost:6006
steps: steps:
- name: Fetch local actions - name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -92,7 +96,7 @@ jobs:
npx nx build twenty-ui npx nx build twenty-ui
npx nx build twenty-front-component-renderer npx nx build twenty-front-component-renderer
- name: Download storybook build - name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@v4
with: with:
name: storybook-static name: storybook-static
path: packages/twenty-front/storybook-static path: packages/twenty-front/storybook-static
@@ -117,7 +121,7 @@ jobs:
# exit 1 # exit 1
# fi # fi
# - name: Upload coverage artifact # - name: Upload coverage artifact
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 # uses: actions/upload-artifact@v4
# with: # with:
# retention-days: 1 # retention-days: 1
# name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }} # name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
@@ -132,12 +136,12 @@ jobs:
# matrix: # matrix:
# storybook_scope: [modules, pages, performance] # storybook_scope: [modules, pages, performance]
# steps: # steps:
# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 # - uses: actions/checkout@v4
# with: # with:
# fetch-depth: 10 # fetch-depth: 10
# - name: Install dependencies # - name: Install dependencies
# uses: ./.github/actions/yarn-install # uses: ./.github/actions/yarn-install
# - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 # - uses: actions/download-artifact@v4
# with: # with:
# pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-* # pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
# merge-multiple: true # merge-multiple: true
@@ -160,8 +164,12 @@ jobs:
matrix: matrix:
task: [lint, typecheck, test] task: [lint, typecheck, test]
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -195,8 +203,12 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=10240" NODE_OPTIONS: "--max-old-space-size=10240"
ANALYZE: "true" ANALYZE: "true"
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -206,7 +218,7 @@ jobs:
- name: Build frontend - name: Build frontend
run: npx nx build twenty-front run: npx nx build twenty-front
# - name: Upload frontend build artifact # - name: Upload frontend build artifact
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 # uses: actions/upload-artifact@v4
# with: # with:
# name: frontend-build # name: frontend-build
# path: packages/twenty-front/build # path: packages/twenty-front/build
+5 -5
View File
@@ -41,11 +41,11 @@ jobs:
ports: ports:
- 6379:6379 - 6379:6379
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@v4
with: with:
node-version: lts/* node-version: lts/*
@@ -53,7 +53,7 @@ jobs:
uses: ./.github/actions/yarn-install uses: ./.github/actions/yarn-install
- name: Restore Nx build cache - name: Restore Nx build cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore) uses: actions/cache/restore@v4
with: with:
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }} key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
restore-keys: | restore-keys: |
@@ -83,7 +83,7 @@ jobs:
- name: Save Nx build cache - name: Save Nx build cache
if: always() if: always()
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save) uses: actions/cache/save@v4
with: with:
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }} key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
path: | path: |
@@ -119,7 +119,7 @@ jobs:
- name: Upload Playwright results - name: Upload Playwright results
if: always() if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: playwright-results name: playwright-results
path: | path: |
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.inputs.ref }} ref: ${{ github.event.inputs.ref }}
@@ -47,7 +47,7 @@ jobs:
printf '%s\n' "$VERSION" > version.txt printf '%s\n' "$VERSION" > version.txt
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 uses: peter-evans/create-pull-request@v6
with: with:
branch: release/${{ steps.sanitize.outputs.version }} branch: release/${{ steps.sanitize.outputs.version }}
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}" commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
fi fi
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
ref: main ref: main
@@ -55,7 +55,7 @@ jobs:
git tag "v${{ env.VERSION }}" git tag "v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}" git push origin "v${{ env.VERSION }}"
- uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5 - uses: release-drafter/release-drafter@v5
if: contains(github.event.pull_request.labels.*.name, 'create_release') if: contains(github.event.pull_request.labels.*.name, 'create_release')
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6 -2
View File
@@ -30,8 +30,12 @@ jobs:
matrix: matrix:
task: [lint, typecheck, test:unit, test:integration] task: [lint, typecheck, test:unit, test:integration]
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -70,7 +74,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
+7 -7
View File
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -65,7 +65,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -83,12 +83,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Get changed upgrade-version-command files - name: Get changed upgrade-version-command files
id: changed-files id: changed-files
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9 uses: tj-actions/changed-files@v45
with: with:
files: | files: |
packages/twenty-server/src/database/commands/upgrade-version-command/** packages/twenty-server/src/database/commands/upgrade-version-command/**
@@ -178,7 +178,7 @@ jobs:
- 6379:6379 - 6379:6379
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -278,7 +278,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -341,7 +341,7 @@ jobs:
SHARD_COUNTER: 10 SHARD_COUNTER: 10
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
+5 -1
View File
@@ -29,8 +29,12 @@ jobs:
matrix: matrix:
task: [lint, typecheck, test] task: [lint, typecheck, test]
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -26,9 +26,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@v3
with: with:
username: ${{ vars.DOCKERHUB_USERNAME }} username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
@@ -101,9 +101,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@v3
with: with:
username: ${{ vars.DOCKERHUB_USERNAME }} username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
+13 -5
View File
@@ -30,8 +30,12 @@ jobs:
matrix: matrix:
task: [lint, typecheck, test] task: [lint, typecheck, test]
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -44,8 +48,12 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -53,7 +61,7 @@ jobs:
- name: Build storybook - name: Build storybook
run: npx nx storybook:build twenty-ui run: npx nx storybook:build twenty-ui
- name: Upload storybook build - name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: storybook-twenty-ui name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static path: packages/twenty-ui/storybook-static
@@ -66,7 +74,7 @@ jobs:
STORYBOOK_URL: http://localhost:6007 STORYBOOK_URL: http://localhost:6007
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -74,7 +82,7 @@ jobs:
- name: Build dependencies - name: Build dependencies
run: npx nx build twenty-shared run: npx nx build twenty-shared
- name: Download storybook build - name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@v4
with: with:
name: storybook-twenty-ui name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static path: packages/twenty-ui/storybook-static
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.action != 'closed' if: github.event.action != 'closed'
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
uses: ./.github/actions/yarn-install uses: ./.github/actions/yarn-install
- name: Utils / Run Danger.js - name: Utils / Run Danger.js
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.action == 'closed' && github.event.pull_request.merged == true if: github.event.action == 'closed' && github.event.pull_request.merged == true
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
uses: ./.github/actions/yarn-install uses: ./.github/actions/yarn-install
- name: Run congratulate-dangerfile.js - name: Run congratulate-dangerfile.js
+6 -2
View File
@@ -19,7 +19,7 @@ jobs:
files: | files: |
package.json package.json
yarn.lock yarn.lock
packages/twenty-website/** packages/twenty-website-new/**
packages/twenty-shared/** packages/twenty-shared/**
website-task: website-task:
needs: changed-files-check needs: changed-files-check
@@ -32,8 +32,12 @@ jobs:
matrix: matrix:
task: [lint, typecheck, test] task: [lint, typecheck, test]
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
+6 -2
View File
@@ -46,7 +46,7 @@ jobs:
- 6379:6379 - 6379:6379
steps: steps:
- name: Fetch custom Github Actions and base branch history - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
@@ -97,8 +97,12 @@ jobs:
matrix: matrix:
task: [lint, typecheck, validate] task: [lint, typecheck, validate]
steps: 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 - name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 10 fetch-depth: 10
- name: Install dependencies - name: Install dependencies
+16 -45
View File
@@ -8,12 +8,10 @@ on:
pull_request_review: pull_request_review:
types: [submitted] types: [submitted]
issues: issues:
types: [opened] types: [opened, assigned]
repository_dispatch: repository_dispatch:
types: [claude-core-team-issues] types: [claude-core-team-issues]
permissions: {}
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.client_payload.issue_number }} group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.client_payload.issue_number }}
cancel-in-progress: false cancel-in-progress: false
@@ -21,30 +19,10 @@ concurrency:
jobs: jobs:
claude: claude:
if: | if: |
( (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
github.event_name == 'issue_comment' && (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
contains(github.event.comment.body, '@claude') && (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && github.event.review.user.type != 'Bot') ||
github.event.comment.user.type != 'Bot' && (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
) ||
(
github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
github.event.comment.user.type != 'Bot' &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
) ||
(
github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
github.event.review.user.type != 'Bot' &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)
) ||
(
github.event_name == 'issues' &&
github.event.action == 'opened' &&
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
)
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
permissions: permissions:
@@ -72,14 +50,14 @@ jobs:
- 6379:6379 - 6379:6379
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install dependencies - name: Install dependencies
uses: ./.github/actions/yarn-install uses: ./.github/actions/yarn-install
- name: Run Claude Code - name: Run Claude Code
id: claude-code id: claude-code
uses: anthropics/claude-code-action@dde2242db6af13460b916652159b6ba19a598f30 # v1 uses: anthropics/claude-code-action@v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: | additional_permissions: |
@@ -144,14 +122,14 @@ jobs:
ports: ports:
- 6379:6379 - 6379:6379
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install dependencies - name: Install dependencies
uses: ./.github/actions/yarn-install uses: ./.github/actions/yarn-install
- name: Build prompt from dispatch payload - name: Build prompt from dispatch payload
id: prompt id: prompt
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@v7
with: with:
script: | script: |
const p = context.payload.client_payload; const p = context.payload.client_payload;
@@ -166,7 +144,7 @@ jobs:
core.setOutput('issue_number', p.issue_number); core.setOutput('issue_number', p.issue_number);
- name: Run Claude Code - name: Run Claude Code
id: claude id: claude
uses: anthropics/claude-code-action@dde2242db6af13460b916652159b6ba19a598f30 # v1 uses: anthropics/claude-code-action@v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: ${{ steps.prompt.outputs.prompt }} prompt: ${{ steps.prompt.outputs.prompt }}
@@ -181,16 +159,9 @@ jobs:
} }
- name: Dispatch response to ci-privileged - name: Dispatch response to ci-privileged
if: always() if: always()
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} with:
REPO: ${{ steps.prompt.outputs.repo }} token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
ISSUE_NUMBER: ${{ steps.prompt.outputs.issue_number }} repository: twentyhq/ci-privileged
RUN_ID: ${{ github.run_id }} event-type: claude-cross-repo-response
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=claude-cross-repo-response \
-f "client_payload[repo]=$REPO" \
-f "client_payload[issue_number]=$ISSUE_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[run_url]=$RUN_URL"
+6 -5
View File
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
token: ${{ github.token }} token: ${{ github.token }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
@@ -153,7 +153,8 @@ jobs:
- name: Trigger i18n automerge - name: Trigger i18n automerge
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true' if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }} with:
run: | token: ${{ secrets.TWENTY_INFRA_TOKEN }}
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
token: ${{ github.token }} token: ${{ github.token }}
ref: ${{ github.ref }} ref: ${{ github.ref }}
@@ -36,7 +36,7 @@ jobs:
run: yarn docs:generate-navigation-template run: yarn docs:generate-navigation-template
- name: Upload docs to Crowdin - name: Upload docs to Crowdin
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2 uses: crowdin/github-action@v2
with: with:
upload_sources: true upload_sources: true
upload_translations: false upload_translations: false
@@ -1,28 +0,0 @@
name: Auto-Draft External PRs
on:
pull_request_target:
types: [opened]
permissions: {}
jobs:
dispatch:
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.author_association != 'MEMBER' &&
github.event.pull_request.author_association != 'OWNER' &&
github.event.pull_request.author_association != 'COLLABORATOR'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_NODE_ID: ${{ github.event.pull_request.node_id }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=convert-pr-to-draft \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_node_id]=$PR_NODE_ID"
+7 -6
View File
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
token: ${{ github.token }} token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }} ref: ${{ github.head_ref || github.ref_name }}
@@ -69,7 +69,7 @@ jobs:
- name: Pull translations from Crowdin - name: Pull translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure' if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2 uses: crowdin/github-action@v2
with: with:
upload_sources: false upload_sources: false
upload_translations: false upload_translations: false
@@ -139,7 +139,8 @@ jobs:
- name: Trigger i18n automerge - name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true' if: steps.compile_translations.outputs.changes_detected == 'true'
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }} with:
run: | token: ${{ secrets.TWENTY_INFRA_TOKEN }}
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+7 -6
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
token: ${{ github.token }} token: ${{ github.token }}
ref: main ref: main
@@ -80,7 +80,7 @@ jobs:
- name: Upload missing translations - name: Upload missing translations
if: steps.check_extract_changes.outputs.changes_detected == 'true' if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2 uses: crowdin/github-action@v2
with: with:
upload_sources: true upload_sources: true
upload_translations: true upload_translations: true
@@ -105,7 +105,8 @@ jobs:
- name: Trigger i18n automerge - name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true' if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }} with:
run: | token: ${{ secrets.TWENTY_INFRA_TOKEN }}
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+7 -14
View File
@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Get PR number from workflow run - name: Get PR number from workflow run
id: pr-info id: pr-info
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@v7
with: with:
script: | script: |
const runId = context.payload.workflow_run.id; const runId = context.payload.workflow_run.id;
@@ -63,16 +63,9 @@ jobs:
- name: Dispatch to ci-privileged - name: Dispatch to ci-privileged
if: steps.pr-info.outputs.has_pr == 'true' if: steps.pr-info.outputs.has_pr == 'true'
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} with:
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }} token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
RUN_ID: ${{ steps.pr-info.outputs.run_id }} repository: twentyhq/ci-privileged
REPOSITORY: ${{ github.repository }} event-type: breaking-changes-report
BRANCH_STATE: ${{ github.event.workflow_run.head_branch }} client-payload: '{"pr_number": ${{ toJSON(steps.pr-info.outputs.pr_number) }}, "run_id": ${{ toJSON(steps.pr-info.outputs.run_id) }}, "repo": ${{ toJSON(github.repository) }}, "branch_state": ${{ toJSON(github.event.workflow_run.head_branch) }}}'
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=breaking-changes-report \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[branch_state]=$BRANCH_STATE"
@@ -1,26 +0,0 @@
name: PR Review Dispatch
on:
pull_request_target:
types: [ready_for_review, synchronize]
permissions: {}
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
dispatch:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=pr-review \
-f "client_payload[pr_number]=$PR_NUMBER"
+18 -13
View File
@@ -1,6 +1,7 @@
name: 'Preview Environment Dispatch' name: 'Preview Environment Dispatch'
permissions: {} permissions:
contents: write
on: on:
pull_request_target: pull_request_target:
@@ -10,6 +11,7 @@ on:
- packages/twenty-server/** - packages/twenty-server/**
- packages/twenty-front/** - packages/twenty-front/**
- .github/workflows/preview-env-dispatch.yaml - .github/workflows/preview-env-dispatch.yaml
- .github/workflows/preview-env-keepalive.yaml
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -33,15 +35,18 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Dispatch preview-env to ci-privileged - name: Trigger preview environment workflow
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} with:
PR_NUMBER: ${{ github.event.pull_request.number }} token: ${{ secrets.GITHUB_TOKEN }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} repository: ${{ github.repository }}
REPOSITORY: ${{ github.repository }} event-type: preview-environment
run: | client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}'
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-environment \ - name: Dispatch to ci-privileged for PR comment
-f "client_payload[pr_number]=$PR_NUMBER" \ uses: peter-evans/repository-dispatch@v2
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \ with:
-f "client_payload[repo]=$REPOSITORY" token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: preview-env-url
client-payload: '{"pr_number": ${{ toJSON(github.event.pull_request.number) }}, "keepalive_dispatch_time": ${{ toJSON(github.event.pull_request.updated_at) }}, "repo": ${{ toJSON(github.repository) }}}'
@@ -0,0 +1,139 @@
name: 'Preview Environment Keep Alive'
permissions:
contents: read
on:
repository_dispatch:
types: [preview-environment]
jobs:
preview-environment:
timeout-minutes: 310
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run compose setup
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.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
echo "Setting up .env file..."
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
echo "Generating secrets..."
echo "" >> packages/twenty-docker/.env
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
echo "SIGN_IN_PREFILLED=true" >> packages/twenty-docker/.env
echo "Docker compose build..."
cd packages/twenty-docker/
docker compose build
working-directory: ./
- name: Create Tunnel
id: expose-tunnel
uses: codetalkio/expose-tunnel@v1.5.0
with:
service: bore.pub
port: 3000
- name: Start services with correct SERVER_URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
cd packages/twenty-docker/
echo "Setting SERVER_URL to $TUNNEL_URL"
sed -i '/SERVER_URL=/d' .env
echo "" >> .env
echo "SERVER_URL=$TUNNEL_URL" >> .env
# Start the services
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
echo "Waiting for services to be ready..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 5
count=$((count+1))
if [ $count -gt 60 ]; then
echo "Timeout waiting for services to be ready"
docker compose logs
exit 1
fi
echo "Still waiting for services... ($count/60)"
done
echo "All services are up and running!"
working-directory: ./
- name: Seed Dev Workspace
run: |
cd packages/twenty-docker/
echo "Seeding full dev workspace..."
if ! docker compose exec -T server yarn command:prod -- workspace:seed:dev; then
echo "❌ Seeding full dev workspace failed. Dumping server logs..."
docker compose logs server
exit 1
fi
working-directory: ./
- name: Output tunnel URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
echo "✅ Preview Environment Ready!"
echo "🔗 Preview URL: $TUNNEL_URL"
echo "⏱️ This environment will be available for 5 hours"
echo "## 🚀 Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY"
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@v4
with:
name: tunnel-url
path: tunnel-url.txt
retention-days: 1
- name: Keep tunnel alive for 5 hours
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
- name: Cleanup
if: always()
run: |
cd packages/twenty-docker/
docker compose down -v
working-directory: ./
@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Determine project and artifact name - name: Determine project and artifact name
id: project id: project
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@v7
with: with:
script: | script: |
const workflowName = context.payload.workflow_run.name; const workflowName = context.payload.workflow_run.name;
@@ -43,7 +43,7 @@ jobs:
- name: Check if storybook artifact exists - name: Check if storybook artifact exists
id: check-artifact id: check-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@v7
with: with:
script: | script: |
const artifactName = '${{ steps.project.outputs.artifact_name }}'; const artifactName = '${{ steps.project.outputs.artifact_name }}';
@@ -65,7 +65,7 @@ jobs:
- name: Get PR number - name: Get PR number
if: steps.check-artifact.outputs.exists == 'true' if: steps.check-artifact.outputs.exists == 'true'
id: pr-info id: pr-info
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@v7
with: with:
script: | script: |
const headBranch = context.payload.workflow_run.head_branch; const headBranch = context.payload.workflow_run.head_branch;
@@ -107,7 +107,7 @@ jobs:
- name: Download storybook artifact from triggering run - name: Download storybook artifact from triggering run
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@v4
with: with:
name: ${{ steps.project.outputs.artifact_name }} name: ${{ steps.project.outputs.artifact_name }}
path: storybook-static path: storybook-static
@@ -120,7 +120,7 @@ jobs:
- name: Upload storybook tarball - name: Upload storybook tarball
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: ${{ steps.project.outputs.tarball_name }} name: ${{ steps.project.outputs.tarball_name }}
path: /tmp/${{ steps.project.outputs.tarball_file }} path: /tmp/${{ steps.project.outputs.tarball_file }}
@@ -128,20 +128,17 @@ jobs:
- name: Dispatch to ci-privileged - name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} with:
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }} token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
RUN_ID: ${{ github.run_id }} repository: twentyhq/ci-privileged
REPOSITORY: ${{ github.repository }} event-type: visual-regression
PROJECT: ${{ steps.project.outputs.project }} client-payload: >-
BRANCH: ${{ github.event.workflow_run.head_branch }} {
COMMIT: ${{ github.event.workflow_run.head_sha }} "pr_number": "${{ steps.pr-info.outputs.pr_number }}",
run: | "run_id": "${{ github.run_id }}",
gh api repos/twentyhq/ci-privileged/dispatches \ "repo": "${{ github.repository }}",
-f event_type=visual-regression \ "project": "${{ steps.project.outputs.project }}",
-f "client_payload[pr_number]=$PR_NUMBER" \ "branch": "${{ github.event.workflow_run.head_branch }}",
-f "client_payload[run_id]=$RUN_ID" \ "commit": "${{ github.event.workflow_run.head_sha }}"
-f "client_payload[repo]=$REPOSITORY" \ }
-f "client_payload[project]=$PROJECT" \
-f "client_payload[branch]=$BRANCH" \
-f "client_payload[commit]=$COMMIT"
+12 -11
View File
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
token: ${{ github.token }} token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }} ref: ${{ github.head_ref || github.ref_name }}
@@ -54,7 +54,7 @@ jobs:
# Strict mode fails if there are missing website translations. # Strict mode fails if there are missing website translations.
- name: Compile website translations - name: Compile website translations
id: compile_translations_strict id: compile_translations_strict
run: npx nx run twenty-website:lingui:compile --strict run: npx nx run twenty-website-new:lingui:compile --strict
continue-on-error: true continue-on-error: true
- name: Stash any changes before pulling translations - name: Stash any changes before pulling translations
@@ -66,13 +66,13 @@ jobs:
- name: Pull website translations from Crowdin - name: Pull website translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure' if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2 uses: crowdin/github-action@v2
with: with:
upload_sources: false upload_sources: false
upload_translations: false upload_translations: false
download_translations: true download_translations: true
source: 'packages/twenty-website/src/locales/en.po' source: 'packages/twenty-website-new/src/locales/en.po'
translation: 'packages/twenty-website/src/locales/%locale%.po' translation: 'packages/twenty-website-new/src/locales/%locale%.po'
export_only_approved: false export_only_approved: false
localization_branch_name: i18n-website localization_branch_name: i18n-website
base_url: 'https://twenty.api.crowdin.com' base_url: 'https://twenty.api.crowdin.com'
@@ -101,9 +101,9 @@ jobs:
- name: Compile website translations - name: Compile website translations
id: compile_translations id: compile_translations
run: | run: |
npx nx run twenty-website:lingui:compile npx nx run twenty-website-new:lingui:compile
git status git status
git add packages/twenty-website/src/locales git add packages/twenty-website-new/src/locales
if ! git diff --staged --quiet --exit-code; then if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations" git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT echo "changes_detected=true" >> $GITHUB_OUTPUT
@@ -128,7 +128,8 @@ jobs:
- name: Trigger i18n automerge - name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true' if: steps.compile_translations.outputs.changes_detected == 'true'
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }} with:
run: | token: ${{ secrets.TWENTY_INFRA_TOKEN }}
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+12 -11
View File
@@ -10,7 +10,7 @@ on:
push: push:
branches: ['main'] branches: ['main']
paths: paths:
- 'packages/twenty-website/**' - 'packages/twenty-website-new/**'
- '.github/crowdin-website.yml' - '.github/crowdin-website.yml'
- '.github/workflows/website-i18n-push.yaml' - '.github/workflows/website-i18n-push.yaml'
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@v4
with: with:
token: ${{ github.token }} token: ${{ github.token }}
ref: main ref: main
@@ -41,14 +41,14 @@ jobs:
run: npx nx build twenty-shared run: npx nx build twenty-shared
- name: Extract website translations - name: Extract website translations
run: npx nx run twenty-website:lingui:extract run: npx nx run twenty-website-new:lingui:extract
- name: Check and commit extracted files - name: Check and commit extracted files
id: check_extract_changes id: check_extract_changes
run: | run: |
git config --global user.name 'github-actions' git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com' git config --global user.email 'github-actions@twenty.com'
git add packages/twenty-website/src/locales git add packages/twenty-website-new/src/locales
if ! git diff --staged --quiet --exit-code; then if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: extract website translations" git commit -m "chore: extract website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT echo "changes_detected=true" >> $GITHUB_OUTPUT
@@ -57,14 +57,14 @@ jobs:
fi fi
- name: Compile website translations - name: Compile website translations
run: npx nx run twenty-website:lingui:compile run: npx nx run twenty-website-new:lingui:compile
- name: Check and commit compiled files - name: Check and commit compiled files
id: check_compile_changes id: check_compile_changes
run: | run: |
git config --global user.name 'github-actions' git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com' git config --global user.email 'github-actions@twenty.com'
git add packages/twenty-website/src/locales/generated git add packages/twenty-website-new/src/locales/generated
if ! git diff --staged --quiet --exit-code; then if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations" git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT echo "changes_detected=true" >> $GITHUB_OUTPUT
@@ -78,7 +78,7 @@ jobs:
- name: Upload missing website translations - name: Upload missing website translations
if: steps.check_extract_changes.outputs.changes_detected == 'true' if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2 uses: crowdin/github-action@v2
with: with:
upload_sources: true upload_sources: true
upload_translations: true upload_translations: true
@@ -104,7 +104,8 @@ jobs:
- name: Trigger i18n automerge - name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true' if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
env: uses: peter-evans/repository-dispatch@v2
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }} with:
run: | token: ${{ secrets.TWENTY_INFRA_TOKEN }}
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
@@ -1,69 +0,0 @@
name: 'Website Preview Dispatch'
permissions:
contents: read
on:
pull_request:
types: [opened, synchronize, reopened, closed, labeled]
paths:
- packages/twenty-website/**
- .github/workflows/website-preview-dispatch.yaml
concurrency:
# Keyed on PR number so independent PRs don't cancel each other. `github.ref`
# would resolve to the base branch under pull_request and collide.
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
trigger-build:
# Same fork PRs from outside the org don't have `secrets.*` so the dispatch
# call would fail anyway — skip explicitly to avoid noise.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action != 'closed' && (
(github.event.action == 'labeled' && github.event.label.name == 'preview-website') ||
(
(
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
) && contains(fromJSON('["opened","synchronize","reopened"]'), github.event.action)
)
)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch website-preview-build to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=website-preview-build \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[pr_head_ref]=$PR_HEAD_REF"
trigger-cleanup:
# Covers both merge and close-without-merge — pull_request `closed` fires
# for both. PRs left open forever are covered by OpenNext's
# `maxVersionAgeDays: 14` + `maxNumberOfVersions: 50` auto-pruning in
# open-next.config.ts, so nothing leaks even if cleanup never runs.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action == 'closed'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch website-preview-cleanup to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=website-preview-cleanup \
-f "client_payload[pr_number]=$PR_NUMBER"
-27
View File
@@ -1,27 +0,0 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": [
"**/dist/**",
"**/build/**",
"**/lib/**",
"**/.next/**",
"**/coverage/**",
"**/generated/**",
"**/generated-admin/**",
"**/generated-metadata/**",
"**/.cache/**",
"**/node_modules/**",
"**/*.min.js",
"**/*.snap",
"**/*.md",
"**/*.mdx",
"**/seed-project/**/*.mjs",
"packages/twenty-zapier/build/**",
"**/upgrade-version-command/**"
]
}
-40
View File
@@ -43,46 +43,6 @@
{ {
"name": "packages/twenty-e2e-testing", "name": "packages/twenty-e2e-testing",
"path": "../packages/twenty-e2e-testing" "path": "../packages/twenty-e2e-testing"
},
{
"name": "packages/twenty-docs",
"path": "../packages/twenty-docs"
},
{
"name": "packages/create-twenty-app",
"path": "../packages/create-twenty-app"
},
{
"name": "packages/twenty-apps",
"path": "../packages/twenty-apps"
},
{
"name": "packages/twenty-claude-skills",
"path": "../packages/twenty-claude-skills"
},
{
"name": "packages/twenty-cli",
"path": "../packages/twenty-cli"
},
{
"name": "packages/twenty-client-sdk",
"path": "../packages/twenty-client-sdk"
},
{
"name": "packages/twenty-companion",
"path": "../packages/twenty-companion"
},
{
"name": "packages/twenty-front-component-renderer",
"path": "../packages/twenty-front-component-renderer"
},
{
"name": "packages/twenty-sdk",
"path": "../packages/twenty-sdk"
},
{
"name": "packages/twenty-website",
"path": "../packages/twenty-website"
} }
], ],
"settings": { "settings": {
-8
View File
@@ -4,14 +4,6 @@ enableHardenedMode: true
enableInlineHunks: true enableInlineHunks: true
enableScripts: false
nodeLinker: node-modules nodeLinker: node-modules
npmMinimalAgeGate: 3d
npmPreapprovedPackages:
- twenty-sdk
- twenty-client-sdk
yarnPath: .yarn/releases/yarn-4.13.0.cjs yarnPath: .yarn/releases/yarn-4.13.0.cjs
+1 -1
View File
@@ -110,7 +110,7 @@ packages/
├── twenty-ui/ # Shared UI components library ├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities ├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email ├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js marketing website ├── twenty-website-new/ # Next.js marketing website
├── twenty-docs/ # Documentation website ├── twenty-docs/ # Documentation website
├── twenty-zapier/ # Zapier integration ├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests └── twenty-e2e-testing/ # Playwright E2E tests
+44 -44
View File
@@ -1,19 +1,19 @@
<p align="center"> <p align="center">
<a href="https://www.twenty.com"> <a href="https://www.twenty.com">
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" /> <img src="./packages/twenty-website-new/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
</a> </a>
</p> </p>
<h2 align="center" >The #1 Open-Source CRM</h2> <h2 align="center" >The #1 Open-Source CRM</h2>
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.webp" width="12" height="12"/> Figma</a></p> <p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website-new/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website-new/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
<p align="center"> <p align="center">
<a href="https://www.twenty.com"> <a href="https://www.twenty.com">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.webp" /> <source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.webp" /> <source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website/public/images/readme/github-cover-light.webp" alt="Twenty banner" /> <img src="./packages/twenty-website-new/public/images/readme/github-cover-light.png" alt="Twenty banner" />
</picture> </picture>
</a> </a>
</p> </p>
@@ -24,17 +24,17 @@
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack. Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
<a href="https://twenty.com/resources/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a> <a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
<br /> <br />
# Installation # Installation
### <img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud ### <img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date. The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
### <img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app ### <img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
Scaffold a new app with the Twenty CLI: Scaffold a new app with the Twenty CLI:
@@ -63,12 +63,12 @@ export default defineObject({
Then ship it to your workspace: Then ship it to your workspace:
```bash ```bash
npx twenty app:publish --private npx twenty deploy
``` ```
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions. See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
### <img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting ### <img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup). Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup).
@@ -79,61 +79,61 @@ Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box. Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference. Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
<table align="center"> <table align="center">
<tr> <tr>
<td width="50%"> <td width="50%">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.webp" /> <source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" /> <source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" alt="Create your apps" /> <img src="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
</picture> </picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p> <p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
</td> </td>
<td width="50%"> <td width="50%">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.webp" /> <source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" /> <source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" alt="Stay on top with version control" /> <img src="./packages/twenty-website-new/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
</picture> </picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p> <p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website-new/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
</td> </td>
</tr> </tr>
<tr> <tr>
<td width="50%"> <td width="50%">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.webp" /> <source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" /> <source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" alt="All the tools you need to build anything" /> <img src="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
</picture> </picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p> <p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
</td> </td>
<td width="50%"> <td width="50%">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.webp" /> <source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.webp" /> <source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.webp" alt="Customize your layouts" /> <img src="./packages/twenty-website-new/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
</picture> </picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p> <p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
</td> </td>
</tr> </tr>
<tr> <tr>
<td width="50%"> <td width="50%">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.webp" /> <source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" /> <source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" alt="AI agents and chats" /> <img src="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
</picture> </picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p> <p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
</td> </td>
<td width="50%"> <td width="50%">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.webp" /> <source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" /> <source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" alt="Plus all the tools of a good CRM" /> <img src="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
</picture> </picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p> <p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
</td> </td>
</tr> </tr>
</table> </table>
@@ -142,23 +142,23 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
# Stack # Stack
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a> - <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
- <a href="https://nx.dev/"><img src="./packages/twenty-website/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a> - <a href="https://nx.dev/"><img src="./packages/twenty-website-new/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a> - <a href="https://nestjs.com/"><img src="./packages/twenty-website-new/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website-new/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a> - <a href="https://reactjs.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
# Thanks # Thanks
<p align="center"> <p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.webp" height="28" alt="Chromatic" /></a> <a href="https://www.chromatic.com/"><img src="./packages/twenty-website-new/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.webp" height="28" alt="Greptile" /></a> <a href="https://greptile.com"><img src="./packages/twenty-website-new/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.webp" height="28" alt="Sentry" /></a> <a href="https://sentry.io/"><img src="./packages/twenty-website-new/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.webp" height="28" alt="Crowdin" /></a> <a href="https://crowdin.com/"><img src="./packages/twenty-website-new/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
</p> </p>
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin). Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
@@ -166,4 +166,4 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
# Join the Community # Join the Community
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p> <p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website-new/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website-new/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website-new/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
+13 -10
View File
@@ -44,12 +44,12 @@
"cache": true, "cache": true,
"options": { "options": {
"cwd": "{projectRoot}", "cwd": "{projectRoot}",
"command": "npx oxlint -c .oxlintrc.json . && (npx oxfmt --check . || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint --configuration=fix' && false))" "command": "npx oxlint -c .oxlintrc.json . && (prettier . --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
}, },
"configurations": { "configurations": {
"ci": {}, "ci": {},
"fix": { "fix": {
"command": "npx oxlint --fix -c .oxlintrc.json . && npx oxfmt ." "command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
} }
}, },
"dependsOn": ["^build", "twenty-oxlint-rules:build"] "dependsOn": ["^build", "twenty-oxlint-rules:build"]
@@ -57,14 +57,13 @@
"lint:diff-with-main": { "lint:diff-with-main": {
"executor": "nx:run-commands", "executor": "nx:run-commands",
"cache": false, "cache": false,
"dependsOn": ["twenty-oxlint-rules:build"],
"options": { "options": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (npx oxfmt --check $FILES || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))", "command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"pattern": "\\.(ts|tsx|js|jsx)$" "pattern": "\\.(ts|tsx|js|jsx)$"
}, },
"configurations": { "configurations": {
"fix": { "fix": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && npx oxfmt $FILES)" "command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && prettier --write $FILES)"
} }
} }
}, },
@@ -73,14 +72,18 @@
"cache": true, "cache": true,
"options": { "options": {
"cwd": "{projectRoot}", "cwd": "{projectRoot}",
"command": "npx oxfmt --check {args.files} {args.write}", "command": "prettier {args.files} --check --cache {args.cache} --cache-location {args.cacheLocation} --write {args.write} --cache-strategy {args.cacheStrategy}",
"files": ".", "cache": true,
"write": "" "cacheLocation": "../../.cache/prettier/{projectRoot}",
"cacheStrategy": "metadata",
"write": false
}, },
"configurations": { "configurations": {
"ci": {}, "ci": {
"cacheStrategy": "content"
},
"fix": { "fix": {
"command": "npx oxfmt {args.files}" "write": true
} }
}, },
"dependsOn": ["^build"] "dependsOn": ["^build"]
+3 -4
View File
@@ -13,7 +13,6 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"nx": "22.5.4", "nx": "22.5.4",
"oxfmt": "0.50.0",
"tsx": "^4.17.0", "tsx": "^4.17.0",
"verdaccio": "^6.3.1" "verdaccio": "^6.3.1"
}, },
@@ -54,18 +53,18 @@
"packages/twenty-ui", "packages/twenty-ui",
"packages/twenty-utils", "packages/twenty-utils",
"packages/twenty-zapier", "packages/twenty-zapier",
"packages/twenty-website", "packages/twenty-website-new",
"packages/twenty-docs", "packages/twenty-docs",
"packages/twenty-e2e-testing", "packages/twenty-e2e-testing",
"packages/twenty-shared", "packages/twenty-shared",
"packages/twenty-sdk", "packages/twenty-sdk",
"packages/twenty-front-component-renderer", "packages/twenty-front-component-renderer",
"packages/twenty-client-sdk", "packages/twenty-client-sdk",
"packages/twenty-apps",
"packages/twenty-cli", "packages/twenty-cli",
"packages/create-twenty-app", "packages/create-twenty-app",
"packages/twenty-oxlint-rules", "packages/twenty-oxlint-rules",
"packages/twenty-companion", "packages/twenty-companion"
"packages/twenty-claude-skills"
] ]
}, },
"prettier": { "prettier": {
+23 -15
View File
@@ -1,7 +1,7 @@
<div align="center"> <div align="center">
<a href="https://twenty.com"> <a href="https://twenty.com">
<picture> <picture>
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website/public/images/core/logo.svg" height="128"> <img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website-new/public/images/core/logo.svg" height="128">
</picture> </picture>
</a> </a>
<h1>Create Twenty App</h1> <h1>Create Twenty App</h1>
@@ -25,31 +25,39 @@ yarn twenty dev
The scaffolder will: The scaffolder will:
1. Create a new project with TypeScript, linting, tests, and a preconfigured `twenty` CLI 1. Create a new project with TypeScript, linting, tests, and a preconfigured `twenty` CLI
2. Start a local Twenty server via Docker (pulls the latest image automatically) 2. Optionally start a local Twenty server (Docker)
3. Authenticate with the development API key 3. Open the browser for OAuth authentication
## Options ## Options
| Flag | Description | | Flag | Description |
| ---------------------------------- | --------------------------------------------------------------------- | | ------------------------------ | --------------------------------------- |
| `--name <name>` | Set the app name | | `--example <name>` | Initialize from an example |
| `--display-name <displayName>` | Set the display name | | `--name <name>` | Set the app name (skips the prompt) |
| `--description <description>` | Set the description | | `--display-name <displayName>` | Set the display name (skips the prompt) |
| `--url <url>` | Twenty workspace URL (default: `http://localhost:2020`) | | `--description <description>` | Set the description (skips the prompt) |
| `--authentication-method <method>` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) | | `--skip-local-instance` | Skip the local server setup prompt |
By default (no flags), a minimal app is generated with core files and an integration test. Use `--example` to start from a richer example:
```bash
npx create-twenty-app@latest my-twenty-app --example hello-world
```
Examples are sourced from [twentyhq/twenty/packages/twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples).
## Documentation ## Documentation
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)**: Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started)**:
- [Quick Start](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start) — scaffold, run a local server, sync your code - [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started) — step-by-step setup, project structure, server management, CI
- [Concepts](https://docs.twenty.com/developers/extend/apps/getting-started/concepts) — how apps work: entity model, sandboxing, lifecycle - [Building Apps](https://docs.twenty.com/developers/extend/apps/building) — entity definitions, API clients, testing
- [Operations](https://docs.twenty.com/developers/extend/apps/operations/overview) — CLI, testing, CI, deploy and publish - [Publishing](https://docs.twenty.com/developers/extend/apps/publishing) — deploy, npm publish, marketplace
## Troubleshooting ## Troubleshooting
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty docker:logs`. - Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: run `yarn twenty remote:add --local` to re-authenticate. - Auth not working: make sure you are logged in to Twenty in the browser, then run `yarn twenty remote add`.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client. - Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing ## Contributing
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "create-twenty-app", "name": "create-twenty-app",
"version": "2.9.0", "version": "2.3.0",
"description": "Command-line interface to create Twenty application", "description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs", "main": "dist/cli.cjs",
"bin": "dist/cli.cjs", "bin": "dist/cli.cjs",
@@ -32,6 +32,7 @@
"chalk": "^5.3.0", "chalk": "^5.3.0",
"commander": "^12.0.0", "commander": "^12.0.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"inquirer": "^10.0.0",
"lodash.camelcase": "^4.3.0", "lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1", "lodash.kebabcase": "^4.1.1",
"lodash.startcase": "^4.4.0", "lodash.startcase": "^4.4.0",
@@ -42,6 +43,7 @@
"@swc/core": "^1.15.11", "@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39", "@swc/jest": "^0.2.39",
"@types/fs-extra": "^11.0.0", "@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7", "@types/lodash.camelcase": "^4.3.7",
"@types/lodash.kebabcase": "^4.1.7", "@types/lodash.kebabcase": "^4.1.7",
+20 -36
View File
@@ -1,10 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import chalk from 'chalk'; import chalk from 'chalk';
import { Command, CommanderError } from 'commander'; import { Command, CommanderError } from 'commander';
import { import { CreateAppCommand } from '@/create-app.command';
type AuthenticationMethod,
CreateAppCommand,
} from '@/create-app.command';
import packageJson from '../package.json'; import packageJson from '../package.json';
const program = new Command(packageJson.name) const program = new Command(packageJson.name)
@@ -15,26 +12,32 @@ const program = new Command(packageJson.name)
'Output the current version of create-twenty-app.', 'Output the current version of create-twenty-app.',
) )
.argument('[directory]') .argument('[directory]')
.option('-n, --name <name>', 'Application name') .option('--example <name>', 'Initialize from an example')
.option('-d, --display-name <displayName>', 'Application display name') .option('-n, --name <name>', 'Application name (skips prompt)')
.option('--description <description>', 'Application description')
.option('--url <url>', 'Twenty server URL (default: http://localhost:2020)')
.option('--api-url <apiUrl>', '[deprecated: use --url]')
.option( .option(
'--authentication-method <method>', '-d, --display-name <displayName>',
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)', 'Application display name (skips prompt)',
) )
.option(
'--description <description>',
'Application description (skips prompt)',
)
.option(
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
)
.option('-y, --yes', 'Auto-confirm prompts (e.g. start existing container)')
.helpOption('-h, --help', 'Display this help message.') .helpOption('-h, --help', 'Display this help message.')
.action( .action(
async ( async (
directory?: string, directory?: string,
options?: { options?: {
example?: string;
name?: string; name?: string;
displayName?: string; displayName?: string;
description?: string; description?: string;
url?: string; skipLocalInstance?: boolean;
apiUrl?: string; yes?: boolean;
authenticationMethod?: AuthenticationMethod;
}, },
) => { ) => {
if (directory && !/^[a-z0-9-]+$/.test(directory)) { if (directory && !/^[a-z0-9-]+$/.test(directory)) {
@@ -51,33 +54,14 @@ const program = new Command(packageJson.name)
process.exit(1); process.exit(1);
} }
if (
options?.authenticationMethod &&
!['oauth', 'apiKey'].includes(options.authenticationMethod)
) {
console.error(
chalk.red(
'Error: --authentication-method must be "oauth" or "apiKey".',
),
);
process.exit(1);
}
if (options?.apiUrl) {
console.warn(
chalk.yellow('Warning: --api-url is deprecated. Use --url instead.'),
);
}
const serverUrl = (options?.url ?? options?.apiUrl)?.replace(/\/+$/, '');
await new CreateAppCommand().execute({ await new CreateAppCommand().execute({
directory, directory,
example: options?.example,
name: options?.name, name: options?.name,
displayName: options?.displayName, displayName: options?.displayName,
description: options?.description, description: options?.description,
serverUrl, skipLocalInstance: options?.skipLocalInstance,
authenticationMethod: options?.authenticationMethod, yes: options?.yes,
}); });
}, },
); );
@@ -1,67 +0,0 @@
## Base documentation
- Getting started:
- https://docs.twenty.com/developers/extend/apps/getting-started/quick-start.md
- https://docs.twenty.com/developers/extend/apps/getting-started/concepts.md
- https://docs.twenty.com/developers/extend/apps/getting-started/project-structure.md
- https://docs.twenty.com/developers/extend/apps/getting-started/local-server.md
- https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding.md
- https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting.md
- Config:
- https://docs.twenty.com/developers/extend/apps/config/overview.md
- https://docs.twenty.com/developers/extend/apps/config/application.md
- https://docs.twenty.com/developers/extend/apps/config/roles.md
- https://docs.twenty.com/developers/extend/apps/config/install-hooks.md
- https://docs.twenty.com/developers/extend/apps/config/public-assets.md
- Data:
- https://docs.twenty.com/developers/extend/apps/data/overview.md
- https://docs.twenty.com/developers/extend/apps/data/objects.md
- https://docs.twenty.com/developers/extend/apps/data/extending-objects.md
- https://docs.twenty.com/developers/extend/apps/data/relations.md
- Logic:
- https://docs.twenty.com/developers/extend/apps/logic/overview.md
- https://docs.twenty.com/developers/extend/apps/logic/logic-functions.md
- https://docs.twenty.com/developers/extend/apps/logic/skills-and-agents.md
- https://docs.twenty.com/developers/extend/apps/logic/connections.md
- Layout:
- https://docs.twenty.com/developers/extend/apps/layout/overview.md
- https://docs.twenty.com/developers/extend/apps/layout/views.md
- https://docs.twenty.com/developers/extend/apps/layout/navigation-menu-items.md
- https://docs.twenty.com/developers/extend/apps/layout/page-layouts.md
- https://docs.twenty.com/developers/extend/apps/layout/front-components.md
- https://docs.twenty.com/developers/extend/apps/layout/command-menu-items.md
- Operations:
- https://docs.twenty.com/developers/extend/apps/operations/overview.md
- https://docs.twenty.com/developers/extend/apps/operations/cli.md
- https://docs.twenty.com/developers/extend/apps/operations/testing.md
- https://docs.twenty.com/developers/extend/apps/operations/publishing.md
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
## Best practice
It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options:
| Entity type | Command | Generated file |
| -------------------- | ---------------------------------------- | ------------------------------------- |
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
This helps automatically generate required IDs etc.
@@ -2,21 +2,10 @@ This is a [Twenty](https://twenty.com) application bootstrapped with [`create-tw
## Getting Started ## Getting Started
This app was scaffolded with a local Twenty server running at [http://localhost:2020](http://localhost:2020).
Login with the default development credentials: `tim@apple.dev` / `tim@apple.dev`.
Run `yarn twenty help` to list all available commands. Run `yarn twenty help` to list all available commands.
## Useful Commands
- `yarn twenty dev` - Start the development server and sync your app
- `yarn twenty docker:status` - Check the local Twenty server status
- `yarn twenty docker:start` - Start the local Twenty server
- `yarn test` - Run integration tests
## Learn More ## Learn More
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start) - [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk) - [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57) - [Discord](https://discord.gg/cx5n4Jzs57)
@@ -11,7 +11,7 @@ permissions:
contents: read contents: read
env: env:
TWENTY_DEPLOY_URL: http://localhost:2020 TWENTY_DEPLOY_URL: http://localhost:3000
concurrency: concurrency:
group: cd-${{ github.ref }} group: cd-${{ github.ref }}
@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none">
<rect width="80" height="80" rx="16" fill="#141414"/>
<rect x="20" y="20" width="16" height="16" rx="4" fill="#FAFAFA"/>
<rect x="44" y="20" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
<rect x="20" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
<rect x="44" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.3"/>
</svg>

Before

Width:  |  Height:  |  Size: 454 B

@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) { if (!apiUrl || !apiKey) {
throw new Error( throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' + 'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.', 'Or set them in vitest env config.',
); );
} }
@@ -4,10 +4,12 @@ import {
APP_DESCRIPTION, APP_DESCRIPTION,
APP_DISPLAY_NAME, APP_DISPLAY_NAME,
APPLICATION_UNIVERSAL_IDENTIFIER, APPLICATION_UNIVERSAL_IDENTIFIER,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers'; } from 'src/constants/universal-identifiers';
export default defineApplication({ export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: APP_DISPLAY_NAME, displayName: APP_DISPLAY_NAME,
description: APP_DESCRIPTION, description: APP_DESCRIPTION,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
}); });
@@ -2,10 +2,3 @@ export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED'; export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED';
export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED'; export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED'; export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
'UUID-TO-BE-GENERATED';
@@ -1,11 +1,11 @@
import { defineApplicationRole } from 'twenty-sdk/define'; import { defineRole } from 'twenty-sdk/define';
import { import {
APP_DISPLAY_NAME, APP_DISPLAY_NAME,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers'; } from 'src/constants/universal-identifiers';
export default defineApplicationRole({ export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: `${APP_DISPLAY_NAME} default function role`, label: `${APP_DISPLAY_NAME} default function role`,
description: `${APP_DISPLAY_NAME} default function role`, description: `${APP_DISPLAY_NAME} default function role`,
@@ -1,291 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
Avatar,
IconBox,
IconHierarchy,
IconLayout,
IconSettingsAutomation,
} from 'twenty-sdk/ui';
import { useState } from 'react';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
const DOCS_BASE_URL = 'https://docs.twenty.com/developers/extend/apps';
const CATEGORIES = [
{
title: 'Data model',
color: '#73D08D',
items: [
{ label: 'CUSTOM OBJECT', href: `${DOCS_BASE_URL}/data/objects` },
{
label: 'CUSTOM FIELDS',
href: `${DOCS_BASE_URL}/data/extending-objects`,
},
],
rotation: '2.4deg',
},
{
title: 'Logic',
color: '#F4D345',
items: [
{
label: 'TOOLS',
href: `${DOCS_BASE_URL}/logic/logic-functions`,
},
{
label: 'LOGIC FUNCTION',
href: `${DOCS_BASE_URL}/logic/logic-functions`,
},
{
label: 'SKILLS',
href: `${DOCS_BASE_URL}/logic/skills-and-agents`,
},
],
rotation: '0deg',
},
{
title: 'Layout',
color: '#C4A2E0',
items: [
{ label: 'VIEWS', href: `${DOCS_BASE_URL}/layout/views` },
{ label: 'WIDGETS', href: `${DOCS_BASE_URL}/layout/page-layouts` },
{
label: 'LAYOUT PAGES',
href: `${DOCS_BASE_URL}/layout/page-layouts`,
},
{
label: 'COMMANDS',
href: `${DOCS_BASE_URL}/layout/command-menu-items`,
},
],
rotation: '-2.8deg',
},
] as const;
const ArrowUpRight = ({ color = '#999' }: { color?: string }) => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path
d="M4.5 3.5H10.5V9.5M10.5 3.5L3.5 10.5"
stroke={color}
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const CategoryCard = ({
title,
color,
items,
rotation,
}: {
title: string;
color: string;
items: ReadonlyArray<{ label: string; href: string }>;
rotation: string;
}) => {
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const CategoryIcon = () => {
if (title === 'Data model') {
return <IconHierarchy color={color} size={'20px'} />;
}
if (title === 'Logic') {
return <IconSettingsAutomation color={color} size={'20px'} />;
}
if (title === 'Layout') {
return <IconLayout color={color} size={'20px'} />;
}
};
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
border: `1px solid ${color}80`,
borderRadius: '12px',
overflow: 'hidden',
width: '240px',
background: '#FFFFFF',
transform: `rotate(${rotation})`,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
}}
>
<div
style={{
padding: '16px 20px',
background: `${color}22`,
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<CategoryIcon />
<span
style={{
fontSize: '16px',
fontWeight: 600,
color: color,
}}
>
{title}
</span>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: '8px',
gap: '4px',
}}
>
{items.map((item) => {
const isHovered = hoveredItem === item.label;
return (
<a
key={item.label}
href={item.href}
target="_blank"
rel="noopener noreferrer"
onMouseEnter={() => setHoveredItem(item.label)}
onMouseLeave={() => setHoveredItem(null)}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
textDecoration: 'none',
cursor: 'pointer',
padding: '10px 12px',
borderRadius: '8px',
background: isHovered ? '#0000000A' : 'transparent',
transition: 'background 0.15s',
}}
>
<IconBox color={color} size={'20px'} />
<span
style={{
fontSize: '13px',
fontWeight: 300,
color: '#333',
letterSpacing: '0.5px',
flex: 1,
}}
>
{item.label}
</span>
{isHovered && <ArrowUpRight />}
</a>
);
})}
</div>
</div>
);
};
const MainPage = () => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
fontFamily:
'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
gap: '8px',
padding: '40px',
}}
>
<Avatar
placeholder={APP_DISPLAY_NAME}
placeholderColorSeed={APP_DISPLAY_NAME}
size="xl"
/>
<span
style={{
fontSize: '24px',
fontWeight: 600,
color: '#333',
marginTop: '8px',
}}
>
{APP_DISPLAY_NAME}
</span>
<span
style={{
fontSize: '13px',
color: '#888',
textAlign: 'center',
lineHeight: '1.5',
}}
>
Was installed successfully.
<br />
You can now add content to your app.
</span>
<a
href="/settings/applications#installed"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
marginTop: '16px',
fontSize: '13px',
color: '#333',
textDecoration: 'none',
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
background: '#fafafa',
transition: 'background 0.15s, border-color 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f0f0f0';
e.currentTarget.style.borderColor = '#ccc';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#fafafa';
e.currentTarget.style.borderColor = '#e0e0e0';
}}
>
Open app settings
<ArrowUpRight color="#333" />
</a>
<div
style={{
display: 'flex',
gap: '16px',
marginTop: '32px',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'flex-start',
}}
>
{CATEGORIES.map((category) => (
<CategoryCard
key={category.title}
title={category.title}
color={category.color}
items={category.items}
rotation={category.rotation}
/>
))}
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
description: `${APP_DISPLAY_NAME} front component displaying the app logo and name`,
component: MainPage,
});
@@ -1,19 +0,0 @@
import {
defineNavigationMenuItem,
NavigationMenuItemType,
} from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineNavigationMenuItem({
universalIdentifier: MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
icon: 'IconFile',
position: -1,
type: NavigationMenuItemType.PAGE_LAYOUT,
pageLayoutUniversalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
});
@@ -1,37 +0,0 @@
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default definePageLayout({
universalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
type: 'STANDALONE_PAGE',
tabs: [
{
universalIdentifier: MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Overview',
position: 0,
icon: 'IconApps',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
title: ' ',
type: 'FRONT_COMPONENT',
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
@@ -1 +0,0 @@
nodeLinker: node-modules
@@ -1,172 +1,96 @@
import { copyBaseApplicationProject } from '@/utils/app-template'; import { copyBaseApplicationProject } from '@/utils/app-template';
import { downloadExample } from '@/utils/download-example';
import { convertToLabel } from '@/utils/convert-to-label'; import { convertToLabel } from '@/utils/convert-to-label';
import { install } from '@/utils/install'; import { install } from '@/utils/install';
import { tryGitInit } from '@/utils/try-git-init'; import { tryGitInit } from '@/utils/try-git-init';
import chalk from 'chalk'; import chalk from 'chalk';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import kebabCase from 'lodash.kebabcase'; import kebabCase from 'lodash.kebabcase';
import * as path from 'path'; import * as path from 'path';
import { basename } from 'path'; import { basename } from 'path';
import { spawn } from 'node:child_process';
import { import {
authLogin,
authLoginOAuth, authLoginOAuth,
checkDockerRunning, checkDockerRunning,
ConfigService, ConfigService,
DEV_API_KEY, containerExists,
DEV_API_URL, detectLocalServer,
serverStart, serverStart,
type ServerStartResult,
} from 'twenty-sdk/cli'; } from 'twenty-sdk/cli';
import { isDefined, normalizeUrl } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import {
getDockerInstallInstructions,
isDockerInstalled,
} from '@/utils/docker-install';
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd(); const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
const IMAGE = 'twentycrm/twenty-app-dev:latest';
export type AuthenticationMethod = 'oauth' | 'apiKey';
type CreateAppOptions = { type CreateAppOptions = {
directory?: string; directory?: string;
example?: string;
name?: string; name?: string;
displayName?: string; displayName?: string;
description?: string; description?: string;
serverUrl?: string; skipLocalInstance?: boolean;
authenticationMethod?: AuthenticationMethod; yes?: boolean;
}; };
export class CreateAppCommand { export class CreateAppCommand {
private stepCounter = 0;
private totalSteps = 0;
async execute(options: CreateAppOptions = {}): Promise<void> { async execute(options: CreateAppOptions = {}): Promise<void> {
const { appName, appDisplayName, appDirectory, appDescription } = const { appName, appDisplayName, appDirectory, appDescription } =
this.getAppInfos(options); await this.getAppInfos(options);
const serverUrl = options.serverUrl ?? DEV_API_URL;
const skipLocalInstance = serverUrl !== DEV_API_URL;
if (!skipLocalInstance && !isDockerInstalled()) {
console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n'));
process.exit(1);
}
if (skipLocalInstance && options.authenticationMethod === 'apiKey') {
console.log(
chalk.yellow(
'API key authentication is only supported on a local Docker instance. Ignoring and switching to OAuth authentication.',
),
);
}
const authenticationMethod = skipLocalInstance
? 'oauth'
: (options.authenticationMethod ?? 'apiKey');
try { try {
await this.validateDirectory(appDirectory); await this.validateDirectory(appDirectory);
this.totalSteps = this.computeTotalSteps({ this.logCreationInfo({ appDirectory, appName });
skipLocalInstance,
});
this.stepCounter = 0;
const dockerPullPromise =
!skipLocalInstance && checkDockerRunning()
? this.pullImageInBackground()
: Promise.resolve(false);
this.logPlan({ appName, appDisplayName, appDescription, appDirectory });
this.logNextStep('Creating project directory');
await fs.ensureDir(appDirectory); await fs.ensureDir(appDirectory);
this.logDetail(appDirectory); if (options.example) {
const exampleSucceeded = await this.tryDownloadExample(
this.logNextStep('Scaffolding project files'); options.example,
appDirectory,
);
if (!exampleSucceeded) {
await copyBaseApplicationProject({ await copyBaseApplicationProject({
appName, appName,
appDisplayName, appDisplayName,
appDescription, appDescription,
appDirectory, appDirectory,
onProgress: (message) => this.logDetail(message), });
}
} else {
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
});
}
await install(appDirectory);
await tryGitInit(appDirectory);
let serverResult: ServerStartResult | undefined;
if (!options.skipLocalInstance) {
const shouldStartServer = await this.shouldStartServer(options.yes);
if (shouldStartServer) {
const startResult = await serverStart({
onProgress: (message: string) => console.log(chalk.gray(message)),
}); });
this.logNextStep('Installing dependencies'); if (startResult.success) {
serverResult = startResult.data;
await install(appDirectory, (message) => this.logDetail(message)); await this.promptConnectToLocal(serverResult.url);
this.logNextStep('Initializing Git repository');
const gitInitialized = await tryGitInit(appDirectory);
if (gitInitialized) {
this.logDetail('Initialized on branch main');
this.logDetail('Created initial commit');
} else { } else {
this.logDetail( console.log(chalk.yellow(`\n${startResult.error.message}`));
'Skipped (Git unavailable, initialization failed, or already in a repository)',
);
} }
console.log('');
let authSucceeded = false;
let resolvedServerUrl = serverUrl;
let serverReady = skipLocalInstance;
if (!skipLocalInstance) {
this.logNextStep('Starting Twenty server');
const serverResult = await this.ensureDockerServer(dockerPullPromise);
if (isDefined(serverResult.url)) {
resolvedServerUrl = serverResult.url;
serverReady = true;
} }
} }
if (serverReady) { this.logSuccess(appDirectory, serverResult);
this.logNextStep('Authenticating');
authSucceeded = await this.tryExistingAuth(resolvedServerUrl);
if (authSucceeded) {
this.logDetail('Reusing existing credentials');
} else if (authenticationMethod === 'oauth') {
this.logDetail('Starting OAuth flow');
authSucceeded = await this.authenticateWithOAuth(resolvedServerUrl);
} else {
this.logDetail('Using development API key');
authSucceeded = await this.authenticateWithDevKey(resolvedServerUrl);
}
}
this.logNextStep('Installing application');
let syncSucceeded = false;
if (serverReady && authSucceeded) {
syncSucceeded = await this.syncApplication(appDirectory);
if (!syncSucceeded) {
this.logDetail('Sync failed. Run `yarn twenty dev --once` manually.');
return;
}
} else {
this.logDetail('Skipped (server or authentication not available)');
}
if (syncSucceeded) {
await this.openMainPage(appDirectory, resolvedServerUrl);
}
this.logSuccess(appDirectory, resolvedServerUrl, authSucceeded);
} catch (error) { } catch (error) {
console.error( console.error(
chalk.red('\nCreate application failed:'), chalk.red('\nCreate application failed:'),
@@ -176,42 +100,66 @@ export class CreateAppCommand {
} }
} }
private computeTotalSteps({ private async getAppInfos(options: CreateAppOptions): Promise<{
skipLocalInstance,
}: {
skipLocalInstance: boolean;
}): number {
let steps = 4; // directory, scaffold, install, git
if (!skipLocalInstance) {
steps += 1; // start server
}
steps += 1; // authenticate (oauth or apiKey)
steps += 1; // sync application
return steps;
}
private getAppInfos(options: CreateAppOptions): {
appName: string; appName: string;
appDisplayName: string; appDisplayName: string;
appDescription: string; appDescription: string;
appDirectory: string; appDirectory: string;
} { }> {
const { directory } = options;
const hasName = isDefined(options.name) || isDefined(directory);
const hasDisplayName = isDefined(options.displayName);
const hasDescription = isDefined(options.description);
const hasExample = isDefined(options.example);
const { name, displayName, description } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Application name:',
when: () => !hasName && !hasExample,
default: 'my-twenty-app',
validate: (input) => {
if (input.length === 0) return 'Application name is required';
return true;
},
},
{
type: 'input',
name: 'displayName',
message: 'Application display name:',
when: () => !hasDisplayName && !hasExample,
default: (answers: { name?: string }) => {
return convertToLabel(
answers?.name ?? options.name ?? directory ?? '',
);
},
},
{
type: 'input',
name: 'description',
message: 'Application description (optional):',
when: () => !hasDescription && !hasExample,
default: '',
},
]);
const appName = ( const appName = (
options.name ?? options.name ??
options.directory ?? name ??
directory ??
options.example ??
'my-twenty-app' 'my-twenty-app'
).trim(); ).trim();
const appDisplayName = const appDisplayName =
options.displayName?.trim() || convertToLabel(appName); (options.displayName ?? displayName)?.trim() || convertToLabel(appName);
const appDescription = (options.description ?? '').trim(); const appDescription = (options.description ?? description ?? '').trim();
const appDirectory = options.directory const appDirectory = directory
? path.join(CURRENT_EXECUTION_DIRECTORY, options.directory) ? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName)); : path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
return { appName, appDisplayName, appDirectory, appDescription }; return { appName, appDisplayName, appDirectory, appDescription };
@@ -230,333 +178,118 @@ export class CreateAppCommand {
} }
} }
private logPlan({ private async tryDownloadExample(
appName, example: string,
appDisplayName, appDirectory: string,
appDescription, ): Promise<boolean> {
appDirectory, try {
}: { await downloadExample(example, appDirectory);
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
}): void {
console.log(chalk.blue('\nCreating Twenty Application\n'));
console.log(chalk.white(` Name: ${appName}`));
console.log(chalk.white(` Display name: ${appDisplayName}`));
if (appDescription) { return true;
console.log(chalk.white(` Description: ${appDescription}`)); } catch (error) {
} console.error(
chalk.red(
console.log(chalk.white(` Directory: ${appDirectory}`)); `\n${error instanceof Error ? error.message : 'Failed to download example.'}`,
console.log('');
}
private logNextStep(title: string): void {
this.stepCounter++;
console.log(
chalk.blue(`\n[${this.stepCounter}/${this.totalSteps}]`) +
chalk.white(` ${title}...`),
);
}
private logDetail(message: string): void {
console.log(chalk.gray(`${message}`));
}
private pullImageInBackground(): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn('docker', ['pull', IMAGE], { stdio: 'ignore' });
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
});
}
private async ensureDockerServer(
dockerPullPromise: Promise<boolean>,
): Promise<{ url?: string }> {
if (!checkDockerRunning()) {
console.log(
chalk.yellow(
'\n Docker is installed but not running.\n' +
' Please start Docker and run this command again.\n',
), ),
); );
return {}; const { useTemplate } = await inquirer.prompt([
} {
type: 'confirm',
this.logDetail('Ensuring latest Twenty server image...'); name: 'useTemplate',
message: 'Would you like to create a default template app instead?',
const pullSucceeded = await dockerPullPromise; default: true,
},
if (!pullSucceeded) {
this.logDetail(
'Image pull failed, continuing with cached image if available...',
);
}
const startResult = await serverStart({
onProgress: (message: string) => this.logDetail(message),
});
if (startResult.success) {
return { url: startResult.data.url };
}
console.log(chalk.yellow(`\n ${startResult.error.message}`));
return {};
}
private async openMainPage(
appDirectory: string,
serverUrl: string,
): Promise<void> {
try {
const configService = new ConfigService();
const config = await configService.getConfig();
const token = config.twentyCLIAccessToken ?? config.apiKey;
if (!token) {
return;
}
const [universalIdentifier, frontUrl] = await Promise.all([
this.readMainPageLayoutUniversalIdentifier(appDirectory),
this.resolveWorkspaceFrontUrl(serverUrl, token),
]); ]);
if (!universalIdentifier || !frontUrl) { if (!useTemplate) {
return; process.exit(1);
} }
const pageLayoutId = await this.resolvePageLayoutId( // Clean up any partial files from the failed download
serverUrl, await fs.emptyDir(appDirectory);
universalIdentifier,
token,
);
if (!pageLayoutId) { return false;
return;
}
const url = `${frontUrl}/page/${pageLayoutId}`;
this.logDetail(`Opening app welcome page: ${url}`);
this.openInBrowser(url);
} catch {
// Best-effort — don't fail the scaffold if browser open fails
} }
} }
private async resolveWorkspaceFrontUrl( private logCreationInfo({
serverUrl: string,
token: string,
): Promise<string | null> {
const query = `{ currentWorkspace { workspaceUrls { subdomainUrl customUrl } } }`;
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query }),
});
if (!response.ok) {
return null;
}
const body = (await response.json()) as {
data?: {
currentWorkspace?: {
workspaceUrls?: { subdomainUrl?: string; customUrl?: string };
};
};
};
const urls = body.data?.currentWorkspace?.workspaceUrls;
if (!urls) {
return null;
}
const frontUrl = urls.customUrl ?? urls.subdomainUrl;
return frontUrl ? normalizeUrl(frontUrl) : null;
}
private async readMainPageLayoutUniversalIdentifier(
appDirectory: string,
): Promise<string | null> {
const filePath = path.join(
appDirectory, appDirectory,
'src', appName,
'constants', }: {
'universal-identifiers.ts', appDirectory: string;
appName: string;
}): void {
console.log(
chalk.blue('\n', 'Creating Twenty Application\n'),
chalk.gray(`- Directory: ${appDirectory}\n`, `- Name: ${appName}\n`),
); );
const content = await fs.readFile(filePath, 'utf-8');
const match = content.match(
/MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER\s*=\s*'([^']+)'/,
);
return match?.[1] ?? null;
} }
private async resolvePageLayoutId( private async shouldStartServer(autoConfirm?: boolean): Promise<boolean> {
serverUrl: string, const existingServerUrl = await detectLocalServer();
universalIdentifier: string,
token: string,
): Promise<string | null> {
const query = `{ getPageLayouts { id universalIdentifier } }`;
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query }),
});
if (!response.ok) {
return null;
}
const body = (await response.json()) as {
data?: {
getPageLayouts?: { id: string; universalIdentifier: string }[];
};
};
const matching = body.data?.getPageLayouts?.find(
(layout) => layout.universalIdentifier === universalIdentifier,
);
return matching?.id ?? null;
}
private sanitizeBrowserUrl(url: string): string | null {
if (/[^\u0020-\u007E]/.test(url)) {
return null;
}
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null;
}
return parsed.toString();
} catch {
return null;
}
}
private openInBrowser(url: string): void {
const safeUrl = this.sanitizeBrowserUrl(url);
if (!safeUrl) {
return;
}
const isWindows = process.platform === 'win32';
const command = isWindows
? 'rundll32'
: process.platform === 'darwin'
? 'open'
: 'xdg-open';
const args = isWindows
? ['url.dll,FileProtocolHandler', safeUrl]
: [safeUrl];
const child = spawn(command, args, {
stdio: 'ignore',
detached: !isWindows,
});
child.on('error', () => undefined);
if (!isWindows) {
child.unref();
}
}
private async syncApplication(appDirectory: string): Promise<boolean> {
this.logDetail('Running `yarn twenty dev --once`...');
return new Promise((resolve) => {
const child = spawn('yarn', ['twenty', 'dev', '--once'], {
cwd: appDirectory,
stdio: ['inherit', 'pipe', 'pipe'],
});
child.stdout?.resume();
child.stderr?.resume();
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
});
}
private async tryExistingAuth(serverUrl: string): Promise<boolean> {
try {
const configService = new ConfigService();
const remoteNames = await configService.getRemotes();
for (const remoteName of remoteNames) {
const remoteConfig = await configService.getConfigForRemote(remoteName);
if (remoteConfig.apiUrl !== serverUrl) {
continue;
}
const token = remoteConfig.twentyCLIAccessToken ?? remoteConfig.apiKey;
if (!token) {
continue;
}
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: '{ currentWorkspace { id } }',
}),
});
if (!response.ok) {
continue;
}
const body = (await response.json()) as {
data?: { currentWorkspace?: { id: string } };
errors?: unknown[];
};
if (isDefined(body.data?.currentWorkspace) && !body.errors) {
ConfigService.setActiveRemote(remoteName);
await configService.setDefaultRemote(remoteName);
if (existingServerUrl) {
return true; return true;
} }
if (checkDockerRunning() && containerExists()) {
if (autoConfirm) {
return true;
} }
return false; const { startExisting } = await inquirer.prompt([
} catch { {
return false; type: 'confirm',
name: 'startExisting',
message:
'An existing Twenty server container was found. Would you like to start it?',
default: true,
},
]);
return startExisting;
} }
if (autoConfirm) {
return true;
}
const { startDocker } = await inquirer.prompt([
{
type: 'confirm',
name: 'startDocker',
message:
'No running Twenty instance found. Would you like to start one using Docker?',
default: true,
},
]);
return startDocker;
}
private async promptConnectToLocal(serverUrl: string): Promise<void> {
const { shouldAuthenticate } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldAuthenticate',
message: `Would you like to authenticate to the local Twenty instance (${serverUrl})?`,
default: true,
},
]);
if (!shouldAuthenticate) {
console.log(
chalk.gray(
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
),
);
return;
} }
private async authenticateWithDevKey(serverUrl: string): Promise<boolean> {
try { try {
const result = await authLogin({ const result = await authLoginOAuth({
apiKey: DEV_API_KEY,
apiUrl: serverUrl, apiUrl: serverUrl,
remote: 'local', remote: 'local',
}); });
@@ -565,111 +298,41 @@ export class CreateAppCommand {
const configService = new ConfigService(); const configService = new ConfigService();
await configService.setDefaultRemote('local'); await configService.setDefaultRemote('local');
this.logDetail('Authenticated as tim@apple.dev (development API key)'); } else {
return true;
}
console.log( console.log(
chalk.yellow( chalk.yellow(
' Authentication failed. Run `yarn twenty remote:add --local` manually.', 'Authentication failed. Run `yarn twenty remote add --local` manually.',
), ),
); );
}
return false;
} catch { } catch {
console.log( console.log(
chalk.yellow( chalk.yellow(
' Authentication failed. Run `yarn twenty remote:add --local` manually.', 'Authentication failed. Run `yarn twenty remote add` manually.',
), ),
); );
return false;
}
}
private deriveRemoteName(url: string): string {
try {
return new URL(url).hostname.replace(/\./g, '-');
} catch {
return 'remote';
}
}
private async authenticateWithOAuth(serverUrl: string): Promise<boolean> {
try {
const remoteName = this.deriveRemoteName(serverUrl);
ConfigService.setActiveRemote(remoteName);
this.logDetail('Opening browser for OAuth...');
const result = await authLoginOAuth({ apiUrl: serverUrl });
if (result.success) {
const configService = new ConfigService();
await configService.setDefaultRemote(remoteName);
this.logDetail(`Authenticated via OAuth to ${serverUrl}`);
return true;
}
console.log(
chalk.yellow(
` OAuth failed: ${result.error.message}\n` +
` Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
),
);
return false;
} catch {
console.log(
chalk.yellow(
` Authentication failed. Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
),
);
return false;
} }
} }
private logSuccess( private logSuccess(
appDirectory: string, appDirectory: string,
serverUrl: string, serverResult?: ServerStartResult,
authSucceeded: boolean,
): void { ): void {
const dirName = basename(appDirectory); const dirName = basename(appDirectory);
console.log(chalk.green('\nApplication created successfully!\n')); console.log(chalk.blue('\nApplication created. Next steps:'));
console.log(chalk.gray(`- cd ${dirName}`));
console.log(chalk.white(' Next steps:\n'));
let stepNumber = 1;
console.log(chalk.white(` ${stepNumber}. Navigate to your project`));
console.log(chalk.cyan(` cd ${dirName}\n`));
stepNumber++;
if (!authSucceeded) {
console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`));
console.log(
chalk.cyan(' yarn twenty remote:add --url <your-instance-url>\n'),
);
stepNumber++;
}
console.log(chalk.white(` ${stepNumber}. Start developing`));
console.log(chalk.cyan(' yarn twenty dev\n'));
stepNumber++;
console.log(chalk.white(` ${stepNumber}. Open your twenty instance`));
console.log(chalk.cyan(` ${serverUrl}\n`));
if (!serverResult) {
console.log( console.log(
chalk.gray( chalk.gray(
' Documentation: https://docs.twenty.com/developers/extend/capabilities/apps', '- yarn twenty remote add # Authenticate with Twenty',
), ),
); );
} }
console.log(
chalk.gray('- yarn twenty dev # Start dev mode'),
);
}
} }
@@ -17,7 +17,6 @@ const UNIVERSAL_IDENTIFIERS_PATH = join(
'constants', 'constants',
'universal-identifiers.ts', 'universal-identifiers.ts',
); );
const YARNRC_PATH = 'yarnrc.yml';
// Template content matching template/src/constants/universal-identifiers.ts // Template content matching template/src/constants/universal-identifiers.ts
const TEMPLATE_UNIVERSAL_IDENTIFIERS = `export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED'; const TEMPLATE_UNIVERSAL_IDENTIFIERS = `export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
@@ -77,16 +76,11 @@ describe('copyBaseApplicationProject', () => {
appDirectory: testAppDirectory, appDirectory: testAppDirectory,
}); });
// Two fs.copy calls: (1) the template directory, (2) AGENTS.md → CLAUDE.md expect(fs.copy).toHaveBeenCalledTimes(1);
expect(fs.copy).toHaveBeenCalledTimes(2);
expect(fs.copy).toHaveBeenCalledWith( expect(fs.copy).toHaveBeenCalledWith(
expect.stringContaining('template'), expect.stringContaining('template'),
testAppDirectory, testAppDirectory,
); );
expect(fs.copy).toHaveBeenCalledWith(
join(testAppDirectory, 'AGENTS.md'),
join(testAppDirectory, 'CLAUDE.md'),
);
}); });
it('should replace placeholders in universal-identifiers.ts with real values', async () => { it('should replace placeholders in universal-identifiers.ts with real values', async () => {
@@ -174,27 +168,6 @@ describe('copyBaseApplicationProject', () => {
expect(publicDirectoryContents).toHaveLength(0); expect(publicDirectoryContents).toHaveLength(0);
}); });
it('should rename yarnrc.yml to .yarnrc.yml in the scaffolded project', async () => {
await fs.writeFile(
join(testAppDirectory, YARNRC_PATH),
'nodeLinker: node-modules',
);
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
expect(await fs.pathExists(join(testAppDirectory, YARNRC_PATH))).toBe(
false,
);
expect(await fs.pathExists(join(testAppDirectory, '.yarnrc.yml'))).toBe(
true,
);
});
it('should handle empty description', async () => { it('should handle empty description', async () => {
await copyBaseApplicationProject({ await copyBaseApplicationProject({
appName: 'my-test-app', appName: 'my-test-app',
@@ -3,6 +3,7 @@ import { join } from 'path';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import createTwentyAppPackageJson from 'package.json'; import createTwentyAppPackageJson from 'package.json';
import chalk from 'chalk';
const SRC_FOLDER = 'src'; const SRC_FOLDER = 'src';
@@ -11,33 +12,25 @@ export const copyBaseApplicationProject = async ({
appDisplayName, appDisplayName,
appDescription, appDescription,
appDirectory, appDirectory,
onProgress,
}: { }: {
appName: string; appName: string;
appDisplayName: string; appDisplayName: string;
appDescription: string; appDescription: string;
appDirectory: string; appDirectory: string;
onProgress?: (message: string) => void;
}) => { }) => {
onProgress?.('Copying base template'); console.log(chalk.gray('Generating application project...'));
await fs.copy(join(__dirname, './constants/template'), appDirectory); await fs.copy(join(__dirname, './constants/template'), appDirectory);
onProgress?.('Configuring dotfiles (.gitignore, .github, .yarnrc.yml)');
await renameDotfiles({ appDirectory }); await renameDotfiles({ appDirectory });
onProgress?.('Mirroring AGENTS.md to CLAUDE.md');
await mirrorAgentsToClaude({ appDirectory });
await addEmptyPublicDirectory({ appDirectory }); await addEmptyPublicDirectory({ appDirectory });
onProgress?.('Generating unique application identifiers');
await generateUniversalIdentifiers({ await generateUniversalIdentifiers({
appDisplayName, appDisplayName,
appDescription, appDescription,
appDirectory, appDirectory,
}); });
onProgress?.('Updating package.json');
await updatePackageJson({ appName, appDirectory }); await updatePackageJson({ appName, appDirectory });
}; };
@@ -47,7 +40,6 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
const renames = [ const renames = [
{ from: 'gitignore', to: '.gitignore' }, { from: 'gitignore', to: '.gitignore' },
{ from: 'github', to: '.github' }, { from: 'github', to: '.github' },
{ from: 'yarnrc.yml', to: '.yarnrc.yml' },
]; ];
for (const { from, to } of renames) { for (const { from, to } of renames) {
@@ -59,19 +51,6 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
} }
}; };
// AGENTS.md is the cross-tool standard; Claude Code prefers CLAUDE.md and only
// falls back to AGENTS.md, so we mirror the file to keep a single source of truth.
const mirrorAgentsToClaude = async ({
appDirectory,
}: {
appDirectory: string;
}) => {
await fs.copy(
join(appDirectory, 'AGENTS.md'),
join(appDirectory, 'CLAUDE.md'),
);
};
const addEmptyPublicDirectory = async ({ const addEmptyPublicDirectory = async ({
appDirectory, appDirectory,
}: { }: {
@@ -1,33 +0,0 @@
import { execSync } from 'node:child_process';
const DOCKER_DOWNLOAD_URLS: Record<string, string> = {
darwin: 'https://docs.docker.com/desktop/setup/install/mac-install/',
win32: 'https://docs.docker.com/desktop/setup/install/windows-install/',
linux: 'https://docs.docker.com/engine/install/',
};
export const isDockerInstalled = (): boolean => {
try {
execSync('docker --version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
export const getDockerInstallInstructions = (): string => {
const url =
DOCKER_DOWNLOAD_URLS[process.platform] ?? DOCKER_DOWNLOAD_URLS.linux;
return [
' Docker is required but not installed.',
'',
` Install Docker: ${url}`,
'',
' Then run this command again.',
'',
' Alternatively, connect to an existing Twenty instance:',
' npx create-twenty-app@latest my-twenty-app --url <your-instance-url>',
].join('\n');
};
@@ -0,0 +1,179 @@
import { execSync } from 'node:child_process';
import * as fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'node:os';
import chalk from 'chalk';
const TWENTY_REPO_OWNER = 'twentyhq';
const TWENTY_REPO_NAME = 'twenty';
const TWENTY_FALLBACK_REF = 'main';
const TWENTY_EXAMPLES_PATH = 'packages/twenty-apps/examples';
const TWENTY_EXAMPLES_URL = `https://github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tree/${TWENTY_FALLBACK_REF}/${TWENTY_EXAMPLES_PATH}`;
// Fetches the latest release tag from the repo, or falls back to main
const resolveRef = async (): Promise<string> => {
const response = await fetch(
`https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/releases/latest`,
{ headers: { Accept: 'application/vnd.github.v3+json' } },
);
if (response.ok) {
const release = (await response.json()) as { tag_name: string };
return release.tag_name;
}
return TWENTY_FALLBACK_REF;
};
// Uses the GitHub Contents API to list directories — fast and doesn't download the repo
const fetchGitHubDirectoryContents = async (
path: string,
ref: string,
): Promise<{ name: string; type: string }[] | null> => {
const apiUrl = `https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/contents/${path}?ref=${ref}`;
const response = await fetch(apiUrl, {
headers: { Accept: 'application/vnd.github.v3+json' },
});
if (!response.ok) {
return null;
}
const data = await response.json();
if (!Array.isArray(data)) {
return null;
}
return data as { name: string; type: string }[];
};
const listAvailableExamples = async (ref: string): Promise<string[]> => {
const contents = await fetchGitHubDirectoryContents(
TWENTY_EXAMPLES_PATH,
ref,
);
if (!contents) {
return [];
}
return contents
.filter((entry) => entry.type === 'dir')
.map((entry) => entry.name);
};
const validateExampleExists = async (
exampleName: string,
ref: string,
): Promise<void> => {
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
const contents = await fetchGitHubDirectoryContents(examplePath, ref);
if (contents !== null) {
return;
}
const availableExamples = await listAvailableExamples(ref);
throw new Error(
`Example "${exampleName}" not found.\n\n` +
(availableExamples.length > 0
? `Available examples:\n${availableExamples.map((name) => ` - ${name}`).join('\n')}\n\n`
: '') +
`Browse all examples: ${TWENTY_EXAMPLES_URL}`,
);
};
export const downloadExample = async (
exampleName: string,
targetDirectory: string,
): Promise<void> => {
if (
exampleName.includes('/') ||
exampleName.includes('\\') ||
exampleName.includes('..')
) {
throw new Error(
`Invalid example name: "${exampleName}". Example names must be simple directory names (e.g., "hello-world").`,
);
}
const ref = await resolveRef();
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
console.log(chalk.gray(`Resolving examples from ref '${ref}'...`));
await validateExampleExists(exampleName, ref);
console.log(chalk.gray(`Example '${examplePath}' validated successfully.`));
const tarballUrl = `https://codeload.github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tar.gz/${ref}`;
const tempDir = join(
tmpdir(),
`create-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
try {
await fs.ensureDir(tempDir);
console.log(chalk.gray(`Downloading tarball from ${tarballUrl}...`));
const response = await fetch(tarballUrl);
if (!response.ok) {
if (response.status === 404) {
throw new Error(
`Could not find repository: ${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME} (ref: ${ref})`,
);
}
throw new Error(
`Failed to download from GitHub: ${response.status} ${response.statusText}`,
);
}
console.log(chalk.gray('Tarball downloaded. Writing to disk...'));
const tarballPath = join(tempDir, 'archive.tar.gz');
const buffer = Buffer.from(await response.arrayBuffer());
await fs.writeFile(tarballPath, buffer);
console.log(
chalk.gray(
`Tarball saved (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Extracting...`,
),
);
execSync(`tar xzf "${tarballPath}" -C "${tempDir}"`, {
stdio: 'pipe',
});
// GitHub tarballs extract to a directory named {repo}-{ref}/
const extractedEntries = await fs.readdir(tempDir);
const extractedDir = extractedEntries.find(
(entry) => entry !== 'archive.tar.gz',
);
if (!extractedDir) {
throw new Error('Failed to extract archive: no directory found');
}
const sourcePath = join(tempDir, extractedDir, examplePath);
if (!(await fs.pathExists(sourcePath))) {
throw new Error(
`Example directory not found in archive: "${examplePath}"`,
);
}
await fs.copy(sourcePath, targetDirectory);
} finally {
await fs.remove(tempDir);
}
};
@@ -4,18 +4,14 @@ import { exec } from 'child_process';
const execPromise = promisify(exec); const execPromise = promisify(exec);
export const install = async ( export const install = async (root: string) => {
root: string, console.log(chalk.gray('Installing yarn dependencies...'));
onProgress?: (message: string) => void,
) => {
onProgress?.('Enabling corepack');
try { try {
await execPromise('corepack enable', { cwd: root }); await execPromise('corepack enable', { cwd: root });
} catch (error: any) { } catch (error: any) {
console.warn(chalk.yellow('corepack enable failed:'), error.stderr); console.warn(chalk.yellow('corepack enabled failed:'), error.stderr);
} }
onProgress?.('Running yarn install');
try { try {
await execPromise('yarn install', { cwd: root }); await execPromise('yarn install', { cwd: root });
} catch (error: any) { } catch (error: any) {
@@ -93,7 +93,7 @@ yarn install
# Register your local Twenty server as a remote (interactive prompt). # Register your local Twenty server as a remote (interactive prompt).
# When asked for the URL use http://localhost:2021 and paste an API key # When asked for the URL use http://localhost:2021 and paste an API key
# from Settings -> Developers in the Twenty UI. # from Settings -> Developers in the Twenty UI.
yarn twenty remote:add yarn twenty remote add
# Build, install, and watch for changes. # Build, install, and watch for changes.
yarn twenty dev yarn twenty dev
@@ -107,8 +107,8 @@ watching `src/`. Edit any file and the change is re-synced within seconds.
```bash ```bash
cd packages/twenty-apps/community/github-connector cd packages/twenty-apps/community/github-connector
yarn install yarn install
yarn twenty remote:add # same prompts as above yarn twenty remote add # same prompts as above
yarn twenty app:install # builds and installs once yarn twenty install # builds and installs once
``` ```
## Configure authentication ## Configure authentication
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace: First, authenticate to your workspace:
```bash ```bash
yarn twenty remote:add --api-url http://localhost:2020 --as local yarn twenty remote add --api-url http://localhost:2020 --as local
``` ```
Then, start development mode to sync your app and watch for changes: Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash ```bash
# Remotes & Authentication # Remotes & Authentication
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status yarn twenty remote status # Check auth status
yarn twenty remote:use # Set default remote yarn twenty remote switch # Switch default remote
yarn twenty remote:list # List all configured remotes yarn twenty remote list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote yarn twenty remote remove <name> # Remove a remote
# Application # Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client) yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item) yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs yarn twenty logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload yarn twenty exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace yarn twenty uninstall # Uninstall app from workspace
``` ```
## Integration Tests ## Integration Tests
@@ -13,7 +13,7 @@ beforeAll(async () => {
if (!apiUrl || !token) { if (!apiUrl || !token) {
throw new Error( throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' + 'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.', 'Or set them in vitest env config.',
); );
} }
@@ -28,6 +28,12 @@ yarn install
yarn twenty dev yarn twenty dev
``` ```
Or use it as a template via create-twenty-app:
```bash
npx create-twenty-app@latest my-app --example postcard
```
## Learn more ## Learn more
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started) - [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started)
@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) { if (!apiUrl || !apiKey) {
throw new Error( throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' + 'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.', 'Or set them in vitest env config.',
); );
} }
@@ -80,8 +80,8 @@ export async function teardown() {
const uninstallResult = await appUninstall({ appPath: APP_PATH }); const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) { if (!uninstallResult.success) {
throw new Error( console.warn(
`App uninstall failed during teardown: ${JSON.stringify(uninstallResult.error, null, 2)}`, `App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
); );
} }
} }
@@ -1,4 +1,4 @@
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define'; import { PermissionFlag, defineRole } from 'twenty-sdk/define';
import { import {
CONTENT_FIELD_UNIVERSAL_IDENTIFIER, CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
POST_CARD_UNIVERSAL_IDENTIFIER, POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -36,5 +36,5 @@ export default defineRole({
canUpdateFieldValue: true, canUpdateFieldValue: true,
}, },
], ],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS], permissionFlags: [PermissionFlag.APPLICATIONS],
}); });
@@ -10,8 +10,8 @@
"packageManager": "yarn@4.9.2", "packageManager": "yarn@4.9.2",
"scripts": { "scripts": {
"dev": "twenty dev", "dev": "twenty dev",
"exec": "twenty dev:fn-exec", "exec": "twenty exec",
"uninstall": "twenty app:uninstall", "uninstall": "twenty uninstall",
"lint": "oxlint -c .oxlintrc.json .", "lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ." "lint:fix": "oxlint --fix -c .oxlintrc.json ."
}, },
@@ -1,17 +0,0 @@
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
@@ -1,4 +1,4 @@
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define'; import { PermissionFlag, defineRole } from 'twenty-sdk/define';
import { import {
CONTENT_FIELD_UNIVERSAL_IDENTIFIER, CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
POST_CARD_UNIVERSAL_IDENTIFIER, POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -36,5 +36,5 @@ export default defineRole({
canUpdateFieldValue: false, canUpdateFieldValue: false,
}, },
], ],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS], permissionFlags: [PermissionFlag.APPLICATIONS],
}); });
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace: First, authenticate to your workspace:
```bash ```bash
yarn twenty remote:add --api-url http://localhost:2020 --as local yarn twenty remote add --api-url http://localhost:2020 --as local
``` ```
Then, start development mode to sync your app and watch for changes: Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash ```bash
# Remotes & Authentication # Remotes & Authentication
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status yarn twenty remote status # Check auth status
yarn twenty remote:use # Set default remote yarn twenty remote switch # Switch default remote
yarn twenty remote:list # List all configured remotes yarn twenty remote list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote yarn twenty remote remove <name> # Remove a remote
# Application # Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client) yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item) yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs yarn twenty logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload yarn twenty exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace yarn twenty uninstall # Uninstall app from workspace
``` ```
## LLMs instructions ## LLMs instructions
@@ -13,7 +13,7 @@ beforeAll(async () => {
if (!apiUrl || !token) { if (!apiUrl || !token) {
throw new Error( throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' + 'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.', 'Or set them in vitest env config.',
); );
} }
@@ -1,18 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"no-unused-vars": "off",
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
}
}
@@ -1,173 +0,0 @@
# Discord for Twenty
Connect Discord to your Twenty workflows. Post messages, edit them, react,
delete, and browse channels — all from the workflow builder or the AI chat.
## What you can do
Once installed and the admin has provided the bot token, five tools become
available in workflows and the AI chat:
- **Send Discord message** — *"post 'New deal closed 🎉' to #general"*. As a
workflow step: provide `channelId` + `messageText`. Optional
`replyToMessageId` turns it into a reply to a previous message.
- **Update Discord message** — edit a message the bot already sent. Useful
for status messages that change as a workflow progresses.
- **Delete Discord message** — remove a bot message (for example after a
mistake or when a workflow is cancelled).
- **Add Discord reaction** — react with a unicode emoji (`👍`, `✅`, `🎉`)
or a custom server emoji (`partyparrot:643452342342342342`). Great for
signalling status at a glance.
- **List Discord channels** — discovers the text-postable channels in a
Discord server, useful as the first step in a workflow that picks a
channel dynamically. **Leave `guildId` blank if your bot is in only one
server — it auto-picks.** If the bot is in multiple servers, the error
response lists each server's name and ID so you can copy the right one.
## Installing
1. Open **Settings → Applications** in your Twenty workspace.
2. Find **Discord** in the available apps and click **Install**.
3. Use any of the tools above in a workflow step or via the AI chat.
> **Heads up:** if you see *"Discord is not configured"* on the first run,
> your Twenty admin needs to follow the [Self-hosting setup](#self-hosting-setup-admin-only)
> section below — they need to create a Discord application and provide the
> bot token before the tools work.
## Finding channel and message IDs
Discord IDs are long numeric strings (e.g. `1234567890123456789`). To copy
them from the Discord client:
1. **Enable Developer Mode** once: **User Settings → Advanced → Developer
Mode** (toggle on). Discord then shows extra "Copy ID" options on
right-click.
2. **Channel ID** — right-click any channel → *Copy Channel ID*.
3. **Message ID** — right-click any message → *Copy Message ID*.
4. **Server (guild) ID** — right-click the server icon → *Copy Server ID*.
Only needed for `discord-list-channels` when your bot is in multiple
servers (otherwise leave it blank for auto-pick).
> **Tip:** when chaining steps, pass `messageId` from a previous *Send
> Discord message* step directly into a follow-up *Update / Delete / Add
> Reaction* step. No copy/paste needed.
## Limitations
What this connector intentionally does **not** support in v1:
- **Direct messages to users.** Discord's DM API needs a different identity
flow.
- **Threads.** Replying inside a thread or starting a new one isn't exposed.
- **Webhooks.** Uses the bot REST API only.
- **Slash commands / interactions.** The bot doesn't register or respond to
`/commands`.
- **Per-workspace identity.** All Twenty workspaces in the same Twenty
deployment share the same Discord bot — see
[Why bot token instead of OAuth?](#why-bot-token-instead-of-oauth) below.
- **2000-character message limit.** Discord rejects longer payloads with
HTTP 400.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| `Discord is not configured` | `DISCORD_BOT_TOKEN` not set | Admin: paste the bot token in Settings → Applications → Discord → **Settings tab** |
| `401 Unauthorized` | Token wrong, copied with whitespace, or rotated | Reset the token in the Discord Developer Portal and re-paste |
| `Missing Access (50001)` | Bot wasn't invited to that server | Re-run the OAuth invite URL for that server (see admin setup) |
| `Missing Permissions (50013)` | Bot is in the server but lacks Send / Manage / React permissions | In Discord: server settings → roles → grant the bot's role the missing permission |
| `Unknown Channel (10003)` | `channelId` is wrong, or bot can't see that channel | Verify the ID; check the bot has *View Channel* permission |
| `Unknown Message (10008)` | `messageId` is wrong, or message was deleted | Verify the ID is from a still-existing message |
| `Cannot edit a message authored by another user (50005)` | Trying to update / delete a message the bot didn't send | The bot can only edit its own messages; it can delete others' messages only with the *Manage Messages* permission |
| `Bot is in N Discord servers — please specify which one` (from list-channels) | Bot is in multiple servers and `guildId` was left blank | Copy the server ID from the error message and paste it into the `guildId` field |
| `Discord API responded with 429` | Hit Discord's rate limit | Reduce workflow concurrency; this connector doesn't yet auto-retry |
---
## Self-hosting setup (admin-only)
This section is for Twenty server admins. If you're on Twenty Cloud, skip
this — the bot credentials are already configured.
### 1. Create a Discord application
1. Visit https://discord.com/developers/applications and click
**New Application**.
2. Name it (e.g. *"Twenty Bot"*) and create. Use a dedicated app — don't
reuse one configured for other bots.
3. Sidebar → **Bot** tab → **Reset Token** → copy the token immediately
(it's only shown once; if you lose it, reset again).
4. *Recommended:* toggle **Public Bot** OFF so randoms can't invite the
bot to their servers.
### 2. Generate the invite URL and add the bot to your server(s)
1. In the Discord Developer Portal: sidebar → **OAuth2****URL Generator**.
2. **Scopes**: tick `bot`.
3. **Bot Permissions**: tick `Send Messages`, `Manage Messages`,
`Add Reactions`, `Read Message History`.
4. Copy the generated URL.
5. Open the URL in your browser, pick the Discord server, authorize.
Repeat for each server you want workflows to post in.
### 3. Wire the bot token into Twenty
1. In Twenty: **Settings → Applications → Discord → Settings tab**
(gear icon).
2. Paste the bot token into the `DISCORD_BOT_TOKEN` row. Saves on blur.
Workspace users can now use the Discord workflow tools immediately — no
further per-user configuration needed (which is unique vs Linear / Slack
where each user connects their own account).
---
## Why bot token instead of OAuth?
Slack and Linear use OAuth-per-workspace
(`defineConnectionProvider({ type: 'oauth' })`) so each Twenty workspace
stores its own access token. Discord works differently:
- Discord's `bot` scope **does** have an OAuth flow, but the `access_token`
it returns is a *user* bearer token — useless for bot actions like posting
messages. To actually send messages as the bot you need the static
**bot token** from the Developer Portal.
- That bot token is global to the Discord application (and therefore to
the Twenty deployment). Discord deprecated per-install bot tokens years
ago.
- Webhooks are a separate auth model but only support posting — no edit,
delete, or reactions — so they don't cover Slack/Linear parity.
The result: this connector skips `defineConnectionProvider` entirely and
reads `DISCORD_BOT_TOKEN` from an `applicationVariable` set once at
deployment scope. See
[Discord's OAuth2 docs](https://discord.com/developers/docs/topics/oauth2#bot-users)
for the underlying reason bot users authenticate via static tokens.
---
## Developers only
If you're working on this app rather than installing the published version:
```bash
cd packages/twenty-apps/internal/twenty-discord
# Day-to-day development (publish + install + watch in one):
yarn twenty dev
# Run unit tests:
yarn test
# Lint:
yarn lint
```
`twenty dev` is recommended for iteration — it publishes to your local
Twenty server, installs the app, and watches for changes in one command.
The Discord REST API (v10) is called directly via `fetch` — no `discord.js`
or other SDK dependency. See
`src/logic-functions/utils/discord-api-request.ts` for the auth and
error-handling wrapper that all handlers go through.
@@ -1,31 +0,0 @@
{
"name": "twenty-discord",
"version": "0.1.0",
"description": "Discord workflow connector for Twenty",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [
"twenty-app"
],
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"twenty-sdk": "2.4.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
"oxlint": "^0.16.0",
"typescript": "^5.9.3",
"vitest": "^3.1.1"
}
}
@@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 127 127"
fill="none"
role="img"
>
<title>Discord</title>
<path
fill="#5865F2"
d="M107.7 26.4A104.6 104.6 0 0 0 81.6 18a0.4 0.4 0 0 0-0.4 0.2 73 73 0 0 0-3.2 6.6 96.6 96.6 0 0 0-29 0A66.8 66.8 0 0 0 45.6 18.2a0.4 0.4 0 0 0-0.4-0.2 104.2 104.2 0 0 0-26.1 8.4 0.4 0.4 0 0 0-0.2 0.2C2.3 51.4-2.2 75.7 0 99.7a0.4 0.4 0 0 0 0.2 0.3 105.2 105.2 0 0 0 31.6 16 0.4 0.4 0 0 0 0.5-0.1 75.6 75.6 0 0 0 6.5-10.6 0.4 0.4 0 0 0-0.2-0.5 69.4 69.4 0 0 1-9.9-4.7 0.4 0.4 0 0 1 0-0.7 54.6 54.6 0 0 0 2-1.6 0.4 0.4 0 0 1 0.4-0.1 75 75 0 0 0 63.9 0 0.4 0.4 0 0 1 0.4 0.1c0.6 0.6 1.3 1.1 2 1.6a0.4 0.4 0 0 1 0 0.7 65.2 65.2 0 0 1-9.9 4.7 0.4 0.4 0 0 0-0.2 0.5 84.8 84.8 0 0 0 6.5 10.6 0.4 0.4 0 0 0 0.5 0.1 104.8 104.8 0 0 0 31.6-16 0.4 0.4 0 0 0 0.2-0.3c2.7-27.8-4.5-51.9-18.9-73.1a0.4 0.4 0 0 0-0.2-0.2zM42.5 85a12.3 12.3 0 0 1-11.5-12.9c0-7.1 5-12.9 11.5-12.9 6.6 0 11.7 5.8 11.6 12.9 0 7-5.1 12.9-11.6 12.9zm42.8 0a12.3 12.3 0 0 1-11.5-12.9c0-7.1 5-12.9 11.5-12.9 6.6 0 11.6 5.8 11.5 12.9 0 7-5 12.9-11.5 12.9z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,30 +0,0 @@
import { defineApplication } from 'twenty-sdk/define';
import {
APPLICATION_UNIVERSAL_IDENTIFIER,
DISCORD_BOT_TOKEN_VARIABLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Twenty Discord',
description:
'Connect Discord to Twenty. Workflow steps post, update, and delete bot messages and add reactions using a Discord bot token shared across the deployment.',
logoUrl: 'public/twenty-discord.svg',
author: 'Twenty',
category: 'Communication',
aboutDescription:
'Official Discord connector for Twenty CRM. Create a Discord application at https://discord.com/developers/applications, copy its bot token into the DISCORD_BOT_TOKEN application variable, then invite the bot to each server you want workflows to post in. Use workflow actions to post, update, or delete bot messages and add reactions.',
websiteUrl: 'https://docs.twenty.com/developers/extend/apps/getting-started',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
applicationVariables: {
DISCORD_BOT_TOKEN: {
universalIdentifier: DISCORD_BOT_TOKEN_VARIABLE_UNIVERSAL_IDENTIFIER,
description:
'Bot token from your Discord application (Developer Portal → Bot tab → Reset Token). Used with the `Bot` auth prefix to call the Discord REST API. The same token authenticates the bot across every guild it has been invited to.',
isSecret: true,
},
},
});
@@ -1,17 +0,0 @@
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
SEND_DISCORD_MESSAGE_COMMAND_UNIVERSAL_IDENTIFIER,
SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineCommandMenuItem({
universalIdentifier: SEND_DISCORD_MESSAGE_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Send Discord message',
shortLabel: 'Discord message',
icon: 'IconBrandDiscord',
isPinned: false,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier:
SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});

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