From b2e43db4008aecd4cc2d8de292d28b850c1fbadd Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Fri, 16 Oct 2026 05:13:32 +0800 Subject: [PATCH] Simplify Docusaurus audit pull request reports Lead with resolved findings, remaining severities and concrete blockers. Fold dependency changes, complete audit findings and verification evidence into details, and reduce the publisher footer to a workflow link. Preserve dependency repair policy, build validation and publication controls. Validate with the complete maintenance suite, lint, formatting, runtime type checks and report regressions. --- .github/workflows/docusaurus-publish.yml | 16 +- docusaurus/legacy-regressions.json | 2 +- .../runtime/templates/dependency_audit.py | 222 +++++++++++---- docusaurus/tests/test_dependency_audit.py | 8 +- docusaurus/tests/test_docusaurus_standard.py | 252 ++++++++++++++++-- 5 files changed, 418 insertions(+), 82 deletions(-) diff --git a/.github/workflows/docusaurus-publish.yml b/.github/workflows/docusaurus-publish.yml index 0472d10..bdf1d7f 100644 --- a/.github/workflows/docusaurus-publish.yml +++ b/.github/workflows/docusaurus-publish.yml @@ -73,20 +73,8 @@ jobs: PR_BRANCH: maintenance/dependency-audit PR_TITLE: 'Docusaurus maintenance: security audit and dependency upgrades' VALIDATION_PASSED: ${{ inputs.validation-passed }} - PR_FOOTER: 'The audit workflow performs the frozen install and production - build; - - review their results above before merging. Failed candidates are drafts - - and fail this workflow. Successful reruns leave existing draft status - - for human review. Normal PR checks remain enabled. Post-merge CI and deployment - - use the normal workflows. Nothing is merged automatically. - - - Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ - github.run_id }} + PR_FOOTER: '[Workflow run](${{ github.server_url }}/${{ github.repository + }}/actions/runs/${{ github.run_id }}) ' uses: $/docusaurus/publish diff --git a/docusaurus/legacy-regressions.json b/docusaurus/legacy-regressions.json index dcd70c3..0e49c14 100644 --- a/docusaurus/legacy-regressions.json +++ b/docusaurus/legacy-regressions.json @@ -2,7 +2,7 @@ "tests/test_dependency_audit.py": { "source_sha256": "54f474d342ff76ce731cf25a20742697248028b8b4821cf5be3621a41fcca4e7", "transferred_path": "docusaurus/tests/test_dependency_audit.py", - "transferred_sha256": "07404b1fe4d46b052a1bb79395652a86c98b8fad3e64e7ff1309813131879322" + "transferred_sha256": "a5a2c76b7c35719059e2f613a2cc6740913b7dfaa3402a7e36a65287a4664755" }, "tests/test_dependency_audit_invariants.py": { "source_sha256": "229cdaa093eac2696febd15e72786886b6c2619ef27e79b4153d4f39f07b3b27", diff --git a/docusaurus/runtime/templates/dependency_audit.py b/docusaurus/runtime/templates/dependency_audit.py index 786952c..58a3d7a 100644 --- a/docusaurus/runtime/templates/dependency_audit.py +++ b/docusaurus/runtime/templates/dependency_audit.py @@ -1099,7 +1099,13 @@ def format_audit(data: dict | str, lock: dict) -> str: if isinstance(via, dict): findings.append( f" - [{via['title']}]({via['url']}); " - f"{via['severity']}; affected range `{via['range']}`." + f"{via['severity']}; " + + ( + f"locked `{name}@{versions(lock, via)}`; " + if source == "pnpm" + else "" + ) + + f"affected range `{via['range']}`." + (f" {via['fixStatus']}" if via.get("fixStatus") else "") ) else: @@ -1161,6 +1167,119 @@ def format_remaining_audit(data: dict | str, lock: dict) -> str: ) +def format_audit_summary(before: dict | str, after: dict | str, source: str) -> str: + if isinstance(after, str): + return f"**Security:** {after}" + counts = after["metadata"]["vulnerabilities"] + total = sum(counts.get(level, 0) for level in SEVERITY_ORDER) + label = "registry advisories" if source == "pnpm" else "affected package entries" + if total == 1: + label = "registry advisory" if source == "pnpm" else "affected package entry" + remaining = ", ".join( + f"{counts[level]} {level}" for level in SEVERITY_ORDER if counts.get(level) + ) + + def entries(data): + if isinstance(data, str): + return [] + return [ + (detail["url"] if source == "pnpm" else name, name, detail) + for name, vulnerability in data.get("vulnerabilities", {}).items() + for detail in ( + [via for via in vulnerability.get("via", []) if isinstance(via, dict)] + if source == "pnpm" + else [vulnerability] + ) + ] + + old, current = entries(before), entries(after) + old_ids, current_ids = {row[0] for row in old}, {row[0] for row in current} + comparable = ( + isinstance(before, dict) + and ( + bool(old) + or not any( + before["metadata"]["vulnerabilities"].get(level, 0) + for level in SEVERITY_ORDER + ) + ) + and (bool(current) or total == 0) + ) + resolved_ids = old_ids - current_ids if comparable else set() + new_ids = current_ids - old_ids if comparable else set() + + def names(rows, selected): + packages = sorted({name for identity, name, _ in rows if identity in selected}) + return ", ".join(f"`{name}`" for name in packages[:5]) + ( + f", and {len(packages) - 5} more packages" if len(packages) > 5 else "" + ) + + resolved = ( + f"{len(resolved_ids)} ({names(old, resolved_ids)})" + if resolved_ids + else "none" + if comparable + else "unconfirmed; initial or detailed audit evidence unavailable" + ) + summary = f"**Resolved:** {resolved}. **Remaining:** {total} {label}" + ( + f" ({remaining})." if remaining else "." + ) + if new_ids: + summary += f" **New:** {len(new_ids)} ({names(current, new_ids)})." + + def reason(detail): + status = detail.get("fixStatus", "") + if "Blocked by retained override" in status: + return "A retained override blocks the published patch." + if "no single patch satisfies all" in status: + parents = sorted(set(re.findall(r"`([^`]+)` requires `[^`]+`", status))) + if len(parents) == 1: + return f"Blocked by the `{parents[0].rsplit('@', 1)[0]}` version constraint." + return "No published patch fits all checked parent constraints." + if status.startswith("No published stable patch"): + return "No published patch matches the fix range." + if status.startswith("No patched version declared"): + return "The registry supplies no patched version." + if "could not be fully verified" in status: + return "Patch compatibility still needs verification." + if status.startswith("Published patch:"): + return "A published patch is available; selection needs review." + candidate = detail.get("fixAvailable") + if isinstance(candidate, dict) and candidate.get("isSemVerMajor"): + return "The offered fix requires a major upgrade." + if source == "npm" and not candidate: + return "No fix offered by npm." + return "Fix availability or resolution needs manual review." + + groups: dict = {} + for identity, name, detail in current: + groups.setdefault(name, {})[identity] = detail + if groups: + summary += "\n\n| Still open | Why |\n| --- | --- |\n" + ordered = sorted( + groups, + key=lambda name: ( + min( + SEVERITY_ORDER.get(detail["severity"], 5) + for detail in groups[name].values() + ), + name, + ), + ) + for name in ordered[:5]: + details = list(groups[name].values()) + severities = ", ".join( + f"{sum(detail['severity'] == level for detail in details)} {level}" + for level in SEVERITY_ORDER + if any(detail["severity"] == level for detail in details) + ) + reasons = " ".join(dict.fromkeys(reason(detail) for detail in details)) + summary += f"| `{name}` ({severities}) | {reasons} |\n" + if len(ordered) > 5: + summary += f"\n{len(ordered) - 5} more affected packages are listed in the audit details." + return summary + + def eligible_version(directory, name, latest, snapshot, notes, deadline): """Select a stable release within the deployment's publication-age policy.""" result = run(directory, "view", name, "time", "--json", deadline=deadline) @@ -1507,60 +1626,67 @@ def prepare(directory: Path, *, validation: dict | None = None) -> str: "parent packages. These are not counts of distinct advisories or " "demonstrated exploit paths." ) - return ( - "# Documentation dependency audit\n\n" - f"Project: `{directory}`; package manager: `{package_manager}`\n\n" - f"Run: {datetime.now(timezone.utc).isoformat()}\n\n" - "This is a best-effort upgrade drive. Remaining vulnerabilities are accepted " - "between maintenance reviews; this report does not certify that the project is " - "free of vulnerabilities. Review compatibility and validation before merging.\n\n" - "## Security findings and fixes\n\n" - "### Changes to affected dependencies\n\n" - + ( - "Initial audit evidence was unavailable; any retained security-fix " - "changes are unconfirmed and cannot be attributed to affected " - "dependencies. The final audit below describes only the final " - "dependency state.\n\n" - if not initial_audit_available - else "" + change_sections = [] + if security_changes: + change_sections.append( + "## Security findings and fixes\n\n" + "\n".join(security_changes) ) - + ( - "\n".join(security_changes) - or ( - "Affected dependency changes cannot be confirmed because initial " - "audit evidence was unavailable." - if not initial_audit_available - else "No upgrades to affected dependencies resolved." - ) + other_changes = [line for name, line in changes if name not in affected] + if other_changes: + change_sections.append( + "## Other stable dependency upgrades\n\n" + "\n".join(other_changes) ) - + "\n\n" - f"{count_explanation} Fix candidates can require manual migrations.\n\n" - "## Other stable dependency upgrades\n\n" - + ( - "\n".join(line for name, line in changes if name not in affected) - or "No other direct dependency upgrades resolved." + if locked_updates: + change_sections.append( + "## Locked dependency changes\n\n" + "\n".join(locked_updates) ) - + "\n\n" - "## Locked dependency changes\n\n" - + ("\n".join(locked_updates) or "No locked dependency version changes.") - + "\n\n" - f"## Remaining advisories\n\n{format_remaining_audit(after, after_lock)}\n\n" + report = ( + f"`{directory}` ยท `{package_manager}`\n\n" + f"- **Changes:** {len(changes)} direct / {len(locked_updates)} locked dependencies.\n" + f"- {format_audit_summary(before, after, package_manager)}\n\n" f"## Production build\n\n{build_status}\n\n" - "## Remaining work\n\n" - "Review retained overrides and peer constraints, transitive dependency fixes, " - "and any major-version migration requirements. Build and installation logs " - "are available in the workflow run.\n\n" + ) + if not initial_audit_available: + report += "Initial audit evidence was unavailable; retained security-fix changes are unconfirmed.\n\n" + if change_sections: + report += ( + "
\nDependency changes\n\n" + + "\n\n".join(change_sections) + + "\n\n
\n\n" + ) + before_audit = format_audit(before, before_lock) + after_audit = format_audit(after, after_lock) + # Keep the complete final findings once, including npm propagated packages. + remaining = ( + ( + "Registry advisory counts: " + if package_manager == "pnpm" + else "Affected package entries: " + ) + + after_audit + if isinstance(after, dict) and after.get("vulnerabilities") + else format_remaining_audit(after, after_lock) + ) + report += ( + f"
\nFull before/after {package_manager} audit\n\n" + f"## Remaining advisories\n\n{remaining}\n\n" + ( - "\n".join(f"- {note}" for note in notes) - or "No additional tool errors recorded." + "Before and after findings are unchanged.\n\n" + if before_audit == after_audit + else f"### Before\n\n{before_audit}\n\n" + ) + + "
\n\n" + "
\nAudit evidence\n\n" + f"Run: {datetime.now(timezone.utc).isoformat()}\n\n" + f"{count_explanation}\n\n" + + ( + "\n".join(f"- {note}" for note in dict.fromkeys(notes)) + "\n\n" + if notes + else "" ) - + "\n" - f"\n
\nFull before/after {package_manager} audit\n\n" - f"### Before\n\n{format_audit(before, before_lock)}\n\n" - f"### After\n\n{format_audit(after, after_lock)}\n\n" - "
\n" - f"\n
\nCandidate dependency files (SHA-256)\n\n{fingerprints}\n\n
\n" + + f"### Candidate dependency files (SHA-256)\n\n{fingerprints}\n\n
\n" ) + return report def main() -> None: diff --git a/docusaurus/tests/test_dependency_audit.py b/docusaurus/tests/test_dependency_audit.py index 7f5d7b4..b82ed8e 100644 --- a/docusaurus/tests/test_dependency_audit.py +++ b/docusaurus/tests/test_dependency_audit.py @@ -96,7 +96,7 @@ def test_upgrade_keeps_manual_constraints_and_reports_unfixed_findings( assert "high: 1, total: 1" in report assert "npm fix candidate not offered by npm" in report assert "## Production build\n\nPassed." in report - assert "does not certify" in report + assert "**Remaining:** 1 affected package entry (1 high)" in report assert "--force" not in [arg for command in commands for arg in command] @@ -110,7 +110,7 @@ def test_failed_resolution_restores_original_files(npm_project, monkeypatch): for name, content in original.items(): assert (npm_project / name).read_bytes() == content assert "from the initial security fix attempt" in report - assert "No other direct dependency upgrades resolved" in report + assert "**Changes:** 0 direct / 0 locked dependencies." in report assert sum(command[:2] == ["audit", "fix"] for command in commands) == 1 @@ -408,7 +408,7 @@ def test_shared_time_budget_stops_commands_and_writes_partial_report( assert commands == [["audit", "--json", "--audit", "--package-lock-only"]] assert "Could not look up renderer" in report assert "Not run: npm frozen install failed (exit 124)" in report - assert "### After\n\nAudit unavailable (exit 124)." in report + assert "**Security:** Audit unavailable (exit 124)." in report assert "time budget was exhausted" in report @@ -486,7 +486,7 @@ def test_cli_entrypoint_prepares_selected_project(npm_project, monkeypatch): ["dependency-audit", "--directory", ".", "--report", str(report)], ) runpy.run_path(audit_module["__file__"], run_name="__main__") - assert "Security findings and fixes" in report.read_text() + assert "**Remaining:**" in report.read_text() assert not (npm_project / "DEPENDENCY-AUDIT.md").exists() manifest = json.loads((npm_project / "package.json").read_text()) assert manifest["dependencies"]["renderer"] == "2.0.0" diff --git a/docusaurus/tests/test_docusaurus_standard.py b/docusaurus/tests/test_docusaurus_standard.py index eae314d..0addaea 100644 --- a/docusaurus/tests/test_docusaurus_standard.py +++ b/docusaurus/tests/test_docusaurus_standard.py @@ -110,6 +110,96 @@ def invoke(command, **kwargs): } +@pytest.mark.parametrize("changed", [False, True], ids=["unchanged", "changed"]) +def test_pnpm_report_keeps_advisory_versions_in_complete_audit( + pnpm_project, monkeypatch, changed +): + before_versions = ("1.0.0", "2.0.0") + after_versions = ("1.1.0", "2.1.0") if changed else before_versions + lockfile = pnpm_project / "pnpm-lock.yaml" + lock = yaml.safe_load(lockfile.read_bytes()) + lock["packages"] = { + f"renderer@{version}": {} for version in (*before_versions, *after_versions) + } + lockfile.write_text(yaml.safe_dump(lock)) + + def findings(versions): + details = [ + ("First advisory", "first", versions[0]), + ("Second advisory", "second", versions[1]), + ] + advisories = { + key: { + "module_name": "renderer", + "severity": "high", + "title": title, + "url": f"https://github.com/advisories/{key}", + "vulnerable_versions": "*", + "patched_versions": "<0.0.0", + "findings": [{"version": version}], + } + for title, key, version in details + } + return { + "metadata": {"vulnerabilities": {"high": 2, "total": 2}}, + "advisories": advisories, + "package_manager": "pnpm", + "vulnerabilities": { + "renderer": { + "severity": "high", + "range": "*", + "nodes": [f"renderer@{version}" for _, _, version in details], + "via": [ + { + "title": title, + "url": f"https://github.com/advisories/{key}", + "severity": "high", + "range": "*", + "nodes": [f"renderer@{version}"], + "patchedRange": "<0.0.0", + } + for title, key, version in details + ], + "fixAvailable": False, + } + }, + } + + audit_results = iter( + [findings(before_versions), findings(after_versions), findings(after_versions)] + ) + + def fake_audit(*args, **kwargs): + return next(audit_results) + + module_globals = prepare.__globals__ + monkeypatch.setitem(module_globals, "audit", fake_audit) + monkeypatch.setitem( + module_globals, + "fix_security", + lambda *args, **kwargs: subprocess.CompletedProcess([], 0, "", ""), + ) + monkeypatch.setitem( + module_globals, "describe_pnpm_fixes", lambda *args, **kwargs: None + ) + + def fake_run(directory, *args, **kwargs): + stdout = '"1.0.0"' if args[:1] == ("view",) else "" + return subprocess.CompletedProcess(args, 0, stdout, "") + + monkeypatch.setitem(module_globals, "run", fake_run) + report = prepare(pnpm_project) + + for title, before_version, after_version in ( + ("First advisory", before_versions[0], after_versions[0]), + ("Second advisory", before_versions[1], after_versions[1]), + ): + lines = [line for line in report.splitlines() if f"[{title}]" in line] + assert lines + assert any(f"locked `renderer@{before_version}`" in line for line in lines) + assert any(f"locked `renderer@{after_version}`" in line for line in lines) + + def fake_npm(monkeypatch, *, install=0, build=0, audit_error=False): def invoke(command, *, cwd, **kwargs): args = command[4:] @@ -445,7 +535,7 @@ def invoke(command, *, cwd, **kwargs): return subprocess.CompletedProcess(command, 0, stdout, "") monkeypatch.setattr(subprocess, "run", invoke) - summary = prepare(project).split("
")[0] + summary = prepare(project).split("Full before/after")[0] if changed: old = ", ".join(sorted({"1.0.0", initial_patched})) @@ -456,7 +546,7 @@ def invoke(command, *, cwd, **kwargs): assert "No upgrades to affected dependencies resolved." not in summary else: assert "- leaf:" not in summary - assert "No upgrades to affected dependencies resolved." in summary + assert "## Security findings and fixes" not in summary @pytest.mark.parametrize("package_manager", ["npm", "pnpm"]) @@ -540,7 +630,7 @@ def invoke(command, *, cwd, **kwargs): return subprocess.CompletedProcess(command, 0, stdout, "") monkeypatch.setattr(subprocess, "run", invoke) - summary = prepare(project).split("
")[0] + summary = prepare(project).split("Full before/after")[0] if changed: assert ( "leaf: dependency paths changed; installed versions `1.0.0, 2.0.0`" @@ -550,7 +640,7 @@ def invoke(command, *, cwd, **kwargs): assert "No upgrades to affected dependencies resolved." not in summary else: assert "- leaf:" not in summary - assert "No upgrades to affected dependencies resolved." in summary + assert "## Security findings and fixes" not in summary @pytest.mark.parametrize("package_manager", ["npm", "pnpm"]) @@ -659,12 +749,11 @@ def invoke(command, *, cwd, **kwargs): return subprocess.CompletedProcess(command, 0, stdout, "") monkeypatch.setattr(subprocess, "run", invoke) - summary = prepare(project).split("
")[0] - security, other = summary.split("## Other stable dependency upgrades") - other = other.split("## Locked dependency changes")[0] + summary = prepare(project).split("Full before/after")[0] + security = summary.split("## Locked dependency changes")[0] final = "3.0.0" if patched else "2.0.0" assert f"- renderer: `1.0.0` โ†’ `{final}`;" in security - assert "- renderer:" not in other + assert "## Other stable dependency upgrades" not in summary assert ( "still reported" in security if not patched @@ -720,7 +809,7 @@ def invoke(command, *, cwd, **kwargs): return subprocess.CompletedProcess(command, 0, stdout, "") monkeypatch.setattr(subprocess, "run", invoke) - summary = prepare(pnpm_project).split("
")[0] + summary = prepare(pnpm_project).split("Full before/after")[0] result = json.loads(manifest_path.read_text()) assert result["dependencies"]["renderer"] == "2.0.0" assert result[section]["second"] == "2.0.0" @@ -763,16 +852,148 @@ def test_summary_precedes_collapsed_full_audit(npm_project, monkeypatch): report = prepare(npm_project) summary, details = report.split("
", 1) - assert "renderer: `1.0.0` โ†’ `2.0.0`" in summary - assert "## Remaining advisories" in summary + assert "**Changes:** 1 direct / 0 locked dependencies." in summary + assert "**Resolved:** none. **Remaining:** 0 affected package entries." in summary assert "## Production build\n\nPassed." in summary - assert summary.index("Security findings") < summary.index("Other stable") + assert len(summary) < 600 + assert "renderer: `1.0.0` โ†’ `2.0.0`" in details + assert "No upgrades to affected dependencies" not in report + assert "No additional tool errors recorded" not in report + assert "This is a best-effort" not in report assert "### Before" not in summary and "### After" not in summary - assert "### Before" in details and "### After" in details + assert "Before and after findings are unchanged." in details assert details.endswith("
\n") assert not (npm_project / "dependency-audit.md").exists() +@pytest.mark.parametrize( + "package_manager, label", + [("pnpm", "registry advisories"), ("npm", "affected package entries")], +) +def test_compact_security_summary_keeps_severity_and_count_meaning( + package_manager, label +): + summarize = audit_module["format_audit_summary"] + before = {"metadata": {"vulnerabilities": {"critical": 3, "high": 2, "total": 5}}} + after = { + "metadata": { + "vulnerabilities": {"critical": 2, "high": 1, "moderate": 1, "total": 4} + } + } + result = summarize(before, after, package_manager) + assert f"**Remaining:** 4 {label} (2 critical, 1 high, 1 moderate)." in result + assert "**Resolved:** unconfirmed" in result + assert "Audit unavailable" in summarize( + before, "Audit unavailable (exit 1).", package_manager + ) + + +def test_unchanged_findings_are_rendered_once(npm_project, monkeypatch): + fake_npm(monkeypatch) + report = prepare(npm_project) + assert report.count("No known vulnerabilities reported by npm.") == 1 + assert report.count("
") == report.count("
") + + +@pytest.fixture +def security_findings(): + def make(rows): + data = {"metadata": {"vulnerabilities": {}}, "vulnerabilities": {}} + counts = data["metadata"]["vulnerabilities"] + for name, identity, severity, status in rows: + counts[severity] = counts.get(severity, 0) + 1 + package = data["vulnerabilities"].setdefault(name, {"via": []}) + package["via"].append( + { + "url": f"https://github.com/advisories/{identity}", + "severity": severity, + "fixStatus": status, + } + ) + return data + + return make + + +def test_security_outcomes_distinguish_resolved_and_new_with_equal_counts( + security_findings, +): + before = security_findings([("old-package", "old", "high", "")]) + after = security_findings([("new-package", "new", "high", "")]) + result = audit_module["format_audit_summary"](before, after, "pnpm") + assert "**Resolved:** 1 (`old-package`)" in result + assert "**New:** 1 (`new-package`)" in result + assert "**Remaining:** 1 registry advisory" in result + + +def test_security_outcomes_keep_shared_advisory_open_after_path_change( + security_findings, +): + before = security_findings([("old-path", "same", "critical", "")]) + after = security_findings([("new-path", "same", "critical", "")]) + result = audit_module["format_audit_summary"](before, after, "pnpm") + assert "**Resolved:** none." in result + assert "**New:**" not in result + + +def test_security_outcomes_report_partial_resolution_for_one_package(security_findings): + first = ("leaf", "first", "high", "") + second = ("leaf", "second", "high", "") + result = audit_module["format_audit_summary"]( + security_findings([first, second]), security_findings([first]), "pnpm" + ) + assert "**Resolved:** 1 (`leaf`)" in result + assert "**Remaining:** 1 registry advisory" in result + assert "| `leaf` (1 high) |" in result + + +def test_security_outcomes_keep_missing_initial_evidence_unconfirmed(security_findings): + result = audit_module["format_audit_summary"]( + "Audit unavailable", security_findings([]), "pnpm" + ) + assert "**Resolved:** unconfirmed" in result + assert "**Resolved:** none" not in result + + +def test_security_outcomes_summarize_concrete_blockers(security_findings): + constraints = "Published patches exist, but no single patch satisfies all checked parent constraints. Blocked by existing parent constraints: `@docusaurus/core@3.10.2` requires `tinypool ^1.0.2`." + before = security_findings( + [ + ("tinypool", "first", "critical", constraints), + ("tinypool", "second", "critical", constraints), + ( + "braces", + "third", + "high", + "No published stable patch matches registry range `>=3.0.4`.", + ), + ] + ) + result = audit_module["format_audit_summary"](before, before, "pnpm") + assert ( + "| `tinypool` (2 critical) | Blocked by the `@docusaurus/core` version constraint. |" + in result + ) + assert "No published patch matches the fix range." in result + assert result.count("| `tinypool`") == 1 + + +def test_security_outcomes_bound_the_visible_package_list(security_findings): + data = security_findings([(f"package-{i}", str(i), "high", "") for i in range(7)]) + result = audit_module["format_audit_summary"](data, data, "pnpm") + assert "**Remaining:** 7 registry advisories" in result + assert "2 more affected packages are listed in the audit details." in result + assert result.count("| `package-") == 5 + + +def test_publisher_footer_is_only_a_run_link(): + workflow = load_workflow() + footer = workflow["jobs"]["pull-request"]["steps"][-1]["env"]["PR_FOOTER"] + assert footer.strip() == ( + "[Workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" + ) + + @pytest.mark.parametrize("install, build", [(1, 0), (0, 1)]) def test_summary_keeps_failed_validation_visible( npm_project, monkeypatch, install, build @@ -827,7 +1048,8 @@ def invoke(command, *, cwd, **kwargs): return subprocess.CompletedProcess(command, 0, "", "") monkeypatch.setattr(subprocess, "run", invoke) - summary = prepare(pnpm_project).split("
")[0] + report = prepare(pnpm_project) + summary = report.split("
")[0] assert "Initial audit evidence was unavailable" in summary assert "retained security-fix changes are unconfirmed" in summary @@ -835,7 +1057,7 @@ def invoke(command, *, cwd, **kwargs): assert "No additional tool errors recorded." not in summary assert ( "## Remaining advisories\n\nNo known vulnerabilities reported by pnpm." - in summary + in report )