Compare commits

...

3 Commits

Author SHA1 Message Date
Charles Bochet 5bda5f0d68 Merge remote-tracking branch 'origin/main' into feat/argos-visual-regression-new-ui 2026-06-05 18:42:59 +02:00
Charles Bochet 5a1ae372dc refactor: simplify visual regression dispatch
Use a single event_type=visual-regression for all Argos projects.
ci-privileged routes based on project/artifact_name in the payload.

Collapsed from 5 dispatch jobs to 4 (twenty-ui, twenty-new-ui,
comparison-baseline, comparison-pr). Removed cloud/self-hosted
distinction since everything targets the same self-hosted Argos.
2026-06-05 18:05:31 +02:00
Charles Bochet 33a403c0bd feat: add Argos visual regression for twenty-new-ui
Set up CI + visual regression infrastructure for twenty-new-ui:

- Add ci-new-ui.yaml workflow (mirrors ci-ui.yaml for twenty-new-ui)
- Update visual-regression-dispatch.yaml to watch both CI UI and CI New UI,
  dispatching to ci-privileged for three Argos projects:
  1. Self-hosted twenty-ui pixel diff (existing, unchanged)
  2. Self-hosted twenty-new-ui pixel diff (new)
  3. Self-hosted twenty-ui vs twenty-new-ui comparison (new)
