From e6614299c6a3b9a6c51146dbb9cac7b2fafeda3a Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 4 Jun 2026 08:46:37 +0200 Subject: [PATCH] feat(ci): integrate Argos visual regression via vitest screenshots (#21210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .github/workflows/ci-ui.yaml | 15 +- .../workflows/visual-regression-dispatch.yaml | 26 ++-- .gitignore | 1 + packages/twenty-ui/package.json | 1 + packages/twenty-ui/vitest.config.ts | 8 ++ yarn.lock | 128 +++++++++++++++++- 6 files changed, 159 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci-ui.yaml b/.github/workflows/ci-ui.yaml index f2adaa6c547..61cc80291db 100644 --- a/.github/workflows/ci-ui.yaml +++ b/.github/workflows/ci-ui.yaml @@ -87,17 +87,18 @@ jobs: npx http-server packages/twenty-ui/storybook-static --port 6007 --silent & timeout 30 bash -c 'until curl -sf http://localhost:6007 > /dev/null 2>&1; do sleep 1; done' npx nx storybook:test twenty-ui + - name: Upload screenshots for visual regression + if: always() && !cancelled() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: argos-screenshots-twenty-ui + path: packages/twenty-ui/screenshots + retention-days: 1 ci-ui-status-check: if: always() && !cancelled() timeout-minutes: 5 runs-on: ubuntu-latest - needs: - [ - changed-files-check, - ui-task, - ui-sb-build, - ui-sb-test, - ] + needs: [changed-files-check, ui-task, ui-sb-build, ui-sb-test] steps: - name: Fail job if any needs failed if: contains(needs.*.result, 'failure') diff --git a/.github/workflows/visual-regression-dispatch.yaml b/.github/workflows/visual-regression-dispatch.yaml index 67e0c15ad3d..e2f85a53af6 100644 --- a/.github/workflows/visual-regression-dispatch.yaml +++ b/.github/workflows/visual-regression-dispatch.yaml @@ -32,16 +32,18 @@ jobs: core.setOutput('artifact_name', 'storybook-static'); core.setOutput('tarball_name', 'storybook-twenty-front-tarball'); core.setOutput('tarball_file', 'storybook-twenty-front.tar.gz'); + core.setOutput('mode', 'storybook-tarball'); } else if (workflowName === 'CI UI') { core.setOutput('project', 'twenty-ui'); - core.setOutput('artifact_name', 'storybook-twenty-ui'); - core.setOutput('tarball_name', 'storybook-twenty-ui-tarball'); - core.setOutput('tarball_file', 'storybook-twenty-ui.tar.gz'); + core.setOutput('artifact_name', 'argos-screenshots-twenty-ui'); + core.setOutput('tarball_name', 'argos-screenshots-twenty-ui-tarball'); + core.setOutput('tarball_file', 'argos-screenshots-twenty-ui.tar.gz'); + core.setOutput('mode', 'argos-screenshots'); } else { core.setFailed(`Unexpected workflow: ${workflowName}`); } - - name: Check if storybook artifact exists + - name: Check if artifact exists id: check-artifact uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: @@ -59,7 +61,7 @@ jobs: core.setOutput('exists', found ? 'true' : 'false'); if (!found) { - core.info(`Artifact "${artifactName}" not found in run ${runId} — storybook build was likely skipped`); + core.info(`Artifact "${artifactName}" not found in run ${runId} — build was likely skipped`); } - name: Get PR number @@ -105,20 +107,20 @@ jobs: core.setOutput('has_pr', 'true'); core.info(`PR #${prNumber}`); - - name: Download storybook artifact from triggering run + - name: Download artifact from triggering run if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: ${{ steps.project.outputs.artifact_name }} - path: storybook-static + path: artifact-content run-id: ${{ github.event.workflow_run.id }} github-token: ${{ github.token }} - - name: Package storybook + - name: Package artifact as tarball if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' - run: tar -czf /tmp/${{ steps.project.outputs.tarball_file }} -C storybook-static . + run: tar -czf /tmp/${{ steps.project.outputs.tarball_file }} -C artifact-content . - - name: Upload storybook tarball + - name: Upload tarball if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: @@ -136,6 +138,7 @@ jobs: PROJECT: ${{ steps.project.outputs.project }} BRANCH: ${{ github.event.workflow_run.head_branch }} COMMIT: ${{ github.event.workflow_run.head_sha }} + MODE: ${{ steps.project.outputs.mode }} run: | gh api repos/twentyhq/ci-privileged/dispatches \ -f event_type=visual-regression \ @@ -144,4 +147,5 @@ jobs: -f "client_payload[repo]=$REPOSITORY" \ -f "client_payload[project]=$PROJECT" \ -f "client_payload[branch]=$BRANCH" \ - -f "client_payload[commit]=$COMMIT" + -f "client_payload[commit]=$COMMIT" \ + -f "client_payload[mode]=$MODE" diff --git a/.gitignore b/.gitignore index 3b1107c6568..21f31cf3a9e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ TRANSLATION_QA_REPORT.md .playwright-mcp/ .playwright-cli/ output/playwright/ +screenshots/ diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index 4206cc131c7..f46da1b6fe5 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -9,6 +9,7 @@ "**/*.css" ], "devDependencies": { + "@argos-ci/storybook": "^6.0.6", "@babel/core": "^7.14.5", "@babel/preset-env": "^7.26.9", "@babel/preset-react": "^7.26.3", diff --git a/packages/twenty-ui/vitest.config.ts b/packages/twenty-ui/vitest.config.ts index 5a9bb4bcd7b..2c32f2f6daf 100644 --- a/packages/twenty-ui/vitest.config.ts +++ b/packages/twenty-ui/vitest.config.ts @@ -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'; @@ -21,6 +22,13 @@ export default defineConfig({ configDir: path.join(dirname, '.storybook'), storybookScript: 'yarn storybook --no-open --port 6007', }), + argosVitestPlugin({ + uploadToArgos: !!process.env.ARGOS_TOKEN, + token: process.env.ARGOS_TOKEN, + apiBaseUrl: process.env.ARGOS_API_BASE_URL, + branch: process.env.ARGOS_BRANCH || undefined, + commit: process.env.ARGOS_COMMIT || undefined, + }), ], test: { name: 'storybook', diff --git a/yarn.lock b/yarn.lock index 5d83cb9bb39..7c6497327d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -526,6 +526,71 @@ __metadata: languageName: node linkType: hard +"@argos-ci/api-client@npm:0.22.0": + version: 0.22.0 + resolution: "@argos-ci/api-client@npm:0.22.0" + dependencies: + debug: "npm:^4.4.3" + openapi-fetch: "npm:^0.17.0" + p-retry: "npm:^8.0.0" + checksum: 10c0/10d61796a7f5c7ef7be3f781522d80f9f7eeef006ea691d64b0139d2337be7ada7ac452e46cdfb148fdbb0f29dc6714905fa9988de3e1ae5334f18594c13f222 + languageName: node + linkType: hard + +"@argos-ci/browser@npm:6.1.0": + version: 6.1.0 + resolution: "@argos-ci/browser@npm:6.1.0" + checksum: 10c0/3b5e59fc1652a86bc22aeed3ef36f19d3c769b6cb8a9f1ac0a97205e3ab90c13ddce6167d483bf43a7786ad23aa4635d4a53654935d614b6de4d43c0d6d31fb2 + languageName: node + linkType: hard + +"@argos-ci/core@npm:6.1.0": + version: 6.1.0 + resolution: "@argos-ci/core@npm:6.1.0" + dependencies: + "@argos-ci/api-client": "npm:0.22.0" + "@argos-ci/util": "npm:4.0.0" + convict: "npm:^6.2.5" + debug: "npm:^4.4.3" + fast-glob: "npm:^3.3.3" + mime-types: "npm:^3.0.2" + sharp: "npm:^0.34.5" + tmp: "npm:^0.2.5" + checksum: 10c0/47d688d1547d3672101c8f02e70736d1858db353852ab94389620e3c8f03a026d903357e2da97e0604a504ae7af229733879b006228d167ac904e8df0866f53a + languageName: node + linkType: hard + +"@argos-ci/playwright@npm:7.0.5": + version: 7.0.5 + resolution: "@argos-ci/playwright@npm:7.0.5" + dependencies: + "@argos-ci/browser": "npm:6.1.0" + "@argos-ci/core": "npm:6.1.0" + "@argos-ci/util": "npm:4.0.0" + chalk: "npm:^5.6.2" + debug: "npm:^4.4.3" + checksum: 10c0/7687306587aacd8654518d1f942816e34d405be9d07aab48fa86a4209dfd3c245f67bdef99436ccc58e18ac379bc5059f2fcab9624d3de87fc9e4c81073c13e0 + languageName: node + linkType: hard + +"@argos-ci/storybook@npm:^6.0.6": + version: 6.0.6 + resolution: "@argos-ci/storybook@npm:6.0.6" + dependencies: + "@argos-ci/core": "npm:6.1.0" + "@argos-ci/playwright": "npm:7.0.5" + "@argos-ci/util": "npm:4.0.0" + checksum: 10c0/37909b5323dc81a5c167e761f31912ad48cbb513732d71189b19c941e02360ecdf08144300a9b89900b93ef379966da032f79a2747ae66ac801d92804eec016c + languageName: node + linkType: hard + +"@argos-ci/util@npm:4.0.0": + version: 4.0.0 + resolution: "@argos-ci/util@npm:4.0.0" + checksum: 10c0/be54a0888dd1d5554bd8830da3f4647ac5596cab691605d3508d1723c2b9522375c9f1c462b145ac857d3cacf8f8a8fdcbf0d263d6cc7f6ef798311c46acc968 + languageName: node + linkType: hard + "@ark-ui/react@npm:^5.31.0": version: 5.31.0 resolution: "@ark-ui/react@npm:5.31.0" @@ -32155,6 +32220,16 @@ __metadata: languageName: node linkType: hard +"convict@npm:^6.2.5": + version: 6.2.5 + resolution: "convict@npm:6.2.5" + dependencies: + lodash.clonedeep: "npm:^4.5.0" + yargs-parser: "npm:^20.2.7" + checksum: 10c0/4773180a3d02eb9d5cf6a3b6ec2b6bc94e6b8dd5073cbf6035cb55f615286835af1fe7f549aee085909a1130f6b461476f9b6a0d1fdbdee733402289e5196c1e + languageName: node + linkType: hard + "cookie-signature@npm:1.0.7, cookie-signature@npm:~1.0.6": version: 1.0.7 resolution: "cookie-signature@npm:1.0.7" @@ -36275,7 +36350,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.7": +"fast-glob@npm:^3.2.7, fast-glob@npm:^3.3.3": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -40709,6 +40784,13 @@ __metadata: languageName: node linkType: hard +"is-network-error@npm:^1.3.0": + version: 1.3.2 + resolution: "is-network-error@npm:1.3.2" + checksum: 10c0/37edc576497b21d022754b49203358ee80fdac4a11a71a09687f38ad789ec437dd930c223301df39f1c920566692b31345e0108ae720996e592cab3879eca74f + languageName: node + linkType: hard + "is-node-process@npm:^1.0.1, is-node-process@npm:^1.2.0": version: 1.2.0 resolution: "is-node-process@npm:1.2.0" @@ -45429,6 +45511,15 @@ __metadata: languageName: node linkType: hard +"mime-types@npm:^3.0.2": + version: 3.0.2 + resolution: "mime-types@npm:3.0.2" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10c0/35a0dd1035d14d185664f346efcdb72e93ef7a9b6e9ae808bd1f6358227010267fab52657b37562c80fc888ff76becb2b2938deb5e730818b7983bf8bd359767 + languageName: node + linkType: hard + "mime@npm:1.6.0, mime@npm:^1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -47572,6 +47663,15 @@ __metadata: languageName: node linkType: hard +"openapi-fetch@npm:^0.17.0": + version: 0.17.0 + resolution: "openapi-fetch@npm:0.17.0" + dependencies: + openapi-typescript-helpers: "npm:^0.1.0" + checksum: 10c0/7e981d28c7839469662a73d44f3a85e05c1a5842a8afd5d6e36c75cbf085aa7f705862871cc7ce07c04605ddc936af3d5850c2cc66d193ee1fea07e7135da192 + languageName: node + linkType: hard + "openapi-fetch@npm:^0.9.7": version: 0.9.8 resolution: "openapi-fetch@npm:0.9.8" @@ -47595,6 +47695,13 @@ __metadata: languageName: node linkType: hard +"openapi-typescript-helpers@npm:^0.1.0": + version: 0.1.0 + resolution: "openapi-typescript-helpers@npm:0.1.0" + checksum: 10c0/3a148cf7d6a7f51124966a0fef64c2fbcf1a5a0a1e6320dd627f0733787ac4259877be43f7a7a2910684e3d5d238c6a9655c6f9e1f3adaa9a2a61477e3b8481f + languageName: node + linkType: hard + "openapi3-ts@npm:^3.0.0": version: 3.2.0 resolution: "openapi3-ts@npm:3.2.0" @@ -48102,6 +48209,15 @@ __metadata: languageName: node linkType: hard +"p-retry@npm:^8.0.0": + version: 8.0.0 + resolution: "p-retry@npm:8.0.0" + dependencies: + is-network-error: "npm:^1.3.0" + checksum: 10c0/81a788f35888c3cdb36f97286577c8ba57ccd8e9347db3c6ded79b3e12e2838bcda733753e0c0bdaac77d4620ddfc7e230d3c9a4bcf2fda3a12ea57bcf9443d1 + languageName: node + linkType: hard + "p-some@npm:^6.0.0": version: 6.0.0 resolution: "p-some@npm:6.0.0" @@ -56159,6 +56275,13 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.5": + version: 0.2.7 + resolution: "tmp@npm:0.2.7" + checksum: 10c0/59eb55584f2f07210d3231b6a1f6b5c2b9794d8a7b509c8ee867ed2acad6d2245ee2448b7937b676ffbff3155a70077edde8a69f9d7cf0f90c86a62e8910c357 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -57361,6 +57484,7 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-ui@workspace:packages/twenty-ui" dependencies: + "@argos-ci/storybook": "npm:^6.0.6" "@babel/core": "npm:^7.14.5" "@babel/preset-env": "npm:^7.26.9" "@babel/preset-react": "npm:^7.26.3" @@ -60728,7 +60852,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2": +"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.7": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72