Skip to content

Commit cd9a8af

Browse files
committed
Add Rails main CI coverage and fix ActiveJob logging
1 parent 68fa27f commit cd9a8af

File tree

14 files changed

+262
-63
lines changed

14 files changed

+262
-63
lines changed

.github/workflows/rails-main.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Rails Main
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 * * *'
6+
workflow_dispatch:
7+
8+
jobs:
9+
rails-main:
10+
name: Rails main integration tests
11+
runs-on: ubuntu-22.04
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Setup Ruby 4.0.1
16+
uses: ruby/setup-ruby@v1
17+
with:
18+
ruby-version: '4.0.1'
19+
bundler-cache: true
20+
21+
- name: Run Rails main integration tests
22+
env:
23+
RAILS_VERSION: latest
24+
run: ./scripts/rails_tests.sh

.github/workflows/test.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ jobs:
137137
name: 'Pilot Test: Latest Ruby / Rails'
138138
runs-on: ubuntu-22.04
139139
env:
140-
RAILS_VERSION: '8.1.1'
140+
RAILS_VERSION: 'latest'
141141
RUBY_VERSION: '4.0.1'
142142
CI: 'true'
143143
BUNDLE_WITHOUT: 'development'
@@ -210,6 +210,10 @@ jobs:
210210
ruby: '3.4'
211211
rails: '8.0.4'
212212

213+
- name: 'Ruby 4.0 / Rails 8.1'
214+
ruby: '4.0.1'
215+
rails: '8.1.2'
216+
213217
# NOTE: Latest Ruby/Rails is tested in the 'pilot' job above with coverage.
214218

215219
env:

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
810
### Changed
911

12+
- **Fix**: ActiveJob integration handles Rails main event reporter subscribers
13+
- **Fix**: Rack error handler avoids deprecated CSRF exception class on Rails main
14+
- **CI**: Added Rails main daily integration run and updated Rails test matrix (7.1.6, 7.2.3, 8.0.4, 8.1.2)
15+
1016
## [0.1.8] - 2026-01-22
1117

1218
- **Fix**: Lograge custom options now appear in request logs

Taskfile.rails.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: '3'
22
run: once
33

44
vars:
5-
RAILS_VERSION: '{{.RAILS_VERSION | default "7.1.5"}}'
5+
RAILS_VERSION: '{{.RAILS_VERSION | default "7.1.6"}}'
66

77
tasks:
88
test:
@@ -16,7 +16,9 @@ tasks:
1616
- cmd: RAILS_VERSION={{.RAILS_VERSION}} FORCE_RECREATE=true ./scripts/rails_tests.sh
1717

1818
test:matrix:
19-
desc: Run Rails integration tests for supported versions (7.1.5 and 8.0.1)
19+
desc: Run Rails integration tests for supported versions (7.1.6, 7.2.3, 8.0.4, 8.1.2)
2020
cmds:
21-
- RAILS_VERSION=7.1.5 ./scripts/rails_tests.sh
22-
- RAILS_VERSION=8.0.1 ./scripts/rails_tests.sh
21+
- RAILS_VERSION=7.1.6 ./scripts/rails_tests.sh
22+
- RAILS_VERSION=7.2.3 ./scripts/rails_tests.sh
23+
- RAILS_VERSION=8.0.4 ./scripts/rails_tests.sh
24+
- RAILS_VERSION=8.1.2 ./scripts/rails_tests.sh

lib/log_struct/integrations/active_job.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ def self.setup(config)
2525
return nil unless config.integrations.enable_activejob
2626

2727
::ActiveSupport.on_load(:active_job) do
28-
# Detach the default text formatter
29-
::ActiveJob::LogSubscriber.detach_from :active_job
28+
if ::ActiveJob::LogSubscriber.respond_to?(:detach_from)
29+
# Detach the default text formatter
30+
::ActiveJob::LogSubscriber.detach_from :active_job
31+
elsif ::ActiveSupport.respond_to?(:event_reporter)
32+
reporter = ::ActiveSupport.event_reporter
33+
reporter.unsubscribe(::ActiveJob::LogSubscriber) if reporter.respond_to?(:unsubscribe)
34+
end
3035

3136
# Attach our structured formatter
3237
Integrations::ActiveJob::LogSubscriber.attach_to :active_job

lib/log_struct/integrations/active_job/log_subscriber.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# typed: strict
22
# frozen_string_literal: true
33

4+
require "active_support/log_subscriber"
45
require_relative "../../enums/source"
56
require_relative "../../enums/event"
67
require_relative "../../log/active_job"
@@ -10,7 +11,7 @@ module LogStruct
1011
module Integrations
1112
module ActiveJob
1213
# Structured logging for ActiveJob
13-
class LogSubscriber < ::ActiveJob::LogSubscriber
14+
class LogSubscriber < ::ActiveSupport::LogSubscriber
1415
extend T::Sig
1516

