diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7652870..eb26b09d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,25 @@ and then the checkout with `--no-deps`; the floors in `pyproject.toml` are uncha When a dependency or an extra changes, run `scripts/lock.sh` and commit what it rewrote, or CI installs the old resolution against the new declaration. +## Coding standards + +Python, 3.11 and later, in the style `ruff` enforces from `pyproject.toml`: + +- **Formatting** is `ruff format` (Black-compatible, line length 100). There is no + discretion here: a file either matches the formatter's output or CI is red. +- **Linting** is `ruff check` with the rule sets `E`, `F`, `W` (pycodestyle and pyflakes), + `I` (import order), `N` (PEP 8 naming), `UP` (no deprecated syntax or APIs), `B` (bugbear's + likely defects), `ANN` (every signature annotated), `SIM` and `RUF`. +- **Types** are checked by `mypy --strict` over `src/`, with `warn_unreachable` on. Untyped + code does not merge. +- **Exceptions are rare and carry their reason.** A `# noqa: ` names the rule and says + why on the same line; a per-file ignore lives in `pyproject.toml` with a comment above it. + A reviewer may ask for either to be removed. + +`scripts/check.sh` runs all three before the tests, cheapest first, and CI runs that same +script, so a style failure is found on your machine in a second rather than in CI in ten +minutes. + ## Specification first Every version is a specification before it is code: `docs/SPEC-v0.1.md` through @@ -68,7 +87,13 @@ spec listed the check against one method and nothing enumerated the others. `doc ## Tests first Write the acceptance tests before the implementation. A red suite is the specification; make -it green. Then: +it green. +**The policy, stated once so that it can be pointed at:** new functionality MUST arrive with +tests for it, in the same pull request, in the automated suite that CI runs. A pull request +that adds behaviour without tests for that behaviour does not merge, whatever else it does +well. A bug fix MUST arrive with the regression test that failed before the fix. + +Then: - **Every MUST is mutation-tested.** Remove the check, confirm the named test fails, restore it, and put the table in the pull request. A row that stays green is a guard nothing @@ -118,6 +143,36 @@ The same standard applies to *not applicable*: `ctrlrun verify` reports a guaran configuration cannot exercise as `N/A` with the reason, never as a pass, and there is no flag that folds one into the count. +## Code review + +Every pull request is reviewed on GitHub by a person other than its author before it merges, +and nobody merges their own. That is the whole of the rule for who; this section is what the +review checks and what makes a change acceptable. + +**How it is conducted.** The reviewer reads the pull request against the specification +section it claims to implement, not against the diff alone, and writes findings as review +comments on the lines they concern. A finding is answered in the pull request, by a change or +by a written reason; a declined finding keeps its reasoning in the thread. Automated review +comments (CodeRabbit, CodeQL) are read and answered the same way, and none of them counts as +the human review. + +**What must be checked.** + +- The specification section exists and the change does what it says, no more. +- The tests came first and the mutation table is in the description for every MUST the change + touches (*Tests first* above, including the four shapes of a false green). +- Every public name is frozen in the spec's names section, every entry point is enumerated, + and `CLAIMS.md` in `CTRLRun/ctrlrun-docs` still holds for every sentence the change affects. +- The change does not weaken a fail-closed rule, widen a default, add an unpinned install, a + write permission to a workflow, a binary, or a dependency that is not in the locks. +- The commits are signed off and the message says what changed and why. + +**What is required to be acceptable.** One approving review from a person who did not write +the change; the required checks green (`check` on every Python version, `package`, `gate`); +every finding answered; and, for anything under `src/`, the maintainer's read. The independent +review below is in addition to this for the parts of the kernel where a defect is a security +defect. + ## Independent review Anything touching authorization, identity, delegation, the gateway, an adapter or the store diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 00000000..8265314a --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,49 @@ +# Governance + +How this project makes decisions, who holds which role, and how it continues if one person +disappears. The rules for a change itself are in [CONTRIBUTING.md](CONTRIBUTING.md); the +rules for a security report are in [SECURITY.md](SECURITY.md). + +## Decisions + +CTRLRun is maintainer-led. The maintainer has the final say on scope, on what a +specification says, and on whether a change merges or a release ships. A decision that +changes a shipped guarantee is written into the specification and the changelog with its +reason, never settled in a review comment. Disagreement is argued in public, in the issue or +the pull request, and the written specification is the record of what was decided. Anyone may +fork under Apache-2.0; that is the check on the maintainer. + +## Roles + +| Role | Who | Responsibilities | +|---|---|---| +| Maintainer | Arpan Ghoshal ([@arpanghoshal](https://github.com/arpanghoshal)) | Owns the specifications and the roadmap. Final say on merges and releases. Answers security reports per SECURITY.md. Administers the `CTRLRun` GitHub organization, the `ctrlrun` project on PyPI, and ctrlrun.dev. | +| Committer | Rohan Kamath ([@rohanrkamath](https://github.com/rohanrkamath)) | Reviews and merges pull requests, the maintainer's included. Can cut a release by pushing a tag. Triages issues. Holds write access to every repository in the organization. | +| Contributor | anyone | Opens issues and pull requests under the rules in CONTRIBUTING.md, with every commit signed off under the DCO. | + +A role is granted and withdrawn by the maintainer, and this file is the record of who holds +which. Nobody merges their own pull request: every change to `main` is reviewed and merged by +a person who did not write it (CONTRIBUTING.md, *Code review*). + +## Continuity + +If any one person is unavailable, the project continues within a week, and this is what +makes that true: + +- **Two people can merge and release.** The maintainer and the committer both hold write + access. A release is a tag: `publish.yml` publishes to PyPI through trusted publishing and + `release.yml` signs the provenance against the workflow's own identity, so no release + depends on a key or a password that one person holds alone. +- **Two people own the organization.** Both are owners of the `CTRLRun` GitHub organization, + so either can grant access, change a workflow, or answer a private vulnerability report. +- **Two people own the package.** Both are owners of `ctrlrun` on PyPI, so the trusted + publisher can be repaired by either. +- **Two people can reach the site.** Both have access to the DNS zone for ctrlrun.dev and to + the documentation host, so the site and the docs keep publishing. + +Nothing about the project lives only on one person's machine: the specifications, the +build, the locks and the release process are all in the repositories. + +## Changing this document + +By pull request, like anything else, reviewed by the other role holder. diff --git a/SECURITY.md b/SECURITY.md index 50eb6b83..a0980a60 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,6 +11,28 @@ sequence of actions. A failing test is the fastest possible report. Expect an acknowledgement within 72 hours and an assessment within seven days. If a fix is warranted you will be credited in the release notes unless you ask not to be. +## Response process + +What happens to a report, in order, and the reporter hears from us at each step: + +1. **Acknowledgement within 72 hours** of the report arriving, from the maintainer, with a + tracking reference that stays private. +2. **Assessment within seven days.** We reproduce the problem, place it against the threat + model at https://ctrlrun.dev/docs/THREAT_MODEL (in scope, out of scope, or a documented + limitation), and tell the reporter which and why. An out-of-scope report gets a written + reason, not silence. +3. **A fix on a private branch**, with the regression test that fails before it and passes + after, reviewed by the other maintainer role holder before it merges. The fix ships in a + patch release on the supported line. +4. **Disclosure.** The release's CHANGELOG entry names the fix. For a report in scope we + publish a GitHub Security Advisory on the repository and request a CVE through it. We aim + to release within 90 days of the report; if a fix takes longer, the reporter is told when + and why, and the advisory is published no later than the fix. +5. **Credit** in the release notes and the advisory, unless the reporter asks not to be named. + +Reports may also be filed through GitHub's private vulnerability reporting on the repository; +the same process applies. + ## Provenance Releases carry PyPI provenance attestations from GitHub Actions. Distributions are published @@ -49,8 +71,13 @@ CTRLRun is pre-1.0. Only the latest release receives fixes. | Version | Supported | |---|---| -| 0.6.x | yes | -| < 0.6 | no | +| the latest release line (the newest `0.x` on PyPI) | yes | +| earlier lines | no | + +The upgrade path is `pip install --upgrade ctrlrun`. Every change to a public name and every +store migration is listed in [CHANGELOG.md](CHANGELOG.md) under the release that made it, +with what to do about it; migrations run forward on first use and are numbered, and a +migration that cannot be reversed says so in its entry. ## What counts as a vulnerability diff --git a/tests/test_repository_signals.py b/tests/test_repository_signals.py index a6f34006..d99cd9ea 100644 --- a/tests/test_repository_signals.py +++ b/tests/test_repository_signals.py @@ -240,9 +240,21 @@ def test_the_community_files_exist_and_say_what_they_must(): # The contribution agreement is the DCO and nothing more; the file has to say so. "Developer Certificate of Origin", "git commit -s", + # The written policies the best-practices criteria point at, each by its heading. + "## Coding standards", + "## Code review", + "new functionality MUST arrive with\ntests", ): assert phrase in contributing, phrase + governance = (REPO_ROOT / "GOVERNANCE.md").read_text(encoding="utf-8") + for phrase in ("## Decisions", "## Roles", "## Continuity", "@arpanghoshal", "@rohanrkamath"): + assert phrase in governance, phrase + + security = (REPO_ROOT / "SECURITY.md").read_text(encoding="utf-8") + for phrase in ("## Response process", "72 hours", "Security Advisory", "Credit"): + assert phrase in security, phrase + conduct = (REPO_ROOT / "CODE_OF_CONDUCT.md").read_text(encoding="utf-8") assert "Contributor Covenant" in conduct assert "contact@arpanghoshal.com" in conduct