maintainer-readiness-kit 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maintainer_readiness/__init__.py +4 -0
- maintainer_readiness/__main__.py +5 -0
- maintainer_readiness/badge.py +17 -0
- maintainer_readiness/checks.py +247 -0
- maintainer_readiness/cli.py +117 -0
- maintainer_readiness/github.py +98 -0
- maintainer_readiness/report.py +100 -0
- maintainer_readiness/sarif.py +106 -0
- maintainer_readiness/templates.py +119 -0
- maintainer_readiness_kit-0.6.1.dist-info/METADATA +275 -0
- maintainer_readiness_kit-0.6.1.dist-info/RECORD +15 -0
- maintainer_readiness_kit-0.6.1.dist-info/WHEEL +5 -0
- maintainer_readiness_kit-0.6.1.dist-info/entry_points.txt +2 -0
- maintainer_readiness_kit-0.6.1.dist-info/licenses/LICENSE +21 -0
- maintainer_readiness_kit-0.6.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def render_badge(result: dict) -> dict:
|
|
5
|
+
percent = float(result.get("percent", 0.0))
|
|
6
|
+
if percent >= 90:
|
|
7
|
+
color = "brightgreen"
|
|
8
|
+
elif percent >= 70:
|
|
9
|
+
color = "yellow"
|
|
10
|
+
else:
|
|
11
|
+
color = "red"
|
|
12
|
+
return {
|
|
13
|
+
"schemaVersion": 1,
|
|
14
|
+
"label": "maintainer readiness",
|
|
15
|
+
"message": f"{percent:.1f}%",
|
|
16
|
+
"color": color,
|
|
17
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, asdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CheckSpec:
|
|
11
|
+
check_id: str
|
|
12
|
+
label: str
|
|
13
|
+
weight: int
|
|
14
|
+
candidates: tuple[str, ...]
|
|
15
|
+
fix: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CHECKS: tuple[CheckSpec, ...] = (
|
|
19
|
+
CheckSpec("readme", "README explains purpose and usage", 12, ("README.md", "README.rst"), "Add a README with install, quick start, and limitations."),
|
|
20
|
+
CheckSpec("license", "Open source license is present", 12, ("LICENSE", "LICENSE.md", "COPYING"), "Add a recognized open source license."),
|
|
21
|
+
CheckSpec("contributing", "Contributor guide is present", 8, ("CONTRIBUTING.md", "docs/CONTRIBUTING.md"), "Add CONTRIBUTING.md with setup and contribution flow."),
|
|
22
|
+
CheckSpec("security", "Security policy is present", 10, ("SECURITY.md", ".github/SECURITY.md"), "Add SECURITY.md with supported versions and disclosure contact."),
|
|
23
|
+
CheckSpec("code_of_conduct", "Code of conduct is present", 5, ("CODE_OF_CONDUCT.md", ".github/CODE_OF_CONDUCT.md"), "Add a code of conduct or link to the community standard."),
|
|
24
|
+
CheckSpec("issue_template", "Issue template is present", 8, (".github/ISSUE_TEMPLATE",), "Add bug and feature issue templates."),
|
|
25
|
+
CheckSpec("pr_template", "Pull request template is present", 8, (".github/PULL_REQUEST_TEMPLATE.md", "PULL_REQUEST_TEMPLATE.md"), "Add a pull request template with test and risk prompts."),
|
|
26
|
+
CheckSpec("ci", "Continuous integration workflow is present", 10, (".github/workflows",), "Add a minimal CI workflow that runs tests or smoke checks."),
|
|
27
|
+
CheckSpec("tests", "Tests are present", 10, ("tests", "test"), "Add a small test suite or smoke test directory."),
|
|
28
|
+
CheckSpec("manifest", "Package or project manifest is present", 8, ("pyproject.toml", "package.json", "go.mod", "Cargo.toml", "pom.xml"), "Add a package manifest or project metadata file."),
|
|
29
|
+
CheckSpec("docs", "Docs or examples are present", 5, ("docs", "examples"), "Add docs or examples for contributors and users."),
|
|
30
|
+
CheckSpec("changelog", "Changelog or release notes are present", 4, ("CHANGELOG.md", "RELEASES.md", "docs/releases.md"), "Add a changelog or release notes file."),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
HIGH_RISK_NAMES = {
|
|
35
|
+
".env",
|
|
36
|
+
".env.local",
|
|
37
|
+
".env.production",
|
|
38
|
+
".npmrc",
|
|
39
|
+
".pypirc",
|
|
40
|
+
"credentials.json",
|
|
41
|
+
"service-account.json",
|
|
42
|
+
"id_rsa",
|
|
43
|
+
"id_ed25519",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class EcosystemRule:
|
|
49
|
+
ecosystem: str
|
|
50
|
+
evidence: tuple[str, ...]
|
|
51
|
+
recommendations: tuple[str, ...]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
ECOSYSTEM_RULES: tuple[EcosystemRule, ...] = (
|
|
55
|
+
EcosystemRule(
|
|
56
|
+
"python",
|
|
57
|
+
("pyproject.toml", "requirements.txt", "setup.py", "setup.cfg"),
|
|
58
|
+
(
|
|
59
|
+
"Run `python -m unittest discover -s tests` or the documented test command in CI.",
|
|
60
|
+
"Keep packaging metadata in `pyproject.toml` and publish wheels from tagged releases.",
|
|
61
|
+
"Use `src/` layout or clear package discovery to avoid importing local files accidentally.",
|
|
62
|
+
"Document supported Python versions and minimum dependency policy.",
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
EcosystemRule(
|
|
66
|
+
"node",
|
|
67
|
+
("package.json", "pnpm-lock.yaml", "package-lock.json", "yarn.lock"),
|
|
68
|
+
(
|
|
69
|
+
"Expose `npm test` or an equivalent package script and run it in CI.",
|
|
70
|
+
"Commit one lockfile for apps, or document why libraries avoid lockfiles.",
|
|
71
|
+
"Document supported Node.js versions in README or package metadata.",
|
|
72
|
+
"Run a package audit or dependency review before releases.",
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
EcosystemRule(
|
|
76
|
+
"rust",
|
|
77
|
+
("Cargo.toml", "Cargo.lock"),
|
|
78
|
+
(
|
|
79
|
+
"Run `cargo test` and `cargo clippy` in CI.",
|
|
80
|
+
"Document supported Rust toolchain or MSRV.",
|
|
81
|
+
"Commit `Cargo.lock` for binaries and document lockfile policy for libraries.",
|
|
82
|
+
"Keep release notes aligned with crate version changes.",
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
EcosystemRule(
|
|
86
|
+
"go",
|
|
87
|
+
("go.mod", "go.sum"),
|
|
88
|
+
(
|
|
89
|
+
"Run `go test ./...` in CI.",
|
|
90
|
+
"Keep `go.mod` and `go.sum` committed and tidy.",
|
|
91
|
+
"Document supported Go version and module path.",
|
|
92
|
+
"Use tagged releases for module consumers.",
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
EcosystemRule(
|
|
96
|
+
"java",
|
|
97
|
+
("pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle"),
|
|
98
|
+
(
|
|
99
|
+
"Run Maven or Gradle tests in CI.",
|
|
100
|
+
"Document the supported Java version and build tool version.",
|
|
101
|
+
"Keep dependency update and vulnerability review policy clear.",
|
|
102
|
+
"Align release notes with package or artifact versions.",
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class CheckResult:
|
|
110
|
+
check_id: str
|
|
111
|
+
label: str
|
|
112
|
+
passed: bool
|
|
113
|
+
weight: int
|
|
114
|
+
evidence: str
|
|
115
|
+
fix: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def inspect_project(root: Path | str, root_label: str | None = None) -> dict:
|
|
119
|
+
root_path = Path(root).resolve()
|
|
120
|
+
check_results = [run_check(root_path, spec) for spec in CHECKS]
|
|
121
|
+
score = sum(item.weight for item in check_results if item.passed)
|
|
122
|
+
max_score = sum(item.weight for item in check_results)
|
|
123
|
+
percent = round((score / max_score) * 100, 1) if max_score else 0.0
|
|
124
|
+
return {
|
|
125
|
+
"root": str(root_path),
|
|
126
|
+
"display_root": root_label or str(root_path),
|
|
127
|
+
"score": score,
|
|
128
|
+
"max_score": max_score,
|
|
129
|
+
"percent": percent,
|
|
130
|
+
"level": classify_level(percent),
|
|
131
|
+
"checks": [asdict(item) for item in check_results],
|
|
132
|
+
"ecosystems": detect_ecosystems(root_path),
|
|
133
|
+
"git": get_git_metrics(root_path),
|
|
134
|
+
"secret_warnings": find_high_risk_files(root_path),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def classify_level(percent: float) -> str:
|
|
139
|
+
if percent >= 90:
|
|
140
|
+
return "ready"
|
|
141
|
+
if percent >= 70:
|
|
142
|
+
return "nearly-ready"
|
|
143
|
+
return "needs-work"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run_check(root: Path, spec: CheckSpec) -> CheckResult:
|
|
147
|
+
evidence = find_first_existing(root, spec.candidates)
|
|
148
|
+
return CheckResult(
|
|
149
|
+
check_id=spec.check_id,
|
|
150
|
+
label=spec.label,
|
|
151
|
+
passed=evidence is not None,
|
|
152
|
+
weight=spec.weight,
|
|
153
|
+
evidence=evidence or "missing",
|
|
154
|
+
fix=spec.fix,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def find_first_existing(root: Path, candidates: Iterable[str]) -> str | None:
|
|
159
|
+
for candidate in candidates:
|
|
160
|
+
path = root / candidate
|
|
161
|
+
if path.exists():
|
|
162
|
+
if path.is_dir():
|
|
163
|
+
child_count = sum(1 for _ in path.iterdir())
|
|
164
|
+
if child_count == 0:
|
|
165
|
+
continue
|
|
166
|
+
return candidate
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def detect_ecosystems(root: Path) -> list[dict]:
|
|
171
|
+
detected: list[dict] = []
|
|
172
|
+
for rule in ECOSYSTEM_RULES:
|
|
173
|
+
evidence = [candidate for candidate in rule.evidence if (root / candidate).exists()]
|
|
174
|
+
if evidence:
|
|
175
|
+
detected.append(
|
|
176
|
+
{
|
|
177
|
+
"ecosystem": rule.ecosystem,
|
|
178
|
+
"evidence": evidence,
|
|
179
|
+
"recommendations": list(rule.recommendations),
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
if not detected:
|
|
183
|
+
detected.append(
|
|
184
|
+
{
|
|
185
|
+
"ecosystem": "generic",
|
|
186
|
+
"evidence": [],
|
|
187
|
+
"recommendations": [
|
|
188
|
+
"Document the primary runtime, test command, and release process.",
|
|
189
|
+
"Add a CI smoke check that exercises the project entry point.",
|
|
190
|
+
"Include examples that show the smallest useful workflow.",
|
|
191
|
+
],
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
return detected
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_git_metrics(root: Path) -> dict:
|
|
198
|
+
if not (root / ".git").exists():
|
|
199
|
+
inside = _git(root, "rev-parse", "--is-inside-work-tree")
|
|
200
|
+
if inside.returncode != 0 or inside.stdout.strip() != "true":
|
|
201
|
+
return {"available": False, "reason": "not a git repository"}
|
|
202
|
+
|
|
203
|
+
branch = _git(root, "rev-parse", "--abbrev-ref", "HEAD")
|
|
204
|
+
last_commit = _git(root, "log", "-1", "--format=%cI")
|
|
205
|
+
count_90 = _git(root, "rev-list", "--count", "--since=90.days.ago", "HEAD")
|
|
206
|
+
dirty = _git(root, "status", "--short")
|
|
207
|
+
return {
|
|
208
|
+
"available": last_commit.returncode == 0,
|
|
209
|
+
"branch": branch.stdout.strip() if branch.returncode == 0 else None,
|
|
210
|
+
"last_commit": last_commit.stdout.strip() if last_commit.returncode == 0 else None,
|
|
211
|
+
"commits_last_90_days": _parse_int(count_90.stdout.strip()) if count_90.returncode == 0 else None,
|
|
212
|
+
"dirty_files": len([line for line in dirty.stdout.splitlines() if line.strip()]) if dirty.returncode == 0 else None,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def find_high_risk_files(root: Path) -> list[dict]:
|
|
217
|
+
warnings: list[dict] = []
|
|
218
|
+
ignored = {".git", ".venv", "venv", "__pycache__", "dist", "build"}
|
|
219
|
+
for path in root.rglob("*"):
|
|
220
|
+
if any(part in ignored for part in path.parts):
|
|
221
|
+
continue
|
|
222
|
+
if path.is_file() and path.name in HIGH_RISK_NAMES:
|
|
223
|
+
warnings.append(
|
|
224
|
+
{
|
|
225
|
+
"path": str(path.relative_to(root)),
|
|
226
|
+
"reason": "high-risk local credential/config filename",
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
return warnings
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _git(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
233
|
+
return subprocess.run(
|
|
234
|
+
["git", *args],
|
|
235
|
+
cwd=str(root),
|
|
236
|
+
text=True,
|
|
237
|
+
stdout=subprocess.PIPE,
|
|
238
|
+
stderr=subprocess.PIPE,
|
|
239
|
+
check=False,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _parse_int(value: str) -> int | None:
|
|
244
|
+
try:
|
|
245
|
+
return int(value)
|
|
246
|
+
except ValueError:
|
|
247
|
+
return None
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .checks import inspect_project
|
|
10
|
+
from .badge import render_badge
|
|
11
|
+
from .github import fetch_github_repo
|
|
12
|
+
from .report import render_markdown
|
|
13
|
+
from .sarif import render_sarif
|
|
14
|
+
from .templates import write_templates
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="maintainer-readiness",
|
|
20
|
+
description="Generate maintainer-readiness reports for OSS repositories.",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
23
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
24
|
+
|
|
25
|
+
inspect_parser = subparsers.add_parser("inspect", help="Inspect a repository.")
|
|
26
|
+
inspect_parser.add_argument("path", nargs="?", default=".", help="Repository path to inspect.")
|
|
27
|
+
inspect_parser.add_argument("--repo", help="Optional GitHub repo as owner/name or URL.")
|
|
28
|
+
inspect_parser.add_argument("--output", help="Write Markdown report to this path.")
|
|
29
|
+
inspect_parser.add_argument("--sarif", help="Write SARIF report to this path.")
|
|
30
|
+
inspect_parser.add_argument("--badge-json", help="Write Shields endpoint JSON to this path.")
|
|
31
|
+
inspect_parser.add_argument("--root-label", help="Display label to use instead of the absolute local root path.")
|
|
32
|
+
inspect_parser.add_argument("--json", action="store_true", help="Print JSON instead of Markdown.")
|
|
33
|
+
inspect_parser.add_argument(
|
|
34
|
+
"--stale-days",
|
|
35
|
+
type=int,
|
|
36
|
+
default=30,
|
|
37
|
+
metavar="DAYS",
|
|
38
|
+
help="Treat open GitHub issues and pull requests as stale after DAYS without updates.",
|
|
39
|
+
)
|
|
40
|
+
inspect_parser.add_argument(
|
|
41
|
+
"--fail-under",
|
|
42
|
+
type=float,
|
|
43
|
+
metavar="SCORE",
|
|
44
|
+
help="Exit non-zero when the readiness percentage is below SCORE.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
init_parser = subparsers.add_parser("init", help="Write starter maintainer templates.")
|
|
48
|
+
init_parser.add_argument("path", nargs="?", default=".", help="Repository path to initialize.")
|
|
49
|
+
init_parser.add_argument("--force", action="store_true", help="Overwrite existing starter files.")
|
|
50
|
+
init_parser.add_argument("--json", action="store_true", help="Print JSON output.")
|
|
51
|
+
return parser
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: list[str] | None = None) -> int:
|
|
55
|
+
parser = build_parser()
|
|
56
|
+
args = parser.parse_args(argv)
|
|
57
|
+
if args.command == "inspect":
|
|
58
|
+
return run_inspect(args)
|
|
59
|
+
if args.command == "init":
|
|
60
|
+
return run_init(args)
|
|
61
|
+
parser.error("unknown command")
|
|
62
|
+
return 2
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_inspect(args: argparse.Namespace) -> int:
|
|
66
|
+
if args.stale_days <= 0:
|
|
67
|
+
raise SystemExit("--stale-days must be a positive integer")
|
|
68
|
+
result = inspect_project(args.path, root_label=args.root_label)
|
|
69
|
+
github = None
|
|
70
|
+
if args.repo:
|
|
71
|
+
github = fetch_github_repo(args.repo, stale_days=args.stale_days)
|
|
72
|
+
exit_code = readiness_exit_code(result, args.fail_under)
|
|
73
|
+
if args.sarif:
|
|
74
|
+
sarif_path = Path(args.sarif)
|
|
75
|
+
sarif_path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
sarif_path.write_text(json.dumps(render_sarif(result, github), ensure_ascii=False, indent=2), encoding="utf-8", newline="\n")
|
|
77
|
+
if args.badge_json:
|
|
78
|
+
badge_path = Path(args.badge_json)
|
|
79
|
+
badge_path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
badge_path.write_text(json.dumps(render_badge(result), ensure_ascii=False, indent=2), encoding="utf-8", newline="\n")
|
|
81
|
+
|
|
82
|
+
if args.json:
|
|
83
|
+
payload = {"readiness": result, "github": github}
|
|
84
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
85
|
+
return exit_code
|
|
86
|
+
|
|
87
|
+
markdown = render_markdown(result, github)
|
|
88
|
+
if args.output:
|
|
89
|
+
output_path = Path(args.output)
|
|
90
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
output_path.write_text(markdown, encoding="utf-8", newline="\n")
|
|
92
|
+
print(f"Wrote {output_path}")
|
|
93
|
+
else:
|
|
94
|
+
print(markdown)
|
|
95
|
+
return exit_code
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def readiness_exit_code(result: dict, fail_under: float | None) -> int:
|
|
99
|
+
if fail_under is None:
|
|
100
|
+
return 0
|
|
101
|
+
if fail_under < 0 or fail_under > 100:
|
|
102
|
+
raise SystemExit("--fail-under must be between 0 and 100")
|
|
103
|
+
return 1 if result["percent"] < fail_under else 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_init(args: argparse.Namespace) -> int:
|
|
107
|
+
result = write_templates(args.path, force=args.force)
|
|
108
|
+
if args.json:
|
|
109
|
+
print(json.dumps({"templates": result}, ensure_ascii=False, indent=2))
|
|
110
|
+
else:
|
|
111
|
+
for item in result:
|
|
112
|
+
print(f"{item['status']}: {item['path']}")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from urllib.error import HTTPError, URLError
|
|
7
|
+
from urllib.request import Request, urlopen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fetch_github_repo(repo: str, token: str | None = None, stale_days: int = 30) -> dict:
|
|
11
|
+
normalized = repo.strip().removeprefix("https://github.com/").strip("/")
|
|
12
|
+
if normalized.count("/") != 1:
|
|
13
|
+
raise ValueError("repo must be owner/name or https://github.com/owner/name")
|
|
14
|
+
|
|
15
|
+
auth_token = token or os.environ.get("GITHUB_TOKEN")
|
|
16
|
+
payload = _github_get_json(f"https://api.github.com/repos/{normalized}", auth_token, normalized)
|
|
17
|
+
try:
|
|
18
|
+
open_items = _github_get_json(
|
|
19
|
+
f"https://api.github.com/repos/{normalized}/issues?state=open&per_page=100&sort=updated&direction=asc",
|
|
20
|
+
auth_token,
|
|
21
|
+
normalized,
|
|
22
|
+
)
|
|
23
|
+
workload = summarize_open_items(open_items, stale_days=stale_days)
|
|
24
|
+
except RuntimeError as exc:
|
|
25
|
+
workload = {"activity_error": str(exc)}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
"full_name": payload.get("full_name"),
|
|
29
|
+
"html_url": payload.get("html_url"),
|
|
30
|
+
"description": payload.get("description"),
|
|
31
|
+
"visibility": payload.get("visibility"),
|
|
32
|
+
"stars": payload.get("stargazers_count"),
|
|
33
|
+
"forks": payload.get("forks_count"),
|
|
34
|
+
"open_issues": payload.get("open_issues_count"),
|
|
35
|
+
"created_at": payload.get("created_at"),
|
|
36
|
+
"updated_at": payload.get("updated_at"),
|
|
37
|
+
"pushed_at": payload.get("pushed_at"),
|
|
38
|
+
"default_branch": payload.get("default_branch"),
|
|
39
|
+
**workload,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _github_get_json(url: str, auth_token: str | None, repo_label: str) -> dict | list[dict]:
|
|
44
|
+
request = Request(url)
|
|
45
|
+
request.add_header("Accept", "application/vnd.github+json")
|
|
46
|
+
if auth_token:
|
|
47
|
+
request.add_header("Authorization", f"Bearer {auth_token}")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with urlopen(request, timeout=15) as response:
|
|
51
|
+
return json.loads(response.read().decode("utf-8"))
|
|
52
|
+
except HTTPError as exc:
|
|
53
|
+
raise RuntimeError(f"GitHub API returned HTTP {exc.code} for {repo_label}") from exc
|
|
54
|
+
except URLError as exc:
|
|
55
|
+
raise RuntimeError(f"GitHub API request failed for {repo_label}: {exc.reason}") from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def summarize_open_items(items: list[dict], now: datetime | None = None, stale_days: int = 30) -> dict:
|
|
59
|
+
if stale_days <= 0:
|
|
60
|
+
raise ValueError("stale_days must be positive")
|
|
61
|
+
now = now or datetime.now(timezone.utc)
|
|
62
|
+
cutoff = now - timedelta(days=stale_days)
|
|
63
|
+
issues = []
|
|
64
|
+
pulls = []
|
|
65
|
+
for item in items:
|
|
66
|
+
updated_at = _parse_github_datetime(item.get("updated_at"))
|
|
67
|
+
if updated_at is None:
|
|
68
|
+
continue
|
|
69
|
+
if item.get("pull_request"):
|
|
70
|
+
pulls.append(updated_at)
|
|
71
|
+
else:
|
|
72
|
+
issues.append(updated_at)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"activity_sample_limit": 100,
|
|
76
|
+
"stale_days": stale_days,
|
|
77
|
+
"open_issue_items_sampled": len(issues),
|
|
78
|
+
"open_pr_items_sampled": len(pulls),
|
|
79
|
+
"stale_issue_items": sum(updated_at < cutoff for updated_at in issues),
|
|
80
|
+
"stale_pr_items": sum(updated_at < cutoff for updated_at in pulls),
|
|
81
|
+
"oldest_open_issue_updated_at": _oldest_iso(issues),
|
|
82
|
+
"oldest_open_pr_updated_at": _oldest_iso(pulls),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_github_datetime(value: str | None) -> datetime | None:
|
|
87
|
+
if not value:
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
91
|
+
except ValueError:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _oldest_iso(values: list[datetime]) -> str | None:
|
|
96
|
+
if not values:
|
|
97
|
+
return None
|
|
98
|
+
return min(values).isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def render_markdown(result: dict, github: dict | None = None) -> str:
|
|
7
|
+
lines: list[str] = []
|
|
8
|
+
lines.append("# Maintainer Readiness Report")
|
|
9
|
+
lines.append("")
|
|
10
|
+
lines.append(f"- Generated: {datetime.now(timezone.utc).isoformat(timespec='seconds')}")
|
|
11
|
+
lines.append(f"- Root: `{result.get('display_root', result['root'])}`")
|
|
12
|
+
lines.append(f"- Score: **{result['score']} / {result['max_score']}** ({result['percent']}%)")
|
|
13
|
+
lines.append(f"- Level: **{result['level']}**")
|
|
14
|
+
lines.append("")
|
|
15
|
+
|
|
16
|
+
if github:
|
|
17
|
+
lines.append("## Public GitHub Signals")
|
|
18
|
+
lines.append("")
|
|
19
|
+
lines.append(f"- Repository: [{github.get('full_name')}]({github.get('html_url')})")
|
|
20
|
+
lines.append(f"- Visibility: `{github.get('visibility')}`")
|
|
21
|
+
lines.append(f"- Stars: `{github.get('stars')}`")
|
|
22
|
+
lines.append(f"- Forks: `{github.get('forks')}`")
|
|
23
|
+
lines.append(f"- Open issues: `{github.get('open_issues')}`")
|
|
24
|
+
lines.append(f"- Last push: `{github.get('pushed_at')}`")
|
|
25
|
+
if github.get("activity_error"):
|
|
26
|
+
lines.append(f"- Open issue/PR activity: unavailable ({github.get('activity_error')})")
|
|
27
|
+
elif github.get("stale_days") is not None:
|
|
28
|
+
lines.append(
|
|
29
|
+
f"- Open issues sampled: `{github.get('open_issue_items_sampled')}` "
|
|
30
|
+
f"(`{github.get('stale_issue_items')}` stale over {github.get('stale_days')} days)"
|
|
31
|
+
)
|
|
32
|
+
lines.append(
|
|
33
|
+
f"- Open PRs sampled: `{github.get('open_pr_items_sampled')}` "
|
|
34
|
+
f"(`{github.get('stale_pr_items')}` stale over {github.get('stale_days')} days)"
|
|
35
|
+
)
|
|
36
|
+
if github.get("oldest_open_issue_updated_at"):
|
|
37
|
+
lines.append(f"- Oldest open issue update: `{github.get('oldest_open_issue_updated_at')}`")
|
|
38
|
+
if github.get("oldest_open_pr_updated_at"):
|
|
39
|
+
lines.append(f"- Oldest open PR update: `{github.get('oldest_open_pr_updated_at')}`")
|
|
40
|
+
lines.append("")
|
|
41
|
+
|
|
42
|
+
git = result.get("git", {})
|
|
43
|
+
lines.append("## Local Maintenance Signals")
|
|
44
|
+
lines.append("")
|
|
45
|
+
if git.get("available"):
|
|
46
|
+
lines.append(f"- Branch: `{git.get('branch')}`")
|
|
47
|
+
lines.append(f"- Last commit: `{git.get('last_commit')}`")
|
|
48
|
+
lines.append(f"- Commits in last 90 days: `{git.get('commits_last_90_days')}`")
|
|
49
|
+
lines.append(f"- Dirty files: `{git.get('dirty_files')}`")
|
|
50
|
+
else:
|
|
51
|
+
lines.append(f"- Git metadata unavailable: {git.get('reason', 'unknown')}")
|
|
52
|
+
lines.append("")
|
|
53
|
+
|
|
54
|
+
passed = [item for item in result["checks"] if item["passed"]]
|
|
55
|
+
missing = [item for item in result["checks"] if not item["passed"]]
|
|
56
|
+
lines.append("## Passing Signals")
|
|
57
|
+
lines.append("")
|
|
58
|
+
for item in passed:
|
|
59
|
+
lines.append(f"- {item['label']}: `{item['evidence']}` (+{item['weight']})")
|
|
60
|
+
if not passed:
|
|
61
|
+
lines.append("- None yet.")
|
|
62
|
+
lines.append("")
|
|
63
|
+
|
|
64
|
+
lines.append("## Missing Signals")
|
|
65
|
+
lines.append("")
|
|
66
|
+
for item in missing:
|
|
67
|
+
lines.append(f"- {item['label']}: {item['fix']} (+{item['weight']})")
|
|
68
|
+
if not missing:
|
|
69
|
+
lines.append("- None.")
|
|
70
|
+
lines.append("")
|
|
71
|
+
|
|
72
|
+
lines.append("## Ecosystem Recommendations")
|
|
73
|
+
lines.append("")
|
|
74
|
+
for item in result.get("ecosystems", []):
|
|
75
|
+
evidence = ", ".join(f"`{value}`" for value in item.get("evidence", [])) or "no manifest detected"
|
|
76
|
+
lines.append(f"### {item['ecosystem'].title()}")
|
|
77
|
+
lines.append("")
|
|
78
|
+
lines.append(f"- Evidence: {evidence}")
|
|
79
|
+
for recommendation in item.get("recommendations", []):
|
|
80
|
+
lines.append(f"- {recommendation}")
|
|
81
|
+
lines.append("")
|
|
82
|
+
|
|
83
|
+
warnings = result.get("secret_warnings", [])
|
|
84
|
+
lines.append("## High-Risk File Warnings")
|
|
85
|
+
lines.append("")
|
|
86
|
+
if warnings:
|
|
87
|
+
for warning in warnings:
|
|
88
|
+
lines.append(f"- `{warning['path']}`: {warning['reason']}")
|
|
89
|
+
else:
|
|
90
|
+
lines.append("- No high-risk credential filenames found.")
|
|
91
|
+
lines.append("")
|
|
92
|
+
|
|
93
|
+
lines.append("## Maintainer Program Notes")
|
|
94
|
+
lines.append("")
|
|
95
|
+
lines.append("- Use this report as evidence, not as a guarantee of eligibility.")
|
|
96
|
+
lines.append("- Do not claim usage, adoption, or maintainer permissions that cannot be verified.")
|
|
97
|
+
lines.append("- For new repositories, describe the project as early-stage and explain the concrete maintainer workflow it supports.")
|
|
98
|
+
lines.append("- Keep human approval in the loop for issue triage, PR review, releases, and public communication.")
|
|
99
|
+
lines.append("")
|
|
100
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def render_sarif(result: dict, github: dict | None = None) -> dict:
|
|
7
|
+
rules = []
|
|
8
|
+
sarif_results = []
|
|
9
|
+
for item in result.get("checks", []):
|
|
10
|
+
rule_id = f"maintainer-readiness.{item['check_id']}"
|
|
11
|
+
rules.append(
|
|
12
|
+
{
|
|
13
|
+
"id": rule_id,
|
|
14
|
+
"name": item["label"],
|
|
15
|
+
"shortDescription": {"text": item["label"]},
|
|
16
|
+
"help": {"text": item["fix"]},
|
|
17
|
+
"properties": {"weight": item["weight"]},
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
if not item.get("passed"):
|
|
21
|
+
sarif_results.append(
|
|
22
|
+
{
|
|
23
|
+
"ruleId": rule_id,
|
|
24
|
+
"level": "warning",
|
|
25
|
+
"message": {"text": item["fix"]},
|
|
26
|
+
"locations": [_location(result, item.get("evidence") or ".")],
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
rules.append(
|
|
31
|
+
{
|
|
32
|
+
"id": "maintainer-readiness.high-risk-file",
|
|
33
|
+
"name": "High-risk credential filename",
|
|
34
|
+
"shortDescription": {"text": "High-risk credential filename"},
|
|
35
|
+
"help": {"text": "Remove local credential/config files before publishing the repository."},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
for warning in result.get("secret_warnings", []):
|
|
39
|
+
sarif_results.append(
|
|
40
|
+
{
|
|
41
|
+
"ruleId": "maintainer-readiness.high-risk-file",
|
|
42
|
+
"level": "error",
|
|
43
|
+
"message": {"text": warning["reason"]},
|
|
44
|
+
"locations": [_location(result, warning["path"])],
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if github and github.get("stale_issue_items"):
|
|
49
|
+
rules.append(_github_rule("stale-issues", "Stale open issues"))
|
|
50
|
+
sarif_results.append(_github_result("stale-issues", f"{github['stale_issue_items']} open issues are stale."))
|
|
51
|
+
if github and github.get("stale_pr_items"):
|
|
52
|
+
rules.append(_github_rule("stale-prs", "Stale open pull requests"))
|
|
53
|
+
sarif_results.append(_github_result("stale-prs", f"{github['stale_pr_items']} open pull requests are stale."))
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
57
|
+
"version": "2.1.0",
|
|
58
|
+
"runs": [
|
|
59
|
+
{
|
|
60
|
+
"tool": {
|
|
61
|
+
"driver": {
|
|
62
|
+
"name": "Maintainer Readiness Kit",
|
|
63
|
+
"informationUri": "https://github.com/YUUDAI-s/maintainer-readiness-kit",
|
|
64
|
+
"rules": rules,
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"invocations": [
|
|
68
|
+
{
|
|
69
|
+
"executionSuccessful": True,
|
|
70
|
+
"endTimeUtc": datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"results": sarif_results,
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _location(result: dict, path: str) -> dict:
|
|
80
|
+
display_root = result.get("display_root") or result.get("root") or "."
|
|
81
|
+
artifact = "." if path == "missing" else path
|
|
82
|
+
return {
|
|
83
|
+
"physicalLocation": {
|
|
84
|
+
"artifactLocation": {
|
|
85
|
+
"uri": artifact,
|
|
86
|
+
"uriBaseId": display_root,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _github_rule(suffix: str, name: str) -> dict:
|
|
93
|
+
return {
|
|
94
|
+
"id": f"maintainer-readiness.github.{suffix}",
|
|
95
|
+
"name": name,
|
|
96
|
+
"shortDescription": {"text": name},
|
|
97
|
+
"help": {"text": "Review stale public GitHub work as part of routine maintainer triage."},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _github_result(suffix: str, message: str) -> dict:
|
|
102
|
+
return {
|
|
103
|
+
"ruleId": f"maintainer-readiness.github.{suffix}",
|
|
104
|
+
"level": "note",
|
|
105
|
+
"message": {"text": message},
|
|
106
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
TEMPLATES: dict[str, str] = {
|
|
7
|
+
"CONTRIBUTING.md": """# Contributing
|
|
8
|
+
|
|
9
|
+
Thanks for helping improve this project.
|
|
10
|
+
|
|
11
|
+
## Local Setup
|
|
12
|
+
|
|
13
|
+
1. Fork and clone the repository.
|
|
14
|
+
2. Install the documented runtime.
|
|
15
|
+
3. Run the smoke test or unit tests before opening a pull request.
|
|
16
|
+
|
|
17
|
+
## Pull Requests
|
|
18
|
+
|
|
19
|
+
- Keep changes focused.
|
|
20
|
+
- Include tests or a manual verification note.
|
|
21
|
+
- Document behavior changes in the README or changelog when relevant.
|
|
22
|
+
""",
|
|
23
|
+
"SECURITY.md": """# Security Policy
|
|
24
|
+
|
|
25
|
+
## Supported Versions
|
|
26
|
+
|
|
27
|
+
The default branch receives security fixes.
|
|
28
|
+
|
|
29
|
+
## Reporting a Vulnerability
|
|
30
|
+
|
|
31
|
+
Please open a private security advisory or contact the maintainer through the
|
|
32
|
+
repository owner profile. Do not disclose exploitable details publicly before a
|
|
33
|
+
fix or mitigation is available.
|
|
34
|
+
""",
|
|
35
|
+
".github/ISSUE_TEMPLATE/bug_report.yml": """name: Bug report
|
|
36
|
+
description: Report a reproducible bug.
|
|
37
|
+
title: "[Bug]: "
|
|
38
|
+
labels: ["bug"]
|
|
39
|
+
body:
|
|
40
|
+
- type: textarea
|
|
41
|
+
id: summary
|
|
42
|
+
attributes:
|
|
43
|
+
label: Summary
|
|
44
|
+
description: What happened?
|
|
45
|
+
validations:
|
|
46
|
+
required: true
|
|
47
|
+
- type: textarea
|
|
48
|
+
id: steps
|
|
49
|
+
attributes:
|
|
50
|
+
label: Reproduction steps
|
|
51
|
+
description: List the smallest steps that reproduce the issue.
|
|
52
|
+
validations:
|
|
53
|
+
required: true
|
|
54
|
+
- type: textarea
|
|
55
|
+
id: environment
|
|
56
|
+
attributes:
|
|
57
|
+
label: Environment
|
|
58
|
+
description: OS, version, runtime, and any relevant config.
|
|
59
|
+
""",
|
|
60
|
+
".github/ISSUE_TEMPLATE/feature_request.yml": """name: Feature request
|
|
61
|
+
description: Suggest an improvement.
|
|
62
|
+
title: "[Feature]: "
|
|
63
|
+
labels: ["enhancement"]
|
|
64
|
+
body:
|
|
65
|
+
- type: textarea
|
|
66
|
+
id: problem
|
|
67
|
+
attributes:
|
|
68
|
+
label: Problem
|
|
69
|
+
description: What problem would this solve?
|
|
70
|
+
validations:
|
|
71
|
+
required: true
|
|
72
|
+
- type: textarea
|
|
73
|
+
id: proposal
|
|
74
|
+
attributes:
|
|
75
|
+
label: Proposal
|
|
76
|
+
description: Describe the smallest useful change.
|
|
77
|
+
""",
|
|
78
|
+
".github/PULL_REQUEST_TEMPLATE.md": """## Summary
|
|
79
|
+
|
|
80
|
+
## Verification
|
|
81
|
+
|
|
82
|
+
- [ ] Tests or smoke checks were run.
|
|
83
|
+
- [ ] Documentation was updated, or no docs change is needed.
|
|
84
|
+
- [ ] Security/privacy impact was considered.
|
|
85
|
+
|
|
86
|
+
## Notes for Maintainers
|
|
87
|
+
""",
|
|
88
|
+
".github/workflows/maintainer-readiness.yml": """name: Maintainer readiness
|
|
89
|
+
|
|
90
|
+
on:
|
|
91
|
+
pull_request:
|
|
92
|
+
push:
|
|
93
|
+
branches: [main]
|
|
94
|
+
|
|
95
|
+
jobs:
|
|
96
|
+
smoke:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v4
|
|
100
|
+
- uses: actions/setup-python@v5
|
|
101
|
+
with:
|
|
102
|
+
python-version: "3.11"
|
|
103
|
+
- run: python -m unittest
|
|
104
|
+
""",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def write_templates(root: Path | str, force: bool = False) -> list[dict]:
|
|
109
|
+
root_path = Path(root).resolve()
|
|
110
|
+
written: list[dict] = []
|
|
111
|
+
for relative_path, content in TEMPLATES.items():
|
|
112
|
+
target = root_path / relative_path
|
|
113
|
+
if target.exists() and not force:
|
|
114
|
+
written.append({"path": relative_path, "status": "skipped"})
|
|
115
|
+
continue
|
|
116
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
target.write_text(content, encoding="utf-8", newline="\n")
|
|
118
|
+
written.append({"path": relative_path, "status": "written"})
|
|
119
|
+
return written
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maintainer-readiness-kit
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Generate maintainer-readiness reports for open source repositories.
|
|
5
|
+
Author: YUUDAI-s
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YUUDAI-s/maintainer-readiness-kit
|
|
8
|
+
Project-URL: Repository, https://github.com/YUUDAI-s/maintainer-readiness-kit
|
|
9
|
+
Project-URL: Issues, https://github.com/YUUDAI-s/maintainer-readiness-kit/issues
|
|
10
|
+
Keywords: open-source,maintainer,github,security,triage
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Maintainer Readiness Kit
|
|
26
|
+
|
|
27
|
+
[](https://github.com/YUUDAI-s/maintainer-readiness-kit/actions/workflows/maintainer-readiness.yml)
|
|
28
|
+
[](action.yml)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
[](pyproject.toml)
|
|
31
|
+
|
|
32
|
+
Maintainer Readiness Kit is a small, dependency-light CLI that audits an open
|
|
33
|
+
source repository for maintainer-facing signals: documentation, license files,
|
|
34
|
+
security policy, issue and pull request templates, CI, tests, recent git
|
|
35
|
+
activity, and high-risk local secret files.
|
|
36
|
+
|
|
37
|
+
The goal is simple: give solo and small-team maintainers a repeatable report
|
|
38
|
+
they can use before publishing a repository, onboarding contributors, or asking
|
|
39
|
+
for support from open source maintainer programs.
|
|
40
|
+
|
|
41
|
+
## Who Should Use It
|
|
42
|
+
|
|
43
|
+
- Maintainers preparing a repository for public contributors.
|
|
44
|
+
- Solo developers who need a concrete pre-release checklist.
|
|
45
|
+
- Teams that want CI to fail when maintainer basics regress.
|
|
46
|
+
- Open source applicants who need honest, shareable evidence instead of vague
|
|
47
|
+
claims.
|
|
48
|
+
|
|
49
|
+
## What It Helps You Decide
|
|
50
|
+
|
|
51
|
+
Use it when you need a quick answer to:
|
|
52
|
+
|
|
53
|
+
- Is this repository ready to make public?
|
|
54
|
+
- What maintainer files are missing before I invite contributors?
|
|
55
|
+
- Will CI fail if the repository falls below a readiness threshold?
|
|
56
|
+
- What ecosystem-specific maintenance steps should I add next?
|
|
57
|
+
- Can I share a report without leaking my local machine path?
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- Scores maintainer-readiness signals with evidence and suggested fixes.
|
|
62
|
+
- Reads local git activity without requiring network access.
|
|
63
|
+
- Optionally enriches the report with public GitHub repository signals.
|
|
64
|
+
- Summarizes stale open issues and pull requests for public GitHub reports.
|
|
65
|
+
- Generates starter maintainer templates for `CONTRIBUTING.md`,
|
|
66
|
+
`SECURITY.md`, issue templates, pull request templates, and a GitHub Actions
|
|
67
|
+
smoke workflow.
|
|
68
|
+
- Performs a conservative high-risk file check before public release.
|
|
69
|
+
- Outputs Markdown or JSON for CI and handoff docs.
|
|
70
|
+
- Outputs SARIF for CI and code-scanning workflows.
|
|
71
|
+
- Outputs Shields endpoint badge JSON for project dashboards.
|
|
72
|
+
- Runs as a reusable GitHub Action.
|
|
73
|
+
- Classifies readiness as `ready`, `nearly-ready`, or `needs-work`.
|
|
74
|
+
- Detects Python, Node.js, Rust, Go, and Java/JVM manifests and adds
|
|
75
|
+
ecosystem-specific maintainer recommendations.
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
Install from the repository:
|
|
80
|
+
|
|
81
|
+
```powershell
|
|
82
|
+
git clone https://github.com/YUUDAI-s/maintainer-readiness-kit.git
|
|
83
|
+
cd maintainer-readiness-kit
|
|
84
|
+
python -m pip install -e .
|
|
85
|
+
maintainer-readiness inspect . --output readiness-report.md
|
|
86
|
+
maintainer-readiness inspect . --fail-under 90
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use it directly in GitHub Actions:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v4
|
|
94
|
+
- uses: YUUDAI-s/maintainer-readiness-kit@v0.6.0
|
|
95
|
+
with:
|
|
96
|
+
repo: owner/name
|
|
97
|
+
fail-under: "80"
|
|
98
|
+
output: readiness-report.md
|
|
99
|
+
sarif: readiness.sarif
|
|
100
|
+
badge-json: readiness-badge.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Public demo repository:
|
|
104
|
+
[`YUUDAI-s/maintainer-readiness-kit-action-demo`](https://github.com/YUUDAI-s/maintainer-readiness-kit-action-demo)
|
|
105
|
+
uses `YUUDAI-s/maintainer-readiness-kit@v0.6.0` in CI.
|
|
106
|
+
|
|
107
|
+
After the package is published to PyPI:
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
python -m pip install maintainer-readiness-kit
|
|
111
|
+
maintainer-readiness inspect . --output readiness-report.md
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For local source development without installation:
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
$env:PYTHONPATH = "src"
|
|
118
|
+
python -m maintainer_readiness inspect . --output readiness-report.md
|
|
119
|
+
python -m maintainer_readiness inspect . --fail-under 90
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Typical output:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
Score: 100 / 100 (100.0%)
|
|
126
|
+
Level: ready
|
|
127
|
+
Ecosystem Recommendations: Python
|
|
128
|
+
High-Risk File Warnings: No high-risk credential filenames found.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To include public GitHub signals:
|
|
132
|
+
|
|
133
|
+
```powershell
|
|
134
|
+
python -m maintainer_readiness inspect . --repo YUUDAI-s/maintainer-readiness-kit --output readiness-report.md
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
To add starter maintainer files to another repository:
|
|
138
|
+
|
|
139
|
+
```powershell
|
|
140
|
+
python -m maintainer_readiness init C:\path\to\repo
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use `--force` only when you intentionally want to overwrite an existing starter
|
|
144
|
+
file.
|
|
145
|
+
|
|
146
|
+
## Commands
|
|
147
|
+
|
|
148
|
+
### `inspect`
|
|
149
|
+
|
|
150
|
+
```powershell
|
|
151
|
+
python -m maintainer_readiness inspect . --output readiness-report.md
|
|
152
|
+
python -m maintainer_readiness inspect . --json
|
|
153
|
+
python -m maintainer_readiness inspect . --repo owner/name
|
|
154
|
+
python -m maintainer_readiness inspect . --root-label public-sample
|
|
155
|
+
python -m maintainer_readiness inspect . --repo owner/name --stale-days 14
|
|
156
|
+
python -m maintainer_readiness inspect . --sarif readiness.sarif
|
|
157
|
+
python -m maintainer_readiness inspect . --badge-json readiness-badge.json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The Markdown report includes:
|
|
161
|
+
|
|
162
|
+
- overall readiness score,
|
|
163
|
+
- readiness level,
|
|
164
|
+
- passing and missing signals,
|
|
165
|
+
- local git maintenance evidence,
|
|
166
|
+
- optional public GitHub evidence,
|
|
167
|
+
- stale open issue and pull request counts when `--repo` is used,
|
|
168
|
+
- high-risk file warnings,
|
|
169
|
+
- ecosystem-specific recommendations,
|
|
170
|
+
- next actions before public release.
|
|
171
|
+
|
|
172
|
+
For CI, use `--fail-under` to make the command return a non-zero exit code when
|
|
173
|
+
the readiness percentage is below your chosen threshold.
|
|
174
|
+
|
|
175
|
+
Use `--stale-days` with `--repo` when your project has a shorter or longer
|
|
176
|
+
triage window than the default 30 days.
|
|
177
|
+
|
|
178
|
+
Use `--sarif readiness.sarif` when you want failed checks and high-risk file
|
|
179
|
+
warnings in a code-scanning compatible format.
|
|
180
|
+
|
|
181
|
+
Use `--badge-json readiness-badge.json` when you want a Shields-compatible
|
|
182
|
+
endpoint JSON payload for a dashboard or docs site.
|
|
183
|
+
|
|
184
|
+
### GitHub Actions
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
name: Maintainer readiness
|
|
188
|
+
|
|
189
|
+
on:
|
|
190
|
+
pull_request:
|
|
191
|
+
push:
|
|
192
|
+
branches: [main]
|
|
193
|
+
|
|
194
|
+
jobs:
|
|
195
|
+
smoke:
|
|
196
|
+
runs-on: ubuntu-latest
|
|
197
|
+
steps:
|
|
198
|
+
- uses: actions/checkout@v4
|
|
199
|
+
- uses: YUUDAI-s/maintainer-readiness-kit@v0.6.0
|
|
200
|
+
with:
|
|
201
|
+
repo: owner/name
|
|
202
|
+
fail-under: "80"
|
|
203
|
+
output: readiness-report.md
|
|
204
|
+
sarif: readiness.sarif
|
|
205
|
+
badge-json: readiness-badge.json
|
|
206
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
207
|
+
if: always()
|
|
208
|
+
with:
|
|
209
|
+
sarif_file: readiness.sarif
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `init`
|
|
213
|
+
|
|
214
|
+
```powershell
|
|
215
|
+
python -m maintainer_readiness init .
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This writes starter maintainer files only when they do not already exist:
|
|
219
|
+
|
|
220
|
+
- `CONTRIBUTING.md`
|
|
221
|
+
- `SECURITY.md`
|
|
222
|
+
- `MAINTAINERS.md`
|
|
223
|
+
- `.github/ISSUE_TEMPLATE/bug_report.yml`
|
|
224
|
+
- `.github/ISSUE_TEMPLATE/feature_request.yml`
|
|
225
|
+
- `.github/PULL_REQUEST_TEMPLATE.md`
|
|
226
|
+
- `.github/workflows/maintainer-readiness.yml`
|
|
227
|
+
|
|
228
|
+
## Design Principles
|
|
229
|
+
|
|
230
|
+
- Honest evidence over vanity metrics.
|
|
231
|
+
- Minimal runtime dependencies.
|
|
232
|
+
- Useful defaults for maintainers who work alone.
|
|
233
|
+
- No external writes from `inspect`.
|
|
234
|
+
- No claims that a repository qualifies for any external program.
|
|
235
|
+
|
|
236
|
+
## Maintainer Workflows
|
|
237
|
+
|
|
238
|
+
This project is built for routine maintainer tasks:
|
|
239
|
+
|
|
240
|
+
- pre-publication checks before making a repository public,
|
|
241
|
+
- contributor onboarding checks before accepting outside PRs,
|
|
242
|
+
- release-readiness checks before tagging a version,
|
|
243
|
+
- safety checks before attaching reports to sponsorship or maintainer-support
|
|
244
|
+
applications,
|
|
245
|
+
- CI-friendly JSON output for repeatable repository hygiene reviews.
|
|
246
|
+
|
|
247
|
+
## Limitations
|
|
248
|
+
|
|
249
|
+
This tool cannot prove that a repository is widely adopted, safe, or eligible
|
|
250
|
+
for any benefit. It only turns common maintainer signals into a compact,
|
|
251
|
+
verifiable report. Program applications still require accurate information
|
|
252
|
+
about the applicant, repository, role, usage, and maintainer status.
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```powershell
|
|
257
|
+
$env:PYTHONPATH = "src"
|
|
258
|
+
python -m unittest discover -s tests
|
|
259
|
+
python -m maintainer_readiness inspect . --output readiness-report.md
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
See [ROADMAP.md](ROADMAP.md) for near-term maintainer-focused work.
|
|
263
|
+
See [examples/reports](examples/reports) for generated reports from real
|
|
264
|
+
repositories.
|
|
265
|
+
See the public action demo at
|
|
266
|
+
[YUUDAI-s/maintainer-readiness-kit-action-demo](https://github.com/YUUDAI-s/maintainer-readiness-kit-action-demo).
|
|
267
|
+
See [docs/pypi.md](docs/pypi.md) for package build and publishing notes.
|
|
268
|
+
See [docs/community-launch.md](docs/community-launch.md) for community launch
|
|
269
|
+
copy and posting rules.
|
|
270
|
+
See [examples/github-action.yml](examples/github-action.yml) for a copyable
|
|
271
|
+
GitHub Actions workflow.
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
maintainer_readiness/__init__.py,sha256=d7z5SbcgKnhdJ_1PjcK0syEAV_NfBXAcbXZ_fb4edYA,116
|
|
2
|
+
maintainer_readiness/__main__.py,sha256=PSQ4rpL0dG6f-qH4N7H-gD9igQkdHzH4yVZDcW8lfZo,80
|
|
3
|
+
maintainer_readiness/badge.py,sha256=_EpBYi4iacavwhjWi2cYTybQxXXlLGGXTyyb3yoQAVM,408
|
|
4
|
+
maintainer_readiness/checks.py,sha256=oKvb10CUUSoJWL9Khh4EOAEOgu_UeTL0cf-dhRkJZT4,9424
|
|
5
|
+
maintainer_readiness/cli.py,sha256=cgDh0aHyIln8W-HCQjJyCRmKrcjtN-FMAaieQ-qdyIs,4637
|
|
6
|
+
maintainer_readiness/github.py,sha256=UfQc7gSg4KqKLIH_4Rp_dFhfgdpUxfnvh0F5aAKqzCo,3692
|
|
7
|
+
maintainer_readiness/report.py,sha256=5w6DRG4d4k8JIye4cbJjVQD0VDxAU_DAwE747KF_-ho,4615
|
|
8
|
+
maintainer_readiness/sarif.py,sha256=hmZSOGcC2dq2BQHs4CZJePTPgFGbLsOyj_afw8oq4Ac,3708
|
|
9
|
+
maintainer_readiness/templates.py,sha256=1LZaApzcr2y0FYltxRc9DklDz48ylIaoGtnxBS-6jkE,3090
|
|
10
|
+
maintainer_readiness_kit-0.6.1.dist-info/licenses/LICENSE,sha256=3Av74KsPftReiKqDpDz_NOXQTqx-r_pDyYwkQumHnv0,1065
|
|
11
|
+
maintainer_readiness_kit-0.6.1.dist-info/METADATA,sha256=LdqmUycwKwLONEyWplcYLavbmoGC9SmBtKmtMDDnUyk,9383
|
|
12
|
+
maintainer_readiness_kit-0.6.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
maintainer_readiness_kit-0.6.1.dist-info/entry_points.txt,sha256=9lIWav-AHLQQWHss0cPoAsjpQrJCUTU0TVK2GdbDMb4,71
|
|
14
|
+
maintainer_readiness_kit-0.6.1.dist-info/top_level.txt,sha256=4RcbzRmQd6_4ogZSvUrlrE9uUC8mMJLieeL0QlqguEc,21
|
|
15
|
+
maintainer_readiness_kit-0.6.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YUUDAI-s
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
maintainer_readiness
|