Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9f3395d
ci: address review feedback and fix bundler-cache test failures
justin808 May 22, 2026
5edb4f7
ci: add BUNDLE_DISABLE_CHECKSUM_VALIDATION to remaining Pro jobs
justin808 May 22, 2026
6176ad6
ci(precompile-check): use non-interactive apt install for libyaml-dev
justin808 May 22, 2026
b40cf13
ci: address remaining bundler-cache review feedback
justin808 May 24, 2026
29e8486
Fix Pro bundler base64 dependency
justin808 May 24, 2026
ba7e3a9
Install libyaml before bundler cache
justin808 May 24, 2026
2010e82
ci: address bundler-cache review feedback
justin808 May 27, 2026
6545846
ci: clarify bundler-cache review notes
justin808 May 24, 2026
921b180
Clarify benchmark bundler cache setup
justin808 May 25, 2026
809a7fb
Clean up Pro integration workflow lint
justin808 May 25, 2026
7338de2
Fix benchmark server port handling
justin808 May 25, 2026
3f6f431
Address remaining PR review reliability notes
justin808 May 25, 2026
e31318f
Fix CI workflow shell lint findings
justin808 May 25, 2026
69e60e1
ci: pin BUNDLE_GEMFILE on Core benchmark steps and harden lint paths
justin808 May 26, 2026
8b3220b
ci: pin BUNDLE_GEMFILE on lint and Main CI steps
justin808 May 26, 2026
9b77b6c
ci: polish CI workflow review comments
justin808 May 26, 2026
03cfb1e
ci: clarify rebased bundle setup
justin808 May 27, 2026
f8b8b17
ci: address benchmark workflow review
justin808 May 28, 2026
20b0e18
ci: use setup-ruby bundler cache
justin808 May 28, 2026
ebdb399
ci: simplify benchmark summary quoting
justin808 May 28, 2026
7689f47
ci: address remaining workflow review
justin808 May 28, 2026
b580a3f
ci: let dummy specs use dummy bundle
justin808 May 28, 2026
bbf71aa
Address review feedback after CI rebase
justin808 May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 18 additions & 41 deletions .github/actions/setup-bundle/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: Setup Bundler dependencies
description: Cache and install Ruby gems for a Bundler project.
description: >
Setup Ruby, cache, and install gems for a Bundler project. On Linux,
ensure libyaml-dev is already present or install it before calling this action.

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 libyaml-dev prerequisite is stated in the description but is not enforced — setup-bundle itself never installs it. Callers using setup-ruby first get it implicitly; callers that use setup-bundle directly (e.g. the Playwright workflow) do not.

Consider adding an install-libyaml input (default 'true') that mirrors the one in setup-ruby, so any direct caller of setup-bundle is protected automatically:

Suggested change
ensure libyaml-dev is already present or install it before calling this action.
Setup Ruby, cache, and install gems for a Bundler project. On Linux,
libyaml-dev is installed automatically (set install-libyaml: 'false' to skip
if the caller already ran setup-ruby).


inputs:
working-directory:
Expand All @@ -8,22 +10,18 @@ inputs:
ruby-version:
description: Ruby version used by the job.
required: true
bundle-path:
description: Bundler install path, relative to the working directory.
required: false
default: vendor/bundle
frozen:
description: Whether to run Bundler in frozen mode. Cache saving is disabled when false; restore still runs.
description: Whether to run Bundler in frozen mode.
required: false
default: 'true'
bundler-version:
description: Bundler version selector. Use lockfile, system, or x.y.z.
description: Bundler version selector. Use Gemfile.lock, default, latest, none, or x.y.z.
required: false
default: lockfile
install-args:
description: Arguments for bundle install.
default: Gemfile.lock
cache-version:
description: Arbitrary cache-version value passed through to ruby/setup-ruby.
required: false
default: --jobs=4 --retry=3
default: '0'

runs:
using: composite
Expand All @@ -37,36 +35,15 @@ runs:
exit 1
fi

- name: Restore Ruby gems cache
id: restore-ruby-gems-cache
uses: actions/cache/restore@v5
with:
path: ${{ inputs.working-directory }}/${{ inputs.bundle-path }}
# Update to bundle-...-v1 in the future to break the cache if necessary
# or v1-bundle-... if you want restore-keys not to trigger either (update the key there in that case)
key: bundle-${{ runner.os }}-${{ inputs.working-directory }}-${{ inputs.bundle-path }}-ruby${{ inputs.ruby-version }}-${{ hashFiles(format('{0}/Gemfile.lock', inputs.working-directory)) }}
restore-keys: |
bundle-${{ runner.os }}-${{ inputs.working-directory }}-${{ inputs.bundle-path }}-ruby${{ inputs.ruby-version }}-

