feat(replay-vision): API validation + lens_result row column #133883
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Storybook | |
| on: | |
| pull_request: | |
| merge_group: | |
| push: | |
| branches: | |
| - master | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Job to decide if we should run storybook ci | |
| # See https://github.com/dorny/paths-filter#conditional-execution for more details | |
| changes: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| name: Determine need to run storybook checks | |
| if: github.event_name != 'merge_group' | |
| # Set job outputs to values from filter step | |
| outputs: | |
| # With dorny's default predicate-quantifier (some), '!path' matches | |
| # every file NOT at that path — effectively matching ALL changes | |
| # including backend files. Instead we use two positive filters and | |
| # compare counts to exclude generated-only PRs. | |
| # | |
| # Additionally skip when a bot commits only frontend/snapshots.yml on | |
| # a PR — that file is updated automatically after human review and CI | |
| # approval, so re-running the full storybook suite is pure waste. | |
| frontend: >- | |
| ${{ | |
| github.event_name == 'push' | |
| || (steps.filter.outputs.frontend == 'true' | |
| && fromJSON(steps.filter.outputs.frontend_count || '0') | |
| > fromJSON(steps.filter.outputs.frontend_generated_count || '0') | |
| && !(github.event_name == 'pull_request' | |
| && (contains(github.actor, '[bot]') || github.actor == 'posthog-bot') | |
| && fromJSON(steps.filter.outputs.snapshots_count || '0') | |
| == fromJSON(steps.filter.outputs.frontend_count || '0'))) | |
| }} | |
| matrix: ${{ steps.matrix.outputs.matrix }} | |
| steps: | |
| - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| id: app-token | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| with: | |
| client-id: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_PRIVATE_KEY }} | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 | |
| id: filter | |
| if: github.event_name != 'push' # Run all tests on master push | |
| with: | |
| token: ${{ steps.app-token.outputs.token || github.token }} | |
| filters: | | |
| frontend: | |
| - 'frontend/**' | |
| - 'products/**/*.{ts,tsx}' | |
| - 'products/**/frontend/**' | |
| - 'common/{esbuilder,mosaic,storybook,tailwind}/**' | |
| - 'ee/frontend/**' | |
| - '.storybook/**' | |
| - 'package.json' | |
| - '.github/workflows/ci-storybook.yml' | |
| - 'playwright.config.ts' | |
| frontend_generated: | |
| - 'frontend/src/generated/**' | |
| - 'products/**/frontend/generated/**' | |
| snapshots: | |
| - 'frontend/snapshots.yml' | |
| # Build the visual-regression matrix. | |
| # On PRs, only run chromium shards — no story currently opts into webkit snapshots | |
| # (see common/storybook/.storybook/test-runner.ts), so webkit shards consume CI | |
| # minutes without producing snapshot diffs. WebKit render-error coverage is | |
| # preserved on master pushes. | |
| - name: Build matrix | |
| id: matrix | |
| run: | | |
| # Shard counts — change here to resize the matrix. | |
| CHROMIUM_SHARDS=16 | |
| WEBKIT_SHARDS=4 | |
| shards() { | |
| jq -cn --arg browser "$1" --argjson count "$2" \ | |
| '[range(1; $count + 1) | {browser: $browser, shard_count: $count, shard: .}]' | |
| } | |
| CHROMIUM=$(shards chromium "$CHROMIUM_SHARDS") | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| MATRIX=$(jq -cn --argjson c "$CHROMIUM" '{include: $c}') | |
| else | |
| WEBKIT=$(shards webkit "$WEBKIT_SHARDS") | |
| MATRIX=$(jq -cn --argjson c "$CHROMIUM" --argjson w "$WEBKIT" '{include: ($c + $w)}') | |
| fi | |
| echo "matrix=$MATRIX" >> $GITHUB_OUTPUT | |
| build-storybook: | |
| name: Build Storybook (depot-ubuntu-latest) | |
| runs-on: depot-ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: changes | |
| if: needs.changes.outputs.frontend == 'true' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 | |
| - name: Fix node-gyp permissions | |
| run: chmod +x ~/setup-pnpm/node_modules/.pnpm/pnpm@*/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| cache-dependency-path: | | |
| pnpm-lock.yaml | |
| .github/workflows/ci-storybook.yml | |
| - name: Install dependencies | |
| run: pnpm --filter=@posthog/storybook... install --frozen-lockfile | |
| - name: Restore webpack build cache | |
| uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 | |
| with: | |
| path: common/storybook/node_modules/.cache/ | |
| key: ${{ runner.os }}-webpack-storybook-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: ${{ runner.os }}-webpack-storybook- | |
| - name: Build Storybook | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=32768 | |
| run: | | |
| bin/turbo --filter=@posthog/storybook prepare | |
| pnpm --filter=@posthog/storybook build --test | |
| - name: Save webpack build cache | |
| if: github.ref == 'refs/heads/master' | |
| uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 | |
| with: | |
| path: common/storybook/node_modules/.cache/ | |
| key: ${{ runner.os }}-webpack-storybook-${{ hashFiles('pnpm-lock.yaml') }} | |
| - name: Upload Storybook build artifact | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: storybook-build | |
| path: common/storybook/dist | |
| retention-days: 1 | |
| # Consumed by the shadow-story-selection job below. Uploaded | |
| # separately so the 20-shard visual-regression matrix doesn't pay | |
| # to download it. | |
| - name: Upload module-graph artifact | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: storybook-module-graph | |
| path: common/storybook/dist/module-graph.json | |
| retention-days: 1 | |
| if-no-files-found: warn | |
| vr-setup: | |
| name: Create Visual Review run | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [changes, build-storybook] | |
| if: needs.changes.outputs.frontend == 'true' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push') | |
| outputs: | |
| run_id: ${{ steps.create.outputs.run_id }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} | |
| sparse-checkout: | | |
| .nvmrc | |
| products/visual_review/cli | |
| products/visual_review/frontend/generated/api.schemas.ts | |
| frontend/snapshots.yml | |
| sparse-checkout-cone-mode: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| - name: Install VR CLI | |
| run: cd products/visual_review/cli && npm ci && npm run build && npm link | |
| - name: Create VR run | |
| id: create | |
| env: | |
| VR_TOKEN: ${{ secrets.VR_API_TOKEN }} | |
| VR_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} | |
| VR_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} | |
| VR_PR: ${{ github.event.pull_request.number }} | |
| # PRs are gating ("review"); master pushes are tracking-only ("observe") since | |
| # there's no PR to approve and we don't want master runs to block or prompt for approval. | |
| VR_PURPOSE: ${{ github.event_name == 'push' && 'observe' || 'review' }} | |
| run: | | |
| RUN_ID=$(vr run create \ | |
| --type storybook \ | |
| --baseline frontend/snapshots.yml \ | |
| --branch "$VR_BRANCH" \ | |
| --commit "$VR_COMMIT" \ | |
| --pr "$VR_PR" \ | |
| --purpose "$VR_PURPOSE" \ | |
| --token "$VR_TOKEN") | |
| echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT | |
| visual-regression: | |
| name: Visual regression tests - ${{ matrix.browser }} (${{ matrix.shard }}/${{ matrix.shard_count }}) | |
| runs-on: ubuntu-latest | |
| needs: [changes, build-storybook, vr-setup] | |
| if: >- | |
| always() | |
| && needs.changes.outputs.frontend == 'true' | |
| && needs.build-storybook.result == 'success' | |
| timeout-minutes: 60 | |
| container: | |
| image: ghcr.io/posthog/playwright:v1.45.0 | |
| strategy: | |
| fail-fast: false | |
| # Shard counts and worker config (maxWorkers in common/storybook/package.json) | |
| # are tuned for runner CPU — consult #team-devex before changing. | |
| # Matrix is built dynamically in the `changes` job: chromium-only on PRs, | |
| # chromium + webkit on master pushes. Storybook CI is skipped entirely on | |
| # merge_group (the `changes` job gates on event_name != 'merge_group'). | |
| matrix: ${{ fromJson(needs.changes.outputs.matrix) }} | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=16384 | |
| OPT_OUT_CAPTURE: 1 | |
| JEST_JUNIT_SUITE_NAME: '{filepath}' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 | |
| - name: Fix node-gyp permissions | |
| run: chmod +x ~/setup-pnpm/node_modules/.pnpm/pnpm@*/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| cache-dependency-path: | | |
| pnpm-lock.yaml | |
| .github/workflows/ci-storybook.yml | |
| - name: Install package.json dependencies with pnpm | |
| run: pnpm --filter=@posthog/storybook... install --frozen-lockfile | |
| - name: Download Storybook build artifact | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| name: storybook-build | |
| path: common/storybook/dist | |
| - name: Serve Storybook in the background | |
| # http-server and wait-on are devDependencies of @posthog/storybook. | |
| # Run via `pnpm --filter` so pnpm resolves them from common/storybook/node_modules/.bin | |
| # (no global install) and cd's into the package — `dist` is relative to common/storybook. | |
| run: | | |
| retries=5 | |
| max_timeout=30 | |
| pnpm --filter=@posthog/storybook exec http-server dist --port 6006 --silent & | |
| server_pid=$! | |
| echo "Started http-server with PID: $server_pid" | |
| # Give the server a moment to start | |
| sleep 2 | |
| while [ $retries -gt 0 ]; do | |
| echo "Checking if Storybook is available (retries left: $retries, timeout: ${max_timeout}s)..." | |
| if pnpm --filter=@posthog/storybook exec wait-on http://127.0.0.1:6006 --timeout $max_timeout; then | |
| echo "✅ Storybook is available at http://127.0.0.1:6006" | |
| break | |
| fi | |
| retries=$((retries-1)) | |
| if [ $retries -gt 0 ]; then | |
| echo "⚠️ Failed to connect to Storybook, retrying... ($retries retries left)" | |
| # Check if server is still running | |
| if ! kill -0 $server_pid 2>/dev/null; then | |
| echo "❌ http-server process is no longer running, restarting it..." | |
| pnpm --filter=@posthog/storybook exec http-server dist --port 6006 --silent & | |
| server_pid=$! | |
| echo "Restarted http-server with PID: $server_pid" | |
| sleep 2 | |
| fi | |
| fi | |
| done | |
| if [ $retries -eq 0 ]; then | |
| echo "❌ Failed to serve Storybook after all retries" | |
| # Try to get some diagnostic information | |
| echo "Checking port 6006 status:" | |
| netstat -tuln | grep 6006 || echo "Port 6006 is not in use" | |
| echo "Checking http-server process:" | |
| ps aux | grep http-server || echo "No http-server process found" | |
| echo "Checking Storybook dist directory:" | |
| ls -la common/storybook/dist || echo "Storybook dist directory not found" | |
| exit 1 | |
| fi | |
| # Wipe committed PNGs so each shard only has freshly captured screenshots. | |
| # Prevents stale hashes from winning the race in VR's get_or_create. | |
| - name: Clean snapshot directory | |
| run: find frontend/__snapshots__ -name '*.png' -delete 2>/dev/null || true | |
| # Capture-only: VR is the gate for visual changes, jest just captures screenshots | |
| - name: Run @storybook/test-runner | |
| id: test-runner | |
| shell: bash | |
| env: | |
| HOME: /root | |
| STORYBOOK_SKIP_TAGS: 'test-skip,test-skip-${{ matrix.browser }}' | |
| run: | | |
| pnpm --filter=@posthog/storybook test:visual:ci:update --browsers ${{ matrix.browser }} --shard ${{ matrix.shard }}/${{ matrix.shard_count }} | |
| # Upload snapshots to Visual Review (runs in parallel across shards) | |
| - name: Install VR CLI | |
| if: always() && needs.vr-setup.outputs.run_id != '' | |
| run: cd products/visual_review/cli && npm ci && npm run build && npm link | |
| - name: Upload snapshots to Visual Review | |
| if: always() && needs.vr-setup.outputs.run_id != '' | |
| env: | |
| VR_TOKEN: ${{ secrets.VR_API_TOKEN }} | |
| run: | | |
| vr run upload \ | |
| --run-id "${{ needs.vr-setup.outputs.run_id }}" \ | |
| --dir frontend/__snapshots__/ \ | |
| --baseline frontend/snapshots.yml \ | |
| --token "$VR_TOKEN" | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: junit-results-storybook-${{ matrix.browser }}-${{ matrix.shard }} | |
| path: common/storybook/junit.xml | |
| if-no-files-found: ignore | |
| # Verify changed story files are stable by re-running them multiple times. | |
| # Catches flaky snapshots before they land on master. | |
| flake-verification: | |
| name: Storybook flake verification | |
| runs-on: ubuntu-latest | |
| needs: [changes, build-storybook] | |
| if: needs.changes.outputs.frontend == 'true' && github.event_name == 'pull_request' | |
| timeout-minutes: 30 | |
| container: | |
| image: ghcr.io/posthog/playwright:v1.45.0 | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=16384 | |
| OPT_OUT_CAPTURE: 1 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| repository: ${{ github.event.pull_request.head.repo.full_name }} | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 | |
| - name: Fix node-gyp permissions | |
| run: chmod +x ~/setup-pnpm/node_modules/.pnpm/pnpm@*/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| cache-dependency-path: | | |
| pnpm-lock.yaml | |
| .github/workflows/ci-storybook.yml | |
| - name: Install dependencies | |
| run: pnpm --filter=@posthog/storybook... install --frozen-lockfile | |
| - name: Download Storybook build artifact | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| name: storybook-build | |
| path: common/storybook/dist | |
| - name: Serve Storybook | |
| # Resolve http-server/wait-on from common/storybook's devDependencies (no global install). | |
| run: | | |
| pnpm --filter=@posthog/storybook exec http-server dist --port 6006 --silent & | |
| sleep 2 | |
| pnpm --filter=@posthog/storybook exec wait-on http://127.0.0.1:6006 --timeout 30 | |
| - name: Verify changed stories are stable | |
| id: flake-verify | |
| env: | |
| HOME: /root | |
| run: | | |
| git config --global --add safe.directory '*' | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| git fetch --no-tags --prune --depth=50 origin "$BASE_SHA" | |
| .github/scripts/verify-storybook-new-stories.sh "$BASE_SHA" 3 | |
| - name: Upload flake verification screenshots | |
| if: failure() && steps.flake-verify.outcome == 'failure' | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: flake-verification-diffs | |
| path: | | |
| frontend/__snapshots__/__diff_output__/ | |
| frontend/__snapshots__/__failures__/ | |
| if-no-files-found: ignore | |
| retention-days: 5 | |
| # Collate matrix + VR completion status for the required check | |
| visual_regression_tests: | |
| needs: [visual-regression, vr-setup, vr-complete] | |
| name: Visual regression tests pass | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: always() | |
| steps: | |
| - name: Check matrix outcome | |
| run: | | |
| if [[ "${{ needs.visual-regression.result }}" != "success" && "${{ needs.visual-regression.result }}" != "skipped" ]]; then | |
| echo "One or more jobs in the visual-regression test matrix failed." | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.vr-setup.result }}" == "failure" ]]; then | |
| echo "Visual Review setup failed — VR CLI or API error." | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.vr-complete.result }}" != "success" && "${{ needs.vr-complete.result }}" != "skipped" ]]; then | |
| echo "Visual Review did not complete successfully (result: ${{ needs.vr-complete.result }})." | |
| exit 1 | |
| fi | |
| echo "All jobs passed or were skipped successfully." | |
| vr-complete: | |
| name: Complete Visual Review run | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [visual-regression, vr-setup] | |
| if: needs.vr-setup.outputs.run_id != '' && needs.visual-regression.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} | |
| sparse-checkout: | | |
| .nvmrc | |
| products/visual_review/cli | |
| products/visual_review/frontend/generated/api.schemas.ts | |
| frontend/snapshots.yml | |
| sparse-checkout-cone-mode: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| - name: Install VR CLI | |
| run: cd products/visual_review/cli && npm ci && npm run build && npm link | |
| - name: Complete Visual Review run | |
| env: | |
| VR_TOKEN: ${{ secrets.VR_API_TOKEN }} | |
| run: | | |
| vr run complete \ | |
| --run-id "${{ needs.vr-setup.outputs.run_id }}" \ | |
| --baseline frontend/snapshots.yml \ | |
| --token "$VR_TOKEN" | |
| calculate-running-time: | |
| name: Calculate running time | |
| needs: [visual-regression, visual_regression_tests, changes] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: # Run on pull requests to PostHog/posthog + on PostHog/posthog outside of PRs - but never on forks or Dependabot (no secrets access) | |
| always() && github.actor != 'dependabot[bot]' && | |
| needs.changes.outputs.frontend == 'true' && ( | |
| (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'PostHog/posthog') || | |
| (github.event_name != 'pull_request' && github.repository == 'PostHog/posthog')) | |
| steps: | |
| - name: Get telemetry app token | |
| id: telemetry-app-token | |
| if: github.run_attempt == '1' | |
| uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| with: | |
| client-id: ${{ secrets.GH_APP_TELEMETRY_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_TELEMETRY_PRIVATE_KEY }} | |
| - name: Capture running time to PostHog | |
| if: github.run_attempt == '1' | |
| uses: PostHog/posthog-github-action@58dea254b598fb5d469c0699c98af8288a7f7650 # v1.2.0 | |
| with: | |
| posthog-token: ${{ secrets.POSTHOG_API_TOKEN }} | |
| event: 'posthog-ci-running-time' | |
| capture-run-duration: true | |
| capture-job-durations: true | |
| github-token: ${{ steps.telemetry-app-token.outputs.token }} | |
| status-job: 'Visual regression tests pass' | |
| - name: Capture running time to DevEx PostHog | |
| if: github.run_attempt == '1' | |
| continue-on-error: true | |
| uses: PostHog/posthog-github-action@58dea254b598fb5d469c0699c98af8288a7f7650 # v1.2.0 | |
| with: | |
| posthog-token: ${{ secrets.POSTHOG_DEVEX_PROJECT_API_TOKEN }} | |
| event: 'posthog-ci-running-time' | |
| capture-run-duration: true | |
| capture-job-durations: true | |
| github-token: ${{ steps.telemetry-app-token.outputs.token }} | |
| status-job: 'Visual regression tests pass' | |
| # Shadow measurement: reports which stories could have rendered differently | |
| # given the PR's changed files, using the webpack module graph emitted by | |
| # common/storybook/.storybook/main.ts's ModuleGraphPlugin. Does not gate | |
| # PRs and does not affect visual-regression selection. Data is used to | |
| # evaluate whether real test selection is worth pursuing. | |
| shadow-story-selection: | |
| name: Shadow story selection | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [changes, build-storybook] | |
| if: | | |
| always() && needs.changes.outputs.frontend == 'true' | |
| && github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 500 | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| repository: ${{ github.event.pull_request.head.repo.full_name }} | |
| - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| - name: Download module-graph artifact | |
| id: download | |
| continue-on-error: true | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| name: storybook-module-graph | |
| path: /tmp/storybook-graph | |
| - name: Compute affected stories | |
| id: selection | |
| continue-on-error: true | |
| env: | |
| BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: | | |
| # Fetch the *current* tip of the base branch, not pull_request.base.sha — | |
| # base.sha is captured at webhook time and goes stale if the branch later | |
| # merges a newer master, which makes `base.sha...HEAD` balloon to include | |
| # every merged-in master change. | |
| git fetch --no-tags origin "$BASE_REF" | |
| echo "### Changed files" >&2 | |
| git diff --name-only "origin/$BASE_REF"...HEAD >&2 | |
| RESULT=$(bin/find-affected-stories \ | |
| --graph /tmp/storybook-graph/module-graph.json \ | |
| --base-ref "origin/$BASE_REF") | |
| echo "$RESULT" > /tmp/selection-result.json | |
| # Stash compact form in step output (single line) for the summary step. | |
| COMPACT=$(node -e 'const d = JSON.parse(require("fs").readFileSync("/tmp/selection-result.json", "utf8")); delete d.affected_stories; delete d.unresolved_changed_files; process.stdout.write(JSON.stringify(d))') | |
| echo "result=$COMPACT" >> "$GITHUB_OUTPUT" | |
| - name: Write summary | |
| if: steps.selection.outcome == 'success' | |
| env: | |
| RESULT: ${{ steps.selection.outputs.result }} | |
| CURRENT_SHARDS: 20 | |
| SHARD_OVERHEAD_MINUTES: 3 | |
| run: | | |
| MODE=$(echo "$RESULT" | jq -r '.mode') | |
| TOTAL=$(echo "$RESULT" | jq -r '.total_story_count') | |
| if [ "$MODE" = "selective" ]; then | |
| COUNT=$(echo "$RESULT" | jq -r '.affected_story_count') | |
| SHARDS=$(echo "$RESULT" | jq -r '.suggested_shards') | |
| AFFECTED_DUR=$(echo "$RESULT" | jq -r '.affected_duration_seconds') | |
| TOTAL_DUR=$(echo "$RESULT" | jq -r '.total_duration_seconds') | |
| SAVED_STORIES=$((TOTAL - COUNT)) | |
| SAVED_SHARDS=$((CURRENT_SHARDS - SHARDS)) | |
| FULL_MINUTES=$(( (CURRENT_SHARDS * SHARD_OVERHEAD_MINUTES) + (TOTAL_DUR / 60) )) | |
| SELECTIVE_MINUTES=$(( (SHARDS * SHARD_OVERHEAD_MINUTES) + (AFFECTED_DUR / 60) )) | |
| SAVED_MINUTES=$((FULL_MINUTES - SELECTIVE_MINUTES)) | |
| cat >> "$GITHUB_STEP_SUMMARY" << EOF | |
| ## Shadow story selection | |
| | Metric | Full run | Selective | Saved | | |
| |---|---|---|---| | |
| | Story files | $TOTAL | $COUNT | **$SAVED_STORIES** | | |
| | Shards | $CURRENT_SHARDS | $SHARDS | **$SAVED_SHARDS** | | |
| | Est. CI minutes | ~${FULL_MINUTES} | ~${SELECTIVE_MINUTES} | **~${SAVED_MINUTES}** | | |
| | Est. render time | ${TOTAL_DUR}s | ${AFFECTED_DUR}s | **$((TOTAL_DUR - AFFECTED_DUR))s** | | |
| > Shadow-only — no effect on CI. Predictor: webpack module graph from build-storybook. | |
| EOF | |
| echo "SHADOW_METRICS: $(echo "$RESULT" | jq -c '{mode, affected: .affected_story_count, total: .total_story_count, shards: .suggested_shards, current_shards: '$CURRENT_SHARDS', affected_dur: .affected_duration_seconds, total_dur: .total_duration_seconds}')" | |
| else | |
| REASON=$(echo "$RESULT" | jq -r '.reason // "unknown"') | |
| cat >> "$GITHUB_STEP_SUMMARY" << EOF | |
| ## Shadow story selection | |
| **Full run required:** $REASON | |
| Total stories: $TOTAL | |
| > Shadow-only — no effect on CI. | |
| EOF | |
| echo "SHADOW_METRICS: $(echo "$RESULT" | jq -c '{mode, reason, total: .total_story_count}')" | |
| fi |