8774bf86042a38f61f093df00610dce939fb788b
496 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d4c3759c70 |
ci(pr-review): stop a skipped label dispatch from cancelling the open dispatch (#23856)
## Problem The standard review silently does not run on PRs that get labelled by a bot right after opening. https://github.com/twentyhq/twenty/pull/23854 is an example: no `PR Review #23854` run exists in `ci-privileged` at all. | time | what | |---|---| | 10:08:35 | PR opened | | 10:08:39 | `twenty-eng-sync[bot]` adds the `-PR: draft` label | | 10:08:40 | dispatch run for `opened` starts, cancelled during "Set up job" | | 10:08:43 | dispatch run for `labeled` is skipped by the job `if` | Concurrency is evaluated before the job-level `if`, so the `labeled` run preempts and cancels the in-flight `opened` run and is then skipped itself (`-PR: draft` does not start with `pr-review-`). The sync bot labels within ~4 seconds of open, which is faster than the app-token mint step, so the `opened` dispatch loses this race essentially every time that label is applied. `opened` is the only event that resolves to the `standard` check, so with no later push the PR gets no review at all. Same class of gap as the one #23708 closed, moved down a layer: the trigger exists now but gets cancelled. ## Fix Scope the concurrency group by event action, and only cancel in-progress runs for `synchronize`. Rapid consecutive pushes still de-duplicate; `opened`, `ready_for_review` and `labeled` no longer cancel each other. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23856?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
997b2c38de |
Add cookie-session integration test suite (#23715)
Stacked on #23642. Integration suite for the cookie-session surface, organized as one successful/failing spec pair per stage of the session lifecycle. 14 spec files, ~36 tests, all over real HTTP against the booted app. ## Coverage by stage **1. Session creation on auth exchanges** (`successful-`/`failing-session-creation`) Flag gating (default off: tokens, no cookie, no row); httpOnly cookie snapshot with 180d expiry window; SHA-256 hash-at-rest with the row bound to the apple seed workspace; scripted sign-ins without an Origin header still get the cookie; login-CSRF refuses the cookie for disallowed origins while returning the token pair; sign-in over an existing session revokes it as `SUPERSEDED`; a failed credentials exchange mints nothing. **2. Cookie delivery** (`successful-session-cookie-delivery`, `secure-deployment-session-cookie`) The runtime side door (`AUTH_COOKIE_SAME_SITE=none` forces the secure path) pins the `__Host-`/`Secure`/`SameSite=None` variant in the default CI run. The exact production combination (`__Host-`, `Secure`, `SameSite=Lax`) is covered by a dedicated spec that requires the app to boot with an https `SERVER_URL`: the secure branch is decided by config, never the transport, so no TLS is needed. It skips itself on plain-http boots; CI runs it as an extra step on one shard with `SERVER_URL=https://localhost:3000`, including the `__Host-` round-trip and the plain-cookie-name downgrade refusal. **3. Per-request authentication and the CSRF read gate** (`successful-`/`failing-session-cookie-authentication`) A cookie-only request resolves the seeded user; a `sess_` token presented as Bearer is rejected; cookie-authenticated unsafe requests with a disallowed or missing Origin get 403 `CSRF_ORIGIN_MISMATCH`; an unknown session token is unauthenticated and its dead cookie is cleared. **3b. Workspace binding** (`successful-session-workspace-binding`) Tim signs into both seeded workspaces (apple and yc); each session row is bound to the workspace its exchange selected (`workspaceId` and `userWorkspaceId` pinned to the seed ids), and each cookie resolves to its own workspace context, with no request-side input able to pivot a session across workspaces. **3c. Credentialed CORS** (`cors-credentialed-origins`) Allowlisted origins get the reflected `Access-Control-Allow-Origin` plus `Access-Control-Allow-Credentials: true` and `Vary: Origin`, preflight included; other origins keep the public wildcard. See tooling notes: this surface was previously untestable. **4. Sessions API** (`successful-`/`failing-user-sessions-api`) `currentUserSessions` marks exactly the presented session as current; `revokeUserSession` revokes by id (`USER_REVOKED`) and drops it from the listing; `revokeAllOtherUserSessions` spares the presented session; cross-user revocation and unauthenticated listing are refused. **5. Exits** (`successful-sign-out`, `failing-session-expiration`) `signOut` revokes with `USER_SIGN_OUT`, clears the cookie, and reuse fails immediately (cache invalidated, not TTL-bound); a cookie-less sign-out clears nothing, so a cross-site POST cannot log a visitor out; absolute-lifetime and idle-timeout expiry both reject and clear the cookie. **7. Cleanup cron** (`user-session-cleanup-cron`) Both halves run in-process against fixtures spanning the 30d retention boundary. Sessions: expired/revoked-beyond-retention deleted; active, recently-expired, and idle-expired rows survive (the idle case pins the known predicate gap). Refresh tokens: old-expired and old-revoked deleted, fresh kept, and a long-expired token of another type survives, pinning the `type` filter that keeps the shared `appToken` table safe from the hard-delete. Not covered here by design: the impersonation park/restore sub-funnel (stage 6, follow-up) and the client-side funnel (stage 8, front-end scope). Password-change revocation and the renewal bridge are also left to follow-ups. ## How the flag is flipped `AUTH_COOKIE_SESSIONS_ENABLED` (and `AUTH_COOKIE_SAME_SITE` for the secure side door) are toggled at runtime through the admin panel config API, reusing the `twenty-config` test utils: `DatabaseConfigDriver.set` updates its cache synchronously and `TwentyConfigService` consults the DB driver before the env driver. No `.env.test` change, no app reboot, runs in the default CI environment without the `ci:auth-cookie-sessions` label. `SERVER_URL` is env-only, hence the dedicated CI step for the production secure-deployment spec. ## Shared tooling changes - **`applyCredentialedCors` extraction (src change)**: the integration harness booted with Nest's wildcard `cors: true`, not the credentialed-allowlist setup living in `main.ts`, so the CORS surface was untestable by construction. The setup moved into `applyCredentialedCors`, now called by both the production bootstrap and `createApp`, making the harness's CORS behavior the deployed one. Behavior-neutral for production. - `makeMetadataAPIRequest` accepts an explicit `null` token for unauthenticated requests. Passing `undefined` silently fell back to the default admin token (parameter defaults apply to `undefined`), which made supposedly public requests Bearer-authenticated, bypassing both the cookie auth path and the CSRF middleware. Existing call sites are unaffected. - The `GetLoginTokenFromCredentials` / `GetAuthTokensFromLoginToken` documents moved into shared query factories; the workspace-origin builder is extracted and generalized to any seeded subdomain (`buildWorkspaceOriginForSubdomain`, reused by `getAccessTokenForCredentials`). - Suite-local helpers: `signInWithCookieCapture` (full credentials exchange returning the raw supertest response, with a `workspaceSubdomain` option), `postMetadataOperationWithHeaders` (Origin/Cookie header control), cookie extraction for both cookie names, clearing-cookie detection, snapshot normalization (token and expiry redacted), and shared `ALLOWED_ORIGIN`/`DISALLOWED_ORIGIN` constants derived from `FRONTEND_URL`. Verified locally: full suite green in CI mode on both plain-http and https-`SERVER_URL` boots; oxlint and tsc clean. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
850d3d70fc |
chore(codeowners): guard .claude, .mcp.json and CLAUDE.md (#23734)
## What Adds `.claude/`, `.mcp.json` and `CLAUDE.md` to CODEOWNERS. ## Why These files configure the coding agent that maintainers attach to PRs: `SessionStart` hooks, MCP servers, and agent instructions. They were previously outside CODEOWNERS coverage (which only spanned `.github/` and `.yarnrc.yml`), so a change to any of them could land on `main` without core-team review. CODEOWNERS gates merge approval only. It does not affect an unmerged branch that a session merely checks out, so this closes the "malicious agent config quietly lands on main" path, not fork-branch execution. It is one layer, not the whole answer. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23734?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
267ecb12db | Migrate web auth from localStorage token pairs to httpOnly cookie sessions (#23642) | ||
|
|
a9ca1eae95 |
ci(pr-review): dispatch on PR open (standard review only) (#23708)
## Why A PR opened directly as non-draft (the normal member flow: push branch → `gh pr create`) fires no dispatcher trigger — the initial commits arrived before the PR existed, so they're an `opened` event, not `synchronize`. With no `opened` trigger, such a PR gets **no review at all** unless it's later pushed to or manually labelled. This is live today: #23697 and #23707 are core-team PRs sitting with the bot's `-PR: draft` label but zero "PR Review" status. ## Change Add `opened` back to the dispatcher, and forward the triggering PR event to the orchestrator: ```yaml types: [opened, ready_for_review, synchronize, labeled] # ... -f pr_number="$PR_NUMBER" -f event="$EVENT" ``` The orchestrator (twentyhq/ci-privileged#65) maps **`opened` → standard review only**; `security` + `triage` stay on pushes / ready-for-review. So opening a PR gives core-team authors the standard (architectural) review early, without firing the full gate on open, and the "opened and never pushed again" hole is closed. No author-role logic lives here — the dispatcher just forwards `pr_number` + `event`; all who-gets-what policy is resolved in the orchestrator. ## Merge order Depends on **twentyhq/ci-privileged#65** (adds the `event` input). Merge that first — it's backward-compatible (empty `event` = today's auto-gate behaviour), so nothing breaks in between. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23708?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
7b7e4a5eca |
docs: fix inaccuracies found auditing the docs against v2.27.0 (#23616)
Prompted by user feedback: *"The documentation doesn't always reflect
the latest release. Some articles are outdated or incomplete."*
I audited every English page under `packages/twenty-docs` against the
code at v2.27.0, verifying each checkable claim (commands, env vars,
enum members, payload shapes, prop tables, API routes) against source in
`packages/`. Anything without a `file:line` citation proving the docs
wrong was dropped.
**Result: 414 findings across 226 pages — 75 critical, 169 major, 170
minor.** The feedback is accurate, and understates it in the
developer-facing sections.
This PR fixes a first slice. The full findings list is below so the rest
can be picked up.
---
## What this PR changes
**Removes the `twenty-ui` component reference** (25 English pages + 325
translations). The section predated the extraction of the design system
into the `twenty-ui` package:
- Not one import path resolved. `twenty-ui/display` and
`twenty-ui/components` are not export subpaths (real ones:
`data-display`, `feedback`, `icon`, `input`, `navigation`, `surfaces`,
`layout`, …), and ~20 more examples imported `@/ui/...` paths no longer
in twenty-front.
- Three documented components no longer exist: `SoonPill`,
`AutosizeTextInput`, `MenuItemCommand`.
- `Chip`'s props table documented the deleted `EntityChip`.
`ProgressBar`'s entire API was replaced
(`duration`/`delay`/`easing`/`barHeight`/`autoStart` →
`value`/`barColor`/`countdownDurationInMs`/…).
It was also unreachable from the navigation, so the pages were indexed
and searchable but maintained by nobody. Storybook is the live source of
truth here, which is why this is a deletion rather than a repair.
**Legal FAQ.** Corrects the workspace deletion timeline to match clause
4.9 of the DPA the product itself generates (~90 days from live systems,
a further ~90 for backups, isolated throughout) instead of the previous
claim of immediate removal with 7-day backup retention. Rephrases the
support-access answer to describe what the product actually does: access
is on by default and can be disabled in Settings → General → Security,
rather than the previous claim that it requires the customer to report
an issue and grant access.
**Self-hosting setup page.** The SMTP configuration block used
`<ArticleTabs>/<ArticleTab>`, leftovers from the pre-Mintlify site.
Those components are undefined here, so the Gmail/Office365/smtp4dev
instructions were not rendering at all. Converted to `<Tabs>/<Tab>`.
**Removes a fabricated Enterprise gate.** A Warning on the app
publishing page claimed cross-workspace sharing of tarball apps requires
an Enterprise key and that the Distribution tab shows an upgrade prompt.
No such gate exists in code, and its link target didn't exist either.
**Link and asset fixes.** Retargeted the two `docs.json` redirects whose
destinations 404'd; fixed the Code of Conduct link (file lives under
`.github/`); fixed the app-roles example link to
`examples/hello-world/src/roles/default-role.ts`; pointed the Contribute
frontend card, four `/developers/extend/apps/getting-started` links and
one `/twenty-ui/display` link at real pages; dropped two `<img>` tags
whose files are absent from the repo.
After this PR: every internal link and image reference resolves, all 171
navigation entries resolve to a file, and no redirect destination is
dead.
---
## Audit: what else is wrong
### Root causes
The failures aren't random rot. Four mechanisms produce nearly all of
them:
1. **Nothing links renaming a symbol to updating the page that documents
it.** Whole pages describe APIs returning zero grep hits:
`MessageQueueServiceBase`, `useScopedHotkeys`/`PageHotkeyScope`,
`@Gate`, `SoonPill`.
2. **"Coming soon" is written once and never revisited.** Nine features
are documented as unavailable that have shipped.
3. **Pages are dropped from navigation but left on disk.** 55 were
unreachable yet still indexed and searchable.
4. **Docs written from intent rather than from code.** One case is
provably born-stale: the `front-components` limitations table was
written in a commit that landed *after* the commit which polyfilled the
APIs it lists as unsupported.
### Priority 1: pages that actively break the reader
**Workflow template variables are wrong across 12 pages.** The largest
cluster — 16 critical findings, one root cause. Record-event triggers
expose the record under `properties.after`/`properties.before`; manual
triggers under `payload`; webhook triggers store the posted body flat
with no wrapper. Docs use `{{trigger.object.*}}`, `{{trigger.body.*}}`,
`{{trigger.subject}}` throughout. Search Records returns `{ first, all,
totalCount }`, not an array, and the resolver is Handlebars, which
doesn't accept `[0]` indexing at all — so `{{searchRecords[0].name}}`
and `{{searchRecords.length}}` cannot work. Iterator exposes
`currentItem`, not `item`/`index`. Evidence:
`generate-fake-object-record-event.ts:44-60`,
`workflow-schema.workspace-service.ts:501-517`,
`find-records.workflow-action.ts:111-117`,
`workflow-iterator-result.type.ts:2-3`,
`twenty-shared/src/utils/evalFromContext.ts`. Every workflow tutorial on
the site is copy-paste-broken. Highest-value fix in the audit, and
mostly mechanical.
**Self-hosting runbook commands don't work.** Backup names a container
and database that don't exist (service is `db` → `twenty-db-1`; database
is `default`, not `twenty`). Restore runs `docker compose stop
twenty-server twenty-front`, neither of which is a service — the compose
file defines `server`, `worker`, `db`, `redis`, and there's no separate
frontend service. The "unable to log in" fix runs `yarn` and `npx nx
database:reset` inside the production container, whose Dockerfile
deletes `npm`/`npx` and ships only `dist/`. Someone following the backup
page ends up with no backup.
**API, webhook and OAuth contracts are wrong.** The documented webhook
payload (`event`, `data`, `timestamp`) is not what the server sends —
the real body is `targetUrl`, `eventName`, `objectMetadata`,
`workspaceId`, `webhookId`, `eventDate`, `userId`, `workspaceMemberId`,
`record`, optional `updatedFields`
(`transform-event-batch-to-webhook-events.ts:34-46`). Any integration
written from that page fails to parse. `GET /oauth/authorize` doesn't
exist (server serves `/oauth/register`, `/token`, `/revoke`,
`/introspect`; authorization is served by the frontend at `/authorize`).
`/oauth/register` never returns a `client_secret` —
`token_endpoint_auth_method` is hard-coded `'none'` — so the documented
response and the "store it securely" warning are fiction, and the Client
Credentials section is unusable with a DCR client. PKCE is mandatory,
not "recommended". Batch limit is 200, not 60 (`QUERY_MAX_RECORDS =
200`), making the derived throughput estimates ~3.3x off.
**Contributor onboarding teaches removed APIs.** `queue.mdx`,
`hotkeys.mdx` and `feature-flags.mdx` are wrong at essentially every
step. Documented nx targets `twenty-server:database:migrate:prod`,
`twenty-server:test:unit` and `npx nx start` aren't real targets and
fail outright. `local-setup.mdx` never mentions
`packages/twenty-utils/setup-dev-env.sh`, the supported entry point.
Both style guides teach the `${({ theme }) => ...}` pattern, which now
returns **zero** hits in twenty-front against 929 files using
`themeCssVariables`. `frontend-commands.mdx` still lists Craco; the
frontend is Vite.
**SSO configuration is substantially fiction.** Twenty supports exactly
two protocols, OIDC and SAML. The docs omit OIDC entirely, present
Google Workspace and Microsoft Entra ID (separate social-login toggles)
as SSO providers, list configuration fields matching neither form, and
instruct the reader to click a **Test Configuration** button that exists
nowhere in the codebase.
**Data model.** The field-type table documents two types that don't
exist (`Domain`, `Long Text`) and omits three users can actually pick
(`Files`, `Full Name`, `Rich Text`). The filter-operator table is wrong
for every field type listed: Text has none of its four documented
operators, Date is missing six of nine.
**Import guidance that fails silently.** `DD/MM/YYYY` is documented as
supported; import uses plain `new Date(value)`, so `15/03/2024` is
always rejected and `03/15/2024` always read US-style — and the sibling
`fix-import-errors.mdx` says the opposite. The company sample CSV is
unusable as written (`Domain / Domain Label` headers don't exist; real
ones are `Domain Name / Link Label`).
### Priority 2: shipped features documented as unavailable
This is the specific complaint in the feedback. Each is a one-line fix.
| Documented as | Reality |
|---|---|
| AI Agent action "Coming soon" (2 pages) |
`WorkflowActionType.AI_AGENT` ships, in the picker, no feature flag |
| "There is no built-in if/else logic" (2 pages) |
`WorkflowActionType.IF_ELSE` ships |
| Webhook event filtering "may be added in future releases" (2 pages) |
per-webhook `operations` array with `*.created` / `person.*` / `*.*`
wildcards |
| Many-to-many "coming in H2 2026" | Junction Relations shipped as
public beta; Twenty's own how-to documents it |
| Email campaigns "available soon" (2 pages) | MessageCampaign object,
send/stats jobs, unsubscribe topics all ship |
| CC/BCC "not yet available" | exists on Send Email |
| Workflow retry "on our roadmap" | run-level retry command plus
per-step `retryOnFailure` |
| front-components limitations table | `getBoundingClientRect`,
`offset*`/`client*`/`scroll*`, `getComputedStyle`, `getElementById` all
polyfilled |
| Node SDK "does not exist" | `twenty-client-sdk@2.27.0` ships and is
documented elsewhere in these docs |
Four "coming soon" claims were checked and are **still accurate** —
webhook trigger authentication, dashboard-level filters, dashboard
timezone, background-job priority. Leave them.
One needs rewording rather than promotion: **gauge charts** are
described as on the roadmap, but the upgrade command
`2-3-workspace-command-...-delete-gauge-widgets` says support was
*removed*.
### Priority 3: structural
**30 orphaned pages remain** after the twenty-ui deletion: 15 of 18
`developers/contribute/*`, all 6 `user-guide/getting-started/*`, plus
`self-host.mdx`, `key-rotation.mdx`, `extend.mdx`,
`views-pipelines/overview.mdx`, `ai/capabilities/mcp.mdx`,
`data-migration/how-tos/export-faq.mdx`,
`extend/capabilities/{apis,webhooks}.mdx`. Each needs an explicit
decision: re-add, or delete plus redirect. Two look worth re-adding
rather than deleting — `user-guide/ai/capabilities/mcp.mdx` is accurate,
documents a shipped feature that's a plan line-item, and is reachable
only via a legacy redirect; `views-pipelines/overview.mdx` is linked
from three in-nav pages.
`user-guide/getting-started/capabilities/implementation-services.mdx`
must be merged rather than deleted, since three in-nav pages deep-link
it.
**Duplicate pages.** `getting-started/core-concepts/glossary.mdx` and
`user-guide/getting-started/capabilities/glossary.mdx` are 99%
identical. `developers/extend/webhooks.mdx` and
`developers/extend/capabilities/webhooks.mdx` are 88% identical and
carry the same wrong payload. `workflow-branches.mdx` and
`use-branches-in-workflows.mdx` are both in the sidebar and give
*contradictory* branch-creation instructions.
**Other.** 44 pages have no frontmatter `description`. The Russian
locale is 14 pages behind every other locale, including the entire
document-generator tutorial.
### Still needs a human owner
The legal FAQ promises breach notification "within 48 hours" while
clause 4.6 of the generated DPA (`dpa-template.constant.ts:178`) targets
72. Per direction, the docs keep 48h — a stricter public commitment than
the contract is a deliberate choice — but the DPA and the docs still
disagree, and someone owning the DPA should decide which moves.
Claims about SOC 2, GDPR attestation, backup cadence and AI-training use
could not be substantiated from the repository either way and need the
same treatment.
### Preventing recurrence
Three cheap guards would have caught most of the 75 criticals:
- **A CI check** that every navigation page resolves, every internal
link and image resolves, and no `.mdx` outside `l/` is orphaned. Catches
the entire structural third. This PR leaves the docs in a state where
such a check would pass.
- **Generate the volatile tables from their source enums** — field
types, workflow actions and triggers, filter operands, permission flags,
chart types, env vars — rather than hand-maintaining them. These
accounted for a large share of the major findings.
- **Treat "coming soon" as an expiring assertion**: tag each with the
symbol it depends on and fail the docs build when that symbol appears in
code.
## Suggested order for the rest
1. Workflow variable syntax across the 12 tutorial pages — largest
cluster, mechanical, most directly matches the feedback.
2. Self-host backup/restore/troubleshooting commands — highest blast
radius per reader.
3. Webhook payload and OAuth endpoints — blocks integrators.
4. The nine "coming soon" claims — one line each, and the most visible
form of "docs don't reflect the latest release".
5. Decide the 30 remaining orphans.
## Test plan
- [x] Every internal link and image reference in the docs resolves
- [x] All 171 navigation entries resolve to a file on disk
- [x] No `docs.json` redirect destination is dead
- [x] No inbound links to the deleted `twenty-ui` pages remain
- [x] `docs.json` structure intact after edit (138 redirects, 14
languages)
- [ ] Visual check of the self-hosting SMTP tabs once the docs preview
builds
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AnUNYYdkN3PMTPb2m6CnqC)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23616?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
a9d996ff7e |
Clarify application licensing and add trademark policy (#23564)
## What - `twenty-sdk`, `twenty-client-sdk`, `create-twenty-app`, `twenty-shared` and `twenty-ui` are now MIT (package.json + LICENSE files). The SDKs are bundled into third-party applications and app front components import twenty-ui, so these need a permissive license for apps to be licensable by their authors. `twenty-shared` is included because both SDKs inline it at build time; an MIT SDK bundling AGPL code would defeat the purpose. Apps under `packages/twenty-apps` were already MIT. - Added a "Twenty Application Exception" to LICENSE (additional permission under AGPLv3 section 7): applications that interact with Twenty through the app platform interfaces (APIs, manifests, logic functions, front components, SDKs) are not subject to copyleft and can be licensed freely by their authors. Modifying Twenty itself remains fully AGPL, including the network clause. - Rewrote the LICENSE intro to describe the three licensing zones (AGPL, Enterprise-marked files, MIT packages) and fixed the intro incorrectly saying "GPL". - Added TRADEMARK.md: what anyone can do without asking (self-host, "built on Twenty", forks under their own name) and what requires permission (using the name or logo for a product, domain, or hosted offering). ## Why Gives app developers and partners legal certainty that building on the platform does not pull their apps under AGPL, while the core stays AGPL. The exception and trademark wording should get a legal review before being announced. |
||
|
|
ea2de2dc2b |
ci(pr-review): label-only manual trigger, thin dispatcher (#23449)
Addresses the review feedback on #23418 (Paul + Copilot + cubic) and switches the manual trigger from comments to **labels**, consistent with the e2e labels. ## What changed - **Manual reviews are label-driven** — add `pr-review-security` / `pr-review-triage` / `pr-review-standard`. Labeling requires write access (team-only). The **`/pr-review` comment trigger is removed.** - Like the e2e labels, a check runs on every push **while its label is present** (the orchestrator reads the PR's current labels each run) — so `pr-review-standard` keeps the deep review current until removed. - **Dispatcher is now dumb** — it forwards only `pr_number`. All resolution + validation lives in the privileged orchestrator (Paul's suggestion: it fetches PR metadata, incl. labels, there anyway). This fixes the bot findings (regex allowlist bypass, `/pr-review`→standard default, delimiter edge cases) at the source. - `cancel-in-progress: true` (latest-push-wins, matching the previous dispatcher). Fires on non-draft PR events (the auto `security,triage` gate) and on `pr-review-*` label adds. ## Depends on A companion change to the privileged CI (reads labels + resolves/validates checks) — merge that first; it's backward-compatible, so nothing breaks in between. |
||
|
|
8211206187 |
ci(pr-review): single PR review dispatcher (#23418)
rm |
||
|
|
6a8457d8c5 |
fix(ci): diff upgrade mutation guard against the merged base, not stale PR base (#23278)
The `server-previous-version-upgrade-mutation-guard` check falsely flags upgrade commands that landed on `main` as being added/modified by unrelated PRs (e.g. [run on #23207](https://github.com/twentyhq/twenty/actions/runs/30097349274/job/89494554178) flagged six `2-23`/`2-24` files the PR never touched). ## Cause The guard action diffs `git diff "$BASE_SHA" HEAD` where the PR caller passes `base_sha: github.event.pull_request.base.sha`. On a `pull_request` event `HEAD` is the `refs/pull/N/merge` ref, whose first parent is the *current* tip of `main` it was merged with. But `pull_request.base.sha` is pinned to the base at the last branch sync and lags behind. Any upgrade command merged into `main` after that point exists in `HEAD` but not in the stale base, so the two-dot diff attributes it to the PR. Verified against the real merge ref for #23207: diffing against `base.sha` reproduces the six false offenders from the failing run; diffing against the merge ref first parent is clean. ## Fix Derive the base from the merge ref first parent (`HEAD^1`, the actual merged `main` tip), falling back to `base.sha` only when `HEAD` is not a merge commit (non-mergeable PR). The merge-queue caller in `ci-merge-queue.yaml` is unaffected: it passes `merge_group.base_sha`, which already matches its checkout. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23278?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9390c28cb6 |
ci: report ci-shared-status-check on merge_group (#23276)
Follow-up to #23275. `ci-shared` was the one required check left off that batch, so `ci-shared-status-check` still sits at "Expected - Waiting for status to be reported" and blocks the merge queue. Same fix as the other workflows: add a `merge_group` trigger and gate `changed-files-check` with `if: github.event_name != 'merge_group'`. On a queued candidate, `changed-files-check` skips, `shared-test` (gated on `any_changed`) cascades to skipped, and the `always()`-gated `ci-shared-status-check` job reports success in seconds. The full suite still runs on `pull_request`. --- _Generated by [Claude Code](https://claude.ai/code/session_015mZozbvyvha1S6wsEVLBnF)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23276?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
29747f5d6b |
ci: report required status checks on merge_group so the queue isn't blocked (#23275)
Follow-up to #23216, which added the `merge_group`-triggered `upgrade-mutation-guard`. This makes the merge queue actually usable. ## Why The merge queue waits for every **required status check** to report a conclusion on the `merge_group` candidate commit, and there is no queue-only subset: it uses the branch's required status checks. Our required `ci-*-status-check` contexts only trigger on `pull_request`, so on a queued PR they sit at "Expected - Waiting for status to be reported" and block the queue until the status-check timeout (60 min), which then counts them as failed. Only `upgrade-mutation-guard` (from #23216) triggers on `merge_group`, so today it is the only check that reports in the queue. ## What Add a `merge_group` trigger to each of the seven required-check workflows and short-circuit the expensive work so the check reports success in seconds, while the full suite keeps running on `pull_request` to gate PRs. `upgrade-mutation-guard` stays the only check the queue genuinely validates against `main`. Mechanism: on `merge_group` the root jobs skip, everything downstream cascades to `skipped`, and the `always()`-gated `*-status-check` job runs, sees no failing needs, and succeeds. Kept as the same job in the same workflow so the required-check context is byte-identical to the PR-level one (a separate pass-through workflow could register a different context and not satisfy branch protection). Per workflow: - **ci-front**: trigger only. It already cascades - `changed-files-check` is `pull_request`-only and `front-sb-build` gates on `push || any_changed`, so nothing runs on `merge_group`. - **ci-server**: trigger + `if: github.event_name != 'merge_group'` on the three ungated root jobs (`changed-files-check`, `upgrade-changed-files-check`, `server-previous-version-upgrade-mutation-guard`). The guard would otherwise fail on `merge_group` since `pull_request.base.sha` is empty there; the queue-side guard in `ci-merge-queue.yaml` already covers that case. - **ci-sdk / ci-website / ci-test-docker-compose**: trigger + the same guard on `changed-files-check`. - **ci-twenty-apps**: trigger + the guard on `discover` (its `ci`/`integration` jobs gate on `discover` output, so they cascade off). ## Settings note This complements the branch-protection changes for the queue (enable the queue, max group size 1, per #23216). If the branch has **other** required checks beyond these seven whose workflows are `pull_request`-only, they need the same `merge_group` treatment or the queue will wait on them too. --- _Generated by [Claude Code](https://claude.ai/code/session_015mZozbvyvha1S6wsEVLBnF)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23275?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
bb4e427196 |
ci: run upgrade mutation guard in the merge queue against main (#23216)
Follow-up to #23215 (merged). Rebased on `main`. ## Why #23215 fixes an instance of a class of bug: an upgrade command whose version is chosen at `generate:instance-command` time from `TWENTY_CURRENT_VERSION`, then left behind when `main` bumps the version before the PR merges (base-drift). The command ships one minor early and instances already on the newer version skip it forever. The existing `server-previous-version-upgrade-mutation-guard` in `ci-server.yaml` runs on `pull_request`, so it validates against the PR's base. When the base is stale (main moved after the branch was cut), the guard reads the branch's own `TWENTY_CURRENT_VERSION` and the check passes even though the command is now a version behind main. That is exactly how the original bug slipped through. ## What The version-directory and append-only-timestamp validation is extracted into a shared composite action, `.github/actions/upgrade-mutation-guard`, diffed against a caller-supplied `base_sha`. It is called from two places: - **`ci-server.yaml`** (PR-level guard, `base = pull_request.base.sha`) for fast feedback. The job keeps its existing name/check. This replaces ~290 lines of inline shell. - **`ci-merge-queue.yaml`** (new, `merge_group`-triggered, `base = merge_group.base_sha`). GitHub builds each merge-queue candidate on top of the current tip of `main`, so the checks read `TWENTY_CURRENT_VERSION` and the existing per-directory timestamps from main's real state at merge time. Because the candidate is rebased onto main, base-drift is caught by construction: the same validation simply runs where the base is guaranteed current. No origin/main comparison hack; the logic now lives in one place. ## Bypass semantics The guard has two independent checks, and they are treated differently on purpose: - **Version-directory check** keeps its `ci:allow-previous-version-upgrade-mutation` bypass, a deliberate, reviewed escape hatch for legitimately touching a previous-version directory. The PR-level guard reads the label directly; the merge-queue guard resolves it from the queued PR (the `merge_group` event carries no labels) and passes it to the composite action, which skips only the version-directory step. - **Timestamp / append-only check has no bypass.** The old `ci:allow-upgrade-command-timestamp-exception` label is removed. A fake or out-of-order timestamp rewinds the upgrade cursor and re-hides already-applied columns, so there is no "allowed" version of it: the timestamp just has to be configured correctly (real epoch millis, strictly greater than every existing command in the same version directory). If a blocking existing max is itself a fabricated future timestamp, re-slot that command to its real merge epoch rather than reaching for a bypass. Preventing previous-version mutation is the guard's primary purpose. In the merge queue the guard job always runs and skips only the version-directory step when the bypass label is set, so it reports a real success/failure (the required check never resolves to a skipped state, and a label-lookup failure fails closed) and the timestamp check always runs. ## Requires a settings change (not in this diff) Enabling the merge queue and marking the check required are branch-protection settings, not file changes. After merge, an admin needs to: 1. Enable the merge queue for `main` in branch protection. 2. Add `CI - Merge Queue / upgrade-mutation-guard` to the merge queue's required checks. ## Notes - Composite action, not a `workflow_call` reusable workflow, deliberately: converting the `ci-server.yaml` job to a reusable-workflow call would rename its status check to `server-previous-version-upgrade-mutation-guard / ` and break that required-check mapping in branch protection. A composite action dedups the logic while keeping both callers' check names intact. --------- Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com> |
||
|
|
6e1e98f4ab |
fix(server): shard the server-test unit job to stop the intermittent crash (#23009)
## Problem `server-test` fails intermittently: exit 1 with **no `FAIL` line and no `Test Suites:/Tests:` summary** — the jest run is aborted mid-way, before the reporter's `onRunComplete`. ## Root cause The `test` target runs the entire unit suite (~6,600 tests) in **one in-band jest process on a single runner VM** (`nx.json` sets `maxWorkers: 1` for the `ci` configuration). A few minutes in, that process is killed by an **external `SIGKILL`** — confirmed *not* OOM (~15 GB free at kill time, no cgroup `oom_kill`) and *not* an in-process crash (a Node diagnostic report armed with `--report-on-fatalerror` + `--report-uncaught-exception` writes nothing). The whole-run kill is why it fails intermittently with no summary. `maxWorkers=2` on one VM still dies, so the threshold is **per-VM**, not per-process. ## Fix Shard the unit suite across VMs, the same way `server-integration-test` already does: - A `twenty-server` `test:ci` target runs jest directly (so `--shard` forwards) with `dependsOn: ["^build"]` so the workspace deps are built. - `server-test` becomes a 4-way matrix; each shard runs a quarter of the suite, well under the kill threshold. - `ci-server-status-check` already aggregates `server-test`, so required checks are unchanged. Also provides two mocks a completed run needs but the SIGKILL had been masking in `ApplicationRegistrationService.upsertFromCatalog` unit tests: the `MetricsService` provider and `applicationRegistrationRepository.createQueryBuilder`. |
||
|
|
cb95410a51 |
ci: test twenty-apps install against latest dockerhub and local server + new trigger (#22636)
## Why App installability can silently regress from two directions, and today CI only covers one of them: 1. A **server** change (about to merge from the monorepo) breaks the ability to install the **current public apps** — a backward-compatibility regression users would hit on upgrade. 2. An **app** change breaks against a server **built from the current monorepo files** (not just the last published image), so the app and the upcoming server drift apart before either ships. Both are compatibility guarantees between the server and the app catalog. Today they are only tested from the app side, against the latest published image. This PR makes CI enforce the contract from both sides: - Any server PR must keep **every** current public app installable. - Any app PR is exercised against both the **released** server (its integration suite — what users run today) and the **upcoming** (monorepo) server (integration plus deploy + install). ## What Shared building blocks so both CIs exercise the same paths instead of duplicating them: - **`spawn-twenty-server`** (composite action) — returns a running server (`server-url` + `api-key`) from either the latest published Docker Hub image or a server built from the monorepo. Both sources expose the same contract, so callers never branch on how the server came up. - **`test-twenty-app`** (composite action) — exercises one app against a given server, delegating deploy + install to the shared `deploy-twenty-app` / `install-twenty-app` actions. - **`discover-apps`** (reusable workflow) — the single source of truth for the app matrix. Parameterized by `scope` (`public` vs `internal-and-public`) and `changed-only`, so both CIs derive their matrix from the filesystem instead of a hand-maintained list. Discovery stays automatic: a newly added public app is picked up with no CI edit, which is what keeps the "every public app" guarantee honest. Wired in: - **CI Server** gains a `server-apps-install-smoke` matrix that installs every public app (`discover-apps` with `scope: public, changed-only: false`) against the about-to-merge server, gated in `ci-server-status-check` so a regression blocks merge. - **CI Twenty Apps** discovers changed apps (`scope: internal-and-public, changed-only: true`) and runs each against both server sources — the released image and the monorepo build. ## Why the coverage differs per side (not "always everything") `test-twenty-app` has three explicit modes — `installation-and-integration-test` (integration + deploy + install), `integration-test-only` (suite only), `installation-only` (deploy + install only) — because the useful signal depends on what actually changed: - **App PR against the monorepo server → `installation-and-integration-test`.** The app changed, so run its whole suite against the upcoming server, install included. - **App PR against the released server → `integration-test-only`.** Checks the app's own suite against what users run today; install against the released image is left to the SDK e2e path. - **Server PR → `installation-only`, across all apps.** The apps did not change; the only question is "can each one still be installed." Running every app's full integration suite on every server PR would be far slower and largely redundant. Installation-only keeps this broad (the whole catalog) and cheap enough to always run and block merge. The tradeoff is deliberate: broad but shallow where nothing in the app changed, deep where it did. ## Notes / trade-offs - On app-only PRs the `local` source pays a full server build per app (the `server-build` cache is only warm on server PRs). Could be optimized later with a shared warm-up job. - SDK-local (Verdaccio) install testing stays in `ci-create-app-e2e-minimal`; this PR's `local` source targets the server build. <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22636?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a> |
||
|
|
60fd322b49 |
Centralize system field side effects + search field metadata (#22594)
## Introduction Closes twentyhq/core-team-issues#2635 and twentyhq/core-team-issues#2642 and twentyhq/core-team-issues#2589 Object system fields (`searchVector` + its GIN index + `searchFieldMetadata`, the reserved system fields, default relations) were provisioned through several scattered, path-specific code paths. As a result the **app-manifest sync path** authored objects with an empty/`NULL` `searchVector` and **zero `searchFieldMetadata`**, so app-owned objects shipped a broken generated search column (see #22657). The generation logic also lived partly in imperative services rather than in the metadata side-effect engine, and relied on non-deterministic (`v4`) universal identifiers that `twenty apply` could not converge, destroying manually backfilled rows. This PR centralizes every object-creation system side effect into the **metadata side-effect engine**, extends the engine to keep search metadata consistent on field delete and object relabel, makes the standard app's search identifiers deterministic, and ships upgrade commands to reconcile existing workspaces. ## What changed ### Side effects moved into the metadata side-effect engine New dedicated, self-contained handlers — so every write path (API and app manifest) gets identical results, and side effects never trigger other side effects. **Object create / delete** (`handlers/object-metadata`) * **`objectSystemFieldsOnCreate`** — generates the 7 reserved system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`). * **`objectSearchVectorOnCreate`** — provisions the full-text search surface as one unit: the `searchVector` `TS_VECTOR` field, its backing GIN index, and the `searchFieldMetadata` row (for searchable objects whose label identifier is a searchable field) that keeps `searchVector` populated instead of `NULL`. * **`objectSystemSideEffectsOnDelete`** — tears the above down on object deletion. **Search-metadata consistency on relabel / field delete** (new — these are what close the manifest-path gaps) * **`objectSearchVectorOnUpdate`** (`handlers/object-metadata`) — when a searchable object is relabeled onto a new searchable field, provisions the `searchFieldMetadata` row that indexes it. Relabeling is **additive**: existing rows (e.g. the provisioned `name` row) are preserved, so the previous label identifier stays searchable. Mirrors the API update path so a manifest re-sync that changes the label identifier reaches search parity. No-ops for junction objects (`id` label identifier) and non-searchable field types. * **`fieldSearchFieldMetadataOnDelete`** (`handlers/field-metadata`) — when a field is deleted, cascade-deletes every `searchFieldMetadata` row that indexes it. `searchFieldMetadata` is excluded from manifest deletion inference, so this explicit cascade is what covers **both the API and manifest paths** (the object-scoped DB cascade only fires on object deletion). Uses the `searchFieldMetadataUniversalIdentifiers` aggregator on the flat field for an O(k) lookup instead of scanning all rows. The **default `name` field and default relations are now caller-provided default fields** (SDK autocomplete on the manifest path, input transpiler on the API path) rather than system side effects — removing duplicate name generation, the imperative `build-default-*-for-custom-object` utilities, and the ad-hoc system-field integrity validator. ### Deterministic identifiers for the standard app The twenty-standard search GIN index and `searchFieldMetadata` now derive deterministic universal identifiers (`getIndexUniversalIdentifier` / `getSearchFieldUniversalIdentifier`) instead of `v4`, so `twenty apply` converges instead of recreating. ### Upgrade commands (`2-20`) to reconcile existing workspaces **Instance commands** (run once per instance; ordered fast → slow → workspace): 1. **`AddIsSystemSideEffectToSearchFieldMetadata`** (fast) — adds the `isSystemSideEffect` column to `core.searchFieldMetadata`. Defaults to `true`, which also correctly backfills every existing row since `searchFieldMetadata` is always system-derived (never user-authored). 2. **`BackfillNameFieldIsSystemSideEffect`** (slow) — re-flags existing `name` fields from `isSystemSideEffect: true` → `false`, since the default `name` field is now a caller-provided default like any other user-owned field (it was provisioned as `true` in 2.15 → 2.19). This is a pure data backfill, so the bulk `UPDATE` lives in `runDataMigration()` rather than `up()` — keeping it out of the fast schema transaction avoids holding an `ACCESS EXCLUSIVE` lock that could stall reads during the deploy. Slow instance commands still run before every workspace command of the version, so the fresh value is in place before the search-reconcile workspace commands recompute the `fieldMetadata` flat-entity cache. Scoping by name alone is safe (no engine-owned field is named `name`); `down()` is best-effort (pre-2.15 `false` rows are indistinguishable from flipped ones). **Workspace commands** (idempotent, dry-run supported): 1. **`reconcile-search-vector-gin-index-universal-identifier`** — re-owns every searchVector GIN index UID to its deterministic value (all applications), then backfills the missing GIN index for installed-app objects. 2. **`reconcile-search-field-metadata`** — re-owns every `searchFieldMetadata` UID (all applications), then backfills the missing rows for installed-app searchable objects. 3. **`rebuild-installed-app-search-vectors`** — rebuilds the `searchVector` column of every installed-app `TS_VECTOR` field, once the index and rows exist. Design notes: * **Re-own is global** (twenty-standard, workspace-custom, installed) — a UID convergence keyed on each row's own application. * **Backfill is installed-app only** — standard/custom objects already have these rows via the manifest funnel. * Re-own runs **before** backfill and is transaction-guarded; a failure aborts that workspace to avoid a unique-identifier collision. ## Tests * Integration: app manifest sync now asserts system fields + searchable objects (searchVector, GIN index, searchFieldMetadata) are created; a new relabel suite drives three manifest syncs and asserts records stay searchable through the old + new label identifiers and lose searchability when a field is removed; removed the obsolete system-fields-integrity suite/snapshots. * Unit: per-handler side-effect specs (including the new `objectSearchVectorOnUpdate` and `fieldSearchFieldMetadataOnDelete` handlers), and per-util specs for the re-own / backfill operation builders and the GIN-index classifier. ## Upgrade / migration notes * Existing workspaces converge on the next upgrade run via the `2-20` instance + workspace commands (idempotent, dry-run supported). * Backfill and rebuild go through the workspace-migration runner (automatic cache invalidation); the re-own step invalidates only the affected flat-entity maps directly. * The cross-version upgrade CI now flushes the cache before running the upgrade, so the new version recomputes every flat-entity map from the database instead of reading blobs the old version serialized in an older shape. ## Follow-up * `object-metadata.service.ts` still carries a `TODO: remove once default view fields move to the metadata side effect engine` — default view fields are the next candidate to move into the engine. * A single manifest sync cannot yet both create a field and relabel the object onto it, because `objectMetadata.update` is ordered before `fieldMetadata.create` in the migration runner. Tracked in twentyhq/core-team-issues#2655; to be fixed in a follow-up. |
||
|
|
bdcdaaa3d8 |
Fail App docs drift check ci if doc drift detected (#22713)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22713?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
57fb39ba00 |
ci: add app-docs drift check agent (#22696)
Part 8 (final) of the app-docs audit series. The preceding PRs (#22688–#22695) fixed the drift that had already accumulated between the app platform and its docs — wrong commands, nonexistent import paths, missing enum values, stale scaffold descriptions. This PR adds the guardrail that keeps it from accumulating again. ## What it does `ci-app-docs-drift.yaml` runs on PRs that touch the app-development surface: - `packages/twenty-sdk/**` (CLI commands/flags, `define*` configs, front-component runtime) - `packages/create-twenty-app/**` (scaffold template and flags) - `packages/twenty-client-sdk/**` (public client surface) - `packages/twenty-shared/src/application/**` and `src/types/**` (manifest types and enum value sets) It launches a Claude agent (same `anthropics/claude-code-action` + `CLAUDE_CODE_OAUTH_TOKEN` setup as the existing `claude.yml`) with a prompt that: 1. Reads the PR diff and filters for genuinely user-facing changes (new/renamed commands or flags, config properties, enum values, exports, env vars, template files) — implementation-only changes short-circuit to "no impact". 2. Follows a source-area → docs-page mapping to read the relevant pages under `packages/twenty-docs/developers/extend/apps/`, and checks whether the PR already updates them correctly. 3. Posts **one sticky comment** (marker-based, updated in place on subsequent pushes): either "no documentation impact" or a table of `Change / Docs page / Status / Suggested fix`. ## Guardrails - Read-only tool allowlist plus `gh pr comment` / `gh api` — the agent cannot edit code or docs, only report. - Skips fork PRs (secrets unavailable) and bot-authored PRs. - `--max-turns 60`, 30-minute timeout, per-ref concurrency with cancel-in-progress. The exhaustive audit that motivated this (every command, flag, export, and enum cross-checked between docs and source, plus a scaffolded app tested against a live server) is exactly the loop this workflow automates in miniature on every relevant PR. --- _Generated by [Claude Code](https://claude.ai/code/session_01ExboyDAT19khDuKXaYXETT)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22696?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Martin <martin@twenty.com> |
||
|
|
cfe0fc7ce6 |
feat(ci): detect bot signatures in PR description, comments and reviews (#22547)
## What Extends the **Blocked Contributors Check** beyond commits so it also scans a PR's: - **Description** (PR body) - **Conversation comments** - **Inline review comments** - **Review summaries** ## Why The check already fails a PR when a commit is attributed to a known bot (via author/committer/email and `Co-Authored-By` trailers). But bot-generated content also leaks into PR prose — descriptions and comments carry attribution footers like `🤖 Generated with Claude Code` that the commit-only scan never saw. ## How - **Commits** keep matching on bot *identity* (`IDENTITY_PATTERNS`: `@anthropic.com`, `cursoragent@cursor.com`, `copilot-swe-agent[bot]`). - **Prose surfaces** are matched only on `SIGNATURE_PATTERNS` — the verbatim auto-generated attribution footers (`Generated with Claude Code`, `Co-Authored-By: Claude`, Cursor equivalents). This is deliberately tight: contributors legitimately discuss Claude/Cursor in comments, so a bare product-name mention must **not** trip the check. Verified that "I used Claude Code to draft this but rewrote it", "works great in Cursor", and human `Co-Authored-By` lines all stay clean while real footers flag. - The workflow now also triggers on `issue_comment`, `pull_request_review` and `pull_request_review_comment` (plus PR `edited`), so bot prose added *between* commit pushes is still caught. `issue_comment` is guarded to PRs only, and `PR_NUMBER` resolves from either event. - Each prose violation reports the surface kind and a clickable URL. ## Notes `SIGNATURE_PATTERNS` are conservative by design and won't catch a footer someone reworded by hand. Widening them is a follow-up if we decide to trade some false positives for broader coverage. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22547?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
024a9b4d94 |
fix(upgrade): re-slot all 2-19 upgrade commands to their real merge epochs and guard timestamps in CI (#22498)
# Fix broken 2-19 upgrade command sequence (dev incident: `lastStreamError` / `workspaceDiscoverability`) ## Incident The dev environment (tracking main) throws: - `Property "lastStreamError" was not found in "AgentChatThreadEntity"` - `Cannot return null for non-nullable field Workspace.workspaceDiscoverability` ## Root cause The 2-19 upgrade commands were committed with **fabricated future timestamps** (year-2027 epochs like `1820000000000`). The upgrade cursor (`upgrade-aware-entity-metadata.adapter.ts`) tracks a **single** most-recent applied step: it looks up the latest `core."upgradeMigration"` row's name in the sequence (sorted by timestamp within kind) and hides every `@WasIntroducedInUpgrade` column at or past that index. Two ways this breaks, and both were live on main: 1. **Cursor regression**: a command merged *later* with a *smaller* timestamp (e.g. `pendingQuestion` at `1811…` after `metadata-overrides` at `1820…` had run) sorts *before* already-applied steps. When migrate runs, the "latest" row now points earlier in the sequence, re-hiding columns that were already applied. 2. **Migrate not running at all** (Felix's hypothesis): if the deploy pipeline skipped `database:migrate:prod`, none of the 2-19 rows exist and every 2-19-gated column is hidden. Both hypotheses have the same fix path; the discriminating query is in the verification section below. ## Fix **Real timestamps** (per maintainer direction — no more fabricated epochs): | Command | Old (fabricated) | New (real merge epoch) | Introduced by | |---|---|---|---| | workspace: backfill-workspace-custom-application-registration | `1820000000000` | `1782853718000` | #22378 | | fast: add-metadata-overrides-column | `1820000100000` | `1782986475000` | #22417 | | slow: backfill-metadata-overrides | `1820000110000` | `1782986476000` (+1s to order after its fast pair) | #22417 | | fast: add-last-stream-error-to-agent-chat-thread | `1821000000000` | `1782996657000` | #22434 | | fast: add-pending-question-to-agent-chat-thread | `1811000000000` | `1782999138000` | #22346 | | fast: add-workspace-discoverability-to-workspace | `1820000001000` | `1783004140000` | #22423 | Each value is the committer epoch of the squash-merge commit that introduced the command on main (verified via `git log --diff-filter=A`). Sorted by real time, the fast sequence is strictly increasing, so the cursor can no longer regress. **Idempotency**: renaming a command changes its step name, so every one of these re-runs on any instance that already applied it under the old name (dev cluster, edge self-hosters — 2.19 is unreleased, so tagged releases are unaffected). All six are now safe to re-run: - `lastStreamError`, `pendingQuestion`, `metadata-overrides` fast: `ADD COLUMN IF NOT EXISTS` (already were) - `metadata-overrides` slow backfill: `WHERE … IS NULL` guard (already was) - workspace command: skips when `applicationRegistrationId` is already set (already did) - `workspaceDiscoverability`: **made idempotent in this PR** — `CREATE TYPE` wrapped in a `duplicate_object` handler, `ADD COLUMN IF NOT EXISTS` **CI guard** (replaces the append-only check added earlier on this branch): - Timestamps must be **real**: within `[now − 60 days, now + 2 days]`. This is the check that would have prevented the original sin — 2027 epochs can never pass. - Still **append-only** within the version directory, but computed from `git diff --name-status --find-renames` so renamed/copied files are checked too (cubic's P2), and files the PR deletes/renames away no longer count toward the existing max (otherwise a re-slotting PR like this one could never pass its own guard). - Covers `workspace-command-<ts>-` filenames, not just `instance-command-fast|slow-<ts>-`; skips `.spec.ts` files. - Failure message documents the escape path (re-slot the fabricated blocker to its real epoch + make it idempotent) and a bypass label `ci:allow-upgrade-command-timestamp-exception` for deliberate exceptions. ## Deploy sequencing (important) After this merges and deploys, `database:migrate:prod` **must run** before the API pods are relied on: the old step names no longer exist in the sequence, so until the renamed commands run once, the cursor resolves to 0 and *every* gated column is hidden. The commands are idempotent, so the re-run is harmless. Running API/worker pods only compute the cursor at boot — restart them after migrate. ## Verification / diagnosis on dev ```sql SELECT name, status, "createdAt" FROM core."upgradeMigration" WHERE "workspaceId" IS NULL ORDER BY "createdAt" DESC LIMIT 15; ``` - Latest rows named `…_182xxxxxxxxxx` (fabricated) and completed → migrate ran, cursor regressed (hypothesis 1). - No 2-19 rows at all → migrate never ran for 2-19 (hypothesis 2). - After the fix: latest row should be `2.19.0_AddWorkspaceDiscoverabilityToWorkspaceFastInstanceCommand_1783004140000`, status `completed`. https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38 |
||
|
|
868ae4cbfd |
feat(last-contact): design for Last contact by + Last contact item (#22308)
To test, go to https://twenty-applications.twenty.com/settings/applications/6aa2ca76-fdbe-456d-89ab-c622452ef055 ## After <img width="1512" height="697" alt="image" src="https://github.com/user-attachments/assets/5b17f94e-6e0e-400a-8291-a39ad796e423" /> ## What Adds the design spec for the next version of the `twenty-last-contact` public app, extending it from a single `lastContactAt` date column to the three-column experience in the app's cover image on the All People view: 1. **Last contact by** — the team member who last interacted with the person (`ACTOR` field). 2. **Last contact** — the existing `lastContactAt` field, unchanged. 3. **Last contact item** — the email or meeting that was the last contact, as a clickable record (`MORPH_RELATION` → message | calendarEvent). All three columns always describe the same single most-recent interaction (atomic "newer wins" update). This PR contains the **design doc only** — `docs/superpowers/specs/2026-06-29-last-contact-by-and-item-design.md`. Implementation follows. ## Why The app today only answers *when* you last talked to someone. These fields also answer *who* on your team and *through which* email/meeting, matching the product vision in the cover. ## Key design decisions - **`lastContactBy` is an ACTOR**, with the team member resolved from the interaction's participants (`messageParticipant` / `calendarEventParticipant` both carry `workspaceMemberId` + `workspaceMember`). - **No provider (Gmail/Outlook) logo.** That data lived on `connectedAccount`, which v2.7 (`drop-connected-account-standard-object`) removed from the app-queryable workspace schema. Confirmed acceptable; the actor still shows the member + an email/calendar source. - **`lastContactItem` is a MORPH_RELATION** following the SDK pattern used by `attachment` / `noteTarget` / `taskTarget` (shared `morphId`, one field per target, reverse relation on each target object). ## Reviewer notes - **Load-bearing open risk** documented in the spec: how to *write* a morph relation through the app's GraphQL API — no app in the repo writes morph yet. The plan starts with a spike on this; if morph writes aren't supported from an app, the fallback is two nullable `RELATION` fields (`lastContactMessage` / `lastContactCalendarEvent`). - No code/behavior change yet — safe to merge or hold as the design of record. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22308?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e82a47c9a2 |
chore: auto pre-translate untranslated docs strings before Crowdin pull (#22334)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22334?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
ba8e1bf5a3 |
chore(docs): self-clean orphans and surface failed languages in i18n … (#22278)
## Summary Two robustness fixes to `docs-i18n-pull.yaml` so localized docs can't silently drift: 1. **Prune orphan localized files.** The pull only adds/updates files, never deletes — so localized copies of renamed/moved/deleted English pages linger and serve dead URLs (recently ~113 of them). A new `prune-orphan-translations` script (run with `--apply` in the workflow, on real pulls only) removes any `l/<lang>/**` file whose English source no longer exists. 2. **Surface per-language download failures.** The loop previously swallowed failures with `|| echo "Warning..."`, so a language whose Crowdin server-side build fails (e.g. `ja`, failing at 79%) was skipped *silently* and froze indefinitely while every other language updated. We now collect failures, still commit the languages that succeeded, and **fail the run at the end** so a broken language is visible. --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
61d094b185 |
chore(docs): exclude code blocks and icon frontmatter from Crowdin translation (#22304)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22304?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
ddb3abfb23 |
fix(website): config-only Crowdin pull to match short-code catalogs (#22262)
## What `website-i18n-pull.yaml` passed inline `source` + `translation: '…/%locale%.po'` to the Crowdin action *on top of* the config file. After #22257 migrated the website to short-code catalogs (`ar.po`/`es.po`, and `crowdin-website.yml` → `%two_letters_code%`), that inline `%locale%` resolves to **full-code** filenames (`ar-SA.po`/`es-ES.po`) that no longer exist in the repo — so the pull never updates the real catalogs. ## Fix Drop the inline `source`/`translation` so the step is **config-only**, driven by `.github/crowdin-website.yml` (`%two_letters_code%`) — matching the front/app pull (`i18n-pull.yaml`), which is config-only and works. ## Also (ops, not in this diff) The pull doesn't run against `main` — its first step is `git checkout -B i18n-website origin/i18n-website`, so it operates on the long-lived `i18n-website` branch (same pattern as `i18n` for front and `i18n-docs` for docs). That branch was still at the **pre-#22257** layout (full-code `.po`, `%locale%` config, old 3-locale list), so it was reset to `main` to carry the new short-code structure. Once this PR merges, a pull run will land translations into the correct `ar.po`/`es.po`/… catalogs. |
||
|
|
012af11d77 |
feat(website): ship all documentation locales (multi-locale site) (#22257)
## What
The marketing site now serves every language the **documentation** ships
— 14 locales (`en, fr, ar, cs, de, es, it, ja, ko, pt, ro, ru, tr, zh`),
up from 3 (`en, es, fr`).
## How
- **Single source of truth.** `WEBSITE_LOCALE_LIST` derives directly
from `DOCUMENTATION_SUPPORTED_LANGUAGES` (`twenty-shared/constants`).
Add a documentation language → it flows to the website automatically.
- **Off `APP_LOCALES` entirely.** The website locale type is now
`DocumentationSupportedLanguage` (short codes), so a locale **is** its
URL segment — no short↔full mapping, and no `pt-BR`/`zh-CN` ambiguity to
resolve.
- **Removed the indirection this exposed** (it only existed because
`AppLocale` was a superset of the deployed set):
- `locale-to-url-segment` / `locale-by-url-segment` (locale == segment)
- `get-locale-messages` pass-through → callers read `MESSAGES_BY_LOCALE`
directly
- the `messages-by-locale` runtime guard → a total
`Record<DocumentationSupportedLanguage, Messages>` (a missing catalog is
now a **compile** error, not a runtime throw)
- `isWebsiteLocale` → a `string → DocumentationSupportedLanguage` type
guard
- the vestigial language-code `split('-')` in `locale-display-name`
## Catalogs
- Renamed `es-ES → es`, `fr-FR → fr`; added 11 new locales (untranslated
for now → **English fallback**).
- `crowdin-website.yml` switched to `%two_letters_code%`.
- Regenerating catalogs also synced `en.po` with current source
(`Boolean` / `Date & Time` / removed `Fields widget` from the
already-merged #22249).
- `ci-website` is unchanged — no `lingui:compile` step added; catalogs
stay committed.
## Testing
- `typecheck` · `lint` (check-conventions + oxlint + oxfmt) · 347/347
tests — all green. PR CI runs exactly lint + typecheck + test.
## Follow-up (out of repo)
Enable the 11 languages on **Crowdin project 4** so `website-i18n-pull`
backfills real translations. Until then, the new locales render with
English fallback (correct behavior).
|
||
|
|
538b180824 |
feat(dpa): self-serve Data Processing Agreement generator (#22243)
## What
A single, region-aware DPA that serves all customers, generated
automatically from the customer's deployment. Two layers:
1. **Click-through DPA** — recorded at signup (acceptance = execution),
resolving merge fields from the deployment region. Cloud only.
2. **In-app signed-PDF generator** — Settings → Legal → Generate DPA:
preview the agreement, enter legal entity + authorized signatory,
download a PDF pre-signed by Twenty, and store the executed copy against
the workspace with its template version + timestamp. Deep-linkable at
`/dpa` (login-gated) for `twenty.com/dpa`.
## How it resolves
A typed variable matrix (`dpa-region-config.constant.ts`) maps the
deployment region to the contracting Processor entity and terms:
- **EU (default)** → Twenty.com SAS, hosting EU/Frankfurt, governing law
France, SCC section dormant.
- **US (custom)** → Twenty, Inc., hosting US, SCC section active.
Region is a deployment-wide setting (`DPA_DEPLOYMENT_REGION`, default
EU) behind a `DpaRegionService` seam so it can later become
per-workspace without touching callers. The legal text is verbatim from
the template (generated into `dpa-template.constant.ts` directly from
the source `.docx`); only the 6 merge fields are filled and the SCC
sections (7.2–7.5) stay in the document for every region per the spec —
only field values branch. Sub-processors are deferred to
trust.twenty.com (not enumerated). Billing stays decoupled (Twenty, Inc.
remains merchant of record regardless of Processor).
## UI
Standard list + create-page pattern (mirrors API keys / webhooks): a
list of executed copies (with re-download) — or the agreement preview
when none exists — and a top-right blue **Generate DPA** CTA opening a
standard create page. The "Legal" item is intentionally **not** in the
settings menu; the page is reached via the `/dpa` deep link.
## Notable implementation details
- **PDF** is rendered server-side with `@react-pdf/renderer`. The
built-in standard-14 fonts only encode ASCII and crash on the template's
curly quotes / em–en dashes / accented Latin, so Liberation Sans (OFL)
is **subset to a Latin glyph set and embedded as base64 data: URLs** —
no font files to ship or resolve at runtime (works in dev, prod-Docker
and CI).
- New `core.dpaAgreement` table via a fast instance command (FK hash
reproduced to match TypeORM).
- Self-hosted deployments (billing disabled) skip click-through
recording and stamp a prominent "not a valid agreement" banner on the
preview and PDF.
## Tests
- Unit: resolver (per-region entity/law/SCC state, EU default, no
unresolved `{{ }}`, SCC sections present in both regions, self-hosted
notice) and HTML renderer.
- Integration (`test/integration/graphql/suites/dpa`): preview has no
unresolved fields; `generateSignedDpa` renders + persists + returns a
downloadable PDF (asserted with accented input to guard the font
regression); list re-download.
## ⚠ Needs legal input before go-live (marked `TODO_CONFIRM` in
`dpa-region-config.constant.ts`)
- Registered-office addresses for Twenty.com SAS and Twenty, Inc.
- US deployment governing law (the template only specifies France).
- DPO name and the Twenty pre-signed authorized signatory name/title.
## Out of scope (flagged per spec)
Intra-group legal agreement and any Stripe/billing-entity changes. A
future e-sign provider would plug in at `DpaService.generateSignedDpa` +
the signatory input.
> Draft until the integration test passes in CI and the legal
`TODO_CONFIRM` values are supplied.
https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22243?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
2662fda647 |
ci(preview-env): run preview environments on free ci-public Actions (#22245)
## Why PR preview environments were broken. The dispatch half (this repo) was fine, but the receiving `preview-env.yaml` in **ci-privileged** failed on every run — and ci-privileged is a **private** repo, so its 5h keepalive job bills paid Actions minutes anyway. The intended design (started but never wired up) runs the preview env on the **public** `twentyhq/ci-public` repo, where Actions minutes are free, and keeps no privileged token on the runner that executes PR-controlled code. This finishes that migration. ## What this PR does Repoints `preview-env-dispatch.yaml` to dispatch `preview-env.yaml` on **ci-public** instead of ci-privileged. The dispatcher app token is simply retargeted (still `actions:write` only, via `workflow_dispatch`). ## Companion PRs (must land together) - **twentyhq/ci-public** — converts `preview-env.yaml` to `workflow_dispatch`; dispatches the tunnel URL back to ci-privileged for the PR comment before any PR code runs. - **twentyhq/ci-privileged** — adds `preview-env-comment.yaml` (posts the PR comment with the App key) and deletes the now-dead `preview-env.yaml`. ## Manual prerequisites (cannot be done in a PR) - Install/authorize the `TWENTY_WORKFLOW_DISPATCHER` GitHub App on **ci-public** with `actions:write`. - On **ci-public**: add `vars.DOCKERHUB_RO_USERNAME`, `secrets.DOCKERHUB_RO_TOKEN`, and `secrets.CI_PRIVILEGED_DISPATCH_TOKEN` (fine-grained PAT, `actions:write` on ci-privileged only). ## Suggested merge order 1. ci-privileged (receiver must exist on `main` before ci-public dispatches to it) 2. ci-public (workflow must exist on `main` before this repo dispatches to it) 3. this PR ## Test plan - [ ] Open an internal PR touching `packages/twenty-docker/**`; confirm `Preview Environment Dispatch` runs and triggers `Preview Environment` on ci-public. - [ ] Confirm the trycloudflare URL sticky comment lands on the PR (posted by ci-privileged's `preview-env-comment`). - [ ] Confirm a `preview-app` label on an external contributor PR triggers the same flow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01VqZBvafHqdCGtHZUT216C5)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22245?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c3183f7828 |
Forward parent commits to Argos visual regression dispatch (#22174)
Part of the Argos orphan-build fix. The dispatch now lists the merge-base plus its ancestors (up to 100) and forwards them as `parent_commits`, so the self-hosted Argos can walk back to the nearest commit with a reference build instead of orphaning when the exact merge-base lacks one. Companion to twentyhq/twenty-argos#11 (deploy that first) and the ci-privileged change that passes the input through to build creation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22174?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
f416f81548 |
fix(ci): retry Danger.js on transient GitHub API fetch errors (#22151)
## Problem The `danger-js` job in **CI Utils** has been failing across most PRs. The failure is not a real Danger violation — it's a transient network error fetching the PR diff/commits from the GitHub API: ``` Failed to fetch GitHub pull request files: FetchError: Invalid response body while trying to fetch https://api.github.com/repos/twentyhq/twenty/pulls/XXXXX/files?page=1&per_page=100: Premature close at Gunzip.<anonymous> (.../node_modules/node-fetch/lib/index.js:400:12) errno: 'ERR_STREAM_PREMATURE_CLOSE', code: 'ERR_STREAM_PREMATURE_CLOSE' ``` GitHub closes the gzipped HTTP response mid-stream, and Danger's bundled `node-fetch` has **no retry** on a dropped connection — so any single blip fails the whole check. ## Why is this happening now? Nothing on our side changed at the boundary where failures started. The Node 24.16 bump landed Jun 8 (the job stayed green for 2+ weeks after), Danger has been pinned at `13.0.4` for months, and there are **zero commits** to `.nvmrc`, `twenty-utils/package.json`, or the `yarn-install` action since Jun 22. What changed is GitHub's API reset rate, and it changed abruptly: | Day | Failures | Successes | Failure rate | |-----|----------|-----------|--------------| | Jun 23 | 1 | 48 | ~2% (green) | | Jun 24 | 38 | 204 | ~16% | | Jun 25 | 23 | 25 | **~48%** | The same PR passes on one run and fails on the next (e.g. one PR shows up as both pass and fail; another failed 3 runs in a row) — a code bug can't flip outcomes on identical input, only an infrastructure flake can. When GitHub's connection-reset rate was ~0% we never noticed; now that it's in the tens of percent, roughly half of all PRs trip it. ## Fix Wrap the Danger invocation in a small retry loop (3 attempts, 5s backoff) so the check absorbs these transient fetch errors instead of red-flagging the PR. Applied to both the `danger-js` and `congratulate` jobs since they share the same failure mode. This is the correct mitigation rather than a code revert — there's no change on our side to revert. 3 attempts drop a ~48% single-shot failure rate to ~11%, and a less-degraded ~16% rate to well under 1%. If GitHub's reliability recovers, the retries simply stop firing and cost nothing. This is a CI-only change — no application code is touched. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22151?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
fb85d29d64 |
chore: disable dependabot version-update PRs (#22119)
## What Sets `open-pull-requests-limit: 0` on all three npm entries in `.github/dependabot.yml`, disabling routine version-update PRs. ## Why Dependabot's `open-pull-requests-limit` is a *concurrent* cap, not a weekly throughput cap. With a large dependency backlog, every time a PR is closed or merged Dependabot backfills the freed slot with the next outdated dependency — producing an endless trickle of individual PRs rather than the intended "few per week". Setting the limit to `0` stops version-update PRs entirely. ## Impact - **Routine version-bump PRs:** disabled across root + public/internal apps. - **Security advisory updates:** unaffected — these are a separate Dependabot channel not governed by this limit, so vulnerability patches still open automatically. The existing `groups:` config on the apps entries is now inert but left in place, so version updates can be re-enabled later by simply raising the limit. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22119?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c71663946e |
chore(apps): move public apps to packages/twenty-apps/public and generalize CI workflow (#22096)
## What Introduces a `packages/twenty-apps/public/` folder and moves the publicly publishable apps into it, then generalizes the apps CI workflow to cover both folders. ### Moves The following apps were moved from `packages/twenty-apps/internal/` to `packages/twenty-apps/public/` (via `git mv`, history preserved): - `people-data-labs` - `twenty-discord` - `twenty-exa` - `twenty-fireflies` - `twenty-last-contact` - `twenty-linear` - `twenty-meeting-bot` - `twenty-slack` These remain in `internal/`: `self-hosting`, `twenty-for-twenty`, `twenty-partners`. ### Workflow - Renamed `.github/workflows/ci-internal-apps.yaml` → `.github/workflows/ci-twenty-apps.yaml`. - The discover job now scans **both** `packages/twenty-apps/internal` and `packages/twenty-apps/public`: - the "no nested `.github`" guard checks both folders, - `changed-files` watches both globs, - the matrix builder iterates over both roots (guarded with `existsSync` so a missing folder is a no-op). - Each matrix entry still carries its own `path`, so the `ci` job works unchanged regardless of which folder an app lives in. ## Notes - The apps are standalone packages (own `yarn.lock`, not part of the root Nx workspaces), so no root `package.json` / `nx.json` / `tsconfig` changes were needed. - The companion publish workflow lives in `twentyhq/twenty-infra` (`publish-internal-apps.yaml` → `publish-public-apps.yaml`) and is updated in a paired PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01Fmu3DWf1yTTkVW49eSkXwh)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22096?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
dd9ad876a4 |
Reduce published twenty-ui npm package size (#22087)
The published `twenty-ui@1.0.0-alpha.0` tarball was ~181 MB unpacked (27 MB compressed, 2,701 files). This was a build-config issue, so a clean CI build would reproduce the same size. Main fix: externalize `@tabler/icons-react` instead of bundling it. It was forced into the bundle and aliased to the full icon barrel, inlining the entire icon set into every entry point in both ESM and CJS (~81% of the package). It stays a `dependency`, so consumers still get it; the dynamic `<Icon name>` registry still resolves icons at runtime. Also: - Stop emitting/shipping declaration maps (`declarationMap: false`). - Exclude the internal `dist/individual` build and `*.map` from the tarball via `files` (it still builds locally for `twenty-front-component-renderer`). - Clean up the stale `files` / `project.json` build outputs at their source, `scripts/generateBarrels.ts`. - Add a `pack-size` CI guard (30 MB unpacked budget) and wire `size` + `pack-size` into `ci-ui.yaml`. Result: ~181 MB to ~2.3 MB unpacked (0.40 MB tarball, 400 files). All export subpaths, types, and icon rendering verified intact in both module formats. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22087?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
5e5c8e0956 |
ci(server,emails): run lingui extract & compile on PRs to gate i18n breakage (#22086)
## Why Translations for `twenty-server` and `twenty-emails` are only extracted **after merge** — in `i18n-push.yaml` (push to `main`). Their PR workflows (`ci-server.yaml`, `ci-emails.yaml`) never run `lingui extract`, so a change that crashes extraction passes every PR check and only fails post-merge — the same process gap that let the frontend crash through (fixed in #22080, frontend gate in #22084). Both projects have a `lingui:extract` target and are extracted by `i18n-push.yaml`, so the equivalent guard applies. ## What - **`ci-server.yaml`** — add `lingui:extract` to the existing `server-lint-typecheck` job's `nx-affected` tasks (`tag: scope:backend`). That job already builds `twenty-shared` and is already part of `ci-server-status-check`, so no new job/wiring is needed: ``` tasks: lint,typecheck -> tasks: lint,typecheck,lingui:extract ``` - **`ci-emails.yaml`** — add a `lingui:extract` step to the `emails-test` job (this workflow has no `nx-affected` job, so a direct target run fits): ```yaml - name: Extract translations (lingui) run: npx nx run twenty-emails:lingui:extract ``` Both run the same extraction command as the post-merge `i18n-push` workflow, so failures are caught before merge. ## Notes - `lingui extract` exits non-zero on extraction failures (verified on the frontend crash: exit code 1), so these steps genuinely fail the job. - Both jobs already gate on `changed-files-check`, so extract only runs when the respective package changes. - `nx affected -t=lingui:extract` only runs for projects that have the target; `lingui:extract`'s `^build` dependency is resolved automatically by nx. Completes the extract-gate coverage started for the frontend in #22084. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22086?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
7820665006 |
ci(front): run lingui extract & compile on PRs to gate i18n breakage (#22084)
## Why `lingui extract` currently only runs **after merge** — in `i18n-push.yaml`, which triggers on push to `main`. PR CI (`ci-front.yaml`) runs `lint`, `typecheck`, `test`, and `build`, but never `lingui extract`. So a change that crashes extraction passes every PR check and only blows up later in the CD `build-front / s3-build` job. That's exactly what happened with the spread-in-`i18n._()` crash fixed in #22080: ``` Cannot process file .../build-crud-tool-status-message.util.ts: Cannot read properties of undefined (reading 'name') at @lingui/babel-plugin-extract-messages/dist/index.cjs:88:22 ``` ## What Add `lingui:extract` to the `front-task` matrix in `ci-front.yaml`. It now runs alongside `lint`/`typecheck`/`test` via the existing `nx-affected` action: ``` npx nx affected -t=lingui:extract --exclude='*,!tag:scope:frontend' ``` This runs the **exact command that fails** in the CD build, so it catches this bug class — and any other change that breaks extraction — before merge, not after. ## Notes / verification - Confirmed `lingui extract --overwrite --clean` exits **non-zero** on the crash (verified locally on the pre-fix source: exit code 1), so the matrix job fails as intended. - The job only runs when frontend files change (`changed-files-check` gate), and `nx affected -t=lingui:extract` only runs for projects that actually have the target, so non-frontend projects are skipped. - Extract writes to `.po` files in the runner; that's ephemeral and not committed — the gate only asserts the command succeeds, it does not check catalog diffs. - Scope is intentionally frontend-only (this workflow is `ci-front`). `twenty-server` / `twenty-emails` extraction is not gated on PRs by this change; a follow-up could add the equivalent to the backend CI if desired. Companion to #22080 (the actual fix); this PR closes the process gap that let it through. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22084?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8842a80a44 |
ci(server): cross-version upgrade check on PRs (v1.22 → from source) (#22065)
## What Adds a pre-merge CI check that proves a database created and seeded by the **oldest supported release** (`twentycrm/twenty:v1.22` from Docker Hub) can be upgraded by the **current version built from source**, and that the upgraded instance comes up healthy with its data still queryable. Runs only on PRs touching the upgrade path (`upgrade-version-command/**` + `core-modules/upgrade/**`), and blocks the PR via `ci-server-status-check`. ## How New reusable workflow `ci-cross-version-upgrade.yaml` (`workflow_call` + `workflow_dispatch`), called from `ci-server.yaml` after `server-build` so the build cache is populated in-run: 1. **Services** — `postgres:16` (prod parity) + `redis:7` on a docker network. 2. **Old version** — pull `twentycrm/twenty:v1.22`, boot it against the DB, `workspace:seed:dev`, sanity-check the seed via `psql`. 3. **New version (from source)** — restore the `server-build` nx cache (best-effort: a miss just cold-builds), `nx build`, run the `upgrade` command against the same DB, `start:ci`, poll `/healthz`. 4. **Smoke** — assert `upgrade:status` shows `Instance: Up to date` / `0 behind, 0 failed`, then run companies/people/metadata GraphQL queries. The job is always invoked but gated by a `skip` input (computed from the upgrade-paths `changed-files` check), with a `no-op` job reporting success when skipped — so the status check always resolves instead of leaving a dangling skipped job, mirroring the twenty-infra pattern. Unlike the equivalent post-merge gate in infra-twenty, this is **pre-merge**, uses **native PR path filtering** (no compare API), and **reuses the from-source build cache** instead of pulling an ECR image — no cross-repo plumbing, no skipped-commit gap. ## Security note No credentials are committed. `APP_SECRET` is generated fresh per run (`openssl rand`, `::add-mask::`'d) and shared between the old container and the from-source server within the job; the smoke-test API token is minted at runtime via `workspace:generate-api-key` against the upgraded server and masked in logs. ## Verified with a real run Validated end-to-end by temporarily touching the upgrade path to trigger the job (trigger commit since dropped), in [CI Server run `28097035272`](https://github.com/twentyhq/twenty/actions/runs/28097035272) → [`cross-version-upgrade` job](https://github.com/twentyhq/twenty/actions/runs/28097035272/job/83189453451) ✅ **all steps green**: - v1.22 container boot → `workspace:seed:dev` → `psql` seed sanity check ✅ - from-source build (nx cache restored) → `upgrade` → **56 workspace(s) succeeded, 0 failed** ✅ - server healthy → API token minted at runtime via `workspace:generate-api-key` ✅ - `upgrade:status` → `Instance: Up to date`, `0 behind, 0 failed` ✅ - companies / people / metadata GraphQL smoke queries ✅ - `no-op` job correctly skipped (real job ran because the gate matched) ✅ The three assumptions originally flagged for first-run all held; one bug was found and fixed in the process — `upgrade:status` colorizes via `chalk` even with `NO_COLOR`, so the assertion now strips ANSI escapes before grepping. > Note: the overall `ci-server` run shows a failure from an **unrelated flaky integration test** (`if-else-workflow.integration-spec.ts`, `column workspaceMember.region does not exist` in shard 11). The same trigger commit passed all 16 integration shards in the prior run — it's a pre-existing flake, not caused by this PR. ## Note Still keeping the equivalent one inside infra-twenty as an final bottleneck just in case <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22065?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
d00d26c4a4 |
ci: remove redundant twenty-meeting-bot per-app workflow (#22057)
## What Removes `.github/workflows/ci-internal-app-twenty-meeting-bot.yaml`. ## Why The generic `ci-internal-apps.yaml` already runs CI for every app under `packages/twenty-apps/internal/` that has a `package.json`. For each discovered app it runs: - `yarn lint` - `yarn typecheck` (when a `typecheck` script exists) - `yarn test:unit` (when a `test:unit` script exists) - `yarn test` integration tests against a spawned Twenty instance (when a `test` script exists) `twenty-meeting-bot` defines all four scripts (`lint`, `typecheck`, `test:unit`, `test`), so it is fully covered by the generic workflow. It was the **last remaining** per-app workflow — all the other internal apps (discord, exa, fireflies, self-hosting, for-twenty, linear) were already migrated to `ci-internal-apps.yaml`. ## Note The removed workflow had two behavioral differences from the generic one, which are the same standardized tradeoffs already accepted for every other internal app: - It built `twenty-server` from source and ran integration tests against it, whereas the generic workflow tests against the published `twentycrm/twenty-app-dev:latest` image via the `spawn-twenty-app-dev-test` action. - It also triggered on changes to `twenty-server` / `twenty-sdk` / `twenty-client-sdk` / `twenty-shared`, whereas the generic workflow only triggers on `packages/twenty-apps/internal/**` changes. > [!NOTE] > If `ci-internal-app-twenty-meeting-bot-status-check` is configured as a required status check in branch protection, that rule should be dropped (and `ci-internal-apps-status-check` kept) so PRs aren't blocked waiting on a check that no longer runs. https://claude.ai/code/session_013WZuk6jw2RmZT77enuMT2y --- _Generated by [Claude Code](https://claude.ai/code/session_013WZuk6jw2RmZT77enuMT2y)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22057?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
de610bc4e7 |
Rename CI New UI workflow to CI UI (#22030)
Renames the `CI New UI` workflow to `CI UI`, dropping the "new ui" terminology everywhere it appeared. - Renamed `.github/workflows/ci-new-ui.yaml` → `ci-ui.yaml` - Updated the workflow `name`, job names (`ui-task`, `ui-sb-build`, `ui-sb-test`, `ci-ui-status-check`), and internal `needs`/`if` references - Updated `visual-regression-dispatch.yaml` which keys off the workflow name (`CI UI`) Note: the required status check in branch protection settings (workflow name / `ci-ui-status-check`) lives in repo settings and will need updating by an admin so PRs don't wait on the old check name. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22030?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6055262012 |
Deploy website only when twenty-website changes (#21977)
## Problem Every push to `main` redeploys the website. `cd-deploy-main.yaml` dispatches the infra `auto-deploy-main` fan-out, which unconditionally triggered `deploy-website`. Since most PRs don't touch `packages/twenty-website/**`, the majority were full, identical Cloudflare builds — wasted CI and noise. ## Fix Detect website changes here (using the existing reusable `changed-files.yaml`) and pass the result as a `deploy_website` input on the **single** `auto-deploy-main` dispatch: - New `website-changed-files` job checks `packages/twenty-website/**`. - The existing `auto-deploy-main` dispatch now carries `-f deploy_website=<true|false>`. - This repo keeps triggering **only** `auto-deploy-main` — it never dispatches `deploy-website` directly. `twenty-infra` decides whether to run the website deploy (per @FelixMalfait's review: the public repo shouldn't be able to run arbitrary `twenty-infra` workflows directly). - Server/front deploy is **unchanged** — still every merge. ## Paired change & merge order Companion PR: **twentyhq/twenty-infra#747** — adds the `deploy_website` input to `auto-deploy-main` and gates the website dispatch on it. **Merge twenty-infra#747 FIRST**, then this one. (If this merges first, it would pass an input the old `auto-deploy-main` doesn't accept, failing the dispatch. Infra-first only delays website auto-deploys until this lands — no breakage.) |
||
|
|
1646bdf35e |
Add twenty-partners on internal ci apps (#21975)
as title <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21975?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
3030b7d0e5 |
Add people-data-labs on internal ci apps (#21941)
Add people-data-labs to internal apps CI: remove from CI_EXCLUDED_APPLICATIONS and unify config with twenty-discord/twenty-slack. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21941?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
64385842bb |
Add twenty-linear on internal ci apps (#21942)
Add twenty-linear to internal apps CI: remove from CI_EXCLUDED_APPLICATIONS and unify config with twenty-discord/twenty-slack. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21942?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
4b868f9b29 |
Add twenty-for-twenty on internal ci apps (#21944)
Add twenty-for-twenty to internal apps CI: remove from CI_EXCLUDED_APPLICATIONS and unify config with twenty-discord/twenty-slack. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21944?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8b9e3a0fc3 |
Add self-hosting on internal ci apps (#21940)
Add self-hosting to internal apps CI: remove from CI_EXCLUDED_APPLICATIONS and unify config with twenty-discord/twenty-slack. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21940?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
b354df3c59 |
Add twenty-fireflies on internal ci apps (#21939)
Add twenty-fireflies to internal apps CI: remove from CI_EXCLUDED_APPLICATIONS and unify config with twenty-discord/twenty-slack. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21939?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
cb5d64fefc |
Add twenty-exa application to internal app ci (#21882)
renamed exa to twenty-exa add twenty-exa to ci check <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21882?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
584c567a7f |
Add twenty-discord on internal ci apps (#21928)
add twenty-discord to ci check <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21928?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
153e41e036 |
ci: block bot contributors from PR commit history (#21926)
## What Adds a CI check (`Blocked Contributors Check`) that runs on every PR and **fails** if any commit is attributed to a known bot — via the commit author, committer, or a `Co-Authored-By:` trailer. Goal: keep automated agents (Claude, Cursor, Copilot, …) out of Twenty's contributor history. ## How - On `pull_request` (`opened`, `synchronize`, `reopened`) it fetches all PR commits via the GitHub API and matches author/committer name+email and the full commit message (for trailers) against an editable blocklist. - Patterns target **bot identities** (emails / `[bot]` handles), **not** bare first names — so a human contributor named "Claude" is *not* flagged. - On failure it emits `::error::` annotations naming the offending SHA + what matched, plus remediation guidance (rebase with `--reset-author`, strip trailers, force-push). Current blocklist: ``` noreply@anthropic.com @anthropic.com cursoragent@cursor.com copilot-swe-agent[bot] ``` Add a line to block another bot — no logic changes needed. ## Notes - This workflow only *reports* a failed status. To actually block merges, add **Blocked Contributors Check** as a required status check in branch-protection rules for `main` (repo Settings → Branches). - `@anthropic.com` also blocks any Anthropic-domain identity; narrow to just `noreply@anthropic.com` if real Anthropic employees may contribute under their work email. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21926?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
a870e034a6 |
Add twenty slack to internal application ci (#21849)
- adds `twenty-slack` to internal application ci - unify config with twenty-last-contact app - add base oxlint config to show error twenty-shared is used in internal app <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21849?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
d19b7f8485 |
Enable getting started translations (#21842)
## Summary The Getting Started pages on the docs site (docs.twenty.com) were only ever available in English, never translated into the other supported languages. **Root cause:** The Getting Started section (added in #19728) was never added to the Crowdin source config (`crowdin-docs.yml`), so its `.mdx` files were never uploaded for translation. Only `user-guide`, `developers`, and `twenty-ui` were configured. This also surfaced a related bug: because the pages had no translations, the navigation generator fell back to the English page path for every language, duplicating paths like `getting-started/introduction` across all 14 language navs. Mintlify treats duplicate cross-language paths as undefined behavior, which broke the language switcher (it always redirected to `/getting-started/introduction`). ## Changes - `.github/crowdin-docs.yml` — add `getting-started/**/*.mdx` as a translation source so the pages get sent to Crowdin. - `packages/twenty-docs/scripts/fix-translated-links.sh` — add `getting-started` link-rewriting rules to match the other sections. - `packages/twenty-docs/scripts/generate-docs-json.ts` — only include a page in a non-default language when its translated file exists; drop empty groups/tabs (removes the duplicate cross-language paths that broke the switcher). - `packages/twenty-docs/docs.json` — regenerated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21842?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |