Skip to content

Commit ea82734

Browse files
Merge pull request Backblaze#975 from reef-technologies/towncrier_checker_for_nox
Nox session for basic validation of changelog entries
2 parents fd36834 + 2951e7e commit ea82734

10 files changed

+128
-9
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ jobs:
3131
if: (contains(github.event.pull_request.labels.*.name, '-changelog') == false) && (github.event.pull_request.base.ref != '')
3232
run: if [ -z "$(git diff --diff-filter=A --name-only origin/${{ github.event.pull_request.base.ref }} changelog.d)" ];
3333
then echo no changelog item added; exit 1; fi
34-
35-
34+
- name: Changelog validation
35+
run: nox -vs towncrier_check
3636
build:
3737
needs: lint
3838
runs-on: ubuntu-latest
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Add `--expires`, `--content-disposition`, `--content-encoding`, `--content-language` options to subcommands `upload-file`, `upload-unbound-stream`, `copy-file-by-id`
1+
Add `--expires`, `--content-disposition`, `--content-encoding`, `--content-language` options to subcommands `upload-file`, `upload-unbound-stream`, `copy-file-by-id`.

changelog.d/+cat.doc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Add `cat` command to documentation
1+
Add `cat` command to documentation.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Changelog entries are now validated as a part of CI pipeline.

changelog.d/+fix_leaking_semaphores.fix.md

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix an error that caused multiprocessing semaphores to leak on OSX.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Use cpython 3.12 (not 3.11) for integration tests with secrets
1+
Use cpython 3.12 (not 3.11) for integration tests with secrets.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Remove unused exception class and outdated todo
1+
Remove unused exception class and outdated todo.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Skip draft step in releases - all successful releases are public
1+
Skip draft step in releases - all successful releases are public.

noxfile.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import string
1717
import subprocess
1818
from glob import glob
19-
from typing import List, Tuple
19+
from typing import List, Set, Tuple
2020

2121
import nox
2222

@@ -601,3 +601,121 @@ def make_release_commit(session):
601601
f' git push {{UPSTREAM_NAME}} v{version}\n'
602602
f' git push {{UPSTREAM_NAME}} {current_branch}'
603603
)
604+
605+
606+
def load_allowed_change_types(project_toml: pathlib.Path = pathlib.Path('./pyproject.toml')
607+
) -> Set[str]:
608+
"""
609+
Load the list of allowed change types from the pyproject.toml file.
610+
"""
611+
import tomllib
612+
configuration = tomllib.loads(project_toml.read_text())
613+
return set(entry['directory'] for entry in configuration['tool']['towncrier']['type'])
614+
615+
616+
def is_changelog_filename_valid(filename: str, allowed_change_types: Set[str]) -> Tuple[bool, str]:
617+
"""
618+
Validates whether the given filename matches our rules.
619+
Provides information about why it doesn't match them.
620+
"""
621+
error_reasons = []
622+
623+
wanted_extension = 'md'
624+
try:
625+
description, change_type, extension = filename.rsplit('.', maxsplit=2)
626+
except ValueError:
627+
# Not enough values to unpack.
628+
return False, "Doesn't follow the \"<description>.<change_type>.md\" pattern."
629+
630+
# Check whether the filename ends with .md.
631+
if extension != wanted_extension:
632+
error_reasons.append(f"Doesn't end with {wanted_extension} extension.")
633+
634+
# Check whether the change type is valid.
635+
if change_type not in allowed_change_types:
636+
error_reasons.append(
637+
f"Change type '{change_type}' doesn't match allowed types: {allowed_change_types}."
638+
)
639+
640+
# Check whether the description makes sense.
641+
try:
642+
int(description)
643+
except ValueError:
644+
if description[0] != '+':
645+
error_reasons.append("Doesn't start with a number nor a plus sign.")
646+
647+
return len(error_reasons) == 0, ' / '.join(error_reasons) if error_reasons else ''
648+
649+
650+
def is_changelog_entry_valid(file_content: str) -> Tuple[bool, str]:
651+
"""
652+
We expect the changelog entry to be a valid sentence in the English language.
653+
This includes, but not limits to, providing a capital letter at the start
654+
and the full-stop character at the end.
655+
656+
Note: to do this "properly", tools like `nltk` and `spacy` should be used.
657+
"""
658+
error_reasons = []
659+
660+
# Check whether the first character is a capital letter.
661+
# Not allowing special characters nor numbers at the very start.
662+
if not file_content[0].isalpha() or not file_content[0].isupper():
663+
error_reasons.append('The first character is not a capital letter.')
664+
665+
# Check if the last character is a full-stop character.
666+
if file_content.strip()[-1] != '.':
667+
error_reasons.append('The last character is not a full-stop character.')
668+
669+
return len(error_reasons) == 0, ' / '.join(error_reasons) if error_reasons else ''
670+
671+
672+
@nox.session(python=PYTHON_DEFAULT_VERSION)
673+
def towncrier_check(session):
674+
"""
675+
Check whether all the entries in the changelog.d follow the expected naming convention
676+
as well as some basic rules as to their format.
677+
"""
678+
expected_non_md_files = {'.gitkeep'}
679+
allowed_change_types = load_allowed_change_types()
680+
681+
is_error = False
682+
683+
for filename in pathlib.Path('./changelog.d/').glob('*'):
684+
# If that's an expected file, it's all right.
685+
if filename.name in expected_non_md_files:
686+
continue
687+
688+
# Check whether the file matches the expected pattern.
689+
is_valid, error_message = is_changelog_filename_valid(filename.name, allowed_change_types)
690+
if not is_valid:
691+
session.log(f"File {filename.name} doesn't match the expected pattern: {error_message}")
692+
is_error = True
693+
continue
694+
695+
# Check whether the file isn't too big.
696+
if filename.lstat().st_size > 16 * 1024:
697+
session.log(
698+
f'File {filename.name} content is too big – it should be smaller than 16kB.'
699+
)
700+
is_error = True
701+
continue
702+
703+
# Check whether the file can be loaded as UTF-8 file.
704+
try:
705+
file_content = filename.read_text(encoding='utf-8')
706+
except UnicodeDecodeError:
707+
session.log(f'File {filename.name} is not a valid UTF-8 file.')
708+
is_error = True
709+
continue
710+
711+
# Check whether the content of the file is anyhow valid.
712+
is_valid, error_message = is_changelog_entry_valid(file_content)
713+
if not is_valid:
714+
session.log(f'File {filename.name} is not a valid changelog entry: {error_message}')
715+
is_error = True
716+
continue
717+
718+
if is_error:
719+
session.error(
720+
'Found errors in the changelog.d directory. Check logs above for more information'
721+
)

0 commit comments

Comments
 (0)