diff --git a/modules/steps.py b/modules/steps.py
index e678a4b..3da8984 100644
--- a/modules/steps.py
+++ b/modules/steps.py
@@ -19,6 +19,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 #
 
+import json
 import os
 import shutil
 import re
@@ -80,20 +81,23 @@ def _make_commit_msg(group):
     versions = list(dict.fromkeys(_get_version(p) for p in group['pkgs']))
     return "{}: upgrade {}".format(pn, ",".join(versions))
 
-def _get_changelog(pn, workdir):
-    """Read the changelog generated by devtool upgrade and copy it to workdir.
-    Returns the changelog text or None."""
-    changelog_path = os.path.join(os.environ.get('BUILDDIR', ''),
-                                  'workspace', 'changelogs', '{}.txt'.format(pn))
-    if os.path.isfile(changelog_path):
-        try:
-            with open(changelog_path, 'r', encoding='utf-8', errors='replace') as f:
-                text = f.read()
-            shutil.copy2(changelog_path, os.path.join(workdir, '{}-changelog.txt'.format(pn)))
-            return text
-        except OSError as e:
-            W('Failed to copy changelog for {}: {}'.format(pn, e))
-    return None
+def _get_devtool_json(pn):
+    """Return the metadata devtool upgrade wrote for pn, or None if there
+    is none (no changelog found, or devtool without changelog support)."""
+    path = os.path.join(os.environ.get('BUILDDIR', ''),
+                        'workspace', 'changelogs', '{}.json'.format(pn))
+    if not os.path.exists(path):
+        return None
+    with open(path, 'r', encoding='utf-8') as f:
+        return json.load(f)
+
+
+def _get_changelog_text(metadata):
+    """Return the changelog text described by devtool upgrade metadata."""
+    path = os.path.join(os.environ.get('BUILDDIR', ''),
+                        'workspace', 'changelogs', metadata['changelog_file'])
+    with open(path, 'r', encoding='utf-8', errors='replace') as f:
+        return f.read()
 
 
 def _append_changelog_to_commit_msg(group, opts):
@@ -119,9 +123,12 @@ def _devtool_upgrade(devtool, bb, git, opts, pkg_ctx, workdir):
         devtool_output = devtool.upgrade(pkg_ctx['PN'], pkg_ctx['NPV'], pkg_ctx['NSRCREV'])
         D(" 'devtool upgrade' printed:\n%s" %(devtool_output))
         # Copy changelog before checking for conflicts — it's useful even on failure
-        changelog_text = _get_changelog(pkg_ctx['PN'], workdir)
-        if changelog_text:
-            pkg_ctx['changelog_text'] = changelog_text
+        pkg_ctx['devtool_upgrade_metadata'] = _get_devtool_json(pkg_ctx['PN'])
+        if pkg_ctx['devtool_upgrade_metadata']:
+            pkg_ctx['changelog_text'] = _get_changelog_text(pkg_ctx['devtool_upgrade_metadata'])
+            with open(os.path.join(workdir, '{}-changelog.txt'.format(pkg_ctx['PN'])),
+                      'w', encoding='utf-8') as f:
+                f.write(pkg_ctx['changelog_text'])
         # If devtool failed to rebase patches, it does not fail, but we should
         if 'conflict' in devtool_output:
             raise DevtoolError("Running 'devtool upgrade' for recipe %s failed." %(pkg_ctx['PN']), devtool_output)
