From 5783f231f8baefd1c80a008c2adc6a467c5be191 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 28 Nov 2023 11:28:40 +0000 Subject: [PATCH] DEV: Introduce `DISCOURSE_ASSET_URL_SALT` (#24596) This value is included when generating static asset URLs. Updating the value will allow site operators to invalidate all asset urls to recover from configuration issues which may have been cached by CDNs/browsers. --- app/assets/javascripts/discourse/ember-cli-build.js | 10 ++++++++++ .../javascripts/discourse/lib/workbox-tree-builder.js | 6 +++++- app/models/javascript_cache.rb | 3 ++- config/discourse_defaults.conf | 5 +++++ config/initializers/assets.rb | 2 +- lib/highlight_js.rb | 3 ++- spec/requests/theme_javascripts_controller_spec.rb | 5 ++++- 7 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js index 6d7a2f33706..16a838cb2df 100644 --- a/app/assets/javascripts/discourse/ember-cli-build.js +++ b/app/assets/javascripts/discourse/ember-cli-build.js @@ -16,6 +16,7 @@ const { Webpack } = require("@embroider/webpack"); const { StatsWriterPlugin } = require("webpack-stats-plugin"); const withSideWatch = require("./lib/with-side-watch"); const RawHandlebarsCompiler = require("discourse-hbr/raw-handlebars-compiler"); +const crypto = require("crypto"); const EMBER_MAJOR_VERSION = parseInt( require("ember-source/package.json").version.split(".")[0], @@ -153,6 +154,13 @@ module.exports = function (defaults) { testStylesheetTree, ]; + const assetCachebuster = process.env["DISCOURSE_ASSET_URL_SALT"] || ""; + const cachebusterHash = crypto + .createHash("md5") + .update(assetCachebuster) + .digest("hex") + .slice(0, 8); + const appTree = compatBuild(app, Webpack, { staticAppPaths: ["static"], packagerOptions: { @@ -160,6 +168,8 @@ module.exports = function (defaults) { devtool: "source-map", output: { publicPath: "auto", + filename: `assets/chunk.[chunkhash].${cachebusterHash}.js`, + chunkFilename: `assets/chunk.[chunkhash].${cachebusterHash}.js`, }, cache: isProduction ? false diff --git a/app/assets/javascripts/discourse/lib/workbox-tree-builder.js b/app/assets/javascripts/discourse/lib/workbox-tree-builder.js index 3babdfb90c0..9bc9c210cbf 100644 --- a/app/assets/javascripts/discourse/lib/workbox-tree-builder.js +++ b/app/assets/javascripts/discourse/lib/workbox-tree-builder.js @@ -29,7 +29,11 @@ module.exports = function generateWorkboxTree() { // Sprockets' default behaviour for these files is disabled via freedom_patches/sprockets.rb. const versionHash = crypto .createHash("md5") - .update(`${versions.join("|")}|${COMPILER_VERSION}`) + .update( + `${versions.join("|")}|${COMPILER_VERSION}|${ + process.env["DISCOURSE_ASSET_URL_SALT"] || "" + }` + ) .digest("hex"); return funnel(mergeTrees(nodes), { diff --git a/app/models/javascript_cache.rb b/app/models/javascript_cache.rb index 1ebfbec15bd..e3dd443dd16 100644 --- a/app/models/javascript_cache.rb +++ b/app/models/javascript_cache.rb @@ -22,7 +22,8 @@ class JavascriptCache < ActiveRecord::Base end def update_digest - self.digest = Digest::SHA1.hexdigest(content) if content_changed? + self.digest = + Digest::SHA1.hexdigest("#{content}|#{GlobalSetting.asset_url_salt}") if content_changed? end def content_cannot_be_nil diff --git a/config/discourse_defaults.conf b/config/discourse_defaults.conf index 9fd3f0208a4..1d1fbd14902 100644 --- a/config/discourse_defaults.conf +++ b/config/discourse_defaults.conf @@ -386,3 +386,8 @@ allow_impersonation = true # The maximum number of characters allowed in a single log line. log_line_max_chars = 160000 + +# this value is included when generating static asset URLs. +# Updating the value will allow site operators to invalidate all asset urls +# to recover from configuration issues which may have been cached by CDNs/browsers. +asset_url_salt = diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 1eb5106f2f8..02e001566c8 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -6,7 +6,7 @@ Rails.application.config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = "2" +Rails.application.config.assets.version = "2-#{GlobalSetting.asset_url_salt}" # Add additional assets to the asset load path. Rails.application.config.assets.paths << "#{Rails.root}/config/locales" diff --git a/lib/highlight_js.rb b/lib/highlight_js.rb index da3c9ad1029..028bc21c522 100644 --- a/lib/highlight_js.rb +++ b/lib/highlight_js.rb @@ -35,7 +35,8 @@ module HighlightJs cache_info = { lang_string: lang_string, - digest: Digest::SHA1.hexdigest(bundle(lang_string.split("|"))), + digest: + Digest::SHA1.hexdigest(bundle(lang_string.split("|")) + "|#{GlobalSetting.asset_url_salt}"), } cache[RailsMultisite::ConnectionManagement.current_db] = cache_info diff --git a/spec/requests/theme_javascripts_controller_spec.rb b/spec/requests/theme_javascripts_controller_spec.rb index 52600fbe01c..093c33fb9b7 100644 --- a/spec/requests/theme_javascripts_controller_spec.rb +++ b/spec/requests/theme_javascripts_controller_spec.rb @@ -167,12 +167,15 @@ RSpec.describe ThemeJavascriptsController do component.save! _, digest = component.baked_js_tests_with_digest + theme_javascript_hash = + component.theme_fields.find_by(upload_id: js_upload.id).javascript_cache.digest + get "/theme-javascripts/tests/#{component.id}-#{digest}.js" expect(response.body).to include( "require(\"discourse/lib/theme-settings-store\").registerSettings(" + "#{component.id}, {\"num_setting\":5,\"theme_uploads\":{\"vendorlib\":" + "\"/uploads/default/test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}/original/1X/#{js_upload.sha1}.js\"},\"theme_uploads_local\":{\"vendorlib\":" + - "\"/theme-javascripts/#{js_upload.sha1}.js?__ws=test.localhost\"}}, { force: true });", + "\"/theme-javascripts/#{theme_javascript_hash}.js?__ws=test.localhost\"}}, { force: true });", ) expect(response.body).to include("assert.ok(true);") ensure