Skip to content

Commit 7354608

Browse files
committed
Add initial support for the npm-health ScanGrimoireLab pipeline
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent a7d3e70 commit 7354608

4 files changed

Lines changed: 251 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ resolve_dependencies = "scanpipe.pipelines.resolve_dependencies:ResolveDependenc
171171
scan_codebase = "scanpipe.pipelines.scan_codebase:ScanCodebase"
172172
scan_for_virus = "scanpipe.pipelines.scan_for_virus:ScanForVirus"
173173
scan_single_package = "scanpipe.pipelines.scan_single_package:ScanSinglePackage"
174+
scan_repo_grimoirelab = "scanpipe.pipelines.scan_repo_grimoirelab:ScanGrimoirelab"
174175

175176
[tool.setuptools.packages.find]
176177
where = ["."]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#
2+
# Copyright (C) AboutCode
3+
#
4+
# This program is free software; you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation; either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
#
17+
18+
import math
19+
20+
# These coefficients were calculated with the notebooks and data available
21+
# at https://github.com/aboutcode-org/healthycode/blob/main/model/npm/README.md
22+
23+
24+
class npmModel:
25+
# We have dropped the low-impact metrics, those with a coefficient close to 0
26+
COEFFICIENTS = {
27+
"elephant_factor": -1.635941,
28+
"coefficient_of_variation": -1.404157,
29+
"total_contributors": -0.991894,
30+
"days_since_last_commit": 0.865738,
31+
"contributor_growth_rate": 0.435875,
32+
"commits_over_periods_rate": -0.410393,
33+
"total_commits": -0.330035,
34+
"message_size_mean": -0.320026,
35+
"found_file_license": 0.266483,
36+
}
37+
38+
# Model Intercept
39+
Z = -0.549873845969752
40+
41+
def __init__(self):
42+
self.coefficients = self.COEFFICIENTS.copy()
43+
self.z = self.Z
44+
45+
def calculate_score(self, metrics: dict[str, float]) -> float:
46+
"""
47+
Calculates the probability of a repository being 'Unhealthy' based on
48+
the pruned logistic regression model metrics.
49+
50+
Parameters
51+
----------
52+
metrics (dict): Dictionary containing the project feature names and values.
53+
54+
Returns
55+
-------
56+
float: Probability score between 0.0 (Healthy) and 1.0 (Unhealthy).
57+
58+
"""
59+
z = self.z
60+
61+
# Calculate the linear combination (log-odds)
62+
for metric, coef in self.coefficients.items():
63+
# FIXME. We set by default 0 if a metric is missing. Is this safe?
64+
value = metrics.get(metric, 0.0)
65+
z += coef * value
66+
67+
# Apply the Sigmoid function to get the final probability
68+
try:
69+
probability = 1 / (1 + math.exp(-z))
70+
except OverflowError:
71+
# Safeguard against extreme values of z
72+
# FIXME Is this correct?
73+
probability = 0.0 if z < 0 else 1.0
74+
75+
return probability
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/aboutcode-org/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
22+
23+
import json
24+
import subprocess
25+
26+
from scanpipe.pipelines import Pipeline
27+
from scanpipe.pipelines.metrics_model import npmModel
28+
29+
30+
class ScanGrimoirelab(Pipeline):
31+
results_url = "/project/{slug}/resources/?extra_data=grimoire_data"
32+
33+
@classmethod
34+
def steps(cls):
35+
return (
36+
cls.collect_grimoire_metric,
37+
cls.compute_and_store_metric_score,
38+
)
39+
40+
def collect_grimoire_metric(self):
41+
metrics_output_path = self.project.get_output_file_path("metrics", "json")
42+
grimoire_config_path = self.project.get_output_file_path(
43+
"grimoire_config", "json"
44+
)
45+
repo_url = "https://github.com/aboutcode-org/fetchcode.git"
46+
47+
grimoirelab_config = {
48+
"spdxVersion": "SPDX-2.3",
49+
"dataLicense": "CC0-1.0",
50+
"SPDXID": "SPDXRef-DOCUMENT",
51+
"name": "GrimoireLab Analysis",
52+
"documentNamespace": "http://spdx.org/spdxdocs/grimoirelab-metrics",
53+
"creationInfo": {
54+
"creators": ["Tool: manual-conversion"],
55+
"created": "2026-07-31T21:41:21Z",
56+
},
57+
"packages": [
58+
{
59+
"name": "grimoirelab",
60+
"SPDXID": "SPDXRef-Package-grimoirelab",
61+
"downloadLocation": repo_url,
62+
}
63+
],
64+
}
65+
66+
with open(grimoire_config_path, "w") as f:
67+
json.dump(grimoirelab_config, f, indent=2)
68+
69+
GRIMOIRELAB_METRICS_EXECUTABLE = (
70+
"/home/ziad-hany/PycharmProjects/healthycode/venv/bin/grimoirelab-metrics"
71+
)
72+
73+
GRIMOIRELAB_URL = "http://localhost:8000"
74+
GRIMOIRELAB_USERNAME = "admin"
75+
GRIMOIRELAB_PASSWORD = "admin"
76+
77+
OPENSEARCH_URL = "https://localhost:9200"
78+
OPENSEARCH_INDEX = "events"
79+
OPENSEARCH_USERNAME = "admin"
80+
OPENSEARCH_PASSWORD = "GrimoireLab.1"
81+
82+
FROM_DATE = "2023-01-01"
83+
TO_DATE = "2026-06-19"
84+
85+
REPOSITORY_TIMEOUT = "3600"
86+
87+
CODE_FILE_PATTERN = r"\.py$|\.js$"
88+
BINARY_FILE_PATTERN = r"\.exe$|\.tar$"
89+
90+
PONY_THRESHOLD = "0.5"
91+
ELEPHANT_THRESHOLD = "0.5"
92+
DEVELOPER_CATEGORIES_THRESHOLDS = ["0.8", "0.95"]
93+
94+
cmd = [
95+
GRIMOIRELAB_METRICS_EXECUTABLE,
96+
str(grimoire_config_path),
97+
"--grimoirelab-url",
98+
GRIMOIRELAB_URL,
99+
"--grimoirelab-user",
100+
GRIMOIRELAB_USERNAME,
101+
"--grimoirelab-password",
102+
GRIMOIRELAB_PASSWORD,
103+
"--opensearch-url",
104+
OPENSEARCH_URL,
105+
"--opensearch-index",
106+
OPENSEARCH_INDEX,
107+
"--opensearch-user",
108+
OPENSEARCH_USERNAME,
109+
"--opensearch-password",
110+
OPENSEARCH_PASSWORD,
111+
"--from-date",
112+
FROM_DATE,
113+
"--to-date",
114+
TO_DATE,
115+
"--repository-timeout",
116+
REPOSITORY_TIMEOUT,
117+
"--code-file-pattern",
118+
CODE_FILE_PATTERN,
119+
"--binary-file-pattern",
120+
BINARY_FILE_PATTERN,
121+
"--pony-threshold",
122+
PONY_THRESHOLD,
123+
"--elephant-threshold",
124+
ELEPHANT_THRESHOLD,
125+
"--dev-categories-thresholds",
126+
*DEVELOPER_CATEGORIES_THRESHOLDS,
127+
"--output",
128+
str(metrics_output_path),
129+
]
130+
131+
try:
132+
subprocess.run(
133+
cmd,
134+
capture_output=True,
135+
text=True,
136+
check=True,
137+
)
138+
self.log(f"Metrics successfully saved to {metrics_output_path}")
139+
with open(metrics_output_path) as f:
140+
self.metrics = json.load(f)
141+
142+
except subprocess.CalledProcessError as e:
143+
self.log(f"failed with exit code {e.returncode}")
144+
except FileNotFoundError:
145+
self.log(
146+
"Error: 'grimoirelab-metrics' command not found. Is it installed and on your PATH?"
147+
)
148+
raise
149+
150+
def compute_and_store_metric_score(self):
151+
model = npmModel()
152+
probability = model.calculate_score(self.metrics)
153+
status = "Healthy" if probability >= 0.5 else "Unhealthy"
154+
155+
self.log(f"Repository Health: {status}, Probability: {probability:.2%}")
156+
score_data = {
157+
"status": status,
158+
"probability": probability,
159+
"metrics": self.metrics,
160+
}
161+
162+
score_output_path = self.project.get_output_file_path("results", "json")
163+
with open(score_output_path, "w") as f:
164+
json.dump(score_data, f, indent=2)
165+
166+
return score_data
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.test import TestCase
2+
3+
4+
class ScanGrimoirelabTest(TestCase):
5+
def test_collect_grimoire_metric(self):
6+
raise NotImplementedError
7+
8+
def test_compute_and_store_metric_score(self):
9+
raise NotImplementedError

0 commit comments

Comments
 (0)