Skip to content

[codex] Add ShakaPerf RSC FOUC release gate#3617

Merged
justin808 merged 17 commits into
mainfrom
jg-codex/shakaperf-release-gate
Jun 5, 2026
Merged

[codex] Add ShakaPerf RSC FOUC release gate#3617
justin808 merged 17 commits into
mainfrom
jg-codex/shakaperf-release-gate

Conversation

@justin808

@justin808 justin808 commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

Adds a ShakaPerf-backed release gate for the React Server Components FOUC regression case. The gate boots the Pro dummy app in GitHub Actions, runs the node renderer and Rails server, then checks the RSC page with ShakaPerf visual-regression tests.

The ShakaPerf test asserts two things that are intended to catch the flash-of-unstyled-content class of regression directly:

  • the RSC stylesheet link is present before hydration when client JS is blocked
  • the first visible RSC CSS probe is already styled, including the expected computed background color

The workflow uploads ShakaPerf reports and server logs as artifacts for release-gate debugging.

Validation

  • pnpm install --frozen-lockfile
  • pnpm exec shaka-perf --version
  • pnpm exec prettier --check package.json pnpm-lock.yaml .github/workflows/shakaperf-release-gates.yml test/shakaperf/rsc-fouc/README.md test/shakaperf/rsc-fouc/abtests.config.ts test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts
  • actionlint .github/workflows/shakaperf-release-gates.yml
  • ShakaPerf positive fixture run: both FOUC checks passed and generated report artifacts
  • ShakaPerf negative fixture run: intentionally delayed the RSC stylesheet; ShakaPerf failed on missing first-paint stylesheet and unstyled first-visible probe
  • Autoreview: clean after fixes
  • Pre-commit hooks: markdown links, trailing newlines, Prettier, ESLint
  • Pre-push hooks: branch lint, markdown links

Notes

I verified the ShakaPerf config and failure mode locally with fixtures. The full Pro dummy app release workflow still needs to run in GitHub Actions with REACT_ON_RAILS_PRO_LICENSE available.


Note

Medium Risk
The release path can block tagging and npm/gem publish if the new gate fails; the workflow depends on Pro license secrets and a full dummy-app stack, but it does not change runtime library code shipped to apps.

Overview
Adds a manual ShakaPerf release gate for React Server Components flash-of-unstyled-content on the Pro dummy route /rsc_posts_page_over_http. A new workflow boots the Pro dummy app (node renderer + Rails), runs shaka-perf compare with absolute visreg assertions (RSC stylesheet before hydration with JS blocked, first-visible probe styled), and uploads reports and server logs.

