diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 1252a4a..86438c7 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -367,6 +367,37 @@ bash -n install.sh ./install.sh --help | grep -- --tag ``` +### Upstream Fix Intake (Cherry-Pick Policy) + +Upstream Vocalinux fixes enter this fork only as hand cherry-picks: taken at +defined sync points, never as continuous tracking and never as wholesale +merges. Like every other change, each port goes through the pull request flow +with the standard test, lint, and type gates. Never land one as a direct or +bot commit to `main`, and follow the worktree rules in `AGENTS.md` (one +worktree per branch, every change via a pull request, never commit on `main` +— only the pull request base changes). + +- **When intake happens.** At each release-preparation cycle, while working + through the steps above on the release-prep branch, plus on demand for + critical fixes: crashes, data loss, and security issues. Critical fixes + follow the Emergency Hotfix Process below. +- **When intake freezes.** Intake freezes once the release branch opens + (Step 6). After the freeze, only justified release-blockers are admitted; + everything else waits for the next cycle. +- **One upstream fix per pull request.** Each port is its own pull request, + referencing both the upstream fix and the fork issue/PR numbers. +- **How conflicts resolve.** Keep the upstream logic and re-apply the fork's + renames on top. Conflicts concentrate at the renamed user-visible surfaces + (application name, entry points, desktop and D-Bus identity, configuration + and data directories, package, website) precisely because the internal + Python module name stays `vocalinux` as a retained internal name — that + split is what keeps hand cherry-picks nearly conflict-free everywhere else. + Upstream URLs and changelog history stay untouched for provenance. +- **What qualifies.** Fixes and documentation that changes user instructions + port. Features are re-decided per release, not ported automatically. + Dependency and CI churn ports only when a security advisory names it. + Out-of-scope material is dropped at triage. + ### Step 6: Create Release Branch and PR ```bash diff --git a/tests/test_upstream_intake_policy.py b/tests/test_upstream_intake_policy.py new file mode 100644 index 0000000..cb672df --- /dev/null +++ b/tests/test_upstream_intake_policy.py @@ -0,0 +1,62 @@ +"""The release documentation carries the upstream-fix intake policy. + +Ticket #30 (spec #16): the fork's upstream-intake rule must live where +maintainers run releases from, not only on a decision ticket. A maintainer +following only docs/RELEASE_PROCESS.md must be able to state when intake +happens, when it freezes, what qualifies, and how conflicts resolve. +""" + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +RELEASE_PROCESS = REPO_ROOT / "docs" / "RELEASE_PROCESS.md" + + +def _policy() -> str: + assert RELEASE_PROCESS.is_file(), "docs/RELEASE_PROCESS.md is missing" + return RELEASE_PROCESS.read_text(encoding="utf-8") + + +def test_policy_states_when_intake_happens() -> None: + policy = _policy() + assert "release-preparation" in policy or "release preparation" in policy + assert "on demand" in policy or "on-demand" in policy + + +def test_policy_states_freeze_once_release_branch_opens() -> None: + policy = _policy() + assert "freeze" in policy.lower() + assert "release branch" in policy.lower() + assert "release-blocker" in policy or "release blocker" in policy + + +def test_policy_states_one_fix_per_pull_request_through_standard_gates() -> None: + policy = _policy() + assert "one upstream fix per" in policy.lower() or "one upstream fix" in policy.lower() + assert "pull request" in policy.lower() + + +def test_policy_states_conflict_resolution() -> None: + policy = _policy() + assert "keep" in policy.lower() and "upstream" in policy.lower() + assert "re-apply" in policy.lower() or "reapply" in policy.lower() + + +def test_policy_states_qualification_rule() -> None: + policy = _policy() + lowered = policy.lower() + assert "security advisory" in lowered # dependency/CI churn ports only then + assert "re-decided" in lowered # features are re-decided per release + assert "triage" in lowered # out-of-scope material is dropped at triage + + +def test_policy_names_retained_internal_module_name() -> None: + policy = _policy() + assert "vocalinux" in policy + assert "internal" in policy.lower() + + +def test_policy_keeps_pull_request_flow_and_never_commit_to_main() -> None: + policy = _policy().lower() + assert "pull request" in policy + assert "never" in policy and "main" in policy