Files
twenty/.github/workflows/visual-regression-dispatch.yaml
Charles Bochet b0d29689d2 feat: add Argos visual regression for twenty-new-ui (#21262)
## 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
2026-06-08 13:04:52 +02:00

286 lines
11 KiB
YAML

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', 'CI New UI']
types: [completed]
permissions:
actions: read
contents: read
pull-requests: read
jobs:
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 = '${{ steps.context.outputs.artifact_name }}';
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 === artifactName);
core.setOutput('exists', found ? 'true' : 'false');
if (!found) {
core.info(`Artifact "${artifactName}" not found in run ${runId} — skipping`);
}
- name: Get PR number
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:
script: |
const headBranch = context.payload.workflow_run.head_branch;
const headRepo = context.payload.workflow_run.head_repository;
let pullRequests = context.payload.workflow_run.pull_requests;
let prNumber;
if (pullRequests && pullRequests.length > 0) {
prNumber = pullRequests[0].number;
} else {
const headLabel = `${headRepo.owner.login}:${headBranch}`;
core.info(`Searching for PR by head label: ${headLabel}`);
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: headLabel,
per_page: 1,
});
if (prs.length > 0) {
prNumber = prs[0].number;
}
}
if (!prNumber) {
core.info('No pull request found — skipping');
core.setOutput('has_pr', 'false');
return;
}
core.setOutput('pr_number', prNumber);
core.setOutput('has_pr', 'true');
core.info(`PR #${prNumber}`);
- name: Compute merge-base for Argos reference
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
id: merge-base
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const headSha = context.payload.workflow_run.head_sha;
try {
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `main...${headSha}`,
});
if (comparison.merge_base_commit?.sha) {
core.setOutput('sha', comparison.merge_base_commit.sha);
core.info(`Merge base: ${comparison.merge_base_commit.sha}`);
} else {
core.info('Could not determine merge base — will skip reference_commit');
core.setOutput('sha', '');
}
} catch (error) {
core.warning(`Failed to compute merge base: ${error instanceof Error ? error.message : String(error)}`);
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
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
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: ${{ 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"
-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[@]}"