From d9eef5f3513161b88dd2fd0d51d60da46482fe73 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 24 Mar 2026 18:13:00 +0100 Subject: [PATCH] Fix visual regression dispatch for fork PRs (#18921) ## Summary - Visual regression dispatch was failing for external contributor PRs because fork PRs don't have access to repo secrets (`CI_PRIVILEGED_DISPATCH_TOKEN`) - Moved the dispatch from inline jobs in `ci-front.yaml` / `ci-ui.yaml` to a new `workflow_run`-triggered workflow - `workflow_run` runs in the base repo context and always has access to secrets, regardless of whether the PR is from a fork - Follows the same pattern already used by `post-ci-comments.yaml` for breaking changes dispatch - Handles the fork case where `workflow_run.pull_requests` is empty by falling back to a head label search ## Test plan - [ ] Verify CI Front and CI UI workflows still pass without the removed jobs - [ ] Verify the new `visual-regression-dispatch.yaml` triggers after CI Front / CI UI complete - [ ] Test with a fork PR to confirm the dispatch succeeds --- .github/workflows/ci-front.yaml | 34 ----- .github/workflows/ci-ui.yaml | 34 ----- .../workflows/visual-regression-dispatch.yaml | 144 ++++++++++++++++++ 3 files changed, 144 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/visual-regression-dispatch.yaml diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 34fe656649a..3525801ac6c 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -151,40 +151,6 @@ 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 }} - visual-regression-dispatch: - needs: front-sb-build - if: github.event_name == 'pull_request' - timeout-minutes: 5 - runs-on: ubuntu-latest - steps: - - name: Download storybook build - uses: actions/download-artifact@v4 - with: - name: storybook-static - path: storybook-static - - name: Package storybook - run: tar -czf /tmp/storybook-twenty-front.tar.gz -C storybook-static . - - name: Upload storybook tarball - uses: actions/upload-artifact@v4 - with: - name: storybook-twenty-front-tarball - path: /tmp/storybook-twenty-front.tar.gz - retention-days: 1 - - name: Dispatch to ci-privileged - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} - repository: twentyhq/ci-privileged - event-type: visual-regression - client-payload: >- - { - "pr_number": "${{ github.event.pull_request.number }}", - "run_id": "${{ github.run_id }}", - "repo": "${{ github.repository }}", - "project": "twenty-front", - "branch": "${{ github.head_ref }}", - "commit": "${{ github.event.pull_request.head.sha }}" - } front-task: needs: changed-files-check if: needs.changed-files-check.outputs.any_changed == 'true' diff --git a/.github/workflows/ci-ui.yaml b/.github/workflows/ci-ui.yaml index 4dbc02c5a4c..1d0ca60bfb9 100644 --- a/.github/workflows/ci-ui.yaml +++ b/.github/workflows/ci-ui.yaml @@ -95,40 +95,6 @@ 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 - visual-regression-dispatch: - needs: ui-sb-build - if: github.event_name == 'pull_request' - timeout-minutes: 5 - runs-on: ubuntu-latest - steps: - - name: Download storybook build - uses: actions/download-artifact@v4 - with: - name: storybook-twenty-ui - path: storybook-static - - name: Package storybook - run: tar -czf /tmp/storybook-twenty-ui.tar.gz -C storybook-static . - - name: Upload storybook tarball - uses: actions/upload-artifact@v4 - with: - name: storybook-twenty-ui-tarball - path: /tmp/storybook-twenty-ui.tar.gz - retention-days: 1 - - name: Dispatch to ci-privileged - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} - repository: twentyhq/ci-privileged - event-type: visual-regression - client-payload: >- - { - "pr_number": "${{ github.event.pull_request.number }}", - "run_id": "${{ github.run_id }}", - "repo": "${{ github.repository }}", - "project": "twenty-ui", - "branch": "${{ github.head_ref }}", - "commit": "${{ github.event.pull_request.head.sha }}" - } ci-ui-status-check: if: always() && !cancelled() timeout-minutes: 5 diff --git a/.github/workflows/visual-regression-dispatch.yaml b/.github/workflows/visual-regression-dispatch.yaml new file mode 100644 index 00000000000..526a9d8d2fe --- /dev/null +++ b/.github/workflows/visual-regression-dispatch.yaml @@ -0,0 +1,144 @@ +name: Visual Regression Dispatch + +# Uses workflow_run to dispatch visual regression to ci-privileged. +# This runs in the context of the base repo (not the fork), so it has +# access to secrets — making it work for external contributor PRs. + +on: + workflow_run: + workflows: ['CI Front', 'CI UI'] + types: [completed] + +permissions: + actions: read + pull-requests: read + +jobs: + dispatch: + if: >- + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Determine project and artifact name + id: project + uses: actions/github-script@v7 + with: + script: | + const workflowName = context.payload.workflow_run.name; + if (workflowName === 'CI Front') { + core.setOutput('project', 'twenty-front'); + core.setOutput('artifact_name', 'storybook-static'); + core.setOutput('tarball_name', 'storybook-twenty-front-tarball'); + core.setOutput('tarball_file', 'storybook-twenty-front.tar.gz'); + } 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'); + } else { + core.setFailed(`Unexpected workflow: ${workflowName}`); + } + + - name: Check if storybook artifact exists + id: check-artifact + uses: actions/github-script@v7 + with: + script: | + const artifactName = '${{ steps.project.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} — storybook build was likely skipped`); + } + + - name: Get PR number + if: steps.check-artifact.outputs.exists == 'true' + id: pr-info + uses: actions/github-script@v7 + with: + script: | + const headBranch = context.payload.workflow_run.head_branch; + const headRepo = context.payload.workflow_run.head_repository; + + // workflow_run.pull_requests is empty for fork PRs, + // so fall back to searching by head label (owner:branch) + 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(`pull_requests is empty (likely a fork PR), searching 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 for this workflow run — skipping'); + core.setOutput('has_pr', 'false'); + return; + } + + core.setOutput('pr_number', prNumber); + core.setOutput('has_pr', 'true'); + core.info(`PR #${prNumber}`); + + - name: Download storybook artifact from triggering run + if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' + uses: actions/download-artifact@v4 + with: + name: ${{ steps.project.outputs.artifact_name }} + path: storybook-static + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Package storybook + 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 . + + - name: Upload storybook tarball + if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.project.outputs.tarball_name }} + path: /tmp/${{ steps.project.outputs.tarball_file }} + retention-days: 1 + + - name: Dispatch to ci-privileged + if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true' + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }} + repository: twentyhq/ci-privileged + event-type: visual-regression + client-payload: >- + { + "pr_number": "${{ steps.pr-info.outputs.pr_number }}", + "run_id": "${{ github.run_id }}", + "repo": "${{ github.repository }}", + "project": "${{ steps.project.outputs.project }}", + "branch": "${{ github.event.workflow_run.head_branch }}", + "commit": "${{ github.event.workflow_run.head_sha }}" + }