Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/openemr/8.0.0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ services:
MYSQL_ROOT_PASSWORD: root
openemr:
restart: always
image: openemr/openemr:7.0.5
image: openemr/openemr:8.0.0
ports:
- 80:80
- 443:443
Expand Down
152 changes: 109 additions & 43 deletions tools/release/tests/SlotRotatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,17 @@ public function testRotationAdvancesNextSlotAndRewritesPinFiles(): void
]);

self::assertFalse($result->isNoOp(), 'Rotation should have changed files');
self::assertContains('docker/openemr/8.1.0/Dockerfile', $result->changedFiles);
// External slot-tracking files follow the next slot.
self::assertContains('.github/workflows/test-bats.yml', $result->changedFiles);
self::assertContains('utilities/container_benchmarking/benchmark.sh', $result->changedFiles);
self::assertContains('docker/openemr/next', $result->changedFiles);
self::assertContains('docker/openemr/OVERVIEW.md', $result->changedFiles);
self::assertContains('tools/release/versions.yml', $result->changedFiles);
// The per-version build dir is immutable: never rewritten by rotation.
self::assertNotContains('docker/openemr/8.1.0/Dockerfile', $result->changedFiles);

$dockerfile = (string) file_get_contents($this->tmpDir . '/docker/openemr/8.1.0/Dockerfile');
self::assertStringContainsString('ARG OPENEMR_VERSION=rel-820', $dockerfile);
self::assertStringNotContainsString('rel-810', $dockerfile);
$bats = (string) file_get_contents($this->tmpDir . '/.github/workflows/test-bats.yml');
self::assertStringContainsString('docker/openemr/8.2.0/**', $bats);
self::assertStringNotContainsString('docker/openemr/8.1.0/**', $bats);

self::assertSame(
'8.2.0',
Expand All @@ -69,6 +72,43 @@ public function testRotationAdvancesNextSlotAndRewritesPinFiles(): void
self::assertStringContainsString('branch: "rel-820"', $registry);
}

public function testCurrentRotationLeavesVersionDirAndDependabotUntouched(): void
{
$dockerfilePath = $this->tmpDir . '/docker/openemr/8.0.0/Dockerfile';
$dependabotPath = $this->tmpDir . '/.github/dependabot.yml';
$dockerfileBefore = (string) file_get_contents($dockerfilePath);
$dependabotBefore = (string) file_get_contents($dependabotPath);

$rotator = new SlotRotator($this->tmpDir, $this->registryPath);

$result = $rotator->rotate([
'current' => [
'minor' => '8.1',
'full' => '8.1.0',
'branch' => 'rel-810',
'docker_dir' => '8.1.0',
],
]);

// Rotating current flips only the symlink + registry.
self::assertContains('docker/openemr/current', $result->changedFiles);
self::assertContains('tools/release/versions.yml', $result->changedFiles);
self::assertNotContains('docker/openemr/8.0.0/Dockerfile', $result->changedFiles);
self::assertNotContains('.github/dependabot.yml', $result->changedFiles);

self::assertSame('8.1.0', readlink($this->tmpDir . '/docker/openemr/current'));
self::assertSame(
$dockerfileBefore,
(string) file_get_contents($dockerfilePath),
'immutable per-version Dockerfile must be byte-for-byte unchanged',
);
self::assertSame(
$dependabotBefore,
(string) file_get_contents($dependabotPath),
'dependabot config must be byte-for-byte unchanged',
);
}

public function testRotationRepointsSlotSymlinkAndLeavesOtherSlotsAlone(): void
{
$rotator = new SlotRotator($this->tmpDir, $this->registryPath);
Expand Down Expand Up @@ -197,6 +237,8 @@ public function testForwardRotationDoesNotCorruptOtherSlots(): void
],
]);

