Skip to content

Commit

Permalink
Full documented support for sharing config between multiple projects
Browse files Browse the repository at this point in the history
  • Loading branch information
iliakur committed Oct 9, 2023
1 parent 0da67ff commit e6a6c8b
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 44 deletions.
3 changes: 2 additions & 1 deletion docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Narrative

tutorial
markdown
monorepo


Reference
Expand Down
52 changes: 52 additions & 0 deletions docs/monorepo.rst
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 5 additions & 5 deletions src/towncrier/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("----")
Expand All @@ -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(
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/towncrier/newsfragments/548.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Full support for monorepo-style setup.
One project with multiple independent news files that share the same towncrier config.
63 changes: 26 additions & 37 deletions src/towncrier/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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())
99 changes: 99 additions & 0 deletions src/towncrier/test/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
32 changes: 32 additions & 0 deletions src/towncrier/test/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

0 comments on commit e6a6c8b

Please sign in to comment.