Repository: twisted/towncrier

Issue:
Enforce fragment name structure
We're using Jira, and want all fragments to be named according to the issue numbering format (e.g. `ABCD-12345.feature.md`).
Is there any way to configure towncrier to enforce the fragment file name somehow? (e.g. supplying a regex)

If there isn't one, I may be able to contribute it.

Discussion:

Patch:
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 5da44ee..771b3b1 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -151,6 +151,12 @@ Top level keys
     -   ``README.rst``
     -   the template file itself
 
+``issue_pattern``
+    Ensure the issue name (file name excluding the category and suffix) matches a certain regex pattern.
+    Make sure to use escape characters properly (e.g. "\\d+" for digit-only file names).
+    When emptry (``""``), all issue names will be considered valid.
+
+    ``""`` by default.
 
 Extra top level keys for Python projects
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py
index 9154f76..a06f6cd 100644
--- a/src/towncrier/_builder.py
+++ b/src/towncrier/_builder.py
@@ -176,7 +176,15 @@ def find_fragments(
                 # Use and increment the orphan news fragment counter.
                 counter = orphan_fragment_counter[category]
                 orphan_fragment_counter[category] += 1
-
+            if config.issue_pattern and (
+                not re.fullmatch(
+                    config.issue_pattern, issue_name := Path(basename).stem
+                )
+            ):
+                raise ClickException(
+                    f"File name '{issue_name}' does not match the "
+                    f"given issue pattern, '{config.issue_pattern}'"
+                )
             full_filename = os.path.join(section_dir, basename)
             fragment_files.append((full_filename, category))
             data = Path(full_filename).read_text(encoding="utf-8", errors="replace")
diff --git a/src/towncrier/_settings/load.py b/src/towncrier/_settings/load.py
index e282a39..c54f979 100644
--- a/src/towncrier/_settings/load.py
+++ b/src/towncrier/_settings/load.py
@@ -55,6 +55,7 @@ class Config:
     create_eof_newline: bool = True
     create_add_extension: bool = True
     ignore: list[str] | None = None
+    issue_pattern: str = ""
 
 
 class ConfigError(ClickException):
diff --git a/src/towncrier/newsfragments/649.feature.rst b/src/towncrier/newsfragments/649.feature.rst
new file mode 100644
index 0000000..506f2ea
--- /dev/null
+++ b/src/towncrier/newsfragments/649.feature.rst
@@ -0,0 +1,1 @@
+Add a config for enforcing issue names using regex.


Test patch:
diff --git a/src/towncrier/test/test_check.py b/src/towncrier/test/test_check.py
index baa3356..c4f53c4 100644
--- a/src/towncrier/test/test_check.py
+++ b/src/towncrier/test/test_check.py
@@ -519,3 +519,33 @@ class TestChecker(TestCase):
         result = runner.invoke(towncrier_check, ["--compare-with", "main"])
         self.assertEqual(1, result.exit_code, result.output)
         self.assertIn("Invalid news fragment name: feature.125", result.output)
+
+    @with_isolated_runner
+    def test_issue_pattern(self, runner):
+        """
+        Fails if an issue name goes against the configured pattern.
+        """
+        create_project(
+            "pyproject.toml",
+            extra_config='issue_pattern = "\\\\d+"',
+        )
+        write(
+            "foo/newsfragments/AAA.BBB.feature",
+            "This fragment has an invalid name (should be digits only)",
+        )
+        write(
+            "foo/newsfragments/123.feature",
+            "This fragment has a valid name",
+        )
+        commit("add stuff")
+
+        result = runner.invoke(towncrier_check, ["--compare-with", "main"])
+        self.assertEqual(1, result.exit_code, result.output)
+        self.assertIn(
+            "Error: File name 'AAA.BBB' does not match the given issue pattern, '\\d+'",
+            result.output,
+        )
+        self.assertNotIn(
+            "Error: File name '123' does not match the given issue pattern, '\\d+'",
+            result.output,
+        )