snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

22
.github/CLA.md vendored Normal file
View File

@@ -0,0 +1,22 @@
# Contributor License Agreement
The following terms are used throughout this agreement:
- You - the person or legal entity including its affiliates asked to accept this agreement. An affiliate is any entity that controls or is controlled by the legal entity, or is under common control with it.
- Project - is an umbrella term that refers to any and all Twenty.com PBC open source projects.
- Contribution - any type of work that is submitted to a Project, including any modifications or additions to existing work.
- Submitted - conveyed to a Project via a pull request, commit, issue, or any form of electronic, written, or verbal communication with Twenty.com PBC, contributors or maintainers.
# Grant of Copyright License.
Subject to the terms and conditions of this agreement, You grant to the Projects’ maintainers, contributors, users and to Twenty.com PBC a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your contributions and such derivative works. Except for this license, You reserve all rights, title, and interest in your contributions.
# Grant of Patent License.
Subject to the terms and conditions of this agreement, You grant to the Projects’ maintainers, contributors, users and to Twenty.com PBC a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer your contributions, where such license applies only to those patent claims licensable by you that are necessarily infringed by your contribution or by combination of your contribution with the project to which this contribution was submitted.
If any entity institutes patent litigation - including cross-claim or counterclaim in a lawsuit - against You alleging that your contribution or any project it was submitted to constitutes or is responsible for direct or contributory patent infringement, then any patent licenses granted to that entity under this agreement shall terminate as of the date such litigation is filed.
# Source of Contribution.
Your contribution is either your original creation, based upon previous work that, to the best of your knowledge, is covered under an appropriate open source license and you have the right under that license to submit that work with modifications, whether created in whole or in part by you, or you have clearly identified the source of the contribution and any license or other restriction (like related patents, trademarks, and license agreements) of which you are personally aware.

9
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,9 @@
/.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
/.claude/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.mcp.json @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/CLAUDE.md @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp

59
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@@ -0,0 +1,59 @@
# Contributor Code of Conduct
## Twenty's Pledge
The contributors and maintainers of this project pledge to ensure a harassment-free experience for everyone in the community.
The focus of both contributors and maintainers is on acting and interacting in ways that promote an open, welcoming, friendly, diverse, inclusive, and healthy community.
## Twenty's Standards
Examples of behavior that contributes to a positive environment for this
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Respectfully giving and accepting constructive feedback
* Accepting responsibility and apologizing to those affected by mistakes,
and learning from the experience
* Focusing on what's best, not just for each individual but also for the community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* The use of aggressive language
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct inappropriate in a professional setting.
## Enforcement Responsibilities
Community leaders and maintainers of this repository are responsible for clarifying and enforcing Twenty's standards of acceptable behavior. They will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders and maintainers of this repository have the right and responsibility to remove, edit, or reject comments, commits, code, issues, and other contributions that aren't aligned with this Code of Conduct. They will also communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing this community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Attribution
This Code of Conduct is an adaption of the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines draw inspiration from [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

66
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,66 @@
# Contributing to Twenty
Thanks for considering contributing to Twenty!
Please make sure to go through the [documentation](https://docs.twenty.com) before.
<br>
## Good first issues
Good first issues are a great way to start contributing and get familiar with the codebase. You can find them on by filtering on the [good first issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue) label.
## Issue assignment
To avoid conflicts, we follow these guidelines:
1. For `Good First Issue` and `Experienced Contributor` issues without `size: long` labels, we'll merge the first PRs that meet our [code quality standards](https://docs.twenty.com/developers). **We don't assign contributors to these issues**. For `priority: high` issues, our core team will step in within days if no adequate contributions are received.
2. For `size: long` Issues, assigned contributors have one week to submit their first draft PR.
## How to Contribute
1. **Fork the Repository:** Click on the 'Fork' button in the upper right corner of the repository's GitHub page. This will create a copy of the repository in your GitHub account.
2. **Clone the Repository:** Clone your forked repository to your local machine using `git clone`.
```shell
git clone https://github.com/yourusername/twenty.git
cd twenty
```
3. **Create a New Branch:** Create a new branch for your changes instead of using the main branch.
```shell
git checkout -b your-branch-name
```
4. **Make Changes:** Make your desired changes and ensure that your code adheres to Twenty's coding standards.
5. **Test Locally:** Test your changes locally to ensure they work as expected.
6. **Commit Changes:** Commit your changes with a clear and concise commit message.
```shell
git commit -m "Add your detailed description here"
```
7. **Push Changes:** Push your changes to your forked repository.
```shell
git push origin your-branch-name
```
8. **Create a Pull Request:** Go to the original Twenty repository and create a pull request. Please provide a detailed description of your changes. Submitting a PR means you agree to the CLA.
9. **Code Review:** Your pull request will undergo a code review.
10. **Merge:** Once approved, maintainers will merge your pull request into the main repository.
## Reporting Issues
If you face any issues or have suggestions, please feel free to [create an issue on Twenty's GitHub repository](https://github.com/twentyhq/twenty/issues/new). Please provide as much detail as possible.

View File

@@ -0,0 +1,55 @@
---
name: Add new feature / behavior
about: Describe the desired product increment
title: 'Ex: Add custom field from Companies / People table options menu'
labels: ''
assignees: ''
---
## Scope & Context
**Example:**
```
We are working on enabling users to add custom fields in their workspace.
The ticket is about adding the custom field feature on Companies and People pages only.
```
## Current behavior
A clear and concise description of what the current behavior is.
Please also add **screenshots** of the existing application.
**Example:**
```
On Companies and People pages, the "field" menu item from the option menu shows both displayed columns, and hidden columns
[screenshot]
```
## Expected behavior
A clear and concise description of what you want to happen.
Please also add **figma screenshots or figma links** if available
**Example:**
```
The "field" menu item from the option menu should only display visible columns. To display a new column, the user should click on "Add field" at the bottom of the menu.
[figma screenshot 1]
[figma link 1]
Clicking on this button should swap the menu with the same menu that is used by pressing the "+" button on the table title bar, showing "Add custom field option"
[figma screenshot 2]
[figma link 2]
Clicking on "Add custom field" should open swap the menu with the "field type" menu
...
```
## Technical inputs
**Example:**
```
- Modify TableOptionsDropdownContent to add the new "Add field Item" using a MenuItem component
- Create an internal state 'isAddingField' to track the progress in the flow. Set this state to true on "Add field Item" onClick
- ...
```

36
.github/ISSUE_TEMPLATE/report-bug.md vendored Normal file
View File

@@ -0,0 +1,36 @@
---
name: Report a bug
about: Report a bug or a functional regression
title: 'Ex: In DarkMode, a blank square appears in bottom right corner while scrolling'
labels: ['type: bug']
assignees: ''
---
## Bug Description
A clear and concise description of what the current behavior is.
Please also add **screenshots** of the existing application.
**Example:**
```
In DarkMode, when scrollbar are displayed (for example on Companies page, with enough companies in the list), we see a blank square in the bottom right corner
[screenshot]
```
## Expected behavior
A clear and concise description of what the expected behavior is.
**Example:**
```
The blank square should be transparent (invisible)
```
## Technical inputs
**Example:**
```
- We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
- Probably fixable with CSS
```

View File

@@ -0,0 +1,33 @@
---
name: Request technical chore
about: Request technical work that does not provide any product increment (aka refactoring)
title: ''
labels: ['type: chore']
assignees: ''
---
## Scope & Context
**Example:**
```
In previous PRs: #1667 and #1636, we have introduced a new MenuItem draggable in the dropdown and implemented a drag and drop behavior.
This is working but it would be great to refactor this into separated components so we can re-use them.
```
## Technical inputs
A clear and detailed description of what the expected change is.
Describe components, files and folders that should be touch and how.
Using a Task list can be helpful
**Example:**
```
Having a list that is draggable will be useful, not only in dropdown.
Create a folder @/ui/draggable-list with a DraggableList component
This component should take as prop: itemsComponents, onDragEnd((previousIndex, nextIndex) => {})
Use this component in ObjectOptionsDropdownHiddenFieldsContent (move the logic from ObjectOptionsDropdownHiddenFieldsContent to DraggableList) by passing a list of DraggableMenuItems
Add a storybook test on this list (we don't know how to actually test the draggable behavior, but we can at least make sure the component renders correctly a list of items)
```

31
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,31 @@
# Security Policy
## Reporting a Vulnerability
Reporting any potential vulnerabilities is strongly encouraged.
If you suspect a vulnerability, please take the following steps:
- Contact the team at `security at twenty.com`.
- Include a comprehensive description of the potential vulnerability and steps to reproduce the issue, if possible. The more information you can provide, the quicker Twenty can address the problem.
You can expect a response to your initial report within one business day.
While the core team works on addressing the issue, please maintain confidentiality about the vulnerability to ensure the security of all users.
Please refrain from exploiting the vulnerability or revealing the problem to others.
While Twenty doesn't have a formal bug bounty program right now due to the project's nascent stage, rest assured that:
- You will get a response within one business day.
- Your report and all accompanying data will receive the highest level of confidentiality.
- Your contribution is greatly appreciated, and Twenty would acknowledge your role in the vulnerability fix, if you opt for identification.
- Twenty will grant you permission to publicly discuss your findings once users have had a reasonable time to apply the patch after it becomes available.
- Twenty guarantees not to pursue any legal action as long as the vulnerability is not exploited.
## Security Features
Efforts are continually made to enhance the security of the product.
If you have any recommendations or feature request that could enhance the product's security, please share them via the discussion forum.
⚠️ Note this does not apply to security vulnerabilities. If you're in doubt, then always follow the security vulnerability process

28
.github/TRADEMARK.md vendored Normal file
View File

@@ -0,0 +1,28 @@
# Twenty Trademark Policy
"Twenty", the Twenty logo, and twenty.com are trademarks of Twenty.com, PBC ("Twenty"), whether registered or unregistered. The source code of Twenty is open source under the licenses described in [LICENSE](../LICENSE); those licenses do not grant any rights to use our trademarks.
The goal of this policy is simple: anyone should be able to say truthfully that they use, build on, or contribute to Twenty, and nobody should suggest an affiliation with or endorsement by Twenty that does not exist, or confuse users about what comes from us.
## You can, without asking
- Use and self-host Twenty, modified or not, for yourself or your organization, and say that you do.
- Make truthful statements such as "built on Twenty", "compatible with Twenty", "powered by Twenty", or "we host Twenty for our clients".
- Name an application or integration "X for Twenty" or "Twenty integration for X", as long as your own brand is the dominant element and you do not use our logo as your own.
- Use the name "Twenty" to refer to the project in articles, talks, tutorials, courses, and comparisons.
- Link to twenty.com and use unmodified screenshots of the product.
## You need our written permission to
- Use "Twenty" or a confusingly similar name in the name of your company, product, service, domain name, social media account, or app store listing (for example "TwentyCloud" or "twenty-hosting.com").
- Market a hosted, managed, or modified version of Twenty in a way that suggests it is an official Twenty offering, or is endorsed by or affiliated with Twenty.
- Use the Twenty logo as, or as part of, your own logo or branding.
- Sell merchandise bearing the Twenty name or logo.
## Forks
You may maintain and distribute forks of the source code under the terms of the applicable licenses. If you distribute or operate a fork publicly, give it its own name and branding. You may state truthfully that it is "a fork of Twenty", but do not use the Twenty name or logo in a way that suggests your fork is the original project.
## Questions
If you are unsure whether your use is allowed, or want to request permission, open an issue on GitHub or contact us through twenty.com.

View File

@@ -0,0 +1,53 @@
name: Deploy Twenty App
description: Build and deploy a Twenty app to a remote instance
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target instance
required: true
app-path:
description: Path to the app directory (relative to repo root). Defaults to repo root.
required: false
default: '.'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY, accessToken: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Deploy
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty app:publish --private --remote target

View File

@@ -0,0 +1,53 @@
name: Install Twenty App
description: Install (or upgrade) a Twenty app on a specific workspace
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target workspace
required: true
app-path:
description: Path to the app directory (relative to repo root). Defaults to repo root.
required: false
default: '.'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY, accessToken: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Install
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty app:install --remote target

39
.github/actions/nx-affected/action.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Nx Affected CI
inputs:
parallel:
required: false
default: '3'
tag:
required: false
tasks:
required: true
configuration:
required: false
default: 'ci'
args:
required: false
runs:
using: "composite"
steps:
- name: Fetch main branch for diff
shell: bash
run: git fetch origin main --depth=1
- name: Get last successful commit
if: env.NX_BASE == ''
uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4
- name: Fallback to origin/main if no base found
if: env.NX_BASE == ''
shell: bash
run: echo "NX_BASE=$(git rev-parse origin/main)" >> $GITHUB_ENV
- name: Run affected command
shell: bash
env:
NX_CONFIGURATION: ${{ inputs.configuration }}
NX_TASKS: ${{ inputs.tasks }}
NX_PARALLEL: ${{ inputs.parallel }}
NX_TAG: ${{ inputs.tag }}
NX_ARGS: ${{ inputs.args }}
# Nx buffers task output into an internal stream by default, so a task
# that gets killed takes its buffer with it: the log ends on whatever last
# happened to flush and the stack trace is never printed.
run: npx nx affected --nxBail --outputStyle=stream --configuration="$NX_CONFIGURATION" -t="$NX_TASKS" --parallel="$NX_PARALLEL" --exclude="*,!tag:$NX_TAG" $NX_ARGS

View File

@@ -0,0 +1,38 @@
name: Restore cache
inputs:
key:
required: true
description: Prefix to the cache key
additional-paths:
required: false
outputs:
cache-primary-key:
description: actions/cache/restore cache-primary-key outputs proxy
value: ${{ steps.restore-cache.outputs.cache-primary-key }}
cache-hit:
description: String bool indicating whether cache has been directly or indirectly hit
value: ${{ steps.restore-cache.outputs.cache-hit == 'true' || steps.restore-cache.outputs.cache-matched-key != '' }}
runs:
using: composite
steps:
- name: Cache primary key builder
id: cache-primary-key-builder
shell: bash
env:
CACHE_KEY: ${{ inputs.key }}
REF_NAME: ${{ github.ref_name }}
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)
id: restore-cache
with:
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
restore-keys: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-
path: |
.cache
.nx
node_modules/.cache
packages/*/node_modules/.cache
${{ inputs.additional-paths }}

24
.github/actions/save-cache/action.yaml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Save cache
inputs:
key:
required: true
description: Primary key to the cache, should be retrieved from `cache-restore` composite action outputs.
additional-paths:
required: false
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)
with:
key: ${{ inputs.key }}
path: |
.cache
.nx
node_modules/.cache
packages/*/node_modules/.cache
${{ inputs.additional-paths }}

View File

@@ -0,0 +1,50 @@
name: Spawn Twenty App Dev Test
description: >
Starts a Twenty all-in-one test instance (server, worker, database, redis)
using the twentycrm/twenty-app-dev Docker image on port 2021.
The server is available at http://localhost:2021 with seeded demo data.
inputs:
twenty-version:
description: 'Twenty Docker Hub image tag for twenty-app-dev (e.g., "latest" or "v1.20.0").'
required: false
default: 'latest'
outputs:
server-url:
description: 'URL where the Twenty test server can be reached'
value: http://localhost:2021
api-key:
description: 'ACCESS token for the seeded Twenty dev admin (Tim)'
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDB9.Ow8z-Dptg4LDiO2nApu5UQcX2a-gXdu_McalWs7Pgt8
runs:
using: 'composite'
steps:
- name: Start twenty-app-dev-test container
shell: bash
run: |
docker run -d \
--name twenty-app-dev-test \
-p 2021:2021 \
-e NODE_PORT=2021 \
-e SERVER_URL=http://localhost:2021 \
-e MARKETPLACE_CATALOG_SYNC_CRON_ENABLED=false \
-e API_RATE_LIMITING_SHORT_LIMIT=100000 \
-e API_RATE_LIMITING_LONG_LIMIT=100000 \
twentycrm/twenty-app-dev:${{ inputs.twenty-version }}
echo "Waiting for Twenty test instance to become healthy…"
TIMEOUT=180
ELAPSED=0
until curl -sf http://localhost:2021/healthz > /dev/null 2>&1; do
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "::error::Twenty did not become healthy within ${TIMEOUT}s"
docker logs twenty-app-dev-test 2>&1 | tail -80
exit 1
fi
sleep 3
ELAPSED=$((ELAPSED + 3))
echo " … waited ${ELAPSED}s"
done
echo "Twenty test instance is ready at http://localhost:2021 (took ~${ELAPSED}s)"

View File

@@ -0,0 +1,88 @@
name: Spawn Twenty Docker Image
description: >
Starts a full Twenty instance (server, worker, database, redis) using Docker
Compose. The server is available at http://localhost:3000 for subsequent steps
in the caller's job.
Accepts "latest" (pulls the latest Docker Hub image, checks out main) or a
semver tag (e.g., v0.40.0).
Designed to be consumed from external repositories (e.g., twenty-app).
inputs:
twenty-version:
description: 'Twenty Docker Hub image tag — either "latest" or a semver tag (e.g., v0.40.0).'
required: true
twenty-repository:
description: 'Twenty repository to checkout docker compose files from.'
required: false
default: 'twentyhq/twenty'
github-token:
description: 'GitHub token for cross-repo checkout. Required when calling from an external repository.'
required: false
default: ${{ github.token }}
outputs:
server-url:
description: 'URL where the Twenty server can be reached'
value: http://localhost:3000
access-token:
description: 'Admin access token for the Twenty instance'
value: ${{ steps.admin-token.outputs.access-token }}
runs:
using: 'composite'
steps:
- name: Resolve version
id: resolve
shell: bash
run: |
VERSION="${{ inputs.twenty-version }}"
if [ "$VERSION" = "latest" ]; then
echo "docker-tag=latest" >> "$GITHUB_OUTPUT"
echo "git-ref=main" >> "$GITHUB_OUTPUT"
elif echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "docker-tag=$VERSION" >> "$GITHUB_OUTPUT"
echo "git-ref=$VERSION" >> "$GITHUB_OUTPUT"
else
echo "::error::twenty-version must be \"latest\" or a semver tag (e.g., v0.40.0). Got: '$VERSION'"
exit 1
fi
- name: Checkout docker compose files
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
repository: ${{ inputs.twenty-repository }}
ref: ${{ steps.resolve.outputs.git-ref }}
token: ${{ inputs.github-token }}
sparse-checkout: |
packages/twenty-docker
sparse-checkout-cone-mode: false
path: .twenty-spawn
- name: Prepare environment
shell: bash
working-directory: ./.twenty-spawn/packages/twenty-docker
run: |
cp .env.example .env
echo "" >> .env
echo "TAG=${{ steps.resolve.outputs.docker-tag }}" >> .env
echo "APP_SECRET=replace_me_with_a_random_string" >> .env
echo "SERVER_URL=http://localhost:3000" >> .env
- name: Start Twenty instance
shell: bash
working-directory: ./.twenty-spawn/packages/twenty-docker
run: |
docker compose up -d --wait || {
echo "::error::Docker compose failed to start or health checks timed out"
docker compose logs
exit 1
}
echo "Twenty instance is ready at http://localhost:3000"
- name: Set admin access token
id: admin-token
shell: bash
run: |
ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik"
echo "::add-mask::$ACCESS_TOKEN"
echo "access-token=$ACCESS_TOKEN" >> "$GITHUB_OUTPUT"

View File

@@ -0,0 +1,124 @@
name: Spawn Twenty Server
description: >
Provisions a running Twenty server and returns its URL + API key, from one of
two sources:
- dockerhub-latest: the latest published twentycrm/twenty-app-dev image
(delegates to spawn-twenty-app-dev-test, port 2021).
- local: a server built from the monorepo's current files (port 3000). It
brings up its own postgres + redis via docker, so it works inside any job
without declaring job-level services.
Both sources expose the same server-url / api-key contract so callers never
branch on how the server came up.
inputs:
source:
description: 'Where the server comes from: "dockerhub-latest" or "local".'
required: false
default: 'local'
twenty-version:
description: 'Docker Hub image tag, only used when source is "dockerhub-latest".'
required: false
default: 'latest'
server-build-cache-key:
description: 'Cache key used to speed up the local server build (nx cache).'
required: false
default: 'server-build'
outputs:
server-url:
description: 'URL where the Twenty server can be reached'
value: ${{ steps.resolve.outputs.server-url }}
api-key:
description: 'API key (or access token) for the seeded workspace'
value: ${{ steps.resolve.outputs.api-key }}
runs:
using: 'composite'
steps:
- name: Spawn from Docker Hub image
id: dockerhub
if: inputs.source == 'dockerhub-latest'
uses: ./.github/actions/spawn-twenty-app-dev-test
with:
twenty-version: ${{ inputs.twenty-version }}
- name: Start postgres and redis
if: inputs.source == 'local'
shell: bash
run: |
docker run -d --name twenty-postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
postgres:18
docker run -d --name twenty-redis -p 6379:6379 redis
echo "Waiting for postgres…"
for i in {1..30}; do
if PGPASSWORD=postgres pg_isready -h localhost -p 5432 -U postgres > /dev/null 2>&1; then
echo "Postgres ready"
break
fi
sleep 2
if [ "$i" -eq 30 ]; then
echo "::error::Postgres did not become ready in time"
docker logs twenty-postgres 2>&1 | tail -40
exit 1
fi
done
- name: Install dependencies
if: inputs.source == 'local'
uses: ./.github/actions/yarn-install
- name: Restore server build cache
if: inputs.source == 'local'
uses: ./.github/actions/restore-cache
with:
key: ${{ inputs.server-build-cache-key }}
- name: Build server from monorepo
if: inputs.source == 'local'
shell: bash
run: |
npx nx build twenty-shared
npx nx build twenty-server
- name: Write server env and create databases
if: inputs.source == 'local'
shell: bash
run: |
npx nx reset:env:e2e-testing-server twenty-server
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Start server
if: inputs.source == 'local'
shell: bash
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
if: inputs.source == 'local'
shell: bash
run: npx wait-on http://localhost:3000/healthz --timeout 180000 --interval 1000
- name: Resolve server url and api key
id: resolve
shell: bash
env:
SOURCE: ${{ inputs.source }}
DOCKERHUB_URL: ${{ steps.dockerhub.outputs.server-url }}
DOCKERHUB_KEY: ${{ steps.dockerhub.outputs.api-key }}
LOCAL_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
run: |
if [ "$SOURCE" = "dockerhub-latest" ]; then
echo "server-url=$DOCKERHUB_URL" >> "$GITHUB_OUTPUT"
echo "api-key=$DOCKERHUB_KEY" >> "$GITHUB_OUTPUT"
elif [ "$SOURCE" = "local" ]; then
echo "server-url=http://localhost:3000" >> "$GITHUB_OUTPUT"
echo "api-key=$LOCAL_KEY" >> "$GITHUB_OUTPUT"
else
echo "::error::Unknown source '$SOURCE' (expected 'dockerhub-latest' or 'local')"
exit 1
fi

View File

@@ -0,0 +1,85 @@
name: Test Twenty App
description: >
Exercises a single Twenty app against an already-running server. In
"installation-and-integration-test" mode it runs the app's integration suite,
then deploys and installs the app. In "installation-only" mode it only deploys
and installs, which is enough to catch server-side install regressions without
paying for each app's full test suite.
inputs:
api-url:
description: Base URL of the target Twenty instance
required: true
api-key:
description: API key or access token for the target workspace
required: true
app-path:
description: Path to the app directory (relative to repo root)
required: true
mode:
description: >
What to run: "installation-and-integration-test" (integration + deploy +
install), "integration-test-only" (integration suite only) or
"installation-only" (deploy + install only).
required: false
default: 'installation-and-integration-test'
runs:
using: composite
steps:
- name: Validate mode
shell: bash
env:
MODE: ${{ inputs.mode }}
run: |
case "$MODE" in
installation-and-integration-test|integration-test-only|installation-only) ;;
*)
echo "::error::Unknown mode '$MODE' (expected 'installation-and-integration-test', 'integration-test-only' or 'installation-only')"
exit 1
;;
esac
- name: Enable Corepack
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'integration-test-only'
shell: bash
run: corepack enable
- name: Setup Node.js
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'integration-test-only'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'integration-test-only'
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Integration tests
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'integration-test-only'
shell: bash
working-directory: ${{ inputs.app-path }}
env:
TWENTY_API_URL: ${{ inputs.api-url }}
TWENTY_API_KEY: ${{ inputs.api-key }}
run: yarn test
- name: Deploy app
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'installation-only'
uses: ./.github/actions/deploy-twenty-app
with:
api-url: ${{ inputs.api-url }}
api-key: ${{ inputs.api-key }}
app-path: ${{ inputs.app-path }}
- name: Install app
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'installation-only'
uses: ./.github/actions/install-twenty-app
with:
api-url: ${{ inputs.api-url }}
api-key: ${{ inputs.api-key }}
app-path: ${{ inputs.app-path }}