- name: Install Ruby gems
shell: bash
working-directory: ${{ inputs.working-directory }}
- name: Setup Ruby and install cached gems
Comment thread
justin808 marked this conversation as resolved.
uses: ruby/setup-ruby@v1
env:
BUNDLE_FROZEN: ${{ inputs.frozen }}
BUNDLE_INSTALL_ARGS: ${{ inputs.install-args }}
BUNDLE_PATH_CONFIG: ${{ inputs.bundle-path }}
BUNDLER_VERSION_CONFIG: ${{ inputs.bundler-version }}
run: |
bundle config set --local path "$BUNDLE_PATH_CONFIG"
bundle config set --local version "$BUNDLER_VERSION_CONFIG"
# shellcheck disable=SC2086
# Intentionally allow install args to split into separate bundle arguments
bundle check || bundle install $BUNDLE_INSTALL_ARGS

- name: Save Ruby gems cache
# We don't want to overwrite the cache for non-frozen Bundler runs
if: inputs.frozen == 'true' && steps.restore-ruby-gems-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
BUNDLE_RETRY: '3'
Comment thread
justin808 marked this conversation as resolved.
BUNDLE_JOBS: '4'
with:
path: ${{ inputs.working-directory }}/${{ inputs.bundle-path }}
key: ${{ steps.restore-ruby-gems-cache.outputs.cache-primary-key }}
ruby-version: ${{ inputs.ruby-version }}
working-directory: ${{ inputs.working-directory }}
bundler: ${{ inputs.bundler-version }}
bundler-cache: true

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.

Behavioral change from old implementation: the previous setup-bundle explicitly skipped the cache-save step when frozen == 'false':

if: inputs.frozen == 'true' && steps.restore-ruby-gems-cache.outputs.cache-hit != 'true'

ruby/setup-ruby with bundler-cache: true always writes the cache regardless of BUNDLE_FROZEN. The minimum-dependency legs that set frozen: 'false' will now write gem caches after every non-frozen bundle install. The cache keys are Gemfile.lock-hash-based so cross-contamination is unlikely, but the old guard made it structurally impossible.

Consider making bundler-cache conditional on the frozen input:

Suggested change
bundler-cache: true
bundler-cache: ${{ inputs.frozen }}

(ruby/setup-ruby accepts 'true'/'false' strings for bundler-cache, so this maps directly.)

cache-version: ${{ inputs.cache-version }}
30 changes: 30 additions & 0 deletions .github/actions/setup-ruby/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Setup Ruby runtime
description: Install Ruby runtime prerequisites and setup Ruby without installing gems.

inputs:
ruby-version:
description: Ruby version used by the job.
required: true
bundler-version:
description: Bundler version selector passed through to ruby/setup-ruby.
required: false
default: Gemfile.lock
install-libyaml:
description: Whether to install libyaml-dev before Ruby/Bundler steps.
required: false
default: 'true'

runs:
using: composite
steps:
- name: Install libyaml-dev
if: runner.os == 'Linux' && inputs.install-libyaml == 'true'
shell: bash
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libyaml-dev

- name: Setup Ruby
uses: ruby/setup-ruby@v1
Comment thread
justin808 marked this conversation as resolved.
with:
ruby-version: ${{ inputs.ruby-version }}
bundler: ${{ inputs.bundler-version }}
bundler-cache: false

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.

Explicit bundler-cache: false here is correct and important — this action is intentionally for Ruby-only setup (used before setup-bundle). Without this explicit false, ruby/setup-ruby defaults to no caching, but being explicit prevents confusion if the default ever changes upstream.

82 changes: 53 additions & 29 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ jobs:
env:
SECRET_KEY_BASE: 'dummy-secret-key-for-ci-testing-not-used-in-production'
REACT_ON_RAILS_PRO_LICENSE: ${{ secrets.REACT_ON_RAILS_PRO_LICENSE_V2 }}
BENCHMARK_PORT: '3001'
Comment thread
justin808 marked this conversation as resolved.

