feat: rolling_deploy_adapter for seeding previous bundle hashes#3173
feat: rolling_deploy_adapter for seeding previous bundle hashes#3173justin808 wants to merge 23 commits intojg/3122-preseed-renderer-cachefrom
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9cd1657f83
ℹ️ 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".
Greptile SummaryThis PR introduces
Confidence Score: 4/5Safe to merge after addressing the nil-adapter + PREVIOUS_BUNDLE_HASHES early-return bug; remaining findings are style/test-coverage. One P1 defect: the early-return guard allows a nil-adapter path that silently fails with NoMethodError and produces a misleading doctor message. This affects any operator who sets PREVIOUS_BUNDLE_HASHES without an adapter, and it is untested. The P2 absolute-symlink inconsistency is low-risk for typical same-filesystem deploys. All other pieces — configuration validation, graceful degradation, doctor probes, deduplication logic — are well-implemented and tested. react_on_rails_pro/lib/react_on_rails_pro/rolling_deploy_cache_stager.rb (early-return guard + symlink mode), react_on_rails/lib/react_on_rails/doctor.rb (misleading 'env override will be used' message) Important Files Changed
Sequence DiagramsequenceDiagram
participant AP as AssetsPrecompile.call
participant PS as PreSeedRendererCache.call
participant RD as RollingDeployCacheStager.call
participant DA as rolling_deploy_adapter
AP->>PS: call(mode: :symlink)
PS->>PS: stage current bundle hashes
PS->>RD: call(cache_dir:, current_hashes:, mode:)
alt adapter nil AND env empty
RD-->>PS: return (no-op)
else adapter present OR PREVIOUS_BUNDLE_HASHES set
RD->>DA: previous_bundle_hashes() [or ENV override]
DA-->>RD: ["abc", "def", ...]
RD->>RD: deduplicate against current_hashes
loop each previous hash
RD->>DA: fetch(hash)
DA-->>RD: {server_bundle:, rsc_bundle:, assets:} or nil
RD->>RD: stage_file(src, dest, mode)
end
end
AP->>AP: publish_current_bundle_if_configured
AP->>DA: upload(current_hash, server_bundle:, rsc_bundle:, assets:)
Reviews (1): Last reviewed commit: "feat: add rolling_deploy_adapter for see..." | Re-trigger Greptile |
Code ReviewThis is a well-structured PR with good overall design — the adapter protocol is clear, error handling degrades gracefully at every layer, test coverage is solid, and the docs are thorough. A few issues worth addressing before merge: Bugs1. Fix: add an explicit guard after hash resolution when adapter is nil, or document that Security2. Shell injection in ControlPlane reference implementation Correctness / Reliability3. 4. No timeout on 5. Race condition in S3 Style / Idioms6. 7. Minor
🤖 Generated with Claude Code |
MUST-FIX (2):
- **Nil adapter + PREVIOUS_BUNDLE_HASHES bug** (greptile P1, claude):
when the env override was set without config.rolling_deploy_adapter,
the stager proceeded past the early-return guard and crashed with
NoMethodError at adapter.fetch. Now warns and returns instead.
Doctor promotes the corresponding check from :info to :warning and
explains that both env and adapter are required.
- **RSC previous-bundle staging path** (codex P2): the renderer routes
RSC requests by rsc_bundle_hash, which differs from server_bundle_hash
and is a separate <cache>/<hash>/<hash>.js entry. The original
protocol merged both bundles under a single hash's directory using
basenames, so the RSC bundle never landed where the renderer looks.
Fix: protocol refactor — each hash is now one bundle's cache entry.
fetch(hash) → { bundle: path, assets: [paths] }
upload(hash, bundle:, assets:)
previous_bundle_hashes → flat list including server AND rsc hashes
AssetsPrecompile.publish_current_bundle_if_configured now calls
upload twice when RSC is enabled (once per hash). Stager stages each
hash at <cache>/<hash>/<hash>.js with its companion assets.
SHOULD-FIX (4):
- **Timeout on adapter.fetch** (claude): wrap with Timeout.timeout
(default 30s) so a hung external store can't block pre-seeding or
assets:precompile indefinitely.
- **Relative symlinks** (greptile P2): stage_file in :symlink mode now
uses the same Pathname#realpath + relative_path_from pattern as
PreSeedRendererCache.make_relative_symlink, matching current-hash
staging and surviving cache-dir moves.
- **Remove stray module_function** (claude): was paired with
private_class_method (an unusual combination that leaks private
instance methods to any include-r). Switched to `def self.` for all
methods, private_class_method for helpers.
- **respond_to? instead of methods.include?** (claude): more idiomatic
duck-typing in both Configuration.validate_rolling_deploy_adapter
and Doctor.report_adapter_protocol.
DOC POLISH (3):
- Control Plane reference impl: switched from backtick/system shell
interpolation to Open3.capture2e with array-form args to avoid
shell injection via env-var contents. Returns both server AND RSC
hashes via previous_bundle_hashes.
- Both S3 and Control Plane examples: lazy env-var accessor methods
instead of constants evaluated at require time, so KeyError can't
fire at class-load in environments that don't configure the adapter.
- S3 reference impl: explicit note about the read-modify-write race
in update_manifest!; documents serializing deploys or using
If-Match/ETag as strict-safety alternatives.
- Updated protocol docstrings + all reference impls (S3, Control Plane,
Filesystem) to the new fetch/upload signatures.
Tests: 13 rolling-deploy specs now (was 7) — adds nil-adapter+env
warning, fetch timeout, relative-symlink assertion. Doctor spec
updated for the warn-vs-info change. 38 Pro specs pass, 167/168
doctor specs pass (1 pre-existing unrelated failure).
Refs: #3122, #3167 (PR B), #3173 (this PR)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addressed review feedback in 2e9b3f8MUST-FIX (2):
SHOULD-FIX (4):
DOC POLISH (3):
Tests: 13 rolling-deploy specs (was 7) — adds nil-adapter+env warning, fetch timeout, relative-symlink assertion. Doctor spec updated for the warn-vs-info change. 38 Pro specs pass, 167/168 doctor specs pass (1 pre-existing unrelated failure in `auto_fix_versions`, unchanged). I (Claude Code) addressed the feedback on behalf of @justin808. |
Code Review — PR #3173:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e9b3f8e58
ℹ️ 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".
Post-revision review surfaced 7 new actionable items. All fixed. Code: 1. PREVIOUS_BUNDLE_HASHES path-traversal (claude): env values are used as directory names under the renderer cache; a malicious or mistyped value like "../../../etc" would escape the cache dir. Now reject anything that doesn't match /\A[A-Za-z0-9_\-.]+\z/ with a warning. 2. node_renderer? guard in publish_current_bundle_if_configured (claude): returns early when ExecJS is configured rather than crashing on NodeRenderingPool.server_bundle_hash. Mirrors the guard used by PreSeedRendererCache. 5. Top-level rescue in publish_current_bundle_if_configured (cursor Medium): collect_assets / server_bundle_hash / rsc_bundle_js_file_path can all raise from the setup code that ran before the per-upload rescue. Extracted publish_bundles helper and wrapped the whole publication path with StandardError rescue so a failure degrades next-deploy seeding but doesn't fail this deploy's assets:precompile. 7. Partial-staging cleanup on asset failure (codex P1): previously if stage_file succeeded on the bundle but failed on an asset, the cache held a bundle file without its companion manifests. The renderer would then find the bundle and serve without those manifests, causing hydration breakage instead of the clean 410 retry fallback. Now rm_rf the hash directory on any failure inside seed_previous_hash — restores the 410-retry safety net. Docs: 3. Control Plane fetch example: replaced Dir[tmp/*.json] with an explicit allowlist so stray JSON metadata in the image layer isn't silently picked up. 4. S3 client mutex: @s3 ||= is not thread-safe under concurrent callers (parallel workers running precompile hooks). Example now uses a Mutex-synchronized memoizer with a doc comment. 6. S3 manifest RETENTION alignment: write and read both trim to RETENTION now, so the persisted manifest matches what discovery returns. Added a comment explaining the intent. Tests: 40 rolling-deploy + pre-seed + shim specs (was 38, +2 for path-traversal reject and partial-staging cleanup). Rubocop clean. Refs: #3122, #3173 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addressed post-revision review feedback in fb3de1bSeven new actionable items since `2e9b3f8e5` — all fixed. None skipped. Security / correctness:
Docs:
Tests: 40 specs pass (was 38; +2 for path-traversal reject + partial-staging cleanup). Rubocop clean. Pre-commit hooks pass. I (Claude Code) addressed the feedback on behalf of @justin808. |
Introduces a pluggable adapter protocol so applications can eliminate
the 410→retry cold-start round-trip for previously-deployed bundle
hashes during rolling deploys — not just the current hash that
PreSeedRendererCache already handles.
Protocol (parallel to remote_bundle_cache_adapter):
module MyAdapter
def self.previous_bundle_hashes
def self.fetch(bundle_hash)
def self.upload(bundle_hash, server_bundle:, rsc_bundle: nil, assets:)
end
ReactOnRailsPro.configure do |config|
config.rolling_deploy_adapter = MyAdapter
end
Integration points:
- PreSeedRendererCache.call invokes RollingDeployCacheStager after
staging the current hash(es). Seeds each previous hash's bundle +
companion assets (loadable-stats.json, RSC manifests) into the
same <cache>/<hash>/... layout so hydration stays consistent with
the deployed asset pipeline for that hash.
- AssetsPrecompile.call auto-invokes adapter.upload(current_hash, ...)
after precompile in production-like environments.
- PREVIOUS_BUNDLE_HASHES env var (comma-separated) overrides
previous_bundle_hashes discovery for CI/testing.
- Config validation checks the adapter responds to all three required
methods at configure time.
Error handling (all degrade gracefully — runtime 410-retry remains
the fallback for any seeding failure):
- Adapter not configured: no-op.
- previous_bundle_hashes raises: warn, skip previous-hash seeding.
- fetch returns nil or raises: warn, skip that hash.
- upload raises in precompile: warn, don't fail precompile.
- Hash matching current hash: deduped.
Doctor probes:
- Verifies protocol conformance.
- Probes previous_bundle_hashes with a 3s timeout; reports latency and
hash count.
- Warns on empty list ("upload side has never run").
- Surfaces resolved renderer cache dir + hash subdirs present.
- Echoes PREVIOUS_BUNDLE_HASHES env if set without an adapter.
- Never calls fetch or upload (side effects).
Docs: new docs/pro/rolling-deploy-adapters.md with protocol spec and
three reference implementations (S3, Control Plane, Filesystem).
docs/pro/node-renderer.md points at the new page.
Tests: 7 new specs for RollingDeployCacheStager covering all
invocation paths plus 5 new doctor specs.
Refs: #3122, #3167 (PR B stacked base)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MUST-FIX (2):
- **Nil adapter + PREVIOUS_BUNDLE_HASHES bug** (greptile P1, claude):
when the env override was set without config.rolling_deploy_adapter,
the stager proceeded past the early-return guard and crashed with
NoMethodError at adapter.fetch. Now warns and returns instead.
Doctor promotes the corresponding check from :info to :warning and
explains that both env and adapter are required.
- **RSC previous-bundle staging path** (codex P2): the renderer routes
RSC requests by rsc_bundle_hash, which differs from server_bundle_hash
and is a separate <cache>/<hash>/<hash>.js entry. The original
protocol merged both bundles under a single hash's directory using
basenames, so the RSC bundle never landed where the renderer looks.
Fix: protocol refactor — each hash is now one bundle's cache entry.
fetch(hash) → { bundle: path, assets: [paths] }
upload(hash, bundle:, assets:)
previous_bundle_hashes → flat list including server AND rsc hashes
AssetsPrecompile.publish_current_bundle_if_configured now calls
upload twice when RSC is enabled (once per hash). Stager stages each
hash at <cache>/<hash>/<hash>.js with its companion assets.
SHOULD-FIX (4):
- **Timeout on adapter.fetch** (claude): wrap with Timeout.timeout
(default 30s) so a hung external store can't block pre-seeding or
assets:precompile indefinitely.
- **Relative symlinks** (greptile P2): stage_file in :symlink mode now
uses the same Pathname#realpath + relative_path_from pattern as
PreSeedRendererCache.make_relative_symlink, matching current-hash
staging and surviving cache-dir moves.
- **Remove stray module_function** (claude): was paired with
private_class_method (an unusual combination that leaks private
instance methods to any include-r). Switched to `def self.` for all
methods, private_class_method for helpers.
- **respond_to? instead of methods.include?** (claude): more idiomatic
duck-typing in both Configuration.validate_rolling_deploy_adapter
and Doctor.report_adapter_protocol.
DOC POLISH (3):
- Control Plane reference impl: switched from backtick/system shell
interpolation to Open3.capture2e with array-form args to avoid
shell injection via env-var contents. Returns both server AND RSC
hashes via previous_bundle_hashes.
- Both S3 and Control Plane examples: lazy env-var accessor methods
instead of constants evaluated at require time, so KeyError can't
fire at class-load in environments that don't configure the adapter.
- S3 reference impl: explicit note about the read-modify-write race
in update_manifest!; documents serializing deploys or using
If-Match/ETag as strict-safety alternatives.
- Updated protocol docstrings + all reference impls (S3, Control Plane,
Filesystem) to the new fetch/upload signatures.
Tests: 13 rolling-deploy specs now (was 7) — adds nil-adapter+env
warning, fetch timeout, relative-symlink assertion. Doctor spec
updated for the warn-vs-info change. 38 Pro specs pass, 167/168
doctor specs pass (1 pre-existing unrelated failure).
Refs: #3122, #3167 (PR B), #3173 (this PR)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Post-revision review surfaced 7 new actionable items. All fixed. Code: 1. PREVIOUS_BUNDLE_HASHES path-traversal (claude): env values are used as directory names under the renderer cache; a malicious or mistyped value like "../../../etc" would escape the cache dir. Now reject anything that doesn't match /\A[A-Za-z0-9_\-.]+\z/ with a warning. 2. node_renderer? guard in publish_current_bundle_if_configured (claude): returns early when ExecJS is configured rather than crashing on NodeRenderingPool.server_bundle_hash. Mirrors the guard used by PreSeedRendererCache. 5. Top-level rescue in publish_current_bundle_if_configured (cursor Medium): collect_assets / server_bundle_hash / rsc_bundle_js_file_path can all raise from the setup code that ran before the per-upload rescue. Extracted publish_bundles helper and wrapped the whole publication path with StandardError rescue so a failure degrades next-deploy seeding but doesn't fail this deploy's assets:precompile. 7. Partial-staging cleanup on asset failure (codex P1): previously if stage_file succeeded on the bundle but failed on an asset, the cache held a bundle file without its companion manifests. The renderer would then find the bundle and serve without those manifests, causing hydration breakage instead of the clean 410 retry fallback. Now rm_rf the hash directory on any failure inside seed_previous_hash — restores the 410-retry safety net. Docs: 3. Control Plane fetch example: replaced Dir[tmp/*.json] with an explicit allowlist so stray JSON metadata in the image layer isn't silently picked up. 4. S3 client mutex: @s3 ||= is not thread-safe under concurrent callers (parallel workers running precompile hooks). Example now uses a Mutex-synchronized memoizer with a doc comment. 6. S3 manifest RETENTION alignment: write and read both trim to RETENTION now, so the persisted manifest matches what discovery returns. Added a comment explaining the intent. Tests: 40 rolling-deploy + pre-seed + shim specs (was 38, +2 for path-traversal reject and partial-staging cleanup). Rubocop clean. Refs: #3122, #3173 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Deduplicate previous-hash list before staging so a late failure on a duplicate entry can't trigger seed_previous_hash rollback of an earlier-successful stage (chatgpt-codex-connector[bot] P2). - Filter missing optional assets before rolling_deploy_adapter#upload so a single missing path no longer aborts the whole hash publication and force the next deploy onto the 410 cold-start path (chatgpt-codex-connector[bot] P2). - Tighten RollingDeployCacheStager.bundle_directory to require the candidate be a subdirectory of the cache root (drop dead equality branch) so a future sanitize_hashes regression can't silently land bundles at <cache>/<hash>.js (claude[bot]). - Add explicit StandardError rescue in fetch_payload so adapter-fetch failures keep the targeted "rolling_deploy_adapter#fetch raised" attribution instead of being rewritten by the outer staging rescue (claude[bot]). - Note the ROLLING_DEPLOY_DISCOVERY_TIMEOUT_SECONDS coupling between doctor.rb and RollingDeployCacheStager::DISCOVERY_TIMEOUT_SECONDS (claude[bot]). - Add RSC bundle upload and missing-asset specs for publish_bundles, plus dedup spec for the stager (claude[bot]). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Doc fix: drop inaccurate "and assets:precompile" from fetch timeout comment in docs/pro/rolling-deploy-adapters.md (assets:precompile calls upload, not fetch). - Simplify restore_previous_bundle_directory: drop redundant File.exist?(backup_dir) check on FileUtils.mv (already guarded above). - Rename rolling_deploy_cache_stager_spec context/example for the invalid asset-path case to reflect the actual behavior (early rejection, not rollback). - Distinguish non-required vs required RSC asset failures in warn_missing_asset_payload and warn_non_file_asset_payload so adapter authors can tell adapter-contract violations from missing required RSC files. - Document fallback coupling in doctor's rolling_deploy_discovery_timeout_seconds. - Note S3 update_manifest! per-upload round-trip cost in adapter doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses claude[bot] re-review feedback (PR #3173) with seven quality improvements; no correctness bugs found. * doctor.rb fallback timeout: add cross-package equality spec in rolling_deploy_cache_stager_spec.rb so changing DISCOVERY_TIMEOUT_SECONDS without updating the OSS-side hardcoded fallback fails the spec instead of silently drifting. * RollingDeployCacheStager.bundle_directory: drop the hidden FileUtils.mkdir_p side effect; the call site in #call now creates the cache root once after dedup confirms there is at least one hash to stage. No-op deploys no longer touch disk. * warn_if_missing_loadable_stats: gate on whether the local build actually produces loadable-stats.json, so single-chunk apps do not see a warning per previous hash on every rolling deploy. * warn_if_unavailable_required_rsc_assets: clarify that the partial RSC entry is rejected on every subsequent deploy until a clean precompile overwrites the same hash, not just the next one. * filter_existing_assets: combine missing/non-file warnings into a single line with a reason breakdown so operators see one log entry per skipped batch. * docs/pro/rolling-deploy-adapters.md: document local tmp/rolling-deploy/<hash>/ cleanup responsibility and call out the symlink-mode lifetime constraint.
Drop `:rest` from `ROLLING_DEPLOY_UPLOAD_ALL_KEYWORD_PARAMS`. A positional splat (`*args`) does not absorb keyword arguments when the method also has explicit keyword params, so signatures like `upload(bundle_hash, *args, region: nil)` were accepted at config time but raised `ArgumentError: unknown keyword: :bundle, :assets` at runtime in Ruby 3 — silently degrading rolling-deploy seeding. The plain `*args` (no explicit keywords) and `*args, **kwargs` cases are still accepted via `accepts_upload_options_hash?` and the `:keyrest` branch respectively. Adds positive specs covering both, plus a regression spec for the rejected `*args, region: nil` shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address four optional review findings from claude[bot] on PR #3173: - Tighten TEMPORARY_DIRECTORY_PATTERN to require >=4-digit PID and >=8-char hex suffix so a real bundle hash like `bundle.staging-1-abc123` cannot accidentally match the temp-dir sweep pattern and be removed after STALE_TEMP_DIR_TTL_SECONDS. - Log a per-hash success message after replace_bundle_directory so successful rolling-deploy seeding is visible in production logs (previously every failure path warned but success was silent). - Document why restore_previous_bundle_directory's File.exist? guard intentionally skips the mv on a TOCTOU race (defer to runtime 410-retry rather than overwrite a competing writer; backup_dir gets swept on a later run). - Filter `.staging-...`/`.previous-...` temp dirs out of the doctor's renderer-cache bundle-hash count via a new rolling_deploy_temp_dir_pattern helper, with a fallback constant for when the Pro gem isn't loaded. Adds regression specs for each change.
The lychee pre-push hook was returning cached 502s for two CHANGELOG.md links. Mirror the existing pattern in this file by excluding both, so unrelated markdown link flakiness doesn't block branch pushes. - #785 - https://github.com/K4sku
Address claude[bot] re-review on PR #3173 (six inline + summary items). Comments and clarifying notes (no behavior change): - replace_bundle_directory: document the brief between-mv unavailability window, the kernel-atomic fast path, and the cross-FS slow path so the trade-off is explicit at the call site. - sweep_stale_temporary_directories: explain the no-op-on-missing-cache_dir behavior is desired for Docker rebuilds but skips one cycle on persistent-volume deploys where the cache_dir was wiped in between. - valid_required_rsc_payload?: note that this only checks basename presence; existence is validated downstream by valid_asset_payload?. - handle_missing_adapter: explicit `return nil` after warn so the caller's `return handle_missing_adapter` semantics don't depend on warn's value. - docs/pro/rolling-deploy-adapters.md: new "When upload runs" section documenting that staging/qa/preview/etc. all publish to the adapter and pointing operators at a per-env-initializer escape hatch. Behavior changes (defensive): - seed_previous_hash: explicit FileUtils.mkdir_p(staging_dir) before the first stage_previous_file call so a permission error surfaces with "Failed to seed previous bundle hash" attribution instead of being side-effect-created downstream by copy_file_atomically. New tests: - TEMPORARY_DIRECTORY_PATTERN consistency: assert it matches both temporary_bundle_directory output AND the .previous-<pid>-<hex> backup suffix produced by replace_bundle_directory, guarding against a future rename of either suffix drifting the sweep regex. - configuration: regression spec for kwarg-only upload signature `def upload(bundle:, assets:)` (no positional bundle_hash) — should be rejected. - assets_precompile: assert publish_current_bundle_if_configured *does* publish in non-dev/non-test envs (staging, production, qa, preview, custom), guarding the skip list from being widened by accident. All 33 rolling_deploy_cache_stager_spec, 92 configuration+assets_precompile spec examples pass. RuboCop clean on touched files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6e64d51 to
7537fc2
Compare
Code ReviewOverall: Well-designed, carefully implemented feature with excellent test coverage. The graceful degradation strategy (every failure path degrades to 410-retry, never to a correctness regression) is exactly right for this kind of infrastructure component. The path-sanitization, atomic staging with temp-dir/backup/rollback, and timeout wrappers are all solid. A few things worth discussing: Potential bug —
|
| if result.changes.any? | ||
| checker.add_info(" ℹ️ FIX=true only updates package.json; update Gemfile constraints manually if needed.") | ||
| end | ||
| checker.add_info(" ℹ️ FIX=true only updates package.json; update Gemfile constraints manually if needed.") |
There was a problem hiding this comment.
This unconditionally adds the "FIX=true only updates package.json" info line regardless of whether any versions were actually changed. The original code only showed it when result.changes.any?. As-is it prints even on a no-op run, which is noisy and slightly misleading. Intentional change or accidental removal of the guard?
| checker.add_info(" ℹ️ FIX=true only updates package.json; update Gemfile constraints manually if needed.") | |
| checker.add_info(" ℹ️ FIX=true only updates package.json; update Gemfile constraints manually if needed.") if result.changes.any? |
| # needing to mutate the filesystem itself. | ||
| FileUtils.mkdir_p(cache_dir) | ||
| puts "[ReactOnRailsPro] Seeding previous bundle hashes for rolling deploy: #{hashes.inspect}" | ||
| hashes.each { |hash| seed_previous_hash(adapter, hash, cache_dir, mode) } |
There was a problem hiding this comment.
Hashes are seeded sequentially, each with up to FETCH_TIMEOUT_SECONDS (30s) per fetch. With RSC enabled, a typical setup produces two hashes per deploy (server + RSC bundle). Seeding the previous N deploys means up to 10s (discovery) + 30s × 2N added to pre_seed_renderer_cache in the worst case.
Worth logging a per-hash "fetching..." line before the adapter.fetch call so operators can see which hash is slow rather than watching a silent hang:
| hashes.each { |hash| seed_previous_hash(adapter, hash, cache_dir, mode) } | |
| hashes.each do |hash| | |
| puts "[ReactOnRailsPro] Fetching previous bundle hash #{hash} via rolling_deploy_adapter..." | |
| seed_previous_hash(adapter, hash, cache_dir, mode) | |
| end |
| warn_missing_asset_payload(hash, missing_assets) | ||
| warn_non_file_asset_payload(hash, non_file_assets) | ||
| false | ||
| end |
There was a problem hiding this comment.
The strict-skip policy (reject the whole hash when any asset path is invalid, even a non-required one) is defensively correct, but the warning text for non-required assets doesn't make it obvious that the entire hash is being dropped. An operator seeing "returned non-required asset path(s) that do not exist … Skipping this hash" may be surprised that a single optional chunk file causes a full skip.
Consider adding a brief clause like "Skipping this hash to avoid staging an incomplete bundle directory." to make the causation explicit.
| "Next deploy's rolling-deploy seeding may degrade; precompile continuing." | ||
| end | ||
|
|
||
| def self.publish_bundles(adapter) |
There was a problem hiding this comment.
publish_bundles and all the helpers below it (filter_existing_assets, warn_skipped_invalid_assets, warn_if_unavailable_required_rsc_assets, required_rsc_asset_basenames, publish_bundle, upload_bundle) are public class methods on a Singleton, with no intended external callers. RollingDeployCacheStager marks its analogous helpers private_class_method throughout. Worth applying the same pattern here to lock down the public surface of AssetsPrecompile.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7537fc2ff3
ℹ️ 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".
| # widths (`\d{4,}` PID, `[0-9a-f]{8,}` random) defeat false positives where | ||
| # a real bundle hash happens to end with `.staging-<digits>-<hex>` — without | ||
| # them, a hash like `bundle.staging-1-abc123` could match and be swept. | ||
| TEMPORARY_DIRECTORY_PATTERN = /\.(?:staging|previous)-\d{4,}-[0-9a-f]{8,}\z/ |
There was a problem hiding this comment.
Match temp-dir regex to actual PID width
Update TEMPORARY_DIRECTORY_PATTERN so it matches temp dirs generated by this module for all process IDs. temporary_bundle_directory and backup names embed Process.pid without padding, but the regex requires \d{4,}; in common container/runtime setups where PID is 1..999, entries like abc123.staging-1-deadbeefcafe are never recognized as temporary, so stale staging/backup directories are never swept and can accumulate indefinitely.
Useful? React with 👍 / 👎.
Summary
Adds a pluggable
config.rolling_deploy_adapterprotocol that eliminates 410→retry cold-start round-trips for previously-deployed bundle hashes during rolling deploys — complementing the current-hash seeding already handled by PRs #3124 + #3167.Stacked on #3167 (PR B). Target base is `jg/3122-unify-renderer-cache-staging`; will rebase onto `master` once PRs A + B merge. Refs #3122. Design doc previously shared: see earlier session context.
Why
PR A seeded the current hash. PR B unified copy/symlink staging. But during a rolling deploy:
Pre-seeding only the current hash leaves `abc` cold on new renderers → every draining-version request hits the 410 retry path. This PR closes that gap.
Protocol
Parallel in shape to `remote_bundle_cache_adapter` — same duck-typed module pattern, same validation at configure time.
The loadable-stats wrinkle
Each bundle hash must carry its own companion `loadable-stats.json` and RSC manifests (built in lockstep with that bundle). Otherwise the renderer emits HTML referencing chunk URLs for the wrong build → client-side hydration breakage. The `fetch` signature returns bundle + assets together to force callers to think about this; see `docs/pro/rolling-deploy-adapters.md` for the full discussion.
Integration points
Error handling — degrades gracefully
Runtime 410-retry remains the fallback for any seeding failure, so rolling-deploy seeding can never cause a correctness regression:
Doctor (`react_on_rails:doctor`) probes
Doctor never calls `fetch` or `upload` — those have side effects.
Docs
Test plan
Relationship to other adapters
Complementary — configure both if useful.
🤖 Generated with Claude Code
Note
Medium Risk
Adds new Pro deploy-time caching/publishing behavior (
assets:precompileuploads;pre_seed_renderer_cachefetches/stages) and a sizeable new stager with filesystem moves/timeouts, which could affect deploy reliability and cache correctness if misconfigured.Overview
Adds a new Pro
config.rolling_deploy_adapterprotocol (previous_bundle_hashes,fetch,upload) to pre-seed previously deployed bundle hashes (and companionloadable-stats.json/RSC manifests) into the Node Renderer cache to avoid 410→retry during rolling deploys.Integrates this into the build/deploy flow:
PreSeedRendererCachenow invokesRollingDeployCacheStager(withPREVIOUS_BUNDLE_HASHESoverride, hash sanitization, timeouts, and safe staging via temp dirs/rollback), andAssetsPrecompilebest-effort publishes current bundle hashes + assets viaadapter.uploadin non-dev/test environments.Extends
react_on_rails:doctorto validate adapter protocol/latency and report cache-dir status, adds adapter configuration validation (includinguploadsignature checks), refactors cache staging helpers (centralstage_file, symlink logic) and ensuresloadable-stats.jsonis included when present; updates docs/sidebar/changelog and adds comprehensive specs.Reviewed by Cursor Bugbot for commit 7537fc2. Bugbot is set up for automated code reviews on this repo. Configure here.