kt-paperclip 8 and ImageMagick 7 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Update kt-paperclip from 7.3.0 to 8.0.0 and make Easy8's Paperclip integration use Debian 13 ImageMagick 7 commands without ImageMagick 6 compatibility.
Architecture: Let kt-paperclip 8 own ImageMagick availability and command selection by deleting Easy8's Paperclip::Helpers#with_imagemagick? override. Keep Easy8-specific attachment lifecycle and processing behavior, but migrate deprecated command APIs and audit each remaining Paperclip patch against 8.0.0. Verify behavior through focused adapter, patch, avatar, and thumbnail tests inside the Debian 13 runtime.
Tech Stack: Ruby 4.0, Rails 8.1, RSpec, kt-paperclip 8.0.0, ImageMagick 7.1.1 on Debian 13, Bundler 4.
File Structure
- Modify
plugins/easyproject/Gemfile: constrain direct dependency tokt-paperclip ~> 8.0.0. - Delete
easy_engines/easy_extensions/easy_patch/paperclip/paperclip_patch.rb: remove legacy standaloneidentifyandconvertprobes completely. - Modify
app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb: usekt-paperclip 8ImageMagick 7 command and geometry APIs. - Modify remaining files under
easy_engines/easy_extensions/easy_patch/paperclip/only where 8.0.0 compatibility requires it; preserve Easy8-specific behavior. - Create
spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb: command and geometry behavior against Paperclip 8 API. - Create
spec/models/easy_avatar_spec.rb: real attachment declaration, style processing contract, and callback behavior. - Create
easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb: prove remaining patches apply to 8.0.0 targets and obsolete helper patch is absent. - Keep
Gemfile.locklocal and untracked; never stage or commit it.
Task 1: Prepare Dedicated Branch and Baseline
Files:
- Inspect: plugins/easyproject/Gemfile:21
- Inspect: Gemfile.lock:446-451,1032
- Inspect: easy_engines/easy_extensions/easy_patch/paperclip/*.rb
- [ ] Step 1: Inspect repository state and prerequisites
Run:
git status --short --branch
git diff
git log --oneline -10
git remote get-url origin
glab auth status
Expected: unrelated untracked files may exist, but no conflicting edits to planned files. Record remote host for glab commands.
- [ ] Step 2: Refresh base and create branch
Run:
git fetch origin
git switch next/minor
git pull --ff-only
git switch --no-track -c feature/692469_update_kt_paperclip_gem origin/next/minor
git status --short --branch
git branch -vv
Expected: current branch is feature/692469_update_kt_paperclip_gem and does not track origin/next/minor.
- [ ] Step 3: Capture baseline dependency graph
Run:
bundle info kt-paperclip
bundle exec ruby -e 'require "paperclip"; puts Paperclip::VERSION'
bundle exec ruby -e 'require "paperclip"; puts Gem.loaded_specs.fetch("kt-paperclip").dependencies.select { |d| d.type == :runtime }.map { |d| "#{d.name} #{d.requirement}" }'
Expected: version 7.3.0; runtime dependencies match local Gemfile.lock.
Task 2: Add ImageMagick 7 API Characterization Specs
Files:
- Create: spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb
- Create: easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb
- [ ] Step 1: Write failing adapter API specs
Create spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb with focused examples that stub command execution, not external processes:
require "easy_extensions/spec_helper"
RSpec.describe EasyExtensions::ImageProcessing::Adapters::ImagemagickAdapter do
describe ".get_geometry" do
subject(:get_geometry) { described_class.get_geometry("image.png") }
let(:geometry) { Paperclip::Geometry.new(100, 50) }
before do
allow(Paperclip::Commands::ImageMagick::GeometryParser)
.to receive(:from_file)
.with("image.png")
.and_return(geometry)
end
it { is_expected.to eq(geometry) }
end
describe ".resize_image_to_fit" do
subject(:resize_image_to_fit) do
described_class.resize_image_to_fit(source.path, 64, 64, dst: destination.path)
end
let(:source) { Tempfile.new(["source", ".png"]) }
let(:destination) { Tempfile.new(["destination", ".png"]) }
before do
allow(Paperclip::Commands::ImageMagick).to receive(:convert)
end
after do
source.close!
destination.close!
end
it "uses kt-paperclip ImageMagick command API" do
resize_image_to_fit
expect(Paperclip::Commands::ImageMagick).to have_received(:convert)
end
end
end
Adjust tempfile setup only if current adapter requires valid image bytes before command dispatch.
- [ ] Step 2: Write failing patch compatibility specs
Create easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb:
require "easy_extensions/spec_helper"
RSpec.describe "Paperclip patches" do
it "does not override ImageMagick availability detection" do
expect(Paperclip::Helpers.ancestors.map(&:name))
.not_to include("EasyPatch::Paperclip::HelpersPatch")
end
it "applies remaining patches to kt-paperclip 8 targets", :aggregate_failures do
class_methods_patch = EasyPatch::Paperclip::GeometryPatch::ClassMethods
expect(Paperclip::Thumbnail.ancestors).to include(EasyPatch::Paperclip::ThumbnailPatch)
expect(Paperclip::HasAttachedFile.ancestors).to include(EasyPatch::Paperclip::HasAttachedFilePatch)
expect(Paperclip::MediaTypeSpoofDetector.ancestors).to include(EasyPatch::Paperclip::MediaTypeSpoofDetectorPatch)
expect(Paperclip::Geometry.singleton_class.ancestors).to include(class_methods_patch)
end
end
Note: assert on the module NAME string for HelpersPatch (ancestors.map(&:name)) because the constant will not exist after Task 4 deletes its only definition — referencing the constant would raise NameError instead of passing.
If EasyPatchManager applies patches with inclusion rather than ancestor-visible prepend, assert the installed wrapped methods instead. Keep assertions tied to observable patch application.
- [ ] Step 3: Run characterization specs and confirm failure
Run:
bundle exec rspec spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb
Expected: failure because 7.3.0 lacks Paperclip::Commands::ImageMagick, helper patch is still installed, or direct adapter calls still use legacy APIs.
Task 3: Update kt-paperclip and Inspect Dependency Cascade
Files:
- Modify: plugins/easyproject/Gemfile:21
- Local only: Gemfile.lock
- [ ] Step 1: Change direct constraint
Change:
to:
- [ ] Step 2: Resolve only requested dependency
Run:
Expected: kt-paperclip 8.0.0; no unrelated direct dependency updates.
- [ ] Step 3: Inspect local lockfile changes
Run:
git diff --no-index /dev/null Gemfile.lock
bundle info kt-paperclip
bundle exec ruby -e 'require "paperclip"; puts Paperclip::VERSION'
bundle exec ruby -e 'require "paperclip"; puts Gem.loaded_specs.fetch("kt-paperclip").dependencies.select { |d| d.type == :runtime }.map { |d| "#{d.name} #{d.requirement}" }'
Expected: root gem is 8.0.0. Any changed transitive lock-only gem must be required by Bundler and documented; any changed explicitly declared gem triggers full dependency-cascade analysis before proceeding.
- [ ] Step 4: Verify Ruby and Rails compatibility in resolved bundle
Run:
bundle exec ruby -e 'spec = Gem.loaded_specs.fetch("kt-paperclip"); puts spec.required_ruby_version; puts RUBY_VERSION'
bundle exec rails runner 'puts [Rails.version, Paperclip::VERSION].join(" / ")'
Expected: required Ruby >= 2.7.0, project Ruby 4.0.6, Rails boot succeeds.
Task 4: Remove Legacy ImageMagick Detection Patch
Files:
- Delete: easy_engines/easy_extensions/easy_patch/paperclip/paperclip_patch.rb
- Test: easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb
- [ ] Step 1: Delete helper patch completely
Delete easy_engines/easy_extensions/easy_patch/paperclip/paperclip_patch.rb. Do not replace it with delegation or fallback logic.
- [ ] Step 2: Verify no Easy8 legacy command probes remain
Run:
rg 'run\("(identify|convert)"|Paperclip\.run|GeometryDetector|command_path' app easy_engines plugins lib config
Expected after all migration tasks: no executable legacy Paperclip calls. Configuration documentation may still mention Redmine's separate thumbnail command and must be assessed independently.
- [ ] Step 3: Verify Paperclip 8 detects ImageMagick 7
Run in Debian 13 runtime:
magick -version
bundle exec rails runner 'abort "ImageMagick 7 not detected" unless Paperclip::Commands::ImageMagick::VersionDetector.detected_version == 7; puts 7'
Expected: ImageMagick 7.1.1-43 family and output 7.
Task 5: Migrate Adapter to ImageMagick 7 APIs
Files:
- Modify: app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb:10-92
- Test: spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb
- [ ] Step 1: Replace deprecated geometry detector
Change .get_geometry to:
def get_geometry(src)
geometry = ::Paperclip::Commands::ImageMagick::GeometryParser.from_file(src)
geometry || raise(EasyExtensions::ImageProcessing::AdapterProcessException)
rescue ::Paperclip::Errors::NotIdentifiedByImageMagickError
raise EasyExtensions::ImageProcessing::AdapterProcessException
end
- [ ] Step 2: Route conversion through Paperclip 8 command API
Replace creation of Paperclip::Thumbnail solely to call protected/legacy convert with direct calls to:
Build parameters with Terrapin placeholders rather than embedding source/destination shell quoting. For example:
parameters = [":source", "-auto-orient", "-strip", "-resize", %(") + geometry.to_s + %(")]
parameters << crop_option(crop) if crop
parameters << ":destination"
::Paperclip::Commands::ImageMagick.convert(
parameters.join(" "),
source: File.expand_path(src),
destination: "#{format}#{File.expand_path(options[:dst] || src)}"
)
Preserve existing resize, crop, format, PDF conversion, and exception translation semantics. Do not add convert fallback.
- [ ] Step 3: Run adapter specs
Run:
Expected: PASS and command expectation targets Paperclip::Commands::ImageMagick.convert.
- [ ] Step 4: Run RuboCop for adapter and spec
Run:
bundle exec rubocop app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb
Expected: no offenses.
Task 6: Audit and Modernize Remaining Paperclip Patches
Files:
- Modify if needed: easy_engines/easy_extensions/easy_patch/paperclip/thumbnail_patch.rb
- Modify if needed: easy_engines/easy_extensions/easy_patch/paperclip/has_attached_file_patch.rb
- Modify if needed: easy_engines/easy_extensions/easy_patch/paperclip/geometry_patch.rb
- Modify if needed: easy_engines/easy_extensions/easy_patch/paperclip/media_type_spoof_detector_patch.rb
- Test: easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb
- [ ] Step 1: Compare each patch target with installed 8.0.0 source
Run:
bundle show kt-paperclip
bundle exec ruby -e 'require "paperclip"; puts Paperclip::Thumbnail.instance_method(:make).source_location; puts Paperclip::HasAttachedFile.instance_method(:define).source_location; puts Paperclip::MediaTypeSpoofDetector.instance_method(:spoofed?).source_location; puts Paperclip::Geometry.method(:from_file).source_location'
Expected: all target constants and methods exist. Record source paths and compare signatures/private state used by patches.
- [ ] Step 2: Replace
alias_method_chainonly where Paperclip 8 breaks patch installation
Use minimal prepend modules when required. Example for HasAttachedFile:
module EasyPatch
module Paperclip
module HasAttachedFilePatch
def define
super
return unless EasyExtensions::EasyProjectSettings.enable_copying_easy_images_to_public
define_easy_assets_callbacks_methods
add_easy_assets_callbacks
end
private
def add_easy_assets_callbacks
@klass.after_save :copy_to_public
@klass.after_destroy :remove_from_public
end
def define_easy_assets_callbacks_methods
@klass.define_method(:copy_to_public) { EasyExtensions::EasyAssets.copy_to_public(self) }
@klass.define_method(:remove_from_public) { EasyExtensions::EasyAssets.remove_from_public(self) }
end
end
end
end
Use repository patch-manager registration expected for prepend-capable patches. Do not refactor patches that already apply and pass behavior tests.
- [ ] Step 3: Preserve explicit spoof-detector policy
Keep existing behavior returning false; do not silently enable validation during dependency update. Remove dead commented code if touched by required compatibility changes. Document security policy in MR description.
- [ ] Step 4: Run patch compatibility specs
Run:
Expected: PASS; helper availability override absent; remaining required patches applied.
- [ ] Step 5: Run RuboCop on touched patch files and spec
Run:
bundle exec rubocop easy_engines/easy_extensions/easy_patch/paperclip/thumbnail_patch.rb easy_engines/easy_extensions/easy_patch/paperclip/has_attached_file_patch.rb easy_engines/easy_extensions/easy_patch/paperclip/geometry_patch.rb easy_engines/easy_extensions/easy_patch/paperclip/media_type_spoof_detector_patch.rb easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb
Pass only files actually touched if some patches need no changes. Expected: no offenses.
Task 7: Add Real EasyAvatar Integration Coverage
Files:
- Create: spec/models/easy_avatar_spec.rb
- Inspect fixtures: test/fixtures/files/ or existing image fixtures
- [ ] Step 1: Write real attachment contract specs
Create model examples using an existing small PNG/JPEG fixture:
require "easy_extensions/spec_helper"
RSpec.describe EasyAvatar, type: :model do
describe "image attachment" do
subject(:avatar) do
described_class.new(entity: user, image: Rack::Test::UploadedFile.new(image_path, "image/png"))
end
let(:user) { build_stubbed(:user) }
let(:image_path) { Rails.root.join("test/fixtures/files/image.png") }
it "defines all generated styles", :aggregate_failures do
expect(avatar.image.styles.keys).to contain_exactly(:original, :large, :medium, :small)
expect(avatar.image.options[:processors]).to eq([:cropper])
end
it { is_expected.to be_valid }
end
end
Use a persisted entity only if polymorphic validation or callback behavior requires it. Choose an existing fixture path verified in repository; do not create a large binary fixture.
- [ ] Step 2: Add public-copy callback assertion
Assert EasyAvatar responds to copy_to_public and remove_from_public when copying is enabled, proving HasAttachedFile patch behavior rather than implementation internals.
- [ ] Step 3: Run model spec
Run:
Expected: PASS without deprecation warnings from GeometryDetector, Paperclip.run, or command_path.
- [ ] Step 4: Run RuboCop
Run:
Expected: no offenses.
Task 8: Verify Real ImageMagick 7 Processing Paths
Files: - No committed file expected unless a failing check exposes required code changes.
- [ ] Step 1: Confirm bundle installation
Run:
Expected: exit code 0, kt-paperclip 8.0.0. Gemfile.lock remains local only.
- [ ] Step 2: Run focused specs
Run:
bundle exec rspec spec/models/easy_avatar_spec.rb spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb spec/models/attachment_spec.rb spec/utils/easy_extensions/easy_assets_spec.rb spec/libs/easy_extensions/avatar_spec.rb
Expected: all pass.
- [ ] Step 3: Exercise actual ImageMagick 7 conversion
Run against an existing image fixture:
bundle exec rails runner 'src = Rails.root.join("test/fixtures/files/image.png"); dst = Tempfile.new(["kt-paperclip-8", ".png"]); EasyExtensions::ImageProcessing::Adapters::ImagemagickAdapter.resize_image_to_fit(src.to_s, 32, 32, dst: dst.path); abort "empty thumbnail" unless File.size?(dst.path); puts dst.path'
Expected: output file exists and is non-empty. Replace fixture path with verified existing PNG path if needed.
- [ ] Step 4: Check for legacy command usage and deprecations
Run:
rg 'run\("(identify|convert)"|Paperclip\.run|GeometryDetector|command_path' app easy_engines plugins lib
Expected: no production matches requiring migration. Any remaining match must be comments, historical migration text, or unrelated Redmine configuration and documented.
- [ ] Step 5: Run all touched-file lint checks
Run:
bundle exec rubocop plugins/easyproject/Gemfile app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb spec/models/easy_avatar_spec.rb easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb
git diff --check
Do not pass plugins/easyproject/Gemfile to RuboCop if project configuration excludes Gemfiles; lint every touched .rb file. Expected: no offenses and no whitespace errors.
Task 9: Review, Commit, Push, and Create MR
Files:
- Commit only intended source and spec files.
- Never commit: Gemfile.lock.
- [ ] Step 1: Review final diff and lock resolution
Run:
git status --short
git diff -- plugins/easyproject/Gemfile app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb easy_engines/easy_extensions/easy_patch/paperclip spec
bundle exec ruby -e 'require "paperclip"; puts Paperclip::VERSION'
Expected: only intended tracked changes; version 8.0.0.
- [ ] Step 2: Stage intended files only
Run explicit git add with actual touched paths. Include deletion of easy_engines/easy_extensions/easy_patch/paperclip/paperclip_patch.rb. Never use git add ..
- [ ] Step 3: Enforce lockfile safety check
Run:
Expected: Gemfile.lock absent. If present, run git restore --staged Gemfile.lock and repeat check.
- [ ] Step 4: Commit
Run:
Expected: commit succeeds with only intended files.
- [ ] Step 5: Verify branch tracking and dry-run push
Run:
git status --short --branch
git branch -vv
git push --dry-run origin HEAD:refs/heads/feature/692469_update_kt_paperclip_gem
Expected destination: exactly refs/heads/feature/692469_update_kt_paperclip_gem.
- [ ] Step 6: Push explicit branch ref
Run:
- [ ] Step 7: Create merge request
Create MR into next/minor with:
Title: chore(gems): update kt-paperclip to 8.0.0 (refs #692469)
Reviewer: merge_request_pool
Assignee: lukasp
Options: remove source branch, squash before merge
MR description must summarize:
kt-paperclip 7.3.0 -> 8.0.0;- Debian 13/ImageMagick 7-only support;
- deleted Easy8 legacy availability patch;
- migrated geometry and conversion APIs;
- remaining patch audit results;
- spoof detection remains intentionally disabled by existing policy;
- exact test and RuboCop commands/results;
-
Gemfile.lockremained local and was not committed. -
[ ] Step 8: Return MR URL and update issue
Post final verification and MR URL to Easy8 issue #692469, then report branch, commit, MR URL, tests, lint, and lockfile safety result.
Self-Review
- Spec coverage: dependency update, patch audit, complete helper-patch deletion, ImageMagick 7-only migration, usage verification, issue/MR workflow all mapped to tasks.
- Scope: one requested root gem, one branch, one commit, one MR. Required integration changes remain in same update.
- Dependency safety: local lockfile resolution inspected but never staged.
- Compatibility: no ImageMagick 6 fallback introduced.
- Security: existing spoof-detector override preserved and surfaced rather than changed implicitly.