View File

@@ -0,0 +1,179 @@
name: Upgrade Mutation Guard
description: >
Validates that added/modified upgrade commands stay in the current version
directory and keep timestamps real and append-only, diffed against a given
base. Shared by the PR-level guard (base = PR base) and the merge-queue guard
(base = merge candidate's base, i.e. the current tip of main).
inputs:
base_sha:
description: Commit to diff the working tree against.
required: true
allow_previous_version_mutation:
description: Skip the version-directory check (PR label bypass).
required: false
default: 'false'
runs:
using: composite
steps:
- name: Fetch base commit
shell: bash
env:
BASE_SHA: ${{ inputs.base_sha }}
run: |
set -euo pipefail
if [ -z "$BASE_SHA" ]; then
echo "::error::base_sha input is empty"
exit 1
fi
git fetch --no-tags --depth=1 origin "$BASE_SHA"
- name: Check upgrade version commands are in current version only
if: ${{ inputs.allow_previous_version_mutation != 'true' }}
shell: bash
env:
BASE_SHA: ${{ inputs.base_sha }}
COMMANDS_ROOT: packages/twenty-server/src/database/commands/upgrade-version-command
run: |
set -euo pipefail
VERSION_CONSTANT_FILE="packages/twenty-server/src/engine/core-modules/upgrade/constants/twenty-current-version.constant.ts"
CURRENT_VERSION=$(sed -n "s/.*TWENTY_CURRENT_VERSION = '\([0-9.]*\)'.*/\1/p" "$VERSION_CONSTANT_FILE")
if [ -z "$CURRENT_VERSION" ]; then
echo "::error::Could not extract TWENTY_CURRENT_VERSION from $VERSION_CONSTANT_FILE"
exit 1
fi
CURRENT_DIR=$(echo "$CURRENT_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+)\..*/\1-\2/')
echo "Current version: $CURRENT_VERSION (directory: $CURRENT_DIR)"
DIFF=$(git diff --name-status --find-renames "$BASE_SHA" HEAD -- "$COMMANDS_ROOT")
ADDED_FILES=$(echo "$DIFF" | awk '$1 == "A" { print $2 } $1 ~ /^[RC]/ { print $3 }')
MODIFIED_FILES=$(echo "$DIFF" | awk '$1 == "M" { print $2 }')
ADDED_OFFENDERS=""
MODIFIED_OFFENDERS=""
check_files() {
local category="$1"
shift
for file in "$@"; do
VERSION_DIR=$(echo "$file" | sed -n 's|.*upgrade-version-command/\([0-9]*-[0-9]*\)/.*|\1|p')
if [ -n "$VERSION_DIR" ] && [ "$VERSION_DIR" != "$CURRENT_DIR" ]; then
if [ "$category" = "added" ]; then
ADDED_OFFENDERS="$ADDED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
else
MODIFIED_OFFENDERS="$MODIFIED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
fi
fi
done
}
check_files "added" $ADDED_FILES
check_files "modified" $MODIFIED_FILES
if [ -n "$ADDED_OFFENDERS" ] || [ -n "$MODIFIED_OFFENDERS" ]; then
echo "This change touches upgrade command files outside the current version directory ($CURRENT_DIR / $CURRENT_VERSION)."
if [ -n "$ADDED_OFFENDERS" ]; then
echo ""
echo "New files added to non-current version directories:"
echo -e "$ADDED_OFFENDERS"
fi
if [ -n "$MODIFIED_OFFENDERS" ]; then
echo ""
echo "Existing files modified in non-current version directories:"
echo -e "$MODIFIED_OFFENDERS"
fi
echo ""
echo "If this is intentional, add the label 'ci:allow-previous-version-upgrade-mutation' to the PR and re-run CI."
echo "Otherwise, move your changes to the current version directory ($CURRENT_DIR)."
echo "::error::Upgrade commands were added or modified in non-current version directories."
exit 1
fi
- name: Check upgrade command timestamps are real and keep the sequence append-only
shell: bash
env:
BASE_SHA: ${{ inputs.base_sha }}
COMMANDS_ROOT: packages/twenty-server/src/database/commands/upgrade-version-command
run: |
set -euo pipefail
extract_ts() {
basename "$1" | sed -n -E 's/.*-(instance-command-(fast|slow)|workspace-command)-([0-9]{13})-.*/\3/p'
}
DIFF=$(git diff --name-status --find-renames "$BASE_SHA" HEAD -- "$COMMANDS_ROOT")
NEW_FILES=$(echo "$DIFF" | awk '$1 == "A" { print $2 } $1 ~ /^[RC]/ { print $3 }')
GONE_FILES=$(echo "$DIFF" | awk '$1 == "D" { print $2 } $1 ~ /^R/ { print $2 }')
NOW_MS=$(($(date +%s) * 1000))
MIN_TS=$((NOW_MS - 60 * 24 * 3600 * 1000))
MAX_TS=$((NOW_MS + 2 * 24 * 3600 * 1000))
FAILED=""
for file in $NEW_FILES; do
case "$file" in *.spec.ts) continue ;; esac
TS=$(extract_ts "$file")
if [ -z "$TS" ]; then
continue
fi
if [ "$TS" -lt "$MIN_TS" ] || [ "$TS" -gt "$MAX_TS" ]; then
FAILED="$FAILED\n - $file (timestamp $TS is outside [now - 60 days, now + 2 days])"
continue
fi
VERSION_DIR=$(dirname "$file")
MAX_BASE_TS=0
for base_file in $(git ls-tree -r --name-only "$BASE_SHA" -- "$VERSION_DIR"); do
case "$base_file" in *.spec.ts) continue ;; esac
if echo "$GONE_FILES" | grep -qxF "$base_file"; then
continue
fi
BASE_TS=$(extract_ts "$base_file")
if [ -n "$BASE_TS" ] && [ "$BASE_TS" -gt "$MAX_BASE_TS" ]; then
MAX_BASE_TS=$BASE_TS
fi
done
if [ "$MAX_BASE_TS" -gt 0 ] && [ "$TS" -le "$MAX_BASE_TS" ]; then
FAILED="$FAILED\n - $file (timestamp $TS <= existing max $MAX_BASE_TS)"
fi
done
if [ -n "$FAILED" ]; then
echo "Upgrade command timestamps must be the real epoch millis of when the command"
echo "is authored (within 60 days before / 2 days after now), and strictly greater"
echo "than every existing command in the same version directory."
echo ""
echo "The upgrade cursor tracks a single last-applied position: a command that sorts"
echo "before already-applied commands moves the cursor backwards when it runs,"
echo "re-hiding columns gated by @WasIntroducedInUpgrade on instances that already"
echo "applied them. Fabricated future timestamps poison the sequence for every"
echo "command added after them."
echo ""
echo "If the blocking existing max is itself a fabricated future timestamp, re-slot"
echo "that command to its real merge epoch (rename the file and the timestamp in the"
echo "decorator/constant, and make its up() idempotent since it will re-run under"
echo "the new name) instead of inflating the new command's timestamp."
echo -e "$FAILED"
echo "::error::Upgrade command timestamps must be real (close to now) and append-only within their version directory."
exit 1
fi

View File

@@ -0,0 +1,56 @@
name: Yarn Install
inputs:
node-version:
required: false
default: '24'
runs:
using: 'composite'
steps:
- name: Free disk space for install
if: runner.os == 'Linux'
shell: bash
run: |
# Default GitHub images ship large SDKs this repo does not use; removing
# them avoids ENOSPC when restoring or linking a full Yarn node_modules.
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
df -h
- name: Cache primary key builder
id: globals
shell: bash
run: |
echo "ACTION_SHELL=bash" >> "${GITHUB_OUTPUT}"
echo "CACHE_KEY_PREFIX=node_modules-cache-node-${{ inputs.node-version }}-${{ hashFiles('yarn.lock') }}" >> "${GITHUB_OUTPUT}"
echo 'PATH_TO_CACHE<<EOF' >> $GITHUB_OUTPUT
echo "node_modules" >> $GITHUB_OUTPUT
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
with:
node-version: ${{ inputs.node-version }}
- name: Restore node_modules
id: cache-node-modules
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
with:
key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
- name: Install Dependencies
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
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)
with:
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}

22
.github/crowdin-app.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
#
# Crowdin CLI configuration for App translations (twenty-front, twenty-server, twenty-emails)
# Project ID: 1
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
preserve_hierarchy: true
base_path: ..
files:
#
# Source files filter - PO files for Lingui
#
- source: packages/twenty-front/src/locales/en.po
#
# Translation files path
#
translation: '%original_path%/%locale%.po'
- source: packages/twenty-server/src/engine/core-modules/i18n/locales/en.po
translation: '%original_path%/%locale%.po'
- source: packages/twenty-emails/src/locales/en.po
translation: '%original_path%/%locale%.po'

47
.github/crowdin-docs.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
#
# Crowdin CLI configuration for Documentation translations
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
"project_id": 2
"preserve_hierarchy": true
"base_url": "https://twenty.api.crowdin.com"
"base_path": ".."
files: [
{
"source": "packages/twenty-docs/getting-started/**/*.mdx",
"translation": "packages/twenty-docs/l/%two_letters_code%/getting-started/**/%original_file_name%",
"type": "mdx_v2",
"exclude_code_blocks": true,
"excluded_front_matter_elements": ["icon"],
},
{
"source": "packages/twenty-docs/user-guide/**/*.mdx",
"translation": "packages/twenty-docs/l/%two_letters_code%/user-guide/**/%original_file_name%",
"type": "mdx_v2",
"exclude_code_blocks": true,
"excluded_front_matter_elements": ["icon"],
},
{
"source": "packages/twenty-docs/developers/**/*.mdx",
"translation": "packages/twenty-docs/l/%two_letters_code%/developers/**/%original_file_name%",
"type": "mdx_v2",
"exclude_code_blocks": true,
"excluded_front_matter_elements": ["icon"],
},
{
"source": "packages/twenty-docs/ui/**/*.mdx",
"translation": "packages/twenty-docs/l/%two_letters_code%/ui/**/%original_file_name%",
"type": "mdx_v2",
"exclude_code_blocks": true,
"excluded_front_matter_elements": ["icon"],
},
{
#
# Navigation labels template - translated into per-locale navigation.json
#
"source": "packages/twenty-docs/navigation/navigation.template.json",
"translation": "packages/twenty-docs/l/%two_letters_code%/navigation.json",
}
]

14
.github/crowdin-website.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
#
# Crowdin CLI configuration for Website translations (twenty-website)
# Project ID: 4
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
project_id: 4
preserve_hierarchy: true
base_url: 'https://twenty.api.crowdin.com'
base_path: ..
files:
- source: packages/twenty-website/src/locales/en.po
translation: '%original_path%/%two_letters_code%.po'

47
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
exclude-paths:
- "packages/twenty-apps/**"
schedule:
interval: "weekly"
# Version-update PRs disabled (limit 0). Security advisory updates are a
# separate channel and still open automatically — only routine version
# bumps are turned off to stop the constant PR churn.
open-pull-requests-limit: 0
versioning-strategy: "lockfile-only"
assignees:
- "mabdullahabaid"
ignore:
- dependency-name: "@graphql-yoga/nestjs"
- dependency-name: "@nestjs/graphql"
- dependency-name: "typeorm"
# chokidar is pinned to v3 via a resolution: v4+ dropped native macOS
# FSEvents and hits EMFILE on a repo this size (see #20316)
- dependency-name: "chokidar"
# Public Twenty apps are standalone packages (own lockfiles) excluded from the
# root entry above. Each app folder is updated weekly, grouped into a single PR.
- package-ecosystem: "npm"
directories:
- "/packages/twenty-apps/public/*"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
versioning-strategy: "lockfile-only"
groups:
twenty-apps-public:
patterns:
- "*"
# Internal Twenty apps, same treatment as the public apps above.
- package-ecosystem: "npm"
directories:
- "/packages/twenty-apps/internal/*"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
versioning-strategy: "lockfile-only"
groups:
twenty-apps-internal:
patterns:
- "*"

4
.github/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
template: |
## What's Changed
$CHANGES

86
.github/vale-styles/docs/Acronyms.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
extends: conditional
message: "'%s' has no definition."
level: warning
ignorecase: false
scope: paragraph
# Ensures that the existence of 'first' implies the existence of 'second'.
first: '\b([A-Z]{3,5})\b'
second: '(?:\b[A-Z][a-z]+ )+\(([A-Z]{3,5})\)'
# ... with the exception of these:
exceptions:
- API
- ASP
- CLI
- CPU
- CMS
- CRM
- CSS
- CSV
- DEBUG
- DOM
- DPI
- FAQ
- GCC
- GDB
- GET
- GPU
- GQL
- GTK
- GUI
- HTML
- HTTP
- HTTPS
- IDE
- JAR
- JSON
- JSX
- LESS
- LLDB
- NET
- NOTE
- NVDA
- ORM
- OSS
- PATH
- PDF
- PHP
- POST
- PII
- RAM
- REPL
- RSA
- SCM
- SCSS
- SDK
- SQL
- SSH
- SSL
- SVG
- TBD
- TCP
- TODO
- TSX
- URI
- URL
- USB
- UTF
- WSL
- XML
- XSS
- YAML
- ZIP
- AWS
- REST
- MDX
- NPM
- SMS
- SID
- VIP
- UPS
- RMA
- JSONB
- USD
- SASL
- SCRAM
- FIRST
- CORS

View File

@@ -0,0 +1,119 @@
extends: substitution
message: "Consider using '%s' instead of '%s'."
ignorecase: true
level: suggestion
action:
name: replace
swap:
"approximate(?:ly)?": about
abundance: plenty
accelerate: speed up
accentuate: stress
accompany: go with
accomplish: carry out|do
accorded: given
accordingly: so
accrue: add
accurate: right|exact
acquiesce: agree
acquire: get|buy
additional: more|extra
address: discuss
addressees: you
adjacent to: next to
adjustment: change
admissible: allowed
advantageous: helpful
advise: tell
aggregate: total
aircraft: plane
alleviate: ease
allocate: assign|divide
alternatively: or
alternatives: choices|options
ameliorate: improve
amend: change
anticipate: expect
apparent: clear|plain
ascertain: discover|find out
assistance: help
attain: meet
attempt: try
authorize: allow
belated: late
bestow: give
cease: stop|end
collaborate: work together
commence: begin
compensate: pay
component: part
comprise: form|include
concept: idea
concerning: about
confer: give|award
consequently: so
consolidate: merge
constitutes: forms
contains: has
convene: meet
demonstrate: show|prove
depart: leave
designate: choose
desire: want|wish
determine: decide|find
detrimental: bad|harmful
disclose: share|tell
discontinue: stop
disseminate: send|give
eliminate: end
elucidate: explain
employ: use
enclosed: inside|included
encounter: meet
endeavor: try
enumerate: count
equitable: fair
equivalent: equal
exclusively: only
expedite: hurry
facilitate: ease
females: women
finalize: complete|finish
frequently: often
identical: same
incorrect: wrong
indication: sign
initiate: start|begin
itemized: listed
jeopardize: risk
liaise: work with|partner with
maintain: keep|support
methodology: method
modify: change
monitor: check|watch
multiple: many
necessitate: cause
notify: tell
numerous: many
objective: aim|goal
obligate: bind|compel
optimum: best|most
permit: let
portion: part
possess: own
previous: earlier
previously: before
prioritize: rank
procure: buy
provide: give|offer
purchase: buy
relocate: move
solicit: request
state-of-the-art: latest
subsequent: later|next
substantial: large
sufficient: enough
terminate: end
transmit: send
utilization: use
utilize: use

View File

@@ -0,0 +1,49 @@
extends: substitution
message: "Use '%s' instead of '%s'."
level: suggestion
ignorecase: true
action:
name: replace
swap:
are not: aren't
cannot: can't
could not: couldn't
did not: didn't
do not: don't
does not: doesn't
has not: hasn't
have not: haven't
how is: how's
is not: isn't
'it is(?!\.)': it's
'it''s(?=\.)': it is
should not: shouldn't
'that is(?!\.)': that's
'that''s(?=\.)': that is
'they are(?!\.)': they're
'they''re(?=\.)': they are
was not: wasn't
'we are(?!\.)': we're
'we''re(?=\.)': we are
'we have(?!\.)': we've
'we''ve(?=\.)': we have
were not: weren't
'what is(?!\.)': what's
'what''s(?=\.)': what is
'when is(?!\.)': when's
'when''s(?=\.)': when is
'where is(?!\.)': where's
'where''s(?=\.)': where is
will not: won't

View File

@@ -0,0 +1,15 @@
extends: existence
message: "Avoid using first person (such as '%s')"
ignorecase: true
level: error
nonword: true
tokens:
- (?:^|\s)I\s
- (?:^|\s)I,\s
- \bI'd\b
- \bI'll\b
- \bI'm\b
- \bI've\b
- \bme\b
- \bmy\b
- \bmine\b

10
.github/vale-styles/docs/Foreign.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
extends: substitution
message: "Use '%s' instead of '%s'."
ignorecase: true
level: error
nonword: true
action:
name: replace
swap:
'\b(?:eg|e\.g\.)[\s,]': for example
'\b(?:ie|i\.e\.)[\s,]': that is

7
.github/vale-styles/docs/Gender.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
extends: existence
message: "Don't use '%s'."
level: error
ignorecase: true
tokens:
- he/she
- s/he

View File

@@ -0,0 +1,7 @@
extends: existence
message: "Capitalize '%s'."
nonword: true
level: warning
scope: heading
tokens:
- ':\s[a-z]'

View File

@@ -0,0 +1,13 @@
extends: existence
message: "Don't use end punctuation in headings."
link: https://medusajs.notion.site/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0#a5043fc5f8bc4b0b84f3ba494f817e42
nonword: true
level: warning
scope: heading
action:
name: edit
params:
- remove
- '.?!'
tokens:
- '[a-z][.?!](?:\s|$)'

7
.github/vale-styles/docs/Npm2Yarn.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
extends: existence
message: "Consider adding npm2yarn to ```bash"
level: warning
scope: raw
ignorecase: true
raw:
- '```bash[\n]+npm'

View File

@@ -0,0 +1,10 @@
extends: substitution
message: Use '%s' instead of '%s'
level: error
scope: raw
ignorecase: false
# swap maps tokens in form of bad: good
swap:
npm i: npm install
npm start: npm run start
npm build: npm run build

View File

@@ -0,0 +1,7 @@
extends: existence
message: "The -g option should be added at the end of the command"
level: error
scope: raw
ignorecase: true
raw:
- '(npm (i|install) (-g|--global)|npm (-g|--global) (i|install))'

13
.github/vale-styles/docs/Numbers.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
extends: existence
message: "Numbers must be spelled out"
level: warning
ignorecase: true
scope: paragraph
tokens:
- '.*[0-9]+\s(?!(AM|PM))+' #need to match words before to add exceptions
- '.*[0-9][.]'
exceptions:
- '[pP]ort [0-9]+'
- 'WSL2'
- '[vV]ersions? [0-9]'
- '[v.]+[0-9]+'

12
.github/vale-styles/docs/Ordinal.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
extends: existence
message: "Don't add -ly to an ordinal number."
level: suggestion
action:
name: edit
params:
- trim
- ly
tokens:
- firstly
- secondly
- thirdly

6
.github/vale-styles/docs/Our.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
extends: existence
message: "Avoid using our"
level: warning
ignorecase: true
tokens:
- ours?

View File

@@ -0,0 +1,8 @@
extends: existence
message: "Use the Oxford comma in '%s'."
link: https://docs.microsoft.com/en-us/style-guide/punctuation/commas
scope: sentence
level: suggestion
nonword: true
tokens:
- '(?:[^\s,]+,){1,} \w+ (?:and|or) \w+[.?!]'

183
.github/vale-styles/docs/Passive.yml vendored Normal file
View File

@@ -0,0 +1,183 @@
extends: existence
message: "'%s' looks like passive voice."
ignorecase: true
level: warning
raw:
- \b(am|are|were|being|is|been|was|be)\b\s*
tokens:
- '[\w]+ed'
- awoken
- beat
- become
- been
- begun
- bent
- beset
- bet
- bid
- bidden
- bitten
- bled
- blown
- born
- bought
- bound
- bred
- broadcast
- broken
- brought
- built
- burnt
- burst
- cast
- caught
- chosen
- clung
- come
- cost
- crept
- cut
- dealt
- dived
- done
- drawn
- dreamt
- driven
- drunk
- dug
- eaten
- fallen
- fed
- felt
- fit
- fled
- flown
- flung
- forbidden
- foregone
- forgiven
- forgotten
- forsaken
- fought
- found
- frozen
- given
- gone
- gotten
- ground
- grown
- heard
- held
- hidden
- hit
- hung
- hurt
- kept
- knelt
- knit
- known
- laid
- lain
- leapt
- learnt
- led
- left
- lent
- let
- lighted
- lost
- made
- meant
- met
- misspelt
- mistaken
- mown
- overcome
- overdone
- overtaken
- overthrown
- paid
- pled
- proven
- put
- quit
- read
- rid
- ridden
- risen
- run
- rung
- said
- sat
- sawn
- seen
- sent
- set
- sewn
- shaken
- shaven
- shed
- shod
- shone
- shorn
- shot
- shown
- shrunk
- shut
- slain
- slept
- slid
- slit
- slung
- smitten
- sold
- sought
- sown
- sped
- spent
- spilt
- spit
- split
- spoken
- spread
- sprung
- spun
- stolen
- stood
- stridden
- striven
- struck
- strung
- stuck
- stung
- stunk
- sung
- sunk
- swept
- swollen
- sworn
- swum
- swung
- taken
- taught
- thought
- thrived
- thrown
- thrust
- told
- torn
- trodden
- understood
- upheld
- upset
- wed
- wept
- withheld
- withstood
- woken
- won
- worn
- wound
- woven
- written
- wrung

View File

@@ -0,0 +1,6 @@
extends: existence
message: "Use a numeral plus the units."
nonword: true
level: error
tokens:
- '\b[a-zA-z]+\spercent\b'

View File

@@ -0,0 +1,6 @@
extends: occurrence
message: "Try to keep sentences short (< 30 words)."
scope: sentence
level: suggestion
max: 30
token: \b(\w+)\b

7
.github/vale-styles/docs/Spacing.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
extends: existence
message: "'%s' should have one space."
level: warning
nonword: true
tokens:
- '[a-z][.?!] {2,}[A-Z]'
- '[a-z][.?!][A-Z]'

8
.github/vale-styles/docs/Terms.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
extends: substitution
message: Consider using '%s' instead of '%s'
level: warning
scope: raw
ignorecase: false
# swap maps tokens in form of bad: good
swap:
e-commerce: ecommerce

View File

@@ -0,0 +1,7 @@
extends: existence
message: "Consider using typescript instead of tsx"
level: suggestion
scope: raw
ignorecase: true
raw:
- '```tsx'

9
.github/vale-styles/docs/We.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
extends: existence
message: "Avoid using first-person plural like '%s'."
level: warning
ignorecase: true
tokens:
- we
- we'(?:ve|re)
- us
- let's

121
.github/vale-styles/docs/Wordiness.yml vendored Normal file
View File

