diff --git a/src/towncrier/create.py b/src/towncrier/create.py index 5242e124..aeddca3a 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -83,11 +83,15 @@ def __main( file_dir, file_basename = os.path.split(filename) if config.orphan_prefix and file_basename.startswith(f"{config.orphan_prefix}."): - # Append a random hex string to the orphan news fragment base name. + # Append a random hex string (with at least one alphabetic character to avoid + # clashing with issue numbers) to the orphan news fragment base name. + random_hash = "" + while not random_hash or random_hash.isdigit(): + random_hash = os.urandom(4).hex() filename = os.path.join( file_dir, ( - f"{config.orphan_prefix}{os.urandom(4).hex()}" + f"{config.orphan_prefix}{random_hash}" f"{file_basename[len(config.orphan_prefix):]}" ), ) diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index 7f0e24b1..b324de49 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -3,7 +3,6 @@ import os import string - from pathlib import Path from textwrap import dedent from unittest import mock @@ -229,6 +228,27 @@ def test_create_orphan_fragment(self, runner: CliRunner): # Length should be '+' character and 8 random hex characters. self.assertEqual(len(change2.stem), 9) + def test_create_orphan_fragment_must_contain_alpha(self): + """ + The random value added to the orphan fragment base name must contain at least + one alphabetic character to avoid clashing with issue numbers. + """ + setup_simple_project() + + frag_path = Path("foo", "newsfragments") + + with mock.patch("towncrier.create.os.urandom") as mock_urandom: + mock_urandom(4).hex.side_effect = [ + "12345678", + "56781234", + "123a5678", + "abcdefgh", + ] + result = CliRunner().invoke(_main, ["+.feature"]) + self.assertEqual(0, result.exit_code, result.output) + fragments = list(frag_path.rglob("*")) + self.assertEqual(fragments, [frag_path / "+123a5678.feature"]) + @with_isolated_runner def test_create_orphan_fragment_custom_prefix(self, runner: CliRunner): """