diff --git a/.changelog/9be6e14916d04fcd8dead098a3e5cdeb.md b/.changelog/9be6e14916d04fcd8dead098a3e5cdeb.md new file mode 100644 index 0000000..d8f0072 --- /dev/null +++ b/.changelog/9be6e14916d04fcd8dead098a3e5cdeb.md @@ -0,0 +1,4 @@ +--- +type: none +--- +switch to changelet module \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 3f5ee4e..b4b652b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,14 +3,15 @@ Pygments==2.19.2 SecretStorage==3.3.3 black==24.10.0 build==1.2.2.post1 -certifi==2025.6.15 +certifi==2025.7.14 cffi==1.17.1 +changelet==0.1.0 charset-normalizer==3.4.2 click==8.1.8; python_version<'3.10' click==8.2.1; python_version>='3.10' cmarkgfm==2024.11.20 -coverage==7.9.1 -cryptography==45.0.4 +coverage==7.10.0 +cryptography==45.0.5 docutils==0.21.2 id==1.5.0 iniconfig==2.1.0 @@ -24,7 +25,7 @@ markdown-it-py==3.0.0 mdurl==0.1.2 more-itertools==10.7.0 mypy_extensions==1.1.0 -nh3==0.2.21 +nh3==0.3.0 packaging==25.0 pathspec==0.12.1 platformdirs==4.3.8 @@ -44,7 +45,8 @@ repoze.lru==0.7 requests-toolbelt==1.0.0 requests==2.32.4 rfc3986==2.0.0 -rich==14.0.0 +rich==14.1.0 +semver==3.0.4 twine==6.1.0 urllib3==2.5.0 wheel==0.45.1 diff --git a/script/changelog b/script/changelog index a2a63bb..4410a5f 100755 --- a/script/changelog +++ b/script/changelog @@ -1,339 +1,18 @@ -#!/usr/bin/env python +#!/bin/sh +set -e -from argparse import ArgumentParser, RawTextHelpFormatter -from datetime import datetime -from importlib import import_module -from io import StringIO -from json import loads -from os import getcwd, listdir, makedirs, remove -from os.path import basename, isdir, join -from subprocess import PIPE, run -from sys import argv, exit, path, stderr -from uuid import uuid4 +cd "$(dirname "$0")/.." +ROOT=$(pwd) -from yaml import safe_load +if [ -z "$VENV_NAME" ]; then + VENV_NAME="env" +fi +ACTIVATE="$VENV_NAME/bin/activate" +if [ ! -f "$ACTIVATE" ]; then + echo "$ACTIVATE does not exist, run ./script/bootstrap" >&2 + exit 1 +fi +. "$ACTIVATE" -def create(argv): - prog = basename(argv.pop(0)) - parser = ArgumentParser( - prog=f'{prog} create', - description='Creates a new changelog entry.', - add_help=True, - formatter_class=RawTextHelpFormatter, - ) - - parser.add_argument( - '-t', - '--type', - choices=('none', 'patch', 'minor', 'major'), - required=True, - help='''The scope of the change. - -* patch - This is a bug fix -* minor - This adds new functionality or makes changes in a fully backwards - compatible way -* major - This includes substantial new functionality and/or changes that break - compatibility and may require careful migration -* none - This change does not need to be mentioned in the changelog - -See https://semver.org/ for more info''', - ) - parser.add_argument( - '-p', - '--pr', - help='Manually override the PR number for the change, maintainer use only.', - ) - parser.add_argument( - '-a', - '--add', - action='store_true', - default=False, - help='git add the newly created changelog entry', - ) - parser.add_argument( - 'md', - metavar='change-description-markdown', - nargs='+', - help='''A short description of the changes in this PR, suitable as an entry in -CHANGELOG.md. Should be a single line. Can include simple markdown formatting -and links.''', - ) - - args = parser.parse_args(argv) - - if not isdir('.changelog'): - makedirs('.changelog') - filepath = join('.changelog', f'{uuid4().hex}.md') - with open(filepath, 'w') as fh: - fh.write('---\ntype: ') - fh.write(args.type) - if args.pr: - fh.write('\npr: ') - fh.write(args.pr) - fh.write('\n---\n') - fh.write(' '.join(args.md)) - - print( - f'Created {filepath}, it can be further edited and should be committed to your branch.' - ) - - if args.add: - run(['git', 'add', filepath]) - - -def check(argv): - if isdir('.changelog'): - result = run( - ['git', 'diff', '--name-only', 'origin/main', '.changelog/'], - check=False, - stdout=PIPE, - ) - entries = { - l - for l in result.stdout.decode('utf-8').split() - if l.endswith('.md') - } - if not result.returncode and entries: - exit(0) - - print( - 'PR is missing required changelog file, run ./script/changelog create', - file=stderr, - ) - exit(1) - - -def _get_current_version(module_name): - cwd = getcwd() - path.append(cwd) - module = import_module(module_name) - return tuple(int(v) for v in module.__version__.split('.', 2)) - - -class _ChangeMeta: - _pr_cache = None - - @classmethod - def get(cls, filepath, data): - if 'pr' in data: - return data['pr'], datetime(year=1970, month=1, day=1) - if cls._pr_cache is None: - result = run( - [ - 'gh', - 'pr', - 'list', - '--base', - 'main', - '--state', - 'merged', - '--limit=50', - '--json', - 'files,mergedAt,number', - ], - check=True, - stdout=PIPE, - ) - cls._pr_cache = {} - for pr in loads(result.stdout): - for file in pr['files']: - path = file['path'] - if path.startswith('.changelog'): - cls._pr_cache[path] = ( - pr['number'], - datetime.fromisoformat(pr['mergedAt']).replace( - tzinfo=None - ), - ) - - try: - return cls._pr_cache[filepath] - except KeyError: - # couldn't find a PR with the changelog file in it - return None, datetime(year=1970, month=1, day=1) - - -def _get_changelogs(): - ret = [] - dirname = '.changelog' - for filename in listdir(dirname): - if not filename.endswith('.md'): - continue - filepath = join(dirname, filename) - with open(filepath) as fh: - pieces = fh.read().split('---\n') - data = safe_load(pieces[1]) - md = pieces[2] - if md[-1] == '\n': - md = md[:-1] - pr, time = _ChangeMeta.get(filepath, data) - if not pr: - continue - ret.append( - { - 'filepath': filepath, - 'md': md, - 'pr': pr, - 'time': time, - 'type': data.get('type', '').lower(), - } - ) - - ordering = {'major': 3, 'minor': 2, 'patch': 1, 'none': 0, '': 0} - ret.sort(key=lambda c: (ordering[c['type']], c['time']), reverse=True) - return ret - - -def _get_new_version(current_version, changelogs): - try: - bump_type = changelogs[0]['type'] - except IndexError: - return None - new_version = list(current_version) - if bump_type == 'major': - new_version[0] += 1 - new_version[1] = 0 - new_version[2] = 0 - elif bump_type == 'minor': - new_version[1] += 1 - new_version[2] = 0 - else: - new_version[2] += 1 - return tuple(new_version) - - -def _format_version(version): - return '.'.join(str(v) for v in version) - - -def bump(argv): - prog = basename(argv.pop(0)) - parser = ArgumentParser( - prog=f'{prog} bump', - description='Builds a changelog update and calculates a new version number.', - add_help=True, - ) - - parser.add_argument( - '--make-changes', - action='store_true', - help='Write changelog update and bump version number', - ) - parser.add_argument( - 'title', nargs='+', help='A short title/quip for the release title' - ) - - args = parser.parse_args(argv) - - buf = StringIO() - - cwd = getcwd() - module_name = basename(cwd).replace('-', '_') - - buf.write('## ') - current_version = _get_current_version(module_name) - changelogs = _get_changelogs() - new_version = _get_new_version(current_version, changelogs) - if not new_version: - print('No changelog entries found that would bump, nothing to do') - exit(1) - new_version = _format_version(new_version) - buf.write(new_version) - buf.write(' - ') - buf.write(datetime.now().strftime('%Y-%m-%d')) - buf.write(' - ') - buf.write(' '.join(args.title)) - buf.write('\n') - - current_type = None - for changelog in changelogs: - md = changelog['md'] - if not md: - continue - - _type = changelog['type'] - if _type == 'none': - # these aren't included in the listing - continue - if _type != current_type: - buf.write('\n') - buf.write(_type.capitalize()) - buf.write(':\n') - current_type = _type - buf.write('* ') - buf.write(md) - - pr = changelog['pr'] - if pr: - pr = str(pr) - buf.write(' [#') - buf.write(pr) - buf.write('](https://github.com/octodns/') - buf.write(module_name) - buf.write('/pull/') - buf.write(pr) - buf.write(')') - - buf.write('\n') - - buf.write('\n') - - if not args.make_changes: - print(f'New version number {new_version}\n') - print(buf.getvalue()) - exit(0) - - with open('CHANGELOG.md') as fh: - existing = fh.read() - - with open('CHANGELOG.md', 'w') as fh: - fh.write(buf.getvalue()) - fh.write(existing) - - with open(f'{module_name}/__init__.py') as fh: - existing = fh.read() - - current_version = _format_version(current_version) - with open(f'{module_name}/__init__.py', 'w') as fh: - fh.write(existing.replace(current_version, new_version)) - - for changelog in changelogs: - remove(changelog['filepath']) - - -cmds = {'create': create, 'check': check, 'bump': bump} - - -def general_usage(msg=None): - global cmds - - exe = basename(argv[0]) - cmds = ','.join(sorted(cmds.keys())) - print(f'usage: {exe} {{{cmds}}} ...') - if msg: - print(msg) - else: - print( - ''' -Creates and checks or changelog entries, located in the .changelog directory. -Additionally supports updating CHANGELOG.md and bumping the package version -based on one or more entries in that directory. -''' - ) - - -try: - cmd = cmds[argv[1].lower()] - argv.pop(1) -except IndexError: - general_usage('missing command') - exit(1) -except KeyError: - if argv[1] in ('-h', '--help', 'help'): - general_usage() - exit(0) - general_usage(f'unknown command "{argv[1]}"') - exit(1) - -cmd(argv) +changelet "$@" diff --git a/setup.py b/setup.py index 91fbbcf..c2ca94a 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ setup( # https://black.readthedocs.io/en/stable/the_black_code_style/index.html#stability-policy 'black>=24.3.0,<25.0.0', 'build>=0.7.0', + 'changelet', 'isort>=5.11.5', 'pycountry>=19.8.18', 'pycountry-convert>=0.7.2',