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/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_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())