-
-
Notifications
You must be signed in to change notification settings - Fork 629
ci: consolidate Ruby setup and Bundler caching #3354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9f3395d
5edb4f7
6176ad6
b40cf13
29e8486
ba7e3a9
2010e82
6545846
921b180
809a7fb
7338de2
3f6f431
e31318f
69e60e1
8b3220b
9b77b6c
03cfb1e
f8b8b17
20b0e18
ebdb399
7689f47
b580a3f
bbf71aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||||||
|
|
||||||
| inputs: | ||||||
| working-directory: | ||||||
|
|
@@ -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 | ||||||
|
|
@@ -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 | ||||||
|
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' | ||||||
|
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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Behavioral change from old implementation: the previous if: inputs.frozen == 'true' && steps.restore-ruby-gems-cache.outputs.cache-hit != 'true'
Consider making
Suggested change
( |
||||||
| cache-version: ${{ inputs.cache-version }} | ||||||
| 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 | ||
|
justin808 marked this conversation as resolved.
|
||
| with: | ||
| ruby-version: ${{ inputs.ruby-version }} | ||
| bundler: ${{ inputs.bundler-version }} | ||
| bundler-cache: false | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explicit |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' | ||
|
justin808 marked this conversation as resolved.
|
||
|
|
||
| steps: | ||
| # ============================================ | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
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 | ||
|
|
@@ -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 | ||
|
|
@@ -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" | ||
|
|
@@ -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..." | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
||
| # ============================================ | ||
|
|
@@ -426,13 +436,15 @@ jobs: | |
| echo "🚀 Starting Pro production server..." | ||
| cd react_on_rails_pro/spec/dummy | ||
|
|
||
| $SERVER_CMD & | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pro benchmark Prepare/Start steps missing BUNDLE_GEMFILE pinningMedium Severity The Core benchmark steps all pin Additional Locations (1)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 | ||
|
|
@@ -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 | ||
|
justin808 marked this conversation as resolved.
|
||
| echo "❌ ERROR: Benchmark execution failed" | ||
| exit 1 | ||
| fi | ||
|
|
@@ -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 | ||
|
|
@@ -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" | ||
|
|
@@ -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)" | ||
|
|
@@ -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 | ||
|
|
||
|
|
||


There was a problem hiding this comment.
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-bundleitself never installs it. Callers usingsetup-rubyfirst get it implicitly; callers that usesetup-bundledirectly (e.g. the Playwright workflow) do not.Consider adding an
install-libyamlinput (default'true') that mirrors the one insetup-ruby, so any direct caller ofsetup-bundleis protected automatically: