diff --git a/.rhiza/rhiza.mk b/.rhiza/rhiza.mk index 25282eaf..3e25ac1e 100644 --- a/.rhiza/rhiza.mk +++ b/.rhiza/rhiza.mk @@ -50,6 +50,11 @@ export PYTHON_VERSION RHIZA_VERSION ?= $(shell cat .rhiza/.rhiza-version 2>/dev/null || echo "0.10.2") export RHIZA_VERSION +# Default sync schedule (cron expression for GitHub Actions sync workflow) +# Override in your root Makefile to customise when sync runs. +# Example: RHIZA_SYNC_SCHEDULE = 0 9 * * 1-5 (weekdays at 9 AM UTC) +RHIZA_SYNC_SCHEDULE ?= 0 0 * * 1 + export UV_NO_MODIFY_PATH := 1 export UV_VENV_CLEAR := 1 @@ -76,7 +81,7 @@ endef export RHIZA_LOGO # Declare phony targets for Rhiza Core -.PHONY: print-logo sync sync-experimental materialize validate readme pre-sync post-sync pre-validate post-validate +.PHONY: print-logo sync sync-experimental materialize validate readme pre-sync post-sync pre-validate post-validate _apply-sync-schedule # Hook targets (double-colon rules allow multiple definitions) # Note: pre-install/post-install are defined in bootstrap.mk @@ -98,9 +103,16 @@ sync: pre-sync ## sync with template repository as defined in .rhiza/template.ym else \ $(MAKE) install-uv; \ ${UVX_BIN} "rhiza==$(RHIZA_VERSION)" sync .; \ + $(MAKE) _apply-sync-schedule; \ fi @$(MAKE) post-sync +_apply-sync-schedule: ## (internal) apply RHIZA_SYNC_SCHEDULE override to GitHub Actions sync workflow + @if [ "$(RHIZA_SYNC_SCHEDULE)" != "0 0 * * 1" ] && [ -f .github/workflows/rhiza_sync.yml ]; then \ + sed -i.bak "s|cron: '[^']*'|cron: '$(RHIZA_SYNC_SCHEDULE)'|" .github/workflows/rhiza_sync.yml && rm -f .github/workflows/rhiza_sync.yml.bak; \ + printf "${BLUE}[INFO] Applied custom sync schedule: $(RHIZA_SYNC_SCHEDULE)${RESET}\n"; \ + fi + materialize: ## [DEPRECATED] use 'make sync' instead — materialize --force is now sync @printf "${YELLOW}[WARN] 'make materialize' is deprecated and will be removed in a future release.${RESET}\n" @printf "${YELLOW}[WARN] Please use 'make sync' instead (e.g. 'materialize --force' is now 'make sync').${RESET}\n" diff --git a/.rhiza/tests/sync/test_sync_schedule.py b/.rhiza/tests/sync/test_sync_schedule.py new file mode 100644 index 00000000..0ba67f31 --- /dev/null +++ b/.rhiza/tests/sync/test_sync_schedule.py @@ -0,0 +1,128 @@ +"""Tests for the RHIZA_SYNC_SCHEDULE override mechanism. + +These tests validate that users can override the default sync schedule +(cron expression) used in the GitHub Actions sync workflow, and that +the override is applied correctly during `make sync`. + +Security Notes: +- S101 (assert usage): Asserts are used in pytest tests to validate conditions +- S603/S607 (subprocess usage): Any subprocess calls are for testing sync targets + in isolated environments with controlled inputs +- Test code operates in a controlled environment with trusted inputs +""" + +from __future__ import annotations + +from pathlib import Path + +from sync.conftest import run_make, strip_ansi + + +class TestSyncScheduleVariable: + """Tests for the RHIZA_SYNC_SCHEDULE Makefile variable.""" + + def test_default_sync_schedule_value(self, logger): + """RHIZA_SYNC_SCHEDULE should default to '0 0 * * 1' (weekly Monday).""" + proc = run_make(logger, ["print-RHIZA_SYNC_SCHEDULE"], dry_run=False) + out = strip_ansi(proc.stdout) + assert "Value of RHIZA_SYNC_SCHEDULE:" in out + assert "0 0 * * 1" in out + + def test_sync_schedule_overridable_via_env(self, logger, tmp_path: Path): + """RHIZA_SYNC_SCHEDULE should be overridable via environment variable.""" + import os + + env = os.environ.copy() + env["RHIZA_SYNC_SCHEDULE"] = "0 9 * * 1-5" + + proc = run_make(logger, ["print-RHIZA_SYNC_SCHEDULE"], dry_run=False, env=env) + out = strip_ansi(proc.stdout) + assert "0 9 * * 1-5" in out + + def test_sync_schedule_overridable_via_makefile(self, logger, tmp_path: Path): + """RHIZA_SYNC_SCHEDULE should be overridable in root Makefile.""" + makefile = tmp_path / "Makefile" + original = makefile.read_text() + new_content = "RHIZA_SYNC_SCHEDULE = 0 6 * * *\n\n" + original + makefile.write_text(new_content) + + proc = run_make(logger, ["print-RHIZA_SYNC_SCHEDULE"], dry_run=False) + out = strip_ansi(proc.stdout) + assert "0 6 * * *" in out + + +class TestApplySyncSchedule: + """Tests for the _apply-sync-schedule target.""" + + def test_apply_sync_schedule_skips_when_default(self, logger, tmp_path: Path): + """_apply-sync-schedule should not modify files when using default schedule.""" + # Create a mock workflow file matching the actual rhiza_sync.yml format + workflow_dir = tmp_path / ".github" / "workflows" + workflow_dir.mkdir(parents=True) + workflow_file = workflow_dir / "rhiza_sync.yml" + original_content = "on:\n schedule:\n - cron: '0 0 * * 1' # Weekly on Monday\n" + workflow_file.write_text(original_content) + + proc = run_make(logger, ["_apply-sync-schedule"], dry_run=False) + assert proc.returncode == 0 + + # File should remain unchanged + assert workflow_file.read_text() == original_content + + def test_apply_sync_schedule_patches_workflow(self, logger, tmp_path: Path): + """_apply-sync-schedule should patch workflow when schedule is overridden.""" + # Create a mock workflow file matching the actual rhiza_sync.yml format + workflow_dir = tmp_path / ".github" / "workflows" + workflow_dir.mkdir(parents=True) + workflow_file = workflow_dir / "rhiza_sync.yml" + workflow_file.write_text("on:\n schedule:\n - cron: '0 0 * * 1' # Weekly on Monday\n") + + # Override the schedule via Makefile + makefile = tmp_path / "Makefile" + original = makefile.read_text() + new_content = "RHIZA_SYNC_SCHEDULE = 0 9 * * 1-5\n\n" + original + makefile.write_text(new_content) + + proc = run_make(logger, ["_apply-sync-schedule"], dry_run=False) + assert proc.returncode == 0 + + # File should be patched + patched = workflow_file.read_text() + assert "0 9 * * 1-5" in patched + assert "0 0 * * 1" not in patched + + def test_apply_sync_schedule_handles_missing_workflow(self, logger, tmp_path: Path): + """_apply-sync-schedule should succeed even if workflow file is missing.""" + # Override the schedule but don't create workflow file + makefile = tmp_path / "Makefile" + original = makefile.read_text() + new_content = "RHIZA_SYNC_SCHEDULE = 0 6 * * *\n\n" + original + makefile.write_text(new_content) + + proc = run_make(logger, ["_apply-sync-schedule"], dry_run=False) + assert proc.returncode == 0 + + def test_apply_sync_schedule_prints_info(self, logger, tmp_path: Path): + """_apply-sync-schedule should print info message when patching.""" + # Create a mock workflow file matching the actual rhiza_sync.yml format + workflow_dir = tmp_path / ".github" / "workflows" + workflow_dir.mkdir(parents=True) + workflow_file = workflow_dir / "rhiza_sync.yml" + workflow_file.write_text("on:\n schedule:\n - cron: '0 0 * * 1'\n") + + # Override the schedule + makefile = tmp_path / "Makefile" + original = makefile.read_text() + new_content = "RHIZA_SYNC_SCHEDULE = 0 12 * * 0\n\n" + original + makefile.write_text(new_content) + + proc = run_make(logger, ["_apply-sync-schedule"], dry_run=False) + out = strip_ansi(proc.stdout) + assert "Applied custom sync schedule" in out + assert "0 12 * * 0" in out + + def test_sync_target_calls_apply_schedule(self, logger): + """The sync target should include _apply-sync-schedule in dry-run output.""" + proc = run_make(logger, ["sync"]) + out = proc.stdout + assert "_apply-sync-schedule" in out diff --git a/docs/CUSTOMIZATION.md b/docs/CUSTOMIZATION.md index 158b0980..948b5fcb 100644 --- a/docs/CUSTOMIZATION.md +++ b/docs/CUSTOMIZATION.md @@ -123,10 +123,44 @@ PYTHON_VERSION = 3.12 # Override test coverage threshold (default: 90) COVERAGE_FAIL_UNDER = 80 +# Override the sync schedule (default: weekly on Monday at midnight UTC) +# Uses cron syntax: minute hour day-of-month month day-of-week +RHIZA_SYNC_SCHEDULE = 0 9 * * 1-5 # Weekdays at 9 AM UTC + # Include the Rhiza API (template-managed) include .rhiza/rhiza.mk ``` +### Sync Schedule Override + +The `RHIZA_SYNC_SCHEDULE` variable controls the cron schedule for the GitHub Actions sync workflow (`.github/workflows/rhiza_sync.yml`). Since this file is template-managed and overwritten during sync, the schedule is automatically patched after each `make sync` to preserve your custom value. + +**Default:** `0 0 * * 1` (weekly on Monday at midnight UTC) + +**Examples:** + +```makefile +# Daily at 6 AM UTC +RHIZA_SYNC_SCHEDULE = 0 6 * * * + +# Weekdays at 9 AM UTC +RHIZA_SYNC_SCHEDULE = 0 9 * * 1-5 + +# First day of each month at midnight UTC +RHIZA_SYNC_SCHEDULE = 0 0 1 * * + +# Every 6 hours +RHIZA_SYNC_SCHEDULE = 0 */6 * * * +``` + +Set this in your root `Makefile` (before the `include` line) and it will be applied automatically every time `make sync` runs. The override is also visible in the sync output: + +``` +[INFO] Applied custom sync schedule: 0 9 * * 1-5 +``` + +> **Note:** For GitLab CI, the sync schedule is configured via the GitLab UI (Settings → CI/CD → Pipeline schedules), so this variable only affects GitHub Actions. + ### On-Demand Configuration You can also pass variables directly to `make` for one-off commands: