Repository: py-cov-action/python-coverage-comment-action
Pull request #263 (2023-09-13 11:41:09)

Restore missing commits
Some commits seem to have been lost due to the change of the default branch and this PR adds these commits back.
So far this is just a copy of #213 but there may be more commits we need to recover.
There were also some Renovate-PRs that went missing like #241 and #240 but Renovate already took care of that in #245

(@ewjoachim I've sent you a message with some more details)

github-actions[bot]:   [End-to-end public repo](https://github.com/mihcaojwe/python-coverage-comment-action-end-to-end-263-public)

github-actions[bot]: ## Coverage report
The coverage rate went from `100%` to `100%` :arrow_right:
The branch rate is `100%`.

`100%` of new lines are covered.

<details>
<summary>Diff Coverage details (click to unfold)</summary>

### coverage_comment/github.py
`100%` of new lines are covered (`100%` of the complete file).

### coverage_comment/settings.py
`100%` of new lines are covered (`100%` of the complete file).

### coverage_comment/main.py
`100%` of new lines are covered (`100%` of the complete file).

### coverage_comment/template.py
`100%` of new lines are covered (`100%` of the complete file).

</details>
<!-- This comment was produced by python-coverage-comment-action -->
ewjoachim: I've checked that there doesn't seem to be other commits missing.

https://github.com/py-cov-action/python-coverage-comment-action/assets/1457576/fc0f58dd-126b-4575-80c9-dccc2dbc3431



Patch:
diff --git a/README.md b/README.md
index 3fda433..278dc94 100644
--- a/README.md
+++ b/README.md
@@ -166,7 +166,7 @@ jobs:
 ### Basic usage without external contributors
 
 If you don't expect external contributors, you don't need all the shenanigans
-with the artifacts and the 2nd workflow. This is likely to be the most straightfoward
+with the artifacts and the 2nd workflow. This is likely to be the most straightforward
 way to configure it for private repositories. It might look like this:
 
 ```yaml
@@ -198,7 +198,6 @@ jobs:
         run: make test # This is the part where you put your own test command
 
       - name: Coverage comment
-        id: coverage_comment
         uses: py-cov-action/python-coverage-comment-action@v3
         with:
           GITHUB_TOKEN: ${{ github.token }}
@@ -327,14 +326,25 @@ jobs:
     COMMENT_ARTIFACT_NAME: python-coverage-comment-action
 
     # Name of the file in which the body of the comment to post on the PR is stored.
-    # You typically don't have to change this unless you're already using this name for something else.
+    # In monorepo setting, see SUBPROJECT_ID.
     COMMENT_FILENAME: python-coverage-comment-action.txt
 
+    # This setting is only necessary if you plan to run the action multiple times
+    # in the same repository. It will be appended to the value of all the
+    # settings that need to be unique, so as for the action to avoid mixing
+    # up results of multiple runs.
+    # Affects `COMMENT_FILENAME`, `COVERAGE_DATA_BRANCH`.
+    # Ideally, use dashes (`-`) rather than underscrores (`_`) to split words,
+    # for consistency
+    SUBPROJECT_ID: null / "lib-name"
+
     # An alternative template for the comment for pull requests. See details below.
     COMMENT_TEMPLATE: The coverage rate is `{{ coverage.info.percent_covered | pct }}`{{ marker }}
 
     # Name of the branch in which coverage data will be stored on the repository.
-    # Please make sure that this branch is not protected.
+    # Default is 'python-coverage-comment-action-data'. Please make sure that this
+    # branch is not protected.
+    # In monorepo setting, see SUBPROJECT_ID.
     COVERAGE_DATA_BRANCH: python-coverage-comment-action-data
 
     # Deprecated, see https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging
@@ -383,6 +393,103 @@ coverage (percentage) of the whole project from the PR build:
 "Coverage: {{ coverage.info.percent_covered | pct }}{{ marker }}"
 ```
 
+## Monorepo setting
+
+In case you want to use the action multiple times with different parts of your
+source (so you have multiple codebases into a single repo), you'll
+need to use SUBPROJECT_ID with a different value for each launch. You may
+still use the same step for storing all files as artifacts. You'll end up with
+a different comment for each launch. Feel free to use the `COMMENT_TEMPLATE` if
+you want each comment to clearly state what it relates to.
+
+```yaml
+# .github/workflows/ci.yml
+name: CI
+
+on:
+  pull_request:
+  push:
+    branches:
+      - "main"
+
+jobs:
+  test:
+    name: Run tests & display coverage
+    runs-on: ubuntu-latest
+    permissions:
+      pull-requests: write
+      contents: write
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Test project 1
+        run: make -C project_1 test
+
+      - name: Test project 2
+        run: make -C project_2 test
+
+      - name: Coverage comment (project 1)
+        id: coverage_comment_1
+        uses: py-cov-action/python-coverage-comment-action@v3
+        with:
+          COVERAGE_PATH: project_1
+          SUBPROJECT_ID: project-1
+          GITHUB_TOKEN: ${{ github.token }}
+
+      - name: Coverage comment (project 2)
+        id: coverage_comment_2
+        uses: py-cov-action/python-coverage-comment-action@v3
+        with:
+          COVERAGE_PATH: project_2/src
+          SUBPROJECT_ID: project-2
+          GITHUB_TOKEN: ${{ github.token }}
+
+      - name: Store Pull Request comment to be posted
+        uses: actions/upload-artifact@v3
+        if: steps.coverage_comment_1.outputs.COMMENT_FILE_WRITTEN == 'true' || steps.coverage_comment_2.outputs.COMMENT_FILE_WRITTEN == 'true'
+        with:
+          name: python-coverage-comment-action
+          # Note the star
+          path: python-coverage-comment-action*.txt
+```
+
+```yaml
+# .github/workflows/coverage.yml
+name: Post coverage comment
+
+on:
+  workflow_run:
+    workflows: ["CI"]
+    types:
+      - completed
+
+jobs:
+  test:
+    name: Run tests & display coverage
+    runs-on: ubuntu-latest
+    if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
+    permissions:
+      pull-requests: write
+      contents: write
+      actions: read
+    steps:
+      - name: Post comment
+        uses: py-cov-action/python-coverage-comment-action@v3
+        with:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }}
+          SUBPROJECT_ID: project-1
+          COVERAGE_PATH: project_1
+
+      - name: Post comment
+        uses: py-cov-action/python-coverage-comment-action@v3
+        with:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }}
+          SUBPROJECT_ID: project-2
+          COVERAGE_PATH: project_2/src
+```
+
 # Other topics
 
 ## Pinning
diff --git a/action.yml b/action.yml
index ef328b7..42249d1 100644
--- a/action.yml
+++ b/action.yml
@@ -27,6 +27,7 @@ inputs:
       Name of the branch in which coverage data will be stored on the repository.
       Default is 'python-coverage-comment-action-data'. Please make sure that this
       branch is not protected.
+      In monorepo setting, see SUBPROJECT_ID.
     default: python-coverage-comment-action-data
     required: false
   COVERAGE_PATH:
@@ -38,13 +39,25 @@ inputs:
   COMMENT_ARTIFACT_NAME:
     description: >
       Name of the artifact in which the body of the comment to post on the PR is stored.
+      You typically don't have to change this unless you're already using this name for something else.
     default: python-coverage-comment-action
     required: false
   COMMENT_FILENAME:
     description: >
       Name of the file in which the body of the comment to post on the PR is stored.
+      In monorepo setting, see SUBPROJECT_ID.
     default: python-coverage-comment-action.txt
     required: false
+  SUBPROJECT_ID:
+    description: >
+      This setting is only necessary if you plan to run the action multiple
+      times in the same repository. It will be appended to the value of all the
+      settings that need to be unique, so as for the action to avoid mixing up
+      results of multiple runs. Ideally, use dashes (`-`) rather than
+      underscrores (`_`) to split words, for consistency.
+      Affects `COMMENT_FILENAME`, `COVERAGE_DATA_BRANCH`.
+    default: null
+    required: false
   MINIMUM_GREEN:
     description: >
       If the coverage percentage is above or equal to this value, the badge
@@ -67,7 +80,7 @@ inputs:
     default: false
   ANNOTATION_TYPE:
     description: >
-      Only needed if ANNOTATE_MISSING_LINES is set to true. This parameter allows you to choose between
+      Only relevant if ANNOTATE_MISSING_LINES is set to true. This parameter allows you to choose between
       notice, warning and error as annotation type. For more information look here:
       https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
     default: warning
@@ -78,8 +91,11 @@ inputs:
 outputs:
   COMMENT_FILE_WRITTEN:
     description: >
-      If "true", a comment file was written to COMMENT_FILENAME. If "false",
-      no comment file was written (because the comment was already posted).
+      This output is only set when running in PR mode. It's a boolean indicating
+      whether a comment file was written to COMMENT_FILENAME or not. If so,
+      you'll need to run the action in workflow_run mode to post it. If
+      "false", no comment file was written (likely because the comment was
+      already posted to the PR).
 runs:
   using: docker
   image: Dockerfile
@@ -91,6 +107,7 @@ runs:
     COVERAGE_PATH: ${{ inputs.COVERAGE_PATH }}
     COMMENT_ARTIFACT_NAME: ${{ inputs.COMMENT_ARTIFACT_NAME }}
     COMMENT_FILENAME: ${{ inputs.COMMENT_FILENAME }}
+    SUBPROJECT_ID: ${{ inputs.SUBPROJECT_ID }}
     MINIMUM_GREEN: ${{ inputs.MINIMUM_GREEN }}
     MINIMUM_ORANGE: ${{ inputs.MINIMUM_ORANGE }}
     MERGE_COVERAGE_FILES: ${{ inputs.MERGE_COVERAGE_FILES }}
diff --git a/coverage_comment/communication.py b/coverage_comment/communication.py
index 33a5e6f..bfe38d5 100644
--- a/coverage_comment/communication.py
+++ b/coverage_comment/communication.py
@@ -9,6 +9,7 @@ def get_readme_and_log(
     html_report_url: str,
     markdown_report: str,
     is_public: bool,
+    subproject_id: str | None = None,
 ) -> tuple[files.WriteFile, str]:
     readme_markdown = template.get_readme_markdown(
         is_public=is_public,
@@ -18,6 +19,7 @@ def get_readme_and_log(
         direct_image_url=image_urls["direct"],
         endpoint_image_url=image_urls["endpoint"],
         dynamic_image_url=image_urls["dynamic"],
+        subproject_id=subproject_id,
     )
     log_message = template.get_log_message(
         is_public=is_public,
@@ -26,6 +28,7 @@ def get_readme_and_log(
         direct_image_url=image_urls["direct"],
         endpoint_image_url=image_urls["endpoint"],
         dynamic_image_url=image_urls["dynamic"],
+        subproject_id=subproject_id,
     )
     readme = files.WriteFile(
         path=pathlib.Path("README.md"),
diff --git a/coverage_comment/main.py b/coverage_comment/main.py
index 8d55ea2..a192895 100644
--- a/coverage_comment/main.py
+++ b/coverage_comment/main.py
@@ -131,13 +131,14 @@ def process_pr(
         previous_coverage_data_file = storage.get_datafile_contents(
             github=gh,
             repository=config.GITHUB_REPOSITORY,
-            branch=config.COVERAGE_DATA_BRANCH,
+            branch=config.FINAL_COVERAGE_DATA_BRANCH,
         )
 
     previous_coverage = None
     if previous_coverage_data_file:
         previous_coverage = files.parse_datafile(contents=previous_coverage_data_file)
 
+    marker = template.get_marker(marker_id=config.SUBPROJECT_ID)
     try:
         comment = template.get_comment_markdown(
             coverage=coverage,
@@ -146,6 +147,8 @@ def process_pr(
             base_template=template.read_template_file("comment.md.j2"),
             custom_template=config.COMMENT_TEMPLATE,
             pr_targets_default_branch=pr_targets_default_branch,
+            marker=marker,
+            subproject_id=config.SUBPROJECT_ID,
         )
     except template.MissingMarker:
         log.error(
@@ -198,7 +201,7 @@ def process_pr(
             repository=config.GITHUB_REPOSITORY,
             pr_number=pr_number,
             contents=comment,
-            marker=template.MARKER,
+            marker=marker,
         )
     except github.CannotPostComment:
         log.debug("Exception when posting comment", exc_info=True)
@@ -209,7 +212,7 @@ def process_pr(
             "on PR comments for external PRs)."
         )
         comment_file.store_file(
-            filename=config.COMMENT_FILENAME,
+            filename=config.FINAL_COMMENT_FILENAME,
             content=comment,
         )
         github.set_output(github_output=config.GITHUB_OUTPUT, COMMENT_FILE_WRITTEN=True)
@@ -263,7 +266,7 @@ def post_comment(
             repository=config.GITHUB_REPOSITORY,
             artifact_name=config.COMMENT_ARTIFACT_NAME,
             run_id=config.GITHUB_PR_RUN_ID,
-            filename=config.COMMENT_FILENAME,
+            filename=config.FINAL_COMMENT_FILENAME,
         )
     except github.NoArtifact:
         log.info(
@@ -279,7 +282,7 @@ def post_comment(
         repository=config.GITHUB_REPOSITORY,
         pr_number=pr_number,
         contents=comment,
-        marker=template.MARKER,
+        marker=template.get_marker(marker_id=config.SUBPROJECT_ID),
     )
     log.info("Comment posted in PR")
 
@@ -327,14 +330,14 @@ def save_coverage_data_files(
         storage.get_raw_file_url,
         is_public=is_public,
         repository=config.GITHUB_REPOSITORY,
-        branch=config.COVERAGE_DATA_BRANCH,
+        branch=config.FINAL_COVERAGE_DATA_BRANCH,
     )
     readme_url = storage.get_repo_file_url(
-        branch=config.COVERAGE_DATA_BRANCH,
+        branch=config.FINAL_COVERAGE_DATA_BRANCH,
         repository=config.GITHUB_REPOSITORY,
     )
     html_report_url = storage.get_html_report_url(
-        branch=config.COVERAGE_DATA_BRANCH,
+        branch=config.FINAL_COVERAGE_DATA_BRANCH,
         repository=config.GITHUB_REPOSITORY,
     )
     readme_file, log_message = communication.get_readme_and_log(
@@ -343,12 +346,13 @@ def save_coverage_data_files(
         image_urls=files.get_urls(url_getter=url_getter),
         html_report_url=html_report_url,
         markdown_report=markdown_report,
+        subproject_id=config.SUBPROJECT_ID,
     )
     operations.append(readme_file)
     storage.commit_operations(
         operations=operations,
         git=git,
-        branch=config.COVERAGE_DATA_BRANCH,
+        branch=config.FINAL_COVERAGE_DATA_BRANCH,
     )
 
     log.info(log_message)
diff --git a/coverage_comment/settings.py b/coverage_comment/settings.py
index 21a55a8..70f77d0 100644
--- a/coverage_comment/settings.py
+++ b/coverage_comment/settings.py
@@ -51,6 +51,7 @@ class Config:
     COVERAGE_PATH: pathlib.Path = pathlib.Path(".")
     COMMENT_ARTIFACT_NAME: str = "python-coverage-comment-action"
     COMMENT_FILENAME: pathlib.Path = pathlib.Path("python-coverage-comment-action.txt")
+    SUBPROJECT_ID: str | None = None
     GITHUB_OUTPUT: pathlib.Path | None = None
     MINIMUM_GREEN: decimal.Decimal = decimal.Decimal("100")
     MINIMUM_ORANGE: decimal.Decimal = decimal.Decimal("70")
@@ -132,6 +133,20 @@ class Config:
             return self.GITHUB_REF.split("/", 2)[2]
         return None
 
+    @property
+    def FINAL_COMMENT_FILENAME(self):
+        filename = self.COMMENT_FILENAME
+        if self.SUBPROJECT_ID:
+            new_name = f"{filename.stem}-{self.SUBPROJECT_ID}{filename.suffix}"
+            return filename.parent / new_name
+        return filename
+
+    @property
+    def FINAL_COVERAGE_DATA_BRANCH(self):
+        return self.COVERAGE_DATA_BRANCH + (
+            f"-{self.SUBPROJECT_ID}" if self.SUBPROJECT_ID else ""
+        )
+
     # We need to type environ as a MutableMapping because that's what
     # os.environ is, and just saying `dict[str, str]` is not enough to make
     # mypy happy
diff --git a/coverage_comment/template.py b/coverage_comment/template.py
index f212204..446fcc0 100644
--- a/coverage_comment/template.py
+++ b/coverage_comment/template.py
@@ -7,7 +7,9 @@ from jinja2.sandbox import SandboxedEnvironment
 
 from coverage_comment import coverage as coverage_module
 
-MARKER = """<!-- This comment was produced by python-coverage-comment-action -->"""
+MARKER = (
+    """<!-- This comment was produced by python-coverage-comment-action{id_part} -->"""
+)
 
 
 def uptodate():
@@ -39,11 +41,17 @@ class TemplateError(Exception):
     pass
 
 
+def get_marker(marker_id: str | None):
+    return MARKER.format(id_part=f" (id: {marker_id})" if marker_id else "")
+
+
 def get_comment_markdown(
     coverage: coverage_module.Coverage,
     diff_coverage: coverage_module.DiffCoverage,
     previous_coverage_rate: decimal.Decimal | None,
     base_template: str,
+    marker: str,
+    subproject_id: str | None = None,
     custom_template: str | None = None,
     pr_targets_default_branch: bool = True,
 ):
@@ -56,13 +64,14 @@ def get_comment_markdown(
             previous_coverage_rate=previous_coverage_rate,
             coverage=coverage,
             diff_coverage=diff_coverage,
-            marker=MARKER,
+            subproject_id=subproject_id,
+            marker=marker,
             pr_targets_default_branch=pr_targets_default_branch,
         )
     except jinja2.exceptions.TemplateError as exc:
         raise TemplateError from exc
 
-    if MARKER not in comment:
+    if marker not in comment:
         raise MissingMarker()
 
     return comment
@@ -76,6 +85,7 @@ def get_readme_markdown(
     html_report_url: str | None,
     dynamic_image_url: str | None,
     endpoint_image_url: str | None,
+    subproject_id: str | None = None,
 ):
     env = SandboxedEnvironment()
     template = jinja2.Template(read_template_file("readme.md.j2"))
@@ -87,6 +97,7 @@ def get_readme_markdown(
         html_report_url=html_report_url,
         dynamic_image_url=dynamic_image_url,
         endpoint_image_url=endpoint_image_url,
+        subproject_id=subproject_id,
     )
 
 
@@ -97,6 +108,7 @@ def get_log_message(
     html_report_url: str | None,
     dynamic_image_url: str | None,
     endpoint_image_url: str | None,
+    subproject_id: str | None = None,
 ):
     env = SandboxedEnvironment()
     template = jinja2.Template(read_template_file("log.txt.j2"))
@@ -107,6 +119,7 @@ def get_log_message(
         endpoint_image_url=endpoint_image_url,
         dynamic_image_url=dynamic_image_url,
         readme_url=readme_url,
+        subproject_id=subproject_id,
     )
 
 
diff --git a/coverage_comment/template_files/comment.md.j2 b/coverage_comment/template_files/comment.md.j2
index 860911e..a4d6339 100644
--- a/coverage_comment/template_files/comment.md.j2
+++ b/coverage_comment/template_files/comment.md.j2
@@ -1,4 +1,4 @@
-{% block title %}## Coverage report{% endblock title %}
+{% block title %}## Coverage report{% if subproject_id %} ({{ subproject_id  }}){% endif %}{% endblock title %}
 {% block coverage_evolution -%}
 {% if previous_coverage_rate -%}
 {% block coverage_evolution_wording -%}
diff --git a/coverage_comment/template_files/log.txt.j2 b/coverage_comment/template_files/log.txt.j2
index 0b69f09..b8d252c 100644
--- a/coverage_comment/template_files/log.txt.j2
+++ b/coverage_comment/template_files/log.txt.j2
@@ -1,3 +1,6 @@
+{% if subproject_id %} Coverage info for {{ subproject_id }}:
+
+{% endif -%}
 {% if is_public -%}
 You can browse the full coverage report at:
     {{ html_report_url }}
diff --git a/coverage_comment/template_files/readme.md.j2 b/coverage_comment/template_files/readme.md.j2
index 8f85075..e7d7d69 100644
--- a/coverage_comment/template_files/readme.md.j2
+++ b/coverage_comment/template_files/readme.md.j2
@@ -1,4 +1,4 @@
-# Repository Coverage
+# Repository Coverage{% if subproject_id %} ({{ subproject_id }}){% endif %}
 
 {% if is_public -%}
 [Full report]({{ html_report_url }})


Test patch:
diff --git a/tests/end_to_end/conftest.py b/tests/end_to_end/conftest.py
index 80e0b1a..177ae05 100644
--- a/tests/end_to_end/conftest.py
+++ b/tests/end_to_end/conftest.py
@@ -191,7 +191,7 @@ def gh_other_username(gh_other):
 
 
 @pytest.fixture
-def git_repo(cd, git, action_ref, code_path):
+def git_repo(cd, git, action_ref, code_path, subproject_id):
     with cd("repo") as repo:
         git("init", "-b", "main")
         # Copy .github
@@ -213,6 +213,7 @@ def git_repo(cd, git, action_ref, code_path):
                 file.read_text()
                 .replace("__ACTION_REF__", action_ref)
                 .replace("__ACTION_COVERAGE_PATH__", str(code_path))
+                .replace("__ACTION_SUBPROJECT_ID__", json_module.dumps(subproject_id))
             )
             file.write_text(content)
 
@@ -239,6 +240,12 @@ def code_path(request):
     return pathlib.Path(*mark.args) if mark else pathlib.Path(".")
 
 
+@pytest.fixture
+def subproject_id(request):
+    mark = request.node.get_closest_marker("subproject_id")
+    return mark.args[0] if mark else None
+
+
 @pytest.fixture
 def repo_full_name(repo_name, gh_me_username):
     return f"{gh_me_username}/{repo_name}"
diff --git a/tests/end_to_end/repo/.github/workflows/ci.yml b/tests/end_to_end/repo/.github/workflows/ci.yml
index 3bfce29..84d837d 100644
--- a/tests/end_to_end/repo/.github/workflows/ci.yml
+++ b/tests/end_to_end/repo/.github/workflows/ci.yml
@@ -41,10 +41,11 @@ jobs:
           ANNOTATE_MISSING_LINES: true
           ANNOTATION_TYPE: notice
           COVERAGE_PATH: __ACTION_COVERAGE_PATH__
+          SUBPROJECT_ID: __ACTION_SUBPROJECT_ID__
 
       - name: Store Pull Request comment to be posted
         uses: actions/upload-artifact@v3
         if: steps.coverage_comment.outputs.COMMENT_FILE_WRITTEN == 'true'
         with:
           name: python-coverage-comment-action
-          path: python-coverage-comment-action.txt
+          path: python-coverage-comment-action*.txt
diff --git a/tests/end_to_end/repo/.github/workflows/coverage-comment.yml b/tests/end_to_end/repo/.github/workflows/coverage-comment.yml
index 9db6642..26668d7 100644
--- a/tests/end_to_end/repo/.github/workflows/coverage-comment.yml
+++ b/tests/end_to_end/repo/.github/workflows/coverage-comment.yml
@@ -22,3 +22,4 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }}
           COVERAGE_PATH: __ACTION_COVERAGE_PATH__
+          SUBPROJECT_ID: __ACTION_SUBPROJECT_ID__
diff --git a/tests/end_to_end/test_all.py b/tests/end_to_end/test_all.py
index ea2f366..39053c9 100644
--- a/tests/end_to_end/test_all.py
+++ b/tests/end_to_end/test_all.py
@@ -6,6 +6,7 @@ import pytest
 
 @pytest.mark.repo_suffix("public")
 @pytest.mark.code_path("subdir")
+@pytest.mark.subproject_id("my-great-project")
 def test_public_repo(
     gh_create_repo,
     wait_for_run_to_start,
@@ -71,15 +72,11 @@ def test_public_repo(
     assert number_of_svgs == 3
 
     # Check that logs point to the branch that has the readme file.
-    data_branch_url = (
-        f"https://github.com/{repo_full_name}/tree/python-coverage-comment-action-data"
-    )
+    data_branch_url = f"https://github.com/{repo_full_name}/tree/python-coverage-comment-action-data-my-great-project"
     assert data_branch_url in links
 
     # Time to check the Readme contents
-    raw_url_prefix = (
-        f"https://github.com/{repo_full_name}/raw/python-coverage-comment-action-data"
-    )
+    raw_url_prefix = f"https://github.com/{repo_full_name}/raw/python-coverage-comment-action-data-my-great-project"
 
     readme_url = f"{raw_url_prefix}/README.md"
     response = client.get(readme_url, follow_redirects=True)
@@ -126,6 +123,11 @@ def test_public_repo(
         fail_value="\n",
     )
     assert ":arrow_up:" in comment
+    assert "## Coverage report (my-great-project)" in comment
+    assert (
+        "This comment was produced by python-coverage-comment-action (id: my-great-project)"
+        in comment
+    )
 
     # Let's merge the PR and see if everything works fine
     gh_me("pr", "merge", "1", "--merge")
diff --git a/tests/unit/test_communication.py b/tests/unit/test_communication.py
index 7fdb2b0..033cdf7 100644
--- a/tests/unit/test_communication.py
+++ b/tests/unit/test_communication.py
@@ -35,6 +35,23 @@ def test_get_readme_and_log__public():
     assert "https://c" in log
 
 
+def test_get_readme_and_log__subproject():
+    readme_file, log = communication.get_readme_and_log(
+        is_public=True,
+        image_urls={
+            "direct": "https://a",
+            "endpoint": "https://b",
+            "dynamic": "https://c",
+        },
+        readme_url="https://readme",
+        html_report_url="https://html_report",
+        markdown_report="**Hello report!**",
+        subproject_id="my-subproject",
+    )
+    assert "Coverage info for my-subproject" in log
+    assert "# Repository Coverage (my-subproject)" in readme_file.contents
+
+
 def test_get_readme_and_log__private():
     readme_file, log = communication.get_readme_and_log(
         is_public=False,
diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py
index d45f6ba..2803f1e 100644
--- a/tests/unit/test_settings.py
+++ b/tests/unit/test_settings.py
@@ -35,6 +35,7 @@ def test_config__from_environ__ok():
             "GITHUB_STEP_SUMMARY": "step_summary",
             "COMMENT_ARTIFACT_NAME": "baz",
             "COMMENT_FILENAME": "qux",
+            "SUBPROJECT_ID": "subproject",
             "COMMENT_TEMPLATE": "footemplate",
             "COVERAGE_DATA_BRANCH": "branchname",
             "COVERAGE_PATH": "source_folder/",
@@ -57,6 +58,7 @@ def test_config__from_environ__ok():
         GITHUB_STEP_SUMMARY=pathlib.Path("step_summary"),
         COMMENT_ARTIFACT_NAME="baz",
         COMMENT_FILENAME=pathlib.Path("qux"),
+        SUBPROJECT_ID="subproject",
         COMMENT_TEMPLATE="footemplate",
         COVERAGE_DATA_BRANCH="branchname",
         COVERAGE_PATH=pathlib.Path("source_folder/"),
@@ -167,3 +169,19 @@ def test_config__invalid_annotation_type():
 )
 def test_str_to_bool(input, output):
     assert settings.str_to_bool(input) is output
+
+
+def test_final_comment_filename(config):
+    config_obj = config(
+        COMMENT_FILENAME=pathlib.Path("foo.txt"),
+        SUBPROJECT_ID="bar",
+    )
+    assert config_obj.FINAL_COMMENT_FILENAME == pathlib.Path("foo-bar.txt")
+
+
+def test_final_coverage_data_branch(config):
+    config_obj = config(
+        COVERAGE_DATA_BRANCH="foo",
+        SUBPROJECT_ID="bar",
+    )
+    assert config_obj.FINAL_COVERAGE_DATA_BRANCH == "foo-bar"
diff --git a/tests/unit/test_template.py b/tests/unit/test_template.py
index 52bda8d..0a8cc64 100644
--- a/tests/unit/test_template.py
+++ b/tests/unit/test_template.py
@@ -13,6 +13,7 @@ def test_get_comment_markdown(coverage_obj, diff_coverage_obj):
             coverage=coverage_obj,
             diff_coverage=diff_coverage_obj,
             previous_coverage_rate=decimal.Decimal("0.92"),
+            marker="<!-- foo -->",
             base_template="""
         {{ previous_coverage_rate | pct }}
         {{ coverage.info.percent_covered | pct }}
@@ -33,7 +34,7 @@ def test_get_comment_markdown(coverage_obj, diff_coverage_obj):
         "75%",
         "80%",
         "bar",
-        "<!-- This comment was produced by python-coverage-comment-action -->",
+        "<!-- foo -->",
     ]
 
     assert result == expected
@@ -45,11 +46,13 @@ def test_template(coverage_obj, diff_coverage_obj):
         diff_coverage=diff_coverage_obj,
         previous_coverage_rate=decimal.Decimal("0.92"),
         base_template=template.read_template_file("comment.md.j2"),
+        marker="<!-- foo -->",
+        subproject_id="foo",
         custom_template="""{% extends "base" %}
         {% block emoji_coverage_down %}:sob:{% endblock emoji_coverage_down %}
         """,
     )
-    expected = """## Coverage report
+    expected = """## Coverage report (foo)
 The coverage rate went from `92%` to `75%` :sob:
 The branch rate is `50%`.
 
@@ -63,7 +66,7 @@ The branch rate is `50%`.
 Missing lines: `7`, `9`
 
 </details>
-<!-- This comment was produced by python-coverage-comment-action -->"""
+<!-- foo -->"""
     assert result == expected
 
 
@@ -147,6 +150,7 @@ def test_template_full():
         coverage=cov,
         diff_coverage=diff_cov,
         previous_coverage_rate=decimal.Decimal("1.0"),
+        marker="<!-- foo -->",
         base_template=template.read_template_file("comment.md.j2"),
     )
     expected = """## Coverage report
@@ -166,7 +170,7 @@ Missing lines: `5`
 `100%` of new lines are covered (`100%` of the complete file).
 
 </details>
-<!-- This comment was produced by python-coverage-comment-action -->"""
+<!-- foo -->"""
     assert result == expected
 
 
@@ -183,6 +187,7 @@ def test_template__no_new_lines_with_coverage(coverage_obj):
         coverage=coverage_obj,
         diff_coverage=diff_cov,
         previous_coverage_rate=decimal.Decimal("1.0"),
+        marker="<!-- foo -->",
         base_template=template.read_template_file("comment.md.j2"),
     )
     expected = """## Coverage report
@@ -192,7 +197,7 @@ The branch rate is `50%`.
 _None of the new lines are part of the tested code. Therefore, there is no coverage data about them._
 
 
-<!-- This comment was produced by python-coverage-comment-action -->"""
+<!-- foo -->"""
     assert result == expected
 
 
@@ -201,6 +206,7 @@ def test_template__no_branch_no_previous(coverage_obj_no_branch, diff_coverage_o
         coverage=coverage_obj_no_branch,
         diff_coverage=diff_coverage_obj,
         previous_coverage_rate=None,
+        marker="<!-- foo -->",
         base_template=template.read_template_file("comment.md.j2"),
     )
     expected = """## Coverage report
@@ -221,13 +227,13 @@ The coverage rate is `75%`.
 Missing lines: `7`, `9`
 
 </details>
-<!-- This comment was produced by python-coverage-comment-action -->"""
+<!-- foo -->"""
     assert result == expected
 
 
 def test_read_template_file():
     assert template.read_template_file("comment.md.j2").startswith(
-        "{% block title %}## Coverage report{% endblock title %}"
+        "{% block title %}## Coverage report{% if subproject_id %}"
     )
 
 
@@ -238,6 +244,7 @@ def test_template__no_marker(coverage_obj, diff_coverage_obj):
             diff_coverage=diff_coverage_obj,
             previous_coverage_rate=decimal.Decimal("0.92"),
             base_template=template.read_template_file("comment.md.j2"),
+            marker="<!-- foo -->",
             custom_template="""foo bar""",
         )
 
@@ -249,6 +256,7 @@ def test_template__broken_template(coverage_obj, diff_coverage_obj):
             diff_coverage=diff_coverage_obj,
             previous_coverage_rate=decimal.Decimal("0.92"),
             base_template=template.read_template_file("comment.md.j2"),
+            marker="<!-- foo -->",
             custom_template="""{% extends "foo" %}""",
         )
 
@@ -270,3 +278,17 @@ def test_pct(value, displayed_coverage):
 
 def test_uptodate():
     assert template.uptodate() is True
+
+
+@pytest.mark.parametrize(
+    "marker_id, result",
+    [
+        (None, "<!-- This comment was produced by python-coverage-comment-action -->"),
+        (
+            "foo",
+            "<!-- This comment was produced by python-coverage-comment-action (id: foo) -->",
+        ),
+    ],
+)
+def test_get_marker(marker_id, result):
+    assert template.get_marker(marker_id=marker_id) == result