Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions continuous_delivery_scripts/spdx_report/spdx_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
get_package_manual_check,
get_package_manual_licence,
)
from continuous_delivery_scripts.utils.git_helpers import get_project_remote_url
from continuous_delivery_scripts.utils.github_helpers import github_blob_url
from continuous_delivery_scripts.spdx_report.spdx_package import SpdxPackage
from continuous_delivery_scripts.spdx_report.licence_assessment import (
LicenceAssessment,
Expand Down Expand Up @@ -104,6 +106,20 @@ def generate_file_based_on_template(
output_filename.write_text(rendered, encoding="utf8")


def _project_licence_href() -> Optional[str]:
project_root = Path(str(configuration.get_value(ConfigurationVariable.PROJECT_ROOT))).resolve()
remote = get_project_remote_url(project_root)
if not remote:
return None
licence_path = Path(str(configuration.get_value(ConfigurationVariable.LICENCE_FILE_PATH)))
try:
relative_path = licence_path.relative_to(project_root) if licence_path.is_absolute() else licence_path
except ValueError:
return None
branch = str(configuration.get_value(ConfigurationVariable.MASTER_BRANCH))
return github_blob_url(remote, branch, str(relative_path).replace("\\", "/"))


class SummaryGenerator:
"""Licensing summary generator."""

Expand Down Expand Up @@ -154,6 +170,7 @@ def _generate_template_arguments(self) -> Dict[str, Any]:
arguments["project"] = {
"name": self.project.name,
"licence": self.project.main_licence,
"licence_href": _project_licence_href(),
"accepted_licences": _configured_licence_policy(),
"compliance": global_compliance,
"compliance_points": compliance_points,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@
<span class="status status--error">Not compliant</span>
{% endif %}
<div class="summary-copy">
{% if project.licence_href %}
<p>The project is licensed under the <a href="{{ project.licence_href }}"><strong>{{ project.licence }}</strong></a> licence.</p>
{% else %}
<p>The project is licensed under the <strong>{{ project.licence }}</strong> licence.</p>
{% endif %}
{% for point in project.compliance_points %}
<p>{{ point }}</p>
{% else %}
Expand Down
14 changes: 13 additions & 1 deletion continuous_delivery_scripts/utils/github_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
from functools import lru_cache
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit, urlunsplit
from urllib.parse import quote, urlsplit, urlunsplit
from urllib.request import Request, urlopen


Expand Down Expand Up @@ -73,3 +73,15 @@ def github_pages_url(remote: str) -> str | None:
"""Find a verified public Pages site, including a redirected custom domain."""
candidate = github_pages_candidate(remote)
return _verified_pages_url(candidate) if candidate else None


def github_blob_url(remote: str, branch: str, path: str) -> str | None:
"""Build a GitHub blob URL for a repository-relative file path."""
repository = _github_repository_path(remote)
if not repository:
return None
cleaned = path.replace("\\", "/").strip("/")
if not cleaned:
return None
encoded_path = "/".join(quote(part, safe="") for part in cleaned.split("/"))
return f"https://github.com/{repository}/blob/{quote(branch, safe='')}/{encoded_path}"
1 change: 1 addition & 0 deletions news/202610142015000000001.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:link: Link the project licence shown in the HTML third-party report to the configured repository licence file.
38 changes: 37 additions & 1 deletion tests/spdx/test_spdx_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,49 @@ def test_generated_report_is_linked_from_documentation_index_once(self):
self.assertEqual(html.count('href="third_party_IP_report.html"'), 1)
self.assertLess(html.index("third-party-ip-report"), html.index("</main>"))
report = Path(output_dir, "third_party_IP_report.html").read_text(encoding="utf8")
self.assertIn("<p>The project is licensed under the <strong>MIT</strong> licence.</p>", report)
expected_licence_link = (
"<p>The project is licensed under the "
'<a href="https://github.com/ARMmbed/continuous-delivery-scripts/blob/main/LICENSE">'
"<strong>MIT</strong></a> licence.</p>"
)
self.assertIn(
expected_licence_link,
report,
)
self.assertIn("<p>The project and all assessed dependencies meet the configured licence policy, ", report)
text_report = Path(output_dir, "third_party_IP_report.txt").read_text(encoding="utf8")
self.assertIn("The project is licensed under the MIT licence.", text_report)
self.assertIn("Audit gaps highlight missing or uncertain dependency licence information", report)
self.assertIn("No dependencies were included in this report.", report)

def test_html_report_links_project_licence_to_repository_file(self):
metadata = ProjectMetadata("test_package")
metadata.project_metadata = PackageMetadata({"Name": "test_package", "License": "MIT"})
parser = Mock()
parser.project_metadata = metadata
get_value = configuration.get_value

with TemporaryDirectory() as temporary_root:
root = Path(temporary_root)
(root / "docs").mkdir()
overrides = {
ConfigurationVariable.PROJECT_ROOT: root,
ConfigurationVariable.LICENCE_FILE_PATH: "legal/LICENSE.txt",
ConfigurationVariable.MASTER_BRANCH: "main",
}
with patch.object(
configuration, "get_value", side_effect=lambda key: overrides.get(key, get_value(key))
), patch(
"continuous_delivery_scripts.spdx_report.spdx_summary.get_project_remote_url",
return_value="https://github.com/acme/example.git",
):
SpdxProject(parser).generate_licensing_summary(root / "docs")
html = Path(root / "docs" / "third_party_IP_report.html").read_text(encoding="utf8")

self.assertIn(
'<a href="https://github.com/acme/example/blob/main/legal/LICENSE.txt"><strong>MIT</strong></a>', html
)

def test_missing_dependency_is_reported_and_can_fail_the_audit(self):
metadata = ProjectMetadata("test_package")
metadata.project_metadata = PackageMetadata({"Name": "test_package", "License": "MIT"})
Expand Down
Loading