diff --git a/stable/duplicate-bugs b/stable/duplicate-bugs
new file mode 100755
index 00000000..fc15e1ef
--- /dev/null
+++ b/stable/duplicate-bugs
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+#
+import sys
+import os
+
+sys.path.append(os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'py3')))
+
+
+from datetime                           import datetime, timedelta
+from argparse                           import ArgumentParser, RawDescriptionHelpFormatter
+from logging                            import basicConfig, DEBUG, INFO, WARNING
+from ktl.log                            import center, cleave, cdebug, cinfo, Clog
+from ktl.launchpad                      import Launchpad
+from ktl.ubuntu                         import Ubuntu
+
+# AppError
+#
+# A general exception that can be raised when an error is encountered in the app.
+#
+class AppError(Exception):
+    # __init__
+    #
+    def __init__(self, error=''):
+        self.msg = error
+
+# Tracking
+#
+class Tracking():
+    '''
+    '''
+    # __init__
+    #
+    def __init__(s, args):
+        s.args = args
+        s.launchpad = Launchpad('start-sru-cycle').service
+        s.project_tracked = 'kernel-sru-workflow'
+        ubuntu  = Ubuntu()
+        series = ubuntu.supported_series_version
+        series.append(ubuntu.development_series_version)
+        s.series = []
+        for ss in sorted(series):
+            s.series.append(ubuntu.db[ss]['name'])
+
+    def get_master(s, project, cycle, series):
+        '''
+        Return the bug id of the master bug
+        '''
+        center(s.__class__.__name__ + '.get_master')
+
+        retval = None
+
+        cdebug('project: %s' % project)
+        cycle = 'kernel-sru-cycle-' + cycle
+        search_tags            = [cycle, series, 'kernel-sru-master-kernel']
+        search_tags_combinator = "All"
+        # A list of the bug statuses that we care about
+        #
+        search_status          = ["New", "In Progress", "Incomplete", "Fix Committed", "Invalid"]
+        # The tracking bugs that we are interested in should have been created recently (days).
+        #
+        search_since           = datetime.utcnow() - timedelta(days=30)
+        lp_project = s.launchpad.projects[project]
+        tasks = lp_project.searchTasks(status=search_status, tags=search_tags, tags_combinator=search_tags_combinator, modified_since=search_since, omit_duplicates=False)
+
+        if len(tasks) == 1:
+            retval = tasks[0].bug.id
+
+        cleave(s.__class__.__name__ + '.get_master')
+        return retval
+
+    def get_derivatives(s, project, bugid):
+        '''
+        Return the list of bug ids that are derivatives or backports of bugid
+        '''
+        center(s.__class__.__name__ + '.get_master')
+
+        retval = []
+
+        cdebug('project: %s' % project)
+        search_tags            = ["kernel-sru-derivative-of-" + str(bugid), "kernel-sru-backport-of-" + str(bugid)]
+        search_tags_combinator = "Any"
+        # A list of the bug statuses that we care about
+        #
+        search_status          = ["New", "In Progress", "Incomplete", "Fix Committed", "Invalid"]
+        # The tracking bugs that we are interested in should have been created recently (days).
+        #
+        search_since           = datetime.utcnow() - timedelta(days=30)
+        lp_project = s.launchpad.projects[project]
+        tasks = lp_project.searchTasks(status=search_status, tags=search_tags, tags_combinator=search_tags_combinator, modified_since=search_since, omit_duplicates=False)
+
+        for task in tasks:
+            retval.append(task.bug.id)
+
+        cleave(s.__class__.__name__ + '.get_master')
+        return retval
+
+    def get_bugs(s, project, cycle, series):
+        master = s.get_master(project, cycle, series)
+        if not master:
+            return []
+        bugs = s.get_derivatives(s.project_tracked, master)
+        bugs.append(master)
+        return bugs
+
+    def get_series(s, lpbug):
+        '''
+            Get series for a given bug
+        '''
+        for series in s.series:
+            if series in lpbug.tags:
+                return series
+        return None
+
+    # main
+    #
+    def main(s):
+        retval = 1
+        try:
+            previous_cycle = s.get_bugs(s.project_tracked, s.args.sru_cycle, s.args.series)
+            next_cycle = s.get_bugs(s.project_tracked, s.args.next_cycle, s.args.series)
+
+            previous_bugs = {}
+            next_bugs = {}
+            for bug in previous_cycle:
+                lpbug = s.launchpad.bugs[bug]
+                package = lpbug.title.split(":")[0]
+                series = s.get_series(lpbug)
+                key = package + ":" + series
+                if previous_bugs.get(key):
+                    raise AppError("duplicate package in previous cycle: %s" % (key))
+                previous_bugs[key] = lpbug
+            for bug in next_cycle:
+                lpbug = s.launchpad.bugs[bug]
+                package = lpbug.title.split(":")[0]
+                series = s.get_series(lpbug)
+                key = package + ":" + series
+                if next_bugs.get(key):
+                    raise AppError("duplicate package in next cycle: %s" % (key))
+                next_bugs[key] = lpbug
+
+            for package in next_bugs:
+                if not previous_bugs.get(package):
+                    raise AppError("could not find package %s in previous cycle" % (package))
+            for package in previous_bugs:
+                if not next_bugs.get(package):
+                    raise AppError("could not find package %s in next cycle" % (package))
+
+            for pkg in previous_bugs:
+                bug = next_bugs[pkg]
+                prev = previous_bugs[pkg]
+                prev.duplicate_of = bug
+                prev.lp_save()
+                print("Marked #{} as duplicate of #{}: {}".format(prev.id, bug.id, bug.title))
+
+            retval = 0
+
+        except AppError as e:
+            print("ERROR: " + str(e), file=sys.stderr)
+
+        # Handle the user presses <ctrl-C>.
+        #
+        except KeyboardInterrupt:
+            print("Aborting ...")
+
+        if retval > 0:
+            print("")
+            print("Due to the above error(s), this script is unable to continue and is terminating.")
+            print("")
+
+        return retval
+
+if __name__ == '__main__':
+    app_description = '''
+    '''
+
+    app_epilog = '''
+    '''
+
+    parser = ArgumentParser(description=app_description, epilog=app_epilog, formatter_class=RawDescriptionHelpFormatter)
+    parser.add_argument('--info',  action='store_true', default=False, help='')
+    parser.add_argument('--debug', action='store_true', default=False, help='')
+    parser.add_argument('--dry-run', action='store_true', default=False, help='')
+    parser.add_argument('--re-run', action='store_true', default=False, help='')
+    parser.add_argument('--sru-cycle', action='store', required=True, help='')
+    parser.add_argument('--next-cycle', action='store', required=True, help='')
+    parser.add_argument('--series', action='store', required=True, help="")
+    args = parser.parse_args()
+
+    # If logging parameters were set on the command line, handle them
+    # here.
+    #
+    Clog.color = True
+    if args.debug:
+        log_format = "%(levelname)s - %(message)s"
+        basicConfig(level=DEBUG, format=log_format)
+        Clog.dbg = True
+    elif args.info:
+        log_format = "%(message)s"
+        basicConfig(level=INFO, format=log_format)
+    else:
+        log_format = "%(message)s"
+        basicConfig(level=WARNING, format=log_format)
+
+    exit(Tracking(args).main())
+
+# vi:set ts=4 sw=4 expandtab syntax=python:
