f164d5cd5b9cfecc0c35f7e08350a2a768e46457
mspeir
  Wed Sep 16 11:40:45 2026 -0700
Sort attributes in staticPage.lua so the docs build is reproducible, no Redmine

attributes() walked the attribute table with pairs(), which has no defined
order, so any element with two or more attributes came out in a different order
from one build to the next.  Eight identical builds of gb101.md produced two
distinct outputs.  It renders the same either way, but it makes the install
rsync re-push files nobody edited and makes a byte comparison of any docs change
meaningless.  Sorting the keys fixes it; no page changes semantically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git docs/staticPage.lua docs/staticPage.lua
index d6ff5208871..d07680a155d 100644
--- docs/staticPage.lua
+++ docs/staticPage.lua
@@ -21,33 +21,43 @@
         return '&gt;'
       elseif x == '&' then
         return '&amp;'
       elseif x == '"' then
         return '&quot;'
       elseif x == "'" then
         return '&#39;'
       else
         return x
       end
     end)
 end
 
 -- Helper function to convert an attributes table into
 -- a string that can be put into HTML tags.
+-- The keys are sorted first because pairs() has no defined order, so an element
+-- with more than one attribute would otherwise come out in a different order
+-- from one build to the next.  That renders the same, but it makes the output
+-- unreproducible and leaves the install rsync re-pushing files nobody edited.
 local function attributes(attr)
+  local keys = {}
+  for x in pairs(attr) do
+    table.insert(keys, x)
+  end
+  table.sort(keys)
   local attr_table = {}
-  for x,y in pairs(attr) do
+  for _,x in ipairs(keys) do
+    local y = attr[x]
     if y and y ~= "" then
       table.insert(attr_table, ' ' .. x .. '="' .. escape(y,true) .. '"')
     end
   end
   return table.concat(attr_table)
 end
 
 -- Run cmd on a temporary file containing inp and return result.
 local function pipe(cmd, inp)
   local tmp = os.tmpname()
   local tmph = io.open(tmp, "w")
   tmph:write(inp)
   tmph:close()
   local outh = io.popen(cmd .. " " .. tmp,"r")
   local result = outh:read("*all")