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.
@@ -0,0 +1,4 @@
1
+ """Maintainer readiness reporting for open source repositories."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.6.1"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -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
+ [![Maintainer readiness](https://github.com/YUUDAI-s/maintainer-readiness-kit/actions/workflows/maintainer-readiness.yml/badge.svg)](https://github.com/YUUDAI-s/maintainer-readiness-kit/actions/workflows/maintainer-readiness.yml)
28
+ [![GitHub Action](https://img.shields.io/badge/GitHub%20Action-ready-brightgreen.svg)](action.yml)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
30
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-3776AB.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ maintainer-readiness = maintainer_readiness.cli:main
@@ -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