- Add visual regression documentation to twenty-new-ui README
- Resolve open question 4 (visual regression tooling: Argos confirmed)
2026-06-05 17:49:44 +02:00
3 changed files with 317 additions and 57 deletions
+106
View File
@@ -0,0 +1,106 @@
name: CI New UI
on:
pull_request:
merge_group:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name == 'pull_request'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-new-ui/**
packages/twenty-shared/**
new-ui-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }}
run: npx nx ${{ matrix.task }} twenty-new-ui
new-ui-sb-build:
needs: changed-files-check
if: >-
always() &&
(github.event_name == 'push' ||
needs.changed-files-check.outputs.any_changed == 'true')
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build storybook
run: npx nx storybook:build twenty-new-ui
- name: Upload storybook build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: storybook-twenty-new-ui
path: packages/twenty-new-ui/storybook-static
retention-days: 1
new-ui-sb-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: new-ui-sb-build
if: always() && needs.new-ui-sb-build.result == 'success'
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Download storybook build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: storybook-twenty-new-ui
path: packages/twenty-new-ui/storybook-static
- name: Install Playwright
run: |
cd packages/twenty-new-ui
npx playwright install
- name: Run storybook tests
run: npx nx storybook:test twenty-new-ui
- name: Upload screenshots for visual regression
if: always() && !cancelled()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-twenty-new-ui
path: packages/twenty-new-ui/screenshots
retention-days: 1
ci-new-ui-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, new-ui-task, new-ui-sb-build, new-ui-sb-test]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+157 -55
View File
@@ -3,10 +3,13 @@ name: Visual Regression Dispatch
# Dispatches visual regression processing to ci-privileged after CI completes.
# Runs in the context of the base repo (not the fork) so it has access to secrets,
# making it work for external contributor PRs.
#
# All dispatches use the same event_type=visual-regression with project/artifact_name
# in the payload. ci-privileged routes to the correct Argos project based on these.
on:
workflow_run:
workflows: ['CI UI']
workflows: ['CI UI', 'CI New UI']
types: [completed]
permissions:
@@ -15,19 +18,37 @@ permissions:
pull-requests: read
jobs:
dispatch-pr:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
resolve-context:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
workflow_name: ${{ steps.context.outputs.workflow_name }}
artifact_name: ${{ steps.context.outputs.artifact_name }}
has_artifact: ${{ steps.check-artifact.outputs.exists }}
is_pr: ${{ steps.pr-info.outputs.has_pr }}
pr_number: ${{ steps.pr-info.outputs.pr_number }}
merge_base_sha: ${{ steps.merge-base.outputs.sha }}
steps:
- name: Resolve workflow context
id: context
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const name = context.payload.workflow_run.name;
const artifactName = name === 'CI New UI'
? 'argos-screenshots-twenty-new-ui'
: 'argos-screenshots-twenty-ui';
core.setOutput('workflow_name', name);
core.setOutput('artifact_name', artifactName);
core.info(`Workflow: ${name}, artifact: ${artifactName}`);
- name: Check if screenshots artifact exists
id: check-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const artifactName = 'argos-screenshots-twenty-ui';
const artifactName = '${{ steps.context.outputs.artifact_name }}';
const runId = context.payload.workflow_run.id;
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
@@ -44,7 +65,9 @@ jobs:
}
- name: Get PR number
if: steps.check-artifact.outputs.exists == 'true'
if: >-
steps.check-artifact.outputs.exists == 'true' &&
github.event.workflow_run.event == 'pull_request'
id: pr-info
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
@@ -111,73 +134,152 @@ jobs:
core.setOutput('sha', '');
}
# ── Dispatch: twenty-ui pixel diff (CI UI, PRs + main) ──
dispatch-twenty-ui:
needs: resolve-context
if: >-
needs.resolve-context.outputs.workflow_name == 'CI UI' &&
needs.resolve-context.outputs.has_artifact == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
PR_NUMBER: ${{ needs.resolve-context.outputs.pr_number }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
REFERENCE_COMMIT: ${{ steps.merge-base.outputs.sha }}
REFERENCE_COMMIT: ${{ needs.resolve-context.outputs.merge_base_sha }}
ARTIFACT_NAME: ${{ needs.resolve-context.outputs.artifact_name }}
run: |
ARGS=(
--method POST
-f event_type=visual-regression
-f "client_payload[pr_number]=$PR_NUMBER"
-f "client_payload[project]=twenty-ui"
-f "client_payload[artifact_name]=$ARTIFACT_NAME"
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
-f "client_payload[repo]=$REPOSITORY"
-f "client_payload[branch]=$BRANCH"
-f "client_payload[commit]=$COMMIT"
)
if [ -n "$PR_NUMBER" ]; then
ARGS+=(-f "client_payload[pr_number]=$PR_NUMBER")
fi
if [ -n "$REFERENCE_COMMIT" ]; then
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
fi
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
# ── Dispatch: twenty-new-ui pixel diff (CI New UI, PRs + main) ──
dispatch-twenty-new-ui:
needs: resolve-context
if: >-
needs.resolve-context.outputs.workflow_name == 'CI New UI' &&
needs.resolve-context.outputs.has_artifact == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
PR_NUMBER: ${{ needs.resolve-context.outputs.pr_number }}
REFERENCE_COMMIT: ${{ needs.resolve-context.outputs.merge_base_sha }}
ARTIFACT_NAME: ${{ needs.resolve-context.outputs.artifact_name }}
run: |
ARGS=(
--method POST
-f event_type=visual-regression
-f "client_payload[project]=twenty-new-ui"
-f "client_payload[artifact_name]=$ARTIFACT_NAME"
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
-f "client_payload[repo]=$REPOSITORY"
-f "client_payload[branch]=$BRANCH"
-f "client_payload[commit]=$COMMIT"
)
if [ -n "$PR_NUMBER" ]; then
ARGS+=(-f "client_payload[pr_number]=$PR_NUMBER")
fi
if [ -n "$REFERENCE_COMMIT" ]; then
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
fi
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
# ── Dispatch: cross-comparison baseline (CI UI on main → twenty-ui-vs-new-ui) ──
dispatch-comparison-baseline:
needs: resolve-context
if: >-
needs.resolve-context.outputs.workflow_name == 'CI UI' &&
needs.resolve-context.outputs.has_artifact == 'true' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged (comparison baseline)
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
ARTIFACT_NAME: ${{ needs.resolve-context.outputs.artifact_name }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
--method POST \
-f event_type=visual-regression \
-f "client_payload[project]=twenty-ui-vs-new-ui" \
-f "client_payload[artifact_name]=$ARTIFACT_NAME" \
-f "client_payload[run_id]=$WORKFLOW_RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[branch]=$BRANCH" \
-f "client_payload[commit]=$COMMIT"
# ── Dispatch: cross-comparison PR (CI New UI on PRs → twenty-ui-vs-new-ui) ──
dispatch-comparison-pr:
needs: resolve-context
if: >-
needs.resolve-context.outputs.workflow_name == 'CI New UI' &&
needs.resolve-context.outputs.has_artifact == 'true' &&
needs.resolve-context.outputs.is_pr == 'true' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged (comparison PR)
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
PR_NUMBER: ${{ needs.resolve-context.outputs.pr_number }}
REFERENCE_COMMIT: ${{ needs.resolve-context.outputs.merge_base_sha }}
ARTIFACT_NAME: ${{ needs.resolve-context.outputs.artifact_name }}
run: |
ARGS=(
--method POST
-f event_type=visual-regression
-f "client_payload[project]=twenty-ui-vs-new-ui"
-f "client_payload[artifact_name]=$ARTIFACT_NAME"
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
-f "client_payload[repo]=$REPOSITORY"
-f "client_payload[branch]=$BRANCH"
-f "client_payload[commit]=$COMMIT"
-f "client_payload[pr_number]=$PR_NUMBER"
)
if [ -n "$REFERENCE_COMMIT" ]; then
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
fi
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
dispatch-main:
if: >-
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check if screenshots artifact exists
id: check-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const runId = context.payload.workflow_run.id;
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
});
const found = artifacts.artifacts.some(a => a.name === 'argos-screenshots-twenty-ui');
core.setOutput('exists', found ? 'true' : 'false');
if (!found) {
core.info(`Artifact not found in run ${runId} — skipping`);
}
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
--method POST \
-f event_type=visual-regression \
-f "client_payload[run_id]=$WORKFLOW_RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[branch]=$BRANCH" \
-f "client_payload[commit]=$COMMIT"
+54 -2
View File
@@ -215,11 +215,63 @@ Apollo error formatting, and the icon/theme-color pickers tied to Twenty's icon
- **Workbench** — Storybook (`@storybook/react-vite`). Every component has stories covering variants, sizes, and states (via `storybook-addon-pseudo-states`), in light and dark, with `autodocs`.
- **Functional** — component/interaction tests via `@storybook/addon-vitest` (real browser); unit tests (Jest) for hooks/utilities; coverage gate via `@storybook/addon-coverage`.
- **Accessibility** — Storybook a11y addon (axe-core) with `parameters.a11y.test = 'error'` so violations fail CI.
- **Visual parity** — visual regression (Chromatic or test-runner image snapshots) plus side-by-side stories rendering the old and new component with identical props; a pixel-diff threshold is the per-component acceptance gate.
- **Visual parity** — visual regression via Argos (self-hosted) plus a cross-package comparison project that diffs `twenty-new-ui` stories against `twenty-ui` stories with identical names; a pixel-diff threshold is the per-component acceptance gate. See [Visual regression](#visual-regression) below.
- **Performance & size** — `size-limit` per entry point with budgets; tree-shaking fixtures (importing one component must not pull the library); build-time tracking; render benchmarks via React Profiler; load-time via Lighthouse/Playwright on the built Storybook. As one concrete benchmark, a dedicated **stress story** renders a very large number of a single component (e.g. 10,000 buttons) and measures total render time — compared against the `twenty-ui` equivalent and gated against a budget to catch per-instance overhead regressions.
CI surfaces a per-PR diff table (`twenty-ui` vs `twenty-new-ui`) for size, a11y, and visual changes.
## Visual regression
Two Argos projects (on argos.twenty-internal.com) provide visual regression in CI:
1. **`twenty-new-ui`** — pixel diff of `twenty-new-ui` stories against the `main` branch baseline. Catches regressions introduced by a PR.
2. **`twenty-ui-vs-new-ui`** — cross-package comparison. The baseline is always `twenty-ui` screenshots from `main`; PR builds upload `twenty-new-ui` screenshots and diff them against the `twenty-ui` baseline. This shows exactly which components still differ between the two implementations.
For the cross-package comparison to produce meaningful diffs, stories in `twenty-new-ui` must use the **same title hierarchy** as `twenty-ui` (e.g. `UI/Input/Toggle`).
### Local visual diff
Run a pixel diff of `twenty-new-ui` components against `twenty-ui` using the self-hosted Argos instance.
**Prerequisites:**
- AWS SSO configured and logged in (`aws sso login --profile twenty-dev`)
- `twenty-infra/super-cli` cloned (sibling of this repo)
**1. Start the Argos tunnel**
In the `twenty-infra/super-cli` directory:
yarn cli argos-tunnel
This port-forwards the Argos service to `http://127.0.0.1:4002`.
Wait until the CLI shows "Argos tunnel is running".
**2. Set your Argos token**
Create a `.env` file in `packages/twenty-new-ui/` (gitignored):
ARGOS_TOKEN=<your-token-from-argos-project-settings>
**3. Run the visual diff**
From the repo root:
npx nx storybook:visual-diff twenty-new-ui
This builds Storybook, captures screenshots of every story, and uploads
them to Argos with build name `<username>/twenty-new-ui`. The diff
compares against the latest approved baseline.
To run `twenty-ui`'s visual diff in the same Argos instance (to build the
cross-package comparison baseline):
npx nx storybook:visual-diff twenty-ui
**4. View results**
Open `http://127.0.0.1:4002` in your browser (while the tunnel is running)
to review diffs.
## Build & publishing
- Vite library mode, dual ESM/CJS, `vite-plugin-dts`, `vite-plugin-svgr`; SCSS via Vite's built-in `sass`; no Babel.
@@ -267,7 +319,7 @@ a passing visual-parity diff, and a within-budget size entry.
1. Published package name: `twenty-new-ui` now, renamed to `twenty-ui` at cut-over (Phase 6).
2. Styling: confirm SCSS Modules vs vanilla-extract vs plain CSS Modules.
3. Variants helper: `clsx` + `data-*` vs `cva`.
4. Visual regression tooling: Chromatic vs self-hosted image snapshots.
4. ~~Visual regression tooling: Chromatic vs self-hosted image snapshots.~~ **Resolved:** Argos (self-hosted at argos.twenty-internal.com). See [Visual regression](#visual-regression).
5. How aggressively to drop `framer-motion` in favor of CSS/Base UI transitions.
6. Scope of `assets` / `testing` / `json-visualizer`: port verbatim or modernize.
7. Where to draw the generic-vs-app-specific line for `modules/ui`, and whether hybrid components live as a headless core in `twenty-new-ui` with a thin app wrapper in `twenty-front`.