// Excluded per-version dirs must never be touched, regardless of which
// slot rotates.
$currentPath = $this->tmpDir . '/docker/openemr/8.0.0/Dockerfile';
$current = (string) file_get_contents($currentPath);
self::assertStringContainsString('--branch rel-800', $current, 'current slot Dockerfile must not change');
Expand All @@ -209,7 +251,8 @@ public function testForwardRotationDoesNotCorruptOtherSlots(): void
public function testDryRunReturnsDiffWithoutWritingFiles(): void
{
$rotator = new SlotRotator($this->tmpDir, $this->registryPath);
$before = (string) file_get_contents($this->tmpDir . '/docker/openemr/8.1.0/Dockerfile');
$batsPath = $this->tmpDir . '/.github/workflows/test-bats.yml';
$before = (string) file_get_contents($batsPath);

$result = $rotator->rotate(
[
Expand All @@ -224,9 +267,9 @@ public function testDryRunReturnsDiffWithoutWritingFiles(): void
);

self::assertFalse($result->isNoOp());
$after = (string) file_get_contents($this->tmpDir . '/docker/openemr/8.1.0/Dockerfile');
$after = (string) file_get_contents($batsPath);
self::assertSame($before, $after, 'dry-run must not touch the file');
self::assertArrayHasKey('docker/openemr/8.1.0/Dockerfile', $result->snapshots);
self::assertArrayHasKey('.github/workflows/test-bats.yml', $result->snapshots);
}

public function testUnknownSlotThrows(): void
Expand All @@ -239,7 +282,7 @@ public function testUnknownSlotThrows(): void

public function testMissingPinFileThrows(): void
{
unlink($this->tmpDir . '/docker/openemr/8.1.0/Dockerfile');
unlink($this->tmpDir . '/.github/workflows/test-bats.yml');
$rotator = new SlotRotator($this->tmpDir, $this->registryPath);

$this->expectException(\RuntimeException::class);
Expand All @@ -258,8 +301,8 @@ public function testMissingPinFileThrows(): void
public function testReplacementAvoidsPartialVersionMatches(): void
{
file_put_contents(
$this->tmpDir . '/docker/openemr/8.1.0/Dockerfile',
"FROM alpine:3.21\nARG OPENEMR_VERSION=rel-810\n# version-like-but-not: 8.1.10 should stay\n",
$this->tmpDir . '/utilities/container_benchmarking/benchmark.sh',
"#!/bin/sh\nIMAGE=openemr/openemr:8.1.0\nBRANCH=rel-810\n# version-like-but-not: 8.1.10 should stay\n",
);
$rotator = new SlotRotator($this->tmpDir, $this->registryPath);

Expand All @@ -272,7 +315,7 @@ public function testReplacementAvoidsPartialVersionMatches(): void
],
]);

$after = (string) file_get_contents($this->tmpDir . '/docker/openemr/8.1.0/Dockerfile');
$after = (string) file_get_contents($this->tmpDir . '/utilities/container_benchmarking/benchmark.sh');
self::assertStringContainsString('8.1.10', $after, '8.1 inside 8.1.10 must NOT be rewritten');
self::assertStringContainsString('rel-820', $after);
}
Expand All @@ -282,15 +325,15 @@ public function testRotationLeavesScriptdirShellcheckDirectiveIntact(): void
$rotator = new SlotRotator($this->tmpDir, $this->registryPath);

