diff --git a/docs/WORKFLOW_GUIDE.md b/docs/WORKFLOW_GUIDE.md index 678842298..28dd6a8b2 100644 --- a/docs/WORKFLOW_GUIDE.md +++ b/docs/WORKFLOW_GUIDE.md @@ -46,6 +46,7 @@ _Inline Gate helper_ - **Gate summary job (`pr-00-gate.yml`)** — Post-CI job that downloads artifacts, computes coverage deltas, runs the label-gated autofix routine, and updates the PR summary comment with a stable marker. ### Maintenance & Repo Health +- **Fleet Renovate intake** — `renovate-presets/fleet.json` bounds routine dependency work to Monday 01:00–05:00 America/Chicago, with two commits per hour and three concurrent branches/PRs. Trusted GitHub Actions digest/pin/minor/patch updates share the green-automerge lane; routine releases wait three days and for non-pending update-branch checks; major updates require explicit Dependency Dashboard approval. Vulnerability alerts bypass those routine delays, and grouped lock-file maintenance uses the same explicit weekly window. - **`maint-39-test-llm-providers.yml`** — Manual dispatch harness to smoke-test GitHub Models and OpenAI credentials via `tools.llm_provider` before other maintenance runs; the workflow now avoids minting redundant GitHub App tokens so the test stays lightweight. - **`maint-45-cosmetic-repair.yml`** — Manual dispatch utility that runs `pytest -q`, applies guard-gated cosmetic fixes via `scripts/ci_cosmetic_repair.py`, and (when not in dry-run mode) opens a labelled PR with the default workflow token—no extra GitHub App mint required. - **`maint-46-post-ci.yml`** — Post-CI recovery watcher triggered by `workflow_run` on Gate completion. It inspects the Gate summary job before touching the repo, and only checks out helpers / installs the token-balanced API client when the summary leg actually failed, keeping the default token pool free unless recovery is required. diff --git a/docs/ci/TOOL_VERSION_MANAGEMENT.md b/docs/ci/TOOL_VERSION_MANAGEMENT.md index 927d832d7..6d90882d7 100644 --- a/docs/ci/TOOL_VERSION_MANAGEMENT.md +++ b/docs/ci/TOOL_VERSION_MANAGEMENT.md @@ -8,6 +8,23 @@ All CI and autofix workflows use tool versions defined in a single source of tru - CI autofix loop - Local development +## Renovate Intake Validation + +Renovate owns routine dependency updates, while `autofix-versions.env` remains the +source for the dev-tool pins it explicitly excludes. The fleet preset uses a bounded +weekly intake window, conservative branch/PR budgets, release-age and update-branch-check +gates, a grouped trusted GitHub Actions lane, and Dependency Dashboard approval for +majors. Vulnerability alerts bypass the routine window and age gate. + +Run the same validation used by `scripts/dev_check.sh` after editing either Renovate +entrypoint or the shared preset: + +```bash +npx --yes --package renovate@43.285.3 -- renovate-config-validator --no-global \ + renovate.json renovate-presets/fleet.json \ + templates/consumer-repo/.github/renovate.json +``` + ## Version File **Location**: `.github/workflows/autofix-versions.env` diff --git a/docs/ops/CONSUMER_REPO_MAINTENANCE.md b/docs/ops/CONSUMER_REPO_MAINTENANCE.md index 043f6ef2f..868e1203c 100644 --- a/docs/ops/CONSUMER_REPO_MAINTENANCE.md +++ b/docs/ops/CONSUMER_REPO_MAINTENANCE.md @@ -60,6 +60,16 @@ Maint 68 implements these exceptions through each entry's typed manifest `skip_repos` rules. There is no separate hard-coded custom-Gate list in the sync script. +### Fleet Renovate intake policy + +All registered consumers extend `renovate-presets/fleet.json`. Routine dependency +work is limited to Monday 01:00–05:00 America/Chicago, two commits per hour, and +three concurrent branches/PRs. Routine releases wait three days and for non-pending +update-branch checks; vulnerability alerts bypass each routine delay. Trusted GitHub Actions +digest, pin, minor, and patch updates are grouped for green automerge, while majors +stay visible in the Dependency Dashboard until explicitly approved. Lock-file +maintenance is grouped into the same weekly maintenance window. + --- ## Bug Triage Process diff --git a/renovate-presets/README.md b/renovate-presets/README.md new file mode 100644 index 000000000..d4b779e07 --- /dev/null +++ b/renovate-presets/README.md @@ -0,0 +1,24 @@ +# Fleet Renovate preset + +`fleet.json` is the single source of truth for Renovate intake across Workflows and +the registered consumer repositories. It opens routine dependency work only in the +Monday 01:00–05:00 America/Chicago maintenance window, permits at most three routine +branches and PRs, and limits commits to two per hour. + +Routine releases must be at least three days old and wait until their update-branch +checks are not pending. Vulnerability alerts bypass the window, release-age delay, +and update-branch check gate so security remediation is never held by the routine +budget. + +Trusted GitHub Actions digest, pin, minor, and patch updates share one green-automerge +lane. Major updates remain visible in the Dependency Dashboard and create a PR only +after an explicit dashboard approval. Lock-file maintenance is a separate, grouped +weekly lane in the same window. + +Validate the repository config before review: + +```bash +npx --yes --package renovate@43.285.3 -- renovate-config-validator --no-global \ + renovate.json renovate-presets/fleet.json \ + templates/consumer-repo/.github/renovate.json +``` diff --git a/renovate-presets/fleet.json b/renovate-presets/fleet.json index 5499cffbe..a43e3eb58 100644 --- a/renovate-presets/fleet.json +++ b/renovate-presets/fleet.json @@ -4,6 +4,24 @@ "extends": ["config:recommended"], "platformAutomerge": true, "labels": ["dependencies"], + "timezone": "America/Chicago", + "schedule": ["after 1am and before 5am on monday"], + "commitHourlyLimit": 2, + "prConcurrentLimit": 3, + "branchConcurrentLimit": 3, + "minimumReleaseAge": "3 days", + "prCreation": "not-pending", + "dependencyDashboard": true, + "vulnerabilityAlerts": { + "schedule": [], + "minimumReleaseAge": null, + "prCreation": "immediate" + }, + "lockFileMaintenance": { + "enabled": true, + "groupName": "weekly lock file maintenance", + "schedule": ["after 1am and before 5am on monday"] + }, "pip-compile": { "managerFilePatterns": ["/(^|/)requirements\\.lock$/"] }, @@ -27,12 +45,18 @@ "automergeType": "pr" }, { - "description": "GitHub Actions minor+patch: group + automerge on green", + "description": "Trusted GitHub Actions digest, pin, minor, and patch updates: group + automerge on green", "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor", "patch"], + "matchUpdateTypes": ["digest", "pin", "minor", "patch"], "groupName": "github-actions", "automerge": true }, + { + "description": "Major updates remain visible in the Dependency Dashboard until a maintainer approves the specific upgrade", + "matchUpdateTypes": ["major"], + "dependencyDashboardApproval": true, + "automerge": false + }, { "description": "Vendored minimatch cascade — keep minimatch, brace-expansion, balanced-match in one PR", "matchFileNames": [".github/scripts/**"], diff --git a/scripts/dev_check.sh b/scripts/dev_check.sh index 6dda2c4ed..e95f0c4ba 100755 --- a/scripts/dev_check.sh +++ b/scripts/dev_check.sh @@ -164,11 +164,25 @@ if [[ "$CHANGED_ONLY" == true ]]; then UNSTAGED_FILES=$(git diff --name-only 2>/dev/null | grep -E '\.(py)$' 2>/dev/null | grep -v -E '^(archive/|\.extraction/)' 2>/dev/null || echo "") ALL_FILES=$(echo -e "$PYTHON_FILES\n$UNSTAGED_FILES" | sort -u | grep -v '^$' 2>/dev/null || echo "") - if [[ -z "$ALL_FILES" ]]; then + # Renovate settings are JSON, so they are deliberately absent from the + # Python formatter/linter target list. They must still keep this command + # running long enough to reach the dedicated validator below. + RENOVATE_CONFIG_CHANGED=$( { + git diff --name-only HEAD~1 2>/dev/null + git diff --name-only 2>/dev/null + } | grep -E '^(renovate\.json|renovate-presets/.*\.json|templates/consumer-repo/\.github/renovate\.json)$' | sort -u || echo "") + + if [[ -z "$ALL_FILES" && -z "$RENOVATE_CONFIG_CHANGED" ]]; then echo -e "${GREEN}No Python files changed (excluding old folders) - nothing to check${NC}" exit 0 fi + if [[ -n "$RENOVATE_CONFIG_CHANGED" && -z "$ALL_FILES" ]]; then + echo -e "${BLUE}Renovate configuration changed; continuing to its dedicated validator:${NC}" + echo "$RENOVATE_CONFIG_CHANGED" | sed 's/^/ /' + echo "" + fi + echo -e "${BLUE}Checking only changed files (excluding old folders):${NC}" echo "$ALL_FILES" | sed 's/^/ /' echo "" @@ -570,6 +584,13 @@ else echo -e "${YELLOW}⚠ Node.js not available; skipping keepalive harness tests${NC}" fi +echo -e "${BLUE}7. Renovate configuration...${NC}" +if command -v npx >/dev/null 2>&1; then + quick_check "Renovate configuration" "npx --yes --package renovate@43.285.3 -- renovate-config-validator --no-global renovate.json renovate-presets/fleet.json templates/consumer-repo/.github/renovate.json" "" +else + echo -e "${YELLOW}⚠ npx not available; skipping Renovate configuration validation${NC}" +fi + echo "" echo -e "${CYAN}=== Quick Check Complete ===${NC}" echo -e "${BLUE}For comprehensive validation, run: ./scripts/check_branch.sh${NC}" diff --git a/tests/workflows/test_renovate_fleet_policy.py b/tests/workflows/test_renovate_fleet_policy.py new file mode 100644 index 000000000..8e108f2d2 --- /dev/null +++ b/tests/workflows/test_renovate_fleet_policy.py @@ -0,0 +1,99 @@ +import json +from pathlib import Path + +PRESET_PATH = Path(__file__).resolve().parents[2] / "renovate-presets" / "fleet.json" +REPO_ROOT = PRESET_PATH.parents[1] + + +def _preset() -> dict: + return json.loads(PRESET_PATH.read_text(encoding="utf-8")) + + +def _rule(preset: dict, **expected: object) -> dict: + matches = [ + rule + for rule in preset["packageRules"] + if all(rule.get(key) == value for key, value in expected.items()) + ] + assert len(matches) == 1 + return matches[0] + + +def test_fleet_renovate_intake_budget_is_bounded_to_the_weekly_window() -> None: + preset = _preset() + + assert preset["timezone"] == "America/Chicago" + assert preset["schedule"] == ["after 1am and before 5am on monday"] + assert preset["commitHourlyLimit"] == 2 + assert preset["prConcurrentLimit"] == 3 + assert preset["branchConcurrentLimit"] == 3 + assert preset["prCreation"] == "not-pending" + assert preset["minimumReleaseAge"] == "3 days" + + +def test_trusted_action_digests_are_grouped() -> None: + rule = _rule(_preset(), matchManagers=["github-actions"]) + + assert rule["matchUpdateTypes"] == ["digest", "pin", "minor", "patch"] + assert rule["groupName"] == "github-actions" + assert rule["automerge"] is True + + +def test_fleet_preset_keeps_workflows_owned_dev_tool_pins_out_of_renovate() -> None: + rule = _rule(_preset(), enabled=False) + + assert rule["matchPackageNames"] == [ + "ruff", + "black", + "mypy", + "pytest", + "pytest-cov", + "pytest-xdist", + "coverage", + "isort", + "docformatter", + ] + + +def test_vulnerability_alerts_bypass_routine_intake_delays() -> None: + alerts = _preset()["vulnerabilityAlerts"] + + assert alerts == { + "schedule": [], + "minimumReleaseAge": None, + "prCreation": "immediate", + } + + +def test_major_updates_stay_visible_but_require_dashboard_approval() -> None: + preset = _preset() + rule = _rule(preset, matchUpdateTypes=["major"]) + + assert preset["dependencyDashboard"] is True + assert rule["dependencyDashboardApproval"] is True + assert rule["automerge"] is False + + +def test_lock_file_maintenance_has_the_same_explicit_weekly_cadence() -> None: + lock_maintenance = _preset()["lockFileMaintenance"] + + assert lock_maintenance == { + "enabled": True, + "groupName": "weekly lock file maintenance", + "schedule": ["after 1am and before 5am on monday"], + } + + +def test_workflows_and_consumer_entrypoints_share_the_bounded_fleet_policy() -> None: + expected_preset = "github>stranske/Workflows//renovate-presets/fleet" + entrypoints = ( + REPO_ROOT / "renovate.json", + REPO_ROOT / "templates" / "consumer-repo" / ".github" / "renovate.json", + ) + + for entrypoint in entrypoints: + config = json.loads(entrypoint.read_text(encoding="utf-8")) + assert config["extends"] == [expected_preset] + + preset = _preset() + assert preset["prConcurrentLimit"] == preset["branchConcurrentLimit"] == 3