docs(release): add release-mechanism gaps + open questions doc [DRAFT]#12599
docs(release): add release-mechanism gaps + open questions doc [DRAFT]#12599bradymiller wants to merge 16 commits into
Conversation
Working notes tracking open gaps + manual procedures in the current release-mechanism. Pairs with PR openemr#12598 (the migration plan). Pre-publication draft. Assisted-by: Claude Code
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #12599 +/- ##
============================================
+ Coverage 24.87% 25.05% +0.17%
- Complexity 84366 84997 +631
============================================
Files 3887 3914 +27
Lines 417119 419966 +2847
============================================
+ Hits 103759 105206 +1447
- Misses 313360 314760 +1400
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Brings PR openemr#12599's working copy in sync with the standalone working notes (which had been kept ahead during 8.1.1 release prep + recovery work on 2026-06-23). Adds four new gaps surfaced during 8.1.1 prep: - **G7 — Conductor tooling drift on pre-820 rel branches.** rel-810/800/704 carry per-branch copies of the conductor's PHP + workflows that silently diverge from master. Discovered live when rel-810's old static BranchVersionResolver::branchToVersion() returned wrong version for conductor dispatch (fixed in openemr#12611). Full conductor sync from master to all pre-820 rel branches is the structural fix; recommended after 8.1.1 ships. - **G8 — No automated regression test for conductor resolvers.** BranchVersionResolver + DispatchDataBuilder + others have no isolated PHPUnit coverage. The G7 bug shipped silently because nothing caught 'returns wrong version for rel-810' before live fire. Concrete test surface enumerated; complements G7 — running the suite on each branch's CI catches future drift automatically. - **G9 — release-docs/<version> PRs on website-openemr don't supersede across version changes.** Each conductor re-resolution to a new version opens a fresh PR at the new branch and leaves the prior orphaned. Two fix options sketched; per-rel-branch head naming (matching openemr/openemr's release-prep/rel-810 pattern) is the structurally cleaner answer. - **G10 — Reusable workflows as a replacement for the byte-identical canary on the docker pipeline.** Captured as a future consideration. If starting today, reusable workflows would be the structurally cleaner answer (master owns impl, rel branches carry thin caller stubs, drift impossible by construction). Canary works and is production-validated, so not urgent; natural time to revisit is when a 'this would have been easier with single-source-of-truth' moment hits the docker side. Assisted-by: Claude Code
…g out) Per maintainer (2026-06-23): rel-800 and rel-704 will not get BranchVersionResolver / conductor tooling backports because both branches will rotate out — no future releases planned on either. Latent drift bugs on those branches will never fire. G7 (drift) scope shrinks to rel-810 only. Full conductor sync recommended after 8.1.1 ships and before the 8.1.2 cycle. If a need ever surfaces to ship from rel-800/rel-704 before retirement, that triggers a per-branch surgical backport at that time, not pre-emptive sync now. G8 (regression tests) scope correspondingly narrows: tests added on master + carried to rel-810 via the G7 sync, rather than to all pre-820 rel branches. Assisted-by: Claude Code
…l paths Adds a pre-ship blocker note to the 'P5. Merge the release-prep PR' section of the canonical 8.1.1 release sequence: The current Infra PR slot (openemr-devops PR openemr#760, release-rotation/auto) is frozen with pre-docker-migration content from 2026-06-10. Its diff edits docker/openemr/8.0.0/Dockerfile, docker/openemr/current, and dependabot.yml references to docker/openemr/* -- all paths now deleted on devops master (404). release-rotation.yml runs successfully on every dispatch but skips force-push + PR-update because the diff against the already-rotated branch is empty. PR openemr#760 stays frozen indefinitely. Shipping 8.1.1 via ship-release.yml today would either: 1. Block on PR openemr#760's unmergeable state (preflight refuses to merge) 2. Or silently undo the docker migration by resurrecting deleted paths Workstream 1 of the migration doc (delete rotation slice + collapse ship-release to 2-PR shape) unblocks this path. Discovered 2026-06-24 while tracing the ship flow end-to-end. Also adds: - 'Recommended path' (ship-release.yml) vs 'Direct path' (merge conductor PR directly) options for P5, with tradeoffs (direct path sidesteps the blocker but loses multi-PR-merge ordering safety net) - Ordering constraints summary updated to note P5 blocked on workstream 1 Assisted-by: Claude Code
…2656 PR openemr#12656 reverses the 2026-06-23 decision that release-targets.yml allows only one row per branch. Multi-row IS needed for the in-dev patch release case: when a rel branch hits a new patch in dev mode (e.g., rel-810 -> 8.1.2-dev), the daily orchestrator still needs to publish the prior stable (8.1.1) until the new dev ships. One row per branch can't express that. PR drops the validator's branch-uniqueness check (the duplicate-docker_tag check stays — push race on Docker Hub still matters). Adds optional 'unreleased: true' row marker for placeholders; orchestrator skip-on-unreleased wired in same PR; dockerhub readme push + demo_farm bumper to follow. Example row added in openemr#12656: second rel-810 entry with docker_tags: 8.1.0 + openemr_version_ref: v8_1_0 + unreleased: true. Wires up the multi-row mechanism without immediately publishing 8.1.0 daily images. Decisions-made section + update log both touched. Assisted-by: Claude Code
Three updates capturing the design discussion of 2026-06-27 around the branch-cut + release-time extensions: G4 — folds into G5's branch-cut automation design instead of exposing scope as a release-prep.yml input. Keeps release-prep.yml's scope narrow (rel-branch-only); the new branch-cut-automation.yml workflow invokes the master-scope mutator as one of its steps. G5 — expanded with full design. on: create trigger fires once on rel-NNN0 branch creation; opens two coordinated PRs (rel-NNN0-side Dockerfile ARG edit + master-side version.php advance + add release-targets row + rotate next + SQL skeleton chain adjust). Notes the conductor's premature release-prep PR interaction and the recommendation to accept it (vs add suppression complexity). G11 (new) — Post-release release-targets.yml updates are manual. Extend conductor to open a SECOND PR (release-finalize/<rel-branch>) on master paired with the existing release-prep/<rel-branch> PR. Carries the slot shuffle, ref pin, and unreleased-row drop. Lands on tag dispatch consumer marking it Ready and auto-merging. Both G5 and G11 map to workstreams 2 (branch-cut) and 3 (per-release optimization) in the planning doc. Implementation work captured there in workstream-specific subsections. Assisted-by: Claude Code
Captures the 2026-06-27 design discussion that meaningfully reshapes G6 from from-scratch render to reconciliation. Cluster identity is sticky across runs because each cluster name maps to a subdomain (eight -> eight.openemr.io) that's referenced from external surfaces (wiki, social, mail). Reassigning a cluster's meaning would break those external references, so the bot reads the CURRENT ip_map_branch.txt as state, applies a reconciliation diff against the desired state computed from upstream, and writes the new file preserving cluster -> subdomain stability. Key refinements: - Per-input source map: release-targets.yml is master-only; Dockerfiles are per-branch; current demo_farm files are read for reconciliation state - Section ownership matrix: Production + Up-for-grabs are fixed cluster identities; Master demos + Release demos are sticky from prior state with new ones taken from parked; Parked is the overflow bench; Misc is hand-curated (bot never touches) - Up-for-grabs flex always from master's Dockerfile, regardless of community-claimed branch override - Cluster count derivation: from current per-cluster state (preserves production=3, up-for-grabs=3, rest=2 pattern); default 2 for newly-assigned - Fail-loud edge cases: no latest holder, empty parked bench when cluster needed, Dockerfile ARG parse failure, CI matrix path missing - Reconciliation algorithm sketch (6 steps from inputs to PR) - Trigger composition: daily cron (load-bearing self-healing) + eager repository_dispatch (optional optimization) + workflow_dispatch (manual / dry-run) - Implementation language: bash + yq + curl + awk (demo_farm self- contained, low contributor barrier) - Coordination boundary with workstreams 2 + 3: release-targets.yml is the contract; no tight coupling - Three-PR scaffolding plan: dry-run -> live PR -> eager dispatch + unreleased skip The unreleased-skip in PR #3 is the demo_farm-side consumer of the openemr#12656 marker that's currently tracked as the remaining follow-up. Assisted-by: Claude Code
…ior bumper) Surveyed demo_farm_openemr today and found the prior automation consists of more than just bump-tag.yml: there's a whole tools/release/ PHP project (IpMapBumper class + CLI + tests + toolchain scaffolding) that exists only to support bump-tag.yml. All of it becomes dead code after the new bot's dispatch consumers land. PR #3 expanded to atomic flip: Add: - repository_dispatch consumers (openemr-tag / openemr-rel-cut / openemr-rel-update) - unreleased: true skip on release-targets.yml rows Delete: - .github/workflows/bump-tag.yml (109 lines) - tools/release/src/IpMapBumper.php + bin/bump-ip-map.php + tests/IpMapBumperTest.php - tools/release/{Taskfile.yml, phpunit.xml.dist, phpcs.xml, phpstan.neon, rector.php, composer.json, composer.lock, .gitignore} — entire PHP toolchain scaffolding since the new bot is bash and there are no other PHP tools in demo_farm Net: ~200 LOC + ~10 config files deleted; ~50 LOC added for the new dispatch wiring. Atomic-ness rationale: - Retiring BEFORE PR #3 -> gap window where openemr-tag dispatches don't update demo_farm - Retiring AFTER PR #3 -> race condition (both workflows fire on every openemr-tag dispatch) - Atomic -> one dispatch consumer at all times, clean cutover Future bash tools live in their own dir; future PHP tools (if ever) re-scaffold their own project — keeping empty scaffolding is bitrot bait. Assisted-by: Claude Code
…ntory + skip-line scenario
…enemr#12697 in-flight Assisted-by: Claude Code
…r (workstream 3 Phase A) (#12662) ## Context (for anyone catching up) This is part of the broader **release-mechanism migration** project. Two living tracking docs describe the larger picture and are required reading for context: - **Planning doc** (still in DRAFT PR #12598): [`docs/release-mechanism-migration-from-devops.md`](https://github.com/bradymiller/openemr/blob/release-mechanism-migration-doc/docs/release-mechanism-migration-from-devops.md) — 6 workstreams of release infrastructure work. See the "**Workstream 3 detail — release-time partner PR + release-cycle-bot**" section for what this PR fits into. Workstreams 1 + 5 (devops cleanup + demo_farm auto-derive bot) already shipped 2026-06-28; workstream 4 (8.1.1 ship from rel-810) is the next actual release event. - **Gaps doc** (still in DRAFT PR #12599): [`docs/release-mechanism-gaps.md`](https://github.com/bradymiller/openemr/blob/release-mechanism-gaps-doc/docs/release-mechanism-gaps.md) — open questions + design notes for each gap. See the "**G11 — Post-release release-targets.yml updates are manual** *(workstream 3 design)*" section, especially the "Phase A refinement (2026-06-28)" subsection that drove this PR's exact scope. **Existing baseline**: the conductor (`release-prep.yml`) lives in every branch and fires on push to `rel-*`. It opens a tracking PR titled `release-prep/<rel-branch>` against the rel branch, force-updated on every push, merged at ship-time. This PR extends that conductor to ALSO open a master-side partner PR (`release-finalize/<rel-branch>`) at the same trigger. ## Summary Phase A of workstream 3 (G11). Automates the three coordinated edits master's `.github/release-targets.yml` needs after a rel branch ships, as a draft `release-finalize/<rel-branch>` partner PR opened alongside the existing `release-prep/<rel-branch>` PR. The three transforms (today done by hand for every release): - Pin the rel branch row's `openemr_version_ref` to the new tag (e.g. `rel-810` -> `v8_1_1`) so daily docker builds stop tracking the branch tip - Slot-shuffle Docker Hub `latest`/`next`/`dev` across rows: just-shipped rel branch promotes `next` -> `latest`, prior `latest` holder drops it, next-upcoming-stable owner (newer rel branch if any else master) acquires `next` - Drop the unreleased placeholder row from the multi-row dev pattern (#12656) when present ## What's in this PR - New mutator `PostReleaseTargetsMutator` (line-based surgical edits to preserve the file's substantial human-authored comments; idempotent; Symfony YAML parser used as post-edit sanity check) - `MutatorContext` gains an optional `relBranch` field + `tagName()` helper; existing mutators ignore it - `ReleasePrepCommand` gains `--rel-branch` (required for `--scope=master`), plumbs to context, rewires master-scope list to a single `PostReleaseTargetsMutator` - `VersionPhpMasterMutator` + `SqlUpgradeSkeletonMutator` stay defined but intentionally unwired — they are branch-cut concerns workstream 2 (G4) will own - `.github/workflows/release-prep.yml` grows steps after the existing prep-PR step (checkout master / composer install in master-checkout / run master-scope mutators / open `release-finalize/<rel-branch>` via peter-evans, draft, test-mode skipped) - `render-pr-body.php` learns `--rel-branch` for the new finalize template ## Lifecycle 1. Conductor fires on push to rel-* -> opens BOTH `release-prep/<rel-branch>` (rel-*) AND `release-finalize/<rel-branch>` (master) as drafts 2. Operator marks the rel-branch PR Ready and merges, triggering the tag 3. Operator marks the finalize PR Ready and merges (auto-mark-Ready on tag creation can land in a follow-up) ## Sequencing with Phase B Phase B (separate PR) cherry-picks the conductor extension to `rel-810` so the upcoming 8.1.1 release benefits — rel-810's conductor invokes its own `src/Common/Command/...` tree, not master's, so both the workflow YAML and the PHP changes must land there. ## References - **Gaps doc G11** (DRAFT PR #12599): [link to file](https://github.com/bradymiller/openemr/blob/release-mechanism-gaps-doc/docs/release-mechanism-gaps.md) — especially the "Phase A refinement (2026-06-28)" subsection and the "Docker Hub tag model" / slot-promotion model section - **Planning doc Workstream 3 detail** (DRAFT PR #12598): [link to file](https://github.com/bradymiller/openemr/blob/release-mechanism-migration-doc/docs/release-mechanism-migration-from-devops.md) - The dual-PR partner pattern generalizes — workstream 2 (G4/G5) will reuse the extended `MutatorContext` + dual-checkout workflow structure for branch-cut automation ## Dry-run output Run against the live `.github/release-targets.yml` with `--target-version=8.1.1 --rel-branch=rel-810 --scope=master`: ```diff @@ -99,30 +99,19 @@ # release cycle (e.g., when version.php switches to 8.3.0-dev). # The docker_tag <-> version.php alignment guard in # docker-validate-release-targets.yml catches drift on PRs. - docker_tags: 8.2.0,dev + docker_tags: 8.2.0,dev,next openemr_version_ref: master - branch: rel-810 - docker_tags: 8.1.1,next - openemr_version_ref: rel-810 - -# Second rel-810 row -- placeholder for the multi-row dev pattern (see -# `unreleased` field doc above). Wires up the second-row mechanism -# without actually publishing 8.1.0 from this configuration. When a real -# dev cycle on rel-810 starts (e.g., 8.1.2-dev with 8.1.1 already -# shipped), this row gets repurposed: docker_tags -> 8.1.1, -# openemr_version_ref -> v8_1_1, `unreleased` flag dropped. -- branch: rel-810 - docker_tags: 8.1.0 - openemr_version_ref: v8_1_0 - unreleased: true + docker_tags: 8.1.1,latest + openemr_version_ref: v8_1_1 - branch: rel-800 # 8.0.0 is the floating "current latest 8.0.0.x" pointer (always advances # with openemr_version_ref); 8.0.0.3 is the specific-patch pointer (only # advances when this row bumps to 8.0.0.4+). Both tags get an auto-appended # "<tag>-YYYY-MM-DD" sibling for immutable per-build pinning. - docker_tags: 8.0.0,8.0.0.3,latest + docker_tags: 8.0.0,8.0.0.3 openemr_version_ref: v8_0_0_3 - branch: rel-704 ``` Second invocation is a no-op (idempotency confirmed). ## Test plan - [x] PHPStan level 10 clean on changed files - [x] PHPCS clean on changed files - [x] Rector dry-run clean on changed files - [x] phpunit-isolated: PostReleaseTargets + ReleasePrep + MutatorContext family passes (10+ test methods covering canonical 8.1.1-from-rel-810 case, idempotency, multi-row placeholder drop, single-rel-branch base case, comment preservation, next-owner-prefers-newer-rel-branch-over-master, missing-rel-branch-throws, historical-secondary-row-not-re-pinned, inline-comments-preserved on scalars, legacy rel-704 idempotency) - [x] Dry-run against the live `.github/release-targets.yml` for 8.1.1-from-rel-810 produces the expected diff (rel-810 pinned to v8_1_1 with next->latest, rel-800 drops latest, master acquires next, placeholder row + its comments dropped) - [x] Second-run idempotency confirmed via CLI - [x] Workflow YAML parses (yaml.safe_load) and actionlint clean - [ ] End-to-end conductor exercise — requires push to a rel-* branch; covered by Phase B's cherry-pick to rel-810 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Context for anyone catching up This PR builds the patch-prep automation arm of the release-mechanism migration — sibling to the branch-cut automation in #12696. The same reference docs lay out the plan: - **Planning doc — workstream 6 detail**: https://github.com/bradymiller/openemr/blob/release-mechanism-migration-doc/docs/release-mechanism-migration-from-devops.md (search for *Workstream 6 — patch-prep automation*) - **Gaps doc — G12** (plus G4 + G5 for the workstream 2 mutator framework this builds on): https://github.com/bradymiller/openemr/blob/release-mechanism-gaps-doc/docs/release-mechanism-gaps.md > **Naming note (2026-06-30):** Originally labelled workstream 3 Phase B; renamed to workstream 6 in the planning/gaps doc updates (PRs #12598 and #12599) to avoid collision with workstream 3's actual Phase B — the cherry-pick of #12662's conductor extension to rel-810 for the 8.1.1 ship scenario. The COMMIT messages on this branch retain the original "workstream 3 phase B" framing; rewriting them would require a force-push and isn't worth it for a naming refinement. The PR title + body here reflect the new naming. **Based on #12696** (workstream 2 — branch-cut automation), which is in turn based on #12662 (workstream 3 phase A — release-finalize partner PR). HEAD of this PR's branch sits on top of #12696's HEAD (`b9cf8d3714`). The diff against `master` therefore includes all three PRs' commits; once #12662 and #12696 land in order, this PR rebases cleanly and the diff shrinks to just this workstream's commits. No overlap with #12696's mutator code — this PR's changes to `DockerUpgradeScaffoldMutator` and `SqlUpgradeSkeletonMutator` are strictly additive (they consult `MutatorContext::$fromVersion` when set, fall through to existing behavior otherwise). ## Summary When a maintainer pushes a `$v_patch` increment to `version.php` on a `rel-*` branch (e.g., rel-810 going `8.1.0` → `8.1.1-dev`), GitHub Actions today does nothing. This PR makes that push event fire a workflow that opens **two coordinated ready-for-review PRs** (not drafts — patch-prep PRs should land fast, paving the way for the new dev cycle): - **rel-side PR** (`patch-prep/rel-NNN0` → base `rel-NNN0`): docker upgrade scaffold + new SQL patch upgrade skeleton. - **master-side PR** (`patch-prep/rel-NNN0-master` → base `master`): same docker upgrade scaffold + same new SQL patch upgrade skeleton + master bridge file rename + `release-targets.yml` row insert + placeholder drop. Maintainer reviews both, marks Ready (already there — not draft), merges rel-side first, then master-side. ## Trigger mechanics + gate logic `on: push` to `branches: rel-*` filtered to `paths: version.php`. From that, the resolve step: 1. **New-branch creation** (`before` SHA all zeros) → skip cleanly. Branch-cut (#12696) owns that lifecycle event. 2. Fetch before-state + after-state `version.php` via the GitHub API; parse `$v_major`, `$v_minor`, `$v_patch`, `$v_tag` with regex. 3. **Gate 1**: `$v_patch` must have strictly increased (`after_patch > before_patch`). Cosmetic edits to other parts of `version.php` skip. 4. **Gate 2**: after-state `$v_tag` must be `-dev`. release-prep's mid-flight strip of `-dev` to ship a release also bumps `$v_patch` in some flows; that's not our event. 5. If both gates pass, derive `target_version = MAJOR.MINOR.after_patch` and `prev_version = MAJOR.MINOR.before_patch`. `workflow_dispatch` provides an escape hatch — maintainer supplies `rel-branch`, `target-version`, `prev-version` explicitly for recovery from a workflow miss. ## Mutator inventory **Reused from #12696 without code modification (just additive context extension):** | Mutator | Side | Behavior | |---------|------|----------| | `DockerUpgradeScaffoldMutator` | both | Bumps 3 docker-version files (N → N+1), creates `fsupgrade-(N+1).sh` stub, extends Dockerfile COPY + chmod blocks. Cross-branch sync requirement per the `docker upgrade actions mandatory per release` rule. | | `SqlUpgradeSkeletonMutator` | both | Creates `sql/<from>-to-<to>_upgrade.sql` with the comment-meta-language header. | **New in this PR:** | Mutator | Side | Behavior | |---------|------|----------| | `MasterSqlPatchBridgeMutator` | master | Renames the long-lived bridge file `sql/<X_Y_(P-1)>-to-<X_(Y+1)_0>_upgrade.sql` → `sql/<X_Y_P>-to-<X_(Y+1)_0>_upgrade.sql`. Contents preserved (they accumulate dev-cycle SQL); only the "from" anchor in the filename advances. | | `PatchPrepReleaseTargetsMutator` | master | Inserts a fresh `- branch: rel-NNN0` row with `docker_tags: <target>,next`; drops any rows for the target branch flagged `unreleased: true`. Other branches' rows untouched. Surgical line-based edits to preserve comments; Symfony YAML parser used as a structural sanity check on the result. | **`MutatorContext` extension** — adds optional `?string $fromVersion` field with `MAJOR.MINOR.PATCH` validation. The patch-prep conductor supplies it because: - Rel side: `version.php` has already been bumped past the value `SqlUpgradeSkeletonMutator` needs to anchor at (post-bump `$v_patch` is the *target*, not the *from*). - Master side: `version.php` is for the next-minor line entirely (e.g., `8.2.0-dev`) and bears no relationship to the rel-branch patch. Both consuming mutators fall through to existing behavior when `fromVersion` is null, so the branch-cut path is unaffected. ## Mutator order (master side) ``` 1. DockerUpgradeScaffoldMutator (cross-branch sync) 2. SqlUpgradeSkeletonMutator (creates new patch's <from>-to-<to>.sql) 3. MasterSqlPatchBridgeMutator (renames the long-lived bridge file) 4. PatchPrepReleaseTargetsMutator (release-targets.yml row insert + placeholder drop) ``` The skeleton mutator runs **before** the bridge mutator deliberately. Both touch disjoint files (skeleton writes `<X_Y_P-1>-to-<X_Y_P>.sql`; bridge renames `<X_Y_P-1>-to-<X_(Y+1)_0>.sql` → `<X_Y_P>-to-<X_(Y+1)_0>.sql`), but in a partial-run scenario the skeleton-first ordering means the new patch file is present before the long-lived bridge gets disturbed. Also matches the rel-side ordering. ## Lifecycle 1. Maintainer bumps `$v_patch` on a rel branch (`rel-810`: `$v_patch='0'` → `$v_patch='1'`, `$v_tag='-dev'`) and pushes. 2. `patch-prep-automation.yml` fires, resolves target-version = 8.1.1 + prev-version = 8.1.0. 3. Workflow checks out the rel branch, runs `php bin/console openemr:patch-prep --side=rel`, opens `patch-prep/rel-810` ready-for-review PR. 4. Workflow checks out master, runs `php bin/console openemr:patch-prep --side=master`, opens `patch-prep/rel-810-master` ready-for-review PR. 5. Maintainer reviews both, merges rel-side first, then master-side. ## Test plan - [x] `openemr-cmd worktree exec ws-patch-prep-automation pit` — full isolated suite passes (3572 tests) - [x] `openemr-cmd worktree exec ws-patch-prep-automation pst` — PHPStan level 10 clean - [x] `openemr-cmd worktree exec ws-patch-prep-automation pr` — phpcs clean on all new files (only finding is the pre-existing `sites/default/sqlconf.php` dev-stack auto-config write, same as #12696) - [x] Local dry-run on this worktree, both sides, producing the diffs in the *Sample diffs* section below - [x] Idempotency verified on both new mutators (`MasterSqlPatchBridgeMutator`, `PatchPrepReleaseTargetsMutator`): each has dedicated `testIdempotentOnRerun` / `testIdempotentWhenNewExistsAndOldGone` tests - [x] YAML output of `PatchPrepReleaseTargetsMutator` parsed via Symfony YAML as structural sanity check - [ ] CI green on this PR's checks - [ ] (Future) End-to-end exercise at the next real patch-prep on a live rel branch ## Note about `sites/default/docker-version` and the docker volume Same as #12696: the easy-dev docker stack used to capture the dry-run diffs below mounts `sites/default/` as a docker volume rather than a host bind, so `DockerUpgradeScaffoldMutator`'s write to `sites/default/docker-version` doesn't propagate back to the host. The mutator DOES write it (verified by inspecting the volume contents inside the container) — it just doesn't surface on the host. The actual CI workflow run won't have this quirk; all 3 docker-version files (top-level `docker-version`, `docker/release/upgrade/docker-version`, `sites/default/docker-version`) will appear in the real patch-prep PR's diff. ## Sample diffs — what the patch-prep PRs will look like Captured via `php bin/console openemr:patch-prep --target-version=8.1.2 --rel-branch=rel-810 --prev-version=8.1.1 --side=<rel|master>` against the worktree at this commit. Scenario: rel-810 has just shipped 8.1.1; maintainer bumps to `8.1.2-dev`. <details> <summary>Master-side patch-prep PR diff (9 files; SQL skeleton header truncated)</summary> ```diff diff --git a/.github/release-targets.yml b/.github/release-targets.yml index e070a34..61b552efe9 100644 --- a/.github/release-targets.yml +++ b/.github/release-targets.yml @@ -103,19 +103,12 @@ openemr_version_ref: master - branch: rel-810 - docker_tags: 8.1.1,next + docker_tags: 8.1.2,next openemr_version_ref: rel-810 -# Second rel-810 row -- placeholder for the multi-row dev pattern (see -# `unreleased` field doc above). Wires up the second-row mechanism -# without actually publishing 8.1.0 from this configuration. When a real -# dev cycle on rel-810 starts (e.g., 8.1.2-dev with 8.1.1 already -# shipped), this row gets repurposed: docker_tags -> 8.1.1, -# openemr_version_ref -> v8_1_1, `unreleased` flag dropped. - branch: rel-810 - docker_tags: 8.1.0 - openemr_version_ref: v8_1_0 - unreleased: true + docker_tags: 8.1.1,next + openemr_version_ref: rel-810 - branch: rel-800 # 8.0.0 is the floating "current latest 8.0.0.x" pointer (always advances diff --git a/docker-version b/docker-version index b4de394..48082f7 100644 --- a/docker-version +++ b/docker-version @@ -1 +1 @@ -11 +12 diff --git a/docker/release/upgrade/docker-version b/docker/release/upgrade/docker-version index b4de394..48082f7 100644 --- a/docker/release/upgrade/docker-version +++ b/docker/release/upgrade/docker-version @@ -1 +1 @@ -11 +12 diff --git a/docker/release/Dockerfile b/docker/release/Dockerfile index 7ba67a1..5b601c1 100644 --- a/docker/release/Dockerfile +++ b/docker/release/Dockerfile @@ -289,6 +289,7 @@ COPY upgrade/docker-version \ upgrade/fsupgrade-9.sh \ upgrade/fsupgrade-10.sh \ upgrade/fsupgrade-11.sh \ + upgrade/fsupgrade-12.sh \ /root/ # Set upgrade scripts as executable (read and execute for owner only) @@ -303,7 +304,8 @@ RUN chmod 500 \ /root/fsupgrade-8.sh \ /root/fsupgrade-9.sh \ /root/fsupgrade-10.sh \ - /root/fsupgrade-11.sh + /root/fsupgrade-11.sh \ + /root/fsupgrade-12.sh ``` Bridge rename (the `sql/8_1_1-to-8_2_0_upgrade.sql` file's contents are preserved exactly; only the filename changes): ```diff -sql/8_1_1-to-8_2_0_upgrade.sql [deleted] +sql/8_1_2-to-8_2_0_upgrade.sql [created, byte-identical to deleted] ``` New files: ``` sql/8_1_1-to-8_1_2_upgrade.sql (new — header-only SQL upgrade skeleton) docker/release/upgrade/fsupgrade-12.sh (new — see contents in rel-side block below) ``` </details> <details> <summary>Rel-side patch-prep PR diff (5 files; byte-identical docker scaffold to master-side, plus skeleton)</summary> ```diff diff --git a/docker-version b/docker-version index b4de394..48082f7 100644 --- a/docker-version +++ b/docker-version @@ -1 +1 @@ -11 +12 diff --git a/docker/release/upgrade/docker-version b/docker/release/upgrade/docker-version index b4de394..48082f7 100644 --- a/docker/release/upgrade/docker-version +++ b/docker/release/upgrade/docker-version @@ -1 +1 @@ -11 +12 diff --git a/docker/release/Dockerfile b/docker/release/Dockerfile index 7ba67a1..5b601c1 100644 --- a/docker/release/Dockerfile +++ b/docker/release/Dockerfile @@ -289,6 +289,7 @@ COPY upgrade/docker-version \ upgrade/fsupgrade-9.sh \ upgrade/fsupgrade-10.sh \ upgrade/fsupgrade-11.sh \ + upgrade/fsupgrade-12.sh \ /root/ @@ -303,7 +304,8 @@ RUN chmod 500 \ /root/fsupgrade-8.sh \ /root/fsupgrade-9.sh \ /root/fsupgrade-10.sh \ - /root/fsupgrade-11.sh + /root/fsupgrade-11.sh \ + /root/fsupgrade-12.sh ``` New `docker/release/upgrade/fsupgrade-12.sh`: ```bash #!/bin/bash # Upgrade number 12 for OpenEMR docker # From prior version 8.1.1 (needed for the sql upgrade script). priorOpenemrVersion="8.1.1" echo "Start: Upgrade to docker-version 12" # TODO: fill in upgrade logic per-release; see prior fsupgrade-*.sh for examples echo "Completed: Upgrade to docker-version 12" ``` New `sql/8_1_1-to-8_1_2_upgrade.sql` (header-only — first 10 of ~190 lines shown): ```sql -- -- Comment Meta Language Constructs: -- -- #IfNotTable -- argument: table_name -- behavior: if the table_name does not exist, the block will be executed -- #IfTable -- argument: table_name -- behavior: if the table_name does exist, the block will be executed ... [comment-meta-language header continues; no upgrade SQL — per-release work fills the body] ``` </details> ## Design decisions worth flagging - **Sibling command, not `--scope=patch-prep` extension.** Matches the precedent set by #12696 (which itself follows #12662's `--scope=master` framing for release-finalize). Each lifecycle event gets its own command with its own mutator list. - **`MasterSqlPatchBridgeMutator` reports BOTH old and new paths** in its `MutatorResult::changedFiles` (since rename = delete + create). The conductor's output shows both for visibility. - **`PatchPrepReleaseTargetsMutator` is scoped to the target rel branch.** Unlike `BranchCutReleaseTargetsMutator` (#12696) which drops all `unreleased: true` rows uniformly, patch-prep only drops placeholders for the branch being patched. Other rel branches' placeholders are not our concern. - **`workflow_dispatch` requires explicit `prev-version`** rather than computing it from current version.php. Recovery scenarios may want a different baseline than auto-detect would pick. ## Notes - DO NOT enable auto-merge. - BASED ON #12696 — will need rebase after that lands. Should be clean: this PR's only overlap with #12696 is on `MutatorContext.php` (additive `fromVersion` field), `DockerUpgradeScaffoldMutator.php` (additive `fromVersion` consumption in one helper), and `SqlUpgradeSkeletonMutator.php` (additive `fromVersion` consumption in one line) — all strictly additive against #12696's versions. - For the `peter-evans/create-pull-request@v8` action, verified beforehand that the workflow's target branches (`patch-prep/rel-810`, `patch-prep/rel-810-master`) do not exist on the remote (clean state for first run). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added patch-prep automation for release branches, including new pull request templates and a workflow that opens coordinated rel-side and master-side PRs. * Added a new command to generate patch-prep changes and report the files updated. * **Bug Fixes** * Improved version handling so patch-prep can use the exact previous patch version when creating upgrade scaffolding. * Added safer handling for master SQL bridge file renames and release-target list updates. * **Tests** * Added coverage for the new patch-prep flow, version validation, and file-update behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Reflect the 2026-07-01 landings to openemr/openemr master: G4 + G5 consumed by workstream 2 branch-cut automation (PR openemr#12696, `6513b487b6`) via new `openemr:branch-cut` command + workflow + 4 new mutators; G11 Phase A shipped as release-finalize partner PR (PR openemr#12662, `abb1e2d940`); G12 shipped as workstream 6 patch-prep automation (PR openemr#12697, `c1af6370a0`). Note in-flight rabbit-fix follow-up PR openemr#12709 addressing 3 Major items missed at openemr#12696 merge. Wire G3's forward reference to `SqlUpgradeSkeletonMutator` to its now-shipped consumers in workstreams 2 + 6. Resolve conditional sequencing subsections now that 8.1.1 is being pursued and all three workstreams landed to master before the 8.1.1 ship. Assisted-by: Claude Code
Assisted-by: Claude Code
…12712) Prep for the imminent rel-820 cut. 8.1.x is being skipped entirely (no 8.1.1 ship), so both rel-810 rows should be gone by the time \`branch-cut-automation.yml\` fires on the \`create\` event. ## Changes - **Add \`unreleased: true\` to the rel-810 \`8.1.1,next\` row.** Pauses daily publishing from rel-810 across the cut window. \`BranchCutReleaseTargetsMutator.dropAllUnreleasedRows()\` (from #12696) will then uniformly drop this row alongside the existing \`8.1.0\` placeholder — leaving no rel-810 rows post-cut, matching the skip-line scenario documented in \`docs/release-mechanism-gaps.md\` G5 refinement. - **Move the \`next\` tag from rel-810 to master.** Interim posture so master owns \`next\` temporarily during the cut. At the \`create\` event, \`BranchCutReleaseTargetsMutator.bumpMasterRow()\` drops master's \`next\` and bumps minor to \`8.3.0,dev\`, and the newly-inserted rel-820 row picks up \`8.2.0,next\`. ## Post-cut end state (once branch-cut workflow runs against rel-820) \`\`\`yaml - branch: master docker_tags: 8.3.0,dev openemr_version_ref: master - branch: rel-820 docker_tags: 8.2.0,next openemr_version_ref: rel-820 - branch: rel-800 docker_tags: 8.0.0,8.0.0.3,latest openemr_version_ref: v8_0_0_3 - branch: rel-704 docker_tags: 7.0.4 openemr_version_ref: v7_0_4 \`\`\` ## Test plan - [ ] \`docker-validate-release-targets.yml\` CI check passes (docker_tag ↔ version.php alignment: master's \`8.2.0,dev,next\` should still align with master's current \`\$v_major.\$v_minor = 8.2\`; rel-810's \`8.1.1\` is on an unreleased row so should be exempt from the guard). - [ ] After merge + rel-820 cut, verify the branch-cut PRs open with the expected end state. ## References Skip-line cut pattern: \`docs/release-mechanism-gaps.md\` G5 "Refinement (2026-06-30) — Skip-line cut scenario" (already in master via #12599).
…rcise 8.1.x is being skipped entirely; no 8.1.1 ship. rel-820 cut becomes the first end-to-end exercise of branch-cut automation. Workstream 3 Phase B (cherry-pick to rel-810) is now unnecessary. Pre-cut posture in openemr#12712. Assisted-by: Claude Code
…20 cut, 2026-07-02) Assisted-by: Claude Code
Summary
Draft companion to PR #12598 (the release-mechanism migration
plan). Adds a working-notes doc tracking gaps + manual procedures
in the current release-mechanism — the canonical place to capture
surprises, manual workarounds, timing nuances, and the "this is how
we actually do X right now" procedures that emerge during release
work.
What's in here
docs/release-mechanism-gaps.mdThe doc covers:
location, patch-flow stance.
--skip-globalsflag on the conductor (why isGlobalsIncMutatordisabled in production runs?)unwired (no auto-bump after a tag ships)
SqlUpgradeSkeletonMutatoris master-only ANDcurrently has no workflow path
--scope=master(cross-tracked in migration plan's deferred-debt)
in migration plan's deferred-debt)
checklist for cutting a new rel branch from master, capturing the
easy-to-forget Dockerfile two-block footgun
per release (this IS the consumer auto-upgrade feature),
cross-branch propagation requirement, ~5-substitution template
pattern for new
fsupgrade-N.shintermediate-minor cases
dev/next/latestareversion-names (not floating pointers), with the slot-promotion
pattern that fires when a release ships
openemr_version_refbranch-tip-vs-tag-pin lifecycle across arelease
vs manual × release time vs cut time)
end-to-end PR flow
Why a draft PR
Visibility for the community + other maintainers' AI assistants who
might be examining release-mechanism work. The gap-tracking content
is substantive enough to warrant the formal-review surface that a PR
provides. Expected to be a living doc — gap discoveries land via
normal PRs against master as they emerge.
Test plan
sibling
docs/release-mechanism-migration-from-devops.mdonceboth land.
🤖 Generated with Claude Code