$rotator->rotate([
'current' => [
'next' => [
'minor' => '8.2',
'full' => '8.2.0',
'branch' => 'rel-820',
'docker_dir' => '8.2.0',
],
]);

$script = (string) file_get_contents($this->tmpDir . '/docker/openemr/8.0.0/openemr.sh');
$script = (string) file_get_contents($this->tmpDir . '/utilities/container_benchmarking/benchmark.sh');
self::assertStringContainsString(
'# shellcheck source=SCRIPTDIR/env.stub',
$script,
Expand All @@ -302,7 +345,7 @@ public function testRotationLeavesScriptdirShellcheckDirectiveIntact(): void
'rotation must never inject a version path into a shellcheck source directive',
);
self::assertStringContainsString(
"echo 'init for docker/openemr/8.2.0'",
"echo 'benchmarking docker/openemr/8.2.0'",
$script,
'sanity: the genuine rotating docker_dir token should have been rewritten',
);
Expand Down Expand Up @@ -330,48 +373,71 @@ private function seedFixtures(): void
branch: "master"
docker_dir: "8.1.1"

# Only genuinely external files that track a slot are rotated. The
# per-version build dirs and dependabot config are immutable; rotation
# flips the docker/openemr/{current,next,dev} symlink, not their contents.
files:
- path: docker/openemr/8.0.0/Dockerfile
slot: current
kinds: [docker_clone_branch]
- path: docker/openemr/8.0.0/openemr.sh
slot: current
kinds: [docker_dir_ref]
- path: docker/openemr/8.1.0/Dockerfile
- path: .github/workflows/test-bats.yml
slot: next
kinds: [bats_test_paths]
- path: utilities/container_benchmarking/benchmark.sh
slot: next
kinds: [docker_arg_branch]
- path: docker/openemr/8.1.1/Dockerfile
slot: dev
kinds: [docker_arg_branch]
- path: docker/openemr/OVERVIEW.md
slot: all
kinds: [overview_table]

excludes: []
kinds: [benchmarking_default]

excludes:
- path: docker/openemr/8.0.0
reason: "immutable per-version build dir; never rotated"
- path: docker/openemr/8.1.0
reason: "immutable per-version build dir; never rotated"
- path: docker/openemr/8.1.1
reason: "immutable per-version build dir; never rotated"
- path: .github/dependabot.yml
reason: "one entry per build dir; never rotated"
YAML);

// External slot-tracking files (in `files:`, next-slotted). These follow
// the next slot when it rotates.
$this->writeFile(
'docker/openemr/8.0.0/Dockerfile',
"FROM alpine:3.21\nRUN git clone https://github.com/openemr/openemr.git --branch rel-800 --depth 1\n",
'.github/workflows/test-bats.yml',
"on:\n pull_request:\n paths:\n - 'docker/openemr/8.1.0/**'\n",
);
$this->writeFile('docker/openemr/8.1.0/Dockerfile', "FROM alpine:3.21\nARG OPENEMR_VERSION=rel-810\n");
$this->writeFile('docker/openemr/8.1.1/Dockerfile', "FROM alpine:3.21\nARG OPENEMR_VERSION=master\n");

// In-container init script for the current slot. It carries a rotating
// docker_dir token (the path in the echo line) alongside a
// self-referential `SCRIPTDIR` shellcheck directive that must never be
// rewritten.
// A benchmarking-style script carrying a rotating docker_dir token (the
// echo path) and a self-referential SCRIPTDIR shellcheck directive that
// must never be rewritten.
$this->writeFile(
'docker/openemr/8.0.0/openemr.sh',
'utilities/container_benchmarking/benchmark.sh',
"#!/bin/sh\nset -e\n"
. "# shellcheck source=SCRIPTDIR/env.stub\n. /root/env.stub\n"
. "echo 'init for docker/openemr/8.0.0'\n",
. "echo 'benchmarking docker/openemr/8.1.0'\n"
. "IMAGE=openemr/openemr:8.1.0\n",
);

// Immutable per-version build dirs (in `excludes:`). Seeded so tests can
// assert rotation leaves them byte-for-byte unchanged.
$this->writeFile(
'docker/openemr/OVERVIEW.md',
"| 8.0.0 | latest |\n| 8.1.0 | next |\n| 8.1.1 | dev |\n",
'docker/openemr/8.0.0/Dockerfile',
"FROM alpine:3.21\nRUN git clone https://github.com/openemr/openemr.git --branch rel-800 --depth 1\n",
);
$this->writeFile('docker/openemr/8.1.0/Dockerfile', "FROM alpine:3.21\nARG OPENEMR_VERSION=rel-810\n");
$this->writeFile('docker/openemr/8.1.1/Dockerfile', "FROM alpine:3.21\nARG OPENEMR_VERSION=master\n");

