From 55debb92a2483a574af7028ac360b724df310453 Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Tue, 26 Sep 2023 12:10:25 +0200 Subject: [PATCH 1/5] Build command supports multiple projects with one config The create command already supports this. --- src/towncrier/build.py | 4 +++- src/towncrier/test/test_build.py | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/towncrier/build.py b/src/towncrier/build.py index 60e0e861..3a07bae1 100644 --- a/src/towncrier/build.py +++ b/src/towncrier/build.py @@ -175,7 +175,9 @@ def __main( click.echo("Finding news fragments...", err=to_err) if config.directory is not None: - fragment_base_directory = os.path.abspath(config.directory) + fragment_base_directory = os.path.abspath( + os.path.join(base_directory, config.directory) + ) fragment_directory = None else: fragment_base_directory = os.path.abspath( diff --git a/src/towncrier/test/test_build.py b/src/towncrier/test/test_build.py index d4561263..206b501f 100644 --- a/src/towncrier/test/test_build.py +++ b/src/towncrier/test/test_build.py @@ -1344,3 +1344,39 @@ def test_with_topline_and_template_and_draft(self): self.assertEqual(0, result.exit_code, result.output) self.assertEqual(expected_output, result.output) + + @with_isolated_runner + def test_projects_share_one_config_with_nondefault_directory(self, runner): + """ + Multiple projects with independent changelogs share one towncrier + configuration. + + For this to work: + 1. We need to leave `config.package` empty. + 2. We need to pass `--dir` to `create` and `build` explicitly. + It must point to the project folder. + 3. We need to pass `--config` pointing at the global configuration. + 4. We need to make sure `config.directory` and `config.filename` are resolved + relative to what we passed as `--dir`. + """ + # We don't want to specify the package because we have multiple ones. + Path("pyproject.toml").write_text( + # Important to customize `config.directory` because the default + # already supports this scenario. + "[tool.towncrier]\n" + 'directory = "changelog.d"\n' + ) + # Each subproject contains the source code... + Path("foo/foo").mkdir(parents=True) + Path("foo/foo/__init__.py").write_text("") + # ... and the changelog machinery. + Path("foo/changelog.d").mkdir() + Path("foo/changelog.d/123.feature").write_text("Adds levitation") + self.assertFalse(Path("foo/NEWS.rst").exists()) + + result = runner.invoke( + cli, + ("--yes", "--config", "pyproject.toml", "--dir", "foo", "--version", "1.0"), + ) + + self.assertEqual(0, result.exit_code) + self.assertTrue(Path("foo/NEWS.rst").exists()) From 0da67ff7a52235fe0b9ef7e3d83f1704f585f030 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:52:26 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/towncrier/test/test_build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/towncrier/test/test_build.py b/src/towncrier/test/test_build.py index 206b501f..adf1861a 100644 --- a/src/towncrier/test/test_build.py +++ b/src/towncrier/test/test_build.py @@ -1363,7 +1363,8 @@ def test_projects_share_one_config_with_nondefault_directory(self, runner): Path("pyproject.toml").write_text( # Important to customize `config.directory` because the default # already supports this scenario. - "[tool.towncrier]\n" + 'directory = "changelog.d"\n' + "[tool.towncrier]\n" + + 'directory = "changelog.d"\n' ) # Each subproject contains the source code... Path("foo/foo").mkdir(parents=True) From e6a6c8b8f94b75650fc530f9ff465c86d48800ec Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Mon, 9 Oct 2023 15:43:19 +0200 Subject: [PATCH 3/5] Full documented support for sharing config between multiple projects --- docs/cli.rst | 3 +- docs/configuration.rst | 3 +- docs/index.rst | 1 + docs/monorepo.rst | 52 +++++++++++++ src/towncrier/check.py | 10 +-- src/towncrier/newsfragments/548.feature | 2 + src/towncrier/test/test_build.py | 63 +++++++--------- src/towncrier/test/test_check.py | 99 +++++++++++++++++++++++++ src/towncrier/test/test_create.py | 32 ++++++++ 9 files changed, 221 insertions(+), 44 deletions(-) create mode 100644 docs/monorepo.rst create mode 100644 src/towncrier/newsfragments/548.feature diff --git a/docs/cli.rst b/docs/cli.rst index 25eb2cea..90b5c899 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -12,7 +12,8 @@ The following options can be passed to all of the commands that explained below: .. option:: --dir PATH - Build fragment in ``PATH``. + The command is executed relative to ``PATH``. + For instance with the default config news fragments are checked and added in ``PATH/newsfragments`` and the news file is built in ``PATH/NEWS.rst``. Default: current directory. diff --git a/docs/configuration.rst b/docs/configuration.rst index 7d73f508..d424e1c0 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -51,12 +51,13 @@ Top level keys The directory storing your news fragments. For Python projects that provide a ``package`` key, the default is a ``newsfragments`` directory within the package. - Otherwise the default is a ``newsfragments`` directory relative to the configuration file. + Otherwise the default is a ``newsfragments`` directory relative to either the directory passed as ``--dir`` or (by default) the configuration file. ``filename`` The filename of your news file. ``"NEWS.rst"`` by default. + Its location is determined the same way as the location of the directory storing the news fragments. ``template`` Path to the template for generating the news file. diff --git a/docs/index.rst b/docs/index.rst index 975373dd..f83e8ce4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ Narrative tutorial markdown + monorepo Reference diff --git a/docs/monorepo.rst b/docs/monorepo.rst new file mode 100644 index 00000000..0deb8392 --- /dev/null +++ b/docs/monorepo.rst @@ -0,0 +1,52 @@ +Multiple Projects Share One Config (Monorepo) +============================================= + +Several projects may have independent release notes with the same format. +For instance packages in a monorepo. +Here's how you can use towncrier to set this up. + +Below is a minimal example: + +.. code-block:: text + + repo + ├── project_a + │ ├── newsfragments + │ │ └── 123.added + │ ├── project_a + │ │ └── __init__.py + │ └── NEWS.rst + ├── project_b + │ ├── newsfragments + │ │ └── 120.bugfix + │ ├── project_b + │ │ └── __init__.py + │ └── NEWS.rst + └── towncrier.toml + +The ``towncrier.toml`` looks like this: + +.. code-block:: toml + + [tool.towncrier] + # It's important to keep these config fields empty + # because we have more than one package/name to manage. + package = "" + name = "" + +Now to add a fragment: + +.. code-block:: console + + towncrier create --config towncrier.toml --dir project_a 124.added + +This should create a file at ``project_a/newsfragments/124.added``. + +To build the news file for the same project: + +.. code-block:: console + + towncrier build --config towncrier.toml --dir project_a --version 1.5 + +Note that we must explicitly pass ``--version``, there is no other way to get the version number. +The ``towncrier.toml`` can only contain one version number and the ``package`` field is of no use for the same reason. diff --git a/src/towncrier/check.py b/src/towncrier/check.py index 2124590c..ee9b612e 100644 --- a/src/towncrier/check.py +++ b/src/towncrier/check.py @@ -93,9 +93,7 @@ def __main( ) sys.exit(0) - files = { - os.path.normpath(os.path.join(base_directory, path)) for path in files_changed - } + files = {os.path.abspath(path) for path in files_changed} click.echo("Looking at these files:") click.echo("----") @@ -109,7 +107,9 @@ def __main( sys.exit(0) if config.directory: - fragment_base_directory = os.path.abspath(config.directory) + fragment_base_directory = os.path.abspath( + os.path.join(base_directory, config.directory) + ) fragment_directory = None else: fragment_base_directory = os.path.abspath( @@ -118,7 +118,7 @@ def __main( fragment_directory = "newsfragments" fragments = { - os.path.normpath(path) + os.path.abspath(path) for path in find_fragments( fragment_base_directory, config.sections, diff --git a/src/towncrier/newsfragments/548.feature b/src/towncrier/newsfragments/548.feature new file mode 100644 index 00000000..51b3adc9 --- /dev/null +++ b/src/towncrier/newsfragments/548.feature @@ -0,0 +1,2 @@ +Full support for monorepo-style setup. +One project with multiple independent news files that share the same towncrier config. diff --git a/src/towncrier/test/test_build.py b/src/towncrier/test/test_build.py index adf1861a..9d42664e 100644 --- a/src/towncrier/test/test_build.py +++ b/src/towncrier/test/test_build.py @@ -143,6 +143,32 @@ def test_in_different_dir_config_option(self, runner): self.assertEqual(0, result.exit_code) self.assertTrue((project_dir / "NEWS.rst").exists()) + @with_isolated_runner + def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): + """ + Config location differs from the base directory for news file and fragments. + + This is useful when multiple projects share one towncrier configuration. + The default `newsfragments` setting already supports this scenario so here + we test that custom settings also do. + """ + Path("pyproject.toml").write_text( + "[tool.towncrier]\n" + 'directory = "changelog.d"\n' + ) + Path("foo/foo").mkdir(parents=True) + Path("foo/foo/__init__.py").write_text("") + Path("foo/changelog.d").mkdir() + Path("foo/changelog.d/123.feature").write_text("Adds levitation") + self.assertFalse(Path("foo/NEWS.rst").exists()) + + result = runner.invoke( + cli, + ("--yes", "--config", "pyproject.toml", "--dir", "foo", "--version", "1.0"), + ) + + self.assertEqual(0, result.exit_code) + self.assertTrue(Path("foo/NEWS.rst").exists()) + @with_isolated_runner def test_no_newsfragment_directory(self, runner): """ @@ -1344,40 +1370,3 @@ def test_with_topline_and_template_and_draft(self): self.assertEqual(0, result.exit_code, result.output) self.assertEqual(expected_output, result.output) - - @with_isolated_runner - def test_projects_share_one_config_with_nondefault_directory(self, runner): - """ - Multiple projects with independent changelogs share one towncrier - configuration. - - For this to work: - 1. We need to leave `config.package` empty. - 2. We need to pass `--dir` to `create` and `build` explicitly. - It must point to the project folder. - 3. We need to pass `--config` pointing at the global configuration. - 4. We need to make sure `config.directory` and `config.filename` are resolved - relative to what we passed as `--dir`. - """ - # We don't want to specify the package because we have multiple ones. - Path("pyproject.toml").write_text( - # Important to customize `config.directory` because the default - # already supports this scenario. - "[tool.towncrier]\n" - + 'directory = "changelog.d"\n' - ) - # Each subproject contains the source code... - Path("foo/foo").mkdir(parents=True) - Path("foo/foo/__init__.py").write_text("") - # ... and the changelog machinery. - Path("foo/changelog.d").mkdir() - Path("foo/changelog.d/123.feature").write_text("Adds levitation") - self.assertFalse(Path("foo/NEWS.rst").exists()) - - result = runner.invoke( - cli, - ("--yes", "--config", "pyproject.toml", "--dir", "foo", "--version", "1.0"), - ) - - self.assertEqual(0, result.exit_code) - self.assertTrue(Path("foo/NEWS.rst").exists()) diff --git a/src/towncrier/test/test_check.py b/src/towncrier/test/test_check.py index 102ab20a..43b07526 100644 --- a/src/towncrier/test/test_check.py +++ b/src/towncrier/test/test_check.py @@ -297,3 +297,102 @@ def test_get_default_compare_branch_fallback(self): self.assertEqual("origin/master", branch) self.assertTrue(w[0].message.args[0].startswith('Using "origin/master')) + + @with_isolated_runner + def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): + """ + Config location differs from the base directory for news file and fragments. + + This is useful when multiple projects share one towncrier configuration. + """ + main_branch = "main" + Path("pyproject.toml").write_text( + # Important to customize `config.directory` because the default + # already supports this scenario. + "[tool.towncrier]\n" + + 'directory = "changelog.d"\n' + ) + subproject1 = Path("foo") + (subproject1 / "foo").mkdir(parents=True) + (subproject1 / "foo/__init__.py").write_text("") + (subproject1 / "changelog.d").mkdir(parents=True) + (subproject1 / "changelog.d/123.feature").write_text("Adds levitation") + initial_commit(branch=main_branch) + call(["git", "checkout", "-b", "otherbranch"]) + + # We add a code change but forget to add a news fragment. + write(subproject1 / "foo/somefile.py", "import os") + commit("add a file") + result = runner.invoke( + towncrier_check, + ( + "--config", + "pyproject.toml", + "--dir", + str(subproject1), + "--compare-with", + "main", + ), + ) + + self.assertEqual(1, result.exit_code) + self.assertTrue( + result.output.endswith("No new newsfragments found on this branch.\n") + ) + + # We add the news fragment. + fragment_path = (subproject1 / "changelog.d/124.feature").absolute() + write(fragment_path, "Adds gravity back") + commit("add a newsfragment") + result = runner.invoke( + towncrier_check, + ("--config", "pyproject.toml", "--dir", "foo", "--compare-with", "main"), + ) + + self.assertEqual(0, result.exit_code, result.output) + self.assertTrue( + result.output.endswith("Found:\n1. " + str(fragment_path) + "\n"), + (result.output, str(fragment_path)), + ) + + # We add a change in a different subproject without a news fragment. + # Checking subproject1 should pass. + subproject2 = Path("bar") + (subproject2 / "bar").mkdir(parents=True) + (subproject2 / "changelog.d").mkdir(parents=True) + write(subproject2 / "bar/somefile.py", "import os") + commit("add a file") + result = runner.invoke( + towncrier_check, + ( + "--config", + "pyproject.toml", + "--dir", + subproject1, + "--compare-with", + "main", + ), + ) + + self.assertEqual(0, result.exit_code, result.output) + self.assertTrue( + result.output.endswith("Found:\n1. " + str(fragment_path) + "\n"), + (result.output, str(fragment_path)), + ) + + # Checking subproject2 should result in an error. + result = runner.invoke( + towncrier_check, + ( + "--config", + "pyproject.toml", + "--dir", + subproject2, + "--compare-with", + "main", + ), + ) + self.assertEqual(1, result.exit_code) + self.assertTrue( + result.output.endswith("No new newsfragments found on this branch.\n") + ) diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index bb33da4c..fea208c2 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -249,3 +249,35 @@ def test_create_orphan_fragment_custom_prefix(self, runner: CliRunner): self.assertEqual(len(change.stem), 11) # Check the remainder are all hex characters. self.assertTrue(all(c in string.hexdigits for c in change.stem[3:])) + + @with_isolated_runner + def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): + """ + Config location differs from the base directory for news file and fragments. + + This is useful when multiple projects share one towncrier configuration. + """ + Path("pyproject.toml").write_text( + # Important to customize `config.directory` because the default + # already supports this scenario. + "[tool.towncrier]\n" + + 'directory = "changelog.d"\n' + ) + Path("foo/foo").mkdir(parents=True) + Path("foo/foo/__init__.py").write_text("") + + result = runner.invoke( + _main, + ( + "--config", + "pyproject.toml", + "--dir", + "foo", + "--content", + "Adds levitation.", + "123.feature", + ), + ) + + self.assertEqual(0, result.exit_code) + self.assertTrue(Path("foo/changelog.d/123.feature").exists()) From e83df6a515b535bdbfc0863764b670592f840d8c Mon Sep 17 00:00:00 2001 From: Ilia Kurenkov Date: Wed, 18 Oct 2023 14:32:09 +0200 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Adi Roiban --- src/towncrier/newsfragments/548.feature | 4 ++-- src/towncrier/test/test_build.py | 11 ++++++----- src/towncrier/test/test_check.py | 5 ++--- src/towncrier/test/test_create.py | 8 +++++--- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/towncrier/newsfragments/548.feature b/src/towncrier/newsfragments/548.feature index 51b3adc9..c48f7670 100644 --- a/src/towncrier/newsfragments/548.feature +++ b/src/towncrier/newsfragments/548.feature @@ -1,2 +1,2 @@ -Full support for monorepo-style setup. -One project with multiple independent news files that share the same towncrier config. +Initial support was added for monorepo-style setup. +One project with multiple independent news files stored in separate sub-directories, that share the same towncrier config. diff --git a/src/towncrier/test/test_build.py b/src/towncrier/test/test_build.py index 9d42664e..ace112ac 100644 --- a/src/towncrier/test/test_build.py +++ b/src/towncrier/test/test_build.py @@ -146,11 +146,12 @@ def test_in_different_dir_config_option(self, runner): @with_isolated_runner def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): """ - Config location differs from the base directory for news file and fragments. - - This is useful when multiple projects share one towncrier configuration. - The default `newsfragments` setting already supports this scenario so here - we test that custom settings also do. + Using the `--dir` CLI argument, the NEWS file can + be generated in a sub-directory from fragments + that are relatives to that sub-directory. + + The path passed to `--dir` becomes the + working directory. """ Path("pyproject.toml").write_text( "[tool.towncrier]\n" + 'directory = "changelog.d"\n' diff --git a/src/towncrier/test/test_check.py b/src/towncrier/test/test_check.py index 43b07526..81861db3 100644 --- a/src/towncrier/test/test_check.py +++ b/src/towncrier/test/test_check.py @@ -301,9 +301,8 @@ def test_get_default_compare_branch_fallback(self): @with_isolated_runner def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): """ - Config location differs from the base directory for news file and fragments. - - This is useful when multiple projects share one towncrier configuration. + It can check the fragments located in a sub-directory + that is specified using the `--dir` CLI argument. """ main_branch = "main" Path("pyproject.toml").write_text( diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index fea208c2..7f0e24b1 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -253,9 +253,11 @@ def test_create_orphan_fragment_custom_prefix(self, runner: CliRunner): @with_isolated_runner def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): """ - Config location differs from the base directory for news file and fragments. - - This is useful when multiple projects share one towncrier configuration. + When the `--dir` CLI argument is passed, + it will create a new file in directory that is + created by combining the `--dir` value + with the `directory` option from the configuration + file. """ Path("pyproject.toml").write_text( # Important to customize `config.directory` because the default From 0625734fd6d7e857e3194d4f9e1168a776ffb7e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 12:33:22 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/towncrier/test/test_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/towncrier/test/test_build.py b/src/towncrier/test/test_build.py index ace112ac..9927939d 100644 --- a/src/towncrier/test/test_build.py +++ b/src/towncrier/test/test_build.py @@ -149,7 +149,7 @@ def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): Using the `--dir` CLI argument, the NEWS file can be generated in a sub-directory from fragments that are relatives to that sub-directory. - + The path passed to `--dir` becomes the working directory. """