code: def parse_changelog(changes_path):
      """Parse the given changelog path and return `(content, parsed, nyr)`
      where `nyr` is the ' (not yet released)' marker and `parsed` looks like:
  
          [{'body': u'\n(nothing yet)\n\n',
            'verline': u'restify 1.0.1 (not yet released)',
            'version': u'1.0.1'},    # version is parsed out for top section only
           {'body': u'...',
            'verline': u'1.0.0'},
           {'body': u'...',
            'verline': u'1.0.0-rc2'},
           {'body': u'...',
            'verline': u'1.0.0-rc1'}]
  
      A changelog (CHANGES.md) is expected to look like this:
  
          # $project Changelog
  
          ## $next_version (not yet released)
  
          ...
  
          ## $version1
  
          ...
  
          ## $version2
  
          ... and so on
  
      The version lines are enforced as follows:
  
      - The top entry should have a " (not yet released)" suffix. "Should"
        because recovery from half-cutarelease failures is supported.
      - A version string must be extractable from there, but it tries to
        be loose (though strict "X.Y.Z" versioning is preferred). Allowed
  
              ## 1.0.0
              ## my project 1.0.1
              ## foo 1.2.3-rc2
  
        Basically, (a) the " (not yet released)" is stripped, (b) the
        last token is the version, and (c) that version must start with
        a digit (sanity check).
      """
      if not exists(changes_path):
          raise Error("changelog file '%s' not found" % changes_path)
      content = codecs.open(changes_path, "r", "utf-8").read()
  
      parser = re.compile(
          r"^##\s*(?P<verline>[^\n]*?)\s*$(?P<body>.*?)(?=^##|\Z)",
          re.M | re.S,
      )
      sections = parser.findall(content)
  
      # Sanity checks on changelog format.
      if not sections:
          template = "## 1.0.0 (not yet released)\n\n(nothing yet)\n"
          raise Error(
              "changelog '%s' must have at least one section, "
              "suggestion:\n\n%s" % (changes_path, _indent(template))
          )
      nyr = " (not yet released)"
      items = []
      for i, section in enumerate(sections):
          item = {"verline": section[0], "body": section[1]}
          if i == 0:
              # We only bother to pull out 'version' for the top section.
              verline = section[0]
              if verline.endswith(nyr):
                  verline = verline[0 : -len(nyr)]
              version = verline.split()[-1]
              try:
                  int(version[0])
              except ValueError:
                  msg = ""
                  if version.endswith(")"):
                      msg = (
                          " (cutarelease is picky about the trailing %r "
                          "on the top version line. Perhaps you misspelled "
                          "that?)" % nyr
                      )
                  raise Error(
                      "changelog '%s' top section version '%s' is "
                      "invalid: first char isn't a number%s"
                      % (changes_path, version, msg)
                  )
              item["version"] = version
          items.append(item)
  
      return content, items, nyr
  
imports: import sys
  import os
  from os.path import exists, basename, splitext
  import re
  import codecs
  import logging
  import optparse
  import json
label_desc: Use image node_id 8 for calling a global function with example usage: parse_changelog(changes_path) and returns: content,  items,  nyr