// Dependabot config (in `excludes:`). One entry per build dir, added at
// dir-scaffold time, never renamed by rotation.
$this->writeFile('.github/dependabot.yml', <<<'YAML'
version: 2
updates:
# Docker - docker/openemr/8.0.0
- package-ecosystem: "docker"
directory: "/docker/openemr/8.0.0"
schedule:
interval: "daily"
# Docker - docker/openemr/8.1.0
- package-ecosystem: "docker"
directory: "/docker/openemr/8.1.0"
schedule:
interval: "daily"
YAML);

// Slot symlinks: the source of truth the consolidated build workflow
// resolves each slot's version from. Rotation re-points these (see
Expand Down
49 changes: 16 additions & 33 deletions tools/release/versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,33 +54,6 @@ slots:
# container_func_test_paths — workflow path filter + script arg
# benchmarking_default — utilities/container_benchmarking default variant
files:
- path: docker/openemr/8.0.0/Dockerfile
slot: current
kinds: [docker_arg_branch]

- path: docker/openemr/8.1.0/Dockerfile
slot: next
kinds: [docker_arg_branch]

- path: docker/openemr/8.1.1/Dockerfile
slot: dev
kinds: [docker_arg_branch]

- path: docker/openemr/8.0.0/README.md
slot: current
kinds: [docker_image_tag]
# FIXME: line 33 currently reads `image: openemr/openemr:7.0.5` — a stale
# copy-paste from before 8.0.0 was tagged. Rotator should overwrite to
# match slot.full on next run; flagged in PR #665 for human confirmation.

- path: docker/openemr/8.1.0/README.md
slot: next
kinds: [docker_image_tag]

- path: docker/openemr/8.1.1/README.md
slot: dev
kinds: [docker_image_tag]

- path: .github/workflows/test-bats.yml
slot: next
kinds: [bats_test_paths]
Expand All @@ -105,12 +78,6 @@ files:
slot: next
kinds: [benchmarking_default]

# Dependabot enumerates each rotating Docker dir explicitly; rotation needs
# to add/remove entries when slot dirs change.
- path: .github/dependabot.yml
slot: all
kinds: [dependabot_directory]

# Container-benchmarking docs reference the next-slot version as defaults.
- path: utilities/container_benchmarking/README.md
slot: next
Expand All @@ -126,6 +93,22 @@ excludes:
- path: docker/openemr/7.0.4
reason: "legacy 7.0.4 support track (entire dir)"

# Dependabot enumerates one entry per build dir; entries are added at
# dir-scaffold time and never renamed by rotation (rotation flips the
# docker/openemr/{current,next,dev} symlink, not the per-dir config).
- path: .github/dependabot.yml
reason: "one entry per build dir, added at dir-scaffold time; never rotated"

# Immutable per-version build dirs (entire dir). Each is pinned to its release
# branch and never rotated — slot rotation flips the
# docker/openemr/{current,next,dev} symlink instead.
- path: docker/openemr/8.0.0
reason: "immutable per-version build dir; pinned to its release branch, never rotated — slot rotation flips the docker/openemr/{current,next,dev} symlink instead"
Comment thread
kojiromike marked this conversation as resolved.
- path: docker/openemr/8.1.0
reason: "immutable per-version build dir; pinned to its release branch, never rotated — slot rotation flips the docker/openemr/{current,next,dev} symlink instead"
- path: docker/openemr/8.1.1
reason: "immutable per-version build dir; pinned to its release branch, never rotated — slot rotation flips the docker/openemr/{current,next,dev} symlink instead"

# Frozen historical builds.
- path: docker/openemr/obsolete
reason: "frozen historical builds; never rotate"
Expand Down