@@ -0,0 +1,121 @@
extends: substitution
message: "Consider using '%s' instead of '%s'."
ignorecase: true
level: suggestion
action:
name: replace
swap:
(?:give|gave) rise to: lead to
(?:previous|prior) to: before
a (?:large)? majority of: most
a (?:large)? number of: many
a myriad of: myriad
adversely impact: hurt
all across: across
all of a sudden: suddenly
all of these: these
all of: all
all-time record: record
almost all: most
almost never: seldom
along the lines of: similar to
an adequate number of: enough
an appreciable number of: many
an estimated: about
any and all: all
are in agreement: agree
as a matter of fact: in fact
as a means of: to
as a result of: because of
as of yet: yet
as per: per
at a later date: later
at all times: always
at the present time: now
at this point in time: at this point
based in large part on: based on
based on the fact that: because
basic necessity: necessity
because of the fact that: because
came to a realization: realized
came to an abrupt end: ended abruptly
carry out an evaluation of: evaluate
close down: close
closed down: closed
complete stranger: stranger
completely separate: separate
concerning the matter of: regarding
conduct a review of: review
conduct an investigation: investigate
conduct experiments: experiment
continue on: continue
despite the fact that: although
disappear from sight: disappear
drag and drop: drag
drag-and-drop: drag
doomed to fail: doomed
due to the fact that: because
during the period of: during
during the time that: while
emergency situation: emergency
except when: unless
excessive number: too many
extend an invitation: invite
fall down: fall
fell down: fell
for the duration of: during
gather together: gather
has the ability to: can
has the capacity to: can
has the opportunity to: could
hold a meeting: meet
if this is not the case: if not
in a careful manner: carefully
in a thoughtful manner: thoughtfully
in a timely manner: timely
in an effort to: to
in between: between
in lieu of: instead of
in many cases: often
in most cases: usually
in order to: to
in some cases: sometimes
in spite of the fact that: although
in spite of: despite
in the (?:very)? near future: soon
in the event that: if
in the neighborhood of: roughly
in the vicinity of: close to
it would appear that: apparently
lift up: lift
made reference to: referred to
make reference to: refer to
mix together: mix
none at all: none
not in a position to: unable
not possible: impossible
of major importance: important
perform an assessment of: assess
pertaining to: about
place an order: order
plays a key role in: is essential to
present time: now
readily apparent: apparent
some of the: some
span across: span
subsequent to: after
successfully complete: complete
sufficient number (?:of)?: enough
take action: act
take into account: consider
the question as to whether: whether
there is no doubt but that: doubtless
this day and age: this age
this is a subject that: this subject
time (?:frame|period): time
under the provisions of: under
until such time as: until
used for fuel purposes: used for fuel
whether or not: whether
with regard to: regarding
with the exception of: except for

View File

@@ -0,0 +1,9 @@
extends: metric
message: "Try to keep the Flesch-Kincaid grade level (%s) below 8."
link: https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests
formula: |
(0.39 * (words / sentences)) + (11.8 * (syllables / words)) - 15.59
condition: "> 8"
level: suggestion

View File

@@ -0,0 +1,14 @@
kanban
kanbans
Kanban
Kanbans
automation
automations
Automation
Automations
S3
PNGs
PNG
JPEG
JPEGs
US

View File

View File

@@ -0,0 +1,702 @@
extends: existence
message: "Try to avoid using clichés like '%s'."
ignorecase: true
level: warning
tokens:
- a chip off the old block
- a clean slate
- a dark and stormy night
- a far cry
- a fine kettle of fish
- a loose cannon
- a penny saved is a penny earned
- a tough row to hoe
- a word to the wise
- ace in the hole
- acid test
- add insult to injury
- against all odds
- air your dirty laundry
- all fun and games
- all in a day's work
- all talk, no action
- all thumbs
- all your eggs in one basket
- all's fair in love and war
- all's well that ends well
- almighty dollar
- American as apple pie
- an axe to grind
- another day, another dollar
- armed to the teeth
- as luck would have it
- as old as time
- as the crow flies
- at loose ends
- at my wits end
- avoid like the plague
- babe in the woods
- back against the wall
- back in the saddle
- back to square one
- back to the drawing board
- bad to the bone
- badge of honor
- bald faced liar
- ballpark figure
- banging your head against a brick wall
- baptism by fire
- barking up the wrong tree
- bat out of hell
- be all and end all
- beat a dead horse
- beat around the bush
- been there, done that
- beggars can't be choosers
- behind the eight ball
- bend over backwards
- benefit of the doubt
- bent out of shape
- best thing since sliced bread
- bet your bottom dollar
- better half
- better late than never
- better mousetrap
- better safe than sorry
- between a rock and a hard place
- beyond the pale
- bide your time
- big as life
- big cheese
- big fish in a small pond
- big man on campus
- bigger they are the harder they fall
- bird in the hand
- bird's eye view
- birds and the bees
- birds of a feather flock together
- bit the hand that feeds you
- bite the bullet
- bite the dust
- bitten off more than he can chew
- black as coal
- black as pitch
- black as the ace of spades
- blast from the past
- bleeding heart
- blessing in disguise
- blind ambition
- blind as a bat
- blind leading the blind
- blood is thicker than water
- blood sweat and tears
- blow off steam
- blow your own horn
- blushing bride
- boils down to
- bolt from the blue
- bone to pick
- bored stiff
- bored to tears
- bottomless pit
- boys will be boys
- bright and early
- brings home the bacon
- broad across the beam
- broken record
- brought back to reality
- bull by the horns
- bull in a china shop
- burn the midnight oil
- burning question
- burning the candle at both ends
- burst your bubble
- bury the hatchet
- busy as a bee
- by hook or by crook
- call a spade a spade
- called onto the carpet
- calm before the storm
- can of worms
- can't cut the mustard
- can't hold a candle to
- case of mistaken identity
- cat got your tongue
- cat's meow
- caught in the crossfire
- caught red-handed
- checkered past
- chomping at the bit
- cleanliness is next to godliness
- clear as a bell
- clear as mud
- close to the vest
- cock and bull story
- cold shoulder
- come hell or high water
- cool as a cucumber
- cool, calm, and collected
- cost a king's ransom
- count your blessings
- crack of dawn
- crash course
- creature comforts
- cross that bridge when you come to it
- crushing blow
- cry like a baby
- cry me a river
- cry over spilt milk
- crystal clear
- curiosity killed the cat
- cut and dried
- cut through the red tape
- cut to the chase
- cute as a bugs ear
- cute as a button
- cute as a puppy
- cuts to the quick
- dark before the dawn
- day in, day out
- dead as a doornail
- devil is in the details
- dime a dozen
- divide and conquer
- dog and pony show
- dog days
- dog eat dog
- dog tired
- don't burn your bridges
- don't count your chickens
- don't look a gift horse in the mouth
- don't rock the boat
- don't step on anyone's toes
- don't take any wooden nickels
- down and out
- down at the heels
- down in the dumps
- down the hatch
- down to earth
- draw the line
- dressed to kill
- dressed to the nines
- drives me up the wall
- dull as dishwater
- dyed in the wool
- eagle eye
- ear to the ground
- early bird catches the worm
- easier said than done
- easy as pie
- eat your heart out
- eat your words
- eleventh hour
- even the playing field
- every dog has its day
- every fiber of my being
- everything but the kitchen sink
- eye for an eye
- face the music
- facts of life
- fair weather friend
- fall by the wayside
- fan the flames
- feast or famine
- feather your nest
- feathered friends
- few and far between
- fifteen minutes of fame
- filthy vermin
- fine kettle of fish
- fish out of water
- fishing for a compliment
- fit as a fiddle
- fit the bill
- fit to be tied
- flash in the pan
- flat as a pancake
- flip your lid
- flog a dead horse
- fly by night
- fly the coop
- follow your heart
- for all intents and purposes
- for the birds
- for what it's worth
- force of nature
- force to be reckoned with
- forgive and forget
- fox in the henhouse
- free and easy
- free as a bird
- fresh as a daisy
- full steam ahead
- fun in the sun
- garbage in, garbage out
- gentle as a lamb
- get a kick out of
- get a leg up
- get down and dirty
- get the lead out
- get to the bottom of
- get your feet wet
- gets my goat
- gilding the lily
- give and take
- go against the grain
- go at it tooth and nail
- go for broke
- go him one better
- go the extra mile
- go with the flow
- goes without saying
- good as gold
- good deed for the day
- good things come to those who wait
- good time was had by all
- good times were had by all
- greased lightning
- greek to me
- green thumb
- green-eyed monster
- grist for the mill
- growing like a weed
- hair of the dog
- hand to mouth
- happy as a clam
- happy as a lark
- hasn't a clue
- have a nice day
- have high hopes
- have the last laugh
- haven't got a row to hoe
- head honcho
- head over heels
- hear a pin drop
- heard it through the grapevine
- heart's content
- heavy as lead
- hem and haw
- high and dry
- high and mighty
- high as a kite
- hit paydirt
- hold your head up high
- hold your horses
- hold your own
- hold your tongue
- honest as the day is long
- horns of a dilemma
- horse of a different color
- hot under the collar
- hour of need
- I beg to differ
- icing on the cake
- if the shoe fits
- if the shoe were on the other foot
- in a jam
- in a jiffy
- in a nutshell
- in a pig's eye
- in a pinch
- in a word
- in hot water
- in the gutter
- in the nick of time
- in the thick of it
- in your dreams
- it ain't over till the fat lady sings
- it goes without saying
- it takes all kinds
- it takes one to know one
- it's a small world
- it's only a matter of time
- ivory tower
- Jack of all trades
- jockey for position
- jog your memory
- joined at the hip
- judge a book by its cover
- jump down your throat
- jump in with both feet
- jump on the bandwagon
- jump the gun
- jump to conclusions
- just a hop, skip, and a jump
- just the ticket
- justice is blind
- keep a stiff upper lip
- keep an eye on
- keep it simple, stupid
- keep the home fires burning
- keep up with the Joneses
- keep your chin up
- keep your fingers crossed
- kick the bucket
- kick up your heels
- kick your feet up
- kid in a candy store
- kill two birds with one stone
- kiss of death
- knock it out of the park
- knock on wood
- knock your socks off
- know him from Adam
- know the ropes
- know the score
- knuckle down
- knuckle sandwich
- knuckle under
- labor of love
- ladder of success
- land on your feet
- lap of luxury
- last but not least
- last hurrah
- last-ditch effort
- law of the jungle
- law of the land
- lay down the law
- leaps and bounds
- let sleeping dogs lie
- let the cat out of the bag
- let the good times roll
- let your hair down
- let's talk turkey
- letter perfect
- lick your wounds
- lies like a rug
- life's a bitch
- life's a grind
- light at the end of the tunnel
- lighter than a feather
- lighter than air
- like clockwork
- like father like son
- like taking candy from a baby
- like there's no tomorrow
- lion's share
- live and learn
- live and let live
- long and short of it
- long lost love
- look before you leap
- look down your nose
- look what the cat dragged in
- looking a gift horse in the mouth
- looks like death warmed over
- loose cannon
- lose your head
- lose your temper
- loud as a horn
- lounge lizard
- loved and lost
- low man on the totem pole
- luck of the draw
- luck of the Irish
- make hay while the sun shines
- make money hand over fist
- make my day
- make the best of a bad situation
- make the best of it
- make your blood boil
- man of few words
- man's best friend
- mark my words
- meaningful dialogue
- missed the boat on that one
- moment in the sun
- moment of glory
- moment of truth
- money to burn
- more power to you
- more than one way to skin a cat
- movers and shakers
- moving experience
- naked as a jaybird
- naked truth
- neat as a pin
- needle in a haystack
- needless to say
- neither here nor there
- never look back
- never say never
- nip and tuck
- nip it in the bud
- no guts, no glory
- no love lost
- no pain, no gain
- no skin off my back
- no stone unturned
- no time like the present
- no use crying over spilled milk
- nose to the grindstone
- not a hope in hell
- not a minute's peace
- not in my backyard
- not playing with a full deck
- not the end of the world
- not written in stone
- nothing to sneeze at
- nothing ventured nothing gained
- now we're cooking
- off the top of my head
- off the wagon
- off the wall
- old hat
- older and wiser
- older than dirt
- older than Methuselah
- on a roll
- on cloud nine
- on pins and needles
- on the bandwagon
- on the money
- on the nose
- on the rocks
- on the spot
- on the tip of my tongue
- on the wagon
- on thin ice
- once bitten, twice shy
- one bad apple doesn't spoil the bushel
- one born every minute
- one brick short
- one foot in the grave
- one in a million
- one red cent
- only game in town
- open a can of worms
- open and shut case
- open the flood gates
- opportunity doesn't knock twice
- out of pocket
- out of sight, out of mind
- out of the frying pan into the fire
- out of the woods
- out on a limb
- over a barrel
- over the hump
- pain and suffering
- pain in the
- panic button
- par for the course
- part and parcel
- party pooper
- pass the buck
- patience is a virtue
- pay through the nose
- penny pincher
- perfect storm
- pig in a poke
- pile it on
- pillar of the community
- pin your hopes on
- pitter patter of little feet
- plain as day
- plain as the nose on your face
- play by the rules
- play your cards right
- playing the field
- playing with fire
- pleased as punch
- plenty of fish in the sea
- point with pride
- poor as a church mouse
- pot calling the kettle black
- pretty as a picture
- pull a fast one
- pull your punches
- pulling your leg
- pure as the driven snow
- put it in a nutshell
- put one over on you
- put the cart before the horse
- put the pedal to the metal
- put your best foot forward
- put your foot down
- quick as a bunny
- quick as a lick
- quick as a wink
- quick as lightning
- quiet as a dormouse
- rags to riches
- raining buckets
- raining cats and dogs
- rank and file
- rat race
- reap what you sow
- red as a beet
- red herring
- reinvent the wheel
- rich and famous
- rings a bell
- ripe old age
- ripped me off
- rise and shine
- road to hell is paved with good intentions
- rob Peter to pay Paul
- roll over in the grave
- rub the wrong way
- ruled the roost
- running in circles
- sad but true
- sadder but wiser
- salt of the earth
- scared stiff
- scared to death
- sealed with a kiss
- second to none
- see eye to eye
- seen the light
- seize the day
- set the record straight
- set the world on fire
- set your teeth on edge
- sharp as a tack
- shoot for the moon
- shoot the breeze
- shot in the dark
- shoulder to the wheel
- sick as a dog
- sigh of relief
- signed, sealed, and delivered
- sink or swim
- six of one, half a dozen of another
- skating on thin ice
- slept like a log
- slinging mud
- slippery as an eel
- slow as molasses
- smart as a whip
- smooth as a baby's bottom
- sneaking suspicion
- snug as a bug in a rug
- sow wild oats
- spare the rod, spoil the child
- speak of the devil
- spilled the beans
- spinning your wheels
- spitting image of
- spoke with relish
- spread like wildfire
- spring to life
- squeaky wheel gets the grease
- stands out like a sore thumb
- start from scratch
- stick in the mud
- still waters run deep
- stitch in time
- stop and smell the roses
- straight as an arrow
- straw that broke the camel's back
- strong as an ox
- stubborn as a mule
- stuff that dreams are made of
- stuffed shirt
- sweating blood
- sweating bullets
- take a load off
- take one for the team
- take the bait
- take the bull by the horns
- take the plunge
- takes one to know one
- takes two to tango
- the more the merrier
- the real deal
- the real McCoy
- the red carpet treatment
- the same old story
- there is no accounting for taste
- thick as a brick
- thick as thieves
- thin as a rail
- think outside of the box
- third time's the charm
- this day and age
- this hurts me worse than it hurts you
- this point in time
- three sheets to the wind
- through thick and thin
- throw in the towel
- tie one on
- tighter than a drum
- time and time again
- time is of the essence
- tip of the iceberg
- tired but happy
- to coin a phrase
- to each his own
- to make a long story short
- to the best of my knowledge
- toe the line
- tongue in cheek
- too good to be true
- too hot to handle
- too numerous to mention
- touch with a ten foot pole
- tough as nails
- trial and error
- trials and tribulations
- tried and true
- trip down memory lane
- twist of fate
- two cents worth
- two peas in a pod
- ugly as sin
- under the counter
- under the gun
- under the same roof
- under the weather
- until the cows come home
- unvarnished truth
- up the creek
- uphill battle
- upper crust
- upset the applecart
- vain attempt
- vain effort
- vanquish the enemy
- vested interest
- waiting for the other shoe to drop
- wakeup call
- warm welcome
- watch your p's and q's
- watch your tongue
- watching the clock
- water under the bridge
- weather the storm
- weed them out
- week of Sundays
- went belly up
- wet behind the ears
- what goes around comes around
- what you see is what you get
- when it rains, it pours
- when push comes to shove
- when the cat's away
- when the going gets tough, the tough get going
- white as a sheet
- whole ball of wax
- whole hog
- whole nine yards
- wild goose chase
- will wonders never cease?
- wisdom of the ages
- wise as an owl
- wolf at the door
- words fail me
- work like a dog
- world weary
- worst nightmare
- worth its weight in gold
- wrong side of the bed
- yanking your chain
- yappy as a dog
- years young
- you are what you eat
- you can run but you can't hide
- you only live once
- you're the boss
- young and foolish
- young and vibrant

View File

@@ -0,0 +1,32 @@
extends: existence
message: "Try to avoid using '%s'."
ignorecase: true
level: suggestion
tokens:
- am
- are
- aren't
- be
- been
- being
- he's
- here's
- here's
- how's
- i'm
- is
- isn't
- it's
- she's
- that's
- there's
- they're
- was
- wasn't
- we're
- were
- weren't
- what's
- where's
- who's
- you're

View File

@@ -0,0 +1,11 @@
extends: repetition
message: "'%s' is repeated!"
level: warning
alpha: true
action:
name: edit
params:
- truncate
- " "
tokens:
- '[^\s]+'

View File

@@ -0,0 +1,183 @@
extends: existence
message: "'%s' may be passive voice. Use active voice if you can."
ignorecase: true
level: warning
raw:
- \b(am|are|were|being|is|been|was|be)\b\s*
tokens:
- '[\w]+ed'
- awoken
- beat
- become
- been
- begun
- bent
- beset
- bet
- bid
- bidden
- bitten
- bled
- blown
- born
- bought
- bound
- bred
- broadcast
- broken
- brought
- built
- burnt
- burst
- cast
- caught
- chosen
- clung
- come
- cost
- crept
- cut
- dealt
- dived
- done
- drawn
- dreamt
- driven
- drunk
- dug
- eaten
- fallen
- fed
- felt
- fit
- fled
- flown
- flung
- forbidden
- foregone
- forgiven
- forgotten
- forsaken
- fought
- found
- frozen
- given
- gone
- gotten
- ground
- grown
- heard
- held
- hidden
- hit
- hung
- hurt
- kept
- knelt
- knit
- known
- laid
- lain
- leapt
- learnt
- led
- left
- lent
- let
- lighted
- lost
- made
- meant
- met
- misspelt
- mistaken
- mown
- overcome
- overdone
- overtaken
- overthrown
- paid
- pled
- proven
- put
- quit
- read
- rid
- ridden
- risen
- run
- rung
- said
- sat
- sawn
- seen
- sent
- set
- sewn
- shaken
- shaven
- shed
- shod
- shone
- shorn
- shot
- shown
- shrunk
- shut
- slain
- slept
- slid
- slit
- slung
- smitten
- sold
- sought
- sown
- sped
- spent
- spilt
- spit
- split
- spoken
- spread
- sprung
- spun
- stolen
- stood
- stridden
- striven
- struck
- strung
- stuck
- stung
- stunk
- sung
- sunk
- swept
- swollen
- sworn
- swum
- swung
- taken
- taught
- thought
- thrived
- thrown
- thrust
- told
- torn
- trodden
- understood
- upheld
- upset
- wed
- wept
- withheld
- withstood
- woken
- won
- worn
- wound
- woven
- written
- wrung

View File

