diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5eb0ff92..caab78cb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -770,6 +770,45 @@ jobs:
ruby -e "require \"css_inline\"; puts CSSInline.inline(\"
test
\")"
'
+ test-ruby-rails:
+ name: Rails ${{ matrix.rails-version }} / Ruby ${{ matrix.ruby-version }}
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ # The boundaries: the floor the gemspec declares, and the newest.
+ ruby-version: ["3.2", "4.0"]
+ rails-version: ["7.1", "8.0"]
+
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Set up Ruby & Rust
+ uses: oxidize-rb/actions/setup-ruby-and-rust@v1.4.4
+ with:
+ ruby-version: ${{ matrix.ruby-version }}
+ bundler-cache: false
+ cargo-cache: true
+ cache-version: v1-${{ matrix.ruby-version }}
+ working-directory: ./bindings/ruby
+
+ # The wrapper depends on the sibling gem by path, so its extension has to
+ # exist before the wrapper's bundle resolves.
+ - name: Build the native extension
+ run: |
+ bundle install
+ bundle exec rake compile
+ working-directory: ./bindings/ruby
+
+ - name: Run specs
+ run: |
+ bundle install
+ bundle exec rake
+ working-directory: ./bindings/ruby/rails
+ env:
+ RAILS_VERSION: ${{ matrix.rails-version }}
+
test-wasm:
name: WASM module tests
runs-on: ubuntu-22.04
diff --git a/.github/workflows/rails-release.yml b/.github/workflows/rails-release.yml
new file mode 100644
index 00000000..48945a44
--- /dev/null
+++ b/.github/workflows/rails-release.yml
@@ -0,0 +1,77 @@
+name: "[Rails] Release"
+
+on:
+ push:
+ tags:
+ - rails-v*
+ workflow_dispatch:
+ inputs:
+ dry_run:
+ description: "Build the gem without publishing it"
+ type: boolean
+ default: true
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ rails-release:
+ runs-on: ubuntu-24.04
+ env:
+ # A manual run from a branch has no version to release.
+ PUBLISH: ${{ !inputs.dry_run && startsWith(github.ref, 'refs/tags/') }}
+
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: "3.4"
+
+ - name: Extract Version
+ run: echo "version=${GITHUB_REF#refs/tags/rails-v}" >> $GITHUB_ENV
+
+ - name: Check the tag matches the gem version
+ if: startsWith(github.ref, 'refs/tags/')
+ working-directory: ./bindings/ruby/rails
+ run: |
+ gem_version=$(ruby -r ./lib/css_inline/rails/version -e "print CSSInline::Rails::VERSION")
+ if [ "$gem_version" != "${{ env.version }}" ]; then
+ echo "::error::Tag says ${{ env.version }}, version.rb says ${gem_version}"
+ exit 1
+ fi
+
+ - name: Build gem
+ working-directory: ./bindings/ruby/rails
+ run: gem build css_inline-rails.gemspec
+
+ - name: GitHub Release
+ if: env.PUBLISH == 'true'
+ uses: softprops/action-gh-release@v3
+ with:
+ make_latest: false
+ draft: true
+ name: "[Rails] Release ${{ env.version }}"
+ files: ./bindings/ruby/rails/*.gem
+
+ - name: Publish to RubyGems
+ if: env.PUBLISH == 'true'
+ working-directory: ./bindings/ruby/rails
+ run: |
+ mkdir -p $HOME/.gem
+ touch $HOME/.gem/credentials
+ chmod 0600 $HOME/.gem/credentials
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
+ gem push css_inline-rails-*.gem
+ env:
+ GEM_HOST_API_KEY: "${{ secrets.RUBYGEMS_API_KEY }}"
+
+ - name: Upload gem (no publish)
+ if: env.PUBLISH != 'true'
+ uses: actions/upload-artifact@v7
+ with:
+ name: gem
+ path: ./bindings/ruby/rails/*.gem
+ if-no-files-found: error
diff --git a/Justfile b/Justfile
index 3e5dc671..88b954e5 100644
--- a/Justfile
+++ b/Justfile
@@ -14,7 +14,7 @@ release COMPONENT VERSION:
python) CL=bindings/python/CHANGELOG.md; FILES=(bindings/python/Cargo.toml) ;;
java) CL=bindings/java/CHANGELOG.md; FILES=(bindings/java/Cargo.toml bindings/java/build.gradle bindings/java/README.md) ;;
php) CL=bindings/php/CHANGELOG.md; FILES=(bindings/php/Cargo.toml bindings/php/stubs/css_inline.php) ;;
- ruby) CL=bindings/ruby/CHANGELOG.md; FILES=(bindings/ruby/css_inline.gemspec bindings/ruby/ext/css_inline/Cargo.toml bindings/ruby/Gemfile.lock) ;;
+ ruby) CL=bindings/ruby/CHANGELOG.md; FILES=(bindings/ruby/css_inline.gemspec bindings/ruby/ext/css_inline/Cargo.toml bindings/ruby/Gemfile.lock bindings/ruby/rails/lib/css_inline/rails/version.rb) ;;
javascript) CL=bindings/javascript/CHANGELOG.md; FILES=(bindings/javascript/Cargo.toml bindings/javascript/package.json bindings/javascript/wasm/package.json bindings/javascript/npm/*/package.json) ;;
*) echo "unknown component: $C"; exit 1 ;;
esac
@@ -32,15 +32,24 @@ release COMPONENT VERSION:
grep -qiE "$ANCHOR" "$f" || { echo "no version line matching '${PREV}' in $f"; exit 1; }
sed -i "0,/${ANCHOR}/Is/${PREV_RE}/${VERSION}/" "$f"
done
- sed -i "0,/^## \[Unreleased\]$/s//## [Unreleased]\n\n## [${VERSION}] - ${DATE}/" "$CL"
- sed -i "s#compare/${C}-v${PREV_RE}\.\.\.HEAD#compare/${C}-v${VERSION}...HEAD#" "$CL"
- sed -i "/^\[Unreleased\]: /a [${VERSION}]: https://github.com/Stranger6667/css-inline/compare/${C}-v${PREV}...${C}-v${VERSION}" "$CL"
+ bump_changelog() {
+ local cl="$1" tag_prefix="$2"
+ sed -i "0,/^## \[Unreleased\]$/s//## [Unreleased]\n\n## [${VERSION}] - ${DATE}/" "$cl"
+ sed -i "s#compare/${tag_prefix}-v${PREV_RE}\.\.\.HEAD#compare/${tag_prefix}-v${VERSION}...HEAD#" "$cl"
+ sed -i "/^\[Unreleased\]: /a [${VERSION}]: https://github.com/Stranger6667/css-inline/compare/${tag_prefix}-v${PREV}...${tag_prefix}-v${VERSION}" "$cl"
+ }
+ bump_changelog "$CL" "$C"
+ # Tags to create. Ruby also ships the Rails wrapper, which has its own gem,
+ # changelog and release workflow but moves in lockstep with `css_inline`.
+ TAGS=("${C}-v${VERSION}")
if [ "$C" = ruby ]; then
+ bump_changelog bindings/ruby/rails/CHANGELOG.md rails
+ TAGS+=("rails-v${VERSION}")
cargo update -p css-inline --manifest-path bindings/ruby/Cargo.toml
cp bindings/ruby/Cargo.lock bindings/ruby/ext/css_inline/Cargo.lock
fi
git add -u
git commit -m "chore(${C}): Release ${VERSION}"
- git tag "${C}-v${VERSION}"
+ for t in "${TAGS[@]}"; do git tag "$t"; done
git push origin master
- git push origin "${C}-v${VERSION}"
+ for t in "${TAGS[@]}"; do git push origin "$t"; done
diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore
index 5bcde9d4..096d4b81 100644
--- a/bindings/ruby/.gitignore
+++ b/bindings/ruby/.gitignore
@@ -1,2 +1,3 @@
tmp
css_inline.so
+css_inline.bundle
diff --git a/bindings/ruby/rails/.gitignore b/bindings/ruby/rails/.gitignore
new file mode 100644
index 00000000..f1b632e9
--- /dev/null
+++ b/bindings/ruby/rails/.gitignore
@@ -0,0 +1,6 @@
+/.bundle/
+/vendor/
+/pkg/
+Gemfile.lock
+/spec/integration/fixtures/tmp/
+/spec/integration/fixtures/log/
diff --git a/bindings/ruby/rails/.rspec b/bindings/ruby/rails/.rspec
new file mode 100644
index 00000000..49d5710b
--- /dev/null
+++ b/bindings/ruby/rails/.rspec
@@ -0,0 +1 @@
+--format documentation
diff --git a/bindings/ruby/rails/CHANGELOG.md b/bindings/ruby/rails/CHANGELOG.md
new file mode 100644
index 00000000..69eb7a44
--- /dev/null
+++ b/bindings/ruby/rails/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+## [Unreleased]
+
+### Added
+
+- Initial release. An ActionMailer interceptor that inlines CSS into outgoing HTML mail with `css_inline`.
+
+[Unreleased]: https://github.com/Stranger6667/css-inline/compare/rails-v0.21.3...HEAD
diff --git a/bindings/ruby/rails/Gemfile b/bindings/ruby/rails/Gemfile
new file mode 100644
index 00000000..9ce235d8
--- /dev/null
+++ b/bindings/ruby/rails/Gemfile
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gemspec
+
+# The sibling native gem, built from this checkout rather than RubyGems.
+gem "css_inline", path: ".."
+
+# Matches premailer-rails' approach: drive the Rails version from the
+# environment instead of pulling in Appraisal.
+rails_version = ENV.fetch("RAILS_VERSION", "8.0")
+
+if rails_version == "head"
+ gem "rails", github: "rails/rails"
+ gem "propshaft", github: "rails/propshaft"
+else
+ gem "rails", "~> #{rails_version}.0"
+ gem "propshaft"
+end
+
+# Both pipelines are installed so their loaders can be tested against the real
+# thing. Only one can be booted per process, so the integration spec picks with
+# PIPELINE; the loader specs drive both directly.
+gem "sprockets-rails"
+
+gem "rspec", "~> 3.13"
diff --git a/bindings/ruby/rails/README.md b/bindings/ruby/rails/README.md
new file mode 100644
index 00000000..a76dfdfe
--- /dev/null
+++ b/bindings/ruby/rails/README.md
@@ -0,0 +1,81 @@
+# css_inline-rails
+
+Inlines CSS into your Rails emails using [`css_inline`](https://github.com/Stranger6667/css-inline),
+which is 50-100x faster than `premailer` and `roadie`.
+
+```ruby
+# Gemfile
+gem "css_inline-rails"
+```
+
+That is the whole setup. An ActionMailer interceptor is registered automatically, and
+every outgoing HTML message gets its `
+ Lorem ipsum