docs: add CONTRIBUTING.md and CII best-practices criteria helper
The scripts/cii_criteria.py helper fetches bestpractices.dev criteria and generates pre-filled edit URLs for each criterion, streamlining badge maintenance. CONTRIBUTING.md backs the contribution and testing criteria referenced by the CII best-practices submission.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# Contributing to MT5-Quant
|
||||
|
||||
Thanks for your interest in improving MT5-Quant! This document explains how to
|
||||
contribute and what we expect from contributions.
|
||||
|
||||
## How to contribute
|
||||
|
||||
1. **Fork** the repository and create a topic branch from `master`.
|
||||
2. Make your change, keeping it focused (one logical change per PR).
|
||||
3. Open a **pull request against `master`**. A pull request template
|
||||
(`.github/PULL_REQUEST_TEMPLATE.md`) is provided — please fill it in.
|
||||
4. Ensure CI passes (build, `cargo test`, clippy, formatting, cargo-deny,
|
||||
CodeQL). Maintainers review and merge once checks and review are satisfied.
|
||||
|
||||
## Development setup
|
||||
|
||||
- Install Rust (stable toolchain) and, for full backtests, Wine/MT5 as described
|
||||
in `README.md` and `docs/QUICKSTART.md`.
|
||||
- Build the release binary with either:
|
||||
- `bash scripts/build-rust.sh`, or
|
||||
- `cargo build --release`
|
||||
- The binary is produced at `target/release/mt5-quant`.
|
||||
|
||||
## Testing requirement
|
||||
|
||||
- **Non-trivial changes must include tests.** New functionality should come with
|
||||
unit or integration tests where practical.
|
||||
- `cargo test` runs automatically in CI (`rust.yml`) on every push and pull
|
||||
request, so a change that breaks tests cannot merge.
|
||||
|
||||
## Commit style
|
||||
|
||||
- Follow **Conventional Commits** (`feat:`, `fix:`, `docs:`, `refactor:`,
|
||||
`test:`, `release:`), as documented in `AGENTS.md`.
|
||||
|
||||
## Issue tracker
|
||||
|
||||
- Bugs and enhancement requests are tracked on the project's
|
||||
[GitHub Issues](https://github.com/masdevid/mt5-quant/issues), with
|
||||
bug-report and feature-request templates under `.github/ISSUE_TEMPLATE/`.
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List incomplete OpenSSF Best Practices criteria and generate a pre-filled edit URL.
|
||||
|
||||
Usage:
|
||||
cii_criteria.py <project_id> [section] [--exclude n1,n2,n3]
|
||||
|
||||
section: passing | silver | gold | baseline-1 | baseline-2 | baseline-3
|
||||
(default: passing)
|
||||
--exclude: comma-separated criterion names to SKIP from the pre-filled URL
|
||||
(they stay at their current Unmet/?/N/A in the form).
|
||||
|
||||
The script fetches the upstream criteria.yml to learn which criteria belong to
|
||||
which tier/section, then restricts BOTH the printed incomplete list AND the
|
||||
generated edit-URL parameters to only that section's criteria.
|
||||
"""
|
||||
import sys, os, json, urllib.request, urllib.parse, argparse
|
||||
import ssl
|
||||
|
||||
try:
|
||||
import certifi
|
||||
_SSL_CTX = ssl.create_default_context(cafile=certifi.where())
|
||||
except Exception:
|
||||
_SSL_CTX = ssl.create_default_context()
|
||||
|
||||
BASE = "https://www.bestpractices.dev"
|
||||
CRITERIA_URL = ("https://raw.githubusercontent.com/ossf/best-practices-badge/"
|
||||
"main/criteria/criteria.yml")
|
||||
|
||||
# CLI section argument -> tier key inside criteria.yml
|
||||
SECTION_TO_TIER = {
|
||||
"passing": "0",
|
||||
"silver": "1",
|
||||
"gold": "2",
|
||||
"baseline-1": "baseline-1",
|
||||
"baseline-2": "baseline-2",
|
||||
"baseline-3": "baseline-3",
|
||||
}
|
||||
|
||||
|
||||
def load_justifications():
|
||||
"""Load real justifications from the sibling cii_justifications.json.
|
||||
|
||||
Returns a dict name -> justification string. Falls back to {} if the file
|
||||
is missing or invalid, so callers should still default to a placeholder.
|
||||
"""
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"cii_justifications.json")
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def fetch(url):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "cii-skill"})
|
||||
with urllib.request.urlopen(req, timeout=20, context=_SSL_CTX) as r:
|
||||
return r.read().decode()
|
||||
|
||||
|
||||
def _collect_names(node, out):
|
||||
"""Recursively walk the criteria.yml nested structure.
|
||||
|
||||
The file is a list of (tier_key, [sections]); each section is
|
||||
(section_name, [ (group, [...]) | (criterion_name, {attrs}) ]). A leaf
|
||||
criterion is a 2-tuple whose second element is a dict (it has 'category').
|
||||
"""
|
||||
if isinstance(node, tuple) and len(node) == 2:
|
||||
key, val = node
|
||||
if isinstance(val, dict):
|
||||
out.append(key) # leaf criterion
|
||||
elif isinstance(val, list):
|
||||
for child in val:
|
||||
_collect_names(child, out)
|
||||
elif isinstance(node, list):
|
||||
for child in node:
|
||||
_collect_names(child, out)
|
||||
|
||||
|
||||
def fetch_section_map():
|
||||
"""Return {section_arg: set(criterion_names)} for every known tier.
|
||||
|
||||
Returns None if criteria.yml could not be fetched/parsed.
|
||||
"""
|
||||
try:
|
||||
raw = fetch(CRITERIA_URL)
|
||||
except Exception as e:
|
||||
print(f"WARNING: could not fetch criteria definitions ({e}); "
|
||||
f"falling back to all-criteria behavior.", file=sys.stderr)
|
||||
return None
|
||||
try:
|
||||
doc = __import__("yaml").safe_load(raw)
|
||||
except Exception as e:
|
||||
print(f"WARNING: could not parse criteria definitions ({e}); "
|
||||
f"falling back to all-criteria behavior.", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# doc is a list of (tier_key, sections)
|
||||
tier_names = {}
|
||||
for item in doc:
|
||||
if isinstance(item, tuple) and len(item) == 2:
|
||||
tier_key, sections = item
|
||||
names = []
|
||||
_collect_names(sections, names)
|
||||
tier_names[tier_key] = set(names)
|
||||
|
||||
section_map = {}
|
||||
for section_arg, tier_key in SECTION_TO_TIER.items():
|
||||
section_map[section_arg] = tier_names.get(tier_key, set())
|
||||
return section_map
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="List incomplete CII/OpenSSF Best Practices criteria.")
|
||||
parser.add_argument("project_id", nargs="?", default="14227")
|
||||
parser.add_argument("section", nargs="?", default="passing",
|
||||
choices=list(SECTION_TO_TIER.keys()))
|
||||
parser.add_argument("--exclude", default="",
|
||||
help="Comma-separated criterion names to skip in the URL.")
|
||||
args = parser.parse_args()
|
||||
|
||||
project_id = args.project_id
|
||||
section = args.section
|
||||
exclude = {x.strip() for x in args.exclude.split(",") if x.strip()}
|
||||
|
||||
justifications = load_justifications()
|
||||
|
||||
data = json.loads(fetch(f"{BASE}/projects/{project_id}.json"))
|
||||
|
||||
# All criteria reported by the project (every tier).
|
||||
criteria = {}
|
||||
for k, v in data.items():
|
||||
if k.endswith("_status") and isinstance(v, str):
|
||||
name = k[: -len("_status")]
|
||||
criteria[name] = {"status": v,
|
||||
"justification": data.get(name + "_justification", "") or ""}
|
||||
|
||||
# Build the section -> criterion-name set from upstream definitions.
|
||||
section_map = fetch_section_map()
|
||||
target_names = section_map.get(section) if section_map else None
|
||||
if target_names is not None and not target_names:
|
||||
print(f"WARNING: no criteria found for section '{section}' in "
|
||||
f"criteria.yml; falling back to all-criteria behavior.",
|
||||
file=sys.stderr)
|
||||
target_names = None # fall back: no section restriction
|
||||
|
||||
# Restrict to the requested section when we have a valid name set.
|
||||
if target_names is not None:
|
||||
scoped = {n: c for n, c in criteria.items() if n in target_names}
|
||||
else:
|
||||
scoped = criteria
|
||||
|
||||
incomplete = {n: c for n, c in scoped.items() if c["status"] != "Met"}
|
||||
doable = {n: c for n, c in incomplete.items() if c["status"] in ("Unmet", "?")}
|
||||
na = {n: c for n, c in incomplete.items() if c["status"] == "N/A"}
|
||||
|
||||
print(f"Project {project_id}: badge_level={data.get('badge_level')} "
|
||||
f"(passing%={data.get('badge_percentage_0')} "
|
||||
f"silver%={data.get('badge_percentage_1')} "
|
||||
f"gold%={data.get('badge_percentage_2')})")
|
||||
scope_note = f"section '{section}'" if target_names is not None else "ALL sections (fallback)"
|
||||
print(f"Scope: {scope_note} | criteria in scope: {len(scoped)} | "
|
||||
f"Incomplete: {len(incomplete)} "
|
||||
f"(Unmet/? : {len(doable)} | N/A: {len(na)})\n")
|
||||
|
||||
print("Incomplete (Unmet/? — candidate to mark Met):")
|
||||
for n, c in sorted(doable.items()):
|
||||
print(f" - {n}: {c['status']} | current note: {c['justification'][:70]}")
|
||||
print("\nN/A (review separately — usually leave as N/A):")
|
||||
for n, c in sorted(na.items()):
|
||||
print(f" - {n}: N/A | note: {c['justification'][:70]}")
|
||||
|
||||
# Build URL params, restricted to the section AND honoring --exclude.
|
||||
# N/A criteria are never pre-filled as Met (they are excluded from `doable`).
|
||||
params = []
|
||||
for n in sorted(doable):
|
||||
if exclude and n in exclude:
|
||||
continue # leave at current Unmet/?/N/A in the form
|
||||
params.append((f"{n}_status", "Met"))
|
||||
params.append((f"{n}_justification",
|
||||
justifications.get(n, "TODO: justify")))
|
||||
params.append(("overrides", "*,osps_*"))
|
||||
edit_url = f"{BASE}/projects/{project_id}/{section}/edit?{urllib.parse.urlencode(params)}"
|
||||
print(f"\n=== Edit URL ({section}"
|
||||
+ (f", excluding {sorted(exclude)}" if exclude else "")
|
||||
+ f") ===\n{edit_url}")
|
||||
print(f"\nURL param count for _status/_justification pairs: "
|
||||
f"{sum(1 for k,_ in params if k.endswith('_status'))}")
|
||||
print("WARNING: pre-fills Unmet/? criteria as 'Met' with a placeholder justification.")
|
||||
print("Review EVERY field before Save; only mark Met what you genuinely satisfy.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"build": "The project builds with Cargo; CI (.github/workflows/rust.yml) runs `cargo build --verbose` on every push and pull request.",
|
||||
"build_common_tools": "Builds use Cargo, the standard and widely-adopted Rust build toolchain.",
|
||||
"build_floss_tools": "All build and CI tooling is FLOSS: Cargo/rustc, GitHub Actions, clippy, and cargo-deny.",
|
||||
"contribution_requirements": "CONTRIBUTING.md documents forking, PRs against `master`, the PR template, dev setup, the testing requirement, and Conventional Commits.",
|
||||
"crypto_call": "Outbound internet calls (GitHub API for version checks/downloads) use HTTPS via the system `curl`, which negotiates TLS — no plaintext internet communication.",
|
||||
"crypto_floss": "TLS is provided by the system `curl`/OS libraries (FLOSS); the project bundles no proprietary cryptography.",
|
||||
"crypto_keylength": "Relies on `curl`/OS TLS defaults (modern ciphers with >=128-bit security and standard RSA/ECDSA key sizes).",
|
||||
"crypto_pfs": "Modern TLS via `curl` negotiates forward secrecy (ECDHE) with current GitHub endpoints.",
|
||||
"crypto_published": "Uses the widely-published TLS protocol through the standard `curl` toolchain rather than any custom or unpublished algorithm.",
|
||||
"crypto_random": "The project generates no security-sensitive random values (only UUIDs for internal job IDs), so no cryptographic RNG is required for its functionality.",
|
||||
"crypto_working": "TLS HTTPS calls to GitHub succeed in practice; the version-check and self-update features work as designed.",
|
||||
"enhancement_responses": "Enhancement requests are tracked on the public GitHub issue tracker (with a feature-request template) and reviewed through PRs.",
|
||||
"no_leaked_credentials": "Sensitive local config (config/mt5-quant.yaml, which can hold passwords/tokens) is gitignored, and no secrets are committed to the repository.",
|
||||
"repo_interim": "The repository at github.com/masdevid/mt5-quant is the canonical project home, not a temporary or interim placeholder.",
|
||||
"report_archive": "GitHub automatically archives the source; tagged releases additionally publish downloadable source and binary archives.",
|
||||
"report_responses": "The project uses GitHub Issues (with bug/feature templates) as its public tracker, and maintainers triage and respond via issues and PR reviews.",
|
||||
"static_analysis": "CI runs Rust Clippy and GitHub CodeQL (Rust) as static analysis on every push and pull request.",
|
||||
"static_analysis_fixed": "Clippy runs with `-D warnings` so defects fail the build, and CodeQL findings are reviewed and remediated.",
|
||||
"static_analysis_often": "Static analysis runs automatically on every push to and pull request against `master` (clippy in rust.yml, CodeQL in codeql.yml).",
|
||||
"test": "Unit and integration tests exist in `tests/` (Rust integration_tests.rs, test_mcp.py, shell tests) and `cargo test` runs in CI.",
|
||||
"test_most": "Unit tests cover core modules (analytics extraction/analysis, storage, report parsing); `cargo test` is part of CI.",
|
||||
"test_policy": "AGENTS.md and CONTRIBUTING.md require that non-trivial changes include tests, and CI enforces `cargo test`.",
|
||||
"tests_are_added": "The `tests/` directory contains Rust, Python, and shell tests, and new non-trivial functionality is expected to add corresponding tests.",
|
||||
"tests_documented_added": "Testing expectations and the `cargo test` requirement are documented in AGENTS.md and CONTRIBUTING.md.",
|
||||
"version_semver": "The `version` field in Cargo.toml uses Semantic Versioning (currently 1.34.3).",
|
||||
"version_tags": "Releases are tagged with `vX.Y.Z` semver tags (e.g., v1.34.1), confirmed via `git tag`.",
|
||||
"version_unique": "Each release carries a unique `version` in Cargo.toml and a corresponding immutable `vX.Y.Z` git tag.",
|
||||
"vulnerabilities_critical_fixed": "No known unpatched critical vulnerabilities; `cargo-deny` advisories and dependency-review run in CI and dependency updates are monitored.",
|
||||
"vulnerabilities_fixed_60_days": "Dependency vulnerabilities are surfaced by `cargo-deny` (advisories) and the dependency-review action in CI, and are updated promptly.",
|
||||
"warnings": "CI enforces a warning-free build via `cargo clippy --all-targets -- -D warnings` plus `cargo fmt --check`.",
|
||||
"warnings_fixed": "Because Clippy uses `-D warnings`, any warning fails CI and must be fixed before a change can merge."
|
||||
}
|
||||
Reference in New Issue
Block a user