Skip to content

Commit

Permalink
Merge pull request #37 from dwhswenson/release-0.0.12
Browse files Browse the repository at this point in the history
Release 0.0.12
  • Loading branch information
dwhswenson authored Dec 1, 2019
2 parents 78b8bf0 + bac5e5f commit 3b813a4
Show file tree
Hide file tree
Showing 18 changed files with 500 additions and 77 deletions.
70 changes: 6 additions & 64 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
version: ~> 1.0

language: python
python:
- '3.8'
- '3.7'
- '3.6'
- '2.7'
- '3.4'
- '3.5'

branches:
only:
Expand All @@ -13,16 +15,14 @@ branches:

env:
global:
- CANONICAL_PYTHON="3.6"
- CODECLIMATE=""
- AUTORELEASE_TEST_TESTPYPI="cd ~ && python -c 'import autorelease'"
- TWINE_USERNAME="dwhswenson"
# TWINE_PASSWORD
- secure: "wp+7rQ11ipJ2r/Yde8AWuZcBVgrPQPOnJdddntFgKSXh5aI3xiZm7mgHyy+mGg5mf8smErF0SwpZ6U4b2VQiXfS8XDwie+/gOrpFNkAPY0jHLRegSK1oXvqBTgZHQI3zeD4Q+qUFDVOIpu70JC+UrM5nEUC3jXo/T2ojJ7AdW7JROfgptewaFWLEaa38KHPZYUGjD9NGiB+49k220xEVdKtRK1cnogcCpxnPBsXiACz9VhYQ5f+r0wHSVDH151eUyiqdXiDjWTJ6o6uyaxm4jbECdFhfX0qoirp4l26Rf1UDk0HJYG+hEvT1IG/fThH6WhubXTpLMpUIdaPgZYvjhZcrpyJymtPMQ6RhrHifJh90nBWaN+vzeBdFwxav9skQypf3uS94XynynOOSTuZKCHwHBzlKypuB3RxSUIrZu9xnrJMkOD5jtLo6fsDhlZeNSy9zzKFZA+LoLPiQaZKpA8W8gUR31PcIo1VDiClGpu54Xu8Nm0agjExlQkBPa9xZNik7yNv381Gm0NLCwnwQtnZXY/V9CNRXUmATAMzoBcLf6xgZxHroQPBYB2mw76e7UB2fzRWYaby8T39GU3C8vczxaqSHDam2qwKieruzNSaRHLOGDQcx7N8WM/f8GtKcXxpPwxdjv0r8sAZ6YoIlbYVcQfpXAOPQD9Gz5vn+JKI="
# AUTORELEASE_TOKEN
- secure: "g0qz8XRM8eiI20K9i7r//E1U6gSZ3MgyBzUYBRtez2C+HwNuKoIQCtX2f33vXQBvfySiKvF0jolp1GIlze5FvH1h3/+G+3BoX/9L/qFl/mioXKxGbczW4J/iD3tt0VjVXs++1MxKtHq9TOZB92sGDy5GG0IBSTS12NDlqFS9QPcRPrgH2DOMzpj8rT1aHGWxsDfR5BKtUv7rkstLK9SghsYhhITYtBPIiKuvsIXOtGziNAMA5i7ohcMQSpCYF7I8DzfSPeibnlN0rdAwKSHubNfHxTSALSnhum6PAj8UkiF1up08wDiazr8iWu/XJZ+l6xCB4ElP/2eiP4db4/rExrbvifhcmNA67nVbVO4D/AIz5GShRxmYdgWdy4LwZfuY/HZUyNIYhRFRf0iu5cbotAiQqeydRqr3O7xVrmxmn+WOEqTvm+13Crlno1J1PpAhLEbJL8tJQDK4JSYOEujAwh/b0Iqc1/QCz1WzAIEDEdjnxfSm1Bl+YpgCQyo7i0xGtQoTk3wG53Tz4H5RV2oHvR37D3mKZX6G/3Vm9gHUcgfwryJbAKEgQJXZ4bzgxrK7Y6rNJ/hY9liRT/om/ubenpvJ0VFntTjdrm85+YJMT67V852x0OG2CITp5AOaJNVQ1289My6fj3cbfjYBp1/w990WhAtCu5BNIak3s+eSm2w="

before_install:
- echo "before install"
- git fetch --tags

install:
Expand All @@ -39,62 +39,4 @@ script:
after_success:
- echo "success!"

jobs:
include:
- stage: deploy testpypi
# This stage runs when you make a PR to stable. It tests that the
# deployment to testpypi works.
if: "(branch = stable) and (type = pull_request)"
python: '3.6'
addons:
apt_packages:
- pandoc
install:
- pip install twine
script:
- pandoc --from=markdown --to=rst --output=README.rst README.md
- python setup.py sdist bdist_wheel
- twine upload --repository-url https://test.pypi.org/legacy/ dist/*
- stage: test testpypi
# This stage run when you make a PR to stable; after the package has
# been deployed to testpypi. It checks that the deployed package
# works.
if: "(branch = stable) and (type = pull_request)"
python: '3.6'
install:
- pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple autorelease
script:
- cd ~
- python -c "import autorelease"
- echo "py.test --pyargs autorelease" # if I had py.test stuff
- stage: cut release
# This stage runs after you merge a PR into stable. It cuts the GitHub
# release based on the current stable branch and the release notes
# from the last PR merged into stable.
if: "(branch = stable) and (not type in (pull_request, cron))"
python: '3.6'
install:
- pip install . # usually would just need to install autorelease
script:
- VERSION=`python setup.py --version`
- PROJECT=`python setup.py --name`
- autorelease-release --project $PROJECT --version $VERSION --token $AUTORELEASE_TOKEN
- stage: deploy pypi
# This stage runs when a version-labelled tag is made. It deploys the
# package to PyPI.
if: tag =~ ^v[0-9]+\.
addons:
apt_packages:
- pandoc
install: skip
script:
- pandoc --from=markdown --to=rst --output=README.rst README.md
deploy:
provider: pypi
distributions: sdist bdist_wheel
skip_cleanup: true # need the readme.rst from the script stage
user: dwhswenson
on:
tags: true
password:
secure: "VnOfkKXPAGeUfWBQra+WISnmgQV1yiz1ZsOExSqzDvdnOkGZ3ToLYiNn0sEpPgzeeYOx8nkjWUYZ8e8b5oYmU1fQ+5AXJ/b8L5T463IMj0Qe4PL4leCfv9Rqn90NywUwGaYQseeOi8c1yB8XaMqWkBmwjFuHmk0B7PQ5DISWJa9NR9/ydGc5Rp8H0/z40eVkaNksNHZjEfr9FHMt1PFM3KWecH6WkGscSVii+B/dfAcDWOW/UKI1094ckQkGNxtOEOMR6NkiLqFgb6iCOUTfyypELRNHLb2mB5d+6/doy93uJbY9dgFgI0glUrr0Ira/Nn3KGObxezvl5noe9+quD10r4KTl338qINR6H/NGL/t62TDG+hXn7xpGJ9i6UjJt5xmZio22neo0UEchSSvERsAkgChYQJ4N2jA2WVVUKuH+REjDEARjWrhx1u8gMB4vQGjI+47nxEvRiSl/ICwLqHswLK2Y+uqdsDdKZZq5kLxgcIqeNxuUzNj2fAGSlEbZVJEMrEe01hVC44OSiz2mQ8ndaP/Nf6i/n+SHgE+oeIJasHbgyNHpfR36RFR5QsVKu0lKyQjjVsm7HtMzz+JnSfO1hfDK8n94AYjDyEP9T/4Hrwo4KAGKLER9rokyC3psrTWNiefYB1ODYJneTC6Uhyh9Sq/+RlNEtPdN8YjGQAE="
import: autorelease-local.yml
File renamed without changes.
5 changes: 5 additions & 0 deletions autorelease-local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import:
- travis_stages/deploy_testpypi.yml
- travis_stages/cut_release.yml
- travis_stages/deploy_pypi.yml
- travis_stages/test_testpypi.yml
8 changes: 7 additions & 1 deletion autorelease/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@

from .check_runners import CheckRunner, DefaultCheckRunner

from .github_release import GitHubReleaser, ProjectOptions, GitHubUser
from .github_release import (
GitHubRepoBase, GitHubReleaser, ProjectOptions, GitHubUser
)

from .release_notes import ReleaseNoteWriter

from .script_utils import AutoreleaseParsingHelper

from .utils import conda_recipe_version

Expand Down
1 change: 1 addition & 0 deletions autorelease/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
# this is just to give us a backup module in case we don't have this

version = "Unknown"
git_hash = "Unknown"
full_version = \
"Unknown: Incorrect installation. Use pip or setup.py to install"
54 changes: 46 additions & 8 deletions autorelease/github_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,51 @@ class GitHubUser(namedtuple('GitHubUser', ['username', 'token'])):
def auth(self):
return (self.username, self.token)

class GitHubRepoBase(object):
"""
Parameters
----------
project: :class:`.ProjectOptions`
github_user: :class:`.GitHubUser`
"""
def __init__(self, project, github_user):
github_api_url = "https://api.github.com/"
self.project = project
self.repo_api_url = (github_api_url + "repos/" + project.repo_owner
+ "/" + project.repo_name + "/")
self.github_user = github_user

class GitHubReleaser(object):
def api_get(self, url_ending, params=None):
return requests.get(url=self.repo_api_url + url_ending,
params=params,
auth=self.github_user.auth)

def api_get_json_all(self, url_ending, params=None):
# only for issues, which limit to 30 per return
my_params = {}
my_params.update(params)
my_params.update({'sort': 'updated', 'direction': 'asc'})
results = {} # we use a dict to easily look up by number
# actual return is list of values
should_continue = True
while should_continue:
local_results_req = self.api_get(url_ending, my_params)
local_results = local_results_req.json()
if local_results:
since = local_results[-1]['updated_at']
# print(local_results[-1]['updated_at'],
# local_results[0]['updated_at'])
my_params['since'] = since
local_result_dict = {result['number']: result
for result in local_results
if result['number'] not in results}
results.update(local_result_dict)
should_continue = local_result_dict
# print(results.keys())
return list(results.values())


class GitHubReleaser(GitHubRepoBase):
"""
Parameters
----------
Expand All @@ -30,17 +73,12 @@ class GitHubReleaser(object):
release_target_commitish : str
"""
def __init__(self, project, version, repo, github_user):
github_api_url = "https://api.github.com/"
self.project = project
super(GitHubReleaser, self).__init__(project, github_user)
self.version = version
self.repo = repo
self.repo_api_url = (github_api_url + "repos/" + project.repo_owner
+ "/" + project.repo_name + "/")
self.github_user = github_user

# pr_re set in pr_pattern
self._pr_pattern = None
self.pr_re = None
self.repo = repo

self.pr_pattern = "Merge pull request #([0-9]+)"
self.release_target_commitish = "stable"
Expand Down
189 changes: 189 additions & 0 deletions autorelease/release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/env python
from __future__ import print_function
import sys
import collections
import yaml
import requests
import dateutil.parser
import datetime

from .github_release import GitHubRepoBase, GitHubUser, ProjectOptions

class ReleaseNoteWriter(GitHubRepoBase):
def __init__(self, config, since_release=None, project=None,
github_user=None):
if isinstance(config, str):
with open(config) as f:
config = yaml.load(f.read(), Loader=yaml.SafeLoader)

self.config = config
self.since_release = since_release
project = self._apply_config_key(project, 'project', ProjectOptions)
github_user = self._apply_config_key(github_user, 'github_user',
GitHubUser)

super(ReleaseNoteWriter, self).__init__(project, github_user)
self._latest_release_tag = None
self._latest_release_tag_name = None
self._latest_release_commit_date = None

def _apply_config_key(self, var, key, obj_cls):
# this is to make codeclimate happy
if var is None and key in self.config.keys():
var = obj_cls(**self.config[key])
return var

def filter_recent_pulls(self, pulls, since):
# filter for date
pulls = [p for p in pulls if p['merged_at'] > since]
# ensure that you don't have the last PR from previous showing up
# (there can be a second in time difference between commit and
# merge)
latest_tag_sha = self._latest_release_tag['commit']['sha']
pulls = [p for p in pulls
if p['merge_commit_sha'] != latest_tag_sha]
# print([p['number'] for p in pulls])
return pulls

def label_organized_merged_pulls(self, since=''):
# the implementation challenge is that the return info from pulls
# doesn't currently include info about labels -- that is contains in
# the return info from issues (all pulls are issues).

