diff --git a/bin/gen_change_record.py b/bin/gen_change_record.py
new file mode 100755
index 0000000000000000000000000000000000000000..ccfa5d32a59259393c7c9e5100b84e74a1f6dece
--- /dev/null
+++ b/bin/gen_change_record.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+
+# Generate a change record for inclusion in an astron-texmf style LaTeX
+# document.
+# Assumes that all the relevant versions in the document have tags which match
+# \d+\.\d+.
+import subprocess
+from dataclasses import dataclass
+from datetime import datetime
+from io import StringIO
+from re import match
+
+
+@dataclass
+class Tag(object):
+    """Represent a git tag"""
+
+    name: str
+    message: str
+
+    @property
+    def rev(self):
+        return subprocess.check_output(["git", "rev-parse", self.name]).decode().strip()
+
+    @property
+    def date(self):
+        for line in (
+            subprocess.check_output(["git", "cat-file", "-p", self.rev])
+            .decode()
+            .strip()
+            .split("\n")
+        ):
+            if line.startswith("tagger"):
+                return datetime.fromtimestamp(int(line.split()[-2]))
+
+
+def escape_latex(text):
+    return text.strip().replace("#", r"\#").replace("&", r"\&")
+
+
+def get_all_tags():
+    return [
+        Tag(*tag.split(None, 1))
+        for tag in subprocess.check_output(["git", "tag", "-n99", "--merged"])
+        .decode()
+        .strip()
+        .split("\n")
+    ]
+
+
+def generate_change_record(tags):
+    output = StringIO()
+    output.write("\setDocChangeRecord{\n")
+    for tag in sorted(tags, key=lambda x: x.name, reverse=True):
+        if match(r"\d+\.\d+", tag.name):
+            output.write(f"    \\addChangeRecord{{{escape_latex(tag.name)}}}")
+            output.write(f"{{{tag.date.strftime('%Y-%m-%d')}}}")
+            output.write(f"{{{escape_latex(tag.message)}}}\n")
+    output.write("}")
+    return output.getvalue()
+
+
+if __name__ == "__main__":
+    print(generate_change_record(get_all_tags()))
diff --git a/changes.make b/changes.make
new file mode 100644
index 0000000000000000000000000000000000000000..257d603fe8f45ad964a4d0c26023c0a4440e00a0
--- /dev/null
+++ b/changes.make
@@ -0,0 +1,4 @@
+.FORCE:
+
+changes.tex: Makefile .FORCE
+	astron-texmf/bin/gen_change_record.py > changes.tex