diff --git a/README.md b/README.md index bdcf04e..bf34ed2 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ uvx --from 'release-creator' release-creator notes \ For automation, replace `release-creator` after `--from` with a tested `release-creator==VERSION`. The command reads the tagged section, maps `Added` to `Features` and `Fixed` to `Bug fixes`, changes linked author handles to GitHub mentions, and leaves bullets unwrapped. It keeps stable category anchors, omits empty categories when they are absent from the -source, and adds artifact and comparison links. Use `--artifact-label` for an ecosystem other than PyPI; omit -`--artifact-url` when there is no external artifact. +source, and adds artifact and comparison links. Use `--artifact-label` for an ecosystem other than PyPI, repeat both +options for each published package, and omit `--artifact-url` when there is no external artifact. The command also accepts a `.rst` changelog with the same category names. It links `:issue:` roles to issues and `:pr:` or `:pull:` roles to pull requests: diff --git a/changelog.d/+artifact-links.feature.rst b/changelog.d/+artifact-links.feature.rst new file mode 100644 index 0000000..ee58bfb --- /dev/null +++ b/changelog.d/+artifact-links.feature.rst @@ -0,0 +1,3 @@ +Repeat ``--artifact-url``, with a matching ``--artifact-label`` for each, to list every published package on one +line. Each package link starts with an emoji for its ecosystem, such as 🐍 for PyPI, and the comparison link now +reads ``📜 Full Changelog``. diff --git a/docs/how-to/render-notes.rst b/docs/how-to/render-notes.rst index 9627d4d..fd92c3a 100644 --- a/docs/how-to/render-notes.rst +++ b/docs/how-to/render-notes.rst @@ -61,9 +61,9 @@ the :doc:`tags command ` can find it. ``--artifact-url`` add - Avoid stale cache reads ([#42](https://github.com/owner/project/pull/42)) by @alice. - [PyPI release](https://pypi.org/project/project/1.2.3/) + [🐍 PyPI release](https://pypi.org/project/project/1.2.3/) - [Full Changelog](https://github.com/owner/project/compare/1.2.2...1.2.3) + [📜 Full Changelog](https://github.com/owner/project/compare/1.2.2...1.2.3) Review that file before passing it to ``gh release create --notes-file release-notes.md``. The ``by`` credit stays in the changelog; the GitHub body uses an ``@alice`` mention. @@ -84,7 +84,10 @@ Changelog`` line, ``notes`` replaces it with one link built from ``--previous`` anchors remain stable. See :doc:`the format reference <../reference/formats>` for every category name. Omit ``--artifact-url`` if there is no external artifact. Use ``--artifact-label`` to name another ecosystem's package -link. If ``--previous`` is absent, the final link points to the commits at this tag instead of a comparison. +link. A project that publishes several packages repeats ``--artifact-url``, each followed by its ``--artifact-label``; +the links share one line, separated by commas. Each link starts with an emoji picked from its host: 🐍 for PyPI, 🧩 for +the JetBrains Marketplace, and 📦 for anything else. If ``--previous`` is absent, the final link points to the commits +at this tag instead of a comparison. Shared changelog with separate packages ======================================= diff --git a/docs/tutorial/first-release.rst b/docs/tutorial/first-release.rst index 2383922..e66fa33 100644 --- a/docs/tutorial/first-release.rst +++ b/docs/tutorial/first-release.rst @@ -152,9 +152,9 @@ linked author to an ``@alice`` mention. With no previous numeric release, the fi - Avoid stale cache reads ([#42](https://github.com/owner/project/pull/42)) by @alice. - [PyPI release](https://pypi.org/project/project/0.1.0/) + [🐍 PyPI release](https://pypi.org/project/project/0.1.0/) - [Full Changelog](https://github.com/owner/project/commits/0.1.0) + [📜 Full Changelog](https://github.com/owner/project/commits/0.1.0) Review the complete body, then create the GitHub Release if the repository does not do that in its tag workflow: diff --git a/src/release_creator/notes/cli.py b/src/release_creator/notes/cli.py index 4d225a3..51dec1c 100644 --- a/src/release_creator/notes/cli.py +++ b/src/release_creator/notes/cli.py @@ -16,18 +16,28 @@ def register(commands: argparse._SubParsersAction[argparse.ArgumentParser]) -> N notes.add_argument("--repository", required=True, help="GitHub owner/repository") notes.add_argument("--package", help="package section in a shared Markdown changelog") notes.add_argument("--previous", help="previous tag for the comparison link") - notes.add_argument("--artifact-url", help="link to the published package or artifact") - notes.add_argument("--artifact-label", default="PyPI release", help="text for the artifact link") + notes.add_argument( + "--artifact-url", action="append", default=[], help="link to a published package or artifact; repeatable" + ) + notes.add_argument( + "--artifact-label", + action="append", + default=[], + help="text for each --artifact-url, in the same order; default PyPI release", + ) notes.set_defaults(handler=_render) def _render(args: argparse.Namespace) -> None: + labels = args.artifact_label or ["PyPI release"] * len(args.artifact_url) + if len(labels) != len(args.artifact_url): + msg = f"got {len(labels)} --artifact-label for {len(args.artifact_url)} --artifact-url" + raise ValueError(msg) context = ReleaseContext( tag=args.tag, repository=args.repository, previous=args.previous, - artifact_url=args.artifact_url, - artifact_label=args.artifact_label, + artifacts=tuple(zip(labels, args.artifact_url, strict=True)), package=args.package, ) changelog = args.changelog.read_text(encoding="utf-8") @@ -49,10 +59,10 @@ def _render(args: argparse.Namespace) -> None: explain(f"Comparison link: commits/{args.tag} (no --previous tag)") else: explain(f"Comparison link: compare/{args.previous}...{args.tag}") - if args.artifact_url is None: + if not args.artifact_url: explain("Artifact link: none (no --artifact-url)") - else: - explain(f"Artifact link: {args.artifact_label} -> {args.artifact_url}") + for label, url in context.artifacts: + explain(f"Artifact link: {label} -> {url}") sys.stdout.write(notes) diff --git a/src/release_creator/notes/render.py b/src/release_creator/notes/render.py index 678b24a..d36af41 100644 --- a/src/release_creator/notes/render.py +++ b/src/release_creator/notes/render.py @@ -5,6 +5,8 @@ lazy import re lazy from dataclasses import dataclass +lazy from typing import Final +lazy from urllib.parse import urlsplit lazy import mdformat lazy from packaging.version import InvalidVersion, Version @@ -12,6 +14,8 @@ lazy from release_creator.categories import RELEASE_CATEGORIES lazy from release_creator.notes.rst import TILDE_PLACEHOLDER, render_rst_section +_ECOSYSTEM_EMOJI: Final = {"pypi.org": "🐍", "plugins.jetbrains.com": "🧩"} + @dataclass(frozen=True, kw_only=True) class ReleaseContext: @@ -20,8 +24,7 @@ class ReleaseContext: tag: str repository: str previous: str | None = None - artifact_url: str | None = None - artifact_label: str = "PyPI release" + artifacts: tuple[tuple[str, str], ...] = () package: str | None = None @@ -49,15 +52,19 @@ def render_rst(changelog: str, context: ReleaseContext) -> str: def _finish(notes: str, context: ReleaseContext) -> str: - if context.artifact_url is not None: - link = f"[{context.artifact_label}]({context.artifact_url})" - notes = f"{notes}\n\n{link}" if notes else link + if context.artifacts: + links = ", ".join(f"[{_ecosystem_emoji(url)} {label}]({url})" for label, url in context.artifacts) + notes = f"{notes}\n\n{links}" if notes else links comparison = ( f"compare/{context.previous}...{context.tag}" if context.previous is not None else f"commits/{context.tag}" ) if notes: notes += "\n\n" - return f"{notes}[Full Changelog](https://github.com/{context.repository}/{comparison})\n" + return f"{notes}[📜 Full Changelog](https://github.com/{context.repository}/{comparison})\n" + + +def _ecosystem_emoji(url: str) -> str: + return _ECOSYSTEM_EMOJI.get(urlsplit(url).hostname or "", "📦") def _extract_rst_body(changelog: str, context: ReleaseContext) -> str: @@ -235,7 +242,13 @@ def _convert_markdown_body(lines: list[str], category_prefix: str) -> str: def _is_comparison_line(line: str) -> bool: - return line.strip().startswith(("**Full Changelog**:", "Full Changelog:", "[Full Changelog](", "`Full Changelog <")) + return line.strip().startswith(( + "**Full Changelog**:", + "Full Changelog:", + "[Full Changelog](", + "[📜 Full Changelog](", + "`Full Changelog <", + )) def _is_version(tag: str) -> bool: diff --git a/tests/release_creator/contributors/test_package.py b/tests/release_creator/contributors/test_package.py index cb92997..02522aa 100644 --- a/tests/release_creator/contributors/test_package.py +++ b/tests/release_creator/contributors/test_package.py @@ -98,7 +98,7 @@ def test_add_new_contributors_package_renders_in_notes(changelog: Path, github: '\n\n### New Contributors\n\n' "- [@alice](https://github.com/alice) made their first contribution in " "[#42](https://github.com/tox-dev/toml-fmt/pull/42)\n\n" - "[Full Changelog](https://github.com/tox-dev/toml-fmt/commits/pyproject-fmt/1.1.0)\n" + "[📜 Full Changelog](https://github.com/tox-dev/toml-fmt/commits/pyproject-fmt/1.1.0)\n" ) diff --git a/tests/release_creator/contributors/test_section.py b/tests/release_creator/contributors/test_section.py index 59e1986..796f0f4 100644 --- a/tests/release_creator/contributors/test_section.py +++ b/tests/release_creator/contributors/test_section.py @@ -416,7 +416,7 @@ def test_release_body_matches_changelog_section(tmp_path: Path, github: GitHub, tag="1.2.3", repository="tox-dev/example", previous="1.2.2", - artifact_url="https://pypi.org/project/example/1.2.3/", + artifacts=(("PyPI release", "https://pypi.org/project/example/1.2.3/"),), ) body = (render_markdown if extension == "md" else render_rst)(changelog.read_text(), context) @@ -439,8 +439,8 @@ def test_release_body_matches_changelog_section(tmp_path: Path, github: GitHub, + added + '\n\n### New Contributors\n\n' + contributor - + "[PyPI release](https://pypi.org/project/example/1.2.3/)\n\n" - "[Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3)\n" + + "[🐍 PyPI release](https://pypi.org/project/example/1.2.3/)\n\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3)\n" ) diff --git a/tests/release_creator/contributors/test_toml_fmt_changelog.py b/tests/release_creator/contributors/test_toml_fmt_changelog.py index 421e8c3..89c9912 100644 --- a/tests/release_creator/contributors/test_toml_fmt_changelog.py +++ b/tests/release_creator/contributors/test_toml_fmt_changelog.py @@ -65,7 +65,7 @@ def test_notes_package_drops_anchor_after_heading() -> None: "### Features\n\n" "- **Require 3.11 and default to 3.15.** ([#480](https://github.com/tox-dev/toml-fmt/pull/480)) by " "[@gaborbernat](https://github.com/gaborbernat).\n\n" - "[Full Changelog](https://github.com/tox-dev/toml-fmt/commits/pyproject-fmt/2.30.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/toml-fmt/commits/pyproject-fmt/2.30.3)\n" ) diff --git a/tests/release_creator/fragments/test_template.py b/tests/release_creator/fragments/test_template.py index 114cc27..e97dfad 100644 --- a/tests/release_creator/fragments/test_template.py +++ b/tests/release_creator/fragments/test_template.py @@ -50,7 +50,7 @@ def test_markdown_template_keeps_numbered_and_named_pull_requests( f"by [@alice](https://github.com/alice).{issue_suffix}\n" "- Fix a named bug ([#124](https://github.com/tox-dev/example/pull/124)) " "by [@alice](https://github.com/alice).\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -108,7 +108,7 @@ def test_markdown_template_preserves_prose_continuations(tmp_path: Path, source: assert notes == ( '\n\n### Bug fixes\n\n' f"{entry}\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -164,7 +164,7 @@ def test_markdown_template_preserves_balanced_literal_block(tmp_path: Path) -> N "- Add support ([#42](https://github.com/tox-dev/example/pull/42)) " "by [@alice](https://github.com/alice).\n\n" "
sample
\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -311,7 +311,7 @@ def test_rst_template_renders_notes_with_stable_anchors( '\n\n### Bug fixes\n\n' "- Fix the loader ([#2](https://github.com/tox-dev/example/pull/2)) by @alice. " "([#1](https://github.com/tox-dev/example/pull/1))\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -327,7 +327,7 @@ def test_rst_template_renders_literal_marker_credit(tmp_path: Path) -> None: '\n\n### Bug fixes\n\n' "- Fix the loader ([#2](https://github.com/tox-dev/example/pull/2)) by @alice:\n\n" " ```\n code output\n ```\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -342,7 +342,7 @@ def test_rst_template_preserves_two_paragraphs(tmp_path: Path) -> None: assert notes == ( '\n\n### Bug fixes\n\n' "- First. Second ([#2](https://github.com/tox-dev/example/pull/2)) by @alice.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) diff --git a/tests/release_creator/notes/test_explain.py b/tests/release_creator/notes/test_explain.py index 9c29561..26b8d2f 100644 --- a/tests/release_creator/notes/test_explain.py +++ b/tests/release_creator/notes/test_explain.py @@ -52,6 +52,36 @@ def test_notes_explains_on_stderr( ) +def test_notes_pairs_each_artifact_with_its_label( + changelog: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + _render( + monkeypatch, + changelog, + *("--artifact-url", "https://pypi.org/project/example/1.2.3/", "--artifact-label", "PyPI: example"), + *("--artifact-url", "https://pypi.org/project/example-bare/1.2.3/", "--artifact-label", "PyPI: example-bare"), + ) + + assert capsys.readouterr().err.splitlines()[-2:] == [ + "Artifact link: PyPI: example -> https://pypi.org/project/example/1.2.3/", + "Artifact link: PyPI: example-bare -> https://pypi.org/project/example-bare/1.2.3/", + ] + + +def test_notes_rejects_unpaired_artifact_label( + changelog: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + with pytest.raises(SystemExit): + _render( + monkeypatch, + changelog, + *("--artifact-url", "https://pypi.org/project/example/1.2.3/", "--artifact-label", "PyPI: example"), + *("--artifact-url", "https://pypi.org/project/example-bare/1.2.3/"), + ) + + assert capsys.readouterr().err.endswith("error: got 1 --artifact-label for 2 --artifact-url\n") + + def test_notes_keeps_stdout_to_the_body( changelog: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: @@ -59,7 +89,7 @@ def test_notes_keeps_stdout_to_the_body( assert capsys.readouterr().out == ( "### Features\n\n- Add a flag.\n- Add a cache.\n\n### Bug fixes\n\n- Correct a bug.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) diff --git a/tests/release_creator/notes/test_render.py b/tests/release_creator/notes/test_render.py index 4013685..c53cd96 100644 --- a/tests/release_creator/notes/test_render.py +++ b/tests/release_creator/notes/test_render.py @@ -38,7 +38,7 @@ def test_render_markdown_maps_categories_and_credits() -> None: tag="1.2.3", repository="tox-dev/example", previous="1.2.2", - artifact_url="https://pypi.org/project/example/1.2.3/", + artifacts=(("PyPI release", "https://pypi.org/project/example/1.2.3/"),), ) result = render_markdown(changelog, context) assert result == dedent("""\ @@ -48,15 +48,15 @@ def test_render_markdown_maps_categories_and_credits() -> None: @ENTRY@ - [PyPI release](https://pypi.org/project/example/1.2.3/) + [🐍 PyPI release](https://pypi.org/project/example/1.2.3/) - [Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3) + [📜 Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3) """).replace("@ENTRY@", entry) def test_render_markdown_first_release_without_artifact() -> None: result = render_markdown("## 0.1.0 (2026-02-13)\n", ReleaseContext(tag="0.1.0", repository="tox-dev/example")) - assert result == "[Full Changelog](https://github.com/tox-dev/example/commits/0.1.0)\n" + assert result == "[📜 Full Changelog](https://github.com/tox-dev/example/commits/0.1.0)\n" def test_render_markdown_preserves_legacy_other_changes() -> None: @@ -66,7 +66,7 @@ def test_render_markdown_preserves_legacy_other_changes() -> None: assert result == ( "### Other changes\n\n- Update development tooling.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -96,7 +96,7 @@ def test_render_markdown_accepts_two_part_historical_tags() -> None: assert result == ( "### Other changes\n\n- Old maintenance entry.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/compare/0.1...0.2)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/compare/0.1...0.2)\n" ) @@ -131,7 +131,7 @@ def test_render_markdown_accepts_existing_version_headings(heading: str) -> None assert result == ( "### Features\n\n- Support another platform.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/v1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/v1.2.3)\n" ) @@ -172,7 +172,7 @@ def test_render_markdown_accepts_historical_version_shapes(tag: str) -> None: assert result == ( "### Bug fixes\n\n- Correct the current release.\n\n" - f"[Full Changelog](https://github.com/tox-dev/example/commits/{tag})\n" + f"[📜 Full Changelog](https://github.com/tox-dev/example/commits/{tag})\n" ) @@ -213,7 +213,7 @@ def test_render_markdown_selects_package_history() -> None: assert result == ( "### Bug fixes\n\n- Preserve formatted input.\n\n" - "[Full Changelog](https://github.com/tox-dev/toml-fmt/compare/" + "[📜 Full Changelog](https://github.com/tox-dev/toml-fmt/compare/" "pyproject-fmt/2.30.3...pyproject-fmt/2.30.4)\n" ) @@ -278,6 +278,7 @@ def test_render_markdown_stops_at_next_package_release_anchor() -> None: pytest.param("**Full Changelog**: https://github.com/tox-dev/example/compare/old...1.2.3", id="bold"), pytest.param("Full Changelog: https://github.com/tox-dev/example/compare/old...1.2.3", id="plain"), pytest.param("[Full Changelog](https://github.com/tox-dev/example/compare/old...1.2.3)", id="link"), + pytest.param("[📜 Full Changelog](https://github.com/tox-dev/example/compare/old...1.2.3)", id="emoji-link"), ], ) def test_render_markdown_replaces_source_comparison(comparison: str) -> None: @@ -287,7 +288,7 @@ def test_render_markdown_replaces_source_comparison(comparison: str) -> None: assert result == ( "### Bug fixes\n\n- Correct a bug.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3)\n" ) @@ -704,7 +705,8 @@ def test_render_markdown_keeps_author_links_in_code(entry: str, expected: str) - result = render_markdown(changelog, ReleaseContext(tag="1.2.3", repository="tox-dev/example")) assert ( - result == f"### Bug fixes\n\n{expected}\n[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + result + == f"### Bug fixes\n\n{expected}\n[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -725,7 +727,7 @@ def test_render_markdown_accepts_credit_after_html_break(opening: str) -> None: assert result == ( "### Bug fixes\n\n" f"- Fix https://github.com/tox-dev/example/pull/5{opening}by @alice.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -746,7 +748,7 @@ def test_render_markdown_accepts_credit_across_one_list_item(separator: str, exp assert result == ( "### Bug fixes\n\n" f"- Fix [#5](https://github.com/tox-dev/example/pull/5){expected_break}by @alice.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -838,7 +840,7 @@ def test_render_rst_maps_categories_and_credits() -> None: '\n\n### Bug fixes\n\n' "- Discard bad cache entries ([#42](https://github.com/tox-dev/example/pull/42)) by @alice and @bob. " "Preserve `None` in the cache.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3)\n" ) @@ -865,7 +867,7 @@ def test_render_rst_keeps_top_level_warning_outside_bullet() -> None: "- A shim remains for Python 2.\n\n" "> **Warning:** Version `20.0.0b1` starts a new release history. The old history is in the " "[legacy docs](https://virtualenv.pypa.io/en/legacy/changes.html).\n\n" - "[Full Changelog](https://github.com/pypa/virtualenv/commits/20.0.0b1)\n" + "[📜 Full Changelog](https://github.com/pypa/virtualenv/commits/20.0.0b1)\n" ) @@ -921,7 +923,7 @@ def test_render_rst_keeps_nested_code_block(language: str, source: str, expected "- Display the virtual environment, for example:\n\n" f" ```{language}\n {expected}\n ```\n\n" " Patch by @alice. ([#2009](https://github.com/pypa/virtualenv/pull/2009))\n\n" - "[Full Changelog](https://github.com/pypa/virtualenv/commits/20.2.0)\n" + "[📜 Full Changelog](https://github.com/pypa/virtualenv/commits/20.2.0)\n" ) @@ -964,7 +966,7 @@ def test_render_rst_keeps_nested_bullets() -> None: "- Upgrade embedded wheels:\n" " - setuptools to `68.1.2` from `68.1.0` on `3.8+`\n" " - wheel to `0.41.2` from `0.41.1` on `3.7+` ([#2628](https://github.com/pypa/virtualenv/pull/2628))\n\n" - "[Full Changelog](https://github.com/pypa/virtualenv/commits/20.24.4)\n" + "[📜 Full Changelog](https://github.com/pypa/virtualenv/commits/20.24.4)\n" ) @@ -983,7 +985,7 @@ def test_render_rst_keeps_nested_by_design_bullet(detail: str) -> None: assert result == ( '\n\n### Features\n\n' f"- Introduce safe mode.\n - {detail}\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1034,7 +1036,7 @@ def test_render_rst_keeps_paragraph_after_nested_bullets() -> None: " - add a CLI flag\n" " - upgrade embedded wheels after 28 days\n\n" " More details under `wheels` by @alice. ([#1821](https://github.com/pypa/virtualenv/issues/1821))\n\n" - "[Full Changelog](https://github.com/pypa/virtualenv/commits/20.0.24)\n" + "[📜 Full Changelog](https://github.com/pypa/virtualenv/commits/20.0.24)\n" ) @@ -1061,7 +1063,7 @@ def test_render_rst_keeps_group_credit_after_nested_bullets() -> None: " - setuptools to `75.2.0`\n" " - Removed pip of `24.0`\n\n" " by @alice. ([#2783](https://github.com/pypa/virtualenv/pull/2783))\n\n" - "[Full Changelog](https://github.com/pypa/virtualenv/commits/20.27.0)\n" + "[📜 Full Changelog](https://github.com/pypa/virtualenv/commits/20.27.0)\n" ) @@ -1082,7 +1084,7 @@ def test_render_rst_joins_wrapped_author_credit() -> None: assert result == ( '\n\n### Bug fixes\n\n' "- Correct a long description. [#42](https://github.com/tox-dev/example/pull/42) by @alice and @bob.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1103,7 +1105,7 @@ def test_render_rst_joins_wrapped_author_marker() -> None: assert result == ( '\n\n### Bug fixes\n\n' "- Correct a long description. [#42](https://github.com/tox-dev/example/pull/42) by @alice and @bob.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1138,7 +1140,7 @@ def test_render_rst_preserves_legacy_other_changes() -> None: assert result == ( '\n\n' "### Other changes\n\n- Update development tooling.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1157,7 +1159,7 @@ def test_render_rst_accepts_two_part_historical_tags(heading: str) -> None: assert result == ( '\n\n' "### Other changes\n\n- Old maintenance entry.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/0.2)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/0.2)\n" ) @@ -1197,7 +1199,7 @@ def test_render_rst_accepts_historical_version_shapes(tag: str) -> None: assert result == ( f'\n\n' "### Bug fixes\n\n- Correct the current release.\n\n" - f"[Full Changelog](https://github.com/tox-dev/example/commits/{tag})\n" + f"[📜 Full Changelog](https://github.com/tox-dev/example/commits/{tag})\n" ) @@ -1225,7 +1227,7 @@ def test_render_rst_accepts_undated_linked_four_part_heading() -> None: assert result == ( '\n\n' "### Bug fixes\n\n- Correct the current release.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/0.12.1.0)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/0.12.1.0)\n" ) @@ -1245,7 +1247,7 @@ def test_render_rst_accepts_linked_prerelease_heading() -> None: assert result == ( '\n\n' "### Features\n\n- Add the first release.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/v2.0.0a1)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/v2.0.0a1)\n" ) @@ -1265,7 +1267,7 @@ def test_render_rst_omits_empty_prerelease_section() -> None: result = render_rst(changelog, ReleaseContext(tag="v2.0.0a2", repository="tox-dev/example")) - assert result == "[Full Changelog](https://github.com/tox-dev/example/commits/v2.0.0a2)\n" + assert result == "[📜 Full Changelog](https://github.com/tox-dev/example/commits/v2.0.0a2)\n" def test_render_rst_ignores_version_without_adornment() -> None: @@ -1339,7 +1341,7 @@ def test_render_rst_converts_external_link(source: str, expected: str) -> None: assert result == ( '\n\n### Features\n\n' f"- Read {expected}.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1417,7 +1419,7 @@ def test_render_rst_keeps_role_display(source: str, expected: str) -> None: assert result == ( '\n\n### Bug fixes\n\n' f"- Call {expected}.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1473,7 +1475,7 @@ def test_render_rst_preserves_field_literals(source: str, expected: str) -> None assert result == ( '\n\n### Bug fixes\n\n' f"- {expected}.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1495,7 +1497,7 @@ def test_render_rst_leaves_out_file_inclusion() -> None: assert result == ( '\n\n### Features\n\n- Add a feature.\n\n' - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1542,7 +1544,7 @@ def test_render_rst_keeps_duplicate_reference_labels() -> None: "- First [#42](https://github.com/tox-dev/example/issues/42).\n\n" '\n\n### Bug fixes\n\n' "- Second [#42](https://github.com/tox-dev/example/issues/42).\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1615,7 +1617,7 @@ def test_render_rst_links_pep_role(role: str, expected: str) -> None: assert result == ( '\n\n### Features\n\n' f"- Support {expected}.\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1634,14 +1636,16 @@ def test_render_rst_keeps_issue_role_as_issue() -> None: ) def test_render_empty_release_section_has_comparison_link(changelog: str, format_name: str) -> None: context = ReleaseContext( - tag="1.2.3", repository="tox-dev/example", artifact_url="https://pypi.org/project/example/1.2.3/" + tag="1.2.3", + repository="tox-dev/example", + artifacts=(("PyPI release", "https://pypi.org/project/example/1.2.3/"),), ) result = (render_markdown if format_name == "markdown" else render_rst)(changelog, context) assert result == ( - "[PyPI release](https://pypi.org/project/example/1.2.3/)\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[🐍 PyPI release](https://pypi.org/project/example/1.2.3/)\n\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1650,7 +1654,7 @@ def test_render_rst_omits_empty_category() -> None: result = render_rst(changelog, ReleaseContext(tag="1.2.3", repository="tox-dev/example")) assert result == ( '\n\n### Bug fixes\n\n- Correct a bug.\n\n' - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1669,7 +1673,7 @@ def test_render_rst_replaces_source_comparison(comparison: str) -> None: assert result == ( '\n\n### Bug fixes\n\n- Correct a bug.\n\n' - "[Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/compare/1.2.2...1.2.3)\n" ) @@ -1696,7 +1700,7 @@ def test_render_rst_stops_at_linked_previous_version() -> None: assert result == ( '\n\n### Bug fixes\n\n- Correct a bug.\n\n' - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1721,7 +1725,7 @@ def test_render_markdown_keeps_new_contributors_section() -> None: "### New Contributors\n\n" "- [@alice](https://github.com/alice) made their first contribution in " "[#42](https://github.com/tox-dev/example/pull/42)\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1750,7 +1754,7 @@ def test_render_rst_keeps_new_contributors_section() -> None: '\n\n' "### New Contributors\n\n" "- @alice made their first contribution in [#42](https://github.com/tox-dev/example/pull/42)\n\n" - "[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + "[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" ) @@ -1776,7 +1780,7 @@ def test_render_rst_keeps_new_contributors_section() -> None: def test_render_markdown_passes_layout_through(changelog: str, body: str) -> None: result = render_markdown(changelog, ReleaseContext(tag="1.2.3", repository="tox-dev/example")) - assert result == f"{body}\n\n[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + assert result == f"{body}\n\n[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" def test_render_markdown_uses_first_package_section() -> None: @@ -1791,7 +1795,7 @@ def test_render_markdown_uses_first_package_section() -> None: assert ( result - == "### Bug fixes\n\n- First.\n\n[Full Changelog](https://github.com/tox-dev/example/commits/example/1.2.3)\n" + == "### Bug fixes\n\n- First.\n\n[📜 Full Changelog](https://github.com/tox-dev/example/commits/example/1.2.3)\n" ) @@ -1823,4 +1827,37 @@ def test_render_markdown_uses_first_package_section() -> None: def test_render_rst_renders_unexpected_layout(changelog: str, body: str) -> None: result = render_rst(changelog, ReleaseContext(tag="1.2.3", repository="tox-dev/example")) - assert result == f"{body}\n\n[Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + assert result == f"{body}\n\n[📜 Full Changelog](https://github.com/tox-dev/example/commits/1.2.3)\n" + + +def test_render_lists_every_artifact_on_one_line() -> None: + context = ReleaseContext( + tag="1.2.3", + repository="tox-dev/example", + artifacts=( + ("PyPI: example", "https://pypi.org/project/example/1.2.3/"), + ("PyPI: example-bare", "https://pypi.org/project/example-bare/1.2.3/"), + ), + ) + + result = render_markdown("## 1.2.3 (today)\n", context) + + assert result.splitlines()[0] == ( + "[🐍 PyPI: example](https://pypi.org/project/example/1.2.3/), " + "[🐍 PyPI: example-bare](https://pypi.org/project/example-bare/1.2.3/)" + ) + + +@pytest.mark.parametrize( + ("url", "emoji"), + [ + pytest.param("https://plugins.jetbrains.com/plugin/1-example", "🧩", id="jetbrains"), + pytest.param("https://github.com/tox-dev/example/releases/download/1.2.3/example.pyz", "📦", id="other"), + ], +) +def test_render_marks_artifact_with_its_ecosystem(url: str, emoji: str) -> None: + context = ReleaseContext(tag="1.2.3", repository="tox-dev/example", artifacts=(("Example", url),)) + + result = render_markdown("## 1.2.3 (today)\n", context) + + assert result.splitlines()[0] == f"[{emoji} Example]({url})"