## 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. -->
## 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. -->
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. -->
## 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. -->
## 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. -->
## 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. -->
## 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. -->
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. -->
## 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.)
## 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. -->
## 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>
## What
Removes the GitHub merge queue and runs what the queue used to gate —
the E2E (Playwright) suite — directly on push to `main`. If that run
fails on `main`, we ping engineering via webhook.
## Why
In the queue, only the `e2e-test` job in `ci-merge-queue.yaml` ran real
work — every other CI workflow's `merge_group` path skipped its
`changed-files-check` and tests, so the queue's status checks for those
were effectively green no-ops. The expensive thing actually gated was
E2E. Moving it to `push: main` validates the merged state post-merge
without the queue's batching overhead.
## Changes
- **Rename** `ci-merge-queue.yaml` → `ci-e2e-main.yaml` (`name: CI E2E
Main`).
- `e2e-test` now triggers on `push` to `main` (the `run-merge-queue` PR
label is kept as a manual opt-in for running E2E on a PR).
- Status-check job renamed `ci-e2e-main-status-check`.
- New `notify-main-ci-failure` job: on a failed **main push**, `POST`s
to `https://engineering.twenty.com/s/main-ci-failing` with the commit
SHA, actor, and run URL.
- **Strip dead merge-queue config** from the other CI workflows: removed
the `merge_group:` triggers and the now-unreachable `if:
github.event_name != 'merge_group'` guards from `ci-server`,
`ci-shared`, `ci-sdk`, `ci-front-component-renderer`,
`ci-test-docker-compose`, `ci-website`, and the `merge_group:` trigger
from `ci-front`, `ci-ui`, `ci-new-ui`.
## Required follow-up (not in this PR)
The merge queue itself is a **repo setting**, not code. After this
merges, disable **"Require merge queue"** on the `main` ruleset/branch
protection (Settings → Rules), otherwise GitHub keeps batching. Required
status checks tied to the old queue should also be dropped/updated.
## Behavior change
E2E now runs **after** merge rather than blocking it in the queue — a
bad change lands on `main` and then alerts (the webhook is the
mitigation), instead of being held back pre-merge.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21722?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. -->
The `twenty-new-ui` Argos project is being renamed to `twenty-ui` (and
the old `twenty-ui` / `twenty-ui-vs-new-ui` projects removed).
Updates the New UI visual-regression flow to target `twenty-ui`: the
dispatch project mapping and the screenshot artifact name (which must
match `argos-screenshots-${project}`), plus the internal storybook
artifact name for consistency.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21728?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. -->
Migrates `twenty-front`, `twenty-sdk`, and
`twenty-front-component-renderer` from `twenty-ui-deprecated` to
`twenty-ui` (mechanical import swap — the packages have API parity) and
deletes the deprecated package along with its workspace/CI/config
wiring.
Also adds `@linaria/react`/`@linaria/core` as direct deps of
`twenty-front` (it used them transitively via the deprecated package).
Note: move the required status check from `ci-ui-status-check` to
`ci-new-ui-status-check`.
Argos: the Storybook box-model/button-reset baseline shift (the bulk of
the visual diffs) is isolated in #21665 — Storybook now loads
twenty-ui's global `reset.scss`, which the production app already ships.
Once #21665 merges and this branch is rebased, the remaining Argos diffs
are component-level visual-parity items only.
# Introduction
Getting rid of the fine grained PAT used to dispatch to internal
repositories.
Repo dispatch requires the contents write permissions which is too wide
for such use
Refactored all senders and target to pass through a workflow dispatch
instead
Creating a centralize app that forges a token with actions: write only
provided permissions to mitigate any token exfiltrations
This PR adds the backend scheduling slice for call recording. It wires
the `twenty-meeting-bot` internal app to reconcile calendar events,
calendar-channel associations, and workspace member auto-record
preference changes, then schedule, cancel, or reschedule Recall bots
based on the resulting policy.
It also adds the needed calendar-channel owner lookup support, generated
metadata updates, app config/default role updates, unit tests, and CI
for the internal app.
Coming next:
- Recall webhook handling and signature validation
- Stale-state convergence for failed Recall cleanup/recreate cases
- Media, transcript, audio, and video ingestion
- Billing charge flow
- Frontend/settings UI for recording controls
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21629?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. -->
## Problem
`server-integration-test` shards have been failing intermittently across
unrelated PRs with a distinctive signature: the shard exits code 1 with
**no jest assertion failure, no `Test Suites:` summary, and no V8
`JavaScript heap out of memory` error** — the process just dies mid-run.
Failures hit random shards and clear on re-run (e.g. an unrelated branch
failed shard 6 once, then passed 3× on identical code), while the
`merge_group` gate stays green.
### Root cause
Each shard runs a **single in-band jest process** that boots one shared
NestJS app (`globalSetup` → `app.listen`) and holds it for the entire
shard, driving heavy metadata migrations + cache rebuilds in that one
process. `NODE_OPTIONS=--max-old-space-size=12288` let V8 grow to 12 GB
— *above* the `ubuntu-latest` runner's available RAM (16 GB, shared with
Postgres/Redis/ClickHouse). V8 therefore deferred aggressive GC and grew
past physical memory, so the **OS OOM-killer killed the process before
V8 hit its own ceiling** — which is why there's no heap error and no
jest summary, just a silent exit.
## Changes (CI/test-only — prod runtime untouched)
- **Lower the integration jest heap cap `12288` → `6144`** so V8
self-limits below physical headroom instead of being OS-killed.
Counterintuitively safer: a real leak now surfaces as a *visible* heap
error naming the test, rather than a silent death. (`database:reset`
keeps 12288 — it runs alone, before jest.)
- **Add `--logHeapUsage`** to the integration jest runs to expose the
per-file heap trend for confirming/pinpointing the growth.
- **Split integration tests across 16 shards (was 10)** to lower the
peak working set per shard.
- **Make perf logging a first-class `LoggerService` tool** (per
@prastoin's review): add `LoggerService.perf()` and unify the existing
`time()`/`timeEnd()` helpers into `perfTime()`/`perfTimeEnd()` (now
routed through the driver), all gated by a new `PERF_LOG_ENABLED` config
var. It **defaults on** so real environments keep emitting the
install-perf logs, and `.env.test` sets it `false` to mute the
per-action flood in integration tests. The `application-manifest` and
`validate-build` services were moved from the built-in `Logger` to
`LoggerService` to use it.
## Notes
- `--max-old-space-size` lives only in the `test:integration` nx target;
it is **not** the prod server heap setting, so prod is unaffected.
- This is mitigation. If `--logHeapUsage` shows monotonic growth across
files, there's a real accumulation in the long-lived app (retained
flat-maps / metadata cache) worth a follow-up heap-snapshot fix.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21588?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. -->
## Context
Dependabot keeps proposing a chokidar v3 to v4 bump (e.g. #21555), but
chokidar is intentionally pinned to v3 via a `resolutions` entry in the
root `package.json`.
chokidar v4 removed the native macOS FSEvents backend and falls back to
`node:fs.watch`, which hits `EMFILE` on a repo this size. That was
diagnosed and fixed in #20316. The bump can't even be satisfied while
the resolution is in place, so the lockfile and `package.json` end up
inconsistent and CI goes red.
## Change
Add `chokidar` to the dependabot ignore list so it stops re-proposing
the bump every cycle. The pin stays until upstream restores a native
watcher backend.
Closes out #21555.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21564?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. -->
## What
Bumps both \`anthropics/claude-code-action\` pins in
\`.github/workflows/claude.yml\` from the floating \`v1\` SHA
(\`dde2242\`) to \`v1.0.146\` (\`ac7e24b\`).
## Why
The old pin predates the fix for **CVE-2026-47751** (Tenable
TRA-2026-27, CVSSv4 5.3): claude-code-action checks out the PR head
branch and unconditionally sets \`enableAllProjectMcpServers: true\`, so
an attacker could ship a malicious \`.mcp.json\` in a PR branch and get
arbitrary command execution in the runner — with access to workflow
secrets — once a privileged user triggers the action.
Fixed upstream in **claude-code-action 1.0.78** (released 2026-03-24).
This pins to the current release, v1.0.146.
## Notes
- \`claude.yml\` is the only workflow in the repo referencing the
action; both job invocations (\`claude\` and \`claude-cross-repo\`) were
updated.
- All existing \`with:\` inputs remain valid in v1.0.146 — no deprecated
args.
Ref: https://www.tenable.com/security/research/tra-2026-27
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21499?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. -->
## Problem
A second `@claude` request on a PR while a Claude run is already in
progress gets silently cancelled. Concrete case on #21443: [this
comment](https://github.com/twentyhq/twenty/pull/21443#discussion_r3404037004)
("@claude please investigate and report", 14:24:28) produced [a
run](https://github.com/twentyhq/twenty/actions/runs/27421867558) that
was cancelled 3 seconds after creation, while [the run for an earlier
comment](https://github.com/twentyhq/twenty/actions/runs/27421856742)
was still in progress. Claude never responded.
## Root cause
The `concurrency` block is declared at the **workflow** level, keyed on
the PR number. Two GitHub Actions behaviors combine badly here:
1. A concurrency group holds at most one running + **one pending** run;
every new run entering the group cancels the previously pending one
(`cancel-in-progress: false` only protects the *running* run).
2. Workflow-level concurrency is acquired **before** job-level `if`
conditions are evaluated — so every `issue_comment` /
`pull_request_review_comment` / `pull_request_review` event on the PR
enters the group, even ones with no `@claude` that end up skipped.
So while a Claude run is in progress, any PR activity evicts the queued
`@claude` run. It's even self-defeating: a review-thread reply fires
*two* webhook events (`pull_request_review_comment` + a companion
`pull_request_review` with an empty body), so the companion event
cancels the queued comment run ~2s later. Claude's own "finished" reply
also fires events that kill whatever is queued.
## Fix
Move concurrency to the **job** level. A job whose `if` evaluates false
is skipped before it ever requests the concurrency slot, so only genuine
`@claude` jobs enter the queue. Real Claude runs on the same PR are
still serialized (no parallel pushes to the same branch).
Also gives `claude-cross-repo` its own group keyed on source repo +
issue number — previously a cross-repo dispatch for issue N shared a
group with PR N in this repo and they could needlessly queue behind each
other.
## Remaining limitation
GitHub keeps only one pending job per group: posting three `@claude`
requests while the first is still running will still cancel the second
when the third arrives. Zero-loss queueing would require a unique group
per comment, which would allow concurrent runs pushing to the same PR
branch — not worth it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21498?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. -->
## What
Adds Argos visual regression for `twenty-front`, reusing the storybook
CI already builds and the existing sharded test matrix. Stories in the
`modules` and `pages` scopes are captured as PNGs during
`front-sb-test`, merged into one artifact, and pixel-diffed against
`main` on the self-hosted Argos with results posted as a PR comment —
same pipeline as `twenty-ui` (#21210 / #21262).
## How
- **Capture**: `@argos-ci/storybook` vitest plugin, same setup as
`twenty-ui`. Skipped for `performance` stories (nondeterministic
profiling reports). Freezes framer-motion to avoid flaky diffs (#21412).
- **Sharding**: each modules/pages shard uploads a partial artifact; a
new `front-sb-screenshots` job merges them into
`argos-screenshots-twenty-front` (`overwrite: true` so re-runs work).
- **Baselines**: `CI Front` now runs on `push: main` — Argos resolves
base builds by exact merge-base commit, so every main commit needs a
build (#21217/#21222 pattern). Main pushes get a per-SHA concurrency
group so back-to-back merges can't cancel queued runs and leave baseline
gaps; the `performance` scope is dropped on push.
- **Dispatch**: `visual-regression-dispatch.yaml` watches `CI Front` →
`project=twenty-front`.
## Rollout
- ✅ Prod Argos project `twenty-front` created (id 68) +
`ARGOS_TOKEN_FRONT` secret set
- ⬜ Merge the twentyhq/ci-privileged companion PR **before** this one
- First PR builds show as *orphan* until the first main push creates a
baseline
(expected, same as the twenty-ui rollout)
Adds a dedicated CI workflow for the People Data Labs app that runs
lint, typecheck, and unit tests only when files under
`packages/twenty-apps/internal/people-data-labs/` change.
The app is a standalone yarn project (its own lockfile, pinned published
SDKs), so it isn't covered by the existing package CI workflows. The
workflow gates on the app folder via the reusable `changed-files.yaml`,
installs dependencies inside the app directory, and runs `yarn lint`,
`yarn typecheck`, and `yarn test`. A status-check job aggregates results
and stays green on skips, so it's safe to mark as a required check.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21487?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. -->
## Description
Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name
**`twenty-ui`** (v0.1.0, publishable) and renames the old package to
**`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports →
`twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates
twenty-front's `Toggle` to the new package (first consumer) as a
drop-in.
## Next steps
- Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` +
`.yarnrc.yml`), then tag `ui/v0.1.0` to publish.
- Continue migrating components from `twenty-ui-deprecated` →
`twenty-ui`.
## Summary
- Add `ci-new-ui.yaml` workflow mirroring `ci-ui.yaml` for
`twenty-new-ui` (lint, typecheck, test, Storybook build, Storybook test
with screenshot capture)
- Update `visual-regression-dispatch.yaml` to watch both `CI UI` and `CI
New UI` workflows, with dispatches for three Argos projects:
1. **Self-hosted twenty-ui pixel diff** (existing, unchanged)
2. **Self-hosted twenty-new-ui pixel diff** (new) -- standard regression
against main
3. **Self-hosted twenty-ui vs twenty-new-ui comparison** (new) --
cross-package visual parity
- Add "Visual regression" documentation section to
`twenty-new-ui/README.md` with local dev workflow (argos-tunnel via
super CLI + `storybook:visual-diff`)
- Resolve open question 4 in README (Argos confirmed as visual
regression tool)
## Companion PR
Requires companion PR on `twentyhq/ci-privileged` for the new
`visual-regression-cloud` dispatch handler, upload script, and
post-comment logic.
## Manual setup
After merging both PRs, create two Argos projects on
argos.twenty-internal.com:
- `twenty-new-ui` (pixel diff)
- `twenty-ui-vs-new-ui` (cross-package comparison, auto-approved branch:
main)
Add secrets `ARGOS_TOKEN_NEW_UI` and `ARGOS_TOKEN_COMPARISON` to
ci-privileged.
## Test plan
- [ ] Verify `CI New UI` workflow triggers on twenty-new-ui changes
- [ ] Verify screenshot artifact `argos-screenshots-twenty-new-ui` is
uploaded
- [ ] Verify dispatch sends `visual-regression-cloud` events to
ci-privileged
- [ ] Verify existing `CI UI` → self-hosted Argos flow is unchanged
# Introduction
Removed never used release dispatch workflow
Now assuming that anyone releasing will create both twenty and npm
family tags
Will create a workflow to ease this later
We will now start to have several github releases, one per namespace
## Summary
- Fixes Argos CI builds showing as "Orphan" (no reference branch) for PR
builds
- Computes the merge-base SHA between the PR head and `main` using the
GitHub API (`compareCommitsWithBasehead`) in the dispatch workflow
- Passes `reference_commit` in the `ci-privileged` dispatch payload so
it can be forwarded to the Argos upload API
## Context
PR builds on Argos were showing as "Orphan" because `ci-privileged`
(where the actual Argos upload happens) has no git history of the
`twenty` repo — it cannot compute the merge-base locally. Without a
`referenceCommit`, Argos can't determine which `main` build to compare
against.
The local `visual-diff.sh` script already passes
`ARGOS_REFERENCE_COMMIT` via `git merge-base HEAD main`, but the CI
pipeline was missing this. This PR adds equivalent logic using the
GitHub API (no checkout needed).
## Note for ci-privileged
The `upload-to-argos.ts` script in `ci-privileged` needs a corresponding
update to read `reference_commit` from the dispatch payload and pass it
as `referenceCommit` in the Argos API call:
```typescript
referenceCommit: process.env.REFERENCE_COMMIT || undefined,
```
## Test plan
- [ ] Verify the workflow runs successfully on a PR (merge-base step
computes a SHA)
- [ ] Confirm Argos PR builds are no longer marked as "Orphan" after the
ci-privileged counterpart is updated
## Summary
- Add explicit `if: always() && needs.ui-sb-build.result == 'success'`
to `ui-sb-test` job
## Context
After merging #21217, the main-branch Argos baseline pipeline doesn't
work: `ui-sb-test` is silently skipped on push to main, so no
`argos-screenshots-twenty-ui` artifact is produced.
**Root cause:** `changed-files-check` is skipped on push events
(PR-only). `ui-sb-build` handles this with `if: always() && ...`, but
`ui-sb-test` has no explicit `if` — GitHub Actions propagates the skip
through the transitive dependency chain (`changed-files-check` →
`ui-sb-build` → `ui-sb-test`).
## Test plan
- Merge this PR and verify the next push to main produces the
`argos-screenshots-twenty-ui` artifact
- Verify `dispatch-main` successfully triggers ci-privileged with the
artifact
## Summary
**CI: Main-branch Argos baselines**
- Run storybook build + screenshot capture on `push` to `main` in CI UI
workflow
- Add `dispatch-main` job in visual regression dispatch to forward
main-branch screenshots to ci-privileged
- Simplify `dispatch-pr` by inlining the artifact name and removing
unused `project` output
**Local visual diff support**
- Add `scripts/visual-diff.sh` for running Argos uploads locally via
tunnel
- Add `storybook:visual-diff` Nx target wrapping the script (depends on
`storybook:build`)
- Honor `STORYBOOK_URL` env in `vitest.config.ts` to reuse pre-served
static builds (mirrors twenty-front pattern)
- Support `ARGOS_BUILD_NAME`, `ARGOS_REFERENCE_BRANCH` env overrides in
vitest plugin config
## Context
Argos builds on PRs are all "Orphan" because there's no reference build
on `main` to compare against. The CI changes add the missing piece:
every merge to main now produces screenshots and uploads them to Argos
as reference builds.
The local visual diff script enables developers to run visual regression
checks from their machine against the self-hosted Argos instance via
`kubectl port-forward` (set up by the twenty-infra `argos-tunnel`
command).
## Related
- twentyhq/twenty-argos#1 (backend config for self-hosted HTTPS
redirect)
- twentyhq/twenty-infra#709 (argos-tunnel super CLI command +
self-hosted mode)
## Test plan
- [ ] Verify CI UI runs on next push to main and produces the
`argos-screenshots-twenty-ui` artifact
- [ ] Verify `dispatch-main` triggers and uploads screenshots to Argos
- [ ] Verify subsequent PR builds show diffs against the main baseline
instead of "Orphan"
- [ ] Run `ARGOS_TOKEN=<token> npx nx storybook:visual-diff twenty-ui`
locally with tunnel active
## Summary
- Only trigger visual regression on `CI UI` workflow (drop `CI Front`)
- Remove tarball re-packaging step — `ci-privileged` now downloads the
artifact directly via GitHub API
- Remove `mode`/`project` parameters from the dispatch payload
(hardcoded to twenty-ui in ci-privileged)
- Pass `run_id` of the triggering CI UI workflow so ci-privileged can
fetch the correct artifact
## Context
Part of the fast visual regression CI initiative. The `ci-privileged`
workflow has been simplified to only handle `twenty-ui` screenshots
uploaded directly to Argos.
## Test plan
- [x] Full E2E verified on production: screenshots → Argos build → diff
results → PR comment
## What & why
The audit-log viewer lived as a full-screen page reachable only via a
"View Logs" button buried in the **Security** tab. This surfaces it as
the **third tab in General settings** (`General | Security | Logs`),
consistent with the other tabs.
## Changes
- **Relocated** the event-logs module
`pages/settings/security/event-logs/` → `modules/settings/event-logs/`
and render it as tab content instead of a `FullScreenContainer` page.
Dropped `SettingsPath.EventLogs`, its route, and the fullscreen handling
in favor of the `general#logs` hash tab.
- **Security tab:** removed the "View Logs" entry; kept the
log-retention setting there.
- **In-tab gating** (shown to users with the Security permission):
Enterprise upgrade card when not entitled, a clear "ClickHouse not
configured" placeholder otherwise (derived from client config), and the
query is skipped when disabled. Replaces a bespoke error component that
string-matched error messages with the shared `SettingsEmptyPlaceholder`
/ `SettingsEnterpriseFeatureGateCard`.
- **Layout:** boxed content column with the table selector + filters
grouped in a `Card` and the results table below, matching settings
conventions. Kept the existing fixed filters (page/event name, member,
period) rather than recreating the record-view filter chips (those are
tightly coupled to record/view context).
Frontend + `twenty-shared` only — no changes to the log query or data.
## Test plan
- [x] `npx nx typecheck twenty-front` and `npx nx lint twenty-front`
pass
- [x] Settings → General shows three tabs; Logs is the third; breadcrumb
stays "Workspace / General"
- [x] With Enterprise + ClickHouse: table selector, filters, refresh,
and the paginated table work
- [x] Non-Enterprise: Enterprise upgrade card shown; no failing query
fires
- [ ] Enterprise without ClickHouse: shows the "ClickHouse not
configured" placeholder
- [ ] Security tab still shows the log-retention setting and the "View
Logs" button is gone
- [ ] A user without the Security permission sees neither the Security
nor Logs tab
## Summary
- Adds `@argos-ci/storybook` vitest plugin to `twenty-ui` for automatic
screenshot capture during vitest storybook tests
- Uploads captured screenshots (PNG, ~5MB) as a CI artifact instead of
passing the full storybook build
- Updates the visual regression dispatch workflow to pass
`mode=argos-screenshots` to ci-privileged, which then uploads
screenshots to Argos via CLI
This replaces the 10-minute Storybook screenshot capture with a ~30s
vitest browser-mode approach. The heavy screenshot work happens on free
public runners, while ci-privileged only handles the Argos API upload
(keeping secrets private).
## Architecture
```
twenty (public, free runners) ci-privileged (private)
───────────────────────────── ────────────────────────
1. Build storybook-static 4. Download screenshots artifact
2. Vitest captures screenshots 5. `argos upload` → Argos API
3. Upload screenshots artifact 6. Poll for results
7. Post PR comment
```
## Test plan
- [x] Verified locally: vitest captures 225 screenshots in ~28s
- [x] Verified `@argos-ci/cli upload` successfully creates Argos build
from captured screenshots
- [x] Argos diffs computed and results visible via API
- [ ] CI runs end-to-end on a PR
## Context
The Install Playwright step ran npx playwright install with no
arguments, which downloads all browsers (Chromium + Firefox + WebKit +
ffmpeg, ~500MB+) on every run with no caching.
Fix:
- Install Chromium only — npx playwright install chromium instead of all
browsers.
- Cache the browser binaries — actions/cache on ~/.cache/ms-playwright,
keyed on the resolved Playwright version (v4-playwright-browsers-${{
runner.os }}-<version>). On a cache hit the install step is skipped
entirely; the cache invalidates automatically when the Playwright
version bumps.
## Summary
Slims `preview-env-dispatch.yaml` to a single dispatch and deletes
`preview-env-keepalive.yaml`. The actual preview-env work moves to
**twentyhq/ci-privileged#22** (must merge as a pair).
## Why
Context: PR #20867 was a credential-exfil attempt against our workflows.
GitHub's default fork-PR-no-secrets policy + our existing gates
(`author_association` checks, `pull_request_target` checking out base,
`enableScripts: false`) neutralized the actual attack — but the audit
surfaced one workflow that *would* have given a malicious external PR
access to a real secret if a maintainer had applied the `preview-app`
label: `preview-env-keepalive.yaml`.
That workflow checked out the PR head SHA, did `docker login` with
`DOCKERHUB_PASSWORD`, then ran the PR's `docker-compose.yml`. A
malicious compose could have mounted `~/.docker/config.json` and
exfiltrated the Dockerhub credential.
After this PR, that workflow lives in `twentyhq/ci-privileged` instead,
paired with a rename of the credential to `DOCKERHUB_RO_TOKEN`
(Dockerhub PAT with `Public Repo Read-only` scope). A read-only PAT has
no exfiltration value — it's equivalent to anonymous Dockerhub access
plus rate-limit headroom — so the credential lives safely on the runner
without further hygiene tricks.
## What this PR does
- **Modifies** `.github/workflows/preview-env-dispatch.yaml`:
- Single dispatch to `twentyhq/ci-privileged` (was: self-dispatch to
twenty for the env + a separate dispatch to ci-privileged for the PR
comment).
- `permissions: {}` (was: `contents: write`).
- Drops `preview-env-keepalive.yaml` from the path-trigger list.
- **Deletes** `.github/workflows/preview-env-keepalive.yaml`. The
207-line workflow now lives in
`twentyhq/ci-privileged/.github/workflows/preview-env.yaml`.
Net `twenty` repo change: **-204 lines / +3 lines**.
## Companion PR
twentyhq/ci-privileged#22 — adds the new `preview-env.yaml`, deletes the
now-redundant `post-preview-comment.yaml`.
## Secrets fallout in this repo
After this PR, `DOCKERHUB_PASSWORD` in `twentyhq/twenty` secrets is only
used by `ci-test-docker-compose.yaml`, where:
- It evaluates to empty for fork PRs (GitHub default — secrets aren't
passed to fork-PR workflows).
- It's only needed for internal / merge_queue runs, for Dockerhub
rate-limit headroom on base-image pulls.
Recommend (separate change): also convert the twenty-side
`DOCKERHUB_PASSWORD` to a `Public Repo Read-only` Dockerhub PAT, and
rename it to `DOCKERHUB_RO_TOKEN` for consistency with ci-privileged.
The workflow change for `ci-test-docker-compose.yaml` would just be a
rename — login flow is identical for password vs. PAT.
## Test plan
- [ ] Merge twentyhq/ci-privileged#22 first (so the dispatched event has
a handler)
- [ ] Open an internal PR touching `packages/twenty-docker/**`, confirm
`Preview Environment Dispatch` runs and ci-privileged's `Preview
Environment` workflow runs the docker compose + posts the URL
- [ ] On an external contributor PR, apply the `preview-app` label,
confirm the same flow
- [ ] Confirm closing the PR doesn't break (no cleanup workflow was
changed)
- update ci-breaking-changes.yaml so it check for api contrat breaks
- check fails properly when removing fix
https://github.com/twentyhq/twenty/pull/20825
- check it turns green again when adding fix back