diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 3cc6afa..98d186d 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,10 +1,13 @@ -# Review feature PRs that follow the CodesWhat dev-branch flow. CodeRabbit -# reviews the default branch without an explicit base-branch entry. +# Request the first review only after the coordinator admits a stable candidate. +# Later fix batches need an explicit review request; pushes do not spend capacity. language: en-US reviews: auto_review: - enabled: true + enabled: false + auto_incremental_review: false drafts: false + labels: + - "review-ready" base_branches: - "^dev/.*$" diff --git a/.github/tests/community_health_contract_test.py b/.github/tests/community_health_contract_test.py index 69c2020..d62de0f 100644 --- a/.github/tests/community_health_contract_test.py +++ b/.github/tests/community_health_contract_test.py @@ -171,11 +171,10 @@ def test_standards_workflow_pins_actions_and_runs_every_contract(self): for expected in ( "persist-credentials: false", "community_health_contract_test.py", - "python3 -m compileall -q .", - "YAML.parse_file", - "json.load", - "markdownlint-cli2@0.23.2", - "markdownlint-profile.yaml", + "python3 .github/tests/tracked_validation_test.py", + "python3 scripts/validate_tracked_files.py python", + "python3 scripts/validate_tracked_files.py data", + "python3 scripts/validate_tracked_files.py markdown", "actionlint", "zizmorcore/zizmor-action@", "github/codeql-action/init@", diff --git a/.github/tests/tracked_validation_test.py b/.github/tests/tracked_validation_test.py new file mode 100644 index 0000000..b657ab1 --- /dev/null +++ b/.github/tests/tracked_validation_test.py @@ -0,0 +1,102 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +VALIDATOR = ROOT / "scripts/validate_tracked_files.py" + + +class TrackedValidationTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="tracked-validation-") + self.addCleanup(self.temp.cleanup) + self.repo = Path(self.temp.name) + subprocess.run( + ["git", "init", "--quiet", str(self.repo)], check=True, + capture_output=True, + ) + self.write(".gitignore", ".planning/\n.claude/\n__pycache__/\n", tracked=True) + + def write(self, name, content, tracked=False): + path = self.repo / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + if tracked: + subprocess.run( + ["git", "add", "--", name], cwd=self.repo, check=True, + capture_output=True, + ) + + def run_validation(self, kind): + return subprocess.run( + [sys.executable, str(VALIDATOR), kind], cwd=self.repo, + capture_output=True, text=True, timeout=60, + ) + + def assert_valid(self, kind): + result = self.run_validation(kind) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + + def assert_invalid(self, kind, diagnostic): + result = self.run_validation(kind) + self.assertNotEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn(diagnostic, result.stdout + result.stderr) + + def test_ignored_and_untracked_files_are_not_validation_inputs(self): + for directory in (".planning", ".claude/worktrees/other", "scratch"): + for suffix, content in ( + ("py", "def broken(:\n"), ("json", "{broken"), + ("yml", "broken: ["), ("yaml", "broken: ["), + ("md", "no heading\n"), + ): + self.write(f"{directory}/bad.{suffix}", content) + self.write("scripts/space [x]\nname.py", "answer = 42\n", tracked=True) + self.write(".github/data [x].json", '{"ok": true}\n', tracked=True) + self.write(".github/data.yml", "ok: true\n", tracked=True) + self.write("docs/a [x].md", "# Valid\n", tracked=True) + for kind in ("python", "data", "markdown"): + with self.subTest(kind=kind): + self.assert_valid(kind) + + def test_invalid_tracked_python_fails(self): + self.write(".github/bad [x].py", "def broken(:\n", tracked=True) + self.assert_invalid("python", "SyntaxError") + + def test_invalid_tracked_json_fails(self): + self.write(".github/bad [x].json", "{broken", tracked=True) + self.assert_invalid("data", "JSONDecodeError") + + def test_invalid_tracked_yaml_fails(self): + for suffix in ("yml", "yaml"): + with self.subTest(suffix=suffix): + name = f".github/bad.{suffix}" + self.write(name, "broken: [", tracked=True) + self.assert_invalid("data", "Psych::SyntaxError") + self.write(name, "ok: true\n", tracked=True) + + def test_invalid_tracked_markdown_paths_are_literal(self): + for name in ("docs/bad [x].md", ".github/hidden.md", "#note.md"): + with self.subTest(name=name): + self.write(name, "no heading\n", tracked=True) + self.assert_invalid("markdown", "MD041") + self.write(name, "# Valid\n", tracked=True) + + def test_profile_keeps_its_separate_rules(self): + self.write("profile/README.md", "
Profile
\n", tracked=True) + config = ".github/markdownlint-profile.yaml" + self.write(config, (ROOT / config).read_text(), tracked=True) + self.assert_valid("markdown") + self.write("profile/README.md", "
Profile
\n", tracked=True) + self.assert_invalid("markdown", "MD009") + + def test_empty_tracked_input_sets_succeed(self): + for kind in ("python", "data", "markdown"): + with self.subTest(kind=kind): + self.assert_valid(kind) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/standards-validation.yml b/.github/workflows/standards-validation.yml index 703d696..112817f 100644 --- a/.github/workflows/standards-validation.yml +++ b/.github/workflows/standards-validation.yml @@ -41,15 +41,15 @@ jobs: run: | python3 .github/tests/community_health_contract_test.py python3 .github/tests/greptile_summon_contract_test.py + python3 .github/tests/tracked_validation_test.py - name: Compile Python - run: python3 -m compileall -q . + run: python3 scripts/validate_tracked_files.py python - name: Parse YAML and JSON run: | set -euo pipefail - ruby -e 'require "yaml"; Dir.glob("**/*.{yml,yaml}", File::FNM_DOTMATCH).sort.each { |path| YAML.parse_file(path) }' - python3 -c 'import json; from pathlib import Path; [json.load(path.open()) for path in Path(".").rglob("*.json")]' + python3 scripts/validate_tracked_files.py data python3 .github/tests/greptile_config_contract_test.py python3 .github/tests/quality_report_contract_test.py python3 .github/tests/reusable_ci_contract_test.py @@ -60,8 +60,7 @@ jobs: - name: Lint Markdown run: | set -euo pipefail - npx --yes markdownlint-cli2@0.23.2 "**/*.md" "#profile/README.md" - npx --yes markdownlint-cli2@0.23.2 "profile/README.md" --config .github/markdownlint-profile.yaml + python3 scripts/validate_tracked_files.py markdown - name: Validate workflows with actionlint run: | diff --git a/AGENTS.md b/AGENTS.md index 0d27337..956f396 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,9 +30,16 @@ contract tests that pin the shapes other repositories depend on. Verify promotions with tree equality (`git diff --quiet origin/main origin/dev/repository-standards`), never commit ancestry. -- CodeRabbit auto-reviews PRs against `dev/*` branches only (scoped in - `.coderabbit.yaml`); summon it explicitly anywhere else and read its - inline comments before merging. +- Admit new hosted review/heavy CI work through the shared capacity roster. + Keep preparation local while queued. After admission, add `review-ready` for + the first CodeRabbit review; request subsequent fix-batch reviews explicitly. + Freeze pushes while review runs, read its output and verify the covered head. + `.coderabbit.yaml` keeps default/dev targets but disables automatic review on + ordinary pushes. Required review and protection remain effective. +- Approve ordinary dependency updates in the Renovate dashboard only after + admission. This repo's wrapper limits ordinary PRs/branches to one at a time + and one new PR per hour. The shared preset is unchanged; urgent vulnerability + updates keep their separate handling. - Commits are plain Conventional Commits, `(scope): `, no emoji, no AI attribution trailers. - Never weaken branch protection or rulesets to land a change. diff --git a/REPOSITORY_ONBOARDING.md b/REPOSITORY_ONBOARDING.md index 84db9ce..06068c5 100644 --- a/REPOSITORY_ONBOARDING.md +++ b/REPOSITORY_ONBOARDING.md @@ -107,27 +107,67 @@ repositories. Language-specific tests do not. - [ ] Confirm the Renovate GitHub app can access the repository. App access is managed at the organization level; do not widen a public-only installation to private repositories as an onboarding shortcut. -- [ ] Add a root `.coderabbit.yaml` based on a current CodesWhat product, then - replace its product-specific tone, ignored paths, and path instructions. - Keep automatic review enabled. If PRs target a non-default integration - branch, include that branch under `reviews.auto_review.base_branches` and - request an explicit CodeRabbit review on the first PR to prove it works. +- [ ] Where CodeRabbit is adopted, use a root `.coderabbit.yaml` appropriate to + the repository. Adopt intentional admission rather than automatically spending + review capacity on every in-progress push. Scope opt-in and incremental-review + changes through normal review, preserving required review evidence. Verify + non-default integration targets and stacked PR behavior before relying on them. - [ ] Confirm CodeRabbit can access the repository and posts an actual review comment. A green check without review output is not enough for this test. - Automated Pro-level reviews are free on public repositories only; on - private repositories the free plan rate-limits automated reviews, and they - have not fired for this organization in practice. Organization policy: on a - private repository skip both CodeRabbit items, use cross-account human - review, and add the config when the repository goes public. + Inspect the effective plan, selected-file limit and remaining capacity; public + repository visibility is not a promise of unlimited review. A quota-only green + check is not delivered review. Required non-author approval still applies, and + multiple accounts operated by one person are not independent human review. - [ ] Keep `greptile.json` at exactly `{"skipReview": "AUTOMATIC"}` (contract-tested in this repository) so Greptile never reviews unbidden. A repository that wants opt-in second opinions adds a label-gated caller workflow (`.github/workflows/greptile.yml`, firing on the `second-opinion` label) that calls this repository's `greptile-summon.yml` at a pinned full commit SHA. Pair the caller with a CodeRabbit `labeling_instructions` entry - for `second-opinion` and `auto_apply_labels` enabled, so applying the label - is criteria-driven rather than left to memory. The label is Greptile's only - trigger; never wire it to review every PR directly. + for `second-opinion` without automatic label application. Applying it is an + explicit, consequence-based admission decision with available capacity. + The label is Greptile's only automatic caller trigger; do not wire it to + review every PR directly. Inspect the provider's resolved configuration and + delivered review rather than assuming a legacy file or summon proves coverage. + +### Capacity and queued development + +Public projects use queue-first development, with adoption work recorded in +their private internal roadmaps. Begin with one newly admitted hosted-review or +heavy-CI candidate across public projects for a seven-day measured pilot. Finish +existing in-flight work safely. A named coordinator records the candidate's +owner, head/base, dependencies, local verification, services, review-pass count, +blocked reason and next eligible time before new expensive work starts. + +Keep one active change per repo and at most one small dependent successor local. +Pilot two-layer stacks for genuine dependencies; verify trunk, bot and merge +compatibility. Draft and stacked PRs can still run CI and deploys. Keep diffs +small, aiming around 25 changed files and within the actual reviewer limit; +batching many unrelated items into one oversized PR is not admission control. + +Freeze pushes during review, then submit one batch of fixes per pass. Prioritize +finishing admitted work over opening another PR. Keep the three-pass cap, all +required checks and valid approvals. Record cooldowns and wait for recovery +without repeated review summons, workflow reruns or indefinite polling. Review +capacity after the pilot using actual service pools before expanding concurrency. + +Admit one new deployment-producing web candidate per shared deployment pool +initially. Pilot ordinary dependency arrivals at one concurrent PR and one new +PR/hour per repo, with staggered schedules or dashboard admission and urgent +vulnerability handling retained. Retain the monthly Qlty reporting policy below. +Provider settings, bot controls and required CI changes need scoped reviewed +rollouts; documentation alone does not configure them. Metadata-only projects +record applicable controls without installing irrelevant tools or paid services. + +This shared repository pilots the controls in its own configuration, without +changing the live Renovate preset for consumers. The coordinator approves an +ordinary dependency update in the dashboard only after admission; the local +wrapper permits one ordinary PR/branch and one new PR per hour. Urgent +vulnerability handling remains separate. After admitting a stable review +candidate, apply `review-ready` for its first CodeRabbit review. Later fix +batches use an explicit review request; pushes do not automatically start +another review. Verify the provider's resolved configuration and delivered head +after landing these settings before claiming the pilot is operational. ### Local gates @@ -294,41 +334,55 @@ Add these only when the behavior exists: ### Qlty -Qlty appears in three places. The committed configuration and the repository-run -gates are the alignment surface; the Qlty Cloud GitHub App is not. - -- [ ] Commit `.qlty/qlty.toml` (`config_version = "0"`) with the organization - exclude baseline for generated, minified, and vendored paths. Drydock's file - is the reference posture and Portwing's is its Go-repo mirror; start from the - closer of the two. Do not copy the trivy plugin block from either reference: - trivy is deprecated in favor of Grype and its removal from both files is - tracked (drydock#753, portwing#135). The trufflehog plugin stays. The same - file drives the local CLI and Qlty Cloud, so changes to it are quality-gate - changes, not formatting. -- [ ] Add the repository's Qlty gate script (`scripts/qlty-check-gate.sh all`) - to `pre-push`, fail-fast like every other local gate. An advisory smells - gate may run alongside it (Drydock pattern) but must not mask the gating - script's failure. -- [ ] Gate Qlty in CI with the repository-run check, which needs no Qlty Cloud - account. Go repositories call `go-ci.yml` with `run-qlty` enabled, - `qlty-egress-policy: block`, and the proven endpoint allowlist — copy the - allowlist from Portwing's `ci-verify.yml` rather than re-deriving it, and do - not widen it. Node repositories run the SHA-pinned - `qltysh/qlty-action/install` plus the same gate script inside their own - workflow (Drydock pattern). - -The Qlty Cloud GitHub App's statuses (`qlty check`, `qlty coverage`, -`qlty coverage diff`) currently error organization-wide with out-of-minutes -billing failures. Treat them as non-gating: never add them to required status -checks, and do not chase these failures on PRs — the repository-run gate above -is the enforced check. Coverage reporting is Codecov's job (decided -2026-08-16): upload coverage to Codecov and carry its badge in the README. -The Qlty Cloud App and its maintainability badge stay installed, but its -coverage upload is not the coverage system and its checks stay non-required. -Qlty Cloud enrollment and usage data are not an organization-wide -prerequisite; do not make a Cloud upload or check required -until the repository is enrolled and the check has proved stable. Local Qlty -checks may still be stricter than this baseline. +Public projects run routine quality checks in their own CI and useful local +hooks. Keep Qlty CLI where it aggregates useful checks; effective native tools +are also valid. Qlty Cloud supplies periodic reporting for existing dashboards +and README metrics. A Cloud subscription is not needed for standalone CLI checks. + +- [ ] Record adoption work or a concrete applicability decision in the project's + private internal roadmap, with an owner, next action and checkable completion + conditions. Metadata-only repositories do not need a new scanner or dashboard + without a useful signal. +- [ ] Where Qlty CLI is used, commit `.qlty/qlty.toml` with exclusions for + generated, minified and vendored paths. Review changes to shared CLI/Cloud + configuration as quality-gate changes. Preserve useful existing checks rather + than duplicating them through another tool. +- [ ] Run the repository's quality gate in CI and useful local hooks. Advisory + duplication/complexity reports must not mask a gate failure. The shared + `go-ci.yml` supports `run-qlty`, which invokes `scripts/ci/go-qlty.sh` in the + caller repository. Keep the existing input default and check name; consumers + adopt changes through deliberate full-SHA pin updates. Node repositories can + use the SHA-pinned `qltysh/qlty-action/install` and their own gate script. +- [ ] For existing Qlty Cloud maintainability reporting, set default-branch + Analysis Frequency to `1 month` in Project Settings > General. A manual scan + for a release or important change is acceptable within the usage budget. The + README maintainability grade represents the last completed scan and may be up + to a month behind the current default branch. +- [ ] Avoid redundant hosted PR analysis after equivalent repository-owned CI + gates are verified. Verify the control stops analysis compute; turning off + comments or commit statuses alone does not establish that scans stop. Do not + weaken branch protection. Any replacement for a required gate goes through a + reviewed repository change and must be verified on the intended commit first. +- [ ] Preserve useful coverage reporting, enforcement and badges, including + Codecov where already adopted. Each Qlty coverage upload costs one standard + analysis minute, including each separately uploaded server-merge part. Count + all workspace scans, uploads, runner multipliers and retries before claiming + the allowance is sufficient. Do not silently reduce PR coverage enforcement + to save reporting minutes. +- [ ] Verify provider-side commit identity, actual analysis/coverage content and + badge freshness after changing settings. Classify quota failures separately + from real analysis/upload failures; a successful upload step or status alone + does not establish delivery or a completed scan. + +This operating policy retains useful Qlty CLI and periodic Cloud reporting. It +does not introduce Sonar, require a paid upgrade, or automatically enroll every +repository in a hosted service. The reusable workflows keep their existing +interfaces and opt-in defaults; a caller's workflow does not configure Qlty +Cloud's service-side scheduling. + +See Qlty's [analysis frequency](https://docs.qlty.sh/cloud/analysis-frequency), +[analysis-minute accounting](https://docs.qlty.sh/cloud/billing/analysis-minutes), +and [CLI versus Cloud](https://docs.qlty.sh/cli-vs-cloud) documentation. ## 5. Add delivery controls only for shipped artifacts diff --git a/renovate.json b/renovate.json index 6bba2f8..3e6d303 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,8 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["local>CodesWhat/.github:renovate-config"] + "extends": ["local>CodesWhat/.github:renovate-config"], + "dependencyDashboardApproval": true, + "prConcurrentLimit": 1, + "prHourlyLimit": 1, + "branchConcurrentLimit": 1 } diff --git a/scripts/generate_profile_svg.py b/scripts/generate_profile_svg.py index 436b7c6..06b4025 100644 --- a/scripts/generate_profile_svg.py +++ b/scripts/generate_profile_svg.py @@ -4,7 +4,6 @@ Creates dark_mode.svg and light_mode.svg. """ -import re from pathlib import Path @@ -92,7 +91,7 @@ def generate_svg(mode="dark"): ] # Wordmark auto-scales to fit the canvas, capped at the logo font size - wordmark_max_len = max(len(l.rstrip()) for l in wordmark_art) + wordmark_max_len = max(len(line.rstrip()) for line in wordmark_art) wordmark_font_size = round(min(font_size, (width - 40) / (0.6 * wordmark_max_len)), 1) wordmark_char_width = wordmark_font_size * 0.6 wordmark_line_height = round(wordmark_font_size * 1.2) @@ -145,7 +144,7 @@ def generate_svg(mode="dark"): # Art lines have ragged lengths; center the block as a whole off the # widest line so every line shares one left edge - art_max_len = max(len(l) for l in logo_art) + art_max_len = max(len(line) for line in logo_art) art_start_x = (width - art_max_len * char_width) / 2 solid_chars = set('█▓▌▐▄▀') diff --git a/scripts/validate.sh b/scripts/validate.sh index e6d9504..8f5f86f 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -17,17 +17,18 @@ python3 .github/tests/greptile_config_contract_test.py python3 .github/tests/quality_report_contract_test.py python3 .github/tests/reusable_ci_contract_test.py python3 .github/tests/renovate_config_contract_test.py +python3 .github/tests/starchart_refresh_contract_test.py +python3 .github/tests/main_is_released_contract_test.py +python3 .github/tests/tracked_validation_test.py echo "==> compile python" -python3 -m compileall -q . +python3 scripts/validate_tracked_files.py python echo "==> parse yaml and json" -ruby -e 'require "yaml"; Dir.glob("**/*.{yml,yaml}", File::FNM_DOTMATCH).sort.each { |path| YAML.parse_file(path) }' -python3 -c 'import json; from pathlib import Path; [json.load(path.open()) for path in Path(".").rglob("*.json")]' +python3 scripts/validate_tracked_files.py data echo "==> markdownlint" -npx --yes markdownlint-cli2@0.23.2 "**/*.md" "#profile/README.md" -npx --yes markdownlint-cli2@0.23.2 "profile/README.md" --config .github/markdownlint-profile.yaml +python3 scripts/validate_tracked_files.py markdown echo "==> actionlint" if command -v actionlint >/dev/null 2>&1; then diff --git a/scripts/validate_tracked_files.py b/scripts/validate_tracked_files.py new file mode 100644 index 0000000..347903d --- /dev/null +++ b/scripts/validate_tracked_files.py @@ -0,0 +1,59 @@ +import argparse +import json +import os +from pathlib import Path +import py_compile +import subprocess +import sys + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("kind", choices=("python", "data", "markdown")) + kind = parser.parse_args().kind + root = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], text=True, + ).strip() + os.chdir(root) + # NUL delimiters preserve filenames containing spaces or newlines. + paths = [ + Path(os.fsdecode(path)) + for path in subprocess.check_output(["git", "ls-files", "-z"]).split(b"\0") + if path + ] + + if kind == "python": + for path in paths: + if path.suffix == ".py": + py_compile.compile(str(path), doraise=True) + elif kind == "data": + for path in paths: + if path.suffix == ".json": + with path.open() as source: + json.load(source) + yaml_paths = [str(path) for path in paths if path.suffix in (".yml", ".yaml")] + if yaml_paths: + subprocess.run( + ["ruby", "-e", 'require "yaml"; ARGV.each { |path| YAML.parse_file(path) }', + "--", *yaml_paths], check=True, + ) + else: + profile = Path("profile/README.md") + markdown = [path for path in paths if path.suffix == ".md" and path != profile] + command = ["npx", "--yes", "markdownlint-cli2@0.23.2", "--no-globs"] + if markdown: + subprocess.run( + [*command, *[":./" + str(path) for path in markdown]], check=True, + ) + if profile in paths: + subprocess.run( + [*command, ":./" + str(profile), + "--config", ".github/markdownlint-profile.yaml"], check=True, + ) + + +if __name__ == "__main__": + try: + main() + except subprocess.CalledProcessError as error: + sys.exit(error.returncode)