Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
314f72abf0 | ||
|
|
d8b50851c9 | ||
|
|
b6ce89794e | ||
|
|
e8ca9d0c85 | ||
|
|
ec6a52c370 | ||
|
|
38fdc76388 |
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -21,7 +21,7 @@ runs:
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '${{ inputs.app-path }}/.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
@@ -21,7 +21,7 @@ runs:
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '${{ inputs.app-path }}/.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
@@ -20,7 +20,7 @@ runs:
|
||||
run: git fetch origin main --depth=1
|
||||
- name: Get last successful commit
|
||||
if: env.NX_BASE == ''
|
||||
uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4
|
||||
uses: nrwl/nx-set-shas@v4
|
||||
- name: Fallback to origin/main if no base found
|
||||
if: env.NX_BASE == ''
|
||||
shell: bash
|
||||
|
||||
@@ -25,7 +25,7 @@ runs:
|
||||
run: |
|
||||
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-cache
|
||||
with:
|
||||
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
|
||||
|
||||
@@ -9,11 +9,8 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
|
||||
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
|
||||
- name: Save cache
|
||||
if: ${{ format('{0}', github.event.pull_request.head.repo.fork) != 'true' }}
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: ${{ inputs.key }}
|
||||
path: |
|
||||
|
||||
@@ -48,7 +48,7 @@ runs:
|
||||
fi
|
||||
|
||||
- name: Checkout docker compose files
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.twenty-repository }}
|
||||
ref: ${{ steps.resolve.outputs.git-ref }}
|
||||
|
||||
@@ -29,12 +29,12 @@ runs:
|
||||
echo "packages/*/node_modules" >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
- name: Setup Node.js and get yarn cache
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
- name: Restore node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
|
||||
restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
|
||||
@@ -44,13 +44,10 @@ runs:
|
||||
shell: ${{ steps.globals.outputs.ACTION_SHELL }}
|
||||
run: |
|
||||
yarn config set enableHardenedMode true
|
||||
yarn config set enableScripts false
|
||||
yarn --immutable --check-cache
|
||||
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
|
||||
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
|
||||
- name: Save cache
|
||||
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' && format('{0}', github.event.pull_request.head.repo.fork) != 'true' }}
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
|
||||
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
|
||||
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
|
||||
|
||||
@@ -14,8 +14,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
run: |
|
||||
gh api repos/twentyhq/twenty-infra/dispatches \
|
||||
-f event_type=auto-deploy-main
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: auto-deploy-main
|
||||
client-payload: '{"github": ${{ toJson(github) }}}' # Passes the entire github context to the downstream workflow
|
||||
|
||||
@@ -14,10 +14,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh api repos/twentyhq/twenty-infra/dispatches \
|
||||
-f event_type=auto-deploy-tag \
|
||||
-f "client_payload[github][ref_name]=$REF_NAME"
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: auto-deploy-tag
|
||||
client-payload: '{"github": ${{ toJson(github) }}}' # Passes the entire github context to the downstream workflow
|
||||
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
any_changed: ${{ steps.changed-files.outputs.any_changed }}
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.6
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync AI model catalog from models.dev'
|
||||
@@ -61,7 +61,8 @@ jobs:
|
||||
|
||||
- name: Trigger automerge
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
run: |
|
||||
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=automated-pr-ready
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: automated-pr-ready
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
@@ -634,7 +634,7 @@ jobs:
|
||||
|
||||
- name: Upload breaking changes report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: breaking-changes-report
|
||||
path: |
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
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" --api-url http://localhost:3000
|
||||
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: |
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --api-url http://localhost:3000
|
||||
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --skip-local-instance --yes
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
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" --api-url http://localhost:3000
|
||||
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: |
|
||||
|
||||
@@ -30,8 +30,12 @@ jobs:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -28,8 +28,13 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
@@ -23,11 +23,9 @@ jobs:
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-server/**
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
!packages/twenty-server/package.json
|
||||
|
||||
example-app-postcard:
|
||||
needs: changed-files-check
|
||||
@@ -56,7 +54,7 @@ jobs:
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
@@ -85,31 +83,6 @@ jobs:
|
||||
working-directory: packages/twenty-apps/examples/postcard
|
||||
run: npx vitest run
|
||||
|
||||
- name: Configure remote for SDK CLI
|
||||
run: |
|
||||
mkdir -p ~/.twenty
|
||||
cat > ~/.twenty/config.json <<EOF
|
||||
{
|
||||
"version": 1,
|
||||
"remotes": {
|
||||
"target": {
|
||||
"apiUrl": "${TWENTY_API_URL}",
|
||||
"apiKey": "${TWENTY_API_KEY}",
|
||||
"accessToken": "${TWENTY_API_KEY}"
|
||||
}
|
||||
},
|
||||
"defaultRemote": "target"
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Deploy postcard app (registry install path)
|
||||
working-directory: packages/twenty-apps/examples/postcard
|
||||
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs deploy --remote target
|
||||
|
||||
- name: Install postcard app (registry install path)
|
||||
working-directory: packages/twenty-apps/examples/postcard
|
||||
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs install --remote target
|
||||
|
||||
ci-example-app-postcard-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
|
||||
@@ -32,8 +32,12 @@ jobs:
|
||||
matrix:
|
||||
task: [build, typecheck, lint]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -46,8 +50,12 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -55,7 +63,7 @@ jobs:
|
||||
- name: Build storybook
|
||||
run: npx nx storybook:build twenty-front-component-renderer
|
||||
- name: Upload storybook build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-front-component-renderer
|
||||
path: packages/twenty-front-component-renderer/storybook-static
|
||||
@@ -68,7 +76,7 @@ jobs:
|
||||
STORYBOOK_URL: http://localhost:6008
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -76,7 +84,7 @@ jobs:
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-front-component-renderer
|
||||
path: packages/twenty-front-component-renderer/storybook-static
|
||||
|
||||
@@ -38,8 +38,12 @@ jobs:
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -51,7 +55,7 @@ jobs:
|
||||
- name: Front / Build storybook
|
||||
run: npx nx storybook:build twenty-front
|
||||
- name: Upload storybook build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: packages/twenty-front/storybook-static
|
||||
@@ -75,7 +79,7 @@ jobs:
|
||||
STORYBOOK_URL: http://localhost:6006
|
||||
steps:
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -92,7 +96,7 @@ jobs:
|
||||
npx nx build twenty-ui
|
||||
npx nx build twenty-front-component-renderer
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: packages/twenty-front/storybook-static
|
||||
@@ -117,7 +121,7 @@ jobs:
|
||||
# exit 1
|
||||
# fi
|
||||
# - name: Upload coverage artifact
|
||||
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# retention-days: 1
|
||||
# name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
|
||||
@@ -132,12 +136,12 @@ jobs:
|
||||
# matrix:
|
||||
# storybook_scope: [modules, pages, performance]
|
||||
# steps:
|
||||
# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 10
|
||||
# - name: Install dependencies
|
||||
# uses: ./.github/actions/yarn-install
|
||||
# - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
# - uses: actions/download-artifact@v4
|
||||
# with:
|
||||
# pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
|
||||
# merge-multiple: true
|
||||
@@ -160,8 +164,12 @@ jobs:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -195,8 +203,12 @@ jobs:
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
ANALYZE: "true"
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -206,7 +218,7 @@ jobs:
|
||||
- name: Build frontend
|
||||
run: npx nx build twenty-front
|
||||
# - name: Upload frontend build artifact
|
||||
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: frontend-build
|
||||
# path: packages/twenty-front/build
|
||||
|
||||
@@ -41,11 +41,11 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Restore Nx build cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Save Nx build cache
|
||||
if: always()
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
path: |
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
|
||||
- name: Upload Playwright results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-results
|
||||
path: |
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
printf '%s\n' "$VERSION" > version.txt
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
branch: release/${{ steps.sanitize.outputs.version }}
|
||||
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
git tag "v${{ env.VERSION }}"
|
||||
git push origin "v${{ env.VERSION }}"
|
||||
|
||||
- uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
if: contains(github.event.pull_request.labels.*.name, 'create_release')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -30,8 +30,12 @@ jobs:
|
||||
matrix:
|
||||
task: [lint, typecheck, test:unit, test:integration]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -70,7 +74,7 @@ jobs:
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -83,12 +83,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Get changed upgrade-version-command files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-server/src/database/commands/upgrade-version-command/**
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -278,7 +278,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -341,7 +341,7 @@ jobs:
|
||||
SHARD_COUNTER: 10
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -29,8 +29,12 @@ jobs:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -26,9 +26,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
@@ -101,9 +101,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
@@ -30,8 +30,12 @@ jobs:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -44,8 +48,12 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -53,7 +61,7 @@ jobs:
|
||||
- name: Build storybook
|
||||
run: npx nx storybook:build twenty-ui
|
||||
- name: Upload storybook build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui
|
||||
path: packages/twenty-ui/storybook-static
|
||||
@@ -66,7 +74,7 @@ jobs:
|
||||
STORYBOOK_URL: http://localhost:6007
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -74,7 +82,7 @@ jobs:
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui
|
||||
path: packages/twenty-ui/storybook-static
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'closed'
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Utils / Run Danger.js
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'closed' && github.event.pull_request.merged == true
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run congratulate-dangerfile.js
|
||||
|
||||
@@ -32,8 +32,12 @@ jobs:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
@@ -97,8 +97,12 @@ jobs:
|
||||
matrix:
|
||||
task: [lint, typecheck, validate]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -8,12 +8,10 @@ on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
issues:
|
||||
types: [opened]
|
||||
types: [opened, assigned]
|
||||
repository_dispatch:
|
||||
types: [claude-core-team-issues]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.client_payload.issue_number }}
|
||||
cancel-in-progress: false
|
||||
@@ -21,30 +19,10 @@ concurrency:
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
contains(github.event.comment.body, '@claude') &&
|
||||
github.event.comment.user.type != 'Bot' &&
|
||||
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
|
||||
) ||
|
||||
(
|
||||
github.event_name == 'pull_request_review_comment' &&
|
||||
contains(github.event.comment.body, '@claude') &&
|
||||
github.event.comment.user.type != 'Bot' &&
|
||||
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
|
||||
) ||
|
||||
(
|
||||
github.event_name == 'pull_request_review' &&
|
||||
contains(github.event.review.body, '@claude') &&
|
||||
github.event.review.user.type != 'Bot' &&
|
||||
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)
|
||||
) ||
|
||||
(
|
||||
github.event_name == 'issues' &&
|
||||
github.event.action == 'opened' &&
|
||||
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
|
||||
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
|
||||
)
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && github.event.review.user.type != 'Bot') ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
@@ -72,14 +50,14 @@ jobs:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run Claude Code
|
||||
id: claude-code
|
||||
uses: anthropics/claude-code-action@dde2242db6af13460b916652159b6ba19a598f30 # v1
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
additional_permissions: |
|
||||
@@ -144,14 +122,14 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build prompt from dispatch payload
|
||||
id: prompt
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const p = context.payload.client_payload;
|
||||
@@ -166,7 +144,7 @@ jobs:
|
||||
core.setOutput('issue_number', p.issue_number);
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@dde2242db6af13460b916652159b6ba19a598f30 # v1
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: ${{ steps.prompt.outputs.prompt }}
|
||||
@@ -181,16 +159,9 @@ jobs:
|
||||
}
|
||||
- name: Dispatch response to ci-privileged
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
REPO: ${{ steps.prompt.outputs.repo }}
|
||||
ISSUE_NUMBER: ${{ steps.prompt.outputs.issue_number }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
-f event_type=claude-cross-repo-response \
|
||||
-f "client_payload[repo]=$REPO" \
|
||||
-f "client_payload[issue_number]=$ISSUE_NUMBER" \
|
||||
-f "client_payload[run_id]=$RUN_ID" \
|
||||
-f "client_payload[run_url]=$RUN_URL"
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
repository: twentyhq/ci-privileged
|
||||
event-type: claude-cross-repo-response
|
||||
client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
@@ -153,7 +153,8 @@ jobs:
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
run: |
|
||||
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.ref }}
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
run: yarn docs:generate-navigation-template
|
||||
|
||||
- name: Upload docs to Crowdin
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Pull translations from Crowdin
|
||||
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
@@ -139,7 +139,8 @@ jobs:
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
run: |
|
||||
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: main
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
|
||||
- name: Upload missing translations
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true'
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
@@ -105,7 +105,8 @@ jobs:
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
run: |
|
||||
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: Get PR number from workflow run
|
||||
id: pr-info
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const runId = context.payload.workflow_run.id;
|
||||
@@ -63,16 +63,9 @@ jobs:
|
||||
|
||||
- name: Dispatch to ci-privileged
|
||||
if: steps.pr-info.outputs.has_pr == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
|
||||
RUN_ID: ${{ steps.pr-info.outputs.run_id }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
BRANCH_STATE: ${{ github.event.workflow_run.head_branch }}
|
||||
run: |
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
-f event_type=breaking-changes-report \
|
||||
-f "client_payload[pr_number]=$PR_NUMBER" \
|
||||
-f "client_payload[run_id]=$RUN_ID" \
|
||||
-f "client_payload[repo]=$REPOSITORY" \
|
||||
-f "client_payload[branch_state]=$BRANCH_STATE"
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
repository: twentyhq/ci-privileged
|
||||
event-type: breaking-changes-report
|
||||
client-payload: '{"pr_number": ${{ toJSON(steps.pr-info.outputs.pr_number) }}, "run_id": ${{ toJSON(steps.pr-info.outputs.run_id) }}, "repo": ${{ toJSON(github.repository) }}, "branch_state": ${{ toJSON(github.event.workflow_run.head_branch) }}}'
|
||||
|
||||
@@ -36,27 +36,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger preview environment workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
gh api repos/"$REPOSITORY"/dispatches \
|
||||
-f event_type=preview-environment \
|
||||
-f "client_payload[pr_number]=$PR_NUMBER" \
|
||||
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
|
||||
-f "client_payload[repo_full_name]=$REPOSITORY"
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
event-type: preview-environment
|
||||
client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}'
|
||||
|
||||
- name: Dispatch to ci-privileged for PR comment
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
KEEPALIVE_DISPATCH_TIME: ${{ github.event.pull_request.updated_at }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
-f event_type=preview-env-url \
|
||||
-f "client_payload[pr_number]=$PR_NUMBER" \
|
||||
-f "client_payload[keepalive_dispatch_time]=$KEEPALIVE_DISPATCH_TIME" \
|
||||
-f "client_payload[repo]=$REPOSITORY"
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
repository: twentyhq/ci-privileged
|
||||
event-type: preview-env-url
|
||||
client-payload: '{"pr_number": ${{ toJSON(github.event.pull_request.number) }}, "keepalive_dispatch_time": ${{ toJSON(github.event.pull_request.updated_at) }}, "repo": ${{ toJSON(github.repository) }}}'
|
||||
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.client_payload.pr_head_sha }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
echo "$TUNNEL_URL" > tunnel-url.txt
|
||||
|
||||
- name: Upload tunnel URL artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tunnel-url
|
||||
path: tunnel-url.txt
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
- name: Determine project and artifact name
|
||||
id: project
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const workflowName = context.payload.workflow_run.name;
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
- name: Check if storybook artifact exists
|
||||
id: check-artifact
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const artifactName = '${{ steps.project.outputs.artifact_name }}';
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- name: Get PR number
|
||||
if: steps.check-artifact.outputs.exists == 'true'
|
||||
id: pr-info
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const headBranch = context.payload.workflow_run.head_branch;
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
- name: Download storybook artifact from triggering run
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.project.outputs.artifact_name }}
|
||||
path: storybook-static
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
|
||||
- name: Upload storybook tarball
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.project.outputs.tarball_name }}
|
||||
path: /tmp/${{ steps.project.outputs.tarball_file }}
|
||||
@@ -128,20 +128,17 @@ jobs:
|
||||
|
||||
- name: Dispatch to ci-privileged
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PROJECT: ${{ steps.project.outputs.project }}
|
||||
BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
-f event_type=visual-regression \
|
||||
-f "client_payload[pr_number]=$PR_NUMBER" \
|
||||
-f "client_payload[run_id]=$RUN_ID" \
|
||||
-f "client_payload[repo]=$REPOSITORY" \
|
||||
-f "client_payload[project]=$PROJECT" \
|
||||
-f "client_payload[branch]=$BRANCH" \
|
||||
-f "client_payload[commit]=$COMMIT"
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
repository: twentyhq/ci-privileged
|
||||
event-type: visual-regression
|
||||
client-payload: >-
|
||||
{
|
||||
"pr_number": "${{ steps.pr-info.outputs.pr_number }}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"project": "${{ steps.project.outputs.project }}",
|
||||
"branch": "${{ github.event.workflow_run.head_branch }}",
|
||||
"commit": "${{ github.event.workflow_run.head_sha }}"
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
|
||||
- name: Pull website translations from Crowdin
|
||||
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
@@ -128,7 +128,8 @@ jobs:
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
run: |
|
||||
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: main
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Upload missing website translations
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true'
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
@@ -104,7 +104,8 @@ jobs:
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
run: |
|
||||
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
Vendored
-40
@@ -43,46 +43,6 @@
|
||||
{
|
||||
"name": "packages/twenty-e2e-testing",
|
||||
"path": "../packages/twenty-e2e-testing"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-docs",
|
||||
"path": "../packages/twenty-docs"
|
||||
},
|
||||
{
|
||||
"name": "packages/create-twenty-app",
|
||||
"path": "../packages/create-twenty-app"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-apps",
|
||||
"path": "../packages/twenty-apps"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-claude-skills",
|
||||
"path": "../packages/twenty-claude-skills"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-cli",
|
||||
"path": "../packages/twenty-cli"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-client-sdk",
|
||||
"path": "../packages/twenty-client-sdk"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-companion",
|
||||
"path": "../packages/twenty-companion"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-front-component-renderer",
|
||||
"path": "../packages/twenty-front-component-renderer"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-sdk",
|
||||
"path": "../packages/twenty-sdk"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-website-new",
|
||||
"path": "../packages/twenty-website-new"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@@ -4,10 +4,6 @@ enableHardenedMode: true
|
||||
|
||||
enableInlineHunks: true
|
||||
|
||||
enableScripts: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
|
||||
@@ -25,19 +25,18 @@ yarn twenty dev
|
||||
The scaffolder will:
|
||||
|
||||
1. Create a new project with TypeScript, linting, tests, and a preconfigured `twenty` CLI
|
||||
2. Start a local Twenty server via Docker (pulls the latest image automatically)
|
||||
3. Authenticate with the development API key
|
||||
2. Optionally start a local Twenty server (Docker)
|
||||
3. Open the browser for OAuth authentication
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description |
|
||||
| ---------------------------------- | --------------------------------------------------------------------- |
|
||||
| `--example <name>` | Initialize from an example |
|
||||
| `--name <name>` | Set the app name |
|
||||
| `--display-name <displayName>` | Set the display name |
|
||||
| `--description <description>` | Set the description |
|
||||
| `--api-url <url>` | Twenty instance URL (default: `http://localhost:2020`) |
|
||||
| `--authentication-method <method>` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) |
|
||||
| Flag | Description |
|
||||
| ------------------------------ | --------------------------------------- |
|
||||
| `--example <name>` | Initialize from an example |
|
||||
| `--name <name>` | Set the app name (skips the prompt) |
|
||||
| `--display-name <displayName>` | Set the display name (skips the prompt) |
|
||||
| `--description <description>` | Set the description (skips the prompt) |
|
||||
| `--skip-local-instance` | Skip the local server setup prompt |
|
||||
|
||||
By default (no flags), a minimal app is generated with core files and an integration test. Use `--example` to start from a richer example:
|
||||
|
||||
@@ -58,7 +57,7 @@ Full documentation is available at **[docs.twenty.com/developers/extend/apps](ht
|
||||
## Troubleshooting
|
||||
|
||||
- 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.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "2.5.0",
|
||||
"version": "2.3.1",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
@@ -32,6 +32,7 @@
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^10.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
@@ -42,6 +43,7 @@
|
||||
"@swc/core": "^1.15.11",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@types/fs-extra": "^11.0.0",
|
||||
"@types/inquirer": "^9.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommanderError } from 'commander';
|
||||
import {
|
||||
type AuthenticationMethod,
|
||||
CreateAppCommand,
|
||||
} from '@/create-app.command';
|
||||
import { CreateAppCommand } from '@/create-app.command';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const program = new Command(packageJson.name)
|
||||
@@ -16,17 +13,20 @@ const program = new Command(packageJson.name)
|
||||
)
|
||||
.argument('[directory]')
|
||||
.option('--example <name>', 'Initialize from an example')
|
||||
.option('-n, --name <name>', 'Application name')
|
||||
.option('-d, --display-name <displayName>', 'Application display name')
|
||||
.option('--description <description>', 'Application description')
|
||||
.option('-n, --name <name>', 'Application name (skips prompt)')
|
||||
.option(
|
||||
'--api-url <apiUrl>',
|
||||
'Twenty instance URL (default: http://localhost:2020)',
|
||||
'-d, --display-name <displayName>',
|
||||
'Application display name (skips prompt)',
|
||||
)
|
||||
.option(
|
||||
'--authentication-method <method>',
|
||||
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
|
||||
'--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.')
|
||||
.action(
|
||||
async (
|
||||
@@ -36,8 +36,8 @@ const program = new Command(packageJson.name)
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
authenticationMethod?: AuthenticationMethod;
|
||||
skipLocalInstance?: boolean;
|
||||
yes?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
@@ -54,26 +54,14 @@ const program = new Command(packageJson.name)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
options?.authenticationMethod &&
|
||||
!['oauth', 'apiKey'].includes(options.authenticationMethod)
|
||||
) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: --authentication-method must be "oauth" or "apiKey".',
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await new CreateAppCommand().execute({
|
||||
directory,
|
||||
example: options?.example,
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
apiUrl: options?.apiUrl,
|
||||
authenticationMethod: options?.authenticationMethod,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
yes: options?.yes,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,19 +2,8 @@ This is a [Twenty](https://twenty.com) application bootstrapped with [`create-tw
|
||||
|
||||
## Getting Started
|
||||
|
||||
This app was scaffolded with a local Twenty server running at [http://localhost:2020](http://localhost:2020).
|
||||
|
||||
Login with the default development credentials: `[email protected]` / `[email protected]`.
|
||||
|
||||
Run `yarn twenty help` to list all available commands.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
- `yarn twenty dev` - Start the development server and sync your app
|
||||
- `yarn twenty server status` - Check the local Twenty server status
|
||||
- `yarn twenty server start` - Start the local Twenty server
|
||||
- `yarn test` - Run integration tests
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_DEPLOY_URL: http://localhost:2020
|
||||
TWENTY_DEPLOY_URL: http://localhost:3000
|
||||
|
||||
concurrency:
|
||||
group: cd-${{ github.ref }}
|
||||
|
||||
@@ -5,29 +5,21 @@ import { install } from '@/utils/install';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import * as path from 'path';
|
||||
import { basename } from 'path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import {
|
||||
authLogin,
|
||||
authLoginOAuth,
|
||||
checkDockerRunning,
|
||||
ConfigService,
|
||||
DEV_API_KEY,
|
||||
DEV_API_URL,
|
||||
containerExists,
|
||||
detectLocalServer,
|
||||
serverStart,
|
||||
} from 'twenty-sdk/cli';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
getDockerInstallInstructions,
|
||||
isDockerInstalled,
|
||||
} from '@/utils/docker-install';
|
||||
|
||||
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
|
||||
const IMAGE = 'twentycrm/twenty-app-dev:latest';
|
||||
|
||||
export type AuthenticationMethod = 'oauth' | 'apiKey';
|
||||
|
||||
type CreateAppOptions = {
|
||||
directory?: string;
|
||||
@@ -35,62 +27,40 @@ type CreateAppOptions = {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
authenticationMethod?: AuthenticationMethod;
|
||||
skipLocalInstance?: boolean;
|
||||
yes?: boolean;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
private stepCounter = 0;
|
||||
private totalSteps = 0;
|
||||
private static TOTAL_STEPS = 4;
|
||||
|
||||
async execute(options: CreateAppOptions = {}): Promise<void> {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
this.getAppInfos(options);
|
||||
|
||||
const apiUrl = options.apiUrl ?? DEV_API_URL;
|
||||
|
||||
const skipLocalInstance = apiUrl !== DEV_API_URL;
|
||||
|
||||
if (!skipLocalInstance && !isDockerInstalled()) {
|
||||
console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (skipLocalInstance && options.authenticationMethod === 'apiKey') {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'API key authentication is only supported on a local Docker instance. Ignoring and switching to OAuth authentication.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const authenticationMethod = skipLocalInstance
|
||||
? 'oauth'
|
||||
: (options.authenticationMethod ?? 'apiKey');
|
||||
await this.getAppInfos(options);
|
||||
|
||||
try {
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
this.totalSteps = this.computeTotalSteps({
|
||||
skipLocalInstance,
|
||||
const confirmed = await this.promptScaffoldConfirmation({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
autoConfirm: options.yes,
|
||||
});
|
||||
|
||||
this.stepCounter = 0;
|
||||
if (!confirmed) {
|
||||
console.log(chalk.gray('\nScaffolding cancelled.'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const dockerPullPromise =
|
||||
!skipLocalInstance && checkDockerRunning()
|
||||
? this.pullImageInBackground()
|
||||
: Promise.resolve(false);
|
||||
|
||||
this.logPlan({ appName, appDisplayName, appDescription, appDirectory });
|
||||
|
||||
this.logNextStep('Creating project directory');
|
||||
console.log('');
|
||||
|
||||
this.logStep(1, 'Creating project directory');
|
||||
await fs.ensureDir(appDirectory);
|
||||
|
||||
this.logDetail(appDirectory);
|
||||
|
||||
this.logNextStep('Scaffolding project files');
|
||||
this.logStep(2, 'Scaffolding project files');
|
||||
|
||||
if (options.example) {
|
||||
const exampleSucceeded = await this.tryDownloadExample(
|
||||
@@ -117,12 +87,10 @@ export class CreateAppCommand {
|
||||
});
|
||||
}
|
||||
|
||||
this.logNextStep('Installing dependencies');
|
||||
|
||||
this.logStep(3, 'Installing dependencies');
|
||||
await install(appDirectory, (message) => this.logDetail(message));
|
||||
|
||||
this.logNextStep('Initializing Git repository');
|
||||
|
||||
this.logStep(4, 'Initializing Git repository');
|
||||
const gitInitialized = await tryGitInit(appDirectory);
|
||||
|
||||
if (gitInitialized) {
|
||||
@@ -136,29 +104,38 @@ export class CreateAppCommand {
|
||||
|
||||
console.log('');
|
||||
|
||||
let hasLocalServer = false;
|
||||
let authSucceeded = false;
|
||||
let resolvedApiUrl = apiUrl;
|
||||
let serverReady = skipLocalInstance;
|
||||
|
||||
if (!skipLocalInstance) {
|
||||
this.logNextStep('Starting Twenty server');
|
||||
const serverResult = await this.ensureDockerServer(dockerPullPromise);
|
||||
if (!options.skipLocalInstance) {
|
||||
const existingServerUrl = await detectLocalServer();
|
||||
|
||||
if (isDefined(serverResult.url)) {
|
||||
resolvedApiUrl = serverResult.url;
|
||||
serverReady = true;
|
||||
if (existingServerUrl) {
|
||||
hasLocalServer = true;
|
||||
authSucceeded = await this.promptConnectToLocal(existingServerUrl);
|
||||
} else {
|
||||
const shouldStart = await this.shouldStartServer(options.yes);
|
||||
|
||||
if (shouldStart) {
|
||||
const startResult = await serverStart({
|
||||
onProgress: (message: string) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
if (startResult.success) {
|
||||
hasLocalServer = true;
|
||||
authSucceeded = await this.promptConnectToLocal(
|
||||
startResult.data.url,
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.yellow(`\n${startResult.error.message}`));
|
||||
}
|
||||
} else {
|
||||
this.logServerSkipped();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serverReady && authenticationMethod === 'oauth') {
|
||||
this.logNextStep('Authenticating via OAuth');
|
||||
authSucceeded = await this.authenticateWithOAuth(resolvedApiUrl);
|
||||
} else if (serverReady && authenticationMethod === 'apiKey') {
|
||||
this.logNextStep('Authenticating via API key');
|
||||
authSucceeded = await this.authenticateWithDevKey(resolvedApiUrl);
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, resolvedApiUrl, authSucceeded);
|
||||
this.logSuccess(appDirectory, hasLocalServer, authSucceeded);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('\nCreate application failed:'),
|
||||
@@ -168,42 +145,66 @@ export class CreateAppCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private computeTotalSteps({
|
||||
skipLocalInstance,
|
||||
}: {
|
||||
skipLocalInstance: boolean;
|
||||
}): number {
|
||||
let steps = 4; // directory, scaffold, install, git
|
||||
|
||||
if (!skipLocalInstance) {
|
||||
steps += 1; // start server
|
||||
}
|
||||
|
||||
steps += 1; // authenticate (oauth or apiKey)
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
private getAppInfos(options: CreateAppOptions): {
|
||||
private async getAppInfos(options: CreateAppOptions): Promise<{
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
} {
|
||||
}> {
|
||||
const { directory } = options;
|
||||
|
||||
const hasName = isDefined(options.name) || isDefined(directory);
|
||||
const hasDisplayName = isDefined(options.displayName);
|
||||
const hasDescription = isDefined(options.description);
|
||||
const hasExample = isDefined(options.example);
|
||||
|
||||
const { name, displayName, description } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Application name:',
|
||||
when: () => !hasName && !hasExample,
|
||||
default: 'my-twenty-app',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) return 'Application name is required';
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'displayName',
|
||||
message: 'Application display name:',
|
||||
when: () => !hasDisplayName && !hasExample,
|
||||
default: (answers: { name?: string }) => {
|
||||
return convertToLabel(
|
||||
answers?.name ?? options.name ?? directory ?? '',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: 'Application description (optional):',
|
||||
when: () => !hasDescription && !hasExample,
|
||||
default: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const appName = (
|
||||
options.name ??
|
||||
options.directory ??
|
||||
name ??
|
||||
directory ??
|
||||
options.example ??
|
||||
'my-twenty-app'
|
||||
).trim();
|
||||
|
||||
const appDisplayName =
|
||||
options.displayName?.trim() || convertToLabel(appName);
|
||||
(options.displayName ?? displayName)?.trim() || convertToLabel(appName);
|
||||
|
||||
const appDescription = (options.description ?? '').trim();
|
||||
const appDescription = (options.description ?? description ?? '').trim();
|
||||
|
||||
const appDirectory = options.directory
|
||||
? path.join(CURRENT_EXECUTION_DIRECTORY, options.directory)
|
||||
const appDirectory = directory
|
||||
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
|
||||
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
|
||||
|
||||
return { appName, appDisplayName, appDirectory, appDescription };
|
||||
@@ -231,30 +232,45 @@ export class CreateAppCommand {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
console.error(
|
||||
chalk.red(
|
||||
`\n${error instanceof Error ? error.message : 'Failed to download example.'}`,
|
||||
),
|
||||
);
|
||||
this.logDetail('Falling back to default template...');
|
||||
|
||||
const { useTemplate } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'useTemplate',
|
||||
message: 'Would you like to create a default template app instead?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!useTemplate) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Clean up any partial files from the failed download
|
||||
await fs.emptyDir(appDirectory);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private logPlan({
|
||||
private async promptScaffoldConfirmation({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
autoConfirm,
|
||||
}: {
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
}): void {
|
||||
autoConfirm?: boolean;
|
||||
}): Promise<boolean> {
|
||||
console.log(chalk.blue('\nCreating Twenty Application\n'));
|
||||
console.log(chalk.white(` Name: ${appName}`));
|
||||
console.log(chalk.white(` Display name: ${appDisplayName}`));
|
||||
@@ -264,13 +280,43 @@ export class CreateAppCommand {
|
||||
}
|
||||
|
||||
console.log(chalk.white(` Directory: ${appDirectory}`));
|
||||
|
||||
console.log(chalk.white('\nThe following steps will be performed:\n'));
|
||||
console.log(chalk.gray(' 1. Create project directory'));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' 2. Scaffold project files from base template\n' +
|
||||
' - Copy template files\n' +
|
||||
' - Configure dotfiles (.gitignore, .github)\n' +
|
||||
' - Generate unique application identifiers\n' +
|
||||
' - Update package.json with app name and SDK versions',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(' 3. Install dependencies (yarn)'));
|
||||
console.log(
|
||||
chalk.gray(' 4. Initialize Git repository with initial commit'),
|
||||
);
|
||||
console.log('');
|
||||
|
||||
if (autoConfirm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { proceed } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Proceed?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
return proceed;
|
||||
}
|
||||
|
||||
private logNextStep(title: string): void {
|
||||
this.stepCounter++;
|
||||
private logStep(step: number, title: string): void {
|
||||
console.log(
|
||||
chalk.blue(`\n[${this.stepCounter}/${this.totalSteps}]`) +
|
||||
chalk.blue(`\n[${step}/${CreateAppCommand.TOTAL_STEPS}]`) +
|
||||
chalk.white(` ${title}...`),
|
||||
);
|
||||
}
|
||||
@@ -279,57 +325,100 @@ export class CreateAppCommand {
|
||||
console.log(chalk.gray(` → ${message}`));
|
||||
}
|
||||
|
||||
private pullImageInBackground(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('docker', ['pull', IMAGE], { stdio: 'ignore' });
|
||||
private async shouldStartServer(autoConfirm?: boolean): Promise<boolean> {
|
||||
console.log(
|
||||
chalk.white(
|
||||
'\n A local Twenty instance is required for app development.\n' +
|
||||
' It provides the API and schema your application connects to.\n',
|
||||
),
|
||||
);
|
||||
|
||||
child.on('close', (code) => resolve(code === 0));
|
||||
child.on('error', () => resolve(false));
|
||||
});
|
||||
if (checkDockerRunning() && containerExists()) {
|
||||
if (autoConfirm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { startExisting } = await inquirer.prompt([
|
||||
{
|
||||
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 ensureDockerServer(
|
||||
dockerPullPromise: Promise<boolean>,
|
||||
): Promise<{ url?: string }> {
|
||||
if (!checkDockerRunning()) {
|
||||
private logServerSkipped(): void {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
'\n To start a Twenty instance later:\n' +
|
||||
' yarn twenty server start\n\n' +
|
||||
' To connect to a remote instance instead:\n' +
|
||||
' yarn twenty remote add\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async promptConnectToLocal(serverUrl: string): Promise<boolean> {
|
||||
console.log(
|
||||
chalk.white(
|
||||
'\n Authentication links your app to a Twenty instance so you can\n' +
|
||||
' sync custom objects, fields, and roles during development.\n' +
|
||||
' This will open a browser window to complete the OAuth flow.\n',
|
||||
),
|
||||
);
|
||||
|
||||
const { shouldAuthenticate } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'shouldAuthenticate',
|
||||
message: `Authenticate to the local Twenty instance (${serverUrl})?`,
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!shouldAuthenticate) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\n Docker is installed but not running.\n' +
|
||||
' Please start Docker and run this command again.\n',
|
||||
chalk.gray(
|
||||
'\n Authentication skipped. To authenticate later:\n' +
|
||||
` yarn twenty remote add --local\n`,
|
||||
),
|
||||
);
|
||||
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logDetail('Ensuring latest Twenty server image...');
|
||||
await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'confirm',
|
||||
message: 'Press Enter to open the browser for authentication...',
|
||||
},
|
||||
]);
|
||||
|
||||
const pullSucceeded = await dockerPullPromise;
|
||||
|
||||
if (!pullSucceeded) {
|
||||
this.logDetail(
|
||||
'Image pull failed, continuing with cached image if available...',
|
||||
);
|
||||
}
|
||||
|
||||
const startResult = await serverStart({
|
||||
onProgress: (message: string) => this.logDetail(message),
|
||||
});
|
||||
|
||||
if (startResult.success) {
|
||||
return { url: startResult.data.url };
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`\n ${startResult.error.message}`));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private async authenticateWithDevKey(apiUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await authLogin({
|
||||
apiKey: DEV_API_KEY,
|
||||
apiUrl,
|
||||
const result = await authLoginOAuth({
|
||||
apiUrl: serverUrl,
|
||||
remote: 'local',
|
||||
});
|
||||
|
||||
@@ -337,68 +426,21 @@ export class CreateAppCommand {
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.setDefaultRemote('local');
|
||||
this.logDetail('Authenticated as [email protected] (development API key)');
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Authentication failed. Run `yarn twenty remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
' Authentication failed. Run `yarn twenty remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
' Authentication failed. Run `yarn twenty remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private deriveRemoteName(url: string): string {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/\./g, '-');
|
||||
} catch {
|
||||
return 'remote';
|
||||
}
|
||||
}
|
||||
|
||||
private async authenticateWithOAuth(apiUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const remoteName = this.deriveRemoteName(apiUrl);
|
||||
|
||||
ConfigService.setActiveRemote(remoteName);
|
||||
|
||||
this.logDetail('Opening browser for OAuth...');
|
||||
|
||||
const result = await authLoginOAuth({ apiUrl });
|
||||
|
||||
if (result.success) {
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.setDefaultRemote(remoteName);
|
||||
this.logDetail(`Authenticated via OAuth to ${apiUrl}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` OAuth failed: ${result.error.message}\n` +
|
||||
` Run \`yarn twenty remote add --api-url ${apiUrl}\` manually.`,
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` Authentication failed. Run \`yarn twenty remote add --api-url ${apiUrl}\` manually.`,
|
||||
'Authentication failed. Run `yarn twenty remote add` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -408,13 +450,12 @@ export class CreateAppCommand {
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
apiUrl: string,
|
||||
hasLocalServer: boolean,
|
||||
authSucceeded: boolean,
|
||||
): void {
|
||||
const dirName = basename(appDirectory);
|
||||
|
||||
console.log(chalk.green('\n✔ Application created successfully!\n'));
|
||||
|
||||
console.log(chalk.white(' Next steps:\n'));
|
||||
|
||||
let stepNumber = 1;
|
||||
@@ -424,21 +465,17 @@ export class CreateAppCommand {
|
||||
stepNumber++;
|
||||
|
||||
if (!authSucceeded) {
|
||||
const remoteCommand = hasLocalServer
|
||||
? 'yarn twenty remote add --local'
|
||||
: 'yarn twenty remote add';
|
||||
|
||||
console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
' yarn twenty remote add --api-url <your-instance-url>\n',
|
||||
),
|
||||
);
|
||||
console.log(chalk.cyan(` ${remoteCommand}\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(` ${apiUrl}\n`));
|
||||
|
||||
console.log(
|
||||
chalk.gray(
|
||||
|
||||
@@ -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 --api-url <your-instance-url>',
|
||||
].join('\n');
|
||||
};
|
||||
@@ -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": "[email protected]",
|
||||
"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: '[email protected]',
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
-17
@@ -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,
|
||||
});
|
||||
-670
@@ -1,670 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
closeSidePanel,
|
||||
enqueueSnackbar,
|
||||
unmountFrontComponent,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
import { SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
type DiscordGuild = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type DiscordChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
parentId: string | null;
|
||||
position: number;
|
||||
};
|
||||
|
||||
const DISCORD_MESSAGE_MAX_LENGTH = 2000;
|
||||
|
||||
// The front component sandbox dispatches events with a non-standard shape.
|
||||
// Values may live on e.detail.value, e.value, or e.target.value.
|
||||
const readSerializedValue = (
|
||||
e: React.SyntheticEvent<HTMLElement>,
|
||||
): string | undefined => {
|
||||
const obj = e as {
|
||||
detail?: { value?: string };
|
||||
value?: string;
|
||||
target?: { value?: string };
|
||||
};
|
||||
|
||||
if (typeof obj.detail?.value === 'string') return obj.detail.value;
|
||||
if (typeof obj.value === 'string') return obj.value;
|
||||
if (typeof obj.target?.value === 'string') return obj.target.value;
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const onValueChange =
|
||||
(fn: (value: string) => void) => (e: React.SyntheticEvent<HTMLElement>) => {
|
||||
const v = readSerializedValue(e);
|
||||
|
||||
if (typeof v === 'string') fn(v);
|
||||
};
|
||||
|
||||
const callAppRoute = async (
|
||||
path: string,
|
||||
method: 'GET' | 'POST',
|
||||
body?: Record<string, unknown>,
|
||||
) => {
|
||||
const apiBaseUrl = process.env.TWENTY_API_URL;
|
||||
const token =
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
|
||||
|
||||
if (!apiBaseUrl || !token) {
|
||||
throw new Error('API configuration missing');
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiBaseUrl}/s${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
|
||||
throw new Error(
|
||||
`Request failed (${response.status}): ${text.slice(0, 200)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const COLOR = {
|
||||
bg: '#f1f1f1',
|
||||
card: '#ffffff',
|
||||
surface: '#fcfcfc',
|
||||
border: '#ebebeb',
|
||||
borderStrong: '#d6d6d6',
|
||||
text: '#333333',
|
||||
textSecondary: '#666666',
|
||||
textTertiary: '#999999',
|
||||
placeholder: '#cccccc',
|
||||
accent: '#5865f2',
|
||||
error: '#e05252',
|
||||
};
|
||||
|
||||
const INLINE_SELECT: React.CSSProperties = {
|
||||
appearance: 'none',
|
||||
WebkitAppearance: 'none' as const,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'inherit',
|
||||
font: 'inherit',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
const STYLES = {
|
||||
outer: {
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
fontSize: '13px',
|
||||
backgroundColor: COLOR.bg,
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
},
|
||||
container: {
|
||||
backgroundColor: COLOR.card,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
color: COLOR.text,
|
||||
},
|
||||
topBar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 12px',
|
||||
borderBottom: `1px solid ${COLOR.border}`,
|
||||
flexShrink: 0,
|
||||
},
|
||||
guildSelect: {
|
||||
appearance: 'none' as const,
|
||||
WebkitAppearance: 'none' as const,
|
||||
background: COLOR.surface,
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
borderRadius: '6px',
|
||||
padding: '3px 8px',
|
||||
color: COLOR.text,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
},
|
||||
channelPicker: {
|
||||
flexShrink: 0,
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
borderBottom: `1px solid ${COLOR.border}`,
|
||||
},
|
||||
channelChip: {
|
||||
appearance: 'none' as const,
|
||||
WebkitAppearance: 'none' as const,
|
||||
background: 'transparent',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
borderRadius: '20px',
|
||||
padding: '3px 9px',
|
||||
color: COLOR.textSecondary,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
display: 'inline-flex' as const,
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
whiteSpace: 'nowrap' as const,
|
||||
lineHeight: '16px',
|
||||
},
|
||||
chipIcon: {
|
||||
fontSize: '12px',
|
||||
lineHeight: 1,
|
||||
flexShrink: 0,
|
||||
color: COLOR.textTertiary,
|
||||
},
|
||||
optionsLoadingIndicator: {
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
spinner: {
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
border: `2px solid ${COLOR.border}`,
|
||||
borderTopColor: COLOR.textSecondary,
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 0.6s linear infinite',
|
||||
},
|
||||
body: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
padding: '24px',
|
||||
overflow: 'auto',
|
||||
minHeight: 0,
|
||||
gap: '6px',
|
||||
},
|
||||
messageInput: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: COLOR.text,
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit',
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
resize: 'none' as const,
|
||||
flex: 1,
|
||||
minHeight: '120px',
|
||||
},
|
||||
charCount: {
|
||||
fontSize: '11px',
|
||||
textAlign: 'right' as const,
|
||||
color: COLOR.textTertiary,
|
||||
flexShrink: 0,
|
||||
},
|
||||
charCountOverLimit: {
|
||||
color: COLOR.error,
|
||||
},
|
||||
actionBar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '6px',
|
||||
padding: '6px 12px',
|
||||
borderTop: `1px solid ${COLOR.border}`,
|
||||
flexShrink: 0,
|
||||
},
|
||||
cancelButton: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: COLOR.textSecondary,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 10px',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
sendButton: {
|
||||
background: COLOR.accent,
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 14px',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
sendButtonDisabled: {
|
||||
background: COLOR.borderStrong,
|
||||
color: COLOR.textTertiary,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
successOuter: {
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
backgroundColor: COLOR.bg,
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
},
|
||||
successContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '10px 14px',
|
||||
boxSizing: 'border-box' as const,
|
||||
backgroundColor: COLOR.card,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
color: COLOR.text,
|
||||
},
|
||||
successCheck: {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#2ea043',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '11px',
|
||||
color: '#fff',
|
||||
flexShrink: 0,
|
||||
},
|
||||
successText: {
|
||||
color: COLOR.text,
|
||||
fontWeight: 500,
|
||||
fontSize: '13px',
|
||||
flex: 1,
|
||||
overflow: 'hidden' as const,
|
||||
textOverflow: 'ellipsis' as const,
|
||||
whiteSpace: 'nowrap' as const,
|
||||
},
|
||||
closeButton: {
|
||||
background: 'transparent',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
color: COLOR.textSecondary,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 14px',
|
||||
borderRadius: '6px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
loadingOuter: {
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
backgroundColor: COLOR.bg,
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
},
|
||||
loadingContainer: {
|
||||
color: COLOR.textSecondary,
|
||||
fontSize: '13px',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: COLOR.card,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
errorOuter: {
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
backgroundColor: COLOR.bg,
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
},
|
||||
errorContainer: {
|
||||
backgroundColor: COLOR.card,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
},
|
||||
errorText: {
|
||||
color: COLOR.error,
|
||||
fontSize: '12px',
|
||||
textAlign: 'center' as const,
|
||||
},
|
||||
retryButton: {
|
||||
background: COLOR.surface,
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
color: COLOR.text,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 12px',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
};
|
||||
|
||||
const SendMessageForm = () => {
|
||||
const [guilds, setGuilds] = useState<DiscordGuild[]>([]);
|
||||
const [guildsLoading, setGuildsLoading] = useState(true);
|
||||
const [guildsError, setGuildsError] = useState<string | null>(null);
|
||||
|
||||
const [channels, setChannels] = useState<DiscordChannel[]>([]);
|
||||
const [channelsLoading, setChannelsLoading] = useState(false);
|
||||
|
||||
const [selectedGuildId, setSelectedGuildId] = useState('');
|
||||
const [selectedChannelId, setSelectedChannelId] = useState('');
|
||||
const [messageText, setMessageText] = useState('');
|
||||
|
||||
const [sending, setSending] = useState(false);
|
||||
const [sentChannelName, setSentChannelName] = useState<string | null>(null);
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
const fetchChannels = useCallback(async (guildId: string) => {
|
||||
if (!guildId) {
|
||||
setChannels([]);
|
||||
setSelectedChannelId('');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setChannelsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await callAppRoute(
|
||||
`/discord/channels?guildId=${encodeURIComponent(guildId)}`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
setChannels([]);
|
||||
await enqueueSnackbar({
|
||||
message: result.error ?? 'Failed to load channels',
|
||||
variant: 'error',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setChannels(result.channels ?? []);
|
||||
} catch (error) {
|
||||
setChannels([]);
|
||||
await enqueueSnackbar({
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Failed to load channels',
|
||||
variant: 'error',
|
||||
});
|
||||
} finally {
|
||||
setChannelsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleGuildChange = useCallback(
|
||||
(newGuildId: string) => {
|
||||
setSelectedGuildId(newGuildId);
|
||||
setSelectedChannelId('');
|
||||
fetchChannels(newGuildId);
|
||||
},
|
||||
[fetchChannels],
|
||||
);
|
||||
|
||||
const fetchGuilds = useCallback(async () => {
|
||||
try {
|
||||
setGuildsError(null);
|
||||
const result = await callAppRoute('/discord/guilds', 'GET');
|
||||
|
||||
if (!result.success) {
|
||||
setGuildsError(result.error ?? 'Failed to load Discord servers');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchedGuilds: DiscordGuild[] = result.guilds ?? [];
|
||||
|
||||
setGuilds(fetchedGuilds);
|
||||
|
||||
// Auto-select when the bot is in exactly one server to skip a needless step.
|
||||
if (fetchedGuilds.length === 1) {
|
||||
setSelectedGuildId(fetchedGuilds[0].id);
|
||||
fetchChannels(fetchedGuilds[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
setGuildsError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to load Discord servers',
|
||||
);
|
||||
} finally {
|
||||
setGuildsLoading(false);
|
||||
}
|
||||
}, [fetchChannels]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGuilds();
|
||||
}, [fetchGuilds]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const trimmedMessage = (messageText ?? '').trim();
|
||||
|
||||
if (
|
||||
!selectedGuildId ||
|
||||
!selectedChannelId ||
|
||||
trimmedMessage.length === 0 ||
|
||||
trimmedMessage.length > DISCORD_MESSAGE_MAX_LENGTH
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSending(true);
|
||||
|
||||
try {
|
||||
const result = await callAppRoute('/discord/messages', 'POST', {
|
||||
channelId: selectedChannelId,
|
||||
messageText: trimmedMessage,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
await enqueueSnackbar({
|
||||
message: result.error ?? 'Failed to send message',
|
||||
variant: 'error',
|
||||
});
|
||||
setSending(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const channelName =
|
||||
channels.find((channel) => channel.id === selectedChannelId)?.name ??
|
||||
'channel';
|
||||
|
||||
setSentChannelName(channelName);
|
||||
await enqueueSnackbar({
|
||||
message: `Message sent to #${channelName}`,
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
await enqueueSnackbar({
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Failed to send message',
|
||||
variant: 'error',
|
||||
});
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setClosing(true);
|
||||
unmountFrontComponent();
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
if (closing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sentChannelName) {
|
||||
return (
|
||||
<div style={STYLES.successOuter}>
|
||||
<div style={STYLES.successContainer}>
|
||||
<div style={STYLES.successCheck}>{'✓'}</div>
|
||||
<span style={STYLES.successText}>
|
||||
Message sent to #{sentChannelName}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
style={STYLES.closeButton}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (guildsLoading) {
|
||||
return (
|
||||
<div style={STYLES.loadingOuter}>
|
||||
<div style={STYLES.loadingContainer}>Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (guildsError) {
|
||||
return (
|
||||
<div style={STYLES.errorOuter}>
|
||||
<div style={STYLES.errorContainer}>
|
||||
<p style={STYLES.errorText}>{guildsError}</p>
|
||||
<button
|
||||
type="button"
|
||||
style={STYLES.retryButton}
|
||||
onClick={fetchGuilds}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const trimmedLength = (messageText ?? '').trim().length;
|
||||
const isOverLimit = (messageText ?? '').length > DISCORD_MESSAGE_MAX_LENGTH;
|
||||
const canSubmit =
|
||||
!!selectedGuildId &&
|
||||
!!selectedChannelId &&
|
||||
trimmedLength > 0 &&
|
||||
!isOverLimit &&
|
||||
!sending;
|
||||
|
||||
return (
|
||||
<div style={STYLES.outer}>
|
||||
<style>{'@keyframes spin{to{transform:rotate(360deg)}}'}</style>
|
||||
<div style={STYLES.container}>
|
||||
<div style={STYLES.topBar}>
|
||||
<select
|
||||
value={selectedGuildId}
|
||||
onChange={onValueChange(handleGuildChange)}
|
||||
style={STYLES.guildSelect}
|
||||
>
|
||||
<option value="">Select server...</option>
|
||||
{guilds.map((guild) => (
|
||||
<option key={guild.id} value={guild.id}>
|
||||
{guild.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{channelsLoading && (
|
||||
<div style={STYLES.optionsLoadingIndicator}>
|
||||
<div style={STYLES.spinner} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={STYLES.channelPicker}>
|
||||
<label style={STYLES.channelChip}>
|
||||
<span style={STYLES.chipIcon}>{'#'}</span>
|
||||
<select
|
||||
value={selectedChannelId}
|
||||
onChange={onValueChange(setSelectedChannelId)}
|
||||
style={INLINE_SELECT}
|
||||
disabled={!selectedGuildId || channelsLoading}
|
||||
>
|
||||
<option value="">
|
||||
{channels.length === 0 ? 'No channels' : 'Select channel'}
|
||||
</option>
|
||||
{channels.map((channel) => (
|
||||
<option key={channel.id} value={channel.id}>
|
||||
{channel.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style={STYLES.body}>
|
||||
<textarea
|
||||
value={messageText}
|
||||
onInput={onValueChange(setMessageText)}
|
||||
onChange={onValueChange(setMessageText)}
|
||||
style={STYLES.messageInput}
|
||||
placeholder="Type a message..."
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
...STYLES.charCount,
|
||||
...(isOverLimit ? STYLES.charCountOverLimit : {}),
|
||||
}}
|
||||
>
|
||||
{(messageText ?? '').length} / {DISCORD_MESSAGE_MAX_LENGTH}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={STYLES.actionBar}>
|
||||
<button
|
||||
type="button"
|
||||
style={STYLES.cancelButton}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
...STYLES.sendButton,
|
||||
...(!canSubmit ? STYLES.sendButtonDisabled : {}),
|
||||
}}
|
||||
disabled={!canSubmit}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{sending ? 'Sending...' : 'Send'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'send-discord-message-form',
|
||||
description:
|
||||
'Form to send a message to a Discord channel: pick the server, pick the channel, type the message, send.',
|
||||
component: SendMessageForm,
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'9b1b7628-ac98-4e44-8e1e-91b401378a5f';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'6aa50eb2-e8c2-470f-8c4d-09debbf6e051';
|
||||
|
||||
export const DISCORD_BOT_TOKEN_VARIABLE_UNIVERSAL_IDENTIFIER =
|
||||
'ac986a3b-19a5-4cee-a87a-2f8d3885ad84';
|
||||
|
||||
export const DISCORD_POST_MESSAGE_UNIVERSAL_IDENTIFIER =
|
||||
'6c03d456-c1fa-4acc-9928-d97ced0925fe';
|
||||
|
||||
export const DISCORD_UPDATE_MESSAGE_UNIVERSAL_IDENTIFIER =
|
||||
'b37285d9-cb11-4fe5-963d-0434780122ba';
|
||||
|
||||
export const DISCORD_DELETE_MESSAGE_UNIVERSAL_IDENTIFIER =
|
||||
'a2efc27c-eae0-4713-b327-bdb1aaeff95f';
|
||||
|
||||
export const DISCORD_ADD_REACTION_UNIVERSAL_IDENTIFIER =
|
||||
'32c77c8a-ebd0-4308-aa1b-d21bb94422ac';
|
||||
|
||||
export const DISCORD_LIST_CHANNELS_UNIVERSAL_IDENTIFIER =
|
||||
'ef2296f6-1b48-4edd-81ac-cafa4fe73f99';
|
||||
|
||||
export const DISCORD_LIST_CHANNELS_ROUTE_UNIVERSAL_IDENTIFIER =
|
||||
'550cd993-af6e-46ae-b697-f838f34cfe39';
|
||||
|
||||
export const DISCORD_LIST_GUILDS_UNIVERSAL_IDENTIFIER =
|
||||
'0aaaca4f-3156-4b7c-a9f5-ee6a2e17cbf1';
|
||||
|
||||
export const DISCORD_LIST_GUILDS_ROUTE_UNIVERSAL_IDENTIFIER =
|
||||
'3cfa36d9-3c04-495c-bc09-f8019ad85dce';
|
||||
|
||||
export const DISCORD_POST_MESSAGE_ROUTE_UNIVERSAL_IDENTIFIER =
|
||||
'eda60c95-450b-4a86-b5a0-6b85f488ae81';
|
||||
|
||||
export const SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
|
||||
'ee8d8efa-2f9d-4d2c-b759-bb3cafc7797d';
|
||||
|
||||
export const SEND_DISCORD_MESSAGE_COMMAND_UNIVERSAL_IDENTIFIER =
|
||||
'8c80b9d8-2f18-4b6c-8933-30d170790685';
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_ADD_REACTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordAddReactionHandler } from 'src/logic-functions/handlers/discord-add-reaction-handler';
|
||||
import { discordAddReactionInputSchema } from './schemas/discord-add-reaction-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_ADD_REACTION_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-add-reaction',
|
||||
description:
|
||||
'Add an emoji reaction to a Discord message (for example a checkmark ✅) so the channel can see status at a glance.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordAddReactionInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Add Discord Reaction',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordAddReactionInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
error: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordAddReactionHandler,
|
||||
});
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_DELETE_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordDeleteMessageHandler } from 'src/logic-functions/handlers/discord-delete-message-handler';
|
||||
import { discordDeleteMessageInputSchema } from './schemas/discord-delete-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_DELETE_MESSAGE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-delete-message',
|
||||
description:
|
||||
'Remove a Discord message this bot sent (for example after a mistake or when a workflow is cancelled).',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordDeleteMessageInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Delete Discord Message',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordDeleteMessageInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
error: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordDeleteMessageHandler,
|
||||
});
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { DISCORD_LIST_CHANNELS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordListChannelsHandler } from 'src/logic-functions/handlers/discord-list-channels-handler';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const params = event.queryStringParameters ?? {};
|
||||
const guildId =
|
||||
typeof params.guildId === 'string' && params.guildId.length > 0
|
||||
? params.guildId
|
||||
: undefined;
|
||||
|
||||
return discordListChannelsHandler({ guildId });
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_LIST_CHANNELS_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-list-channels-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/discord/channels',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_LIST_CHANNELS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordListChannelsHandler } from 'src/logic-functions/handlers/discord-list-channels-handler';
|
||||
import { discordListChannelsInputSchema } from './schemas/discord-list-channels-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_LIST_CHANNELS_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-list-channels',
|
||||
description:
|
||||
'List the text-postable channels (types `GUILD_TEXT` and `GUILD_ANNOUNCEMENT`) in a Discord server, sorted by Discord position. Each entry exposes `id`, `name`, `type`, `parentId` (category), and `position`. Pass the server (guild) ID to scope the listing; if omitted and the bot is in exactly one server, that server is auto-selected. If the bot is in multiple servers, the error response lists each server name and ID for explicit selection. Voice channels, threads, forums, and categories are excluded.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordListChannelsInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'List Discord Channels',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordListChannelsInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
channels: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
type: { type: 'number' },
|
||||
parentId: { type: 'string' },
|
||||
position: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number' },
|
||||
error: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordListChannelsHandler,
|
||||
});
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
import { DISCORD_LIST_GUILDS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordListGuildsHandler } from 'src/logic-functions/handlers/discord-list-guilds-handler';
|
||||
|
||||
const handler = async (_event: RoutePayload) => {
|
||||
return discordListGuildsHandler();
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_LIST_GUILDS_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-list-guilds-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/discord/guilds',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_LIST_GUILDS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordListGuildsHandler } from 'src/logic-functions/handlers/discord-list-guilds-handler';
|
||||
import { discordListGuildsInputSchema } from './schemas/discord-list-guilds-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_LIST_GUILDS_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-list-guilds',
|
||||
description:
|
||||
'List the Discord servers (guilds) the bot has been invited to. Each entry exposes `id` and `name`. Use this to discover which servers the bot can post in, then pass the chosen server ID to `discord-list-channels` or other server-scoped actions.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordListGuildsInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'List Discord Servers',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordListGuildsInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
guilds: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number' },
|
||||
error: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordListGuildsHandler,
|
||||
});
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { DISCORD_POST_MESSAGE_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordPostMessageHandler } from 'src/logic-functions/handlers/discord-post-message-handler';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const body = event.body as Record<string, unknown> | null;
|
||||
const rawChannelId = body?.channelId;
|
||||
const rawMessageText = body?.messageText;
|
||||
|
||||
if (typeof rawChannelId !== 'string' || typeof rawMessageText !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to post Discord message',
|
||||
error: '`channelId` and `messageText` must be strings.',
|
||||
};
|
||||
}
|
||||
|
||||
const channelId = rawChannelId.trim();
|
||||
const messageText = rawMessageText.trim();
|
||||
|
||||
if (!isNonEmptyString(channelId) || !isNonEmptyString(messageText)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to post Discord message',
|
||||
error: '`channelId` and `messageText` are required.',
|
||||
};
|
||||
}
|
||||
|
||||
return discordPostMessageHandler({ channelId, messageText });
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_POST_MESSAGE_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-post-message-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/discord/messages',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_POST_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordPostMessageHandler } from 'src/logic-functions/handlers/discord-post-message-handler';
|
||||
import { discordPostMessageInputSchema } from './schemas/discord-post-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_POST_MESSAGE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-post-message',
|
||||
description:
|
||||
'Send a message to a Discord channel as the bot. Optionally reply to an existing message using its message ID from a previous step.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordPostMessageInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Send Discord Message',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordPostMessageInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
error: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordPostMessageHandler,
|
||||
});
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_UPDATE_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordUpdateMessageHandler } from 'src/logic-functions/handlers/discord-update-message-handler';
|
||||
import { discordUpdateMessageInputSchema } from './schemas/discord-update-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_UPDATE_MESSAGE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-update-message',
|
||||
description:
|
||||
'Change the text of a Discord message this bot already sent. You need the channel ID and the message ID from when it was posted.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordUpdateMessageInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Update Discord Message',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordUpdateMessageInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
error: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordUpdateMessageHandler,
|
||||
});
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { type DiscordAddReactionInput } from 'src/logic-functions/types/discord-add-reaction-input.type';
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
import { buildDiscordFailureResult } from 'src/logic-functions/utils/build-discord-failure-result';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
export const discordAddReactionHandler = async (
|
||||
parameters: DiscordAddReactionInput,
|
||||
): Promise<DiscordToolResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Discord is not configured',
|
||||
error: tokenResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
const channelId = parameters.channelId.trim();
|
||||
const messageId = parameters.messageId.trim();
|
||||
const emoji = parameters.emoji.trim();
|
||||
|
||||
const encodedChannelId = encodeURIComponent(channelId);
|
||||
const encodedMessageId = encodeURIComponent(messageId);
|
||||
const encodedEmoji = encodeURIComponent(emoji);
|
||||
|
||||
try {
|
||||
const result = await discordApiRequest({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'PUT',
|
||||
path: `/channels/${encodedChannelId}/messages/${encodedMessageId}/reactions/${encodedEmoji}/@me`,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to add Discord reaction',
|
||||
error: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Reaction "${emoji}" added to the message.`,
|
||||
messageId,
|
||||
channelId,
|
||||
};
|
||||
} catch (error) {
|
||||
return buildDiscordFailureResult('Failed to add Discord reaction', error);
|
||||
}
|
||||
};
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { type DiscordDeleteMessageInput } from 'src/logic-functions/types/discord-delete-message-input.type';
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
import { buildDiscordFailureResult } from 'src/logic-functions/utils/build-discord-failure-result';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
export const discordDeleteMessageHandler = async (
|
||||
parameters: DiscordDeleteMessageInput,
|
||||
): Promise<DiscordToolResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Discord is not configured',
|
||||
error: tokenResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await discordApiRequest({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'DELETE',
|
||||
path: `/channels/${encodeURIComponent(
|
||||
parameters.channelId,
|
||||
)}/messages/${encodeURIComponent(parameters.messageId)}`,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to delete Discord message',
|
||||
error: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Discord message deleted.',
|
||||
messageId: parameters.messageId,
|
||||
channelId: parameters.channelId,
|
||||
};
|
||||
} catch (error) {
|
||||
return buildDiscordFailureResult('Failed to delete Discord message', error);
|
||||
}
|
||||
};
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type DiscordListChannelsInput } from 'src/logic-functions/types/discord-list-channels-input.type';
|
||||
import {
|
||||
type DiscordListChannelsResult,
|
||||
type DiscordListChannelsResultChannel,
|
||||
} from 'src/logic-functions/types/discord-list-channels-output.type';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { fetchDiscordGuilds } from 'src/logic-functions/utils/fetch-discord-guilds';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
const GUILD_TEXT_CHANNEL_TYPE = 0;
|
||||
const GUILD_ANNOUNCEMENT_CHANNEL_TYPE = 5;
|
||||
|
||||
const POSTABLE_CHANNEL_TYPES = new Set<number>([
|
||||
GUILD_TEXT_CHANNEL_TYPE,
|
||||
GUILD_ANNOUNCEMENT_CHANNEL_TYPE,
|
||||
]);
|
||||
|
||||
type DiscordChannelResponse = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
type: number;
|
||||
parent_id: string | null;
|
||||
position: number;
|
||||
};
|
||||
|
||||
const buildFailureResult = (error: string): DiscordListChannelsResult => ({
|
||||
success: false,
|
||||
channels: [],
|
||||
count: 0,
|
||||
error,
|
||||
});
|
||||
|
||||
const resolveGuildId = async ({
|
||||
botToken,
|
||||
providedGuildId,
|
||||
}: {
|
||||
botToken: string;
|
||||
providedGuildId: string | undefined;
|
||||
}): Promise<{ ok: true; guildId: string } | { ok: false; error: string }> => {
|
||||
const trimmed = providedGuildId?.trim();
|
||||
|
||||
if (isDefined(trimmed) && trimmed.length > 0) {
|
||||
return { ok: true, guildId: trimmed };
|
||||
}
|
||||
|
||||
const guildsResult = await fetchDiscordGuilds(botToken);
|
||||
|
||||
if (!guildsResult.ok) {
|
||||
return { ok: false, error: guildsResult.errorMessage };
|
||||
}
|
||||
|
||||
const guilds = guildsResult.data;
|
||||
|
||||
if (guilds.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
'Bot has not been invited to any Discord server yet. Use the OAuth2 → URL Generator in the Discord Developer Portal (scope `bot`) to invite it to a server, then retry.',
|
||||
};
|
||||
}
|
||||
|
||||
if (guilds.length > 1) {
|
||||
const guildList = guilds
|
||||
.map((guild) => `"${guild.name}" (${guild.id})`)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: `Bot is in ${guilds.length} Discord servers — please specify which one via the guildId input. Available: ${guildList}.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, guildId: guilds[0].id };
|
||||
};
|
||||
|
||||
export const discordListChannelsHandler = async (
|
||||
parameters: DiscordListChannelsInput,
|
||||
): Promise<DiscordListChannelsResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return buildFailureResult(tokenResult.error);
|
||||
}
|
||||
|
||||
const guildIdResult = await resolveGuildId({
|
||||
botToken: tokenResult.botToken,
|
||||
providedGuildId: parameters.guildId,
|
||||
});
|
||||
|
||||
if (!guildIdResult.ok) {
|
||||
return buildFailureResult(guildIdResult.error);
|
||||
}
|
||||
|
||||
const result = await discordApiRequest<DiscordChannelResponse[]>({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'GET',
|
||||
path: `/guilds/${encodeURIComponent(guildIdResult.guildId)}/channels`,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return buildFailureResult(result.errorMessage);
|
||||
}
|
||||
|
||||
const channels: DiscordListChannelsResultChannel[] = result.data
|
||||
.filter(
|
||||
(channel) =>
|
||||
POSTABLE_CHANNEL_TYPES.has(channel.type) && isDefined(channel.name),
|
||||
)
|
||||
.map((channel) => ({
|
||||
id: channel.id,
|
||||
name: channel.name as string,
|
||||
type: channel.type,
|
||||
parentId: channel.parent_id,
|
||||
position: channel.position,
|
||||
}))
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
channels,
|
||||
count: channels.length,
|
||||
};
|
||||
};
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { type DiscordListGuildsResult } from 'src/logic-functions/types/discord-list-guilds-output.type';
|
||||
import { fetchDiscordGuilds } from 'src/logic-functions/utils/fetch-discord-guilds';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
const buildFailureResult = (error: string): DiscordListGuildsResult => ({
|
||||
success: false,
|
||||
guilds: [],
|
||||
count: 0,
|
||||
error,
|
||||
});
|
||||
|
||||
export const discordListGuildsHandler =
|
||||
async (): Promise<DiscordListGuildsResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return buildFailureResult(tokenResult.error);
|
||||
}
|
||||
|
||||
const result = await fetchDiscordGuilds(tokenResult.botToken);
|
||||
|
||||
if (!result.ok) {
|
||||
return buildFailureResult(result.errorMessage);
|
||||
}
|
||||
|
||||
const guilds = result.data.map((guild) => ({
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
guilds,
|
||||
count: guilds.length,
|
||||
};
|
||||
};
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type DiscordPostMessageInput } from 'src/logic-functions/types/discord-post-message-input.type';
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
import { buildDiscordFailureResult } from 'src/logic-functions/utils/build-discord-failure-result';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
type DiscordMessageResponse = {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
};
|
||||
|
||||
export const discordPostMessageHandler = async (
|
||||
parameters: DiscordPostMessageInput,
|
||||
): Promise<DiscordToolResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Discord is not configured',
|
||||
error: tokenResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
const replyToMessageId = parameters.replyToMessageId?.trim();
|
||||
const body: Record<string, unknown> = {
|
||||
content: parameters.messageText,
|
||||
};
|
||||
|
||||
if (isDefined(replyToMessageId) && replyToMessageId.length > 0) {
|
||||
body.message_reference = { message_id: replyToMessageId };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await discordApiRequest<DiscordMessageResponse>({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'POST',
|
||||
path: `/channels/${encodeURIComponent(parameters.channelId)}/messages`,
|
||||
body,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to post Discord message',
|
||||
error: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Message posted to Discord (id=${result.data.id}).`,
|
||||
messageId: result.data.id,
|
||||
channelId: result.data.channel_id,
|
||||
};
|
||||
} catch (error) {
|
||||
return buildDiscordFailureResult('Failed to post Discord message', error);
|
||||
}
|
||||
};
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
import { type DiscordUpdateMessageInput } from 'src/logic-functions/types/discord-update-message-input.type';
|
||||
import { buildDiscordFailureResult } from 'src/logic-functions/utils/build-discord-failure-result';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
type DiscordMessageResponse = {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
};
|
||||
|
||||
export const discordUpdateMessageHandler = async (
|
||||
parameters: DiscordUpdateMessageInput,
|
||||
): Promise<DiscordToolResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Discord is not configured',
|
||||
error: tokenResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await discordApiRequest<DiscordMessageResponse>({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'PATCH',
|
||||
path: `/channels/${encodeURIComponent(
|
||||
parameters.channelId,
|
||||
)}/messages/${encodeURIComponent(parameters.messageId)}`,
|
||||
body: { content: parameters.newMessageText },
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to update Discord message',
|
||||
error: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Discord message updated.',
|
||||
messageId: result.data.id,
|
||||
channelId: result.data.channel_id,
|
||||
};
|
||||
} catch (error) {
|
||||
return buildDiscordFailureResult('Failed to update Discord message', error);
|
||||
}
|
||||
};
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordAddReactionInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
label: 'Discord channel ID',
|
||||
description: 'Channel where the message lives (Discord channel ID).',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
label: 'Message ID',
|
||||
description:
|
||||
'The message to react to: its **message ID** (same value as `messageId` returned when the bot posted it, or right-click the message → "Copy Message ID").',
|
||||
},
|
||||
emoji: {
|
||||
type: 'string',
|
||||
label: 'Emoji',
|
||||
description:
|
||||
'Emoji to add. For a standard unicode emoji, paste the emoji directly (e.g. `👍`, `✅`, `🎉`). For a custom server emoji, use the format `name:id` (e.g. `partyparrot:643452342342342342`). The bot must have **Add Reactions** permission in the channel.',
|
||||
},
|
||||
},
|
||||
required: ['channelId', 'messageId', 'emoji'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordDeleteMessageInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
label: 'Discord channel ID',
|
||||
description: 'Channel that contains the message (Discord channel ID).',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
label: 'Message ID',
|
||||
description:
|
||||
'Which message to remove: its **message ID** (same value as `messageId` returned when the bot posted it). The bot can delete its own messages without extra permissions; deleting others\' messages requires the **Manage Messages** permission.',
|
||||
},
|
||||
},
|
||||
required: ['channelId', 'messageId'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordListChannelsInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
guildId: {
|
||||
type: 'string',
|
||||
label: 'Discord server (guild) ID',
|
||||
description:
|
||||
'Optional. The Discord server ID to list channels for (a long number like `1234567890123456789`). In Discord with Developer Mode on: right-click the server icon → "Copy Server ID". If omitted and the bot is in exactly one server, that server is used automatically; if the bot is in multiple servers, the error response lists each server name and ID so you can pick one.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordListGuildsInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordPostMessageInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
label: 'Discord channel ID',
|
||||
description:
|
||||
'Where to post: the Discord channel ID (a long number like `1234567890123456789`). In Discord with Developer Mode on: right-click the channel → "Copy Channel ID". The bot must be a member of the server and have permission to send messages in this channel.',
|
||||
},
|
||||
messageText: {
|
||||
type: 'string',
|
||||
label: 'Message',
|
||||
multiline: true,
|
||||
description:
|
||||
'The message text. Supports Discord-flavoured markdown (**bold**, *italics*, ```code blocks```, lists, links). Max 2000 characters — Discord rejects longer payloads.',
|
||||
},
|
||||
replyToMessageId: {
|
||||
type: 'string',
|
||||
label: 'Reply to message ID',
|
||||
description:
|
||||
'Optional. To reply to an existing message, paste its ID (from a previous step\'s `messageId` output, or right-click the message → "Copy Message ID"). Leave empty for a normal new message.',
|
||||
},
|
||||
},
|
||||
required: ['channelId', 'messageText'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordUpdateMessageInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
label: 'Discord channel ID',
|
||||
description: 'Channel that contains the message (Discord channel ID).',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
label: 'Message ID',
|
||||
description:
|
||||
'Which message to edit: its **message ID** (same value as `messageId` returned when the bot posted the message). You can only edit messages this bot sent.',
|
||||
},
|
||||
newMessageText: {
|
||||
type: 'string',
|
||||
label: 'New message',
|
||||
multiline: true,
|
||||
description:
|
||||
'Replacement text shown in Discord instead of the old content. Supports Discord-flavoured markdown. Max 2000 characters.',
|
||||
},
|
||||
},
|
||||
required: ['channelId', 'messageId', 'newMessageText'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
export type DiscordAddReactionInput = {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
emoji: string;
|
||||
};
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export type DiscordDeleteMessageInput = {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
};
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export type DiscordListChannelsInput = {
|
||||
guildId?: string;
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
export type DiscordListChannelsResultChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
parentId: string | null;
|
||||
position: number;
|
||||
};
|
||||
|
||||
export type DiscordListChannelsResult = {
|
||||
success: boolean;
|
||||
channels: DiscordListChannelsResultChannel[];
|
||||
count: number;
|
||||
error?: string;
|
||||
};
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
export type DiscordListGuildsResultGuild = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DiscordListGuildsResult = {
|
||||
success: boolean;
|
||||
guilds: DiscordListGuildsResultGuild[];
|
||||
count: number;
|
||||
error?: string;
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
export type DiscordPostMessageInput = {
|
||||
channelId: string;
|
||||
messageText: string;
|
||||
replyToMessageId?: string;
|
||||
};
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
export type DiscordToolResult = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
error?: string;
|
||||
messageId?: string;
|
||||
channelId?: string;
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
export type DiscordUpdateMessageInput = {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
newMessageText: string;
|
||||
};
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
|
||||
export const buildDiscordFailureResult = (
|
||||
message: string,
|
||||
error: unknown,
|
||||
): DiscordToolResult => ({
|
||||
success: false,
|
||||
message,
|
||||
error: error instanceof Error ? error.message : 'Discord request failed',
|
||||
});
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const DISCORD_API_BASE_URL = 'https://discord.com/api/v10';
|
||||
|
||||
type DiscordApiSuccess<TData> = {
|
||||
ok: true;
|
||||
status: number;
|
||||
data: TData;
|
||||
};
|
||||
|
||||
type DiscordApiFailure = {
|
||||
ok: false;
|
||||
status: number;
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
export type DiscordApiResult<TData> =
|
||||
| DiscordApiSuccess<TData>
|
||||
| DiscordApiFailure;
|
||||
|
||||
type DiscordErrorBody = {
|
||||
code?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type DiscordApiRequestParams = {
|
||||
botToken: string;
|
||||
path: string;
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
body?: unknown;
|
||||
};
|
||||
|
||||
export const discordApiRequest = async <TData = unknown>({
|
||||
botToken,
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
}: DiscordApiRequestParams): Promise<DiscordApiResult<TData>> => {
|
||||
const url = `${DISCORD_API_BASE_URL}${path}`;
|
||||
const hasBody = isDefined(body);
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bot ${botToken}`,
|
||||
...(hasBody ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
...(hasBody ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 0,
|
||||
errorMessage: `Discord API request failed: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return { ok: true, status: 204, data: undefined as TData };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await extractDiscordErrorMessage(response);
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = (await response.json()) as TData;
|
||||
|
||||
return { ok: true, status: response.status, data };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
errorMessage: `Discord API returned a non-JSON response: ${
|
||||
(error as Error).message
|
||||
}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const extractDiscordErrorMessage = async (
|
||||
response: Response,
|
||||
): Promise<string> => {
|
||||
const fallback = `Discord API responded with ${response.status}`;
|
||||
|
||||
try {
|
||||
const errorBody = (await response.json()) as DiscordErrorBody;
|
||||
|
||||
if (isDefined(errorBody.message)) {
|
||||
return isDefined(errorBody.code)
|
||||
? `${errorBody.message} (Discord error code ${errorBody.code})`
|
||||
: errorBody.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
discordApiRequest,
|
||||
type DiscordApiResult,
|
||||
} from 'src/logic-functions/utils/discord-api-request';
|
||||
|
||||
export type DiscordGuildResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const fetchDiscordGuilds = (
|
||||
botToken: string,
|
||||
): Promise<DiscordApiResult<DiscordGuildResponse[]>> =>
|
||||
discordApiRequest<DiscordGuildResponse[]>({
|
||||
botToken,
|
||||
method: 'GET',
|
||||
path: '/users/@me/guilds',
|
||||
});
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const DISCORD_BOT_TOKEN_ENV_VAR = 'DISCORD_BOT_TOKEN';
|
||||
|
||||
export const getDiscordBotToken = ():
|
||||
| { success: true; botToken: string }
|
||||
| { success: false; error: string } => {
|
||||
const botToken = process.env[DISCORD_BOT_TOKEN_ENV_VAR];
|
||||
|
||||
if (!isDefined(botToken) || botToken.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Discord is not configured. Open the Twenty Discord app settings and set the DISCORD_BOT_TOKEN application variable (Developer Portal → Bot tab → Reset Token).',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, botToken };
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineApplicationRole } from 'twenty-sdk/define';
|
||||
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Twenty Discord tools role',
|
||||
description:
|
||||
'No CRM data access — tools only forward requests to Discord using the configured bot token.',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: [],
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"target": "es2020",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"src/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user