github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/misc/gen_release.py (about)

     1  #!/usr/bin/env python3
     2  """Script to create Github releases & generate release notes."""
     3  
     4  import json
     5  import logging
     6  import os
     7  import subprocess
     8  import sys
     9  import zipfile
    10  
    11  from third_party.python import colorlog, requests
    12  from third_party.python.absl import app, flags
    13  
    14  logging.root.handlers[0].setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)s: %(message)s'))
    15  
    16  
    17  flags.DEFINE_string('github_token', None, 'Github API token')
    18  flags.DEFINE_string('signer', None, 'Release signer binary')
    19  flags.DEFINE_bool('dry_run', False, "Don't actually do the release, just print it.")
    20  flags.mark_flag_as_required('github_token')
    21  FLAGS = flags.FLAGS
    22  
    23  
    24  PRERELEASE_MESSAGE = """
    25  This is a prerelease version of Please. Bugs and partially-finished features may abound.
    26  
    27  Caveat usor!
    28  """
    29  
    30  
    31  class ReleaseGen:
    32  
    33      def __init__(self, github_token:str, dry_run:bool=False):
    34          self.url = 'https://api.github.com'
    35          self.releases_url = self.url + '/repos/thought-machine/please/releases'
    36          self.upload_url = self.releases_url.replace('api.', 'uploads.') + '/<id>/assets?name='
    37          self.session = requests.Session()
    38          self.session.verify = '/etc/ssl/certs/ca-certificates.crt'
    39          if not dry_run:
    40              self.session.headers.update({
    41                  'Accept': 'application/vnd.github.v3+json',
    42                  'Authorization': 'token ' + github_token,
    43              })
    44          self.version = self.read_file('VERSION').strip()
    45          self.version_name = 'Version ' + self.version
    46          self.is_prerelease = 'a' in self.version or 'b' in self.version
    47          self.known_content_types = {
    48              '.gz': 'application/gzip',
    49              '.xz': 'application/x-xz',
    50              '.asc': 'text/plain',
    51          }
    52  
    53      def needs_release(self):
    54          """Returns true if the current version is not yet released to Github."""
    55          url = self.releases_url + '/tags/v' + self.version
    56          logging.info('Checking %s for release...', url)
    57          response = self.session.get(url)
    58          return response.status_code == 404
    59  
    60      def release(self):
    61          """Submits a new release to Github."""
    62          data = {
    63              'tag_name': 'v' + self.version,
    64              'target_commitish': os.environ.get('CIRCLE_SHA1'),
    65              'name': 'Please v' + self.version,
    66              'body': '\n'.join(self.get_release_notes()),
    67              'prerelease': self.is_prerelease,
    68              'draft': not self.is_prerelease,
    69          }
    70          if FLAGS.dry_run:
    71              logging.info('Would post the following to Github: %s', json.dumps(data, indent=4))
    72              return
    73          logging.info('Creating release: %s',  json.dumps(data, indent=4))
    74          response = self.session.post(self.releases_url, json=data)
    75          response.raise_for_status()
    76          data = response.json()
    77          self.upload_url = data['upload_url'].replace('{?name,label}', '?name=')
    78          logging.info('Release id %s created', data['id'])
    79  
    80      def upload(self, artifact:str):
    81          """Uploads the given artifact to the new release."""
    82          # Artifact names aren't unique between OSs; make them so.
    83          arch = 'darwin_amd64' if 'darwin' in artifact else 'linux_amd64'
    84          filename = os.path.basename(artifact).replace(self.version, self.version + '_' + arch)
    85          _, ext = os.path.splitext(filename)
    86          content_type = self.known_content_types[ext]
    87          url = self.upload_url + filename
    88          if FLAGS.dry_run:
    89              logging.info('Would upload %s to %s as %s', filename, url, content_type)
    90              return
    91          logging.info('Uploading %s to %s as %s', filename, url, content_type)
    92          with open(artifact, 'rb') as f:
    93              self.session.headers.update({'Content-Type': content_type})
    94              response = self.session.post(url, data=f)
    95              response.raise_for_status()
    96          print('%s uploaded' % filename)
    97  
    98      def sign(self, artifact:str) -> str:
    99          """Creates a detached ASCII-armored signature for an artifact."""
   100          # We expect the PLZ_GPG_KEY and GPG_PASSWORD env vars to be set.
   101          out = artifact + '.asc'
   102          if FLAGS.dry_run:
   103              logging.info('Would sign %s into %s', artifact, out)
   104          else:
   105              subprocess.check_call([FLAGS.signer, '-o', out, '-i', artifact])
   106          return out
   107  
   108      def get_release_notes(self):
   109          """Yields the changelog notes for a given version."""
   110          found_version = False
   111          for line in self.read_file('ChangeLog').split('\n'):
   112              if line.startswith(self.version_name):
   113                  found_version = True
   114                  yield 'This is Please v%s' % self.version
   115              elif line.startswith('------'):
   116                  continue
   117              elif found_version:
   118                  if line.startswith('Version '):
   119                      return
   120                  elif line.startswith('   '):
   121                      # Markdown comes out nicer if we remove some of the spacing.
   122                      line = line[3:]
   123                  yield line
   124          if self.is_prerelease:
   125              logging.warning("No release notes found, continuing anyway since it's a prerelease")
   126              yield PRERELEASE_MESSAGE.strip()
   127          else:
   128              raise Exception("Couldn't find release notes for " + self.version_name)
   129  
   130      def read_file(self, filename):
   131          """Read a file from the .pex."""
   132          with zipfile.ZipFile(sys.argv[0]) as zf:
   133              return zf.read(filename).decode('utf-8')
   134  
   135  
   136  def main(argv):
   137      r = ReleaseGen(FLAGS.github_token, dry_run=FLAGS.dry_run)
   138      if not r.needs_release():
   139          logging.info('Current version has already been released, nothing to be done!')
   140          return
   141      # Check we can sign the artifacts before trying to create a release.
   142      signatures = [r.sign(artifact) for artifact in argv[1:]]
   143      r.release()
   144      for artifact, signature in zip(argv[1:], signatures):
   145          r.upload(artifact)
   146          r.upload(signature)
   147  
   148  
   149  if __name__ == '__main__':
   150      app.run(main)