@@ -0,0 +1,27 @@
Based on [write-good](https://github.com/btford/write-good).
> Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too.
```
The MIT License (MIT)
Copyright (c) 2014 Brian Ford
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

5
.github/vale-styles/write-good/So.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
extends: existence
message: "Don't start a sentence with '%s'."
level: error
raw:
- '(?:[;-]\s)so[\s,]|\bSo[\s,]'

View File

@@ -0,0 +1,6 @@
extends: existence
message: "Don't start a sentence with '%s'."
ignorecase: false
level: error
raw:
- '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b'

View File

@@ -0,0 +1,219 @@
extends: existence
message: "'%s' is too wordy."
ignorecase: true
level: warning
tokens:
- a number of
- abundance
- accede to
- accelerate
- accentuate
- accompany
- accomplish
- accorded
- accrue
- acquiesce
- acquire
- additional
- adjacent to
- adjustment
- admissible
- advantageous
- adversely impact
- advise
- aforementioned
- aggregate
- aircraft
- all of
- all things considered
- alleviate
- allocate
- along the lines of
- already existing
- alternatively
- amazing
- ameliorate
- anticipate
- apparent
- appreciable
- as a matter of fact
- as a means of
- as far as I'm concerned
- as of yet
- as to
- as yet
- ascertain
- assistance
- at the present time
- at this time
- attain
- attributable to
- authorize
- because of the fact that
- belated
- benefit from
- bestow
- by means of
- by virtue of
- by virtue of the fact that
- cease
- close proximity
- commence
- comply with
- concerning
- consequently
- consolidate
- constitutes
- demonstrate
- depart
- designate
- discontinue
- due to the fact that
- each and every
- economical
- eliminate
- elucidate
- employ
- endeavor
- enumerate
- equitable
- equivalent
- evaluate
- evidenced
- exclusively
- expedite
- expend
- facilitate
- factual evidence
- feasible
- finalize
- first and foremost
- for all intents and purposes
- for the most part
- for the purpose of
- forfeit
- formulate
- have a tendency to
- honest truth
- however
- if and when
- impacted
- implement
- in a manner of speaking
- in a timely manner
- in a very real sense
- in accordance with
- in addition
- in all likelihood
- in an effort to
- in between
- in excess of
- in lieu of
- in light of the fact that
- in many cases
- in my opinion
- in order to
- in regard to
- in some instances
- in terms of
- in the case of
- in the event that
- in the final analysis
- in the nature of
- in the near future
- in the process of
- inception
- incumbent upon
- indicate
- indication
- initiate
- irregardless
- is applicable to
- is authorized to
- is responsible for
- it is
- it is essential
- it seems that
- it was
- magnitude
- maximum
- methodology
- minimize
- minimum
- modify
- monitor
- multiple
- necessitate
- nevertheless
- not certain
- not many
- not often
- not unless
- not unlike
- notwithstanding
- null and void
- numerous
- objective
- obligate
- obtain
- on the contrary
- on the other hand
- one particular
- optimum
- overall
- owing to the fact that
- participate
- particulars
- pass away
- pertaining to
- point in time
- portion
- possess
- preclude
- previously
- prior to
- prioritize
- procure
- proficiency
- provided that
- purchase
- put simply
- readily apparent
- refer back
- regarding
- relocate
- remainder
- remuneration
- requirement
- reside
- residence
- retain
- satisfy
- shall
- should you wish
- similar to
- solicit
- span across
- strategize
- subsequent
- substantial
- successfully complete
- sufficient
- terminate
- the month of
- the point I am trying to make
- therefore
- time period
- took advantage of
- transmit
- transpire
- until such time as
- utilization
- utilize
- validate
- various different
- what I mean to say is
- whether or not
- with respect to
- with the exception of
- witnessed

View File

@@ -0,0 +1,206 @@
extends: existence
message: "'%s' is a weasel word!"
ignorecase: true
level: warning
tokens:
- absolutely
- accidentally
- additionally
- allegedly
- alternatively
- angrily
- anxiously
- approximately
- awkwardly
- badly
- barely
- beautifully
- blindly
- boldly
- bravely
- brightly
- briskly
- bristly
- bubbly
- busily
- calmly
- carefully
- carelessly
- cautiously
- cheerfully
- clearly
- closely
- coldly
- completely
- consequently
- correctly
- courageously
- crinkly
- cruelly
- crumbly
- cuddly
- currently
- daily
- daringly
- deadly
- definitely
- deliberately
- doubtfully
- dumbly
- eagerly
- early
- easily
- elegantly
- enormously
- enthusiastically
- equally
- especially
- eventually
- exactly
- exceedingly
- exclusively
- extremely
- fairly
- faithfully
- fatally
- fiercely
- finally
- fondly
- few
- foolishly
- fortunately
- frankly
- frantically
- generously
- gently
- giggly
- gladly
- gracefully
- greedily
- happily
- hardly
- hastily
- healthily
- heartily
- helpfully
- honestly
- hourly
- hungrily
- hurriedly
- immediately
- impatiently
- inadequately
- ingeniously
- innocently
- inquisitively
- interestingly
- irritably
- jiggly
- joyously
- justly
- kindly
- largely
- lately
- lazily
- likely
- literally
- lonely
- loosely
- loudly
- loudly
- luckily
- madly
- many
- mentally
- mildly
- monthly
- mortally
- mostly
- mysteriously
- neatly
- nervously
- nightly
- noisily
- normally
- obediently
- occasionally
- openly
- painfully
- particularly
- patiently
- perfectly
- politely
- poorly
- powerfully
- presumably
- previously
- promptly
- punctually
- quarterly
- quickly
- quietly
- rapidly
- rarely
- really
- recently
- recklessly
- regularly
- remarkably
- relatively
- reluctantly
- repeatedly
- rightfully
- roughly
- rudely
- sadly
- safely
- selfishly
- sensibly
- seriously
- sharply
- shortly
- shyly
- significantly
- silently
- simply
- sleepily
- slowly
- smartly
- smelly
- smoothly
- softly
- solemnly
- sparkly
- speedily
- stealthily
- sternly
- stupidly
- substantially
- successfully
- suddenly
- surprisingly
- suspiciously
- swiftly
- tenderly
- tensely
- thoughtfully
- tightly
- timely
- truthfully
- unexpectedly
- unfortunately
- usually
- very
- victoriously
- violently
- vivaciously
- warmly
- waverly
- weakly
- wearily
- weekly
- wildly
- wisely
- worldly
- wrinkly
- yearly

View File

@@ -0,0 +1,4 @@
{
"feed": "https://github.com/errata-ai/write-good/releases.atom",
"vale_version": ">=1.0.0"
}

17
.github/vale.ini vendored Normal file
View File

@@ -0,0 +1,17 @@
StylesPath = .github/vale-styles
MinAlertLevel = suggestion
Packages = write-good
[*.{md,mdx}]
BasedOnStyles = Vale, write-good, docs
Vale.Spelling=warning
write-good.E-Prime=No
write-good.So=No
write-good.ThereIs=No
Vale.Terms=No
[formats]
mdx = md

27
.github/verdaccio-config.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
storage: /tmp/verdaccio-storage
# Published tarballs are base64-encoded in the registry request body.
max_body_size: 50mb
auth:
htpasswd:
file: /tmp/verdaccio-htpasswd
max_users: 100
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
'twenty-sdk':
access: $all
publish: $all
'twenty-client-sdk':
access: $all
publish: $all
'twenty-ui':
access: $all
publish: $all
'create-twenty-app':
access: $all
publish: $all
'**':
access: $all
proxy: npmjs
log: { type: stdout, format: pretty, level: warn }

View File

@@ -0,0 +1,85 @@
name: App Prod-Parity E2E Dispatch
on:
push:
branches: [main]
pull_request:
types: [labeled, synchronize, opened, reopened]
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
dispatch:
# main push + manual always run; PR events only when the label is present
# and the PR comes from a branch in this repo (forks are excluded, since they
# have no access to secrets or a writable token for the dispatch).
if: >-
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
contains(github.event.pull_request.labels.*.name, 'run-e2e-apps-prod-parity-tests'))
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
statuses: write
steps:
- name: Resolve target SHA
id: sha
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
EVENT_SHA: ${{ github.sha }}
run: |
if [ -n "$PR_HEAD_SHA" ]; then
echo "head_sha=$PR_HEAD_SHA" >> "$GITHUB_OUTPUT"
else
echo "head_sha=$EVENT_SHA" >> "$GITHUB_OUTPUT"
fi
- name: Post pending status
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ steps.sha.outputs.head_sha }}
run: |
gh api "repos/$REPO/statuses/$HEAD_SHA" \
-f state=pending \
-f context=app-prod-parity-e2e \
-f description="Dispatching prod-parity e2e to twenty-factory"
- name: Mint twenty-factory dispatch token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.TWENTY_WORKFLOW_DISPATCHER_CLIENT_ID }}
private-key: ${{ secrets.TWENTY_WORKFLOW_DISPATCHER_PRIVATE_KEY }}
owner: twentyhq
repositories: twenty-factory
permission-actions: write
- name: Dispatch to twenty-factory
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
HEAD_SHA: ${{ steps.sha.outputs.head_sha }}
run: |
gh workflow run app-prod-parity-e2e.yaml \
--repo twentyhq/twenty-factory \
--ref main \
-f head_sha="$HEAD_SHA"
- name: Post dispatch-failed status
if: failure()
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ steps.sha.outputs.head_sha }}
run: |
gh api "repos/$REPO/statuses/$HEAD_SHA" \
-f state=failure \
-f context=app-prod-parity-e2e \
-f description="Failed to dispatch prod-parity e2e to twenty-factory"

37
.github/workflows/cd-deploy-main.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: CD deploy main
permissions:
contents: read
on:
push:
branches:
- main
jobs:
website-changed-files:
uses: ./.github/workflows/changed-files.yaml
with:
files: packages/twenty-website/**
deploy-main:
needs: website-changed-files
timeout-minutes: 3
runs-on: ubuntu-latest
steps:
- name: Mint twenty-infra dispatch token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.TWENTY_WORKFLOW_DISPATCHER_CLIENT_ID }}
private-key: ${{ secrets.TWENTY_WORKFLOW_DISPATCHER_PRIVATE_KEY }}
owner: twentyhq
repositories: twenty-infra
permission-actions: write
- name: Trigger twenty-infra deploy
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEPLOY_WEBSITE: ${{ needs.website-changed-files.outputs.any_changed }}
run: |
gh workflow run auto-deploy-main.yaml --repo twentyhq/twenty-infra --ref main -f deploy_website="$DEPLOY_WEBSITE"

35
.github/workflows/cd-deploy-tag.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: CD deploy tag
permissions:
contents: read
on:
push:
tags:
- 'twenty/v*'
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
jobs:
dispatch-tag:
timeout-minutes: 3
runs-on: ubuntu-latest
steps:
- name: Mint twenty-infra dispatch token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.TWENTY_WORKFLOW_DISPATCHER_CLIENT_ID }}
private-key: ${{ secrets.TWENTY_WORKFLOW_DISPATCHER_PRIVATE_KEY }}
owner: twentyhq
repositories: twenty-infra
permission-actions: write
- name: Trigger staging CD
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REF_NAME: ${{ github.ref_name }}
run: |
gh workflow run staging-ci.yaml --repo twentyhq/twenty-infra --ref main -f tag="$REF_NAME"

75
.github/workflows/cd-storybook-ui.yaml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: CD Storybook UI
on:
push:
branches: [main]
tags: ['sdk/v*', 'ui/v*']
paths:
- .github/workflows/cd-storybook-ui.yaml
- .github/actions/yarn-install/**
- package.json
- yarn.lock
- nx.json
- tsconfig.base.json
- packages/twenty-ui/**
- packages/twenty-shared/**
- packages/twenty-docs/**
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
jobs:
build:
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
persist-credentials: false
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build public Storybook
run: npx nx storybook:build twenty-ui --configuration=docs
- name: Check embedded stories against the build
run: npx tsx packages/twenty-docs/scripts/check-ui-story-embeds.mts packages/twenty-ui/storybook-static/index.json
- name: Upload public Storybook
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: storybook-twenty-ui-public
path: packages/twenty-ui/storybook-static
if-no-files-found: error
retention-days: 7
dispatch:
needs: build
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Mint twenty-factory dispatch token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
client-id: ${{ vars.TWENTY_WORKFLOW_DISPATCHER_CLIENT_ID }}
private-key: ${{ secrets.TWENTY_WORKFLOW_DISPATCHER_PRIVATE_KEY }}
owner: twentyhq
repositories: twenty-factory
permission-actions: write
- name: Request Storybook deployment
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
SOURCE_RUN_ID: ${{ github.run_id }}
run: |
gh workflow run deploy-storybook-ui.yaml --repo twentyhq/twenty-factory --ref main \
-f source_run_id="$SOURCE_RUN_ID"

31
.github/workflows/changed-files.yaml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Changed files reusable workflow
on:
workflow_call:
inputs:
files:
required: true
type: string
outputs:
any_changed:
value: ${{ jobs.changed-files.outputs.any_changed }}
permissions:
contents: read
jobs:
changed-files:
timeout-minutes: 5
runs-on: ubuntu-latest
outputs:
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
with:
fetch-depth: 10
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9
with:
files: ${{ inputs.files }}

View File

@@ -0,0 +1,199 @@
name: CI Agent Skills Drift
# When a PR changes the app-development surface (twenty-sdk, create-twenty-app,
# twenty-client-sdk, or the shared manifest types), an agent checks whether the
# canonical skill content under packages/twenty-agent-skills is impacted and,
# only when it is, posts a single sticky comment with its findings.
#
# This is the skills counterpart to ci-app-docs-drift.yaml: the same source
# changes that stale the docs also stale the guidance agents follow.
on:
pull_request:
permissions:
contents: read
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-sdk/**
packages/create-twenty-app/**
packages/twenty-client-sdk/**
packages/twenty-shared/src/application/**
packages/twenty-shared/src/types/**
agent-skills-drift-check:
needs: changed-files-check
# Secrets are unavailable on fork PRs; skip there and on bot PRs.
if: >-
needs.changed-files-check.outputs.any_changed == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.type != 'Bot'
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Run agent skills drift agent
id: drift-agent
uses: anthropics/claude-code-action@ac7e24bf2938964b8ab203e417a2773802392ddd # v1.0.146
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: |
--max-turns 60 --json-schema '{"type":"object","properties":{"hasDrift":{"type":"boolean"},"summary":{"type":"string"}},"required":["hasDrift","summary"]}' --allowedTools "Bash(git diff *),Bash(git log *),Bash(git show *),Bash(grep *),Bash(cat *),Bash(ls *),Bash(find *),Bash(head *),Bash(tail *),Bash(gh pr comment *)"
prompt: |
You are the agent-skills drift checker for the twenty repository.
A pull request changed files in the app-development platform. Your job is to
decide whether the skill guidance under `packages/twenty-agent-skills/`
is impacted, and report your findings as ONE pull request comment.
These skills tell coding agents how to build Twenty apps. When they go stale
an agent confidently runs a command that no longer exists, so wrong guidance
is worse here than a missing doc page.
## Canonical source only
`packages/twenty-agent-skills/skills/` and
`packages/twenty-agent-skills/references/` are the canonical content.
`packages/twenty-agent-skills/dist/` is generated for distribution and is
not committed to main. Report the canonical file that needs an edit.
## Step 1 — Understand the change
The PR branch is checked out with full history; the base branch is
`origin/${{ github.base_ref }}`. Inspect what the PR changes however you
see fit. Only these kinds of changes are skill-relevant:
- CLI commands, subcommands, flags, defaults, or help text
(`packages/twenty-sdk/src/cli/commands/**`)
- `define*` function signatures, config properties, validation rules,
or their warnings (`packages/twenty-sdk/src/sdk/define/**`)
- Enum / string-union values and manifest types
(`packages/twenty-shared/src/application/**`, `packages/twenty-shared/src/types/**`)
- Exports added/removed/renamed in the `exports` maps of `twenty-sdk` or
`twenty-client-sdk` package.json, or in their public entry points
- Front-component runtime hooks and host API
(`packages/twenty-sdk/src/sdk/front-component/**`)
- The scaffold template (`packages/create-twenty-app/src/constants/template/**`)
and scaffolder flags/prompts (`packages/create-twenty-app/src/cli.ts`,
`create-app.command.ts`)
- Environment variables injected into logic functions or front components
- Error codes surfaced to app developers
- Default ports and instance URLs used during development
Internal refactors, tests, and implementation-only changes are NOT relevant —
if the changes contain only those, say so and stop.
## Step 2 — Map to skill content
Use this mapping to know which files to check (read the actual files, all
paths relative to `packages/twenty-agent-skills/`):
| Source area | Skill content |
|---|---|
| CLI commands (`src/cli/commands`) | `references/manage-app/cli-and-sync.md`, `skills/manage-app/SKILL.md`, `references/concepts/operating-rules.md` |
| Scaffolder flags, prompts, template | `skills/create-app/SKILL.md`, `references/develop-app/app-structure.md` |
| `defineObject`/`defineField`/relations/indexes | `references/develop-app/data-model.md` |
| `defineApplication`/roles/install hooks/variables | `references/concepts/how-apps-work.md`, `references/publish-app/prepare-for-app-store.md` |
| Logic functions, triggers, connections, agents/skills | `references/develop-app/logic.md` |
| Workflows and their lifecycle | `references/develop-app/workflows.md` |
| Views, navigation, page layouts, command menu | `references/develop-app/layout.md`, `references/develop-app/standalone-pages.md` |
| Front components, SDK imports, runtime hooks | `references/develop-app/front-components.md`, `references/design/front-component-ui.md` |
| Test layout, test target ports | `references/develop-app/tests.md`, `references/concepts/operating-rules.md` |
| Publish/install/deploy, versioning | `references/publish-app/prepare-for-app-store.md`, `skills/publish-app/SKILL.md` |
| MCP endpoints, transport, auth | `references/use-twenty-mcp/setup.md`, `references/use-twenty-mcp/result-formatting.md` |
| Enum value sets (FieldType, ViewType, availabilityType, NavigationMenuItemType, ...) | the reference documenting that value set (search for the enum name) |
Two extra checks specific to this package:
- `scripts/validators/` encodes cross-document contracts as literal string
fragments (exact commands, ports, section headings). A renamed command or
changed port can make a validator assert something no longer true, so check
whether the change invalidates a fragment there.
- `references/concepts/operating-rules.md` is the single source of truth for
the durable rules and names exact commands. A command rename lands here.
Also check whether the PR already updates the impacted files — if it does,
verify the update matches the code change.
## Step 3 — Report
If there is NO skill impact, do NOT post or create any comment.
Skip the comment entirely and go straight to Step 4.
Only when the skills are impacted, post exactly one comment on PR
#${{ github.event.pull_request.number }} with:
`gh pr comment ${{ github.event.pull_request.number }} --edit-last --create-if-none --body ...`
(this updates the previous drift-check comment in place on subsequent runs).
Start the comment body with the marker `<!-- agent-skills-drift-check -->`.
Comment format:
- Title line: `### Agent skills drift check`
- A table with columns **Change** (what changed in the code),
**Skill file** (repo-relative path, canonical only), **Status**
(`✅ already updated in this PR` / `⚠️ needs update`), and **Suggested fix**
(one concrete sentence, e.g. the exact command to replace).
- When any row needs an update, end with one line reminding the author to run
`npx nx run twenty-agent-skills:validate` after editing the canonical content.
- Be terse. No preamble, no sign-off. Only report genuine drift — if unsure
whether something is user-facing, say it's uncertain rather than asserting.
## Step 4 — Return the verdict
After posting the comment, return your final answer as the structured
output the CI gate consumes:
- `hasDrift`: `true` if at least one impacted skill file still needs updating
(drift is NOT fully resolved by this PR); `false` if there is no skill
impact, or every impacted file is already correctly updated in this PR.
- `summary`: one terse sentence describing the verdict.
Environment: GH_TOKEN is available for `gh`. The PR branch is checked out.
settings: |
{
"env": {
"GH_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
}
}
- name: Gate on drift verdict
if: always()
env:
AGENT_OUTCOME: ${{ steps.drift-agent.outcome }}
STRUCTURED_OUTPUT: ${{ steps.drift-agent.outputs.structured_output }}
run: |
if [ "$AGENT_OUTCOME" != "success" ]; then
echo "::error::Agent skills drift checker did not complete (agent step outcome: $AGENT_OUTCOME)."
exit 1
fi
if [ -z "$STRUCTURED_OUTPUT" ]; then
echo "::error::Agent skills drift checker did not return a structured verdict."
exit 1
fi
has_drift=$(printf '%s' "$STRUCTURED_OUTPUT" | jq -r '.hasDrift')
if [ "$has_drift" = "true" ]; then
echo "::error::Agent skills drift detected. See the agent-skills-drift-check comment on this PR."
exit 1
fi
if [ "$has_drift" = "false" ]; then
echo "No agent skills drift."
exit 0
fi
echo "::error::Structured verdict present but 'hasDrift' was not a recognizable boolean: $has_drift"
exit 1

199
.github/workflows/ci-agent-skills.yaml vendored Normal file
View File

@@ -0,0 +1,199 @@
name: CI Agent Skills
on:
push:
branches:
- main
paths:
- package.json
- packages/twenty-agent-skills/**
- .github/workflows/ci-agent-skills.yaml
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-agent-skills/**
.github/workflows/ci-agent-skills.yaml
agent-skills-validate:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Agent Skills / Build and validate
run: npx nx run twenty-agent-skills:validate
- name: Agent Skills / Test
run: npx nx run twenty-agent-skills:test
- name: Agent Skills / List with the skills CLI
run: |
set -euo pipefail
LISTING="$(npx -y skills@latest add ./packages/twenty-agent-skills/dist --list)"
echo "$LISTING"
for SKILL in create-app develop-app manage-app publish-app use-twenty-mcp; do
grep -q "$SKILL" <<<"$LISTING" || {
echo "::error::$SKILL missing from the collection listing"
exit 1
}
done
# The package path must scope discovery to the app skills; a listing that
# also carries repository-wide skills means the source is wrong.
if grep -q 'syncable-entity' <<<"$LISTING"; then
echo "::error::listing leaked internal contributor skills"
exit 1
fi
- name: Agent Skills / Install each skill independently
run: |
set -euo pipefail
for SKILL in create-app develop-app manage-app publish-app use-twenty-mcp; do
INSTALL_DIR="$(mktemp -d)"
(
cd "$INSTALL_DIR"
npx -y skills@latest add "$GITHUB_WORKSPACE/packages/twenty-agent-skills/dist" \
--skill "$SKILL" --agent claude-code --agent codex --copy -y
)
npx nx run twenty-agent-skills:verify:install -- "$INSTALL_DIR" "$SKILL"
rm -rf "$INSTALL_DIR"
done
- name: Agent Skills / Install the whole collection
run: |
set -euo pipefail
INSTALL_DIR="$(mktemp -d)"
(
cd "$INSTALL_DIR"
npx -y skills@latest add "$GITHUB_WORKSPACE/packages/twenty-agent-skills/dist" \
--skill '*' --agent claude-code --agent codex --copy -y
)
npx nx run twenty-agent-skills:verify:install -- "$INSTALL_DIR"
rm -rf "$INSTALL_DIR"
- name: Archive the verified distribution
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: tar -C packages/twenty-agent-skills/dist -czf "$RUNNER_TEMP/twenty-agent-skills.tar.gz" .
- name: Upload the verified distribution
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: twenty-agent-skills
path: ${{ runner.temp }}/twenty-agent-skills.tar.gz
retention-days: 1
if-no-files-found: error
agent-skills-publish:
needs: agent-skills-validate
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'twentyhq/twenty'
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
released: ${{ steps.publish.outputs.released }}
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Download the verified distribution
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: twenty-agent-skills
path: ${{ runner.temp }}/agent-skills-artifact
- name: Publish distribution branch
id: publish
run: |
set -euo pipefail
git fetch --depth=1 --no-tags origin main
if [ "$(git rev-parse "$GITHUB_SHA:packages/twenty-agent-skills")" != "$(git rev-parse 'FETCH_HEAD:packages/twenty-agent-skills')" ]; then
echo 'Skipping this distribution because the skills source has changed on main.'
exit 0
fi
DISTRIBUTION_DIR="$RUNNER_TEMP/agent-skills-dist"
PUBLISH_DIR="$RUNNER_TEMP/agent-skills-release"
mkdir "$DISTRIBUTION_DIR"
tar -xzf "$RUNNER_TEMP/agent-skills-artifact/twenty-agent-skills.tar.gz" -C "$DISTRIBUTION_DIR"
REMOTE_BRANCH="$(git ls-remote --heads origin refs/heads/agent-skills)"
if [ -n "$REMOTE_BRANCH" ]; then
git fetch --no-tags origin refs/heads/agent-skills
git worktree add --detach "$PUBLISH_DIR" FETCH_HEAD
else
git worktree add --detach --no-checkout "$PUBLISH_DIR"
git -C "$PUBLISH_DIR" switch --orphan agent-skills
fi
rsync -a --delete --exclude=/.git "$DISTRIBUTION_DIR/" "$PUBLISH_DIR/"
cd "$PUBLISH_DIR"
git add --all
if git diff --cached --quiet; then
echo 'The agent-skills distribution is unchanged.'
echo 'released=true' >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git commit -m "Publish agent skills from $GITHUB_SHA"
git push origin HEAD:refs/heads/agent-skills
echo 'released=true' >> "$GITHUB_OUTPUT"
agent-skills-remote-source:
needs: agent-skills-publish
if: needs.agent-skills-publish.outputs.released == 'true'
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '24'
- name: Download the verified distribution
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: twenty-agent-skills
path: ${{ runner.temp }}/agent-skills-artifact
- name: Agent Skills / Install from the documented remote source
run: |
set -euo pipefail
mkdir -p packages/twenty-agent-skills/dist
tar -xzf "$RUNNER_TEMP/agent-skills-artifact/twenty-agent-skills.tar.gz" -C packages/twenty-agent-skills/dist
INSTALL_DIR="$(mktemp -d)"
(
cd "$INSTALL_DIR"
npx -y skills@latest add 'https://github.com/twentyhq/twenty/tree/agent-skills' \
--skill '*' --agent claude-code --agent codex --copy -y
)
node packages/twenty-agent-skills/scripts/verify-install.js "$INSTALL_DIR"
rm -rf "$INSTALL_DIR"

View File

@@ -0,0 +1,96 @@
name: AI Catalog Sync
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
workflow_dispatch: # Allow manual trigger
permissions:
contents: write
pull-requests: write
jobs:
sync-catalog:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
NODE_OPTIONS: '--max-old-space-size=4096'
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Run catalog sync
env:
ARTIFICIAL_ANALYSIS_API_KEY: ${{ secrets.ARTIFICIAL_ANALYSIS_API_KEY }}
run: |
npx nx run twenty-server:ts-node-no-deps-transpile-only -- \
./scripts/ai-catalog-sync/index.ts --report "${RUNNER_TEMP}/coverage.md"
- name: Check for changes
id: changes
run: |
catalog_dir=packages/twenty-server/src/engine/metadata-modules/ai/ai-models
if git diff --quiet "$catalog_dir"; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Compose pull request body
if: steps.changes.outputs.changed == 'true'
run: |
cat > "${RUNNER_TEMP}/pr-body.md" <<'PREAMBLE'
Automated daily sync of the AI model catalog.
- Model identity, pricing, context windows and availability come from [models.dev](https://models.dev).
- Intelligence, speed and cost per task come from [Artificial Analysis](https://artificialanalysis.ai/),
which requires `ARTIFICIAL_ANALYSIS_API_KEY`.
New models meeting inclusion criteria (tool calling, pricing data, context limits) are added
automatically. Benchmark data is best-effort: a model Artificial Analysis has not rated keeps
every other field and simply carries no `benchmark` entry.
**Please review before merging** and check the coverage summary below for models that silently
stopped matching.
PREAMBLE
cat "${RUNNER_TEMP}/coverage.md" >> "${RUNNER_TEMP}/pr-body.md"
- name: Create pull request
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync AI model catalog'
title: 'chore: sync AI model catalog'
body-path: ${{ runner.temp }}/pr-body.md
branch: chore/ai-catalog-sync
base: main
labels: ai, automated
delete-branch: true
- name: Mint twenty-infra dispatch token
id: app-token
if: steps.changes.outputs.changed == 'true'
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.TWENTY_WORKFLOW_DISPATCHER_CLIENT_ID }}
private-key: ${{ secrets.TWENTY_WORKFLOW_DISPATCHER_PRIVATE_KEY }}
owner: twentyhq
repositories: twenty-infra
permission-actions: write
- name: Trigger automerge
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh workflow run automerge-i18n.yaml --repo twentyhq/twenty-infra --ref main

172
.github/workflows/ci-app-docs-drift.yaml vendored Normal file
View File

@@ -0,0 +1,172 @@
name: CI App Docs Drift
# When a PR changes the app-development surface (twenty-sdk, create-twenty-app,
# twenty-client-sdk, or the shared manifest types), an agent checks whether the
# app documentation under packages/twenty-docs/developers/extend/apps is
# impacted and, only when it is, posts a single sticky comment with its findings.
on:
pull_request:
permissions:
contents: read
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-sdk/**
packages/create-twenty-app/**
packages/twenty-client-sdk/**
packages/twenty-shared/src/application/**
packages/twenty-shared/src/types/**
docs-drift-check:
needs: changed-files-check
# Secrets are unavailable on fork PRs; skip there and on bot PRs.
if: >-
needs.changed-files-check.outputs.any_changed == 'true' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.type != 'Bot'
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Run docs drift agent
id: drift-agent
uses: anthropics/claude-code-action@ac7e24bf2938964b8ab203e417a2773802392ddd # v1.0.146
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: |
--max-turns 60 --json-schema '{"type":"object","properties":{"hasDrift":{"type":"boolean"},"summary":{"type":"string"}},"required":["hasDrift","summary"]}' --allowedTools "Bash(git diff *),Bash(git log *),Bash(git show *),Bash(grep *),Bash(cat *),Bash(ls *),Bash(find *),Bash(head *),Bash(tail *),Bash(gh pr comment *)"
prompt: |
You are the app-documentation drift checker for the twenty repository.
A pull request changed files in the app-development platform. Your job is to
decide whether the documentation under
`packages/twenty-docs/developers/extend/apps/` (English pages only — ignore
`packages/twenty-docs/l/`) is impacted, and report your findings as ONE pull
request comment.
## Step 1 — Understand the change
The PR branch is checked out with full history; the base branch is
`origin/${{ github.base_ref }}`. Inspect what the PR changes however you
see fit. Only these kinds of changes are documentation-relevant:
- CLI commands, subcommands, flags, defaults, or help text
(`packages/twenty-sdk/src/cli/commands/**`)
- `define*` function signatures, config properties, validation rules,
or their warnings (`packages/twenty-sdk/src/sdk/define/**`)
- Enum / string-union values and manifest types
(`packages/twenty-shared/src/application/**`, `packages/twenty-shared/src/types/**`)
- Exports added/removed/renamed in the `exports` maps of `twenty-sdk` or
`twenty-client-sdk` package.json, or in their public entry points
- Front-component runtime hooks and host API
(`packages/twenty-sdk/src/sdk/front-component/**`)
- The scaffold template (`packages/create-twenty-app/src/constants/template/**`)
and scaffolder flags/prompts (`packages/create-twenty-app/src/cli.ts`,
`create-app.command.ts`)
- Environment variables injected into logic functions or front components
- Error codes surfaced to app developers
Internal refactors, tests, and implementation-only changes are NOT relevant —
if the changes contain only those, say so and stop.
## Step 2 — Map to documentation
Use this mapping to know which pages to check (read the actual pages):
| Source area | Docs pages |
|---|---|
| CLI commands (`src/cli/commands`) | `operations/cli.mdx`, `getting-started/quick-start.mdx`, `operations/sync-and-recovery.mdx`, `getting-started/local-server.mdx`, `getting-started/scaffolding.mdx` |
| `defineObject`/`defineField`/relations/indexes | `data/*.mdx` |
| `defineApplication`/roles/install hooks/variables | `config/*.mdx` |
| Logic functions, triggers, connections, agents/skills | `logic/*.mdx` |
| Views, navigation, page layouts, front components, command menu | `layout/*.mdx` |
| Publish/install/deploy, versioning, engines | `operations/publishing.mdx`, `operations/sync-and-recovery.mdx` |
| Testing APIs (`twenty-sdk/cli` operations) | `operations/testing.mdx` |
| Scaffold template | `getting-started/project-structure.mdx`, `getting-started/quick-start.mdx`, `operations/testing.mdx`, `operations/publishing.mdx` |
| client SDK (`core`/`metadata`/`rest`/`generate`) | `logic/logic-functions.mdx`, `layout/front-components.mdx`, `operations/cli.mdx` |
| Enum value sets (FieldType, ViewType, availabilityType, NavigationMenuItemType, ...) | the page documenting that value set (search for the enum name) |
Also check whether the PR already updates the impacted docs pages — if it
does, verify the update matches the code change.
## Step 3 — Report
If there is NO documentation impact, do NOT post or create any comment.
Skip the comment entirely and go straight to Step 4.
Only when the documentation is impacted, post exactly one comment on PR
#${{ github.event.pull_request.number }} with:
`gh pr comment ${{ github.event.pull_request.number }} --edit-last --create-if-none --body ...`
(this updates the previous drift-check comment in place on subsequent runs).
Start the comment body with the marker `<!-- app-docs-drift-check -->`.
Comment format:
- Title line: `### App docs drift check`
- A table with columns **Change** (what changed in the code),
**Docs page** (repo-relative path), **Status** (`✅ already updated in this PR`
/ `⚠️ needs update`), and **Suggested fix** (one concrete sentence, e.g. the
exact value to add to a table).
- Be terse. No preamble, no sign-off. Only report genuine drift — if unsure
whether something is user-facing, say it's uncertain rather than asserting.
## Step 4 — Return the verdict
After posting the comment, return your final answer as the structured
output the CI gate consumes:
- `hasDrift`: `true` if at least one impacted docs page still needs updating
(drift is NOT fully resolved by this PR); `false` if there is no
documentation impact, or every impacted page is already correctly updated
in this PR.
- `summary`: one terse sentence describing the verdict.
Environment: GH_TOKEN is available for `gh`. The PR branch is checked out.
settings: |
{
"env": {
"GH_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
}
}
- name: Gate on drift verdict
if: always()
env:
AGENT_OUTCOME: ${{ steps.drift-agent.outcome }}
STRUCTURED_OUTPUT: ${{ steps.drift-agent.outputs.structured_output }}
run: |
if [ "$AGENT_OUTCOME" != "success" ]; then
echo "::error::Docs drift checker did not complete (agent step outcome: $AGENT_OUTCOME)."
exit 1
fi
if [ -z "$STRUCTURED_OUTPUT" ]; then
echo "::error::Docs drift checker did not return a structured verdict."
exit 1
fi
has_drift=$(printf '%s' "$STRUCTURED_OUTPUT" | jq -r '.hasDrift')
if [ "$has_drift" = "true" ]; then
echo "::error::App docs drift detected. See the app-docs-drift-check comment on this PR."
exit 1
fi
if [ "$has_drift" = "false" ]; then
echo "No app docs drift."
exit 0
fi
echo "::error::Structured verdict present but 'hasDrift' was not a recognizable boolean: $has_drift"
exit 1

View File

@@ -0,0 +1,35 @@
name: Blocked Contributors Check
on:
pull_request:
types: [opened, synchronize, reopened, edited]
issue_comment:
types: [created, edited]
pull_request_review:
types: [submitted, edited]
pull_request_review_comment:
types: [created, edited]
permissions:
contents: read
issues: read
pull-requests: read
jobs:
check-blocked-contributors:
runs-on: ubuntu-latest
timeout-minutes: 10
if: ${{ github.event_name != 'issue_comment' || github.event.issue.pull_request }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Check PR commits, description, comments and reviews for blocked contributors
run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/check-blocked-contributors.ts
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}

View File

@@ -0,0 +1,746 @@
name: GraphQL and OpenAPI Breaking Changes Detection
on:
pull_request:
types: [opened, synchronize, edited]
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
MAIN_SERVER_PORT: 3000
CURRENT_SERVER_PORT: 3002
permissions:
contents: read
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
packages/twenty-shared/**
.github/workflows/ci-breaking-changes.yaml
api-breaking-changes:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 45
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
clickhouse:
image: clickhouse/clickhouse-server:25.8.8
env:
CLICKHOUSE_PASSWORD: clickhousePassword
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
ports:
- 8123:8123
- 9000:9000
options: >-
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout current branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Try to merge main into current branch
id: merge_attempt
run: |
echo "Attempting to merge main into current branch..."
git config user.email "ci@twenty.com"
git config user.name "CI"
git fetch origin main
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Current branch: $CURRENT_BRANCH"
if git merge origin/main --no-edit; then
echo "✅ Successfully merged main into current branch"
echo "merged=true" >> $GITHUB_OUTPUT
echo "BRANCH_STATE=merged" >> $GITHUB_ENV
else
echo "❌ Merge failed due to conflicts"
echo "⚠️ Falling back to comparing current branch against main without merge"
# Abort the failed merge (may not exist if merge never started)
git merge --abort 2>/dev/null || true
echo "merged=false" >> $GITHUB_OUTPUT
echo "BRANCH_STATE=conflicts" >> $GITHUB_ENV
fi
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build shared dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-emails
- name: Build current branch server
run: npx nx build twenty-server
- name: Setup databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "current_branch";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "main_branch";'
- name: Run ClickHouse migrations
run: npx nx clickhouse:migrate twenty-server
env:
CLICKHOUSE_URL: http://default:clickhousePassword@localhost:8123/twenty
CLICKHOUSE_PASSWORD: clickhousePassword
- name: Setup current branch database
run: |
npx nx reset:env twenty-server
set_env_var() {
local var_name="$1"
local var_value="$2"
local env_file="packages/twenty-server/.env"
echo "" >> "$env_file"
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
else
echo "${var_name}=${var_value}" >> "$env_file"
fi
}
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/current_branch"
set_env_var "NODE_PORT" "${{ env.CURRENT_SERVER_PORT }}"
set_env_var "REDIS_URL" "redis://localhost:6379"
set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty"
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
- name: Flush cache before seeding current branch
run: npx nx command-no-deps twenty-server -- cache:flush
- name: Seed current branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
- name: Start current branch server in background
run: |
echo "=== Current branch .env file contents ==="
cat packages/twenty-server/.env
echo "=== Starting current branch server ==="
nohup npx nx run twenty-server:start:prod > /tmp/current-server.log 2>&1 &
echo $! > /tmp/current-server.pid
echo "Current server PID: $(cat /tmp/current-server.pid)"
- name: Wait for current branch server to be ready
run: |
echo "Waiting for current branch server to start..."
timeout=60
interval=5
elapsed=0
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
while [ $elapsed -lt $timeout ]; do
GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}')
if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \
curl -fsS "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then
echo "Current branch server is ready!"
break
fi
echo "Current branch server not ready yet, waiting ${interval}s..."
sleep $interval
elapsed=$((elapsed + interval))
done
if [ $elapsed -ge $timeout ]; then
echo "❌ Timed out waiting for current branch server to serve a valid schema."
echo "Current server log:"
cat /tmp/current-server.log || echo "No current server log found"
exit 1
fi
- name: Download GraphQL and REST responses from current branch
run: |
# Read admin token from shared test tokens file (single source of truth)
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
# Load introspection query from file
INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql)
# Prepare the query payload
QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g')
echo "Downloading GraphQL schema from current server..."
curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o current-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
echo "Downloading GraphQL metadata schema from current server..."
curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/metadata" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o current-metadata-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
# Download current branch OpenAPI specs
echo "Downloading OpenAPI specifications from current server..."
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o current-rest-api.json \
-w "HTTP Status: %{http_code}\n"
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/metadata" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o current-rest-metadata-api.json \
-w "HTTP Status: %{http_code}\n"
# Verify the downloads
echo "Current branch files downloaded:"
ls -la current-*
- name: Preserve current branch files
run: |
# Create a temp directory to store current branch files
mkdir -p /tmp/current-branch-files
# Move current branch files to temp directory
mv current-* /tmp/current-branch-files/ 2>/dev/null || echo "No current-* files to preserve"
# Both servers must be introspected with the same query, and checking
# out main would otherwise swap in main's copy of it
cp packages/twenty-utils/graphql-introspection-query.graphql /tmp/graphql-introspection-query.graphql
echo "Preserved current branch files for later restoration"
- name: Stop current branch server
run: |
if [ -f /tmp/current-server.pid ]; then
echo "Stopping current branch server..."
kill $(cat /tmp/current-server.pid) || true
# Wait a bit for graceful shutdown
sleep 5
# Force kill if still running
kill -9 $(cat /tmp/current-server.pid) 2>/dev/null || true
rm -f /tmp/current-server.pid
fi
- name: Flush Redis between server runs
run: |
# Clear all Redis caches to prevent stale data from the current branch
# server contaminating the main branch server. Both servers share the
# same Redis instance, and CoreEntityCacheService/WorkspaceCacheService
# persist cached entities across process restarts.
redis-cli -h localhost -p 6379 FLUSHALL || echo "::warning::Failed to flush Redis"
- name: Checkout main branch
run: |
git stash
git checkout origin/main
git reset --hard
git clean -xfd -ff
rm -rf node_modules packages/*/node_modules packages/*/dist dist .nx/cache
- name: Install dependencies for main branch
uses: ./.github/actions/yarn-install
- name: Build main branch dependencies
run: |
npx nx reset
npx nx build twenty-shared
npx nx build twenty-emails
- name: Build main branch server
run: npx nx build twenty-server
- name: Setup main branch database
run: |
npx nx reset:env twenty-server
set_env_var() {
local var_name="$1"
local var_value="$2"
local env_file="packages/twenty-server/.env"
echo "" >> "$env_file"
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
else
echo "${var_name}=${var_value}" >> "$env_file"
fi
}
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/main_branch"
set_env_var "NODE_PORT" "${{ env.MAIN_SERVER_PORT }}"
set_env_var "REDIS_URL" "redis://localhost:6379"
set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty"
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
- name: Flush cache before seeding main branch
run: npx nx command-no-deps twenty-server -- cache:flush
- name: Seed main branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
- name: Start main branch server in background
run: |
echo "=== Main branch .env file contents ==="
cat packages/twenty-server/.env
echo "=== Starting main branch server ==="
nohup npx nx run twenty-server:start:prod > /tmp/main-server.log 2>&1 &
echo $! > /tmp/main-server.pid
echo "Main server PID: $(cat /tmp/main-server.pid)"
- name: Wait for main branch server to be ready
run: |
echo "Waiting for main branch server to start..."
timeout=300
interval=5
elapsed=0
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
while [ $elapsed -lt $timeout ]; do
GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}')
if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \
curl -fsS "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then
echo "Main branch server is ready!"
break
fi
echo "Main branch server not ready yet, waiting ${interval}s..."
sleep $interval
elapsed=$((elapsed + interval))
done
if [ $elapsed -ge $timeout ]; then
echo "❌ Timed out waiting for main branch server to serve a valid schema."
echo "Main server log:"
cat /tmp/main-server.log || echo "No main server log found"
exit 1
fi
- name: Download GraphQL and REST responses from main branch
run: |
# Read admin token from shared test tokens file (single source of truth)
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
# Load introspection query preserved from the current branch
INTROSPECTION_QUERY=$(cat /tmp/graphql-introspection-query.graphql)
# Prepare the query payload
QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g')
echo "Downloading GraphQL schema from main server..."
curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o main-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
echo "Downloading GraphQL metadata schema from main server..."
curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/metadata" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o main-metadata-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
# Download main branch OpenAPI specs
echo "Downloading OpenAPI specifications from main server..."
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o main-rest-api.json \
-w "HTTP Status: %{http_code}\n"
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/metadata" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o main-rest-metadata-api.json \
-w "HTTP Status: %{http_code}\n"
# Verify the downloads
echo "Main branch files downloaded:"
ls -la main-*
- name: Restore current branch files
run: |
# Move current branch files back to working directory
mv /tmp/current-branch-files/* . 2>/dev/null || echo "No files to restore"
# Verify all files are present
echo "All API files restored:"
ls -la current-* main-* 2>/dev/null || echo "Some files may be missing"
# Clean up temp directory
rm -rf /tmp/current-branch-files
- name: Validate downloaded schema files
id: validate-schemas
run: |
valid=true
for file in main-schema-introspection.json current-schema-introspection.json \
main-metadata-schema-introspection.json current-metadata-schema-introspection.json; do
if [ ! -f "$file" ]; then
echo "::warning::Missing GraphQL schema file: $file"
valid=false
elif ! jq -e '.data.__schema' "$file" >/dev/null 2>&1; then
echo "::warning::File $file is not a valid GraphQL introspection result. First 200 bytes: $(head -c 200 "$file")"
valid=false
fi
done
for file in main-rest-api.json current-rest-api.json \
main-rest-metadata-api.json current-rest-metadata-api.json; do
if [ ! -f "$file" ]; then
echo "::warning::Missing OpenAPI spec file: $file"
valid=false
elif ! jq -e '.openapi // .swagger' "$file" >/dev/null 2>&1; then
echo "::warning::File $file is not a valid OpenAPI spec. First 200 bytes: $(head -c 200 "$file")"
valid=false
fi
done
echo "valid=$valid" >> $GITHUB_OUTPUT
- name: Install OpenAPI Diff Tool
run: |
# Using the Java-based OpenAPITools/openapi-diff via Docker
echo "Using OpenAPITools/openapi-diff via Docker"
- name: Generate GraphQL Schema Diff Reports
id: graphql-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
npm install -g @graphql-inspector/cli
echo "=== GENERATING GRAPHQL DIFF REPORTS ==="
# Check if GraphQL schema has changes
echo "Checking GraphQL schema for changes..."
if graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL schema"
else
echo "⚠️ Changes detected in GraphQL schema, generating report..."
echo "core_breaking=true" >> $GITHUB_OUTPUT
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
echo "" >> graphql-schema-diff.md
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || {
echo "⚠️ **Breaking changes or errors detected in GraphQL schema**" >> graphql-schema-diff.md
echo "" >> graphql-schema-diff.md
echo "\`\`\`" >> graphql-schema-diff.md
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json 2>&1 >> graphql-schema-diff.md || echo "Error generating diff" >> graphql-schema-diff.md
echo "\`\`\`" >> graphql-schema-diff.md
}
fi
# Check if GraphQL metadata schema has changes
echo "Checking GraphQL metadata schema for changes..."
if graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL metadata schema"
else
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
echo "metadata_breaking=true" >> $GITHUB_OUTPUT
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
echo "" >> graphql-metadata-diff.md
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || {
echo "⚠️ **Breaking changes or errors detected in GraphQL metadata schema**" >> graphql-metadata-diff.md
echo "" >> graphql-metadata-diff.md
echo "\`\`\`" >> graphql-metadata-diff.md
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json 2>&1 >> graphql-metadata-diff.md || echo "Error generating diff" >> graphql-metadata-diff.md
echo "\`\`\`" >> graphql-metadata-diff.md
}
fi
# Show summary
echo "Generated diff files:"
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
- name: Check REST API Breaking Changes
id: rest-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
# Use the Java-based openapi-diff via Docker
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \
--json /specs/rest-api-diff.json \
/specs/main-rest-api.json /specs/current-rest-api.json || echo "OpenAPI diff completed with exit code $?"
# Check if the output file was created and is valid JSON
if [ -f "rest-api-diff.json" ] && jq empty rest-api-diff.json 2>/dev/null; then
# Check for breaking changes using Java openapi-diff JSON structure
incompatible=$(jq -r '.incompatible // false' rest-api-diff.json)
different=$(jq -r '.different // false' rest-api-diff.json)
# Count changes
new_endpoints=$(jq -r '.newEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0")
missing_endpoints=$(jq -r '.missingEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0")
changed_operations=$(jq -r '.changedOperations | length' rest-api-diff.json 2>/dev/null || echo "0")
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST API"
echo "breaking=true" >> $GITHUB_OUTPUT
# Generate breaking changes report
echo "# REST API Breaking Changes" > rest-api-diff.md
echo "" >> rest-api-diff.md
echo "⚠️ **Breaking changes detected that may affect existing API consumers**" >> rest-api-diff.md
echo "" >> rest-api-diff.md
# Parse and format the changes from Java openapi-diff
jq -r '
if (.missingEndpoints | length) > 0 then
"## 🚨 Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" +
(.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n"))
else "" end,
([(.changedOperations // [])[] | select(.incompatible)] | if length > 0 then
"\n## ⚠️ Incompatible Operations (" + (length | tostring) + ")\n" +
(map("- **" + .httpMethod + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n"))
else "" end),
if (.newEndpoints | length) > 0 then
"\n## ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
else "" end
' rest-api-diff.json >> rest-api-diff.md
elif [ "$different" = "true" ]; then
echo "📝 Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed) - no PR comment will be posted"
# Don't create markdown file for non-breaking changes to avoid PR comments
else
echo "✅ No changes detected in REST API"
# Don't create diff file for no changes
fi
else
echo "⚠️ OpenAPI diff tool could not process the files"
echo "# REST API Analysis Error" > rest-api-diff.md
echo "" >> rest-api-diff.md
echo "⚠️ **Error occurred while analyzing REST API changes**" >> rest-api-diff.md
echo "" >> rest-api-diff.md
echo "## Error Output" >> rest-api-diff.md
echo "\`\`\`" >> rest-api-diff.md
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-api.json /specs/current-rest-api.json 2>&1 >> rest-api-diff.md || echo "Could not capture error output"
echo "\`\`\`" >> rest-api-diff.md
# Don't fail the workflow for tool errors
echo "::warning::REST API analysis tool error - continuing workflow"
fi
- name: Check REST Metadata API Breaking Changes
id: rest-metadata-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
# Use the Java-based openapi-diff for metadata API as well
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \
--json /specs/rest-metadata-api-diff.json \
/specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json || echo "OpenAPI diff completed with exit code $?"
# Check if the output file was created and is valid JSON
if [ -f "rest-metadata-api-diff.json" ] && jq empty rest-metadata-api-diff.json 2>/dev/null; then
# Check for breaking changes using Java openapi-diff JSON structure
incompatible=$(jq -r '.incompatible // false' rest-metadata-api-diff.json)
different=$(jq -r '.different // false' rest-metadata-api-diff.json)
# Count changes
new_endpoints=$(jq -r '.newEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
missing_endpoints=$(jq -r '.missingEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
changed_operations=$(jq -r '.changedOperations | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST Metadata API"
echo "breaking=true" >> $GITHUB_OUTPUT
# Generate breaking changes report (only for breaking changes)
echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "⚠️ **Breaking changes detected that may affect existing API consumers**" >> rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
# Parse and format the changes from Java openapi-diff
jq -r '
if (.missingEndpoints | length) > 0 then
"## 🚨 Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" +
(.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n"))
else "" end,
([(.changedOperations // [])[] | select(.incompatible)] | if length > 0 then
"\n## ⚠️ Incompatible Operations (" + (length | tostring) + ")\n" +
(map("- **" + .httpMethod + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n"))
else "" end),
if (.newEndpoints | length) > 0 then
"\n## ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
else "" end
' rest-metadata-api-diff.json >> rest-metadata-api-diff.md
elif [ "$different" = "true" ]; then
echo "📝 Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed) - no PR comment will be posted"
# Don't create markdown file for non-breaking changes to avoid PR comments
else
echo "✅ No changes detected in REST Metadata API"
fi
else
echo "⚠️ OpenAPI diff tool could not process the metadata API files"
echo "# REST Metadata API Analysis Error" > rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "⚠️ **Error occurred while analyzing REST Metadata API changes**" >> rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "## Error Output" >> rest-metadata-api-diff.md
echo "\`\`\`" >> rest-metadata-api-diff.md
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json 2>&1 >> rest-metadata-api-diff.md || echo "Could not capture error output"
echo "\`\`\`" >> rest-metadata-api-diff.md
# Don't fail the workflow for tool errors
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
fi
- name: Fail on breaking changes
if: steps.validate-schemas.outputs.valid == 'true'
run: |
breaking=false
if [ "${{ steps.graphql-diff.outputs.core_breaking }}" = "true" ]; then
echo "❌ GraphQL core schema has breaking changes"
breaking=true
if [ -f graphql-schema-diff.md ]; then
echo ""
cat graphql-schema-diff.md
echo ""
fi
fi
if [ "${{ steps.graphql-diff.outputs.metadata_breaking }}" = "true" ]; then
echo "❌ GraphQL metadata schema has breaking changes"
breaking=true
if [ -f graphql-metadata-diff.md ]; then
echo ""
cat graphql-metadata-diff.md
echo ""
fi
fi
if [ "${{ steps.rest-diff.outputs.breaking }}" = "true" ]; then
echo "❌ REST core API has breaking changes"
breaking=true
if [ -f rest-api-diff.json ]; then
echo ""
jq -r '
(if (.missingEndpoints | length) > 0 then
" Removed endpoints:\n" +
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end),
([(.changedOperations // [])[] | select(.incompatible)] | if length > 0 then
" Incompatible operations:\n" +
(map(" - " + (.httpMethod // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end)
' rest-api-diff.json | sed '/^$/d'
echo ""
fi
fi
if [ "${{ steps.rest-metadata-diff.outputs.breaking }}" = "true" ]; then
echo "❌ REST metadata API has breaking changes"
breaking=true
if [ -f rest-metadata-api-diff.json ]; then
echo ""
jq -r '
(if (.missingEndpoints | length) > 0 then
" Removed endpoints:\n" +
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end),
([(.changedOperations // [])[] | select(.incompatible)] | if length > 0 then
" Incompatible operations:\n" +
(map(" - " + (.httpMethod // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end)
' rest-metadata-api-diff.json | sed '/^$/d'
echo ""
fi
fi
if [ "$breaking" = "true" ]; then
echo ""
echo "This PR introduces breaking changes to the public API."
echo "If intentional, deprecate the old endpoint and introduce a new one."
echo "See the breaking changes report artifact and PR comment for details."
exit 1
fi
echo "✅ No breaking API changes detected"
- name: Upload breaking changes report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: breaking-changes-report
path: |
*-diff.md
*-diff.json
if-no-files-found: ignore
retention-days: 3
- name: Cleanup servers
if: always()
run: |
if [ -f /tmp/current-server.pid ]; then
kill $(cat /tmp/current-server.pid) || true
fi
if [ -f /tmp/main-server.pid ]; then
kill $(cat /tmp/main-server.pid) || true
fi

55
.github/workflows/ci-client-sdk.yaml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: CI Client SDK
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-client-sdk/**
packages/twenty-shared/**
yarn.lock
.github/workflows/ci-client-sdk.yaml
!packages/twenty-client-sdk/package.json
client-sdk-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=4096'
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:client-sdk
tasks: ${{ matrix.task }}
ci-client-sdk-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, client-sdk-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

43
.github/workflows/ci-companion.yaml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: CI Desktop
on:
pull_request:
paths:
- 'packages/twenty-companion/**'
- 'packages/twenty-ui/**'
- 'packages/twenty-front/**'
- 'packages/twenty-shared/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/ci-companion.yaml'
push:
branches: [main]
paths:
- 'packages/twenty-companion/**'
- 'packages/twenty-ui/**'
- 'packages/twenty-front/**'
- 'packages/twenty-shared/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/ci-companion.yaml'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
desktop:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/yarn-install
- name: Typecheck
run: yarn workspace @twentyhq/companion typecheck
- name: Test
run: yarn workspace @twentyhq/companion test
- name: Build
run: yarn workspace @twentyhq/companion build

View File

@@ -0,0 +1,220 @@
name: CI Create App E2E minimal
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-ui/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-ui/package.json
!packages/twenty-shared/package.json
create-app-e2e-minimal:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
# All four get the CI version and are built. Only the three libraries are
# published before the template lockfile is generated; create-twenty-app is
# published after it, carrying that lockfile in its tarball.
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-ui twenty-sdk create-twenty-app
LIBRARY_PACKAGES: twenty-client-sdk twenty-ui twenty-sdk
# Deliberately beyond any realistic policy rather than a value anyone would
# set. With a working template lockfile nothing is resolved, so no gate of
# any length applies and this number is inert; it only bites if the lockfile
# stops shipping, and picking a large one means this job still catches that
# however long a real consumer's gate happens to be.
SCAFFOLD_AGE_GATE: 30d
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish libraries to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $LIBRARY_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Generate template lockfile
run: |
npx nx run create-twenty-app:generate-template-lock \
--releaseVersion=$CI_VERSION --registry=http://localhost:4873
- name: Publish create-twenty-app to local registry
run: |
cd packages/create-twenty-app
yarn npm publish --tag ci
- name: Scaffold app using published create-twenty-app
env:
YARN_NPM_MINIMAL_AGE_GATE: ${{ env.SCAFFOLD_AGE_GATE }}
YARN_NPM_REGISTRY_SERVER: http://localhost:4873
YARN_UNSAFE_HTTP_WHITELIST: localhost
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --url http://localhost:3000
- name: Verify scaffolded app ships a usable lockfile
env:
YARN_NPM_MINIMAL_AGE_GATE: ${{ env.SCAFFOLD_AGE_GATE }}
YARN_NPM_REGISTRY_SERVER: http://localhost:4873
YARN_UNSAFE_HTTP_WHITELIST: localhost
run: |
cd /tmp/e2e-test-workspace/test-app
if [ ! -f yarn.lock ]; then
echo "::error::The scaffolded app has no yarn.lock"
exit 1
fi
if ! grep -q '^__metadata:' yarn.lock; then
echo "::error::The scaffolded yarn.lock is not a Yarn 4 lockfile"
exit 1
fi
for pkg in $LIBRARY_PACKAGES; do
if ! grep -q "resolution: \"$pkg@npm:$CI_VERSION\"" yarn.lock; then
echo "::error::$pkg@$CI_VERSION is missing from the scaffolded lockfile"
exit 1
fi
done
# What the generated project's own CI workflow runs.
yarn install --immutable
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking scaffolded app package versions ---"
node -e "
const pkg = require('./package.json');
for (const name of ['twenty-client-sdk', 'twenty-sdk', 'twenty-ui']) {
const version = pkg.devDependencies[name];
if (version !== process.env.CI_VERSION) {
console.error('Expected', name, 'version', process.env.CI_VERSION, 'got:', version);
process.exit(1);
}
console.log(name, 'version in scaffolded app:', version);
}
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote:add --api-key ${{ env.TWENTY_API_KEY }} --url ${{ env.TWENTY_API_URL }}
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-minimal-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-minimal]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

54
.github/workflows/ci-create-app.yaml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: CI Create App
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
!packages/create-twenty-app/package.json
create-app-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build create-twenty-app
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:create-app
tasks: ${{ matrix.task }}
ci-create-app-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View File

@@ -0,0 +1,365 @@
name: CI - Cross-Version Upgrade
run-name: cross-version-upgrade ${{ inputs.from_version || 'v1.22' }} → current (from source)
on:
workflow_call:
inputs:
skip:
type: boolean
required: false
default: false
description: 'Set to true to skip the upgrade check and report success immediately'
from_version:
type: string
required: false
description: 'Old Twenty version to upgrade from (defaults to oldest supported: v1.22)'
workflow_dispatch:
inputs:
from_version:
type: string
required: false
default: ''
description: 'Old Twenty version to upgrade from (defaults to oldest supported: v1.22)'
permissions:
contents: read
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
env:
SERVER_BUILD_CACHE_KEY: server-build
OLDEST_SUPPORTED_VERSION: v1.22
# Local CI containers only — not real secrets. APP_SECRET is generated fresh
# per run (see "Compute run config") so nothing reconstructible is committed.
OLD_PG_DATABASE_URL: postgres://postgres:postgres@postgres:5432/default
OLD_REDIS_URL: redis://redis:6379
# Standard dev-seed workspace id (workspace:seed:dev) — public, not a secret.
SEED_WORKSPACE_ID: 20202020-1c25-4d02-bf25-6aeccf7ea419
jobs:
no-op:
if: format('{0}', inputs.skip) == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Cross-version upgrade check skipped (upgrade path not touched)"
cross-version-upgrade:
if: format('{0}', inputs.skip) != 'true'
timeout-minutes: 45
runs-on: ubuntu-latest
steps:
- name: Compute run config
env:
INPUT_FROM_VERSION: ${{ inputs.from_version }}
run: |
FROM_VERSION="${INPUT_FROM_VERSION:-}"
if [ -z "$FROM_VERSION" ]; then
FROM_VERSION="$OLDEST_SUPPORTED_VERSION"
fi
printf 'FROM_VERSION=%s\n' "$FROM_VERSION" >> "$GITHUB_ENV"
# Random per-run secret shared by the old container and the from-source
# server within this job. Never committed, never reused across runs.
GENERATED_APP_SECRET="$(openssl rand -hex 32)"
echo "::add-mask::${GENERATED_APP_SECRET}"
printf 'APP_SECRET=%s\n' "$GENERATED_APP_SECRET" >> "$GITHUB_ENV"
echo "Testing upgrade from ${FROM_VERSION} to current (built from source)"
- name: Create Docker network
run: docker network create twenty-net
- name: Start Postgres
run: |
docker run -d \
--name postgres \
--network twenty-net \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=default \
-p 5432:5432 \
postgres:16
for i in $(seq 1 30); do
if docker exec postgres pg_isready -U postgres 2>/dev/null; then
echo "Postgres is ready"
exit 0
fi
echo "Waiting for Postgres… ($i/30)"
sleep 2
done
echo "::error::Postgres did not become ready in time"
docker logs postgres 2>&1 | tail -50 || true
exit 1
- name: Start Redis
run: |
docker run -d \
--name redis \
--network twenty-net \
-p 6379:6379 \
redis:7-alpine
for i in $(seq 1 10); do
if docker exec redis redis-cli ping 2>/dev/null | grep -q PONG; then
echo "Redis is ready"
exit 0
fi
echo "Waiting for Redis… ($i/10)"
sleep 1
done
echo "::error::Redis did not become ready in time"
docker logs redis 2>&1 | tail -50 || true
exit 1
# ================================================================
# Phase 1 — Old version (from Docker Hub): seed and sanity check
# ================================================================
- name: Old version / Start container
run: |
docker run -d \
--name twenty-old \
--network twenty-net \
-p 3000:3000 \
-e NODE_PORT=3000 \
-e SERVER_URL=http://localhost:3000 \
-e PG_DATABASE_URL=${OLD_PG_DATABASE_URL} \
-e REDIS_URL=${OLD_REDIS_URL} \
-e APP_SECRET=${APP_SECRET} \
"twentycrm/twenty:${FROM_VERSION}"
- name: Old version / Wait for API ready
run: |
TIMEOUT=600
CONTAINER="twenty-old"
ELAPSED=0
echo "Waiting for Twenty on port 3000 (timeout ${TIMEOUT}s)…"
while true; do
healthz=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3000/healthz" 2>/dev/null || echo "000")
graphql=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3000/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{ __typename }"}' 2>/dev/null || echo "000")
if [ "$healthz" = "200" ] && [ "$graphql" != "000" ] && [ "$graphql" != "404" ]; then
echo "Twenty is ready! healthz=${healthz} graphql=${graphql} (took ~${ELAPSED}s)"
break
fi
container_status=$(docker inspect --format='{{.State.Status}}' "$CONTAINER" 2>/dev/null || echo "unknown")
if [ "$container_status" = "exited" ]; then
echo "::error::Container ${CONTAINER} exited unexpectedly"
docker logs "$CONTAINER"
exit 1
fi
ELAPSED=$((ELAPSED + 5))
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "::error::Twenty did not become ready within ${TIMEOUT}s (healthz=${healthz} graphql=${graphql})"
docker logs "$CONTAINER" 2>&1 | tail -100
exit 1
fi
echo " … waited ${ELAPSED}s [healthz=${healthz} graphql=${graphql}]"
sleep 5
done
- name: Old version / Seed dev data
run: |
docker exec twenty-old sh -c "cd /app/packages/twenty-server && yarn command:prod workspace:seed:dev"
- name: Old version / Sanity check seeded data
# Verify the seed populated the database directly via psql — avoids
# needing an API token on the old version (v1.22 has no token-mint command).
run: |
COUNT=$(docker exec -e PGPASSWORD=postgres postgres \
psql -U postgres -d default -tAc \
"SELECT count(*) FROM core.workspace WHERE id = '${SEED_WORKSPACE_ID}';")
echo "Seed workspaces found: ${COUNT}"
if [ "${COUNT:-0}" -lt 1 ]; then
echo "Sanity check failed: seed workspace ${SEED_WORKSPACE_ID} not found"
docker logs twenty-old
exit 1
fi
- name: Old version / Stop container
run: |
mkdir -p upgrade-logs
docker logs twenty-old > upgrade-logs/twenty-old.log 2>&1 || true
docker stop twenty-old
docker rm twenty-old
# ================================================================
# Phase 2 — Current version (from source): upgrade on same database
# ================================================================
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore server build cache
# Best-effort: a hit skips the cold rebuild; a miss simply builds from scratch below.
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Write .env
run: npx nx reset:env twenty-server
- name: Server / Align .env with seeded database
run: |
ENV_FILE="packages/twenty-server/.env"
# Must match the old container's APP_SECRET (same DB, same run-generated secret).
sed -i "s|^APP_SECRET=.*|APP_SECRET=${APP_SECRET}|" "$ENV_FILE"
# PG_DATABASE_URL / REDIS_URL come from .env.example (postgres@localhost:5432/default,
# redis@localhost:6379). APP_SECRET is intentionally not echoed.
grep -E '^(PG_DATABASE_URL|REDIS_URL)=' "$ENV_FILE"
- name: Server / Build
run: npx nx build twenty-server
- name: Server / Flush cache
run: npx nx run twenty-server:command-no-deps -- cache:flush
- name: Server / Run upgrade command
run: npx nx run twenty-server:command-no-deps -- upgrade --verbose
- name: Server / Start
run: |
mkdir -p upgrade-logs
npx nx start:ci twenty-server > upgrade-logs/twenty-new.log 2>&1 &
- name: Waiting for server starting...
run: |
for i in {1..30}; do
if curl -f -s http://localhost:3000/healthz > /dev/null; then
echo "Server ready!"
exit 0
fi
echo "Waiting..."
sleep 5
done
echo "::error::Server did not become healthy in time"
tail -100 upgrade-logs/twenty-new.log || true
exit 1
# ================================================================
# Phase 3 — Smoke tests on upgraded instance
# ================================================================
# Health is already asserted by the "Waiting for server starting..." loop.
- name: Smoke / Generate API token
# Mint a token against the running upgraded server (signed with this run's
# APP_SECRET). No token is committed to the repo.
run: |
OUTPUT=$(NO_COLOR=1 npx nx run twenty-server:command-no-deps -- \
workspace:generate-api-key -w "${SEED_WORKSPACE_ID}" -n "cross-version-upgrade-ci")
echo "$OUTPUT"
TOKEN=$(echo "$OUTPUT" | grep -o 'TOKEN:[A-Za-z0-9._-]*' | head -1 | cut -d: -f2-)
if [ -z "$TOKEN" ]; then
echo "::error::Failed to generate API token"
exit 1
fi
echo "::add-mask::$TOKEN"
printf 'API_KEY=%s\n' "$TOKEN" >> "$GITHUB_ENV"
- name: Smoke / Upgrade status
run: |
RAW=$(FORCE_COLOR=0 NO_COLOR=1 npx nx run twenty-server:command-no-deps -- upgrade:status 2>&1)
# chalk colorizes even with NO_COLOR under nx — strip ANSI codes before asserting.
OUTPUT=$(printf '%s\n' "$RAW" | sed -E 's/\x1b\[[0-9;]*m//g')
echo "$OUTPUT"
echo "$OUTPUT" > upgrade-logs/upgrade-status.log
echo "$OUTPUT" | grep -q "Instance: Up to date" \
|| { echo "FAIL: Instance is not up to date"; exit 1; }
echo "$OUTPUT" | grep -q "0 behind, 0 failed" \
|| { echo "FAIL: Some workspaces are behind or failed"; exit 1; }
- name: Smoke / Query companies
run: |
HTTP_CODE=$(curl -s -o /tmp/response.json -w '%{http_code}' http://localhost:3000/graphql \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{"query":"{ companies { edges { node { id name } } } }"}' || true)
echo "HTTP status: ${HTTP_CODE}"
jq . /tmp/response.json 2>/dev/null || cat /tmp/response.json
if [ -z "$HTTP_CODE" ] || [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
echo "FAIL: API returned HTTP ${HTTP_CODE}"; exit 1
fi
jq -e '.data.companies.edges | length > 0' /tmp/response.json \
|| { echo "FAIL: expected companies with edges"; exit 1; }
- name: Smoke / Query people
run: |
HTTP_CODE=$(curl -s -o /tmp/response.json -w '%{http_code}' http://localhost:3000/graphql \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{"query":"{ people { edges { node { id } } } }"}' || true)
echo "HTTP status: ${HTTP_CODE}"
jq . /tmp/response.json 2>/dev/null || cat /tmp/response.json
if [ -z "$HTTP_CODE" ] || [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
echo "FAIL: API returned HTTP ${HTTP_CODE}"; exit 1
fi
jq -e '.data.people.edges != null' /tmp/response.json \
|| { echo "FAIL: expected people edges to be non-null"; exit 1; }
- name: Smoke / Query metadata objects
run: |
HTTP_CODE=$(curl -s -o /tmp/response.json -w '%{http_code}' http://localhost:3000/metadata \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{"query":"{ objects { edges { node { id nameSingular } } } }"}' || true)
echo "HTTP status: ${HTTP_CODE}"
jq . /tmp/response.json 2>/dev/null || cat /tmp/response.json
if [ -z "$HTTP_CODE" ] || [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
echo "FAIL: API returned HTTP ${HTTP_CODE}"; exit 1
fi
jq -e '.data.objects.edges | length > 0' /tmp/response.json \
|| { echo "FAIL: expected metadata objects"; exit 1; }
# ================================================================
# Phase 4 — Collect logs & cleanup
# ================================================================
- name: Upload upgrade logs
if: always()
id: upload-logs
uses: actions/upload-artifact@v4
with:
name: cross-version-upgrade-logs
path: upgrade-logs/
if-no-files-found: ignore
- name: Job summary
if: always()
env:
RESULT: ${{ job.status }}
ARTIFACT_URL: ${{ steps.upload-logs.outputs.artifact-url }}
run: |
{
echo "## Cross-Version Upgrade: ${FROM_VERSION} → current (from source)"
echo ""
echo "| | |"
echo "|---|---|"
echo "| **Result** | \`${RESULT}\` |"
echo "| **Old version** | \`${FROM_VERSION}\` (Docker Hub) |"
echo "| **New version** | current branch (built from source) |"
if [ -n "${ARTIFACT_URL:-}" ]; then
echo ""
echo "**[Download full logs](${ARTIFACT_URL})**"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Cleanup
if: always()
run: |
docker stop twenty-old 2>/dev/null || true
docker rm twenty-old 2>/dev/null || true
docker stop postgres 2>/dev/null || true
docker rm postgres 2>/dev/null || true
docker stop redis 2>/dev/null || true
docker rm redis 2>/dev/null || true
docker network rm twenty-net 2>/dev/null || true

53
.github/workflows/ci-docs.yaml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: CI Docs
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-ui/**
packages/twenty-shared/**
.github/workflows/ci-docs.yaml
packages/twenty-docs/**
docs-lint:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Docs / Lint
run: npx nx lint twenty-docs
- name: Docs / Test
run: npx nx test twenty-docs
- name: Docs / Generated UI references
run: npx nx check:ui twenty-docs
- name: Docs / Storybook embeds
run: npx nx check:ui-stories twenty-docs

View File

@@ -0,0 +1,79 @@
name: DPA Sub-Processor Sync
on:
schedule:
- cron: '0 6 * * 1' # Weekly, Monday at 6 AM UTC
workflow_dispatch: # Allow manual trigger
permissions:
contents: write
pull-requests: write
jobs:
sync-subprocessors:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
NODE_OPTIONS: '--max-old-space-size=4096'
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Run sub-processor sync
run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/dpa-sync-subprocessors.ts
- name: Check for changes
id: changes
run: |
if git diff --quiet packages/twenty-server/src/engine/core-modules/dpa/constants/subprocessors.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Create pull request
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync DPA sub-processors from trust center'
title: 'chore: sync DPA sub-processors from trust center'
body: |
Automated weekly sync of `subprocessors.json` from Twenty's Trust Center (OneLeet).
This keeps the DPA's Annex C (the SCC Annex III list of Sub-Processors) in
lockstep with the canonical list at https://trust.twenty.com — the Trust
Center is the single source of truth; this file is generated from it.
**Please review before merging** — confirm the added/removed Sub-Processors
are expected, and that customers were notified per Section 6.2 where required.
branch: chore/dpa-subprocessors-sync
base: main
labels: automated
delete-branch: true
- name: Mint twenty-infra dispatch token
id: app-token
if: steps.changes.outputs.changed == 'true'
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.TWENTY_WORKFLOW_DISPATCHER_CLIENT_ID }}
private-key: ${{ secrets.TWENTY_WORKFLOW_DISPATCHER_PRIVATE_KEY }}
owner: twentyhq
repositories: twenty-infra
permission-actions: write
- name: Trigger automerge
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh workflow run automerge-i18n.yaml --repo twentyhq/twenty-infra --ref main

571
.github/workflows/ci-e2e-main.yaml vendored Normal file
View File

@@ -0,0 +1,571 @@
name: CI E2E Main
on:
push:
branches: [main]
pull_request:
types: [labeled, synchronize, opened, reopened]
# Manual QA Scout run against an already-merged PR (executes on main).
workflow_dispatch:
inputs:
pr_number:
description: "Merged PR number for the QA Scout (defaults to the PR of the latest main commit)"
required: false
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
e2e-test:
if: >
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'qa-scout')) ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'run-merge-queue'))
runs-on: ubuntu-latest-8-cores
# 60 so build + suite plus the QA Scout's 15-minute step cap cannot hit
# the job ceiling: a job timeout overrides step continue-on-error and
# would redden the status check.
timeout-minutes: 60
outputs:
qa-scout-run: ${{ steps.qa-scout-pr.outputs.run }}
qa-scout-pr-number: ${{ steps.qa-scout-pr.outputs.pr_number }}
env:
NODE_OPTIONS: "--max-old-space-size=10240"
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
# Audit only: records every outbound endpoint this job touches in the
# StepSecurity insights, blocks nothing, changes no behavior. This is
# the observation phase for the QA Scout containment follow-up: the
# agent step's allowlist scopes what it does, not where the runner can
# reach, so once the job's endpoint set is known and stable from a few
# weeks of audit data, egress-policy block with that allowlist makes
# real-time exfiltration impossible rather than merely unscoped.
- name: Harden runner (egress audit)
# Fail-open while auditing: an observer must never redden the status
# check. The eventual block-mode flip drops this so it fails closed.
continue-on-error: true
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore Nx build cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
with:
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
v4-e2e-build-${{ github.ref_name }}-
v4-e2e-build-main-
path: |
.nx
node_modules/.cache
packages/*/node_modules/.cache
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Install Playwright Browsers
run: npx nx setup twenty-e2e-testing
- name: Setup environment files
run: |
cp packages/twenty-front/.env.example packages/twenty-front/.env
npx nx reset:env:e2e-testing-server twenty-server
- name: Build frontend
run: NODE_ENV=production npx nx build twenty-front
- name: Build server
run: npx nx build twenty-server
- name: Save Nx build cache
if: always()
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
with:
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
path: |
.nx
node_modules/.cache
packages/*/node_modules/.cache
- name: Create and setup database
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
# Cookie sessions are credentialed, so the front only authenticates from
# the API's own origin, workspace subdomains included.
- name: Serve the frontend from the server
run: cp -r packages/twenty-front/build packages/twenty-server/dist/front
# Logs are teed to files so the QA Scout can diff its own log window; the
# console output is unchanged.
- name: Start server
run: |
mkdir -p /tmp/qa-scout
npx nx run twenty-server:start:ci 2>&1 | tee /tmp/qa-scout/server.log &
echo "Waiting for server to be ready..."
timeout 120 bash -c 'until curl -sf http://localhost:3000/healthz > /dev/null; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker 2>&1 | tee /tmp/qa-scout/worker.log &
echo "Worker started"
- name: Run Playwright tests
env:
FRONTEND_BASE_URL: http://localhost:3000
run: npx nx test twenty-e2e-testing
- name: Upload Playwright results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-results
path: |
packages/twenty-e2e-testing/run_results/
packages/twenty-e2e-testing/test-results/
retention-days: 7
# ── QA Scout ─────────────────────────────────────────────────────────
# Post-merge browser QA agent (.claude/skills/qa-scout/SKILL.md): scopes
# scenarios from the merged PR's diff, drives the already-running app,
# watches server/worker logs, and writes a verdict. Shadow-safe: every
# step is continue-on-error so the Scout can never redden main CI. Runs
# after the deterministic suite even when it failed (a broken main is
# exactly when a loud report helps), but not on merge-queue PR runs.
- name: QA Scout / Prepare merged PR context
id: qa-scout-pr
if: >-
${{ !cancelled() && (
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'qa-scout') &&
github.event.pull_request.head.repo.full_name == github.repository)
) }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
mkdir -p /tmp/qa-scout/context /tmp/qa-scout/output /tmp/qa-scout/browser
MODE="post-merge"
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
# Label-triggered pre-merge validation: the checkout IS the PR
# head, so the Scout QAs the PR itself before it merges.
MODE="pre-merge"
PR_NUMBER="${EVENT_PR_NUMBER:-}"
else
PR_NUMBER="${INPUT_PR_NUMBER:-}"
if [ -z "$PR_NUMBER" ]; then
PR_NUMBER=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" --jq '.[0].number // empty' || true)
fi
fi
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "No target PR resolved; skipping QA Scout."
echo "run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
--json number,title,body,author,url,mergedAt > /tmp/qa-scout/context/pr.json
# The post-merge Scout QAs main, so an unmerged PR (reachable via
# manual dispatch) would get a verdict about code main does not
# contain. Pre-merge label runs check out the PR head, so the guard
# does not apply there.
if [ "$MODE" = "post-merge" ] && [ -z "$(jq -r '.mergedAt // empty' /tmp/qa-scout/context/pr.json)" ]; then
echo "PR #${PR_NUMBER} is not merged; skipping QA Scout."
echo "run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s\n' "$MODE" > /tmp/qa-scout/context/mode
TITLE=$(jq -r '.title' /tmp/qa-scout/context/pr.json)
# Mirrors mustBeQa in the eng app: [no-qa]/[noqa] escape hatch, i18n
# translation PRs are auto-generated and never QA candidates.
LOWER=$(printf '%s' "$TITLE" | tr '[:upper:]' '[:lower:]')
case "$LOWER" in
*"[no-qa]"* | *"[noqa]"*)
echo "Skipping QA Scout: [no-qa] in PR title."
echo "run=false" >> "$GITHUB_OUTPUT"
exit 0
;;
i18n*translation*)
echo "Skipping QA Scout: i18n translation PR."
echo "run=false" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
# gh rejects --slurp together with --jq, so aggregate the per-page
# jq output with a local jq instead.
gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" \
--jq '.[] | {path: .filename, additions, deletions, status}' \
| jq -s '.' > /tmp/qa-scout/context/files.json
gh pr diff "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" > /tmp/qa-scout/full.diff
DIFF_BYTES=$(wc -c < /tmp/qa-scout/full.diff)
head -c 2000000 /tmp/qa-scout/full.diff > /tmp/qa-scout/context/pr.diff
if [ "$DIFF_BYTES" -gt 2000000 ]; then
printf '\n[pr.diff truncated: 2000000 of %s bytes shown; files.json is complete]\n' "$DIFF_BYTES" >> /tmp/qa-scout/context/pr.diff
fi
rm /tmp/qa-scout/full.diff
cat > /tmp/qa-scout/mcp.json <<'EOF'
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"-y",
"@playwright/mcp@0.0.79",
"--headless",
"--browser", "chrome",
"--isolated",
"--output-dir", "/tmp/qa-scout/browser",
"--allowed-origins", "http://localhost:3000;http://127.0.0.1:3000;http://app.localhost:3000;http://apple.localhost:3000"
]
}
}
}
EOF
echo "run=true" >> "$GITHUB_OUTPUT"
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
# The Scout invokes the Claude Code CLI directly rather than through
# claude-code-action: the action rejects push events outright
# ("Unsupported event type: push", still true at its latest version),
# which silently killed the post-merge mode this Scout exists for, and
# its GitHub bootstrap was the only reason a GitHub token entered this
# step at all. Direct invocation runs on every trigger and leaves the
# agent holding no GitHub credential whatsoever; the PR comment is
# posted by the qa-scout-comment job from the uploaded verdict, so
# untrusted page/log content can never reach a privileged credential.
# The Bash allowlist scopes the agent to its task; it is not the
# containment boundary (psql alone is arbitrarily powerful in this
# disposable env). Containment is the absence of credentials beyond
# inference, the scrub step, and Actions secret masking on the token.
- name: QA Scout / Run
id: qa-scout-run
if: ${{ !cancelled() && steps.qa-scout-pr.outputs.run == 'true' }}
continue-on-error: true
timeout-minutes: 15
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
PGPASSWORD: postgres
run: |
set -euo pipefail
npm install -g @anthropic-ai/claude-code@2.1.247
# stream-json teed into the artifact tree, ahead of the scrub,
# keeps per-turn detail and permission denials so allowlist tuning
# reads real data. stderr and the exit code are kept too: a run that
# never reaches a result line (bad flag, exhausted quota) explains
# itself only there, and the summary step reads both back.
set +e
claude -p "Read .claude/skills/qa-scout/SKILL.md and follow it exactly. This is the CI invocation: every path and credential in the skill's Inputs table applies verbatim." \
--max-turns 150 \
--model opus \
--add-dir /tmp/qa-scout \
--strict-mcp-config \
--mcp-config /tmp/qa-scout/mcp.json \
--allowedTools "Read,Grep,Glob,Write,TodoWrite,Bash(cat *),Bash(tail *),Bash(head *),Bash(wc *),Bash(ls *),Bash(grep *),Bash(jq *),Bash(echo *),Bash(diff *),Bash(sort *),Bash(uniq *),Bash(cut *),Bash(tr *),Bash(mkdir *),Bash(sleep *),Bash(date *),Bash(psql *),mcp__playwright" \
--output-format stream-json --verbose \
2> >(tee /tmp/qa-scout/agent-stderr.log >&2) \
| tee /tmp/qa-scout/execution.json
AGENT_EXIT=${PIPESTATUS[0]}
set -e
printf '%s' "$AGENT_EXIT" > /tmp/qa-scout/agent-exit.txt
exit "$AGENT_EXIT"
# The agent process necessarily holds the inference credential, and a
# prompt-injected agent could copy it into any file it can write. The
# artifact ships the whole /tmp/qa-scout tree (context, browser output,
# logs, report), so redact Anthropic token shapes across all of it, in
# the trusted lane, before anything is published.
- name: QA Scout / Scrub outputs
if: ${{ !cancelled() && steps.qa-scout-pr.outputs.run == 'true' }}
continue-on-error: true
run: |
set -euo pipefail
# NUL-delimited paths and no -I: an agent-chosen filename with
# spaces or newlines, or a token tucked into a binary-looking file,
# must not escape redaction.
if grep -rq 'sk-ant-' /tmp/qa-scout 2>/dev/null; then
echo "Redacting Anthropic token shapes from Scout files:"
grep -rl 'sk-ant-' /tmp/qa-scout 2>/dev/null
grep -rlZ 'sk-ant-' /tmp/qa-scout 2>/dev/null \
| xargs -0 sed -i -E 's/sk-ant-[A-Za-z0-9_-]+/[REDACTED]/g'
fi
# Always leaves a breadcrumb on push/dispatch runs so a broken prepare
# step can never disable the Scout silently.
- name: QA Scout / Job summary
if: ${{ !cancelled() && steps.qa-scout-pr.outcome != 'skipped' }}
continue-on-error: true
env:
SCOUT_RUN: ${{ steps.qa-scout-pr.outputs.run }}
SCOUT_PREP_OUTCOME: ${{ steps.qa-scout-pr.outcome }}
run: |
# Why the run ended, from the evidence the agent leaves behind: the
# final stream-json result when it produced one, stderr when it died
# before that. Without this the summary can only say "no report".
scout_failure_reason() {
local result_line exit_code
exit_code=$(cat /tmp/qa-scout/agent-exit.txt 2>/dev/null || echo "unknown")
echo "**Why it stopped** (exit code \`${exit_code}\`)"
echo ""
result_line=$(grep -a '"type":"result"' /tmp/qa-scout/execution.json 2>/dev/null | tail -1 || true)
if [ -n "$result_line" ]; then
printf '%s' "$result_line" | jq -r '
"- outcome: `\(.subtype // "unknown")`" +
" · \(.num_turns // "?") turns" +
" · \(((.duration_ms // 0) / 60000 * 10 | round) / 10) min" +
" · $\(((.total_cost_usd // 0) * 100 | round) / 100)",
(if ((.errors? // []) | length) > 0 then "- error: " + ((.errors | join("; "))) else empty end)
' 2>/dev/null || echo "- the final result line could not be parsed; see the artifact"
else
echo "- the agent produced no result line, so it failed before or during startup"
fi
if [ -s /tmp/qa-scout/agent-stderr.log ]; then
echo ""
echo "<details><summary>stderr (last 10 lines)</summary>"
echo ""
echo '```'
tail -n 10 /tmp/qa-scout/agent-stderr.log
echo '```'
echo ""
echo "</details>"
fi
}
{
echo "## QA Scout"
if [ -f /tmp/qa-scout/output/report.md ]; then
if [ "$(jq -r '.status // "final"' /tmp/qa-scout/output/verdict.json 2>/dev/null || echo final)" != "final" ]; then
echo "> [!WARNING]"
echo "> The Scout stopped before finishing; the partial state it left follows."
echo ""
scout_failure_reason
echo ""
fi
cat /tmp/qa-scout/output/report.md
elif [ -f /tmp/qa-scout/output/verdict.json ]; then
jq -r '"**\(.verdict)**: \(.headline // "no headline recorded")\n\n_Full report missing (agent stopped early)._"' /tmp/qa-scout/output/verdict.json
echo ""
scout_failure_reason
elif [ "$SCOUT_RUN" = "true" ]; then
echo "The Scout produced no verdict."
echo ""
scout_failure_reason
elif [ "$SCOUT_PREP_OUTCOME" = "failure" ]; then
echo "Did not run: the prepare step failed; see its logs."
else
echo "Did not run: skipped by the prepare step (no merged PR, [no-qa], or i18n translation PR)."
fi
} >> "$GITHUB_STEP_SUMMARY"
# Surfaced on the run page too, so a dead Scout is visible without
# opening the summary.
if [ "$SCOUT_RUN" = "true" ] && [ ! -f /tmp/qa-scout/output/report.md ]; then
echo "::warning title=QA Scout produced no report::$(head -n 1 /tmp/qa-scout/agent-stderr.log 2>/dev/null || echo 'see the job summary for why')"
fi
- name: QA Scout / Upload report
if: ${{ !cancelled() && steps.qa-scout-pr.outputs.run == 'true' }}
continue-on-error: true
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: qa-scout-report
path: |
/tmp/qa-scout/context/
/tmp/qa-scout/output/
/tmp/qa-scout/browser/
/tmp/qa-scout/server.log
/tmp/qa-scout/worker.log
/tmp/qa-scout/execution.json
/tmp/qa-scout/agent-stderr.log
/tmp/qa-scout/agent-exit.txt
if-no-files-found: ignore
retention-days: 14
ci-e2e-main-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
notify-main-ci-failure:
if: always() && github.event_name == 'push' && contains(needs.*.result, 'failure')
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [e2e-test]
steps:
- name: Ping engineering on main CI failure
run: |
curl -fsS -X POST https://engineering.twenty.com/s/main-ci-failing \
-H 'Content-Type: application/json' \
-d '{
"commit": "${{ github.sha }}",
"actor": "${{ github.actor }}",
"run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}'
# Posts the QA Scout verdict as a comment on the merged PR. Separate job so
# pull-requests:write never coexists with the agent step: the agent reads
# untrusted page/log content, so the write TARGET is bound to the prepare
# step's trusted pr_number output and the agent-authored artifact can only
# influence the comment's content, never where it lands.
# Quiet on PASS; FAIL/INVESTIGATE post (or update) a single marked comment.
# Kill switch: set repo variable QA_SCOUT_DISABLE_COMMENTS=true.
qa-scout-comment:
if: >
always() && !cancelled() &&
needs.e2e-test.outputs.qa-scout-run == 'true' &&
vars.QA_SCOUT_DISABLE_COMMENTS != 'true'
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [e2e-test]
permissions:
contents: read
pull-requests: write
steps:
- name: Download QA Scout report
id: download
continue-on-error: true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: qa-scout-report
path: qa-scout-report
- name: Post verdict comment
if: steps.download.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
PR_NUMBER: ${{ needs.e2e-test.outputs.qa-scout-pr-number }}
run: |
set -euo pipefail
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "No trusted PR number from the prepare step; nothing to post."
exit 0
fi
VERDICT_FILE=$(find qa-scout-report -name verdict.json | head -1 || true)
REPORT_FILE=$(find qa-scout-report -name report.md | head -1 || true)
if [ -z "$VERDICT_FILE" ]; then
echo "No verdict in the artifact; nothing to post."
exit 0
fi
# A run killed on turns or time can leave the verdict without the
# report; a synthesized body beats silence on a FAIL.
if [ -z "$REPORT_FILE" ]; then
REPORT_FILE=report-fallback.md
jq -r '"**\(.verdict)**: \(.headline // "no headline recorded")\n\n" + ([.scenarios[]? | "- \(.name): \(.result)"] | join("\n")) + "\n\n_Full report missing (agent stopped early); details in the qa-scout-report artifact._"' \
"$VERDICT_FILE" > "$REPORT_FILE"
fi
VERDICT=$(jq -r '.verdict // empty' "$VERDICT_FILE")
case "$VERDICT" in
PASS | INVESTIGATE | FAIL) ;;
*)
echo "Invalid or missing verdict '${VERDICT}' in verdict.json; not posting."
exit 0
;;
esac
# A checkpoint left by a run that died is not a finding: it says
# the Scout failed, not that the PR is broken. Only a finished run
# speaks, unless the partial state already caught something real: a
# failed scenario, or a top-level FAIL (which is how a boot failure
# arrives, before any scenario exists to fail).
STATUS=$(jq -r '.status // "final"' "$VERDICT_FILE" 2>/dev/null || echo final)
FAILED_COUNT=$(jq '[.scenarios[]? | select(.result == "fail")] | length' "$VERDICT_FILE" 2>/dev/null || echo 0)
if [ "$STATUS" != "final" ] && [ "${FAILED_COUNT:-0}" -eq 0 ] && [ "$VERDICT" != "FAIL" ]; then
echo "Scout did not finish (status=${STATUS}) and recorded no failing scenario; nothing to post. See the job summary and artifact."
exit 0
fi
ARTIFACT_PR=$(jq -r '.prNumber // empty' "$VERDICT_FILE")
if [ "$ARTIFACT_PR" != "$PR_NUMBER" ]; then
echo "verdict.json prNumber (${ARTIFACT_PR:-none}) differs from the trusted target #${PR_NUMBER}; the trusted target wins."
fi
EXISTING=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments?per_page=100" \
--jq '.[] | select((.body // "") | startswith("<!-- qa-scout -->")) | .id' \
| jq -rs 'first // empty')
if [ "$VERDICT" = "PASS" ]; then
if [ -z "$EXISTING" ]; then
echo "Verdict PASS for PR #${PR_NUMBER}; staying quiet (report is in the job summary and artifact)."
exit 0
fi
# A stale FAIL/INVESTIGATE comment misleads forever if a later
# PASS never speaks: refresh the existing comment, still without
# creating one.
{
echo "<!-- qa-scout -->"
echo "> [!NOTE]"
echo "> A newer QA Scout run passed; the earlier finding on this PR no longer stands."
echo ""
echo "[QA Scout run](${RUN_URL}) · verdict: PASS · re-run: Actions → CI E2E Main → Run workflow with pr_number=${PR_NUMBER}"
} > comment.md
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING}" -F body=@comment.md
exit 0
fi
{
echo "<!-- qa-scout -->"
if [ "$STATUS" != "final" ]; then
echo "> [!WARNING]"
echo "> The QA Scout stopped before finishing; what follows is partial."
echo ""
fi
# Drops a multi-byte character split by the byte cap so the payload
# stays valid UTF-8 (iconv -c exits 1 on exactly this case, so
# python does the lenient decode).
head -c 60000 "$REPORT_FILE" | python3 -c "import sys; sys.stdout.write(sys.stdin.buffer.read().decode('utf-8', 'ignore'))"
echo ""
echo ""
echo "[QA Scout run](${RUN_URL}) · verdict: ${VERDICT} · re-run: Actions → CI E2E Main → Run workflow with pr_number=${PR_NUMBER}"
} > comment.md
# Belt over the scrub step's suspenders: a token shape surviving to
# here means the scrub was bypassed, so fail loudly instead of
# publishing.
if grep -q 'sk-ant-' comment.md; then
echo "Refusing to post: comment contains an Anthropic token shape."
exit 1
fi
if [ -n "$EXISTING" ]; then
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING}" -F body=@comment.md
else
gh api --method POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" -F body=@comment.md
fi

68
.github/workflows/ci-emails.yaml vendored Normal file
View File

@@ -0,0 +1,68 @@
name: CI Emails
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-emails/**
emails-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-emails
run: npx nx build twenty-emails
- name: Extract translations (lingui)
run: npx nx run twenty-emails:lingui:extract
- name: Compile translations (lingui)
run: npx nx run twenty-emails:lingui:compile
- name: Run email tests
run: |
# Start the email server in the background
npx nx run twenty-emails:start &
SERVER_PID=$!
# Wait for server to start
sleep 20
# Check if server is running
if ! curl -s http://localhost:4001/preview/test.email > /dev/null; then
echo "Email server failed to start"
kill $SERVER_PID
exit 1
fi
# Kill the server
kill $SERVER_PID
ci-emails-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, emails-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View File

@@ -0,0 +1,94 @@
name: CI Example App Hello World
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-apps/examples/hello-world/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
example-app-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build SDK packages
run: npx nx build twenty-sdk
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Run integration tests
working-directory: packages/twenty-apps/examples/hello-world
run: npx vitest run
ci-example-app-hello-world-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, example-app-hello-world]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View File

@@ -0,0 +1,121 @@
name: CI Example App Postcard
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-apps/examples/postcard/**
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
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build SDK packages
run: npx nx build twenty-sdk
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Run integration tests
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
runs-on: ubuntu-latest
needs: [changed-files-check, example-app-postcard]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View File

@@ -0,0 +1,115 @@
name: CI Front Component Renderer
on:
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-front-component-renderer/**
packages/twenty-sdk/**
packages/twenty-shared/**
packages/twenty-ui/**
!packages/twenty-sdk/package.json
renderer-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [build, typecheck, lint]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }}
run: npx nx ${{ matrix.task }} twenty-front-component-renderer
renderer-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build storybook
run: npx nx storybook:build twenty-front-component-renderer
- name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
retention-days: 1
renderer-sb-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: renderer-sb-build
env:
STORYBOOK_URL: http://localhost:6008
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-sdk
- name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd packages/twenty-front-component-renderer
npx playwright install chromium
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-front-component-renderer/storybook-static --port 6008 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6008 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-front-component-renderer
ci-front-component-renderer-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
renderer-task,
renderer-sb-build,
renderer-sb-test,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

274
.github/workflows/ci-front.yaml vendored Normal file
View File

@@ -0,0 +1,274 @@
name: CI Front
on:
pull_request:
merge_group:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}${{ github.ref == 'refs/heads/main' && format('-{0}', github.sha) || '' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
# restore-cache action adds 'v4-' prefix and '-<branch>-<sha>' suffix to the key
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-ubuntu-latest-8-cores-runner
STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION: v4-storybook-build-ubuntu-latest-8-cores-runner-${{ github.ref_name }}-${{ github.sha }}
jobs:
changed-files-check:
if: github.event_name == 'pull_request'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-front/**
packages/twenty-front-component-renderer/**
packages/twenty-ui/**
packages/twenty-shared/**
packages/twenty-sdk/**
!packages/twenty-sdk/package.json
front-sb-build:
needs: changed-files-check
if: >-
always() &&
(github.event_name == 'push' ||
needs.changed-files-check.outputs.any_changed == 'true')
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
env:
REACT_APP_SERVER_BASE_URL: http://localhost:3000
steps:
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Diagnostic disk space issue
run: df -h
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Front / Build storybook
run: npx nx storybook:build twenty-front
- name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: storybook-static
path: packages/twenty-front/storybook-static
retention-days: 1
- name: Save storybook build cache
uses: ./.github/actions/save-cache
with:
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION }}
front-sb-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: front-sb-build
if: always() && needs.front-sb-build.result == 'success'
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
storybook_scope: ${{ github.event_name == 'push' && fromJSON('["modules", "pages"]') || fromJSON('["modules", "pages", "performance"]') }}
env:
SHARD_COUNTER: 4
REACT_APP_SERVER_BASE_URL: http://localhost:3000
STORYBOOK_URL: http://localhost:6006
steps:
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore storybook build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION }}
- name: Clean stale storybook vitest cache
run: rm -rf packages/twenty-front/node_modules/.cache/storybook
- name: Build dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-ui
npx nx build twenty-front-component-renderer
- name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: storybook-static
path: packages/twenty-front/storybook-static
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd packages/twenty-front
npx playwright install chromium
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-front/storybook-static --port 6006 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6006 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
- name: Upload screenshots for visual regression
if: always() && !cancelled() && matrix.storybook_scope != 'performance'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-twenty-front-${{ matrix.storybook_scope }}-${{ matrix.shard }}
path: packages/twenty-front/screenshots
retention-days: 1
overwrite: true
# - name: Rename coverage file
# run: |
# if [ -f "packages/twenty-front/coverage/storybook/coverage-final.json" ]; then
# mv packages/twenty-front/coverage/storybook/coverage-final.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
# else
# echo "Error: coverage-final.json not found"
# ls -la packages/twenty-front/coverage/storybook/ || echo "Coverage directory does not exist"
# exit 1
# fi
# - name: Upload coverage artifact
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# with:
# retention-days: 1
# name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
# path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
# merge-reports-and-check-coverage:
# timeout-minutes: 30
# runs-on: ubuntu-latest
# needs: front-sb-test
# env:
# PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
# strategy:
# matrix:
# storybook_scope: [modules, pages, performance]
# steps:
# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# with:
# fetch-depth: 10
# - name: Install dependencies
# uses: ./.github/actions/yarn-install
# - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
# with:
# pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
# merge-multiple: true
# path: coverage-artifacts
# - name: Merge coverage reports
# run: |
# mkdir -p ${{ env.PATH_TO_COVERAGE }}
# npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
# - name: Checking coverage
# run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
front-sb-screenshots:
timeout-minutes: 5
runs-on: ubuntu-latest
needs: front-sb-test
if: always() && needs.front-sb-test.result == 'success'
steps:
- name: Download shard screenshot artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: argos-screenshots-twenty-front-*
merge-multiple: true
path: screenshots
- name: Upload merged screenshots artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-twenty-front
path: screenshots
retention-days: 1
overwrite: true
front-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=6144'
TASK_CACHE_KEY: front-task-${{ matrix.task }}
strategy:
matrix:
task: [lint, typecheck, test, lingui:extract, lingui:compile]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore ${{ matrix.task }} cache
id: restore-task-cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.TASK_CACHE_KEY }}
- name: Reset .env
uses: ./.github/actions/nx-affected
with:
tag: scope:frontend
tasks: reset:env
- name: Run ${{ matrix.task }} task
id: run-task
uses: ./.github/actions/nx-affected
with:
tag: scope:frontend
tasks: ${{ matrix.task }}
- name: Save ${{ matrix.task }} cache
uses: ./.github/actions/save-cache
with:
key: ${{ steps.restore-task-cache.outputs.cache-primary-key }}
front-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
env:
NODE_OPTIONS: "--max-old-space-size=10240"
ANALYZE: "true"
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Build frontend
run: npx nx build twenty-front
# - name: Upload frontend build artifact
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# with:
# name: frontend-build
# path: packages/twenty-front/build
# retention-days: 1
ci-front-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
front-task,
front-build,
# merge-reports-and-check-coverage,
front-sb-test,
front-sb-build,
front-sb-screenshots,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

60
.github/workflows/ci-merge-queue.yaml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: CI - Merge Queue
# Runs when a PR is queued to merge. GitHub builds the merge candidate on top of
# the current tip of main, so these checks validate against main's real state at
# merge time rather than the PR's (possibly stale) base. This is what closes the
# upgrade-command base-drift race: a command generated against an old version,
# then left behind when main bumps, is caught here even if the PR-level guard
# passed on a stale base.
on:
merge_group:
permissions:
contents: read
pull-requests: read
jobs:
# merge_group carries no PR labels, so resolve them from the queued PR (its
# number is in the merge-queue ref) and pass the version-mutation bypass to the
# guard as an input, which skips only the version-directory step. The job
# always runs and reports a real success/failure, so the required check never
# resolves to a skipped state and a label-lookup failure fails closed. The
# timestamp / append-only check has no bypass and always runs: violating it
# rewinds the upgrade cursor.
upgrade-mutation-guard:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout merge candidate
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 2
- name: Resolve version-mutation bypass label from the queued PR
id: labels
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_REF: ${{ github.event.merge_group.head_ref }}
run: |
set -euo pipefail
PR_NUMBER=$(echo "$HEAD_REF" | sed -n -E 's|.*/pr-([0-9]+)-[0-9a-f]+$|\1|p')
skip=false
if [ -n "$PR_NUMBER" ]; then
LABELS=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json labels --jq '.labels[].name')
echo "Queued PR #$PR_NUMBER labels:"
echo "$LABELS"
if echo "$LABELS" | grep -qxF 'ci:allow-previous-version-upgrade-mutation'; then
skip=true
fi
else
echo "Could not parse a PR number from '$HEAD_REF'; enforcing strictly."
fi
echo "skip=$skip" >> "$GITHUB_OUTPUT"
- name: Validate upgrade command mutations against main
uses: ./.github/actions/upgrade-mutation-guard
with:
base_sha: ${{ github.event.merge_group.base_sha }}
allow_previous_version_mutation: ${{ steps.labels.outputs.skip }}

