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
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ keywords:
- bot
- software
license: MIT
version: 0.5.3
date-released: 2026-07-06
version: 0.5.4.dev1
date-released: 2026-07-08
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.19468976.svg)](https://doi.org/10.5281/zenodo.19468976)
[![SWH](https://archive.softwareheritage.org/badge/swh:1:dir:9f3d474c040158cc675e8db135767e0d2d3fba7b/)](https://archive.softwareheritage.org/swh:1:dir:9f3d474c040158cc675e8db135767e0d2d3fba7b;origin=https://github.com/SoftwareUnderstanding/sw-metadata-bot;visit=swh:1:snp:03da984544982fdf5aa1f556012b917250f1e4c3;anchor=swh:1:rev:fc64150edf2e35f045cf3858d81b00ca6e7f6e68)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-0.5.3-blue.svg)](https://github.com/SoftwareUnderstanding/sw-metadata-bot/releases)
[![Version](https://img.shields.io/badge/version-0.5.4.dev1-blue.svg)](https://github.com/SoftwareUnderstanding/sw-metadata-bot/releases)
[![Python 3.11--3.12](https://img.shields.io/badge/python-3.11--3.12-blue.svg)](https://www.python.org/downloads/)
[![CI (build)](https://github.com/SoftwareUnderstanding/sw-metadata-bot/actions/workflows/ci.yml/badge.svg)](https://github.com/SoftwareUnderstanding/sw-metadata-bot/actions/workflows/ci.yml)
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
Expand Down
6 changes: 3 additions & 3 deletions codemeta.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
],
"codeRepository": "https://github.com/SoftwareUnderstanding/sw-metadata-bot",
"dateCreated": "2025-12-15",
"dateModified": "2026-07-06",
"dateModified": "2026-07-08",
"datePublished": "2026-01-16",
"description": "A repository to keep the code of the RSMetaCheck bot for pushing issues with existing repository metadata",
"downloadUrl": "https://github.com/SoftwareUnderstanding/sw-metadata-bot/releases",
Expand All @@ -83,8 +83,8 @@
"python-dotenv>=1.0.0",
"requests>=2.32.5"
],
"version": "0.5.3",
"softwareVersion": "0.5.3",
"version": "0.5.4.dev1",
"softwareVersion": "0.5.4.dev1",
"maintainer": {
"@id": "https://orcid.org/0000-0001-5226-3089",
"givenName": "Tom",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "sw-metadata-bot"
version = "0.5.3"
version = "0.5.4.dev1"
description = "Metadata quality bot for software repositories, leveraging metacheck for analysis and GitHub/GitLab APIs for issue management."
readme = "README.md"
requires-python = ">=3.11,<3.13"
Expand Down
2 changes: 2 additions & 0 deletions src/sw_metadata_bot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@
FILENAME_CONFIG_SNAPSHOT,
}
)

FILENAME_REPORT_SUMMARY = "report_summary.json"
"""Files to exclude when cleaning up metacheck output directories."""


Expand Down
2 changes: 2 additions & 0 deletions src/sw_metadata_bot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .pipeline import run_analysis_command
from .publish import publish_command, simulate_publish_command
from .report_summary import summarize_report_command
from .verify_tokens import verify_tokens_command


Expand All @@ -22,6 +23,7 @@ def cli():
cli.add_command(run_analysis_command, name="run-analysis")
cli.add_command(publish_command, name="publish")
cli.add_command(simulate_publish_command, name="simulate-publish")
cli.add_command(summarize_report_command, name="report-summary")


def main():
Expand Down
131 changes: 131 additions & 0 deletions src/sw_metadata_bot/report_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Create a summary from existing report for dashboard"""

import json
from collections import Counter
from pathlib import Path

import click

from . import constants
from .reporting import RunReport, load_report


@click.command()
@click.option(
"--analysis-root",
type=click.Path(exists=True, dir_okay=True, path_type=Path),
default=None,
help="Previous run_report.json used for incremental issue handling.",
)
def summarize_report_command(analysis_root: Path):
"""Summarize report.json file and save the summary file in the analysis root folder."""
summary = summarize_analysis_folder(analysis_root)

save_summary_report(analysis_root, summary)


def summarize_report(report: RunReport) -> dict[str, object]:
"""Build a compact summary for plotting and comparison."""
repository_count = len(report.records)
toolmetadata = report.get_tool_metadata()

pitfalls_by_id: Counter[str] = Counter()
warnings_by_id: Counter[str] = Counter()
total_pitfalls = 0
total_warnings = 0
issues_created = 0
actions: Counter[str] = Counter()
reason_codes: Counter[str] = Counter()
reason_codes_by_action: dict[str, Counter[str]] = {}

for record in report.records:
for pitfall_id in record.pitfalls_ids:
pitfalls_by_id[pitfall_id] += 1
for warning_id in record.warnings_ids:
warnings_by_id[warning_id] += 1

total_pitfalls += int(record.pitfalls_count or 0)
total_warnings += int(record.warnings_count or 0)

if record.action == constants.ACTION_CREATED:
issues_created += 1

if record.action is not None:
actions[record.action] += 1
reason_codes_by_action.setdefault(record.action, Counter())

if record.reason_code is not None:
reason_codes[record.reason_code] += 1
if record.action is not None:
reason_codes_by_action[record.action][record.reason_code] += 1

reason_code_percentages_by_action: dict[str, dict[str, float]] = {}
for action_name, counter in reason_codes_by_action.items():
total_for_action = sum(counter.values())
if total_for_action == 0:
reason_code_percentages_by_action[action_name] = {}
continue
reason_code_percentages_by_action[action_name] = {
reason_code: (count / total_for_action) * 100.0
for reason_code, count in sorted(counter.items())
}

return {
"tool_metadata": toolmetadata.to_dict(),
"run_metadata": report.run_metadata,
"repository_count": repository_count,
"pitfalls_by_id": dict(sorted(pitfalls_by_id.items())),
"warnings_by_id": dict(sorted(warnings_by_id.items())),
"total_pitfalls": total_pitfalls,
"total_warnings": total_warnings,
"issues_created": issues_created,
"actions": dict(sorted(actions.items())),
"reason_codes": dict(sorted(reason_codes.items())),
"reason_codes_by_action": {
action_name: dict(sorted(counter.items()))
for action_name, counter in sorted(reason_codes_by_action.items())
},
"reason_code_percentages_by_action": {
action_name: dict(sorted(percentages.items()))
for action_name, percentages in sorted(
reason_code_percentages_by_action.items()
)
},
"pitfalls_per_repository": (
total_pitfalls / repository_count if repository_count else 0.0
),
"warnings_per_repository": (
total_warnings / repository_count if repository_count else 0.0
),
"issues_created_per_repository": (
issues_created / repository_count if repository_count else 0.0
),
}


def summarize_analysis_folder(analysis_folder: Path | str) -> dict[str, object]:
"""Load run_report.json from an analysis directory and summarize it."""
analysis_path = Path(analysis_folder)
report_path = analysis_path / constants.FILENAME_RUN_REPORT
if analysis_path.is_file():
report_path = analysis_path

return summarize_report(load_report(report_path))


def save_summary_report(analysis_folder: Path, summary_data: dict):
"""Save summary of the analysis"""
if summary_data:
summary_path = analysis_folder / constants.FILENAME_REPORT_SUMMARY
with open(summary_path, "w", encoding="utf-8") as f:
json.dump(summary_data, f, indent=2)
else:
print("Error: No summary info")


if __name__ == "__main__":
analysis_root = Path("./outputs/ossr/20260702/")

summary = summarize_analysis_folder(analysis_root)

save_summary_report(analysis_root, summary)
Loading
Loading