# because of problems that the pull close date doesn't always match
# the issue close date (I see a difference of seconds), we look for
# any pulls/issues closed a minute before the last release. User can
# manually remove problematic things.
since_datetime = dateutil.parser.parse(since)
delta = datetime.timedelta(0, 60)
since = (since_datetime - delta).isoformat()
params = {'state': 'closed', 'since': since}
recent_pulls = self.api_get("pulls", params=params).json()
# print([p['number'] for p in recent_pulls], len(recent_pulls))
# remove closed (unmerged) pulls
recent_pulls = [p for p in recent_pulls if p['merged_at'] is not None]
# print([p['number'] for p in recent_pulls], len(recent_pulls))
# print([(p['number'], p['merged_at']) for p in recent_pulls])
recent_pulls = self.filter_recent_pulls(recent_pulls, since)
issue_params = {'filter': 'all'}
issue_params.update(params)
recent_issues = self.api_get_json_all("issues", params=issue_params)
# print([(iss['number'], iss['closed_at']) for iss in recent_issues])
issues_by_number = {iss['number']: iss for iss in recent_issues}
# print(list(issues_by_number.keys()))
desired_pulls = collections.defaultdict(list)
for pull in recent_pulls:
issue = issues_by_number[pull['number']]
label_names = [label['name'] for label in issue['labels']]
label_names = [None] if not label_names else label_names
for label in label_names:
desired_pulls[label] += [pull]

return desired_pulls

def _latest_release_tag_info(self, tag_name):
tags = self.api_get("tags").json()
desired_tag_list = [t for t in self.api_get("tags").json()
if t['name'] == tag_name]
assert len(desired_tag_list) == 1
self._latest_release_tag = desired_tag_list[0]

@property
def latest_release_commit_date(self):
return self.set_release_info()

def release_commit_date(self, release_name=None):
# note that we use the date of the commit of the release, not the
# date of the release itself (can release long after the commit)
if release_name is None:
release_name = "latest"
else:
release_name = "tags/" + release_name

latest_release = self.api_get("releases/" + release_name).json()
if self._latest_release_tag_name != latest_release['tag_name']:
self._latest_release_tag_info(latest_release['tag_name'])
latest_tag_commit_sha = \
self._latest_release_tag['commit']['sha']
commit = self.api_get("commits/" + latest_tag_commit_sha).json()
self._latest_release_commit_date = \
commit['commit']['committer']['date']
self._latest_release_tag_name = latest_release['tag_name']
return self._latest_release_commit_date

def write_pull_line(self, pull, extra_labels=None):
if extra_labels is None:
extra_labels = []
title = pull['title']
number = str(pull['number'])
author = pull['user']['login']
out_str = "* " + title + " (#" + number + ")"
if author not in self.config['standard_contributors']:
out_str += " @" + author
for label in extra_labels:
out_str += " #" + label
out_str += "\n"
return out_str

@staticmethod
def _pull_to_labels(pull_dict):
pull_to_labels = collections.defaultdict(list)
for label in pull_dict:
for pull in pull_dict[label]:
pull_to_labels[pull['number']] += [label]
return pull_to_labels

def output_for_known_labels(self, pull_dict, pull_to_labels):
out_str = ""
for lbl in self.config['labels']:
label = lbl['label']
out_str += "\n# " + lbl['heading'] + "\n"
for pull in pull_dict[label]:
pull_labels = set(pull_to_labels[pull['number']])
extra_labels = pull_labels - set([label])
out_str += self.write_pull_line(pull, extra_labels)
return out_str

def output_for_unknown_labels(self, pull_dict, unknown_labels,
treated_pulls):
out_str = "\n-----\n\n# Pulls with unknown labels\n"
for label in unknown_labels:
untreated = [pull for pull in pull_dict[label]
if pull['number'] not in treated_pulls]
if untreated:
out_str += "\n## " + str(label) + "\n"
for pull in untreated:
out_str += self.write_pull_line(pull)
treated_pulls.append(pull['number'])
return out_str

def release_notes_from_pulls(self, pull_dict):
config_labels = [lbl['label'] for lbl in self.config['labels']]
unknown_labels = set(pull_dict) - set(config_labels)
pull_to_labels = self._pull_to_labels(pull_dict)

config_label_pulls = sum([pull_dict[lbl]
for lbl in config_labels], [])
treated_pulls = set([p['number'] for p in config_label_pulls])

out_str = self.output_for_known_labels(pull_dict, pull_to_labels)

if len(treated_pulls) != len(pull_to_labels):
out_str += self.output_for_unknown_labels(pull_dict,
unknown_labels,
list(treated_pulls))
return out_str

def write_release_notes(self, outfile=None):
if outfile is None:
outfile = sys.stdout
elif isinstance(outfile, str):
outfile = open(outfile)
release_date = self.release_commit_date(self.since_release)
pull_dict = self.label_organized_merged_pulls(since=release_date)
notes = self.release_notes_from_pulls(pull_dict)
outfile.write(notes)
outfile.flush()
if outfile != sys.stdout:
outfile.close()

Loading

0 comments on commit 3b813a4

Please sign in to comment.