View File

@@ -0,0 +1,59 @@
name: "Release: create"
permissions:
contents: read
pull-requests: write
on:
workflow_dispatch:
inputs:
version:
required: true
description: Version to release, without the v (e.g. 1.2.3)
ref:
default: main
description: Ref to start the release from (e.g. main, sha)
create_release:
type: boolean
default: true
description: Create a release after merging the PR
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
jobs:
create_pr:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.inputs.ref }}
- name: Sanitize version
id: sanitize
env:
RAW_VERSION: ${{ github.event.inputs.version }}
run: |
VERSION="${RAW_VERSION#v}"
printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT"
- name: Update versions
env:
VERSION: ${{ steps.sanitize.outputs.version }}
run: |
printf '%s\n' "$VERSION" > version.txt
- name: Create Pull Request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
branch: release/${{ steps.sanitize.outputs.version }}
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
committer: Github Action Deploy <github-action-deploy@twenty.com>
author: Github Action Deploy <github-action-deploy@twenty.com>
title: Release v${{ steps.sanitize.outputs.version }}
labels: |
release
${{ github.event.inputs.create_release == true && 'create_release' || '' }}

109
.github/workflows/ci-sdk.yaml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: CI SDK
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-server/**
packages/twenty-ui/**
.github/workflows/ci-sdk.yaml
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
sdk-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test:unit, test:integration]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:sdk
tasks: ${{ matrix.task }}
sdk-e2e-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: [changed-files-check, sdk-test]
if: needs.changed-files-check.outputs.any_changed == 'true'
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build SDK
run: npx nx build twenty-sdk
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server > /tmp/twenty-server.log 2>&1 &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: SDK / Run e2e Tests
run: NODE_ENV=test npx vitest run --config ./vitest.e2e.config.ts
working-directory: packages/twenty-sdk
- name: Server / Dump logs on failure
if: failure()
run: tail -100 /tmp/twenty-server.log || echo "No server log file found"
ci-sdk-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, sdk-test, sdk-e2e-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

415
.github/workflows/ci-server.yaml vendored Normal file
View File

@@ -0,0 +1,415 @@
name: CI Server
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
SERVER_BUILD_CACHE_KEY: server-build
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-server/**
packages/twenty-front/src/generated/**
packages/twenty-front/src/generated-metadata/**
packages/twenty-front/src/generated-admin/**
packages/twenty-client-sdk/**
packages/twenty-emails/**
packages/twenty-shared/**
upgrade-changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-server/src/database/commands/upgrade-version-command/**
packages/twenty-server/src/engine/core-modules/upgrade/**
server-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore server build cache
id: restore-server-build-cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Write .env
run: npx nx reset:env twenty-server
- name: Server / Build
run: npx nx build twenty-server
- name: Save server build cache
uses: ./.github/actions/save-cache
with:
key: ${{ steps.restore-server-build-cache.outputs.cache-primary-key }}
server-lint-typecheck:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Run lint, typecheck & lingui extract
uses: ./.github/actions/nx-affected
with:
tag: scope:backend
tasks: lint,typecheck,lingui:extract
# Separate step so compile never races extract on the .po files.
- name: Server / Run lingui compile
uses: ./.github/actions/nx-affected
with:
tag: scope:backend
tasks: lingui:compile
server-previous-version-upgrade-mutation-guard:
if: github.event_name != 'merge_group'
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 2
# HEAD is the pull_request merge ref; its first parent is the exact main
# commit it was merged with. pull_request.base.sha can lag behind that,
# making commands merged into main since then look added by this PR.
- name: Resolve merged base commit
id: merged-base
shell: bash
run: |
if git rev-parse -q --verify HEAD^2 >/dev/null; then
BASE_SHA=$(git rev-parse HEAD^1)
else
BASE_SHA="${{ github.event.pull_request.base.sha }}"
fi
echo "sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
- name: Validate upgrade command mutations
uses: ./.github/actions/upgrade-mutation-guard
with:
base_sha: ${{ steps.merged-base.outputs.sha }}
allow_previous_version_mutation: ${{ contains(github.event.pull_request.labels.*.name, 'ci:allow-previous-version-upgrade-mutation') }}
server-validation:
needs: server-build
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore server build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Write .env
run: npx nx reset:env twenty-server
- name: Server / Build
run: npx nx build twenty-server
- name: Server / Create DB
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:init:prod
- name: Worker / Run
run: |
timeout 30s npx nx run twenty-server:worker || exit_code=$?
if [ $exit_code -eq 124 ]; then
exit 0
elif [ $exit_code -ne 0 ]; then
exit $exit_code
fi
- name: Server / Start
run: npx nx start:ci twenty-server &
- name: Waiting for server starting...
run: |
for i in {1..60}; do
if curl -f http://localhost:3000/healthz; then
echo "Server ready!"
exit 0
fi
echo "Waiting..."
sleep 2
done
echo "Server did not become healthy in time" >&2
exit 1
- name: Server / Check for Pending Migrations
run: |
npx nx database:migrate:generate twenty-server -- --name pending-migration-check || true
if ! git diff --quiet; then
echo "::error::Unexpected migration files were generated. Please run 'npx nx database:migrate:generate twenty-server -- --name <migration-name>' and commit the result."
echo ""
echo "The following migration changes were detected:"
echo "==================================================="
git diff
echo "==================================================="
echo ""
git checkout -- .
exit 1
fi
- name: Check for Pending Code Generation
run: |
HAS_ERRORS=false
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
npx nx run twenty-front:graphql:generate --configuration=admin
if ! git diff --quiet -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata packages/twenty-front/src/generated-admin; then
echo "::error::GraphQL schema changes detected. Please run the three graphql:generate configurations ('data', 'metadata', 'admin') and commit the changes."
echo ""
echo "The following GraphQL schema changes were detected:"
echo "==================================================="
git diff -- packages/twenty-front/src/generated packages/twenty-front/src/generated-metadata packages/twenty-front/src/generated-admin
echo "==================================================="
echo ""
HAS_ERRORS=true
fi
npx nx run twenty-client-sdk:generate-metadata-client
if ! git diff --quiet -- packages/twenty-client-sdk/src/metadata/generated; then
echo "::error::SDK metadata client changes detected. Please run 'npx nx run twenty-client-sdk:generate-metadata-client' and commit the changes."
echo ""
echo "The following SDK metadata client changes were detected:"
echo "==================================================="
git diff -- packages/twenty-client-sdk/src/metadata/generated
echo "==================================================="
echo ""
HAS_ERRORS=true
fi
if [ "$HAS_ERRORS" = true ]; then
exit 1
fi
server-test:
needs: server-build
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Restore server build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Run Tests (shard ${{ matrix.shard }}/4)
run: npx nx run twenty-server:test:ci --shard=${{ matrix.shard }}/4
server-integration-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: server-build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
clickhouse:
image: clickhouse/clickhouse-server:25.8.8
env:
CLICKHOUSE_PASSWORD: clickhousePassword
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
ports:
- 8123:8123
- 9000:9000
options: >-
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
NODE_ENV: test
ANALYTICS_ENABLED: true
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
CLICKHOUSE_PASSWORD: clickhousePassword
SHARD_COUNTER: 16
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Update .env.test for integrations tests
run: |
echo "" >> .env.test
echo "IS_BILLING_ENABLED=true" >> .env.test
echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
- name: Restore server build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Server / Build
run: npx nx build twenty-server
- name: Build dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-emails
- name: Server / Create Test DB
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Run ClickHouse migrations
run: npx nx clickhouse:migrate twenty-server
- name: Run ClickHouse seeds
run: npx nx clickhouse:seed twenty-server
- name: Server / Run Integration Tests
uses: ./.github/actions/nx-affected
with:
tag: scope:backend
tasks: 'test:integration'
configuration: 'with-db-reset'
args: ${{ format('--shard={0}/{1}', matrix.shard, env.SHARD_COUNTER) }}
# The secure-deployment suite boots the app as a production https
# deployment (SERVER_URL is env-only, forced by its jest config), so it
# cannot ride the main run. One shard is enough.
- name: Server / Run secure-deployment integration tests
if: matrix.shard == 1
run: npx nx run twenty-server:test:integration:secure
cross-version-upgrade:
needs: [upgrade-changed-files-check, server-build]
uses: ./.github/workflows/ci-cross-version-upgrade.yaml
with:
skip: ${{ needs.upgrade-changed-files-check.outputs.any_changed != 'true' }}
discover-public-apps:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
uses: ./.github/workflows/discover-apps.yaml
with:
scope: public
changed-only: false
server-apps-install-smoke:
needs: [discover-public-apps, server-build]
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(needs.discover-public-apps.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Spawn Twenty server from monorepo
id: twenty
uses: ./.github/actions/spawn-twenty-server
with:
source: local
server-build-cache-key: ${{ env.SERVER_BUILD_CACHE_KEY }}
- name: Install app against server
uses: ./.github/actions/test-twenty-app
with:
api-url: ${{ steps.twenty.outputs.server-url }}
api-key: ${{ steps.twenty.outputs.api-key }}
app-path: ${{ matrix.app.path }}
mode: installation-only
ci-server-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
server-build,
server-lint-typecheck,
server-previous-version-upgrade-mutation-guard,
server-validation,
server-test,
server-integration-test,
cross-version-upgrade,
discover-public-apps,
server-apps-install-smoke,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

51
.github/workflows/ci-shared.yaml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: CI Shared
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-shared/**
shared-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=4096'
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:shared
tasks: ${{ matrix.task }}
ci-shared-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, shared-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

View File

@@ -0,0 +1,165 @@
name: CI Docker
permissions:
contents: read
on:
pull_request:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-docker/**
docker-compose.yml
test-compose:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# Pull base images through Google's Docker Hub mirror — avoids Docker Hub
# rate limits and needs no credentials (this repo is public).
- name: Configure Docker Hub mirror
run: |
echo '{"registry-mirrors":["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
- name: Run compose
run: |
echo "Patching docker-compose.yml..."
yq eval 'del(.services.server.image)' -i docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i docker-compose.yml
yq eval '.services.server.restart = "no"' -i docker-compose.yml
echo "Setting up .env file..."
cp .env.example .env
echo "Generating secrets..."
echo "" >> .env
echo "# === Randomly generated secrets ===" >>.env
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
docker compose logs db server -f &
pid=$!
echo "Waiting for database to start..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ]; do
sleep 1;
count=$((count+1));
if [ $(docker inspect --format='{{.State.Status}}' twenty-db-1) = "exited" ]; then
echo "Database exited"
docker compose logs db
exit 1
fi
if [ $count -gt 300 ]; then
echo "Failed to start database after 5 minutes"
docker compose logs db
exit 1
fi
echo "Still waiting for database... (${count}/60)"
done
echo "Waiting for server to start..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 1;
count=$((count+1));
if [ $(docker inspect --format='{{.State.Status}}' twenty-server-1) = "exited" ]; then
echo "Server exited"
docker compose logs server
exit 1
fi
if [ $count -gt 300 ]; then
echo "Failed to start server after 5 minutes"
docker compose logs server
exit 1
fi
echo "Still waiting for server... (${count}/300s)"
done
working-directory: ./packages/twenty-docker/
test-app-dev:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# Pull base images through Google's Docker Hub mirror — avoids Docker Hub
# rate limits and needs no credentials (this repo is public).
- name: Configure Docker Hub mirror
run: |
echo '{"registry-mirrors":["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
- name: Create frontend placeholder
run: |
mkdir -p packages/twenty-front/build
echo '<html><body>CI placeholder</body></html>' > packages/twenty-front/build/index.html
- name: Build app-dev image
run: |
docker build \
--target twenty-app-dev \
-f packages/twenty-docker/twenty/Dockerfile \
-t twenty-app-dev-ci \
.
- name: Start container
run: |
docker run -d --name twenty-app-dev \
-p 2020:2020 \
twenty-app-dev-ci
docker logs twenty-app-dev -f &
- name: Wait for server health
run: |
echo "Waiting for twenty-app-dev to become healthy..."
count=0
while true; do
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:2020/healthz 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
echo "Server is healthy!"
curl -s http://localhost:2020/healthz
break
fi
container_status=$(docker inspect --format='{{.State.Status}}' twenty-app-dev 2>/dev/null || echo "unknown")
if [ "$container_status" = "exited" ]; then
echo "Container exited unexpectedly"
docker logs twenty-app-dev
exit 1
fi
count=$((count+1))
if [ $count -gt 300 ]; then
echo "Server did not become healthy within 5 minutes"
docker logs twenty-app-dev
exit 1
fi
echo "Still waiting... (${count}/300s) [HTTP ${status}]"
sleep 1
done
ci-test-docker-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, test-compose, test-app-dev]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

115
.github/workflows/ci-twenty-apps.yaml vendored Normal file
View File

@@ -0,0 +1,115 @@
name: CI Twenty Apps
on:
push:
branches:
- main
pull_request:
merge_group:
workflow_dispatch:
inputs:
application:
description: 'App folder name to test (e.g. "twenty-linear"). Leave empty to test all apps.'
required: false
default: ''
type: string
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
discover:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/discover-apps.yaml
with:
scope: internal-and-public
changed-only: true
application: ${{ inputs.application }}
ci:
needs: discover
if: needs.discover.outputs.has_apps == 'true'
name: ${{ matrix.app.name }}
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(needs.discover.outputs.matrix) }}
defaults:
run:
working-directory: ${{ matrix.app.path }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: ${{ matrix.app.path }}/yarn.lock
- name: Install dependencies
run: yarn install --immutable
- name: Lint
run: yarn lint
- name: Typecheck
if: matrix.app.hasTypecheck
run: yarn typecheck
- name: Unit tests
if: matrix.app.hasUnit
run: yarn test:unit
integration:
needs: discover
if: needs.discover.outputs.has_apps == 'true'
name: ${{ matrix.app.name }} (${{ matrix.server-source }})
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(needs.discover.outputs.matrix) }}
server-source: [dockerhub-latest, local]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Spawn Twenty server
id: twenty
if: matrix.app.hasIntegration
uses: ./.github/actions/spawn-twenty-server
with:
source: ${{ matrix.server-source }}
- name: Test app against server
if: matrix.app.hasIntegration
uses: ./.github/actions/test-twenty-app
with:
api-url: ${{ steps.twenty.outputs.server-url }}
api-key: ${{ steps.twenty.outputs.api-key }}
app-path: ${{ matrix.app.path }}
mode: ${{ matrix.server-source == 'local' && 'installation-and-integration-test' || 'integration-test-only' }}
ci-twenty-apps-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [discover, ci, integration]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

108
.github/workflows/ci-ui.yaml vendored Normal file
View File

@@ -0,0 +1,108 @@
name: CI UI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}${{ github.ref == 'refs/heads/main' && format('-{0}', github.sha) || '' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name == 'pull_request'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-ui/**
packages/twenty-shared/**
ui-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test, size, pack-size]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }}
run: npx nx ${{ matrix.task }} twenty-ui
- name: Check generated theme artifacts are up to date
if: matrix.task == 'lint'
run: npx nx generate:check twenty-ui
ui-sb-build:
needs: changed-files-check
if: >-
always() &&
(github.event_name == 'push' ||
needs.changed-files-check.outputs.any_changed == 'true')
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build storybook
run: npx nx storybook:build twenty-ui
- name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
retention-days: 1
ui-sb-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: ui-sb-build
if: always() && needs.ui-sb-build.result == 'success'
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
- name: Install Playwright
run: |
cd packages/twenty-ui
npx playwright install
- name: Run storybook tests
run: npx nx storybook:test twenty-ui
- name: Upload screenshots for visual regression
if: always() && !cancelled()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-twenty-ui
path: packages/twenty-ui/screenshots
retention-days: 1
ci-ui-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, ui-task, ui-sb-build, ui-sb-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

45
.github/workflows/ci-utils-test.yaml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: CI Utils Test
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-utils/**
utils-test:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run utils tests
run: npx nx test twenty-utils
ci-utils-test-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, utils-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

75
.github/workflows/ci-utils.yaml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: CI Utils
on:
# it's usually not recommended to use pull_request_target
# but we consider it's safe here if we keep the same steps
# see: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
# and: https://github.com/facebook/react-native/pull/34370/files
pull_request_target:
types: [opened, synchronize, reopened, closed]
permissions:
checks: write
pull-requests: write
statuses: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# We don't cancel in-progress because this workflow is triggered on
# pull_request_target, which means the ref can be the same accross two PRs.
# cancel-in-progress: true
jobs:
danger-js:
timeout-minutes: 5
runs-on: ubuntu-latest
if: github.event.action != 'closed'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Utils / Run Danger.js
# Danger's bundled node-fetch intermittently fails to decode gzipped
# GitHub API responses with ERR_STREAM_PREMATURE_CLOSE. Retry to absorb
# those transient fetch errors instead of failing the whole check.
run: |
cd packages/twenty-utils
for attempt in 1 2 3; do
if npx nx danger:ci; then
exit 0
fi
if [ "${attempt}" -lt 3 ]; then
echo "Danger.js attempt ${attempt} failed, retrying in 5s..."
sleep 5
fi
done
echo "Danger.js failed after 3 attempts"
exit 1
env:
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
congratulate:
timeout-minutes: 3
runs-on: ubuntu-latest
if: github.event.action == 'closed' && github.event.pull_request.merged == true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run congratulate-dangerfile.js
# Same transient ERR_STREAM_PREMATURE_CLOSE protection as danger-js.
run: |
cd packages/twenty-utils
for attempt in 1 2 3; do
if npx nx danger:congratulate; then
exit 0
fi
if [ "${attempt}" -lt 3 ]; then
echo "Danger.js congratulate attempt ${attempt} failed, retrying in 5s..."
sleep 5
fi
done
echo "Danger.js congratulate failed after 3 attempts"
exit 1
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

55
.github/workflows/ci-website.yaml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: CI Website
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-website/**
packages/twenty-shared/**
packages/twenty-ui/**
website-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=6144'
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:website
tasks: ${{ matrix.task }}
ci-website-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, website-task]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

121
.github/workflows/ci-zapier.yaml vendored Normal file
View File

@@ -0,0 +1,121 @@
name: CI Zapier
on:
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
SERVER_SETUP_CACHE_KEY: server-setup
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/twenty-zapier/**
packages/twenty-server/**
!packages/twenty-zapier/package.json
!packages/twenty-zapier/CHANGELOG.md
server-setup:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Write .env
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Server / Build
run: npx nx build twenty-server
- name: Create and setup database
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Server / Start
run: |
npx nx run twenty-server:start:ci &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -sf http://localhost:3000/healthz; do sleep 2; done'
- name: Start worker
working-directory: packages/twenty-server
run: |
NODE_ENV=development node dist/queue-worker/queue-worker.js &
echo "Worker started"
- name: Zapier / Build
run: npx nx build twenty-zapier
- name: Zapier / Run Tests
uses: ./.github/actions/nx-affected
with:
tag: scope:zapier
tasks: test
zapier-test:
needs: server-setup
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, validate]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-zapier
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:zapier
tasks: ${{ matrix.task }}
ci-zapier-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, zapier-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1

208
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,208 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
issues:
types: [opened]
repository_dispatch:
types: [claude-core-team-issues]
permissions: {}
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)
)
# Job-level (not workflow-level) concurrency: skipped jobs never enter the
# group, so PR chatter without @claude can't evict a queued Claude run.
concurrency:
group: claude-code-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run Claude Code
id: claude-code
uses: anthropics/claude-code-action@ac7e24bf2938964b8ab203e417a2773802392ddd # v1.0.146
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: |
actions: read
claude_args: '--max-turns 200 --model opus --allowedTools "Edit,Write,WebFetch,Bash(bash packages/twenty-utils/setup-dev-env.sh),Bash(npx nx *),Bash(npx jest *),Bash(yarn *),Bash(git *),Bash(gh *),Bash(sed *),Bash(python3 *),Bash(rm *),Bash(find *),Bash(grep *),Bash(cat *),Bash(ls *),Bash(head *),Bash(tail *),Bash(wc *),Bash(sort *),Bash(uniq *),Bash(mkdir *),Bash(cp *),Bash(mv *),Bash(touch *),Bash(chmod *),Bash(echo *),Bash(curl *),Bash(cd *),Bash(pwd *),Bash(diff *),Bash(xargs *),Bash(awk *),Bash(cut *),Bash(tee *),Bash(tr *)"'
settings: |
{
"env": {
"PG_DATABASE_URL": "postgres://postgres:postgres@localhost:5432/default"
}
}
- name: Post Create-PR link if Claude ran out of turns
if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH=$(git branch --show-current)
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "" ]; then
exit 0
fi
AHEAD=$(git rev-list --count main.."$BRANCH" 2>/dev/null || echo "0")
if [ "$AHEAD" = "0" ]; then
exit 0
fi
EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING_PR" ]; then
exit 0
fi
ISSUE_NUMBER="${{ github.event.issue.number || github.event.pull_request.number }}"
ENCODED_BRANCH=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$BRANCH")
PR_URL="https://github.com/${{ github.repository }}/compare/main...${ENCODED_BRANCH}?quick_pull=1"
BODY="⚠️ Claude ran out of turns before creating a PR. Work has been pushed to [\`$BRANCH\`](https://github.com/${{ github.repository }}/tree/$ENCODED_BRANCH).\n\n[**Create PR →**]($PR_URL)"
if [ -n "$ISSUE_NUMBER" ]; then
gh issue comment "$ISSUE_NUMBER" --body "$(echo -e "$BODY")"
fi
claude-cross-repo:
if: github.event_name == 'repository_dispatch'
concurrency:
group: claude-cross-repo-${{ github.event.client_payload.repo_full_name }}-${{ github.event.client_payload.issue_number }}
cancel-in-progress: false
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
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
with:
script: |
const p = context.payload.client_payload;
let prompt;
if (p.comment_body) {
prompt = `You are responding to a comment on issue #${p.issue_number} ("${p.issue_title}") in the ${p.repo_full_name} repository.\n\nThe comment by @${p.sender} says:\n\n${p.comment_body}\n\nIssue body:\n\n${p.issue_body}\n\nPlease help with this request. The code you are working with is the twenty codebase (this repository).`;
} else {
prompt = `You are responding to issue #${p.issue_number} ("${p.issue_title}") in the ${p.repo_full_name} repository, opened by @${p.sender}.\n\nIssue body:\n\n${p.issue_body}\n\nPlease help with this request. The code you are working with is the twenty codebase (this repository).`;
}
core.setOutput('prompt', prompt);
core.setOutput('repo', p.repo_full_name);
core.setOutput('issue_number', p.issue_number);
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@ac7e24bf2938964b8ab203e417a2773802392ddd # v1.0.146
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: ${{ steps.prompt.outputs.prompt }}
additional_permissions: |
actions: read
claude_args: '--max-turns 200 --model opus --allowedTools "Edit,Write,WebFetch,Bash(bash packages/twenty-utils/setup-dev-env.sh),Bash(npx nx *),Bash(npx jest *),Bash(yarn *),Bash(git *),Bash(gh *),Bash(sed *),Bash(python3 *),Bash(rm *),Bash(find *),Bash(grep *),Bash(cat *),Bash(ls *),Bash(head *),Bash(tail *),Bash(wc *),Bash(sort *),Bash(uniq *),Bash(mkdir *),Bash(cp *),Bash(mv *),Bash(touch *),Bash(chmod *),Bash(echo *),Bash(curl *),Bash(cd *),Bash(pwd *),Bash(diff *),Bash(xargs *),Bash(awk *),Bash(cut *),Bash(tee *),Bash(tr *)"'
settings: |
{
"env": {
"PG_DATABASE_URL": "postgres://postgres:postgres@localhost:5432/default"
}
}
- name: Mint twenty-factory dispatch token
id: app-token
if: always()
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.TWENTY_WORKFLOW_DISPATCHER_CLIENT_ID }}
private-key: ${{ secrets.TWENTY_WORKFLOW_DISPATCHER_PRIVATE_KEY }}
owner: twentyhq
repositories: twenty-factory
permission-actions: write
- name: Dispatch response to twenty-factory
if: always()
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ steps.prompt.outputs.repo }}
ISSUE_NUMBER: ${{ steps.prompt.outputs.issue_number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh workflow run post-claude-response.yaml --repo twentyhq/twenty-factory --ref main \
-f repo="$REPO" \
-f issue_number="$ISSUE_NUMBER" \
-f run_url="$RUN_URL"

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