Update CI: Argos visual regression for twenty-front storybook (#21454)

## 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)
This commit is contained in:
Raphaël Bosi
2026-06-12 15:36:16 +02:00
committed by GitHub
parent ab7c760872
commit c4453923f0
14 changed files with 97 additions and 57 deletions
+38 -4
View File
@@ -3,12 +3,14 @@ name: CI Front
on:
pull_request:
merge_group:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}${{ github.ref == 'refs/heads/main' && format('-{0}', github.sha) || '' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
@@ -18,7 +20,7 @@ env:
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
if: github.event_name == 'pull_request'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
@@ -33,7 +35,10 @@ jobs:
!packages/twenty-sdk/package.json
front-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
if: >-
always() &&
(github.event_name == 'push' ||
needs.changed-files-check.outputs.any_changed == 'true')
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
env:
@@ -65,11 +70,12 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: front-sb-build
if: always() && needs.front-sb-build.result == 'success'
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
storybook_scope: [modules, pages, performance]
storybook_scope: ${{ github.event_name == 'push' && fromJSON('["modules", "pages"]') || fromJSON('["modules", "pages", "performance"]') }}
env:
SHARD_COUNTER: 4
REACT_APP_SERVER_BASE_URL: http://localhost:3000
@@ -119,6 +125,14 @@ jobs:
npx http-server packages/twenty-front/storybook-static --port 6006 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6006 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
- name: Upload screenshots for visual regression
if: always() && !cancelled() && matrix.storybook_scope != 'performance'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-twenty-front-${{ matrix.storybook_scope }}-${{ matrix.shard }}
path: packages/twenty-front/screenshots
retention-days: 1
overwrite: true
# - name: Rename coverage file
# run: |
# if [ -f "packages/twenty-front/coverage/storybook/coverage-final.json" ]; then
@@ -160,6 +174,25 @@ jobs:
# npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
# - name: Checking coverage
# run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
front-sb-screenshots:
timeout-minutes: 5
runs-on: ubuntu-latest
needs: front-sb-test
if: always() && needs.front-sb-test.result == 'success'
steps:
- name: Download shard screenshot artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: argos-screenshots-twenty-front-*
merge-multiple: true
path: screenshots
- name: Upload merged screenshots artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-twenty-front
path: screenshots
retention-days: 1
overwrite: true
front-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
@@ -235,6 +268,7 @@ jobs:
# merge-reports-and-check-coverage,
front-sb-test,
front-sb-build,
front-sb-screenshots,
]
steps:
- name: Fail job if any needs failed
+1 -1
View File
@@ -10,7 +10,7 @@ permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}${{ github.ref == 'refs/heads/main' && format('-{0}', github.sha) || '' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
+1 -1
View File
@@ -10,7 +10,7 @@ permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}${{ github.ref == 'refs/heads/main' && format('-{0}', github.sha) || '' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
@@ -9,7 +9,7 @@ name: Visual Regression Dispatch
on:
workflow_run:
workflows: ['CI UI', 'CI New UI']
workflows: ['CI UI', 'CI New UI', 'CI Front']
types: [completed]
permissions:
@@ -24,6 +24,7 @@ jobs:
timeout-minutes: 5
outputs:
workflow_name: ${{ steps.context.outputs.workflow_name }}
project: ${{ steps.context.outputs.project }}
artifact_name: ${{ steps.context.outputs.artifact_name }}
has_artifact: ${{ steps.check-artifact.outputs.exists }}
is_pr: ${{ steps.pr-info.outputs.has_pr }}
@@ -36,12 +37,17 @@ jobs:
with:
script: |
const name = context.payload.workflow_run.name;
const artifactName = name === 'CI New UI'
? 'argos-screenshots-twenty-new-ui'
: 'argos-screenshots-twenty-ui';
const projectByWorkflow = {
'CI UI': 'twenty-ui',
'CI New UI': 'twenty-new-ui',
'CI Front': 'twenty-front',
};
const project = projectByWorkflow[name] ?? 'twenty-ui';
const artifactName = `argos-screenshots-${project}`;
core.setOutput('workflow_name', name);
core.setOutput('project', project);
core.setOutput('artifact_name', artifactName);
core.info(`Workflow: ${name}, artifact: ${artifactName}`);
core.info(`Workflow: ${name}, project: ${project}, artifact: ${artifactName}`);
- name: Check if screenshots artifact exists
id: check-artifact
@@ -134,18 +140,17 @@ jobs:
core.setOutput('sha', '');
}
# ── Dispatch: twenty-ui pixel diff (CI UI, PRs + main) ──
dispatch-twenty-ui:
# ── Dispatch: pixel diff for the triggering workflow's project (PRs + main) ──
dispatch-pixel-diff:
needs: resolve-context
if: >-
needs.resolve-context.outputs.workflow_name == 'CI UI' &&
needs.resolve-context.outputs.has_artifact == 'true'
if: 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 }}
PROJECT: ${{ needs.resolve-context.outputs.project }}
PR_NUMBER: ${{ needs.resolve-context.outputs.pr_number }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
@@ -157,47 +162,7 @@ jobs:
ARGS=(
--method POST
-f event_type=visual-regression
-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[project]=$PROJECT"
-f "client_payload[artifact_name]=$ARTIFACT_NAME"
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
-f "client_payload[repo]=$REPOSITORY"
@@ -1,6 +1,9 @@
import { setProjectAnnotations } from '@storybook/react-vite';
import { MotionGlobalConfig } from 'framer-motion';
import * as projectAnnotations from './preview';
MotionGlobalConfig.skipAnimations = true;
// Pre-warm the dynamic import used by WorkflowStepDecorator so the
// module is cached before any test runs (avoids flaky timeouts in CI).
import('~/testing/utils/getTestEnrichedObjectMetadataItemsMock');
+1
View File
@@ -148,6 +148,7 @@
"zod": "^4.1.11"
},
"devDependencies": {
"@argos-ci/storybook": "^6.0.6",
"@babel/core": "^7.14.5",
"@babel/preset-typescript": "^7.24.6",
"@graphql-codegen/cli": "^6.3.1",
@@ -3,6 +3,7 @@ import { expect, userEvent, within } from 'storybook/test';
import { ComponentDecorator } from 'twenty-ui-deprecated/testing';
import { CodeExecutionDisplay } from '@/ai/components/CodeExecutionDisplay';
import { ARGOS_CAPTURE_DISABLED } from '~/testing/constants/ArgosCaptureDisabled';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const samplePythonCode = `import pandas as pd
@@ -32,6 +33,7 @@ const meta: Meta<typeof CodeExecutionDisplay> = {
component: CodeExecutionDisplay,
decorators: [SnackBarDecorator, ComponentDecorator],
parameters: {
argos: ARGOS_CAPTURE_DISABLED,
container: { width: 600 },
},
args: {
@@ -1,6 +1,7 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ComponentDecorator } from 'twenty-ui-deprecated/testing';
import { ARGOS_CAPTURE_DISABLED } from '~/testing/constants/ArgosCaptureDisabled';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { WorkflowStepExecutionResult } from '@/workflow/components/WorkflowStepExecutionResult';
@@ -9,6 +10,9 @@ const meta: Meta<typeof WorkflowStepExecutionResult> = {
title: 'Modules/Workflow/Components/StepExecutionResult',
component: WorkflowStepExecutionResult,
decorators: [ComponentDecorator, SnackBarDecorator],
parameters: {
argos: ARGOS_CAPTURE_DISABLED,
},
args: {
result: JSON.stringify(
{ message: 'Hello World', status: 'success' },
@@ -7,6 +7,7 @@ import {
ComponentDecorator,
RouterDecorator,
} from 'twenty-ui-deprecated/testing';
import { ARGOS_CAPTURE_DISABLED } from '~/testing/constants/ArgosCaptureDisabled';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
@@ -80,6 +81,7 @@ const meta: Meta<typeof WorkflowEditActionCode> = {
title: 'Modules/Workflow/Actions/Code/EditAction',
component: WorkflowEditActionCode,
parameters: {
argos: ARGOS_CAPTURE_DISABLED,
msw: {
handlers: [
...graphqlMocks.handlers,
@@ -2,6 +2,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, within } from 'storybook/test';
import { ComponentDecorator } from 'twenty-ui-deprecated/testing';
import { ARGOS_CAPTURE_DISABLED } from '~/testing/constants/ArgosCaptureDisabled';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { HttpRequestExecutionResult } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/HttpRequestExecutionResult';
@@ -12,6 +13,7 @@ const meta: Meta<typeof HttpRequestExecutionResult> = {
component: HttpRequestExecutionResult,
decorators: [ComponentDecorator, SnackBarDecorator],
parameters: {
argos: ARGOS_CAPTURE_DISABLED,
layout: 'fullscreen',
},
};
@@ -4,6 +4,7 @@ import { within } from 'storybook/test';
import { getImageAbsoluteURI } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { SettingsLogicFunctionDetail } from '~/pages/settings/logic-functions/SettingsLogicFunctionDetail';
import { ARGOS_CAPTURE_DISABLED } from '~/testing/constants/ArgosCaptureDisabled';
import {
PageDecorator,
type PageDecoratorArgs,
@@ -29,6 +30,7 @@ const meta: Meta<PageDecoratorArgs> = {
},
},
parameters: {
argos: ARGOS_CAPTURE_DISABLED,
msw: {
handlers: [
...graphqlMocks.handlers,
@@ -0,0 +1,5 @@
export const ARGOS_CAPTURE_DISABLED = {
modes: {
default: { disabled: true },
},
};
+19
View File
@@ -1,3 +1,4 @@
import { argosVitestPlugin } from '@argos-ci/storybook/vitest-plugin';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';
import path from 'node:path';
@@ -6,6 +7,10 @@ import { defineConfig } from 'vitest/config';
const MINUTES_IN_MS = 60 * 1000;
const shouldCaptureArgosScreenshots = ['modules', 'pages'].includes(
process.env.STORYBOOK_SCOPE ?? '',
);
const dirname =
typeof __dirname !== 'undefined'
? __dirname
@@ -28,6 +33,20 @@ export default defineConfig({
? { storybookUrl: process.env.STORYBOOK_URL }
: { storybookScript: 'yarn storybook --no-open' }),
}),
...(shouldCaptureArgosScreenshots
? [
argosVitestPlugin({
uploadToArgos: !!process.env.ARGOS_TOKEN,
token: process.env.ARGOS_TOKEN,
apiBaseUrl: process.env.ARGOS_API_BASE_URL,
buildName: process.env.ARGOS_BUILD_NAME || undefined,
branch: process.env.ARGOS_BRANCH || undefined,
commit: process.env.ARGOS_COMMIT || undefined,
referenceCommit:
process.env.ARGOS_REFERENCE_COMMIT || undefined,
}),
]
: []),
],
test: {
name: 'storybook',
+1
View File
@@ -54822,6 +54822,7 @@ __metadata:
resolution: "twenty-front@workspace:packages/twenty-front"
dependencies:
"@apollo/client": "npm:^4.0.0"
"@argos-ci/storybook": "npm:^6.0.6"
"@babel/core": "npm:^7.14.5"
"@babel/preset-typescript": "npm:^7.24.6"
"@blocknote/mantine": "npm:^0.51.4"