diff --git a/.github/workflows/ci-new-ui.yaml b/.github/workflows/ci-new-ui.yaml new file mode 100644 index 0000000000..7a005ce4a4 --- /dev/null +++ b/.github/workflows/ci-new-ui.yaml @@ -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 diff --git a/.github/workflows/visual-regression-dispatch.yaml b/.github/workflows/visual-regression-dispatch.yaml index bdc898cf1e..6447f03102 100644 --- a/.github/workflows/visual-regression-dispatch.yaml +++ b/.github/workflows/visual-regression-dispatch.yaml @@ -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" diff --git a/packages/twenty-new-ui/README.md b/packages/twenty-new-ui/README.md index b09043b79d..13f7e4ec91 100644 --- a/packages/twenty-new-ui/README.md +++ b/packages/twenty-new-ui/README.md @@ -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= + +**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 `/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`.