Tooling: pins shaka-perf / shaka-shared at the repo root (pnpm onlyBuiltDependencies), ignores compare-results/, and extends Knip so the workflow and test/shakaperf/** count as used. The release rake path dispatches this workflow after the version-bump push and waits for success before tagging and publishing (overridable via RELEASE_CI_STATUS_OVERRIDE).

Reviewed by Cursor Bugbot for commit 2465a15. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Tests

    • Added automated visual/regression release-gate tests and config to detect CSS FOUC in React Server Components, including two experiment checks.
  • Chores

    • Added a manual/tag-triggered release-gate workflow that runs comparisons, collects reports/logs, and uploads artifacts.
    • Integrated the gate into the release flow and updated tooling and dev-dependency allowlists; ignored compare-results output.
  • Documentation

    • Added README documenting the new ShakaPerf gate and local run/reporting instructions.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a ShakaPerf “rsc-fouc” release gate: GitHub Actions workflow to build and run the Pro dummy app, start Rails and Node renderer, run ShakaPerf compare with abtests, upload reports/logs, integrate the gate into rake release, and add tests, workspace and dependency updates to support the gate.

Changes

ShakaPerf RSC FOUC Release Gate Implementation

Layer / File(s) Summary
GitHub Actions workflow setup and orchestration
.github/workflows/shakaperf-release-gates.yml
Defines workflow triggers, minimal permissions, pinned Node/Ruby env, checkout/setup steps, Pro license validation, dependency installs, Pro gem/package builds, and dummy app preparation.
Server startup, readiness check, and test execution
.github/workflows/shakaperf-release-gates.yml
Starts node-renderer and Rails server in background, captures PIDs/logs, polls control endpoints (/empty, /rsc_posts_page_over_http) for readiness with early failure on process death and log tails on timeout, then runs pnpm exec shaka-perf compare for rsc-fouc.
Artifact collection and process cleanup
.github/workflows/shakaperf-release-gates.yml
Uploads comparison report and server logs as artifacts (30-day retention, if: always()), and stops background Rails/node-renderer processes via pidfiles on completion.
ShakaPerf abtests configuration
test/shakaperf/rsc-fouc/abtests.config.ts
Exports ShakaPerf config with env-backed control/experiment URLs, desktop viewport(s), visreg pixel-diff thresholds/retries, Chromium options, and perf/audit measurement parameters.
FOUC detection test cases and README
test/shakaperf/rsc-fouc/README.md, test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts
Adds two visreg abtests: one that blocks client JS and asserts server-attached stylesheet and probe background color; another that waits for first-visibility and inspects computed styles. README documents target route, checks, and local run instructions; reports written to compare-results/.
rake release integration and helpers
rakelib/release.rake, react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb
Adds ShakaPerf release-gate constants and helper methods to dispatch and poll GitHub workflow runs tied to current HEAD, integrates the gate into rake release flow (with override/dry-run behavior) and adjusts release push/tag ordering; includes RSpec coverage for new helpers.
Dependencies, workspace configuration, and .gitignore
package.json, knip.ts, .gitignore
Adds shaka-perf and shaka-shared devDependencies (0.1.3), updates pnpm.onlyBuiltDependencies, expands Knip workspace entries/projects and ignore rules (new create-react-on-rails-app workspace and added entry files), and ignores /compare-results/.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • AbanoubGhadban

Suggested labels

documentation, codex

Poem

🐰 A tiny rabbit hops through CI,
Spinning gates where styles must be,
Servers wake and probes take flight,
Pixels checked by morning light,
Reports tucked safe — the garden's right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title '[codex] Add ShakaPerf RSC FOUC release gate' clearly and concisely summarizes the main change: introducing a ShakaPerf-based release gate for detecting RSC FOUC regressions. It is directly related to the changeset's primary objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg-codex/shakaperf-release-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Code Review: ShakaPerf RSC FOUC Release Gate

Overview

This PR adds a tag-triggered GitHub Actions release gate that runs ShakaPerf visual-regression tests against the Pro dummy app to catch RSC "use client" CSS FOUC regressions. Changes are CI/test-only with no production runtime impact.


Workflow: .github/workflows/shakaperf-release-gates.yml

Positives:

  • Minimal contents: read permissions and persist-credentials: false — good security hygiene
  • if: always() on cleanup and artifact upload steps — correct
  • Log tailing on timeout is helpful for debugging
  • 45-minute timeout is appropriate for a full app boot + visual regression run

Issues:

1. Server health check does not detect process death. The loop polls for 120 seconds (60 x 2s) but never checks whether the background Rails/node-renderer processes are still alive. If either crashes immediately, the loop spins uselessly until timeout. A kill -0 liveness check inside the loop would catch this early and surface a clear error.

2. Secret unavailable on forks. REACT_ON_RAILS_PRO_LICENSE_V2 is referenced in two steps. If it is empty, the dummy app boots without a valid license and produces a confusing failure. A pre-flight step that fails fast with a clear ::error:: message would help.

3. SHAKAPERF_TARGET_URL vs. SHAKAPERF_CONTROL_URL/SHAKAPERF_EXPERIMENT_URL. abtests.config.ts reads SHAKAPERF_CONTROL_URL and SHAKAPERF_EXPERIMENT_URL but those env vars are not set in the workflow. The --controlURL/--experimentURL CLI flags override correctly, but SHAKAPERF_TARGET_URL is only consumed by the health-check curl. A comment or rename would reduce confusion for future maintainers.


Test: rsc-fouc-release-gate.abtest.ts

Positives:

  • Blocking app JS via installRequestBlocking to isolate server-rendered styles is the right approach for FOUC testing
  • RAF double-frame wait before capture is a sound pattern for paint timing
  • Two-gate structure (stylesheet present before hydration, then first-visible probe styled) catches the regression at two distinct layers

Issues:

4. Hard-coded EXPECTED_BACKGROUND (rgb(212, 250, 236)). If the Pro dummy app CSS changes the probe color, this test breaks silently at the assertion level rather than at the CSS level. At minimum, add a comment pointing to the exact CSS rule this value tracks so future maintainers know where to update it.

5. isControl used only in the error message string in the second abTest. The assertion is symmetric (intentional per the README), but destructuring isControl without using it in logic looks like a latent bug to a reader. Either remove it from the destructure or add a brief comment explaining the symmetric design.

6. readyTimeout: 5_000 on the second test may be tight on a cold CI runner. Bumping to 10,000 ms (matching the first test) would reduce flakiness risk with no meaningful cost.


Config: abtests.config.ts

7. compareRetries: 0 — any single-pixel rendering blip fails the gate with no retry. CI visual tests are susceptible to font-rendering and GPU compositing variance. compareRetries: 1 with a short delay (e.g. 500 ms) would absorb transient flakiness without masking real regressions.

8. Dead perf and audit config blocks. The gate only runs --categories visreg. A brief comment clarifying these blocks are kept for future use would prevent confusion.


package.json

  • Pinning shaka-perf to an exact version (0.1.3) is appropriate for a testing tool where reproducibility matters.
  • The onlyBuiltDependencies comment block is thorough and matches the project style.

.gitignore

  • compare-results/ correctly matches the ShakaPerf output directory. No issues.

Summary

The design is sound and the failure modes have clearly been tested (positive and negative fixture runs per the PR description). The two highest-priority items before this gate blocks a real release are process-death detection during the server health check (issue 1) and the brittle hard-coded background color (issue 4). The secret pre-flight check (issue 2) and retry configuration (issue 7) are valuable hardening steps as well.

Comment thread .github/workflows/shakaperf-release-gates.yml
Comment thread test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts
Comment thread test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts
Comment thread test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts Outdated
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 60.77 KB (0%)
react-on-rails/client bundled (gzip) (time) 60.77 KB (0%)
react-on-rails/client bundled (brotli) 52.1 KB (0%)
react-on-rails/client bundled (brotli) (time) 52.1 KB (0%)
react-on-rails-pro/client bundled (gzip) 62.04 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 62.04 KB (0%)
react-on-rails-pro/client bundled (brotli) 53.3 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 53.3 KB (0%)
registerServerComponent/client bundled (gzip) 71.33 KB (0%)
registerServerComponent/client bundled (gzip) (time) 71.33 KB (0%)
registerServerComponent/client bundled (brotli) 61.33 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.33 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 65.46 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 65.46 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.19 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.19 KB (0%)

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Code Review: Add ShakaPerf RSC FOUC Release Gate

Overview

This PR adds a ShakaPerf-backed visual regression release gate that guards against Flash of Unstyled Content (FOUC) regressions in the React Server Components CSS path. The gate runs on version tags and workflow_dispatch, boots the Pro dummy app, and asserts:

  1. The RSC stylesheet link is present before JS hydration (via Playwright request-blocking).
  2. The first visible RSC CSS probe is already styled to the expected background color.

Strengths

  • Minimal permissions (contents: read) and persist-credentials: false — good CI security hygiene.
  • Cleanup step runs if: always() — background processes are reliably killed even on failure.
  • Double-RAF pattern before screenshot capture is a well-known paint-settling technique.
  • Explicit FOUC detection logic — the JS-blocking test is a clever and direct regression signal.
  • Clear README with local repro command.

Issues & Suggestions

Potential flakiness: 1-second stylesheet timeout
In rsc-fouc-release-gate.abtest.ts the waitForSelector(RSC_STYLESHEET_SELECTOR, { timeout: 1_000 }) fires right after the CSS probe becomes visible. With a cold Rails + node-renderer start in CI, any minor HTML buffering delay will turn this into a spurious failure. Raising this to 5_000 would match the surrounding waits and reduce noise.

Hardcoded EXPECTED_BACKGROUND = 'rgb(212, 250, 236)'
This computed-color assertion breaks silently whenever the dummy app's CSS changes, with no obvious connection from the failure message to the root cause. A comment pointing to the CSS declaration in the dummy app would help future debuggers.

compareRetries: 0 in abtests.config.ts
Zero retries means any transient CI infrastructure noise (network hiccup, renderer slowness) turns into a hard gate failure requiring a manual re-run. compareRetries: 1 with a short compareRetryDelay would absorb most transient flakes without masking real FOUC regressions.

waitForLoadState('networkidle') flakiness risk
After waitForLoadState('load'), the subsequent waitForLoadState('networkidle') in the first test can stall indefinitely if the page has long-polling connections, analytics beacons, or slow CDN assets. Since the screenshot already happens post-RAF settle, this networkidle wait may not add useful signal and could cause spurious timeouts.

perf block in abtests.config.ts is unused
The workflow only runs --categories visreg. The perf and audit config blocks are never exercised. Not harmful, but could confuse future editors into thinking perf tests are also expected to run.

Unrelated Knip changes
knip.ts adds entries for create-react-on-rails-app, src/capabilities/ssr.rsc.ts!, src/cache/index.stub.ts!, and src/registerDefaultRSCProvider.client.tsx! — none of these are related to the ShakaPerf gate. Happy to see pre-existing Knip false-positives fixed here, but confirming the intent in the PR description would be helpful for reviewers.

No secret-availability guard
If REACT_ON_RAILS_PRO_LICENSE_V2 is not configured (e.g. workflow_dispatch from a fork), the Rails server will fail to start with a cryptic license error. The 2-minute health-check loop will then time out before the license error is visible in the log. An early [[ -z "${REACT_ON_RAILS_PRO_LICENSE}" ]] && echo "::error::License secret missing" && exit 1 guard would surface this immediately.


Overall: The design is sound and the test logic directly catches the FOUC regression it's meant to protect. The main actionable concerns are the tight 1-second stylesheet timeout and zero retries, both of which increase the risk of spurious CI failures on an otherwise correct build.

Comment thread test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts Outdated
Comment thread test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts
Comment thread test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts
Comment thread test/shakaperf/rsc-fouc/abtests.config.ts
Comment thread .github/workflows/shakaperf-release-gates.yml
Comment thread test/shakaperf/rsc-fouc/abtests.config.ts Outdated
@justin808 justin808 marked this pull request as ready for review June 5, 2026 00:10

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

Reviewed by Cursor Bugbot for commit 323c586. Configure here.

Comment thread .github/workflows/shakaperf-release-gates.yml

Copy link
Copy Markdown
Member Author

Handled the review feedback on the ShakaPerf release gate.

Mattered:

  • Added early failure when REACT_ON_RAILS_PRO_LICENSE_V2 is unavailable.
  • Added Rails/node-renderer process liveness checks during the health loop.
  • Switched the workflow to explicit SHAKAPERF_CONTROL_URL and SHAKAPERF_EXPERIMENT_URL values and documented the same-candidate comparison.
  • Reduced CI flake risk by removing networkidle, raising the stylesheet/ready timeouts, and adding one ShakaPerf compare retry.

Optional/polish:

  • Added the actual Pro dummy CSS source path for the expected background color.
  • Clarified why isControl is only used for the failure label.
  • Clarified why perf/audit config blocks remain even though this gate runs --categories visreg.

Validation after fixes:

  • actionlint .github/workflows/shakaperf-release-gates.yml
  • pnpm exec prettier --check .github/workflows/shakaperf-release-gates.yml test/shakaperf/rsc-fouc/abtests.config.ts test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts
  • pnpm exec eslint test/shakaperf/rsc-fouc/abtests.config.ts test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts
  • pnpm exec knip --exclude binaries
  • pnpm exec knip --production --exclude binaries
  • ShakaPerf positive fixture run passed both FOUC checks with 0 diff pixels.
  • Autoreview clean: no accepted/actionable findings.

Future full-PR review scans can start after this comment unless check all reviews is requested.

@coderabbitai coderabbitai Bot added review-needed benchmark P2 Backlog priority codex-wip Codex is actively working this PR labels Jun 5, 2026
@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a ShakaPerf-backed release gate that runs on version tags and manual dispatch. It boots the Pro dummy app (node renderer + Rails), then runs two visual-regression abtests that assert the RSC stylesheet is server-rendered and the CSS probe element is already styled before client JS hydrates — directly catching FOUC regressions.

  • New workflow (.github/workflows/shakaperf-release-gates.yml): installs dependencies, builds Pro packages, starts the app with a health-check loop, runs shaka-perf compare, and uploads reports and server logs as artifacts.
  • New ShakaPerf test suite (test/shakaperf/rsc-fouc/): two abtests pointing both control and experiment at the same candidate URL so absolute style assertions still fail on a broken build.
  • Root tooling updates: shaka-perf/shaka-shared 0.1.3 added as devDependencies, compare-results/ ignored in git, and Knip extended to cover the new workflow and test entries.

Confidence Score: 4/5

CI-only change with no impact on shipped packages; safe to merge with a minor ordering tweak recommended.

The workflow is well-structured with proper cleanup, artifact uploads, and fail-fast error messages. The test logic correctly blocks JS to catch pre-hydration FOUC by checking both stylesheet presence and computed CSS. The only notable issue is that the license secret check is placed after several minutes of installation work, so a missing secret wastes CI time before failing — but it does fail correctly and never reaches the app execution steps.

.github/workflows/shakaperf-release-gates.yml — specifically the ordering of the license check relative to the install and build steps.

Important Files Changed

Filename Overview
.github/workflows/shakaperf-release-gates.yml New CI workflow that boots the Pro dummy app and runs ShakaPerf visual-regression checks against the RSC page; license check is placed after expensive install/build steps, wasting time when the secret is missing
test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts Two abtest definitions: one blocks JS and asserts the RSC stylesheet link + computed background are present pre-hydration; the other checks the first-visible probe is styled — logic is sound
test/shakaperf/rsc-fouc/abtests.config.ts ShakaPerf config pointing both control and experiment at the same URL; includes required perf/audit stubs so the config validates cleanly even though only visreg runs
knip.ts Extends root workspace entries to cover the new workflow and test/shakaperf files; adds create-react-on-rails-app workspace and additional entry points in react-on-rails and react-on-rails-pro
package.json Adds shaka-perf and shaka-shared 0.1.3 as devDependencies and lists shaka-perf in onlyBuiltDependencies
.gitignore Adds /compare-results/ to ignore ShakaPerf output artifacts from being committed

Sequence Diagram

sequenceDiagram
    participant GH as GitHub Actions
    participant Build as Build Steps
    participant Rails as Rails Server (3030)
    participant Node as Node Renderer (3800)
    participant SP as shaka-perf compare

    GH->>Build: pnpm install + bundle install
    GH->>Build: Build JS packages
    GH->>Build: Check Pro license secret
    GH->>Build: db:prepare + packs + playwright install
    GH->>Node: Start node-renderer (background)
    GH->>Rails: Start rails server (background)
    loop Health check (up to 60x2s)
        GH->>Rails: curl /rsc_posts_page_over_http
        Rails-->>GH: 200 OK - proceed
    end
    GH->>SP: shaka-perf compare --categories visreg
    SP->>Rails: Load /rsc_posts_page_over_http (JS blocked)
    SP->>SP: Assert RSC stylesheet link present (pre-hydration)
    SP->>SP: "Assert probe background-color = rgb(212,250,236)"
    SP->>Rails: Load /rsc_posts_page_over_http (normal)
    SP->>SP: Assert first-visible probe is styled
    SP-->>GH: Pass / Fail
    GH->>GH: Upload report + server logs as artifacts
Loading

Reviews (1): Last reviewed commit: "Bound ShakaPerf health check curl" | Re-trigger Greptile

Comment thread .github/workflows/shakaperf-release-gates.yml Outdated
@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

placeholder review — replacing shortly

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 323c586037

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/shakaperf-release-gates.yml Outdated
@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Overview

Adds a ShakaPerf visual-regression release gate to catch FOUC regressions in the RSC use-client CSS path. Runs on version tags and manual dispatch; boots the Pro dummy app and asserts the RSC stylesheet is present before hydration and the first visible probe is styled. Solid approach overall.


Strengths

  • persist-credentials: false on checkout; license secret handled correctly; PIDs written to RUNNER_TEMP
  • Artifacts upload on success and failure via if: always()
  • Cleanup step also uses if: always()
  • waitUntil: commit + JS blocking is the correct signal for this regression class
  • README clearly explains the same-URL design intent

Concerns

1. Lock file bloat from shaka-perf transitive deps (~2,000 lines added)

shaka-perf 0.1.3 brings in esbuild@0.28.0, sharp (20+ platform binaries), @sentry/node@9, ~15 new @opentelemetry instrumentation packages, @puppeteer/browsers, and more. Peer resolution also changed fingerprints for shakapacker, terser-webpack-plugin, webpack, and tailwindcss. Not a correctness issue, but every CI job now downloads substantially more. If shaka-perf is only used by this workflow, a dedicated sub-workspace (e.g. test/shakaperf/) would limit blast radius on the main lockfile.

2. Hardcoded CSS color is fragile

EXPECTED_BACKGROUND = 'rgb(212, 250, 236)' is tightly coupled to the .probe rule in UseClientCssProbe.module.scss. A cosmetic color change in the dummy app will silently break the release gate with no obvious connection to FOUC. Consider asserting any non-default color, or sourcing the value from a shared constants file, to make the coupling explicit.

3. Knip changes bundle unrelated scope

The knip.ts diff adds a workspace for packages/create-react-on-rails-app and two Pro entry points (ssr.rsc.ts, cache/index.stub.ts). These look like pre-existing suppressions. Worth splitting out or confirming they were strictly required to unblock Knip with the new test/shakaperf entries.

4. Readiness probe uses the RSC page itself

Curling /rsc_posts_page_over_http for readiness means a slow cold-start can produce a false-positive ready signal. A lighter health endpoint (/up or a static asset) for the probe -- with ShakaPerf's own readyTimeout handling the actual page -- would be more robust and less tied to the route name.

5. compareRetries: 1 is low for very tight thresholds

misMatchThreshold: 0.001 and maxNumDiffPixels: 10 are extremely tight. On a shared CI runner, one retry at 500 ms may not absorb subpixel anti-aliasing jitter. Bumping to compareRetries: 2 or 3 is a low-risk fix if the gate shows intermittent failures.


Minor nits: SECRET_KEY_BASE is hardcoded (fine for RAILS_ENV: test, but a comment noting it is intentionally dummy would preempt security reports); timeout-minutes: 45 is generous; tag pattern '-rc' matches any tag containing -rc anywhere (e.g. hotfix-rc-patch).


Summary: Core approach is solid. Main items before merging: (1) fragile hardcoded color assertion, (2) confirming lock file impact at root workspace scope is acceptable, (3) whether the unrelated Knip additions should be split out.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4da642d837

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rakelib/release.rake Outdated
@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: ShakaPerf RSC FOUC Release Gate

Overview

This PR adds a ShakaPerf-backed visual-regression release gate that detects CSS flash-of-unstyled-content in the RSC integration path. The gate is wired into the release Rake task between the version-bump push and tagging/publishing. The overall approach is sound and the test logic is well thought-out.


Correctness & Reliability

Push-before-gate leaves a dangling version-bump commit on failure

The Rake task now pushes the version-bump commit (git push) before the gate runs. If the gate fails or times out, the remote branch has the bumped version with no corresponding tag or published packages. The handle_shakaperf_release_gate_violation! error message explains recovery, which is good — but this is a non-trivial operator burden. Consider whether the recovery steps should also appear in the release runbook or a .claude/docs/ file so future operators have a clear reference without having to read the rake source.

Thread not joined when terminate_process raises in capture_gh_output_with_timeout

output_reader.value is only reached on the happy path. If terminate_process (or any code in the deadline branch) raises an unexpected exception before break, the method exits via ensure, closing reader out from under the still-running thread. The thread then raises IOError: closed stream internally. This is an edge case and does not block merging, but joining the thread (or rescuing/discarding its exception) in the ensure block before reader.close would make the method more robust.

1-hour watch timeout exceeds the workflow cap

SHAKAPERF_RELEASE_GATE_WATCH_TIMEOUT_SECONDS = 60 * 60 can block the release script for an hour. The workflow itself declares timeout-minutes: 45, so the run resolves in ≤ 45 min. Setting the Rake-side value to 50 * 60 (45 min + 5 min slack) would make the bound meaningful rather than theoretical.


Security

  • persist-credentials: false on checkout ✅
  • SECRET_KEY_BASE is a documented dummy value with # gitleaks:allow
  • shaka-perf@0.1.3 runs a postinstall. package.json already instructs re-auditing before upgrades — no additional action needed here.
  • --no-sandbox in Chromium args is standard for sandboxed Linux CI. Revisit if this workflow moves to a self-hosted runner.

Test Coverage

The RSpec suite covers the core helper contracts well (dispatch, watch, timeout, stale-run filtering, override/dry-run bypasses). Two notes:

  1. capture_gh_output_with_timeout / terminate_process are not directly tested — the watch-timeout spec mocks the whole method. Acceptable given process-management complexity.
  2. The earliest_created_at filter relies on GitHub returning ISO 8601 timestamps. Specs use second-granular fixtures; sub-second clock skew could theoretically cause the first poll to miss the new run. The 5-second poll interval makes this harmless in practice since we retry.

Code Quality

  • The two waitForFunction bodies in rsc-fouc-release-gate.abtest.ts are nearly identical. A shared helper (e.g. waitForFirstVisibleProbeState) would reduce future drift risk (see inline comment).
  • The knip.ts hunk adds a create-react-on-rails-app workspace entry unrelated to the ShakaPerf gate. If required to make Knip pass, a brief note in the PR description or commit message would help reviewers understand it is not accidental scope creep.
  • UNSTYLED_BACKGROUNDS uses exact-string matching on getComputedStyle output. Chromium returns rgba(0, 0, 0, 0) consistently, so this is reliable in practice. The comment correctly advises changing the probe color if the expected style ever coincides with a default.

GitHub Actions Workflow

  • concurrency.cancel-in-progress: false is correct — stale gate runs should not be cancelled by a new dispatch.
  • The health probe loop with kill -0 process-liveness checks is solid defensive shell scripting.
  • Artifact retention at 30 days is appropriate for release debugging.

Summary

The implementation is well-constructed and the tests cover the critical paths. Key items before merge:

  1. (Medium) Document the push-before-gate recovery procedure prominently (release runbook or .claude/docs/).
  2. (Minor) Tighten SHAKAPERF_RELEASE_GATE_WATCH_TIMEOUT_SECONDS to ~50 minutes to align with the 45-minute workflow cap.
  3. (Minor) Extract the duplicated waitForFunction body in the abtest into a shared helper.
  4. (Nit) Add a brief note explaining the create-react-on-rails-app Knip entry.

Comment thread rakelib/release.rake
Comment thread rakelib/release.rake Outdated
Comment thread test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts Outdated
justin808 added 3 commits June 4, 2026 16:12
…lease-gate

* origin/main:
  Update RSC rollout to react-on-rails-rsc 19.0.5-rc.6 (#3577)
  fix(pro): polish rolling-deploy auto-route prefix + hash constraint (#3548) (#3611)
  fix(pro): log unmountComponentAtNode teardown failure at error level (#3592) (#3610)
  ci(rspack): decouple per-bundler build + integration jobs so one bundler's build failure doesn't skip the other (#3593) (#3613)
  ci: stop benchmarks/tests running on internal-docs & issue-template-only PRs (#3608)
  docs: define RC release gate tracking (#3597)

# Conflicts:
#	pnpm-lock.yaml

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2465a15ed2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

abTest(
'rsc first paint use-client css emits stylesheet before hydration',
{
startingPath: '/rsc_posts_page_over_http',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the probe-only route in the gate

When this gate runs against /rsc_posts_page_over_http without query params, the ERB view defaults postsCount to 2, and the rendered Post components include https://placehold.co/200 images. In this first abtest we later call page.waitForLoadState('load'), so a release can be blocked or made flaky by that external image host rather than by the RSC CSS behavior the gate is meant to check. The existing e2e probe uses ?posts_count=0 specifically to avoid rendering posts; use the same probe-only URL here (and in the companion abtest) so the release gate only depends on the CSS probe.

Useful? React with 👍 / 👎.

@justin808 justin808 merged commit 33c0ccc into main Jun 5, 2026
22 checks passed
@justin808 justin808 deleted the jg-codex/shakaperf-release-gate branch June 5, 2026 02:34
@justin808

Copy link
Copy Markdown
Member Author

Late review thread about using the probe-only URL is addressed in follow-up PR #3630. PR #3617 had already been squash-merged before this thread was visible in my final review-thread poll.

@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Design: Fix CSS FOUC behind 'use client' boundaries (issue #3211)

Status: approved 2026-06-03. Single PR (patch + renderer + tests).

Update 2026-06-04: The upstream manifest fix landed in
react-on-rails-rsc@19.0.5-rc.6, which records .css siblings (plus .mjs
chunk support and href normalization) natively. The local pnpm patch
described in Part A has been removed — the rollout now pins rc.6 and
relies on the published plugin. The renderer-side design (Part B onward) is
unchanged and still in force.

Problem

When a true React Server Component renders a 'use client' boundary, any CSS
imported by that boundary (or its descendants) is not preloaded. It loads
only as a side-effect of the JS chunk evaluating — several hundred ms to several
seconds after first paint — producing a visible flash of unstyled content
(FOUC). On slow networks the gap is large enough to be jarring.

Two independent gaps cause this, and both must be closed:

  1. Manifest gap. The patched react-server-dom-webpack-plugin.js shipped by
    react-on-rails-rsc@19.0.4 records only .js files for each client
    reference. The mini-css-extract-plugin CSS sibling of the same chunk group
    is dropped, so nothing downstream even knows the CSS exists.
  2. Renderer gap. The Pro RSC renderer never emits any CSS metadata — no
    <link rel="stylesheet">, no preloadStyle/preinitStyle, no precedence.
    The only path by which CSS reaches the page is the mini-css-extract runtime
    appending a <link> when each JS chunk evaluates — which is exactly the
    too-late path that causes the flicker.

The bug is invisible in react_on_rails_pro/spec/dummy historically because its
RSC components used inline style={{…}}. PR #3527 (merged) added a real CSS
module behind a 'use client' boundary (UseClientCssProbe) plus a pending
manifest spec that traps gap #1.

Why the fix is entirely in this repo

The upstream package fix (shakacode/react_on_rails_rsc#35) is still open and
unpublished. There is no fixed release to depend on. So the manifest half ships
here as a pnpm patch against 19.0.4, mirroring what rsc#35 will eventually
publish; when a fixed release lands we drop the patch.

The mechanism we rely on: React 19 <link precedence>

All RSC frameworks (Next App Router, Vite RSC/Waku, TanStack Start) converge on
one React 19 primitive: render <link rel="stylesheet" href precedence> into
the React tree. React 19 then:

  • hoists each <link> into <head>,
  • dedupes by href, and
  • blocks tree commit until the stylesheet's load fires.

That last property is what makes FOUC structurally impossible. Because we emit
the links inside the RSC Flight payload, the same elements re-materialize on
client navigation (createFromReadableStream) and React hoists/dedupes/suspends
identically — so SSR and CSR are both covered with no CSR-specific code.

Design

Part A — Manifest plugin patch (record CSS)

patches/react-on-rails-rsc@19.0.4.patch, applied via pnpm
patchedDependencies, rewrites the chunk-collection loop in
dist/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js.

Current (buggy) inner loop records at most the first .js per chunk and breaks
on the first non-.js file, dropping CSS:

chunkGroup.chunks.forEach(function (c) {
  for (const file of c.files) {
    if (!file.endsWith('.js')) break; // drops css
    if (file.endsWith('.hot-update.js')) break;
    chunks.push(c.id, file);
    break; // one js per chunk
  }
});

Patched loop preserves one-.js-per-chunk recording while iterating every entry
in c.files, and additionally collects every .css file into a sibling
cssChunks array:

chunkGroup.chunks.forEach(function (c) {
  let jsRecorded = false;
  for (const file of c.files) {
    if (file.endsWith('.hot-update.js')) continue;
    if (!jsRecorded && file.endsWith('.js')) {
      chunks.push(c.id, file);
      jsRecorded = true;
    } else if (file.endsWith('.css')) {
      cssChunks.push(file);
    }
  }
});

recordModule writes { id, chunks, css: cssChunks, name: "*" }. The manifest
type ImportManifestEntry gains an optional css?: string[] — backward
compatible; the Flight client runtime ignores it.

Decisions:

  • Keep the existing per-chunkGroup overwrite semantics for both chunks and
    css (no chunk-merge change). Minimal, parity with current JS handling. If a
    test later proves CSS needs cross-chunk-group merge, that is a separate change.
  • css filenames are bare emitted names (e.g. css/2696-bb8356be.css); the
    moduleLoading.prefix from the manifest is prepended at render time to form a
    usable href (handles CDN / non-default publicPath).

Part B — Renderer emits <link rel="stylesheet" precedence> (SSR)

Injection point: streamRenderRSCComponent in
packages/react-on-rails-pro/src/capabilities/proRSC.ts, immediately before
renderToPipeableStream(await reactRenderingResult, …).

New pure helper resolveCssHrefs(manifest): string[] walks
filePathToModuleMetadata, collects every entry's css, dedupes while
preserving manifest/chunk order, and prepends moduleLoading.prefix. The client
manifest is loaded through a small cached accessor added next to the existing
loaders in cache/ so the renderer can read it without re-reading the file per
unchanged manifest file signature.

Wrap the user tree:

const tree = await reactRenderingResult;
const wrapped = React.createElement(
  React.Fragment,
  null,
  ...cssHrefs.map((href) =>
    React.createElement('link', { key: href, rel: 'stylesheet', href, precedence: 'ror-rsc' }),
  ),
  tree,
);
renderToPipeableStream(wrapped, { onError });

precedence value: ror-rsc.

v1 = eager emission: emit every client-reference CSS chunk on every RSC
render, in manifest-traversal order. A few KB for the dummy app. Acceptable when
CSS Modules are properly scoped. Tree-walked emission (emit only references
encountered during render, via prepareDestinationForModule /
preinitStyle) is the documented future optimization, not v1.

Part C — CSR (prerender: false)

No code change. The <link>s live inside the RSC Flight payload; on client
navigation they decode and React hoists/dedupes/suspends commit identically.

Part D — Tests

  1. Manifest (Part A). Flip the merged rsc_use_client_css_manifest_spec.rb
    from pending to asserting metadata.fetch("css") includes a .css href.

  2. Render (Part B/C). Reuse the existing UseClientCssProbe, already
    rendered by RSCPostsPage/Main.jsx on the live streaming-RSC route
    /rsc_posts_page_over_http. Assert:

    • <head> contains link[rel="stylesheet"][data-precedence] whose href
      resolves to the probe's CSS, and
    • the probe element's computed background-color is the styled value
      (rgb(212, 250, 236)) after load.

    This is a deterministic, structural assertion of the anti-FOUC mechanism — no
    network-timing race.

Explicitly out of scope (YAGNI)

  • No mini-css-extract insert override (Next's _N_E_STYLE_LOAD).
  • No data-rsc-css-href markers + post-decode harvest (TanStack pattern).
  • No per-route entry-CSS map (Next's entryCSSFiles).
  • No CSR-side preinitStyle (deferred perf optimization, not correctness).
  • No throttled "Slow 3G" e2e variant (flaky; the structural assertion above
    covers the regression deterministically). Optional manual follow-up only.
  • No non-CSS asset types (fonts, images).
  • No separate RSCFOUCDemo fixture — the merged UseClientCssProbe is the
    single canonical probe.

Touch points

File Change
package.json (pnpm.patchedDependencies) register the patch
patches/react-on-rails-rsc@19.0.4.patch (new) collect .css into css[]
packages/react-on-rails-pro/src/resolveCssHrefs.ts (new) manifest → ordered deduped hrefs
cache/ client-manifest accessor cached read of client manifest for the renderer, invalidated by file signature
packages/react-on-rails-pro/src/capabilities/proRSC.ts wrap tree with <link precedence> before renderToPipeableStream
…/spec/dummy/spec/requests/rsc_use_client_css_manifest_spec.rb unpend; assert css
…/spec/dummy/e2e-tests/ assert <link precedence> in <head> + computed bg color
react_on_rails_rsc types (consumed) ImportManifestEntry.css?: string[] (via patch + local type)

Migration / compatibility

  • Manifest extension is backward compatible (css optional, JS chunks
    unchanged).
  • Apps without 'use client' boundaries behave identically.
  • React Flight client never reads css; only the SSR renderer consumes it.

Test plan (commands)

  • pnpm install (applies the patch)
  • cd react_on_rails_pro/spec/dummy && bundle exec rake react_on_rails:generate_packs
  • cd react_on_rails_pro/spec/dummy && pnpm run build:test
  • bundle exec rspec spec/requests/rsc_use_client_css_manifest_spec.rb → green (no pending)
  • Playwright e2e for /rsc_posts_page_over_http → link-in-head + bg-color assertions pass
  • bundle exec rubocop on changed Ruby; lint changed JS/TS

@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR adds a ShakaPerf visual-regression release gate for the RSC FOUC regression. The gate dispatches a workflow_dispatch-only GitHub Actions workflow after the version-bump push, waits for it to pass, and only then creates the tag and publishes packages. Comparing the candidate app against itself with absolute assertions is a clean approach to catching a CSS-delivery regression without needing a known-good baseline.

Overall the implementation is well thought-out. A few items worth noting for future maintenance:


Correctness

Run-matching race window (release.rake lines 371-392)

The code snapshots existing_run_ids, then captures dispatch_started_at, then dispatches. A run created in that narrow window (between snapshot and dispatch) would have an ID not in ignored_run_ids and a createdAt >= earliest_created_at, so it could be mistaken for the newly dispatched run. Tiny in practice, but this is a GitHub API limitation since the dispatch response does not return the new run ID.

shakaperf_release_gate_dispatch_started_at truncates to whole seconds (release.rake line 299)

Time.at(Time.now.to_i).utc floors to the nearest second intentionally, to prevent sub-second clock differences from causing the earliest_created_at filter to miss the run. A short inline comment explaining why this is truncated (vs Time.now.utc) would help a future editor avoid treating it as dead code.


Test Coverage

wait_for_shakaperf_release_gate_run! and watch_shakaperf_release_gate_run! are not tested independently. The spec tests run_shakaperf_release_gate! by stubbing those two methods, so the polling and timeout logic inside them has no direct coverage. Given this is a release gate that blocks publishing, that logic deserves its own unit tests.

fetch_shakaperf_release_gate_runs JSON error path is covered only indirectly. One extra example for the JSON::ParserError branch would close that gap.


ShakaPerf Test Quality

The UNSTYLED_BACKGROUNDS list is the right pattern — an explicit list of known-bad computed values is less fragile than asserting a specific expected color. The comment about changing the probe color if it ever collides with this list is exactly right.

The two abTest calls are well split: test 1 is the SSR correctness guard (JS blocked), test 2 is the visual report (JS unblocked). Using isControl only for error-message labeling is appropriate since both sides hit the same URL.

The waitForFunction pattern is correct — returning false until ready, then the state object, works cleanly with Playwright's truthy-return contract. polling: 'raf' is the right choice here.


GitHub Actions Workflow

Good security posture throughout: persist-credentials: false, secrets scoped to steps that need them, set -euo pipefail, and early process-death detection in the startup poll loop.

cancel-in-progress: false is correct for a release gate — queuing is safer than canceling a gate already in progress.

Cleanup step uses if: always() with kill ... || true. Clean.

Minor nit: The startup loop polls the RSC page with --max-time 5 (line 148). On a cold first hit that page does a full RSC fetch and could be slow. --max-time 10 for that specific probe would reduce spurious "waiting" log lines without meaningfully extending the 120-second total window.


Dependency Additions

shaka-perf@0.1.3 and shaka-shared@0.1.3 are exact-pinned, appropriate given the postinstall step and the note to re-audit before upgrading. Adding shaka-perf to onlyBuiltDependencies is correct. The Knip ignoreBinaries entry with an explanatory comment is the right approach for a CLI used only from a YAML shell step.


Summary

Area Status
Workflow security Clean
Gate logic correctness Sound (minor race-window caveat noted)
ShakaPerf test assertions Well designed
Release rake integration Correct
Unit test coverage Gap: wait_for_* / watch_* polling methods not tested directly
Documentation README covers local run and design intent

@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Code Review: ShakaPerf RSC FOUC Release Gate

Overview

This PR adds a manual/tag-triggered ShakaPerf visual-regression release gate that catches CSS flash-of-unstyled-content (FOUC) regressions in the React Server Components path. It integrates into the release rake flow between the version-bump push and tagging/publishing, with a documented override escape hatch.

The design is sound and well-tested. A few things worth noting below.


Strengths

  • Correct release ordering: the gate runs after git push but before tagging and npm publish. The override instructions in the abort message correctly note that the version-bump commit is already pushed, which guides operators on retry vs. revert.
  • Triple-filter run matching (headSha + ignored_run_ids + earliest_created_at) guards against picking up a stale workflow run for the same commit. The second-precision timestamp truncation (Time.at(Time.now.to_i).utc) is subtle but correct: shakaperf_release_gate_run_created_after? uses >= so it matches runs created in the same second as dispatch.
  • Security posture: permissions: contents: read, persist-credentials: false, no shell interpolation of the Pro license secret, and gitleaks:allow annotation on the dummy SECRET_KEY_BASE. All appropriate.
  • Cleanup step: Stop Pro dummy app runs if: always() and explicitly checks for PID files before killing, so it is safe even if the start step never wrote them.
  • Self-comparison with absolute assertions: using the same URL for both control and experiment is unconventional but the README explains it clearly. The programmatic throw inside the ShakaPerf callback is the actual gate; the visreg diff is an artifact for release debugging.
  • Test coverage: the Ruby helper specs cover dispatch success, override skip, dry-run skip, dispatch failure, run-watch failure, watch timeout, SHA matching, stale-run exclusion, timestamp boundary, and start timeout. Solid.

Issues and Suggestions

1. waitForFunction body is duplicated verbatim between the two abtests

Both abtests in rsc-fouc-release-gate.abtest.ts share the exact same waitForFunction callback. If the probe selector or the returned state shape changes, both copies need updating. A shared helper constant or function at the top of the file would remove the duplication without adding any API surface.

2. Workflow timeout-minutes: 45 vs. rake SHAKAPERF_RELEASE_GATE_WATCH_TIMEOUT_SECONDS = 3600

The workflow job will be killed by GitHub at 45 minutes, which means gh run watch --exit-status will receive a non-zero exit before the rake-side 1-hour timeout fires. This is harmless in practice (the rake watcher correctly treats the failed status as a violation), but the operator-visible error message comes from the GitHub Actions timeout rather than handle_shakaperf_release_gate_violation!. Consider either documenting this or reducing SHAKAPERF_RELEASE_GATE_WATCH_TIMEOUT_SECONDS to ~50 minutes so the rake message fires first on a true hang.

3. Unrelated Knip changes

The PR adds a packages/create-react-on-rails-app workspace entry and two new entry paths for react-on-rails-pro to knip.ts. These look like pre-existing gaps being fixed opportunistically. Worth calling out so reviewers know the scope is wider than the ShakaPerf files.

4. shaka-shared missing from onlyBuiltDependencies

shaka-perf is listed in onlyBuiltDependencies with an explanation comment, but shaka-shared is not, even though both are exact-pinned. If shaka-shared also has a postinstall step (the lockfile shows it resolves against playwright-core), it may need the same treatment. Worth verifying with pnpm ignored-builds.

5. No test for fetch_shakaperf_release_gate_runs failure inside the wait loop

wait_for_shakaperf_release_gate_run! delegates to fetch_shakaperf_release_gate_runs on every poll, which calls handle_shakaperf_release_gate_violation! (and abort) if gh run list fails mid-wait. There is no spec that exercises this path through wait_for_shakaperf_release_gate_run! directly. Low priority, but useful for confidence.


Minor nits

  • The README says "blocks app JavaScript" but installRequestBlocking(context, ['.js']) blocks all .js requests regardless of path. Fine behavior, but could be more precise.
  • compareRetries: 3 with compareRetryDelay: 500 is good for flake resilience.
  • gotoParameters: { waitUntil: 'commit' } is correct for the pre-hydration test; the waitForLoadState('load') call after the assertions in the first abtest ensures the screenshot is stable before capture.

Summary

The core approach is correct and the implementation is well thought-out. Main actionable items: de-duplicate the waitForFunction body, verify shaka-shared postinstall needs, and consider aligning the watch timeout so operators get a clear rake-level error message on a true hang. The Knip changes should be explicitly acknowledged as intentional scope.

Comment thread rakelib/release.rake
end

def shakaperf_release_gate_dispatch_started_at
Time.at(Time.now.to_i).utc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This intentional truncation to whole seconds deserves an inline comment — without one, a future editor might "clean it up" to Time.now.utc and break the earliest_created_at filter for runs dispatched at the very start of the same second.

Suggested change
Time.at(Time.now.to_i).utc
# Floor to whole seconds: GitHub's createdAt timestamps are second-granular, and a
# sub-second Time.now.utc would filter out a run whose createdAt equals the truncated second.
def shakaperf_release_gate_dispatch_started_at
Time.at(Time.now.to_i).utc
end

Comment thread rakelib/release.rake
Comment on lines +371 to +374
existing_run_ids = fetch_shakaperf_release_gate_runs(repo_slug: repo_slug, ref: ref).map do |run|
run["databaseId"].to_s
end
dispatch_started_at = shakaperf_release_gate_dispatch_started_at

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a narrow window here: existing_run_ids is snapshotted before dispatch_started_at is captured, and both are captured before the dispatch call. A run created between the snapshot and the actual dispatch (a few milliseconds) would have an ID not in ignored_run_ids and a createdAtearliest_created_at, so it could be mistaken for the new run.

In practice this is extremely unlikely (requires a concurrent release), and it is a GitHub API limitation (the dispatch response does not return the run ID). Just worth documenting with a comment so future reviewers understand why the order matters.

end
end

describe "#run_shakaperf_release_gate!" do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The run_shakaperf_release_gate! tests are thorough, but wait_for_shakaperf_release_gate_run! and watch_shakaperf_release_gate_run! are only exercised through stubs here. The polling/timeout logic in those methods (the ignored_run_ids filter, earliest_created_at comparison, process-timeout path) has no direct coverage. For a release gate that blocks publishing, consider adding unit tests for at least:

  • wait_for_shakaperf_release_gate_run! returns the matching run (correct headSha, not in ignored_run_ids, createdAtearliest_created_at)
  • wait_for_shakaperf_release_gate_run! calls handle_shakaperf_release_gate_violation! on timeout
  • watch_shakaperf_release_gate_run! calls the violation handler when timed_out: true

# and the RSC page proves both sides are warm enough for the gate.
if curl --connect-timeout 2 --max-time 5 -fsS "http://127.0.0.1:3800/info" > /dev/null &&
curl --connect-timeout 2 --max-time 5 -fsS "$SHAKAPERF_CONTROL_URL/empty" > /dev/null &&
curl --connect-timeout 2 --max-time 5 -fsS "$SHAKAPERF_CONTROL_URL/rsc_posts_page_over_http" > /dev/null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RSC page probe uses --max-time 5, but on a cold first hit this page triggers a full RSC server-component fetch inside the node renderer. On a slow CI runner that round-trip can exceed 5 seconds, causing the poll to fail and restart the 2-second sleep even though the server is actually up. --max-time 10 here would reduce spurious "waiting" lines without extending the 120-second total window.

Suggested change
curl --connect-timeout 2 --max-time 5 -fsS "$SHAKAPERF_CONTROL_URL/rsc_posts_page_over_http" > /dev/null
curl --connect-timeout 2 --max-time 10 -fsS "$SHAKAPERF_CONTROL_URL/rsc_posts_page_over_http" > /dev/null

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Pro Node Renderer Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Fail% Status
Pro Node Renderer: simple_eval (non-RSC) 2234.63 4.33 5.33 0.0 200=67042
Pro Node Renderer: react_ssr (non-RSC) 1927.11 5.0 6.07 0.0 200=57818

🔴 significant regression · 🟢 significant improvement (vs baseline; tracked measures only)

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Pro (shard 1/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Fail% Status
/: Pro 169.02 52.35 66.09 0.0 200=5107
/error_scenarios_hub: Pro 362.27 20.86 31.63 0.0 200=10949
/ssr_async_error: Pro 333.87 23.14 34.86 0.0 200=10088
/ssr_async_prop_error: Pro 319.82 24.63 38.66 0.0 200=9663
/non_existing_react_component: Pro 334.44 26.14 35.13 0.0 200=10103
/non_existing_rsc_payload: Pro 349.75 22.38 35.28 0.0 200=10567
/cached_react_helmet: Pro 286.59 20.09 48.3 0.0 200=8674
/cached_redux_component: Pro 373.6 21.2 32.66 0.0 200=11287
/lazy_apollo_graphql: Pro 123.1 48.13 119.32 0.0 200=3724
/redis_receiver: Pro 87.52 75.51 141.64 0.0 200=2621,3xx=37
/stream_shell_error_demo: Pro 320.82 23.63 36.7 0.0 200=9695
/test_incremental_rendering: Pro 170.8 🔴 16.66 341.67 0.0 200=5198
/rsc_posts_page_over_redis: Pro 104.42 73.35 114.01 0.0 200=3158
/async_on_server_sync_on_client: Pro 268.15 21.64 71.39 0.0 200=8104
/server_router: Pro 342.47 25.67 35.14 0.0 200=10283
/unwrapped_rsc_route_client_render: Pro 376.88 20.24 30.48 0.0 200=11405
/async_render_function_returns_string: Pro 349.8 24.76 34.5 0.0 200=10502
/async_components_demo: Pro 215.22 38.77 51.84 0.0 200=6517
/stream_native_metadata: Pro 354.96 21.93 32.65 0.0 200=10725
/rsc_native_metadata: Pro 334.07 23.16 38.37 0.0 200=10094
/client_side_hello_world: Pro 340.72 16.52 30.82 0.0 200=10299
/client_side_hello_world_shared_store_controller: Pro 309.45 24.82 36.79 0.0 200=9354
/server_side_hello_world_shared_store: Pro 260.29 30.93 44.92 0.0 200=7865
/server_side_hello_world_shared_store_defer: Pro 218.42 26.39 56.42 0.0 200=6603
/server_side_hello_world_hooks: Pro 302.72 18.44 34.84 0.0 200=9153
/server_side_log_throw: Pro 304.94 25.77 37.04 0.0 200=9214
/server_side_log_throw_raise: Pro 503.13 11.47 15.42 0.0 3xx=15301
/server_side_hello_world_es5: Pro 277.96 20.45 24.65 0.0 200=8455
/server_side_hello_world_with_options: Pro 334.8 23.66 36.59 0.0 200=10054
/client_side_manual_render: Pro 367.25 15.11 28.59 0.0 200=11175
/react_router: Pro 416.45 17.62 31.82 0.0 200=12583
/css_modules_images_fonts_example: Pro 339.27 22.89 36.79 0.0 200=10252
/rendered_html: Pro 333.45 23.4 34.11 0.0 200=10077
/react_helmet: Pro 306.98 25.32 37.01 0.0 200=9279
/image_example: Pro 316.61 27.75 36.64 0.0 200=9507
/posts_page: Pro 877.83 9.2 13.92 100.0 5xx=26519

🔴 significant regression · 🟢 significant improvement (vs baseline; tracked measures only)

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Core Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Fail% Status
/: Core 2.82 2860.33 3430.68 0.0 200=95
/client_side_hello_world: Core 738.21 11.13 19.27 0.0 200=22301
/client_side_rescript_hello_world: Core 708.72 7.27 16.05 0.0 200=21416
/client_side_hello_world_shared_store: Core 711.01 8.54 15.06 0.0 200=21484
/client_side_hello_world_shared_store_controller: Core 696.01 9.43 19.44 0.0 200=21029
/client_side_hello_world_shared_store_defer: Core 668.17 8.84 22.85 0.0 200=20186
/server_side_hello_world_shared_store: Core 12.26 686.62 850.86 0.0 200=377
/server_side_hello_world_shared_store_controller: Core 12.41 673.04 841.64 0.0 200=382
/server_side_hello_world_shared_store_defer: Core 10.39 570.2 771.41 0.0 200=321
/server_side_hello_world: Core 25.5 328.54 392.56 0.0 200=774
/server_side_hello_world_hooks: Core 18.09 342.31 480.84 0.0 200=551
/server_side_hello_world_props: Core 19.78 305.09 472.34 0.0 200=602
/client_side_log_throw: Core 503.09 11.7 17.42 0.0 200=15199
/server_side_log_throw: Core 24.79 338.56 401.84 0.0 200=755
/server_side_log_throw_plain_js: Core 24.96 328.55 394.52 0.0 200=762
/server_side_log_throw_raise: Core 25.12 335.75 399.78 0.0 3xx=763
/server_side_log_throw_raise_invoker: Core 802.14 8.12 17.74 0.0 200=24229
/server_side_hello_world_es5: Core 25.34 331.89 390.11 0.0 200=772
/server_side_redux_app: Core 24.33 346.81 405.43 0.0 200=743
/server_side_hello_world_with_options: Core 25.15 234.0 382.98 0.0 200=770
/server_side_redux_app_cached: Core 736.74 11.89 17.55 0.0 200=22254
/client_side_manual_render: Core 704.78 8.6 21.18 0.0 200=21290
/render_js: Core 19.38 317.34 348.53 0.0 200=593
/react_router: Core 19.13 320.16 477.9 0.0 200=582
/pure_component: Core 25.72 323.91 389.74 0.0 200=782
/css_modules_images_fonts_example: Core 25.21 235.57 381.11 0.0 200=771
/turbolinks_cache_disabled: Core 734.14 11.38 19.48 0.0 200=22177
/rendered_html: Core 25.62 328.54 390.25 0.0 200=779
/xhr_refresh: Core 12.97 656.97 830.75 0.0 200=399
/react_helmet: Core 20.93 281.03 372.88 0.0 200=640
/broken_app: Core 24.65 263.43 394.2 0.0 200=754
/image_example: Core 25.15 334.82 392.51 0.0 200=766
/turbo_frame_tag_hello_world: Core 761.33 8.51 17.19 0.0 200=23002
/manual_render_test: Core 751.66 8.64 17.77 0.0 200=22708

🔴 significant regression · 🟢 significant improvement (vs baseline; tracked measures only)

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Pro (shard 2/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Fail% Status
/empty: Pro 1253.91 6.41 9.56 0.0 200=37873
/ssr_shell_error: Pro 340.32 25.61 35.61 0.0 200=10285
/ssr_sync_error: Pro 337.03 23.03 34.57 0.0 200=10183
/rsc_component_error: Pro 350.04 25.08 34.83 0.0 200=10510
/non_existing_stream_react_component: Pro 353.24 21.81 35.98 0.0 200=10674
/server_side_redux_app_cached: Pro 343.94 25.36 33.76 0.0 200=10393
/loadable: Pro 292.43 11.03 35.19 0.0 200=8897
/apollo_graphql: Pro 146.92 52.81 85.3 0.0 200=4442
/console_logs_in_async_server: Pro 3.57 2123.28 2141.02 0.0 200=122
/stream_error_demo: Pro 331.34 23.8 37.75 0.0 200=10011
/stream_async_components: Pro 346.81 22.1 33.84 0.0 200=10481
/rsc_posts_page_over_http: Pro 337.96 22.89 34.66 0.0 200=10211
/rsc_echo_props: Pro 226.87 35.15 50.14 0.0 200=6858
/async_on_server_sync_on_client_client_render: Pro 369.23 23.48 32.77 0.0 200=11157
/server_router_client_render: Pro 363.46 17.45 28.66 0.0 200=10985
/unwrapped_rsc_route_stream_render: Pro 360.37 15.28 29.52 0.0 200=10893
/async_render_function_returns_component: Pro 357.01 22.03 34.12 0.0 200=10785
/native_metadata: Pro 345.85 25.23 33.53 0.0 200=10448
/hybrid_metadata_streaming: Pro 348.85 22.33 33.24 0.0 200=10550
/cache_demo: Pro 264.19 21.68 48.79 0.0 200=7985
/client_side_hello_world_shared_store: Pro 335.76 22.95 33.88 0.0 200=10146
/client_side_hello_world_shared_store_defer: Pro 351.46 21.89 32.12 0.0 200=10620
/server_side_hello_world_shared_store_controller: Pro 291.12 27.38 41.36 0.0 200=8799
/server_side_hello_world: Pro 347.13 22.64 35.43 0.0 200=10487
/client_side_log_throw: Pro 384.88 20.23 31.51 0.0 200=11627
/server_side_log_throw_plain_js: Pro 400.65 19.86 30.97 0.0 200=12107
/server_side_log_throw_raise_invoker: Pro 426.71 17.71 27.03 0.0 200=12894
/server_side_redux_app: Pro 341.75 22.91 33.57 0.0 200=10330
/server_side_redux_app_cached: Pro 385.36 20.52 29.15 0.0 200=11644
/render_js: Pro 392.77 19.7 29.31 0.0 200=11868
/pure_component: Pro 355.75 21.96 32.21 0.0 200=10748
/turbolinks_cache_disabled: Pro 391.46 19.34 29.03 0.0 200=11831
/xhr_refresh: Pro 300.53 28.82 38.68 0.0 200=9081
/broken_app: Pro 358.4 22.01 35.08 0.0 200=10833
/server_render_with_timeout: Pro 352.64 22.49 33.76 0.0 200=10659

🔴 significant regression · 🟢 significant improvement (vs baseline; tracked measures only)

justin808 added a commit that referenced this pull request Jun 5, 2026
Follow-up to #3617 after a late review thread noted that
/rsc_posts_page_over_http renders posts by default, including external
placeholder images. This points both ShakaPerf FOUC abtests at the
existing probe-only route with posts_count=0 so the release gate depends
on the CSS probe, not image loading.\n\nVerification:\n- pnpm exec
eslint
test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts\n- pnpm
exec prettier --check
test/shakaperf/rsc-fouc/ab-tests/rsc-fouc-release-gate.abtest.ts\n- git
diff --check

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only URL change in ShakaPerf abtests; no production or auth paths
affected.
> 
> **Overview**
> Both ShakaPerf RSC FOUC release-gate abtests now load
**`/rsc_posts_page_over_http?posts_count=0`** via a shared
**`RSC_CSS_PROBE_PATH`** constant instead of the bare posts route.
> 
> That keeps the gate focused on the CSS probe and RSC stylesheet
behavior, avoiding default post rendering and external placeholder image
loads that could add noise or flakiness to visual regression checks.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5f4b362. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Improved A/B test configuration for CSS rendering and first paint
performance validation by standardizing the test route path across
related test cases.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
justin808 added a commit that referenced this pull request Jun 5, 2026
* origin/main: (21 commits)
  docs(benchmarks): archive RSC FOUC ShakaPerf investigation
  feat(generator): scaffold native RSCRspackPlugin for rspack RSC
  fix(pro): admit react-on-rails-rsc RC prereleases in peer range
  ci(benchmarks): richer summary table
  fix(benchmarks): seed Pro dummy DB so /posts_page stops 500ing (#3602) (#3615)
  fix(benchmark): detect backgrounded server/renderer crashes during readiness and benchmark (#3579)
  refactor: harden ShakapackerConfigHelpers module (#3626) (#3629)
  fix: classify renderer connection failures separately from bundle errors (#3614)
  Align docs demo naming and version guidance (#3598)
  Harden async props pending specs (#3600)
  docs(ci): link latest/minimum coupling comment to tracking issue #3622 (#3623)
  Use probe-only route for ShakaPerf FOUC gate (#3630)
  [codex] Add ShakaPerf RSC FOUC release gate (#3617)
  refactor: consolidate Shakapacker bundler/config diagnostics helpers (#3619)
  docs(pro): document CI pattern for renderer-backed Rails tests (#3612)
  Update RSC rollout to react-on-rails-rsc 19.0.5-rc.6 (#3577)
  fix(pro): polish rolling-deploy auto-route prefix + hash constraint (#3548) (#3611)
  fix(pro): log unmountComponentAtNode teardown failure at error level (#3592) (#3610)
  ci(rspack): decouple per-bundler build + integration jobs so one bundler's build failure doesn't skip the other (#3593) (#3613)
  ci: stop benchmarks/tests running on internal-docs & issue-template-only PRs (#3608)
  ...

# Conflicts:
#	CHANGELOG.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

benchmark codex PRs created from codex-named branches codex-wip Codex is actively working this PR documentation enhancement full-ci P2 Backlog priority review-needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant