Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nox session for basic validation of changelog entries #235

Merged
merged 1 commit into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ jobs:
if: (contains(github.event.pull_request.labels.*.name, '-changelog') == false) && (github.event.pull_request.base.ref != '')
run: if [ -z "$(git diff --diff-filter=A --name-only origin/${{ github.event.pull_request.base.ref }} changelog.d)" ];
then echo no changelog item added; exit 1; fi


- name: Changelog validation
run: nox -vs towncrier_check
build:
needs: lint
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion changelog.d/+add_header_options.added.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Add `--expires`, `--content-disposition`, `--content-encoding`, `--content-language` options to subcommands `upload-file`, `upload-unbound-stream`, `copy-file-by-id`
Add `--expires`, `--content-disposition`, `--content-encoding`, `--content-language` options to subcommands `upload-file`, `upload-unbound-stream`, `copy-file-by-id`.
2 changes: 1 addition & 1 deletion changelog.d/+cat.doc.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Add `cat` command to documentation
Add `cat` command to documentation.
1 change: 1 addition & 0 deletions changelog.d/+checking-changelog-entries.infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Changelog entries are now validated as a part of CI pipeline.
1 change: 0 additions & 1 deletion changelog.d/+fix_leaking_semaphores.fix.md

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/+fix_leaking_semaphores.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix an error that caused multiprocessing semaphores to leak on OSX.
2 changes: 1 addition & 1 deletion changelog.d/+python3.12.infrastructure.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Use cpython 3.12 (not 3.11) for integration tests with secrets
Use cpython 3.12 (not 3.11) for integration tests with secrets.
2 changes: 1 addition & 1 deletion changelog.d/+remove_redundant_todo.infrastructure.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Remove unused exception class and outdated todo
Remove unused exception class and outdated todo.
2 changes: 1 addition & 1 deletion changelog.d/+skip_draft_step_in_releases.infrastructure.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Skip draft step in releases - all successful releases are public
Skip draft step in releases - all successful releases are public.
120 changes: 119 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import string
import subprocess
from glob import glob
from typing import List, Tuple
from typing import List, Set, Tuple

import nox

Expand Down Expand Up @@ -601,3 +601,121 @@ def make_release_commit(session):
f' git push {{UPSTREAM_NAME}} v{version}\n'
f' git push {{UPSTREAM_NAME}} {current_branch}'
)


def load_allowed_change_types(project_toml: pathlib.Path = pathlib.Path('./pyproject.toml')
) -> Set[str]:
"""
Load the list of allowed change types from the pyproject.toml file.
"""
import tomllib
configuration = tomllib.loads(project_toml.read_text())
return set(entry['directory'] for entry in configuration['tool']['towncrier']['type'])


def is_changelog_filename_valid(filename: str, allowed_change_types: Set[str]) -> Tuple[bool, str]:
"""
Validates whether the given filename matches our rules.
Provides information about why it doesn't match them.
"""
error_reasons = []

wanted_extension = 'md'
try:
description, change_type, extension = filename.rsplit('.', maxsplit=2)
except ValueError:
# Not enough values to unpack.
return False, "Doesn't follow the \"<description>.<change_type>.md\" pattern."

# Check whether the filename ends with .md.
if extension != wanted_extension:
error_reasons.append(f"Doesn't end with {wanted_extension} extension.")

# Check whether the change type is valid.
if change_type not in allowed_change_types:
error_reasons.append(
f"Change type '{change_type}' doesn't match allowed types: {allowed_change_types}."
)

# Check whether the description makes sense.
try:
int(description)
except ValueError:
if description[0] != '+':
error_reasons.append("Doesn't start with a number nor a plus sign.")

return len(error_reasons) == 0, ' / '.join(error_reasons) if error_reasons else ''


def is_changelog_entry_valid(file_content: str) -> Tuple[bool, str]:
"""
We expect the changelog entry to be a valid sentence in the English language.
This includes, but not limits to, providing a capital letter at the start
and the full-stop character at the end.

Note: to do this "properly", tools like `nltk` and `spacy` should be used.
"""
error_reasons = []

# Check whether the first character is a capital letter.
# Not allowing special characters nor numbers at the very start.
if not file_content[0].isalpha() or not file_content[0].isupper():
error_reasons.append('The first character is not a capital letter.')

# Check if the last character is a full-stop character.
if file_content.strip()[-1] != '.':
error_reasons.append('The last character is not a full-stop character.')

return len(error_reasons) == 0, ' / '.join(error_reasons) if error_reasons else ''


@nox.session(python=PYTHON_DEFAULT_VERSION)
def towncrier_check(session):
"""
Check whether all the entries in the changelog.d follow the expected naming convention
as well as some basic rules as to their format.
"""
expected_non_md_files = {'.gitkeep'}
allowed_change_types = load_allowed_change_types()

is_error = False

for filename in pathlib.Path('./changelog.d/').glob('*'):
# If that's an expected file, it's all right.
if filename.name in expected_non_md_files:
continue

# Check whether the file matches the expected pattern.
is_valid, error_message = is_changelog_filename_valid(filename.name, allowed_change_types)
if not is_valid:
session.log(f"File {filename.name} doesn't match the expected pattern: {error_message}")
is_error = True
continue

# Check whether the file isn't too big.
if filename.lstat().st_size > 16 * 1024:
session.log(
f'File {filename.name} content is too big – it should be smaller than 16kB.'
)
is_error = True
continue

# Check whether the file can be loaded as UTF-8 file.
try:
file_content = filename.read_text(encoding='utf-8')
except UnicodeDecodeError:
session.log(f'File {filename.name} is not a valid UTF-8 file.')
is_error = True
continue

# Check whether the content of the file is anyhow valid.
is_valid, error_message = is_changelog_entry_valid(file_content)
if not is_valid:
session.log(f'File {filename.name} is not a valid changelog entry: {error_message}')
is_error = True
continue

if is_error:
session.error(
'Found errors in the changelog.d directory. Check logs above for more information'
)