steps:
# ============================================
Expand All @@ -167,7 +168,7 @@ jobs:
- name: Add tools directory to PATH
run: |
mkdir -p ~/bin
echo "$HOME/bin" >> $GITHUB_PATH
echo "$HOME/bin" >> "$GITHUB_PATH"

- name: Cache Vegeta binary
id: cache-vegeta
Expand All @@ -183,11 +184,11 @@ jobs:
echo "📦 Installing Vegeta v${VEGETA_VERSION}"

# Download and extract vegeta binary
wget -q https://github.com/tsenart/vegeta/releases/download/v${VEGETA_VERSION}/vegeta_${VEGETA_VERSION}_linux_amd64.tar.gz
tar -xzf vegeta_${VEGETA_VERSION}_linux_amd64.tar.gz
wget -q "https://github.com/tsenart/vegeta/releases/download/v${VEGETA_VERSION}/vegeta_${VEGETA_VERSION}_linux_amd64.tar.gz"
tar -xzf "vegeta_${VEGETA_VERSION}_linux_amd64.tar.gz"

# Store in cache directory
mv vegeta ~/bin/
mv vegeta "$HOME/bin/"

- name: Setup k6
uses: grafana/setup-k6-action@v1
Expand All @@ -198,13 +199,15 @@ jobs:
# STEP 3: START APPLICATION SERVER
# ============================================

# Sets up Ruby for unconditional steps (`gem env home`, Foreman cache/install).
# Dummy app gems are installed in the conditional setup-bundle steps below.
- name: Setup Ruby
Comment thread
justin808 marked this conversation as resolved.
uses: ruby/setup-ruby@v1
uses: ./.github/actions/setup-ruby
with:
ruby-version: ${{ env.RUBY_VERSION }}

- name: Get gem home directory
run: echo "GEM_HOME_PATH=$(gem env home)" >> $GITHUB_ENV
run: echo "GEM_HOME_PATH=$(gem env home)" >> "$GITHUB_ENV"

- name: Cache foreman gem
id: cache-foreman
Expand All @@ -217,9 +220,6 @@ jobs:
if: steps.cache-foreman.outputs.cache-hit != 'true'
run: gem install foreman

- name: Fix dependency for libyaml-dev
run: sudo apt install libyaml-dev -y

# Follow https://github.com/pnpm/action-setup?tab=readme-ov-file#use-cache-to-reduce-installation-time
- name: Setup pnpm
uses: pnpm/action-setup@v4
Expand Down Expand Up @@ -255,13 +255,13 @@ jobs:
SERVER_CMD="nice -n -20 taskset -c 1-$((NPROC-1)) bin/prod"
BENCH_CMD="nice -n -10 taskset -c 0 ruby"

echo "SERVER_CMD=$SERVER_CMD" >> $GITHUB_ENV
echo "BENCH_CMD=$BENCH_CMD" >> $GITHUB_ENV
echo "SERVER_CMD=$SERVER_CMD" >> "$GITHUB_ENV"
echo "BENCH_CMD=$BENCH_CMD" >> "$GITHUB_ENV"

# Set Puma workers to match available server CPUs (only if not explicitly set)
if [ -z "$WEB_CONCURRENCY" ]; then
WEB_CONCURRENCY=$((NPROC-1))
echo "WEB_CONCURRENCY=$WEB_CONCURRENCY" >> $GITHUB_ENV
echo "WEB_CONCURRENCY=$WEB_CONCURRENCY" >> "$GITHUB_ENV"
echo "WEB_CONCURRENCY (auto): $WEB_CONCURRENCY"
else
echo "WEB_CONCURRENCY (from input): $WEB_CONCURRENCY"
Expand All @@ -283,8 +283,12 @@ jobs:
working-directory: react_on_rails/spec/dummy
ruby-version: ${{ env.RUBY_VERSION }}

# Pin Core steps that shell out to bundle exec, directly or via
# bin/prod-assets, bin/prod, or benchmarks/bench.rb, to the Core dummy Gemfile.
- name: Prepare Core production assets
if: env.RUN_CORE
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/react_on_rails/spec/dummy/Gemfile
run: |
set -e # Exit on any error
echo "🔨 Building production assets..."
Expand All @@ -299,18 +303,20 @@ jobs:

- name: Start Core production server
if: env.RUN_CORE
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/react_on_rails/spec/dummy/Gemfile
run: |
set -e # Exit on any error
echo "🚀 Starting production server..."
cd react_on_rails/spec/dummy

$SERVER_CMD &
PORT="$BENCHMARK_PORT" $SERVER_CMD &
echo "Server started in background"

# Wait for server to be ready (max 30 seconds)
echo "⏳ Waiting for server to be ready..."
for i in {1..30}; do
if curl -fsS http://localhost:3001 > /dev/null; then
if curl -fsS "http://localhost:${BENCHMARK_PORT}" > /dev/null; then
echo "✅ Server is ready and responding"
exit 0
fi
Expand All @@ -325,14 +331,18 @@ jobs:
# STEP 4: RUN CORE BENCHMARKS
# ============================================

# bench.rb shells out to `bundle exec rails routes` from the Core dummy;
# see the BUNDLE_GEMFILE note above the Core production steps.
- name: Execute Core benchmark suite
if: env.RUN_CORE
timeout-minutes: 120
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/react_on_rails/spec/dummy/Gemfile
run: |
set -e # Exit on any error
echo "🏃 Running Core benchmark suite..."

if ! $BENCH_CMD benchmarks/bench.rb; then
if ! BASE_URL="localhost:${BENCHMARK_PORT}" $BENCH_CMD benchmarks/bench.rb; then
echo "❌ ERROR: Benchmark execution failed"
exit 1
fi
Expand Down Expand Up @@ -375,19 +385,19 @@ jobs:
# Kill all server-related processes (safe in isolated CI environment)
pkill -9 -f "ruby|node|foreman|overmind|puma" || true

# Wait for port 3001 to be free
echo "⏳ Waiting for port 3001 to be free..."
# Wait for benchmark port to be free
echo "⏳ Waiting for port ${BENCHMARK_PORT} to be free..."
for _ in {1..10}; do
if ! lsof -ti:3001 > /dev/null 2>&1; then
echo "✅ Port 3001 is now free"
if ! lsof -ti:"${BENCHMARK_PORT}" > /dev/null 2>&1; then
echo "✅ Port ${BENCHMARK_PORT} is now free"
exit 0
fi
sleep 1
done

echo "❌ ERROR: Port 3001 is still in use after 10 seconds"
echo "Processes using port 3001:"
lsof -i:3001 || true
echo "❌ ERROR: Port ${BENCHMARK_PORT} is still in use after 10 seconds"
echo "Processes using port ${BENCHMARK_PORT}:"
lsof -i:"${BENCHMARK_PORT}" || true
exit 1

# ============================================
Expand Down Expand Up @@ -426,13 +436,15 @@ jobs:
echo "🚀 Starting Pro production server..."
cd react_on_rails_pro/spec/dummy

$SERVER_CMD &

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pro benchmark Prepare/Start steps missing BUNDLE_GEMFILE pinning

Medium Severity

The Core benchmark steps all pin BUNDLE_GEMFILE for "Prepare Core production assets", "Start Core production server", and "Execute Core benchmark suite", with a comment explaining that bin/prod-assets, bin/prod, and bench.rb shell out through bundle exec. The Pro equivalents only pin BUNDLE_GEMFILE for "Execute Pro benchmark suite" but omit it for "Prepare Pro production assets" and "Start Pro production server", even though those steps also invoke bin/prod-assets and bin/prod which shell out via bundle exec. Since setup-bundle now internally calls ruby/setup-ruby@v1 (which may persist BUNDLE_GEMFILE across steps), the Pro Prepare and Start steps could resolve against the wrong bundle if the environment retains a stale value from the preceding Core setup-bundle invocation.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bbf71aa. Configure here.

# Foreman assigns PORT=5000 by default; pin it to the benchmark port
# so readiness checks and benchmarks hit the Rails process we started.
PORT="$BENCHMARK_PORT" $SERVER_CMD &
echo "Server started in background"

# Wait for server to be ready (max 30 seconds)
echo "⏳ Waiting for server to be ready..."
for i in {1..30}; do
if curl -fsS http://localhost:3001 > /dev/null; then
if curl -fsS "http://localhost:${BENCHMARK_PORT}" > /dev/null; then
echo "✅ Server is ready and responding"
exit 0
fi
Expand All @@ -447,14 +459,18 @@ jobs:
# STEP 6: RUN PRO BENCHMARKS
# ============================================

