@@ -49,14 +49,17 @@ public function testRotationAdvancesNextSlotAndRewritesPinFiles(): void
4949 ]);
5050
5151 self ::assertFalse ($ result ->isNoOp (), 'Rotation should have changed files ' );
52- self ::assertContains ('docker/openemr/8.1.0/Dockerfile ' , $ result ->changedFiles );
52+ // External slot-tracking files follow the next slot.
53+ self ::assertContains ('.github/workflows/test-bats.yml ' , $ result ->changedFiles );
54+ self ::assertContains ('utilities/container_benchmarking/benchmark.sh ' , $ result ->changedFiles );
5355 self ::assertContains ('docker/openemr/next ' , $ result ->changedFiles );
54- self ::assertContains ('docker/openemr/OVERVIEW.md ' , $ result ->changedFiles );
5556 self ::assertContains ('tools/release/versions.yml ' , $ result ->changedFiles );
57+ // The per-version build dir is immutable: never rewritten by rotation.
58+ self ::assertNotContains ('docker/openemr/8.1.0/Dockerfile ' , $ result ->changedFiles );
5659
57- $ dockerfile = (string ) file_get_contents ($ this ->tmpDir . '/docker/openemr/8.1.0/Dockerfile ' );
58- self ::assertStringContainsString ('ARG OPENEMR_VERSION=rel-820 ' , $ dockerfile );
59- self ::assertStringNotContainsString ('rel-810 ' , $ dockerfile );
60+ $ bats = (string ) file_get_contents ($ this ->tmpDir . '/.github/workflows/test-bats.yml ' );
61+ self ::assertStringContainsString ('docker/openemr/8.2.0/** ' , $ bats );
62+ self ::assertStringNotContainsString ('docker/openemr/8.1.0/** ' , $ bats );
6063
6164 self ::assertSame (
6265 '8.2.0 ' ,
@@ -69,6 +72,43 @@ public function testRotationAdvancesNextSlotAndRewritesPinFiles(): void
6972 self ::assertStringContainsString ('branch: "rel-820" ' , $ registry );
7073 }
7174
75+ public function testCurrentRotationLeavesVersionDirAndDependabotUntouched (): void
76+ {
77+ $ dockerfilePath = $ this ->tmpDir . '/docker/openemr/8.0.0/Dockerfile ' ;
78+ $ dependabotPath = $ this ->tmpDir . '/.github/dependabot.yml ' ;
79+ $ dockerfileBefore = (string ) file_get_contents ($ dockerfilePath );
80+ $ dependabotBefore = (string ) file_get_contents ($ dependabotPath );
81+
82+ $ rotator = new SlotRotator ($ this ->tmpDir , $ this ->registryPath );
83+
84+ $ result = $ rotator ->rotate ([
85+ 'current ' => [
86+ 'minor ' => '8.1 ' ,
87+ 'full ' => '8.1.0 ' ,
88+ 'branch ' => 'rel-810 ' ,
89+ 'docker_dir ' => '8.1.0 ' ,
90+ ],
91+ ]);
92+
93+ // Rotating current flips only the symlink + registry.
94+ self ::assertContains ('docker/openemr/current ' , $ result ->changedFiles );
95+ self ::assertContains ('tools/release/versions.yml ' , $ result ->changedFiles );
96+ self ::assertNotContains ('docker/openemr/8.0.0/Dockerfile ' , $ result ->changedFiles );
97+ self ::assertNotContains ('.github/dependabot.yml ' , $ result ->changedFiles );
98+
99+ self ::assertSame ('8.1.0 ' , readlink ($ this ->tmpDir . '/docker/openemr/current ' ));
100+ self ::assertSame (
101+ $ dockerfileBefore ,
102+ (string ) file_get_contents ($ dockerfilePath ),
103+ 'immutable per-version Dockerfile must be byte-for-byte unchanged ' ,
104+ );
105+ self ::assertSame (
106+ $ dependabotBefore ,
107+ (string ) file_get_contents ($ dependabotPath ),
108+ 'dependabot config must be byte-for-byte unchanged ' ,
109+ );
110+ }
111+
72112 public function testRotationRepointsSlotSymlinkAndLeavesOtherSlotsAlone (): void
73113 {
74114 $ rotator = new SlotRotator ($ this ->tmpDir , $ this ->registryPath );
@@ -197,6 +237,8 @@ public function testForwardRotationDoesNotCorruptOtherSlots(): void
197237 ],
198238 ]);
199239
240+ // Excluded per-version dirs must never be touched, regardless of which
241+ // slot rotates.
200242 $ currentPath = $ this ->tmpDir . '/docker/openemr/8.0.0/Dockerfile ' ;
201243 $ current = (string ) file_get_contents ($ currentPath );
202244 self ::assertStringContainsString ('--branch rel-800 ' , $ current , 'current slot Dockerfile must not change ' );
@@ -209,7 +251,8 @@ public function testForwardRotationDoesNotCorruptOtherSlots(): void
209251 public function testDryRunReturnsDiffWithoutWritingFiles (): void
210252 {
211253 $ rotator = new SlotRotator ($ this ->tmpDir , $ this ->registryPath );
212- $ before = (string ) file_get_contents ($ this ->tmpDir . '/docker/openemr/8.1.0/Dockerfile ' );
254+ $ batsPath = $ this ->tmpDir . '/.github/workflows/test-bats.yml ' ;
255+ $ before = (string ) file_get_contents ($ batsPath );
213256
214257 $ result = $ rotator ->rotate (
215258 [
@@ -224,9 +267,9 @@ public function testDryRunReturnsDiffWithoutWritingFiles(): void
224267 );
225268
226269 self ::assertFalse ($ result ->isNoOp ());
227- $ after = (string ) file_get_contents ($ this -> tmpDir . ' /docker/openemr/8.1.0/Dockerfile ' );
270+ $ after = (string ) file_get_contents ($ batsPath );
228271 self ::assertSame ($ before , $ after , 'dry-run must not touch the file ' );
229- self ::assertArrayHasKey ('docker/openemr/8.1.0/Dockerfile ' , $ result ->snapshots );
272+ self ::assertArrayHasKey ('.github/workflows/test-bats.yml ' , $ result ->snapshots );
230273 }
231274
232275 public function testUnknownSlotThrows (): void
@@ -239,7 +282,7 @@ public function testUnknownSlotThrows(): void
239282
240283 public function testMissingPinFileThrows (): void
241284 {
242- unlink ($ this ->tmpDir . '/docker/openemr/8.1.0/Dockerfile ' );
285+ unlink ($ this ->tmpDir . '/.github/workflows/test-bats.yml ' );
243286 $ rotator = new SlotRotator ($ this ->tmpDir , $ this ->registryPath );
244287
245288 $ this ->expectException (\RuntimeException::class);
@@ -258,8 +301,8 @@ public function testMissingPinFileThrows(): void
258301 public function testReplacementAvoidsPartialVersionMatches (): void
259302 {
260303 file_put_contents (
261- $ this ->tmpDir . '/docker/openemr/8.1.0/Dockerfile ' ,
262- "FROM alpine:3.21 \n ARG OPENEMR_VERSION =rel-810 \n# version-like-but-not: 8.1.10 should stay \n" ,
304+ $ this ->tmpDir . '/utilities/container_benchmarking/benchmark.sh ' ,
305+ "#!/bin/sh \n IMAGE=openemr/openemr:8.1.0 \n BRANCH =rel-810 \n# version-like-but-not: 8.1.10 should stay \n" ,
263306 );
264307 $ rotator = new SlotRotator ($ this ->tmpDir , $ this ->registryPath );
265308
@@ -272,7 +315,7 @@ public function testReplacementAvoidsPartialVersionMatches(): void
272315 ],
273316 ]);
274317
275- $ after = (string ) file_get_contents ($ this ->tmpDir . '/docker/openemr/8.1.0/Dockerfile ' );
318+ $ after = (string ) file_get_contents ($ this ->tmpDir . '/utilities/container_benchmarking/benchmark.sh ' );
276319 self ::assertStringContainsString ('8.1.10 ' , $ after , '8.1 inside 8.1.10 must NOT be rewritten ' );
277320 self ::assertStringContainsString ('rel-820 ' , $ after );
278321 }
@@ -282,15 +325,15 @@ public function testRotationLeavesScriptdirShellcheckDirectiveIntact(): void
282325 $ rotator = new SlotRotator ($ this ->tmpDir , $ this ->registryPath );
283326
284327 $ rotator ->rotate ([
285- 'current ' => [
328+ 'next ' => [
286329 'minor ' => '8.2 ' ,
287330 'full ' => '8.2.0 ' ,
288331 'branch ' => 'rel-820 ' ,
289332 'docker_dir ' => '8.2.0 ' ,
290333 ],
291334 ]);
292335
293- $ script = (string ) file_get_contents ($ this ->tmpDir . '/docker/openemr/8.0.0/openemr .sh ' );
336+ $ script = (string ) file_get_contents ($ this ->tmpDir . '/utilities/container_benchmarking/benchmark .sh ' );
294337 self ::assertStringContainsString (
295338 '# shellcheck source=SCRIPTDIR/env.stub ' ,
296339 $ script ,
@@ -302,7 +345,7 @@ public function testRotationLeavesScriptdirShellcheckDirectiveIntact(): void
302345 'rotation must never inject a version path into a shellcheck source directive ' ,
303346 );
304347 self ::assertStringContainsString (
305- "echo 'init for docker/openemr/8.2.0' " ,
348+ "echo 'benchmarking docker/openemr/8.2.0' " ,
306349 $ script ,
307350 'sanity: the genuine rotating docker_dir token should have been rewritten ' ,
308351 );
@@ -330,48 +373,71 @@ private function seedFixtures(): void
330373 branch: "master"
331374 docker_dir: "8.1.1"
332375
376+ # Only genuinely external files that track a slot are rotated. The
377+ # per-version build dirs and dependabot config are immutable; rotation
378+ # flips the docker/openemr/{current,next,dev} symlink, not their contents.
333379 files:
334- - path: docker/openemr/8.0.0/Dockerfile
335- slot: current
336- kinds: [docker_clone_branch]
337- - path: docker/openemr/8.0.0/openemr.sh
338- slot: current
339- kinds: [docker_dir_ref]
340- - path: docker/openemr/8.1.0/Dockerfile
380+ - path: .github/workflows/test-bats.yml
381+ slot: next
382+ kinds: [bats_test_paths]
383+ - path: utilities/container_benchmarking/benchmark.sh
341384 slot: next
342- kinds: [docker_arg_branch]
343- - path: docker/openemr/8.1.1/Dockerfile
344- slot: dev
345- kinds: [docker_arg_branch]
346- - path: docker/openemr/OVERVIEW.md
347- slot: all
348- kinds: [overview_table]
349-
350- excludes: []
385+ kinds: [benchmarking_default]
386+
387+ excludes:
388+ - path: docker/openemr/8.0.0
389+ reason: "immutable per-version build dir; never rotated"
390+ - path: docker/openemr/8.1.0
391+ reason: "immutable per-version build dir; never rotated"
392+ - path: docker/openemr/8.1.1
393+ reason: "immutable per-version build dir; never rotated"
394+ - path: .github/dependabot.yml
395+ reason: "one entry per build dir; never rotated"
351396 YAML);
352397
398+ // External slot-tracking files (in `files:`, next-slotted). These follow
399+ // the next slot when it rotates.
353400 $ this ->writeFile (
354- 'docker/openemr/8.0.0/Dockerfile ' ,
355- "FROM alpine:3.21 \n RUN git clone https://github.com/openemr/openemr.git --branch rel-800 --depth 1 \n" ,
401+ '.github/workflows/test-bats.yml ' ,
402+ "on: \n pull_request: \n paths: \n - 'docker/openemr/8.1.0/**' \n" ,
356403 );
357- $ this ->writeFile ('docker/openemr/8.1.0/Dockerfile ' , "FROM alpine:3.21 \nARG OPENEMR_VERSION=rel-810 \n" );
358- $ this ->writeFile ('docker/openemr/8.1.1/Dockerfile ' , "FROM alpine:3.21 \nARG OPENEMR_VERSION=master \n" );
359404
360- // In-container init script for the current slot. It carries a rotating
361- // docker_dir token (the path in the echo line) alongside a
362- // self-referential `SCRIPTDIR` shellcheck directive that must never be
363- // rewritten.
405+ // A benchmarking-style script carrying a rotating docker_dir token (the
406+ // echo path) and a self-referential SCRIPTDIR shellcheck directive that
407+ // must never be rewritten.
364408 $ this ->writeFile (
365- 'docker/openemr/8.0.0/openemr .sh ' ,
409+ 'utilities/container_benchmarking/benchmark .sh ' ,
366410 "#!/bin/sh \nset -e \n"
367411 . "# shellcheck source=SCRIPTDIR/env.stub \n. /root/env.stub \n"
368- . "echo 'init for docker/openemr/8.0.0' \n" ,
412+ . "echo 'benchmarking docker/openemr/8.1.0' \n"
413+ . "IMAGE=openemr/openemr:8.1.0 \n" ,
369414 );
370415
416+ // Immutable per-version build dirs (in `excludes:`). Seeded so tests can
417+ // assert rotation leaves them byte-for-byte unchanged.
371418 $ this ->writeFile (
372- 'docker/openemr/OVERVIEW.md ' ,
373- "| 8.0.0 | latest | \n | 8.1.0 | next | \n | 8.1.1 | dev | \n" ,
419+ 'docker/openemr/8.0.0/Dockerfile ' ,
420+ "FROM alpine:3.21 \n RUN git clone https://github.com/openemr/openemr.git --branch rel-800 --depth 1 \n" ,
374421 );
422+ $ this ->writeFile ('docker/openemr/8.1.0/Dockerfile ' , "FROM alpine:3.21 \nARG OPENEMR_VERSION=rel-810 \n" );
423+ $ this ->writeFile ('docker/openemr/8.1.1/Dockerfile ' , "FROM alpine:3.21 \nARG OPENEMR_VERSION=master \n" );
424+
425+ // Dependabot config (in `excludes:`). One entry per build dir, added at
426+ // dir-scaffold time, never renamed by rotation.
427+ $ this ->writeFile ('.github/dependabot.yml ' , <<<'YAML'
428+ version: 2
429+ updates:
430+ # Docker - docker/openemr/8.0.0
431+ - package-ecosystem: "docker"
432+ directory: "/docker/openemr/8.0.0"
433+ schedule:
434+ interval: "daily"
435+ # Docker - docker/openemr/8.1.0
436+ - package-ecosystem: "docker"
437+ directory: "/docker/openemr/8.1.0"
438+ schedule:
439+ interval: "daily"
440+ YAML);
375441
376442 // Slot symlinks: the source of truth the consolidated build workflow
377443 // resolves each slot's version from. Rotation re-points these (see
0 commit comments