1617
sig { params(event: ::ActiveSupport::Notifications::Event).void }

lib/log_struct/integrations/rack_error_handler/middleware.rb

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -81,38 +81,40 @@ def call(env)
8181
::Rails.logger.warn(security_log)
8282

8383
[FORBIDDEN_STATUS, IP_SPOOF_HEADERS.dup, [IP_SPOOF_HTML]]
84-
rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
85-
# Create a security log for CSRF error
86-
security_log = Log::Security::CSRFViolation.new(
87-
path: request.path,
88-
http_method: request.method,
89-
source_ip: request.remote_ip,
90-
user_agent: request.user_agent,
91-
referer: request.referer,
92-
request_id: request.request_id,
93-
message: invalid_auth_token_error.message,
94-
timestamp: Time.now
95-
)
96-
LogStruct.error(security_log)
97-
98-
# Report to error reporting service and/or re-raise
99-
context = extract_request_context(env, request)
100-
LogStruct.handle_exception(invalid_auth_token_error, source: Source::Security, context: context)
101-
102-
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
103-
# If we are only logging or reporting these security errors, then return a default response
104-
[FORBIDDEN_STATUS, CSRF_HEADERS.dup, [CSRF_HTML]]
10584
rescue => error
106-
# Extract request context for error reporting
107-
context = extract_request_context(env, request)
108-
109-
# Create and log a structured exception with request context
110-
exception_log = Log.from_exception(Source::Rails, error, context)
111-
LogStruct.error(exception_log)
112-
113-
# Re-raise any standard errors to let Rails or error reporter handle it.
114-
# Rails will also log the request details separately
115-
raise error
85+
if csrf_error?(error)
86+
# Create a security log for CSRF error
87+
security_log = Log::Security::CSRFViolation.new(
88+
path: request.path,
89+
http_method: request.method,
90+
source_ip: request.remote_ip,
91+
user_agent: request.user_agent,
92+
referer: request.referer,
93+
request_id: request.request_id,
94+
message: error.message,
95+
timestamp: Time.now
96+
)
97+
LogStruct.error(security_log)
98+
99+
# Report to error reporting service and/or re-raise
100+
context = extract_request_context(env, request)
101+
LogStruct.handle_exception(error, source: Source::Security, context: context)
102+
103+
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
104+
# If we are only logging or reporting these security errors, then return a default response
105+
[FORBIDDEN_STATUS, CSRF_HEADERS.dup, [CSRF_HTML]]
106+
else
107+
# Extract request context for error reporting
108+
context = extract_request_context(env, request)
109+
110+
# Create and log a structured exception with request context
111+
exception_log = Log.from_exception(Source::Rails, error, context)
112+
LogStruct.error(exception_log)
113+
114+
# Re-raise any standard errors to let Rails or error reporter handle it.
115+
# Rails will also log the request details separately
116+
raise error
117+
end
116118
end
117119
end
118120

@@ -146,6 +148,13 @@ def extract_request_context(env, request = nil)
146148
{error_extracting_context: error.message}
147149
end
148150

151+
sig { params(error: StandardError).returns(T::Boolean) }
152+
def csrf_error?(error)
153+
error_name = error.class.name
154+
error_name == "ActionController::InvalidAuthenticityToken" ||
155+
error_name == "ActionController::InvalidCrossOriginRequest"
156+
end
157+
149158
sig { params(configured_proxies: T.untyped).returns(T.untyped) }
150159
def normalized_trusted_proxies(configured_proxies)
151160
if configured_proxies.nil? || (configured_proxies.respond_to?(:empty?) && configured_proxies.empty?)

rails_test_app/create_app.rb

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
# Silence Ruby warnings early for this process
77
ENV["RUBYOPT"] = "-W0"
88
rails_version = ENV["RAILS_VERSION"] || "7.0"
9+
rails_version = "main" if ["latest", "main"].include?(rails_version)
910

1011
# Map major.minor versions to specific patch versions
1112
# This mapping will be updated by scripts/update_rails_versions.rb
12-
if rails_version.count(".") < 2
13+
if rails_version != "main" && rails_version.count(".") < 2
1314
latest_version = case rails_version
1415
when "7.1"
15-
"7.1.5.1" # Updated by update_rails_versions script
16+
"7.1.6" # Updated by update_rails_versions script
1617
when "7.2"
17-
"7.2.2.1" # Updated by update_rails_versions script
18+
"7.2.3" # Updated by update_rails_versions script
1819
when "8.0"
19-
"8.0.1" # Updated by update_rails_versions script
20+
"8.0.4" # Updated by update_rails_versions script
2021
when "8.1"
21-
"8.1.0.beta1" # Pre-release lane
22+
"8.1.2" # Updated by update_rails_versions script
2223
else
2324
raise "Unrecognized Rails version #{rails_version}"
2425
end
@@ -28,6 +29,7 @@
2829
end
2930

3031
def install_rails_if_missing(version)
32+
return if version == "main"
3133
# Robust detection using RubyGems API
3234
begin
3335
if Gem::Specification.find_all_by_name("rails", version).any?
@@ -116,7 +118,11 @@ def install_rails_if_missing(version)
116118
skip_app_creation = ENV["SKIP_APP_CREATION"] == "true"
117119

118120
# Extract major and minor version for migrations and load_defaults
119-
@rails_major_minor = (rails_version.split(".")[0..1] || []).join(".")
121+
@rails_major_minor = if rails_version == "main"
122+
"8.2"
123+
else
124+
(rails_version.split(".")[0..1] || []).join(".")
125+
end
120126

121127
# Create directories
122128
FileUtils.mkdir_p(RAILS_APP_DIR)
@@ -148,7 +154,9 @@ def copy_template(file, target_path = nil)
148154

149155
# Use version selector underscore to pick the exact rails generator version.
150156
# Run in a temporary empty dir to avoid Rails app loader picking up our repo.
151-
rails_cmd = ["rails", "_#{rails_version}_", "new", RAILS_APP_DIR,
157+
rails_cmd = ["rails"]
158+
rails_cmd << "_#{rails_version}_" unless rails_version == "main"
159+
rails_cmd += ["new", RAILS_APP_DIR,
152160
"--force", "--skip-bundle",
153161
"--skip-git", "--skip-keeps", "--skip-action-cable",
154162
"--skip-sprockets", "--skip-javascript", "--skip-hotwire",
@@ -172,7 +180,14 @@ def copy_template(file, target_path = nil)
172180
gemfile_content = File.read(gemfile_path)
173181

174182
# Ensure Rails gem version matches the requested rails_version
175-
gemfile_content.gsub!(/^\s*gem\s+"rails".*$/, "gem \"rails\", \"~> #{rails_version}\"")
183+
if rails_version == "main"
184+
gemfile_content.gsub!(
185+
/^\s*gem\s+"rails".*$/,
186+
"gem \"rails\", github: \"rails/rails\", branch: \"main\""
187+
)
188+
else
189+
gemfile_content.gsub!(/^\s*gem\s+"rails".*$/, "gem \"rails\", \"~> #{rails_version}\"")
190+
end
176191

177192
# Do not force a Ruby version in the generated app; CI matrix will pick Ruby
178193

scripts/rails_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
66
TEST_APP_DIR="$PROJECT_ROOT/rails_test_app/logstruct_test_app"
77
CREATE_APP_SCRIPT="$PROJECT_ROOT/rails_test_app/create_app.rb"
88
VERSION_FILE="$TEST_APP_DIR/.rails_version"
9-
RAILS_VERSION="${RAILS_VERSION:-"7.1.5.1"}"
9+
RAILS_VERSION="${RAILS_VERSION:-"7.1.6"}"
1010
FORCE_RECREATE="${FORCE_RECREATE:-false}"
1111

1212
# Show what we're testing

scripts/update_rails_versions.rb

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
require "net/http"
99

1010
# Rails versions we support
11-
SUPPORTED_MAJOR_MINOR = ["7.0", "7.1", "7.2", "8.0"]
11+
SUPPORTED_MAJOR_MINOR = ["7.1", "7.2", "8.0", "8.1"]
1212

1313
# Files to update
1414
CREATE_APP_SCRIPT = File.expand_path("../rails_test_app/create_app.rb", __dir__)
@@ -76,13 +76,7 @@ def update_github_workflow(versions)
7676
content = File.read(GITHUB_WORKFLOW)
7777
updated_content = content.dup
7878

79-
# Update the pilot test Rails version
80-
if /RAILS_VERSION:\s*'8\.0[\.\d]*'/.match?(updated_content)
81-
updated_content.gsub!(/RAILS_VERSION:\s*'8\.0[\.\d]*'/, "RAILS_VERSION: '#{versions["8.0"]}'")
82-
puts " Updated pilot test Rails version to #{versions["8.0"]}"
83-
else
84-
puts " Warning: Could not find RAILS_VERSION for pilot test in test.yml"
85-
end
79+
puts " Skipping pilot job Rails version (uses Rails main)"
8680

8781
# Update the matrix versions
8882
versions.each do |major_minor, version|

0 commit comments

Comments
 (0)