react_on_rails_pro 17.0.1 → 17.1.0.rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +4 -3
- data/CONTEXT.md +34 -27
- data/Gemfile.development_dependencies +1 -0
- data/Gemfile.lock +18 -3
- data/LICENSE_SETUP.md +42 -9
- data/README.md +12 -1
- data/app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb +69 -95
- data/app/helpers/react_on_rails_pro_helper.rb +178 -45
- data/docs/adr/0001-boot-seed-rolling-deploy-bundles.md +28 -17
- data/lib/react_on_rails_pro/assets_precompile.rb +34 -20
- data/lib/react_on_rails_pro/async_props_emitter.rb +64 -1
- data/lib/react_on_rails_pro/cache.rb +1 -1
- data/lib/react_on_rails_pro/concerns/async_rendering.rb +11 -6
- data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +7 -0
- data/lib/react_on_rails_pro/concerns/stream.rb +17 -9
- data/lib/react_on_rails_pro/configuration.rb +79 -11
- data/lib/react_on_rails_pro/engine.rb +11 -5
- data/lib/react_on_rails_pro/error.rb +5 -0
- data/lib/react_on_rails_pro/license_task_formatter.rb +1 -1
- data/lib/react_on_rails_pro/license_validator.rb +38 -38
- data/lib/react_on_rails_pro/open_telemetry.rb +210 -0
- data/lib/react_on_rails_pro/pre_seed_renderer_cache.rb +269 -35
- data/lib/react_on_rails_pro/renderer_artifact.rb +176 -0
- data/lib/react_on_rails_pro/renderer_artifact_support.rb +287 -0
- data/lib/react_on_rails_pro/renderer_cache_helpers.rb +58 -32
- data/lib/react_on_rails_pro/renderer_http_client.rb +134 -39
- data/lib/react_on_rails_pro/request.rb +249 -118
- data/lib/react_on_rails_pro/rolling_deploy/tarball.rb +48 -18
- data/lib/react_on_rails_pro/rolling_deploy_adapters/http.rb +242 -93
- data/lib/react_on_rails_pro/rolling_deploy_cache_stager.rb +77 -19
- data/lib/react_on_rails_pro/server_rendering_js_code.rb +48 -4
- data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +46 -7
- data/lib/react_on_rails_pro/stream_cache.rb +18 -0
- data/lib/react_on_rails_pro/stream_request.rb +9 -2
- data/lib/react_on_rails_pro/utils.rb +77 -17
- data/lib/react_on_rails_pro/version.rb +1 -1
- data/lib/react_on_rails_pro.rb +1 -0
- data/sig/react_on_rails_pro/configuration.rbs +7 -0
- data/sig/react_on_rails_pro/open_telemetry.rbs +52 -0
- data/sig/react_on_rails_pro/renderer_http_client.rbs +18 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17f3b1f4dbed1f84fc13ee3f3a34720bdbd3dd32d04d758584e4fd1d8d90dd1d
|
|
4
|
+
data.tar.gz: 2708693a5d1cb985b18a5620955000b4b5e9491f84cdaa8a19c00441e411e0a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59c9879ece288731279d23dee866571cf7c370512e60d189b55767d3b780bb976d5f5125a2bf654fbf2a1fb3fc469395d3354e8b23f6b7db1bb9571f56c651d6
|
|
7
|
+
data.tar.gz: 275bd2a78f52a9a5557ad6e72790d645b44aee7b95ce7678fdca725acacc494943da1b1b531343a3b494b92898fa78834278b85f0c97ac5ee9e06a5d16090579
|
data/CLAUDE.md
CHANGED
|
@@ -88,10 +88,11 @@ Order matters. If the base package isn't published first, the chain breaks.
|
|
|
88
88
|
|
|
89
89
|
### License Validation
|
|
90
90
|
|
|
91
|
-
`ReactOnRailsPro::LicenseValidator` runs
|
|
91
|
+
`ReactOnRailsPro::LicenseValidator` runs after Rails initialization via JWT validation.
|
|
92
92
|
|
|
93
|
-
-
|
|
94
|
-
-
|
|
93
|
+
- Rails token sources: `config.license_token`, then `REACT_ON_RAILS_PRO_LICENSE`
|
|
94
|
+
- Node renderer token sources: `licenseToken`, then `REACT_ON_RAILS_PRO_LICENSE`
|
|
95
|
+
- Missing, invalid, and expired licenses are logged without blocking startup
|
|
95
96
|
- License is checked in Pro engine initializer (`lib/react_on_rails_pro/engine.rb`)
|
|
96
97
|
|
|
97
98
|
## Pro CI Workflows
|
data/CONTEXT.md
CHANGED
|
@@ -35,22 +35,31 @@ environment's config and a _snapshot_ of its then-live bundle. Frozen into the
|
|
|
35
35
|
image layer.
|
|
36
36
|
|
|
37
37
|
**Release-time seed** (a.k.a. **boot seed**):
|
|
38
|
-
Pre-seed run by
|
|
39
|
-
|
|
40
|
-
environment's _actually-live_ bundle
|
|
38
|
+
Pre-seed run by a Ruby-capable release, init, or startup step via
|
|
39
|
+
`bundle exec rake react_on_rails_pro:pre_seed_renderer_cache`, resolving the target
|
|
40
|
+
environment's _actually-live_ bundle. A combined Ruby+Node image can run it
|
|
41
|
+
before Node; a Node-only renderer needs a Ruby-capable step with the promoted
|
|
42
|
+
app artifact/config. That step and the renderer must mount the same writable
|
|
43
|
+
shared volume at `RENDERER_SERVER_BUNDLE_CACHE_PATH`, or copy/sync the completed
|
|
44
|
+
cache into the renderer before it starts. Renderer start/readiness waits for
|
|
45
|
+
completion. Without a Ruby-capable step that makes the completed cache available
|
|
46
|
+
to the renderer, use multi-source fallback plus 410 recovery or a combined
|
|
47
|
+
shape.
|
|
41
48
|
_Avoid_: "runtime seed" (ambiguous with the per-request 410 path).
|
|
42
49
|
|
|
43
50
|
**Multi-source seed**:
|
|
44
51
|
A build-time seed whose **pre-seed source** is a _list_ of endpoints (the
|
|
45
52
|
built-in HTTP adapter's `rolling_deploy_previous_urls` accepts more than one).
|
|
46
|
-
Staging
|
|
47
|
-
|
|
53
|
+
Staging can carry bundles then advertised by both staging and production, which
|
|
54
|
+
improves fallback warmth but cannot determine a later production drain.
|
|
48
55
|
|
|
49
56
|
**Promotion model**:
|
|
50
57
|
Production is deployed by promoting the _exact staging image_, not by building a
|
|
51
58
|
fresh production image. Consequence: the image's **build-time seed** used the
|
|
52
|
-
_staging_ pipeline's config and
|
|
53
|
-
production's
|
|
59
|
+
_staging_ pipeline's config and snapshots taken at staging-build time. A
|
|
60
|
+
multi-source configuration can include production's then-live bundle, but it
|
|
61
|
+
still cannot know the production bundle that will be draining at a later
|
|
62
|
+
promotion.
|
|
54
63
|
|
|
55
64
|
**410 fallback**:
|
|
56
65
|
The self-healing but slow per-request cold path: cache miss → `410 Gone` → Rails
|
|
@@ -75,35 +84,33 @@ called **renderer-before-Rails**.
|
|
|
75
84
|
- **Seed correctness (R1)** and **Deploy ordering (R2)** are independent and
|
|
76
85
|
_both_ required. R2 alone (a delay) cannot rescue a wrong seed; R1 alone
|
|
77
86
|
cannot rescue Rails cutting over before the renderer is warm.
|
|
78
|
-
- Under the **promotion model**, a **build-time seed**
|
|
79
|
-
runs in the staging pipeline
|
|
80
|
-
**draining bundle** is only known at the
|
|
81
|
-
-
|
|
82
|
-
image
|
|
83
|
-
|
|
87
|
+
- Under the **promotion model**, a **build-time seed** cannot satisfy R1 for
|
|
88
|
+
every promotion: it runs against snapshots taken in the staging pipeline, but
|
|
89
|
+
production's **draining bundle** is only known at the later promotion.
|
|
90
|
+
- Set staging's **pre-seed source** to both **staging and production**, so the
|
|
91
|
+
image carries each environment's then-live bundle. This keeps staging's own
|
|
92
|
+
rolling deploys warm and improves fallback warmth, but still goes stale across
|
|
93
|
+
_two pending promotions_.
|
|
84
94
|
- The residual staleness window (two images built before either is promoted) is
|
|
85
95
|
closed by a **release-time seed** at promotion, which resolves production's
|
|
86
96
|
actually-live **draining bundle**.
|
|
87
|
-
- Staging seeding prod bundles means staging's _own_ rolling deploys cold-miss
|
|
88
|
-
(staging drains staging's-previous, which is not seeded). Accepted: staging
|
|
89
|
-
SSR performance during deploys is not a goal.
|
|
90
97
|
|
|
91
98
|
### Three layers of defense
|
|
92
99
|
|
|
93
100
|
Correctness is layered; each layer bounds the failure the prior one leaves open.
|
|
94
101
|
|
|
95
|
-
1. **Multi-source build-time seed** —
|
|
96
|
-
if the boot seed
|
|
97
|
-
promotion.)
|
|
102
|
+
1. **Multi-source build-time seed** — carries then-advertised bundles as a
|
|
103
|
+
failure floor if the boot seed cannot reach the live endpoint.
|
|
98
104
|
2. **Boot seed (release-time)** — the correctness path: pulls the _actually-live_
|
|
99
|
-
**draining bundle**
|
|
100
|
-
window.
|
|
105
|
+
**draining bundle** before renderer readiness, closing the
|
|
106
|
+
two-pending-promotions window. Layer 1 remains only as a fallback.
|
|
101
107
|
3. **Deploy ordering (R2)** — renderer live+warm before Rails; readiness gates on
|
|
102
108
|
the boot seed _completing_ (not succeeding — a failed seed degrades to the 410
|
|
103
109
|
fallback rather than wedging the deploy).
|
|
104
110
|
|
|
105
111
|
**Interdependency:** the **boot seed** returns the correct **draining bundle**
|
|
106
|
-
|
|
112
|
+
only when its release-time step completes before renderer readiness and Rails
|
|
113
|
+
cutover (R2). Before new Rails is live,
|
|
107
114
|
the environment's live endpoint is still served by the draining old pods, so it
|
|
108
115
|
advertises the draining hash. If Rails cut over first, the live endpoint would
|
|
109
116
|
advertise the _new_ hash and the boot seed would miss the very bundle it needs.
|
|
@@ -116,11 +123,11 @@ Layer 2 depends on Layer 3.
|
|
|
116
123
|
> **Domain expert:** "Because the **build-time seed** ran in the staging
|
|
117
124
|
> pipeline — the image holds staging's previous bundle, not prod's **draining
|
|
118
125
|
> bundle**. The seed was correct for the wrong environment."
|
|
119
|
-
> **Dev:** "So
|
|
120
|
-
> promoted image
|
|
121
|
-
> **Domain expert:** "
|
|
122
|
-
>
|
|
123
|
-
> seed** against prod's live bundle."
|
|
126
|
+
> **Dev:** "So does staging's **pre-seed source** including staging and
|
|
127
|
+
> production guarantee the promoted image has prod's draining bundle?"
|
|
128
|
+
> **Domain expert:** "No. It carries then-advertised bundles and improves the
|
|
129
|
+
> fallback, but two pending promotions can still go stale. The **release-time
|
|
130
|
+
> seed** against prod's live bundle establishes correctness."
|
|
124
131
|
> **Dev:** "And that's enough?"
|
|
125
132
|
> **Domain expert:** "That's **R1**. You still need **R2** — the new renderer
|
|
126
133
|
> fleet live and warm before Rails cuts over — or you get the 410 storm anyway."
|
data/Gemfile.lock
CHANGED
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (17.0.
|
|
12
|
+
react_on_rails (17.1.0.rc.0)
|
|
13
13
|
addressable
|
|
14
14
|
connection_pool
|
|
15
15
|
execjs (~> 2.5)
|
|
@@ -20,14 +20,14 @@ PATH
|
|
|
20
20
|
PATH
|
|
21
21
|
remote: .
|
|
22
22
|
specs:
|
|
23
|
-
react_on_rails_pro (17.0.
|
|
23
|
+
react_on_rails_pro (17.1.0.rc.0)
|
|
24
24
|
async (>= 2.29)
|
|
25
25
|
async-http (~> 0.95)
|
|
26
26
|
execjs (~> 2.9)
|
|
27
27
|
io-endpoint (~> 0.17.0)
|
|
28
28
|
jwt (>= 2.5, < 4)
|
|
29
29
|
nokogiri (>= 1.12, < 2)
|
|
30
|
-
react_on_rails (= 17.0.
|
|
30
|
+
react_on_rails (= 17.1.0.rc.0)
|
|
31
31
|
|
|
32
32
|
GEM
|
|
33
33
|
remote: https://rubygems.org/
|
|
@@ -256,6 +256,20 @@ GEM
|
|
|
256
256
|
racc (~> 1.4)
|
|
257
257
|
nokogiri (1.19.4-x86_64-linux-gnu)
|
|
258
258
|
racc (~> 1.4)
|
|
259
|
+
opentelemetry-api (1.11.0)
|
|
260
|
+
logger
|
|
261
|
+
opentelemetry-common (0.25.1)
|
|
262
|
+
opentelemetry-api (~> 1.0)
|
|
263
|
+
opentelemetry-registry (0.6.3)
|
|
264
|
+
opentelemetry-api (~> 1.1)
|
|
265
|
+
opentelemetry-sdk (1.13.0)
|
|
266
|
+
logger
|
|
267
|
+
opentelemetry-api (~> 1.1)
|
|
268
|
+
opentelemetry-common (~> 0.20)
|
|
269
|
+
opentelemetry-registry (~> 0.2)
|
|
270
|
+
opentelemetry-semantic_conventions
|
|
271
|
+
opentelemetry-semantic_conventions (1.43.0)
|
|
272
|
+
opentelemetry-api (~> 1.0)
|
|
259
273
|
ostruct (0.6.3)
|
|
260
274
|
package_json (0.2.0)
|
|
261
275
|
pg (1.5.6)
|
|
@@ -481,6 +495,7 @@ DEPENDENCIES
|
|
|
481
495
|
net-http
|
|
482
496
|
net-imap
|
|
483
497
|
net-smtp
|
|
498
|
+
opentelemetry-sdk
|
|
484
499
|
ostruct
|
|
485
500
|
pg
|
|
486
501
|
pry (>= 0.14.1)
|
data/LICENSE_SETUP.md
CHANGED
|
@@ -51,7 +51,21 @@ This change allows your application to start even with license issues, giving yo
|
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
53
|
|
|
54
|
-
###
|
|
54
|
+
### Rails Application Configuration
|
|
55
|
+
|
|
56
|
+
Rails applications can read the token from Rails credentials or another application-owned secret provider:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# config/initializers/react_on_rails_pro.rb
|
|
60
|
+
ReactOnRailsPro.configure do |config|
|
|
61
|
+
config.license_token = Rails.application.credentials.dig(:react_on_rails_pro, :license_token)
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Explicit nonblank configuration takes precedence over `REACT_ON_RAILS_PRO_LICENSE`. Blank configured values fall
|
|
66
|
+
through to the environment variable.
|
|
67
|
+
|
|
68
|
+
### Environment Variable Alternative
|
|
55
69
|
|
|
56
70
|
Set the `REACT_ON_RAILS_PRO_LICENSE` environment variable:
|
|
57
71
|
|
|
@@ -72,8 +86,23 @@ heroku config:set REACT_ON_RAILS_PRO_LICENSE="your_token"
|
|
|
72
86
|
# Add to your CI environment variables if needed
|
|
73
87
|
```
|
|
74
88
|
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
Never commit license tokens to version control. Use Rails credentials, environment variables, or another secure secret
|
|
90
|
+
manager.
|
|
91
|
+
|
|
92
|
+
### Standalone Node Renderer Configuration
|
|
93
|
+
|
|
94
|
+
The standalone Node renderer is a separate process and cannot read Rails credentials. Configure it independently using
|
|
95
|
+
its `licenseToken` option or the same environment-variable fallback:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
reactOnRailsProNodeRenderer({
|
|
99
|
+
// Application-defined secret-manager integration:
|
|
100
|
+
licenseToken: loadLicenseTokenFromYourSecretManager(),
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Explicit nonblank `licenseToken` configuration takes precedence over `REACT_ON_RAILS_PRO_LICENSE`; blank or omitted
|
|
105
|
+
configuration falls back to the environment. Token values are masked in the renderer's sanitized configuration logs.
|
|
77
106
|
|
|
78
107
|
## License Validation and Signals
|
|
79
108
|
|
|
@@ -94,10 +123,12 @@ When no license is present, the application runs in **unlicensed mode**. This is
|
|
|
94
123
|
|
|
95
124
|
No license setup is needed for development. Developers can install and use React on Rails Pro immediately.
|
|
96
125
|
|
|
97
|
-
For production deployments, configure a paid license
|
|
126
|
+
For production deployments, configure a paid license through `config.license_token` or
|
|
127
|
+
`REACT_ON_RAILS_PRO_LICENSE`. Configure a standalone Node renderer separately when you use one.
|
|
98
128
|
|
|
99
129
|
> Migration note: `config/react_on_rails_pro_license.key` is no longer read.
|
|
100
|
-
> If you used that file previously, move the token to `
|
|
130
|
+
> If you used that file previously, move the token to `config.license_token` or
|
|
131
|
+
> `REACT_ON_RAILS_PRO_LICENSE`.
|
|
101
132
|
|
|
102
133
|
### For CI/CD
|
|
103
134
|
|
|
@@ -259,7 +290,9 @@ window.railsContext.rorPro;
|
|
|
259
290
|
|
|
260
291
|
### Warning: "No license found"
|
|
261
292
|
|
|
262
|
-
This is expected behavior in development, test, and CI environments. The application will run in unlicensed mode. For
|
|
293
|
+
This is expected behavior in development, test, and CI environments. The application will run in unlicensed mode. For
|
|
294
|
+
production, ensure the license is available through application configuration or `REACT_ON_RAILS_PRO_LICENSE` in each
|
|
295
|
+
process that validates it.
|
|
263
296
|
|
|
264
297
|
### Error: "Invalid license signature"
|
|
265
298
|
|
|
@@ -279,7 +312,7 @@ This is expected behavior in development, test, and CI environments. The applica
|
|
|
279
312
|
**Solutions:**
|
|
280
313
|
|
|
281
314
|
1. Contact [support@shakacode.com](mailto:support@shakacode.com) to renew your paid license
|
|
282
|
-
2. Update the `REACT_ON_RAILS_PRO_LICENSE`
|
|
315
|
+
2. Update `config.license_token`, the Node renderer's `licenseToken`, or `REACT_ON_RAILS_PRO_LICENSE` with the new token
|
|
283
316
|
|
|
284
317
|
### Error: "License plan is not valid for production use"
|
|
285
318
|
|
|
@@ -333,8 +366,8 @@ Need help?
|
|
|
333
366
|
|
|
334
367
|
## Security Best Practices
|
|
335
368
|
|
|
336
|
-
1. ✅ **Never commit licenses to Git** — Keep license tokens in environment variables or secret managers
|
|
337
|
-
2. ✅ **
|
|
369
|
+
1. ✅ **Never commit licenses to Git** — Keep license tokens in Rails credentials, environment variables, or secret managers
|
|
370
|
+
2. ✅ **Give each validating process access to the token through its own configuration**
|
|
338
371
|
3. ✅ **Use CI secrets for production deployment pipelines**
|
|
339
372
|
4. ✅ **Don't share licenses publicly**
|
|
340
373
|
|
data/README.md
CHANGED
|
@@ -78,12 +78,23 @@ React on Rails Pro works **without a license token** for evaluation, development
|
|
|
78
78
|
|
|
79
79
|
### License Setup
|
|
80
80
|
|
|
81
|
-
Once you have a paid license,
|
|
81
|
+
Once you have a paid license, configure it through Rails credentials:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
ReactOnRailsPro.configure do |config|
|
|
85
|
+
config.license_token = Rails.application.credentials.dig(:react_on_rails_pro, :license_token)
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or set the environment variable:
|
|
82
90
|
|
|
83
91
|
```bash
|
|
84
92
|
export REACT_ON_RAILS_PRO_LICENSE="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
85
93
|
```
|
|
86
94
|
|
|
95
|
+
A standalone Node renderer cannot read Rails credentials, so pass the same token through its `licenseToken` option or
|
|
96
|
+
environment when you use that deployment mode.
|
|
97
|
+
|
|
87
98
|
**📖 Detailed setup instructions**: See [LICENSE_SETUP.md](./LICENSE_SETUP.md) for complete configuration guide, team setup, and troubleshooting.
|
|
88
99
|
|
|
89
100
|
---
|
|
@@ -93,10 +93,12 @@ module ReactOnRailsPro
|
|
|
93
93
|
# mount) must pass distinct prefixes so the Rails route registry
|
|
94
94
|
# doesn't raise on duplicate names.
|
|
95
95
|
def draw_routes(mapper, path:, as_prefix: DEFAULT_ROUTE_PREFIX)
|
|
96
|
-
|
|
96
|
+
route_prefix = path.to_s.dup
|
|
97
|
+
route_prefix.delete_suffix!("/") while route_prefix.end_with?("/")
|
|
98
|
+
mapper.get("#{route_prefix}/manifest",
|
|
97
99
|
to: "react_on_rails_pro/rolling_deploy/bundles#manifest",
|
|
98
100
|
as: :"#{as_prefix}_manifest")
|
|
99
|
-
mapper.get("#{
|
|
101
|
+
mapper.get("#{route_prefix}/bundles/:hash",
|
|
100
102
|
to: "react_on_rails_pro/rolling_deploy/bundles#show",
|
|
101
103
|
constraints: { hash: ROUTE_HASH_PATTERN },
|
|
102
104
|
as: :"#{as_prefix}_bundle")
|
|
@@ -118,15 +120,17 @@ module ReactOnRailsPro
|
|
|
118
120
|
# bundle file.
|
|
119
121
|
BUNDLE_ENTRY_NAME = "bundle.js"
|
|
120
122
|
|
|
121
|
-
PROTOCOL_VERSION =
|
|
123
|
+
PROTOCOL_VERSION = 2
|
|
124
|
+
ARTIFACT_IDENTITY = { scheme: "rorp-v2-sha256", version: 2 }.freeze
|
|
122
125
|
|
|
123
126
|
def manifest
|
|
124
|
-
|
|
127
|
+
artifacts = safe_current_artifacts
|
|
125
128
|
render json: {
|
|
126
|
-
hashes:
|
|
129
|
+
hashes: artifacts.map(&:id),
|
|
127
130
|
rsc_enabled: ReactOnRailsPro.configuration.enable_rsc_support,
|
|
128
131
|
generated_at: Time.now.utc.iso8601,
|
|
129
|
-
protocol_version: PROTOCOL_VERSION
|
|
132
|
+
protocol_version: PROTOCOL_VERSION,
|
|
133
|
+
artifact_identity: ARTIFACT_IDENTITY
|
|
130
134
|
}
|
|
131
135
|
end
|
|
132
136
|
|
|
@@ -137,12 +141,10 @@ module ReactOnRailsPro
|
|
|
137
141
|
# filesystem operation looks at it.
|
|
138
142
|
return head(:not_found) unless SAFE_HASH_PATTERN.match?(hash)
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return head(:not_found) unless match
|
|
144
|
+
artifact = safe_current_artifacts.find { |candidate| candidate.id == hash }
|
|
145
|
+
return head(:not_found) unless artifact
|
|
143
146
|
|
|
144
|
-
|
|
145
|
-
serve_bundle_tarball(bundle_path)
|
|
147
|
+
serve_bundle_tarball(artifact)
|
|
146
148
|
end
|
|
147
149
|
|
|
148
150
|
private
|
|
@@ -188,13 +190,7 @@ module ReactOnRailsPro
|
|
|
188
190
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
189
191
|
end
|
|
190
192
|
|
|
191
|
-
|
|
192
|
-
# so the manifest endpoint can still 200 with an empty hashes list during
|
|
193
|
-
# the brief window after Rails boots but before assets:precompile has
|
|
194
|
-
# produced the bundle on this dyno. Returns `[]` rather than raising so
|
|
195
|
-
# the build-CI side sees "this server has nothing to seed" instead of a
|
|
196
|
-
# 500 that would otherwise show up as a noisy deploy alert.
|
|
197
|
-
def safe_current_bundle_sources
|
|
193
|
+
def safe_current_artifacts
|
|
198
194
|
unless ReactOnRailsPro.configuration.node_renderer?
|
|
199
195
|
Rails.logger.warn(
|
|
200
196
|
"[ReactOnRailsPro::RollingDeploy::BundlesController] " \
|
|
@@ -205,103 +201,81 @@ module ReactOnRailsPro
|
|
|
205
201
|
return []
|
|
206
202
|
end
|
|
207
203
|
|
|
208
|
-
|
|
209
|
-
|
|
204
|
+
artifacts = ReactOnRailsPro::Utils.renderer_artifacts(action_description: "serving rolling-deploy tarball")
|
|
205
|
+
artifacts.select { |artifact| artifact_servable?(artifact) }
|
|
210
206
|
rescue StandardError => e
|
|
211
207
|
Rails.logger.warn(
|
|
212
208
|
"[ReactOnRailsPro::RollingDeploy::BundlesController] " \
|
|
213
|
-
"
|
|
209
|
+
"artifact discovery failed: #{e.class}: #{e.message}. " \
|
|
214
210
|
"Returning empty manifest — verify bundles have been precompiled."
|
|
215
211
|
)
|
|
216
212
|
[]
|
|
217
213
|
end
|
|
218
214
|
|
|
219
|
-
def serve_bundle_tarball(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
215
|
+
def serve_bundle_tarball(artifact)
|
|
216
|
+
# artifact_servable? validates the original source provenance before
|
|
217
|
+
# this point. Materialize only the bytes captured by that validated
|
|
218
|
+
# artifact, then compose and consume the tarball before both bounded
|
|
219
|
+
# temp scopes close. Live webpack paths are never read under a stale ID.
|
|
220
|
+
artifact.with_materialized_files(bundle_name: BUNDLE_ENTRY_NAME) do |bundle, companions|
|
|
221
|
+
entries = { BUNDLE_ENTRY_NAME => bundle }.merge(companions)
|
|
222
|
+
ReactOnRailsPro::RollingDeploy::Tarball.compose_to_tempfile(entries) do |io|
|
|
223
|
+
# Identity correctness requires the operation-scoped artifact byte
|
|
224
|
+
# snapshots above. `send_data` adds one compressed response String;
|
|
225
|
+
# none of these bodies are memoized after this request. This is an
|
|
226
|
+
# explicit peak-memory tradeoff for a simple authenticated endpoint.
|
|
227
|
+
# Tarball::DEFAULT_MAX_SIZE is an extraction-side safety cap, not a
|
|
228
|
+
# compose-side memory limit; a future streaming response can lower
|
|
229
|
+
# peak memory without weakening the immutable-byte contract.
|
|
230
|
+
send_data io.read,
|
|
231
|
+
type: "application/gzip",
|
|
232
|
+
disposition: "inline",
|
|
233
|
+
filename: "#{params[:hash]}.tar.gz"
|
|
234
|
+
end
|
|
233
235
|
end
|
|
234
236
|
end
|
|
235
237
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
# Companions are skipped (with a warning) when they would shadow the
|
|
243
|
-
# bundle entry, collide with another companion's basename, or carry a
|
|
244
|
-
# name that the tarball helper would reject during compose. This
|
|
245
|
-
# matches the publish-side behavior in AssetsPrecompile where missing
|
|
246
|
-
# or unsafe assets degrade rather than fail the build.
|
|
247
|
-
def tarball_entries(bundle_path)
|
|
248
|
-
entries = { BUNDLE_ENTRY_NAME => bundle_path }
|
|
249
|
-
companion_assets.each do |asset_path|
|
|
250
|
-
name = File.basename(asset_path)
|
|
251
|
-
next if skip_companion?(name, asset_path, entries)
|
|
252
|
-
|
|
253
|
-
entries[name] = asset_path
|
|
254
|
-
end
|
|
255
|
-
entries
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def skip_companion?(name, asset_path, entries)
|
|
259
|
-
if name == BUNDLE_ENTRY_NAME
|
|
260
|
-
warn_companion_skipped(
|
|
261
|
-
"companion #{asset_path.inspect} basename collides with bundle entry #{BUNDLE_ENTRY_NAME.inspect}"
|
|
262
|
-
)
|
|
263
|
-
return true
|
|
264
|
-
end
|
|
265
|
-
unless ReactOnRailsPro::RollingDeploy::Tarball::ENTRY_NAME_PATTERN.match?(name)
|
|
266
|
-
warn_companion_skipped(
|
|
267
|
-
"companion #{asset_path.inspect} basename #{name.inspect} is not a safe tarball entry name"
|
|
238
|
+
def artifact_servable?(artifact)
|
|
239
|
+
rails_root = File.realpath(File.expand_path(Rails.root.to_s))
|
|
240
|
+
if artifact.companions.each_key.any? { |name| name.casecmp?(BUNDLE_ENTRY_NAME) }
|
|
241
|
+
Rails.logger.warn(
|
|
242
|
+
"[ReactOnRailsPro::RollingDeploy::BundlesController] artifact #{artifact.id} cannot be served as a " \
|
|
243
|
+
"complete artifact because a companion collides with #{BUNDLE_ENTRY_NAME.inspect}."
|
|
268
244
|
)
|
|
269
|
-
return
|
|
245
|
+
return false
|
|
270
246
|
end
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
247
|
+
return false unless bundle_source_servable?(artifact)
|
|
248
|
+
|
|
249
|
+
invalid = artifact.companions.find do |name, source|
|
|
250
|
+
source.is_a?(RendererArtifact::InlineCompanion) ||
|
|
251
|
+
!ReactOnRailsPro::RollingDeploy::Tarball.safe_entry_name?(name) ||
|
|
252
|
+
!safe_companion_asset_path(source.to_s, rails_root, rails_root)
|
|
277
253
|
end
|
|
254
|
+
return true unless invalid
|
|
255
|
+
|
|
256
|
+
name, source = invalid
|
|
257
|
+
Rails.logger.warn(
|
|
258
|
+
"[ReactOnRailsPro::RollingDeploy::BundlesController] artifact #{artifact.id} cannot be served as a " \
|
|
259
|
+
"complete artifact because #{name.inspect} resolves to unsupported source #{source_label(source)}."
|
|
260
|
+
)
|
|
278
261
|
false
|
|
279
262
|
end
|
|
280
263
|
|
|
281
|
-
def
|
|
282
|
-
|
|
283
|
-
end
|
|
264
|
+
def bundle_source_servable?(artifact)
|
|
265
|
+
return true if File.file?(artifact.bundle)
|
|
284
266
|
|
|
285
|
-
def companion_assets
|
|
286
|
-
rails_root = File.expand_path(Rails.root.to_s)
|
|
287
|
-
rails_root_realpath = File.realpath(rails_root)
|
|
288
|
-
|
|
289
|
-
# `collect_assets` returns the live build's loadable-stats + RSC
|
|
290
|
-
# manifests; missing assets are silently dropped to match the
|
|
291
|
-
# publish-side behavior in AssetsPrecompile.
|
|
292
|
-
ReactOnRailsPro::RendererCacheHelpers.collect_assets
|
|
293
|
-
.map(&:to_s)
|
|
294
|
-
.reject { |p| ReactOnRailsPro::RendererCacheHelpers.http_url?(p) }
|
|
295
|
-
.filter_map do |path|
|
|
296
|
-
safe_companion_asset_path(path, rails_root, rails_root_realpath)
|
|
297
|
-
end
|
|
298
|
-
rescue StandardError => e
|
|
299
267
|
Rails.logger.warn(
|
|
300
|
-
"[ReactOnRailsPro::RollingDeploy::BundlesController] " \
|
|
301
|
-
"
|
|
302
|
-
"
|
|
268
|
+
"[ReactOnRailsPro::RollingDeploy::BundlesController] artifact #{artifact.id} cannot be served as a " \
|
|
269
|
+
"complete artifact because #{BUNDLE_ENTRY_NAME.inspect} resolves to unsupported source " \
|
|
270
|
+
"#{source_label(artifact.bundle)}."
|
|
303
271
|
)
|
|
304
|
-
|
|
272
|
+
false
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def source_label(source)
|
|
276
|
+
return "redacted inline URL" if source.is_a?(RendererArtifact::InlineCompanion)
|
|
277
|
+
|
|
278
|
+
source.to_s.inspect
|
|
305
279
|
end
|
|
306
280
|
|
|
307
281
|
def safe_companion_asset_path(path, rails_root, rails_root_realpath)
|