Project

General

Profile

Actions

Defect #44240

open

Automatic asset precompilation (redmine_detect_update) fails to detect new assets whose files have older mtimes than the existing manifest

Added by Jonathan Cormier about 20 hours ago. Updated about 14 hours ago.

Status:
New
Priority:
Normal
Assignee:
-
Category:
Themes
Target version:
-
Resolution:
Affected version:

Description

Some struggles have been going on trying to get the "new" asset compilation stuff to work automatically.
https://www.redmine.org/boards/2/topics/70963?r=71143

With some help from claude I think I've tracked down the issue I've been seeing.

Environment: Redmine 7.0.0, Propshaft 1.3.2 (Rails 8 asset pipeline)
Note: This affects Redmines 6.0+

Summary:
Redmine 7's Propshaft integration is supposed to auto-recompile assets on boot when it detects changes, via config.assets.redmine_detect_update (config/environments/production.rb). This relies on comparing file modification times, which is unreliable when new asset files (e.g. a newly installed theme or plugin) are copied in a way that preserves an original mtime — the change goes undetected and the assets silently 404 in production until a manual rake assets:precompile is run.

Root cause:

In config/initializers/10-patches.rb, Redmine patches Propshaft::Assembly:

module Propshaft
  Assembly.prepend(Module.new do
    def needs_precompile?
      !config.manifest_path.exist? || manifest_outdated?
    end

    def manifest_outdated?
      !!load_path.asset_files.detect { |f| f.mtime > config.manifest_path.mtime }
    end
  end)
end

and in config/initializers/30-redmine.rb:

Rails.application.config.after_initialize do |app|
  if app.config.assets.redmine_detect_update && app.assets.needs_precompile?
    app.assets.processor.process
  end
end

manifest_outdated? only checks whether any asset file's mtime is newer than the manifest file's mtime. It does not check whether a given asset is actually present as an entry in the manifest, and it does not use content hashes for the detection (only for the digested filename after the fact).

Failure mode:
A file that is genuinely new (never compiled before, no manifest entry) but whose mtime happens to be older than the manifest's mtime — e.g. because it was extracted from an archive/theme zip, or copied with a tool that preserves the original timestamp (rsync -a, docker cp, tar extraction, cp -p, a git checkout of an old commit, etc.) — is invisible to this check. detect finds no file newer than the manifest, manifest_outdated? returns false, and the auto-precompile is skipped, even though the new asset was never added to public/assets/.manifest.json and has never been compiled/copied to the output directory. Requests for it then 404 until someone manually runs assets:precompile.

Reproduction:

  • Boot a fresh Redmine 7 instance (this creates the initial manifest, with mtime = boot time).
  • Install a theme by copying its directory into themes/ using a method that preserves the source's original file mtimes (e.g. extract a theme .zip on one machine, rsync -a/docker cp the extracted files into place — their mtimes reflect when the zip's contents were authored, not "now").
  • Restart the app (or start a new worker process).
  • Observe: the theme appears in Redmine::Themes.themes (directory-scan based, unaffected) and in Administration → Display Settings, and can be selected/saved.
  • However, requesting the theme's compiled assets (/assets/themes/<name>/application-<hash>.css, favicon, etc.) returns 404 — they were never written to public/assets, because needs_precompile? returned false.
  • Manually running bin/rails assets:precompile immediately fixes it.
  • Expected behavior: Any asset file with no corresponding manifest entry should be treated as needing precompilation, regardless of its mtime relative to the manifest.

Suggested fix direction: manifest_outdated? should also check for asset files that have no entry in the existing manifest (comparing logical/digested paths, or content hash, rather than relying solely on mtime > manifest.mtime), e.g.:

def manifest_outdated?
  manifest = Propshaft::Manifest.from_path(config.manifest_path)
  load_path.assets.any? { |asset| manifest[asset.logical_path.to_s].nil? } ||
    !!load_path.asset_files.detect { |f| f.mtime > config.manifest_path.mtime }
end

or more robustly, compare each asset's current content digest against the manifest's recorded digest for that logical path, since mtime-based staleness checks are inherently fragile against any copy method that preserves source timestamps.

Workaround: after installing/updating themes at container startup, explicitly run bin/rails assets:precompile rather than relying on redmine_detect_update.


Related issues

Related to Redmine - Feature #39111: Enable Asset Pipeline Integration using PropshaftClosedMarius BĂLTEANUActions
Actions #1

Updated by Marius BĂLTEANU about 15 hours ago

  • Related to Feature #39111: Enable Asset Pipeline Integration using Propshaft added
Actions #2

Updated by Marius BĂLTEANU about 14 hours ago

Takashi Kato, it makes sense to me, but I didn't test the fix. What do you think?

Actions

Also available in: Atom PDF