# bench.rb shells out to `bundle exec rails routes`; pin the Pro dummy
# Gemfile so those calls use the same bundle as the Pro server.
- name: Execute Pro benchmark suite
if: env.RUN_PRO_RAILS
timeout-minutes: 120
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/react_on_rails_pro/spec/dummy/Gemfile
run: |
set -e
echo "🏃 Running Pro benchmark suite..."

if ! PRO=true $BENCH_CMD benchmarks/bench.rb; then
if ! BASE_URL="localhost:${BENCHMARK_PORT}" PRO=true $BENCH_CMD benchmarks/bench.rb; then
Comment thread
justin808 marked this conversation as resolved.
echo "❌ ERROR: Benchmark execution failed"
exit 1
fi
Expand Down Expand Up @@ -546,12 +562,14 @@ jobs:
# branch — new reports compare against main's own history.
# Feature branches (PRs, dispatch) use --start-point to inherit
# historical data and thresholds from main.
START_POINT_HASH_ARG=""
if [ "${{ github.event_name }}" = "push" ]; then
BRANCH="main"
START_POINT_ARGS=""
elif [ "${{ github.event_name }}" = "pull_request" ]; then
BRANCH="$GITHUB_HEAD_REF"
START_POINT_ARGS="--start-point $GITHUB_BASE_REF --start-point-hash ${{ github.event.pull_request.base.sha }} --start-point-clone-thresholds --start-point-reset"
START_POINT_HASH_ARG="--start-point-hash ${{ github.event.pull_request.base.sha }}"
START_POINT_ARGS="--start-point $GITHUB_BASE_REF $START_POINT_HASH_ARG --start-point-clone-thresholds --start-point-reset"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
BRANCH="${{ github.ref_name }}"
if [ "$BRANCH" = "main" ]; then
Expand All @@ -562,7 +580,8 @@ jobs:
START_POINT_HASH=$(gh api "repos/${{ github.repository }}/compare/main...$BRANCH" --jq '.merge_base_commit.sha' || true)
if [ -n "$START_POINT_HASH" ]; then
echo "Found merge-base via API: $START_POINT_HASH"
START_POINT_ARGS="--start-point main --start-point-hash $START_POINT_HASH --start-point-clone-thresholds --start-point-reset"
START_POINT_HASH_ARG="--start-point-hash $START_POINT_HASH"
START_POINT_ARGS="--start-point main $START_POINT_HASH_ARG --start-point-clone-thresholds --start-point-reset"
else
echo "⚠️ Could not find merge-base with main via GitHub API, continuing without hash"
START_POINT_ARGS="--start-point main --start-point-clone-thresholds --start-point-reset"
Expand Down Expand Up @@ -640,7 +659,11 @@ jobs:
# when the pinned hash isn't in its DB. A single combined pattern
# avoids false-positive matches across unrelated lines.
if [ $BENCHER_EXIT_CODE -ne 0 ] && grep -q "Head Version.*not found" "$BENCHER_STDERR" && ! grep -qiE "alert|threshold violation|boundary violation" "$BENCHER_STDERR"; then
RETRY_ARGS=$(echo "$START_POINT_ARGS" | sed 's/--start-point-hash [^ ]*//')
if [ -n "$START_POINT_HASH_ARG" ]; then
RETRY_ARGS="${START_POINT_ARGS/$START_POINT_HASH_ARG/}"
else
RETRY_ARGS="$START_POINT_ARGS"
fi
if [ "$RETRY_ARGS" != "$START_POINT_ARGS" ]; then
echo ""
echo "⚠️ Start-point hash not found in Bencher — retrying without --start-point-hash (will use latest baseline)"
Expand Down Expand Up @@ -745,12 +768,13 @@ jobs:
--state open \
--limit 1 \
--json number \
--jq '.[0].number // empty')
--jq ".[0].number // empty")

# Build the benchmark summary snippet (defensive: don't let column failure block alerting)
SUMMARY=""
if [ -f bench_results/summary.txt ]; then
FORMATTED=$(column -t -s $'\t' "bench_results/summary.txt" 2>/dev/null) || FORMATTED=$(cat "bench_results/summary.txt")
# shellcheck disable=SC2016
SUMMARY=$(printf '\n### Benchmark Summary\n\n```\n%s\n```' "$FORMATTED")
fi

Expand Down
Loading
Loading