LDM Testing Protocol¶
π‘οΈ Compatibility (Verified Environments)¶
| Architecture | Host OS | Docker Provider | Docker Engine | Hardening | LDM Version | Verified | Report |
|---|---|---|---|---|---|---|---|
| Apple Silicon | macOS 26 Tahoe | Colima | 29.2.1 |
2.21.1-pre.4 |
β | verify-apple-silicon-macos-26-tahoe-colima-pass.txt | |
| Apple Silicon | macOS 26 Tahoe | OrbStack | 29.4.0 |
2.26.5-pre.5 |
β | verify-apple-silicon-macos-26-tahoe-orbstack-pass.txt | |
| Apple Silicon | macOS 27 Golden Gate | Colima | 29.2.1 |
2.26.7-pre.6 |
β | verify-apple-silicon-macos-27-golden-gate-colima-pass.txt | |
| Apple Silicon | macOS 27 Golden Gate | OrbStack | 29.4.0 |
2.26.7-pre.6 |
β | verify-apple-silicon-macos-27-golden-gate-orbstack-pass.txt | |
| Linux Workstation | Alpine 3.24 (CI) | Native Docker | 28.0.4 |
2.26.7 |
β | verify-linux-workstation-alpine-3.24-ci-native-docker-pass.txt | |
| Linux Workstation | Debian 12 (CI) | Native Docker | 28.0.4 |
2.26.7 |
β | verify-linux-workstation-debian-12-ci-native-docker-pass.txt | |
| Linux Workstation | Fedora 44 | Native Docker | 29.9.1 |
2.26.7-pre.6 |
β | verify-linux-workstation-fedora-44-native-docker-pass.txt | |
| Linux Workstation | Fedora 44 (CI) | Native Docker | 28.0.4 |
2.26.7 |
β | verify-linux-workstation-fedora-44-ci-native-docker-pass.txt | |
| Linux Workstation | Rocky Linux 9.3 (CI) | Native Docker | 28.0.4 |
2.26.7 |
β | verify-linux-workstation-rocky-linux-9.3-ci-native-docker-pass.txt | |
| Linux Workstation | Ubuntu 24.04 (CI) | Native Docker | 28.0.4 |
2.26.7 |
β | verify-linux-workstation-ubuntu-24.04-ci-native-docker-pass.txt | |
| Windows PC | Windows 11 | Native WSL2 WSL 3.0.3.0 |
29.3.0 |
2.26.7-pre.6 |
β | verify-windows-pc-windows-11-native-wsl2-pass.txt | |
| Windows PC | Windows 11 PowerShell 5.1 | Docker Desktop | 29.4.0 |
2.26.7-pre.6 |
β | verify-windows-pc-windows-11-powershell-5.1-docker-desktop-pass.txt | |
| Windows PC | Windows 11 PowerShell 7 | Docker Desktop | 29.4.0 |
2.26.7-pre.6 |
β | verify-windows-pc-windows-11-powershell-7-docker-desktop-pass.txt |
Global Infrastructure¶
| Component | Verified Versions | Notes |
|---|---|---|
| Traefik | v3.6.1+ |
Automatic API version negotiation enabled. |
| Elasticsearch | 8.19.1, 7.17.24 |
Dual support. ES 8.17.x+ required for Liferay 2025.Q2+ (ES 7 deprecated). |
Phase 1: Tool & Security Readiness¶
π€ Automated (CI / E2E)¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 1.2 | Exit Code Integrity | ldm doctor --skip-project; echo $? |
Returns 0 if healthy, 1 if critical. |
| 1.3 | Shell Completion | ldm completion zsh |
Generates a valid script. |
| 1.5 | Self-Repair | ldm upgrade --repair -y |
Successfully reaches preparation phase. |
π οΈ Manual¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 1.1 | No-Sudo Guard | sudo ldm run |
Command blocked with security warning. |
| 1.4 | Native Manual | man ldm (Run ldm init-common first) |
Manual page opens correctly. |
| 1.6 | Version Management | LDM_DEV_MODE=true ldm version --bump beta -y |
Increments beta version atomically. |
| 1.7 | Version Promotion | LDM_DEV_MODE=true ldm version --promote -y |
Prompts for promotion; results in stable. |
| 1.8 | Dev Guardrails | ldm version --bump patch |
Blocks execution with safety warning. |
| 1.9 | Safety Hatch | ldm upgrade (Run while on beta) |
Prompts to switch back to stable tier. |
Scheduled Monitors (Upstream Data)¶
Some invariants depend on data LDM does not own β the tag families published to
liferay/dxp and liferay/portal. These cannot be asserted in the unit suite (every
test there mocks the registry, by design) and must not gate pull requests, because they
fail when upstream changes rather than when the diff does.
| Monitor | Runs | Asserts | On failure |
|---|---|---|---|
scripts/tag_discovery_canary.py( .github/workflows/tag-discovery-canary.yml) |
Mondays 06:00 UTC, plus manual dispatch | Every advertised release channel resolves to a tag matching LDM's own patterns; the resolved tag is one the registry still serves (not withdrawn, not a name LDM mis-derived); no tag family has outgrown TAG_DISCOVERY_MAX_PAGES |
Opens β or comments on β a single issue labelled tag-discovery-canary, and closes it when the invariants hold again |
Run it by hand against the live registry at any time:
It writes nothing outside a temporary directory: LDM_HOME is redirected before
ldm_core is imported, so neither the real ~/.ldm nor the 24-hour tag cache is
touched.
Deliberately no assertions on specific tag values β those change every few weeks, and a canary that needs updating on every Liferay release is a canary that gets switched off. The sweep-headroom check warns from 70% of the ceiling, because the families only ever grow and that warning is the only advance notice anyone gets (LDM-#1650).
Phase 2: Global Infrastructure¶
π€ Automated (E2E)¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 2.1 | Infra Setup | ldm infra-setup --search |
Starts Traefik and ES8. Idempotent. |
| 2.2 | Shared DB Mode | ldm init <Name> --no-up --no-seed --database-mode shared --db postgresql |
Valid compose (no depends_on on an undefined service), JDBC URL targets liferay-db-global, derived database name is lowercase, database_mode persisted to meta. |
| 2.3 | Shared DB Mode (MySQL) | ldm init <Name> --no-up --no-seed --database-mode shared --db mysql |
Succeeds. JDBC URL targets liferay-db-mysql-global:3306 -- not liferay-db-global, which would aim a MariaDB driver at the PostgreSQL container -- derived database name is lowercase, no depends_on on an undefined service. |
| 2.4 | Shared Search Mode | ldm init <Name> --no-up --no-seed --search-mode shared |
search_mode persisted to meta; an ElasticsearchConfiguration.config written under osgi/configs with productionModeEnabled, the global cluster address and a lowercase indexNamePrefix; that directory mounted into the container. |
π οΈ Manual¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 2.2 | Infra Restart | ldm infra-restart --search |
Restarts Traefik and ES8 cleanly. |
| 2.3 | DNS Alignment | Edit /etc/hosts with wrong IP |
ldm doctor warns about IP mismatch. |
| 2.4 | Auto-Healing DNS | ldm fix-hosts broken.local |
Prompts for sudo; fixes the entry. |
| 2.5 | Doctor DNS Fix | ldm doctor --fix-hosts |
Batch fixes all missing entries. |
| 2.6 | DNS Cleanup (Surg) | ldm rm test-dns --clean-hosts |
Removes specific project host entries. |
| 2.7 | DNS Cleanup (Glob) | ldm prune --clean-hosts |
Removes ALL LDM-managed host entries. |
| 2.8 | External DB Mode | ldm init <Name> --no-up --no-seed --db postgresql --database-mode external (answer the JDBC wizard) |
No <Name>-db service in the compose file and no depends_on naming one. portal-ext.properties carries the supplied URL/user/password plus jdbc.default.driverClassName and hibernate.dialect. meta records db_type: postgresql, database_mode: external. |
| 2.9 | Legacy --db external |
ldm init <Name> --no-up --no-seed --db external (supply a jdbc:mysql:// URL) |
Still succeeds. meta is migrated to db_type: mysql, database_mode: external; a MySQL/MariaDB driver and dialect are written. |
| 2.10 | Impossible pairings refused | ldm init <Name> --db hypersonic --database-mode shared; ldm init <Name> --db external --database-mode shared; ldm init <Name> --db postgresql --database-mode embedded |
Each exits 1 with a message naming both values. Nothing is created. |
Phase 3: Project Lifecycle¶
π€ Automated (E2E)¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 3.2 | Effective Names | ldm info <non-ASCII name> |
Heading shows the verbatim project name; every Liferay:/Database:/Tunnel: row shows the transcoded name Docker holds, and none shows the verbatim form. |
| 3.3 | Fresh Project Run | ldm run test-run -y --tag-latest |
Starts a fresh project from seed. |
| 3.4 | Hot Deploy (OSGi) | Drop test-bundle.jar into deploy/ |
Verifies deployment via Gogo shell (lb). |
π Standalone UI Testing¶
UI health checks have been decoupled from the strict binary E2E suite to prevent false negatives caused by host VM rendering delays.
- Run manually:
python3 scripts/test_ui.py(Verifies portal login and Control Panel navigation).
π οΈ Manual¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 3.1 | Explicit Init | ldm init test-init -y --tag-latest |
Scaffolds folders/metadata immediately. |
| 3.2 | Missing Tag Guard | ldm run test-fail -y (no tag) |
Fails gracefully with "No tag specified". |
| 3.4 | Project Collision | ldm init test-init (in different dir) |
Blocks; identifies original path. |
| 3.5 | Hostname Collision | ldm run --host-name existing.local |
Blocks execution due to registry conflict. |
| 3.6 | Captcha Switch | ldm init test-captcha --no-captcha |
Generates OSGi config and portal property to disable CAPTCHA. Reversible by running without the flag. |
| 3.7 | Fast Login Switch | ldm run test-fast --fast-login |
Applies properties to bypass terms of use and password reset prompts. Warns if used with --db hypersonic. |
| 3.8 | Feature Flags Switch | ldm run test-feature --feature dev LPS-122920 |
Generates portal properties to enable specific Liferay feature flags. |
| 3.10 | License Discovery | Drop .xml into deploy/ |
Doctor identifies the XML as a license. |
| 3.10 | Sample Hydration | ldm init test-samples --samples |
Scaffolds project and populates samples. |
| 3.11 | Import Integrity | ldm import source.zip |
Verifies source.zip.sha256 before extraction. |
Phase 4: Runtime Configuration & UX¶
π€ Automated (E2E)¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 4.5 | Fail-Fast Logic | Delete docker-compose.yml |
ldm logs stops with "Not a project". |
π οΈ Manual¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 4.1 | Env Sync | ldm env test-init KEY=VAL |
YAML updated immediately without run. |
| 4.2 | Redaction Check | ldm -v run test-init (w/ secret) |
Secret is masked as [REDACTED] in logs. |
| 4.3 | WSL Browser | ldm run test-init (WSL Only) |
Opens host browser without UNC errors. |
| 4.4 | Intermixed Flags | ldm ps -y test-init |
Global -y recognized after subcommand. |
Phase 5: Data Integrity & Recovery¶
π€ Automated (E2E)¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 5.1 | SHA-256 Generation | ldm snapshot test-init |
File contains valid hash of files.tar.gz. |
| 5.2 | SHA-256 Verify | ldm restore test-init |
Verify logs show "Integrity verified". |
π οΈ Manual¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 5.3 | Corruption Guard | Corrupt files.tar.gz manually |
ldm restore fails with integrity error. |
| 5.4 | Project Reset | ldm reset state test-init |
Clears state while container is stopped. |
| 5.5 | Verification Bypass | ldm restore test-init --no-verify |
Restores tampered snapshot without error. |
Phase 6: Advanced Integrations¶
π οΈ Manual¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 6.1 | Multi-Node Scaling | ldm scale test-init liferay=2 |
Disables host-mapping; injects cluster. |
| 6.2 | Search Migration | ldm migrate-search |
Reconfigures ES settings automatically. |
| 6.3 | Cloud Env Sync | ldm cloud-fetch project-id |
Fetches and merges remote variables. |
Phase 7: Cleanup & Pruning¶
π€ Automated (E2E)¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 7.1 | Registry Cleanup | ldm down test-init --delete |
Project removed from the global registry. |
π οΈ Manual¶
| ID | Test Case | Steps | Expected Outcome |
|---|---|---|---|
| 7.2 | SSL Hygiene | Check ~/.ldm/infra/proxy/ |
Configs removed after project teardown. |
| 7.3 | Non-Interactive Prune | ldm prune -y |
Silently removes orphaned containers. |
| 7.4 | Self-Healing Reg | Delete project folder manually | Dead path detected and pruned from registry. |
π§ͺ Unit Testing & Mocking Guidelines¶
To prevent test-runner hangs, memory exhaustion, and side-effect leakage in CI pipelines, follow these guidelines when writing unit tests:
- Avoid Global built-in / Path Mocking:
- Do not globally mock classes or standard libraries like
pathlib.Path.exists,pathlib.Path.read_text,builtins.open, orshutil.rmtree. - Patching standard library methods globally can corrupt Python's internal mechanisms (such as timezone updates in
time.strftimeor mock tracking inunittest.mock) and lead to infinite recursion. - Prefer Real Filesystem Sandboxing:
- Use
tempfile.TemporaryDirectoryto create actual sandbox environments for tests. - Let LDM interact with real, lightweight files on disk. The cleanups inside LDM and
TemporaryDirectorycontexts will automatically ensure that no files are left behind. - Use Mock Side-Effects for Specific Interceptions:
- If files need to be simulated, write a side effect for a specific dependency (like mocking
safe_extractto write dummy meta files directly to the temporary directory). - All Test Files MUST Live Under
ldm_core/tests/: testpathsinpyproject.tomland the explicitpython -m pytest ldm_core/tests/invocation in.github/workflows/ci.ymlboth scan that single directory. A test file placed anywhere else is silently never executed β it does not fail, it simply never runs.- This applies even to tests covering code outside the package. Tests for
scripts/modules belong here too: seeldm_core/tests/test_sync_compatibility.pyandtest_manage_target_nodes.py, which reach their target viasys.path.insert(0, <repo>/scripts)and then import the module by its bare name. -
test_no_test_files_outside_suite_directoryinldm_core/tests/test_architectural_contracts.pyenforces this, and fails the suite if a test-defining file appears elsewhere. Standalone scripts that merely look like tests (e.g.scripts/test_ui.py, a manual Playwright driver) are exempt, because they define no collectable test functions or classes. -
Never Touch the Developer's Real
~/.ldm: - The
isolate_ldm_homeautouse fixture inldm_core/tests/conftest.pypointsLDM_HOMEat a fresh temporary directory for every test. Do not remove it, and do not write tests that depend on the real home. - Before it existed, a full run registered pytest tempdirs as real projects β entries like
tmp58psgp9wandtest-projectappeared in the developer'sldm listβ overwrote~/.ldm/last-command.log(destroying the trace needed to diagnose whatever was last run), and deleted real registry entries whose paths no longer resolved. LDM_HOMEis the only lever available:get_actual_home()rebuilds/Users/<user>fromSUDO_USER/USERon macOS and ignoresHOMEentirely.- Tests that patch
get_actual_homethemselves are unaffected β a patch replaces the function, so it never consults the environment. - This isolates the filesystem only. Docker is covered separately, by the guard below.
-
test_suite_never_touches_the_developers_real_ldm_homeinldm_core/tests/test_architectural_contracts.pyenforces that the fixture is in force. -
Never Touch the Machine's Docker (LDM-#1409):
- The
block_real_dockerautouse fixture inldm_core/tests/conftest.pyfails any test that reaches the real daemon, naming the test and the offending command. The boundary is enforced, not merely documented. - It was measured before it existed: 117 Docker invocations from 74 tests in one run. Among them a unit test running
docker execinside the developer's ownliferay-search-global, four doingdocker rm -f liferay-proxy-global, one creating a realwslDocker context pointing atssh://dev@192.168.1.10, and thedocker run --rm -v <pytest tmpdir> alpinecalls that leakedCreatedcontainers holding bind mounts on deleted paths (docker system prunedoes not collect those). - The hook is at the
subprocessboundary, not atCommandRunner. Only 81 of those 117 calls went throughCommandRunner.run; the rest come from the ~20 places inldm_core/{snapshot,runtime,workspace}that callsubprocessdirectly. A guard on the wrapper alone polices two thirds of the problem and reports success. - It intercepts rather than mocks β anything that is not a Docker argv is passed straight through to the real function, so the warning above about globally replacing stdlib machinery still holds and is not violated here.
- Companion stubs remove the bulk of the traffic at its source:
stub_docker_environment_probesanswersget_compose_cmd()andget_docker_socket_path()without asking the machine and neutralisesreclaim_volume_permissions()(which shells out toalpine chown/chmod), andDockerService's module-scoperun_commandis stubbed so the whole facade (is_running/exists/stop/rm/inspect/β¦) cannot reach a daemon. -
A test that genuinely needs Docker marks itself
@pytest.mark.needs_dockerand is allowed through.ldm_core/tests/test_e2e_interactive.pyis the only such suite today; it drives the real CLI as a subprocess. Run without them: -
Three traps this repeatedly walked into, worth knowing before adding a patch:
DockerService's statics call a module-scoperun_commandimported indocker_service.py, sopatch.object(self.manager, "run_command")never reaches them. That is LDM-#1365; ten more instances of it survived intest_infra.pyalone.- Some handlers import inside the function body (
cmd_target_statusdoesfrom ldm_core.utils import run_command), which re-resolves the name at call time and ignores a patch on the handler module's attribute.test_cmd_target_status_offlineread as fully mocked while running a realdocker info. - A class with two
setUpdefinitions silently uses the second one. Adding a patcher to the first has no effect at all. - A platform-gated Docker call is invisible to a single-platform measurement.
pipelines/run.pycallsreclaim_volume_permissions()only whenplatform.system() == "linux", so a macOS run of this suite measured zero daemon calls through it while Linux CI hit it from three tests. The 117-call baseline above is a macOS number and the Linux one is higher. When auditing for side effects, grep forplatform.system()around the call path before trusting a clean local run β or force the branch, asreclaim_volume_permissionswas verified: patchplatform.systemto"Linux"for the offending tests, confirm the guard fires, then confirm the stub silences it.
-
Own Your Scratch Paths (LDM-#1402):
- Eight suites built their
pathsfixtures from the constant/tmp/proj, and let the code under test write into it. Two concurrent runs on one machine therefore raced, and one deleted a file the other was about to read β a failure indistinguishable from a real regression in whatever diff was under test. - They now derive from
TEST_TMP_ROOTinldm_core/tests/tmproot.py, unique per pytest process. - If you patch
get_actual_homeand set a project path, keep the project under that home._check_global_config_and_networkdoestest_path.relative_to(get_actual_home())and silently skips its whole volume check when the two are unrelated.
π Local E2E Platform Verification Scripts (Multi-OS)¶
Console encoding on Windows¶
Windows PowerShell 5.1 defaults to a non-UTF-8 console code page, and that
flattens non-ASCII output to ? β including in the saved report, which is
the project's durable record of what was tested. Measured on
v2.18.0-pre.11: the Windows report contained zero non-ASCII bytes.
Three settings are needed, at two different layers, and only the first two were present until LDM-#1465:
| Setting | Layer |
|---|---|
$env:PYTHONUTF8 = 1 |
what the child process encodes in |
[Console]::OutputEncoding |
how PowerShell decodes what the child wrote |
chcp 65001 |
the console code page the child writes into |
The third is the one that was missing. On a non-UTF-8 code page the Windows console can substitute unrepresentable characters at write time, before PowerShell decodes anything β so no amount of decoding configuration recovers them.
The script restores the previous code page on exit, since chcp changes the
user's console rather than just the script's view of it.
Console encoding and the verbatim-name check¶
The non-ASCII naming check asserts that ldm info echoes the name the user
typed. That assertion depends on the console being able to carry the name,
and on Windows PowerShell 5.1 it frequently cannot (LDM-#1452).
[Console]::OutputEncoding = UTF8 and PYTHONUTF8=1 are already set near the
top of the PowerShell script, and they are not sufficient. Measured against
v2.18.0-pre.11: the saved report contained zero non-ASCII bytes β box
borders, status glyphs and project names alike had been flattened to ? before
anything could compare them.
ldm list --json passed for the same three names in that same run, so LDM does
store and report them correctly. The failure was the console, not the product;
asserting against a rendering that cannot represent the value is measuring the
terminal.
Both scripts now test whether the console can round-trip the name and skip visibly when it cannot β never silently, for the same reason as the other "refusing to skip silently" guards here. The JSON assertion still runs and still proves the contract.
This is why the surrounding checks parse --json rather than the rendered
table: parse the data, do not string-match a rendering of it.
Disk accounting¶
Every run reports what it cost (LDM-#1438):
Disk exhaustion broke verification on three platforms during the v2.18.0 cycle, on machines whose only workload was these scripts β and it was invisible until a run died at 100% capacity. Free space was printed once, at the start, and never mentioned again, so a run that consumed 4 GB and reclaimed none looked identical to one that cleaned up perfectly.
A run that reclaims more than it consumes says so, rather than printing a negative.
Note
Until LDM-#2238 that line reached the console and never the report. The
finalizer renames the report before this point, and the write still named
the old path; tee -a and Out-File -Append both create a missing file, so
the text landed in a dotfile nothing reads. Measured across the 224 reports
in references/verification-results/, Disk: this run consumed appeared in
none of them β including the three v2.18.0 platforms the figure exists to
explain. The same slip hid the LDM-#2084 built-image sweep, the LDM-#2214
reap and the LDM-#2085 pre-flight refusal, so a run refused for running
beside a live project wrote a -fail report naming no cause. Everything
after finalisation now goes through trap_report / Write-TrapNote, which
resolve the live destination.
--prune-after is available for machines dedicated to verification. It runs
docker system prune -af --volumes after the project teardown, so it only
reclaims what nothing else owns, and reports what it freed. It is opt-in and
never the default: images are shared between projects and expensive to
re-pull, the same reasoning LDM-#1414 used to exclude them from project
teardown. It cannot be combined with --keep, which preserves the artefacts it
would remove.
The #1345 diagnosis check needs an ssh client¶
The unreachable-node check works by genuinely attempting and genuinely failing
an SSH connection β the only way to exercise
diagnose_remote_context_failure, because the thing under test is the
failure.
With no ssh on PATH, Docker's connection helper fails to invoke it rather
than failing to connect. The stderr is executable file not found, so neither
the phrase table nor the connect to host <h> port <p> regex matches, and LDM
falls back to its generic "could not be reached" β correct behaviour for an
unrecognised failure, which the assertion then reads as a regression.
Observed on Alpine 3.24.1, which ships no ssh client, against
v2.18.0-pre.11. The signature is both parsers missing at once and --user
absent from the tip: that is a failure which was never an SSH failure.
Both scripts now skip the check visibly when no ssh client is present (LDM-#1444). An ssh client is a dependency the scripts do not control, which is exactly the principle LDM-#1383 set out β and the check was verified on macOS only, the same single-platform blind spot that produced LDM-#1425.
Port-conflict diagnostics¶
When a port check cannot proceed, both scripts name what holds the port (LDM-#1428) rather than aborting with only the port number. The output goes to the durable report, not just the console.
Two sources are queried, and both are needed. Neither can answer the question alone:
| Source | Answers |
|---|---|
docker ps --filter publish=<port> |
which container holds it β the only source that can |
lsof / ss / netstat / Get-NetTCPConnection |
which host process holds it, when no container does |
A container-published port is held on the host by the runtime's forwarder, which never identifies the container:
| Runtime | What the native tool names |
|---|---|
| Colima / Lima | ssh (the Lima SSH mux β it forwards ports over SSH) |
| Docker Desktop (macOS) | com.docker.backend |
| Docker Desktop / WSL2 | wslrelay / vpnkit |
| Native Linux | docker-proxy |
Measured on Colima with a container publishing 5601, lsof reports ssh while
docker ps reports the container. Printing only the lsof line would send the
operator chasing a process that is working perfectly, so a recognised forwarder
is labelled as such and the reader is pointed back at the container.
It fires at three points: before the check starts (a foreign listener would make the assertion pass for the wrong reason), when the holder cannot be created, and when LDM exits with the wrong code β that last one distinguishes "LDM failed to detect a real conflict" from "the fixture never held the port".
Disk space pre-flight¶
Both scripts refuse to start unless Docker has room to finish (LDM-#1406). The
default floor is 15 GB; override with LDM_VERIFY_MIN_DISK_GB.
The floor was 10 GB until LDM-#1430, and the gate is -lt, so a machine
with exactly 10 GB passed β then died mid-snapshot with ENOSPC, on a run where
ldm prune had reclaimed 2.957 GB immediately beforehand. The images alone are
~7.5 GB (liferay/dxp ~5.3, postgres ~0.7, elasticsearch ~1.5) before the
running stack grows; 10 GB covered the pull and nothing after it.
The check asks both Docker and the host (LDM-#1435). Neither is sufficient alone:
| View | Catches | Misses |
|---|---|---|
Docker (docker run alpine df /) |
the VM's own smaller disk | host exhaustion |
Host (df on the engine's storage path) |
a full host volume | the VM limit |
Docker's disk is usually a sparse image on the host filesystem, so the space
it reports is a promise the host may be unable to keep. Measured on a developer
machine at one moment: Docker reported 77.9 GB free while the host volume
had 2.8 GB at 100% capacity. The pre-flight passed and the run died with
ENOSPC mid-snapshot.
The host side measures the volume backing the engine, not $HOME: storage
is often relocated. On one machine ~/.colima is a symlink to an external
drive, where the home volume showed 154 GB free and the volume Docker actually
uses showed 480 GB β checking $HOME there would fail a run with ample space.
The Docker side asks Docker, not the host:
On Docker Desktop, Colima and OrbStack the engine's storage lives inside a VM with its own, far smaller disk. Measured on a developer machine mid-verification:
| View | Free |
|---|---|
Host (df /) |
109.2 GB |
| Docker VM | 12.5 GB |
A host-side check would have waved that run through. This is the same reasoning
as Doctor._check_absolute_disk_space (LDM-#1095), and using a throwaway
container keeps the .sh and .ps1 implementations identical rather than
needing two host-specific ones.
The check runs twice (LDM-#1430). A single up-front check cannot cover a run
whose disk usage peaks late: between the pre-flight and the snapshot the run
pulls two large images, starts the stack, deploys a bundle and generates logs,
so the headroom at the check says little about the headroom at peak. The second
check runs immediately before the snapshot β the disk-hungry phase, writing a
database dump plus a tar of every payload directory β and needs 5 GB of
remaining headroom. Failing at a named check beats failing inside tar, and a
snapshot that cannot write its payload is not a snapshot (LDM-#1429).
The run refuses before pulling anything, so a machine that cannot finish never produces a half-written report. That matters because a report which failed for lack of disk still reads as a defect finding, and these reports are the project's honest record of what was actually tested.
To verify the complete container lifecycle, volume mount synchronization, and CLI options natively on local developer machines:
Fetching the script that matches your binary¶
The script and the binary are versioned together β SCRIPT_VERSION is stamped
by scripts/release.py on every bump β so a mismatched pair will assert against
behaviour that is not there. Fetch the script for the version you are running.
Do not embed ldm version directly in the URL. If the binary cannot
determine its version it prints nothing, the URL becomes .../v/scripts/...,
and the download returns 404 β which reads as a missing script and hides the
real failure. That has happened: on Windows a one-file binary that could not
unpack to a full %TEMP% produced exactly this (LDM-#1437).
For a pre-release binary, fetch from the release branch, not the tag
(LDM-#1613). Tags are immutable under the Burn Rule, so a harness fix landed
after vX.Y.Z-pre.N was cut is unreachable from that tag for ever -- the URL
keeps serving the broken script. release/vX.Y.Z is the only ref carrying both
the fix and the matching SCRIPT_VERSION stamp; master has the fix but the
wrong stamp, so it trips the version gate. This was hit for real on
LDM-#1610/#1612. A stable version's tag is immutable by design -- it is
exactly what shipped, and the release branch is deleted once the version is
promoted -- so that case keeps the tag.
LDM_VER="$(ldm version 2>/dev/null)"
if [ -z "$LDM_VER" ]; then
echo "Could not determine the LDM version -- is 'ldm' on PATH and working?" >&2
echo "A binary that cannot unpack itself prints nothing here (LDM-#1437)." >&2
exit 1
fi
# LDM-#1613: pre-releases resolve to the mutable release branch.
case "$LDM_VER" in
*-pre.*) LDM_REF="release/v${LDM_VER%%-pre.*}" ;;
*) LDM_REF="v${LDM_VER}" ;;
esac
curl -fsSL "https://raw.githubusercontent.com/peterrichards-lr/liferay-docker-manager/${LDM_REF}/scripts/verify_e2e_refactor.sh" \
-o verify_e2e_refactor.sh && chmod +x verify_e2e_refactor.sh
$LdmVer = (ldm version 2>$null)
if (-not $LdmVer) {
Write-Error "Could not determine the LDM version -- is 'ldm' on PATH and working? A binary that cannot unpack itself prints nothing here (LDM-#1437)."
exit 1
}
# LDM-#1613: pre-releases resolve to the mutable release branch.
if ($LdmVer -like '*-pre.*') {
$LdmRef = "release/v" + (($LdmVer -split '-pre\.')[0])
} else {
$LdmRef = "v$LdmVer"
}
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/peterrichards-lr/liferay-docker-manager/$LdmRef/scripts/verify_e2e_refactor.ps1" -OutFile "verify_e2e_refactor.ps1"
Both check the version is non-empty before building the URL, so a broken binary produces a message naming the real problem instead of a 404. The scripts' own mismatch banner derives the ref the same way, so the hint it prints and the instructions here can never disagree.
Important
Prefer the verification bundle. Fetching the script on its own gets you the
script and nothing else -- and the suite needs the common/ folder beside it,
which carries the DXP activation key and the Elasticsearch configuration.
Without it LDM only warns:
The suite then runs to completion, exits 0 and reports success -- having
never applied the activation key or the search configuration. Every assertion
genuinely passes; they just tested a smaller system than the report claims
(LDM-#1718).
The bundle also removes a second hazard in the commands above: LDM_REF is
derived from whatever binary happens to be on PATH, so the script can come
from a different release than the binary under test. The bundle is published
per tag, so the pair is fixed by construction.
Use the installer (recommended)¶
scripts/install_verification.sh (and install_verification.ps1 on Windows)
does the whole staging sequence: fetches the bundle for a tag, verifies every
checksum including its own, unpacks it so common/ sits beside the script,
makes the suite executable, removes the archive once it is verified, and finds
your activation key without being told where it is.
It is published as a release asset, so no checkout is needed -- which is the point: a release should be verifiable from its published artifacts alone, the way a user does it.
Run it from the directory that already holds your common/ folder, since that
is where the activation key lives on each machine.
macOS / Linux¶
ldm system upgrade --beta # take the release under test
LDM_TAG=v$(ldm version)
curl -fsSL -O "https://github.com/peterrichards-lr/liferay-docker-manager/releases/download/${LDM_TAG}/install_verification.sh"
chmod +x install_verification.sh
./install_verification.sh --tag "${LDM_TAG}" --no-binary --dir .
./verify_e2e_refactor.sh
Windows¶
ldm system upgrade --beta
# A downloaded .ps1 is blocked by default. Process-scoped, so it does not persist.
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
$LdmTag = "v$(ldm version)"
Invoke-WebRequest -UseBasicParsing -OutFile install_verification.ps1 `
"https://github.com/peterrichards-lr/liferay-docker-manager/releases/download/$LdmTag/install_verification.ps1"
.\install_verification.ps1 -Tag $LdmTag -NoBinary -Dir .
.\verify_e2e_refactor.ps1
Warning
On Windows, ldm system upgrade returns before it has finished -- it
stages the swap and relaunches in a new window, because a running executable
cannot replace itself. Run ldm version too promptly and it reports the
old version, so $LdmTag stages the wrong release: the suite and the
binary under test then come from different releases, which is exactly the
pairing the per-tag bundle exists to guarantee. Confirm ldm version shows
the version you expect before using it, or pin the tag literally
(LDM-#1743). macOS and Linux upgrade in-process and are unaffected.
The flags that matter¶
| Flag | PowerShell | Why |
|---|---|---|
--tag |
-Tag |
the release to stage. Omit to take the latest. |
--dir . |
-Dir . |
stage in place rather than in ./ldm-verification |
--no-binary |
-NoBinary |
skip the binary -- you already have it from the upgrade |
--activation-key |
-ActivationKey |
only needed when discovery cannot find your key |
--no-self-check |
-NoSelfCheck |
skip verifying the installer against the release |
--dir defaults to ldm-verification, created relative to where you run the
script, which keeps your source common/ pristine and gives you one directory
per release. --dir . stages into the current folder instead: one directory, no
duplicated key, but each run overwrites the suite in place, so it stops being a
record of any particular release.
Deriving the tag from ldm version¶
Safe only because the upgrade runs first -- it reads whatever binary is on
PATH, so without the upgrade it stages the previous release. Subject to the
Windows caveat above. Pin the tag literally if you are not upgrading first.
The activation key¶
The bundle cannot carry one (see below), so the installer looks for yours in two places, in order:
- the target's own
common/-- used in place, nothing copied ./common/relative to where you ran the script -- copied in
It says which it found. If neither exists it warns loudly and continues, because offline staging is legitimate -- but heed it: without a key Liferay runs unlicensed, LDM only warns, and the suite still exits 0 reporting success, having verified a smaller system than it claims.
Discovery matches the filename pattern only, not the contents, so a placeholder file stages cleanly and then verifies an unlicensed DXP.
Or stage it by hand¶
LDM_TAG=vX.Y.Z
curl -fsSL -o verification-bundle.zip \
"https://github.com/peterrichards-lr/liferay-docker-manager/releases/download/${LDM_TAG}/verification-bundle.zip"
unzip -o verification-bundle.zip -d ldm-verification
cd ldm-verification
shasum -a 256 -c SHA256SUMS
cp /path/to/activation-key-*.xml ./common/ # see the note below
bash verify_e2e_refactor.sh
Important
The bundle does not carry a DXP activation key, and cannot (LDM-#1733).
common/activation-key-*.xml is gitignored -- it is licensed and must not be
published -- so no CI checkout has one to package. Without it Liferay runs
unlicensed, LDM only warns, and the suite still reports success: exactly the
degraded-run-reported-as-pass the bundle exists to prevent, surviving in the
one half that could not travel. Copy your own key into common/ before
running. The bundle's MANIFEST.txt says which case it is.
$LdmVer = (ldm version 2>$null).Trim()
gh release download "v$LdmVer" --pattern verification-bundle.zip
Expand-Archive -Force verification-bundle.zip -DestinationPath ldm-verification
Set-Location ldm-verification
pwsh -File .\verify_e2e_refactor.ps1
The bundle contains both scripts, common/, the optional fragment-override
harness, a MANIFEST.txt naming the tag it came from, and SHA256SUMS. Run the
suite from the directory you unzip into, so common/ sits beside the script.
1. macOS & Linux¶
Run the Bash E2E verification script:
2. Windows¶
Run the PowerShell E2E verification script (ensure your PowerShell ExecutionPolicy permits running scripts):
Running only part of the suite (LDM_E2E_SECTIONS)¶
The full run is long, and for years the only thing that started it was
release-e2e.yml on push: tags: ["v*"] β that is, after a version number
had already been burnt. Every client-extension and routes regression of the
2.26 cycle (LDM-#1944, #1918, #1928, #1962, #1969) was therefore found by the
suite meant to prevent it, one tag too late (LDM-#1975).
Both halves of the script accept LDM_E2E_SECTIONS, a comma-separated list of
named sections. It defaults to all, so an unset variable behaves exactly as
before.
| Section | What it covers |
|---|---|
guardrails |
CLI refusals, compute targets, port-conflict diagnosis. No portal is booted for these. |
boundary |
The portal's umask and the permissions of the config trees it publishes, plus the client-extension deploy, the services LDM generates for it, the routes tree it scaffolds, and the assertion that Liferay's own routes directories are the ones LDM mounted (LDM-#1944/#1918/#1928/#1923/#1573). |
project |
The remaining assertions against the booted portal β hot deploy, snapshots, JSON schemas, exit codes, the reported access URL. |
extras |
Portal patch overlay, non-ASCII naming, shared database and shared search. Each of these boots or rebuilds another stack. |
$env:LDM_E2E_SECTIONS = "boundary"
powershell -ExecutionPolicy Bypass -File scripts/verify_e2e_refactor.ps1
Everything outside a section always runs, because every section needs it:
the environment prep, the image pull, infra setup, provisioning and booting
the test project, the teardown and the report. A slice is therefore not
proportional to the checks it keeps β the Liferay boot is a floor no selection
can get under. Measured against run 35862407660, the last green full run
before this was added:
| Phase | Duration | In a boundary slice |
|---|---|---|
Prelude (venv, image pull, infra setup) |
1m58s | runs |
guardrails |
2m54s | skipped |
| Provision + Liferay health wait | 1m39s | runs |
project |
3m17s | skipped |
boundary |
53s | runs |
extras |
11m34s | skipped |
| Final status + teardown | 5s | runs |
| Total | 22m20s | ~4m35s, plus up to 180s for the LDM-#1944 routes-pairing wait added after that run |
Two honesty guards come with it, and neither is optional:
- An unknown section name is refused, not treated as "matches nothing". A typo that silently verified nothing would be the LDM-#1611 failure mode exactly β a green run that checked nothing.
- A partial run writes
Sections: β¦ (PARTIAL RUN β not a full verification)into the report header, ends withE2E SECTIONS PASSEDrather thanALL E2E VERIFICATIONS PASSED, and is not copied intoreferences/verification-results/. That directory is whatscripts/sync_compatibility.pybuilds the compatibility matrix from, and a slice must never enter it looking like a full run.
.github/workflows/pr-boundary-e2e.yml runs LDM_E2E_SECTIONS=boundary on
every pull request to master that touches ldm_core/handlers/composer.py,
ldm_core/workspace/**, ldm_core/pipelines/** or the suite itself. That is
the whole point of the mechanism: boundary regressions are now discovered
before the tag instead of after it.
Declaring which environment a run represents¶
scripts/sync_compatibility.py builds the compatibility matrix from the
report's content, and it recognises only Fedora and Ubuntu by name. Every
other Linux distro used to fall through to a generic "Linux Workstation /
Linux", so Debian, Rocky and Alpine all produced the same canonical report
name and each silently overwrote the previous β three genuine CI passes went in
and one row came out, labelled as none of them (LDM-#1614).
Set LDM_ENV_LABEL so the run declares its own identity. It becomes an
Env Label: line in the report, which is what survives into the committed
record:
$env:LDM_ENV_LABEL = "debian"
powershell -ExecutionPolicy Bypass -File scripts/verify_e2e_refactor.ps1
The label names the distro; the version still comes from the report's own
Platform: line, so debian on Debian 12 becomes a Debian 12 row. The
verify-linux CI matrix passes its matrix.distro key automatically, so all
five of its arms are distinguished without anything to remember. Leaving the
variable unset keeps the previous behaviour exactly, and Fedora and Ubuntu are
unaffected either way.
The label also settles the matrix's Docker Provider column. That was
derived by looking for the literal word linux in the Platform: line, and
every containerised arm satisfied it only by luck β Fedora Linux 44 (Container
Image), Rocky Linux 9.3 (Blue Onyx), Alpine Linux v3.24, Debian
GNU/Linux 12 (bookworm). Upstream shortening any of those (Alpine to
Alpine 3.25, say) would have published a user-facing row reading provider
Unknown and renamed the canonical report β¦-unknown-pass.txt. A declared
label is now sufficient on its own, so the column no longer depends on a
vendor's PRETTY_NAME wording (LDM-#1631). The platform-string match remains
as the fallback for a hand-run with no LDM_ENV_LABEL set, and a run without
a label is never refused.
Should two reports still resolve to one environment name from genuinely
different environments, sync_compatibility.py refuses and exits non-zero
before moving anything, naming each report and its platform β the same
approach LDM-#1390 took to a version mismatch. Repeat runs of the same
environment continue to supersede one another as before.
A blank header field reads as Unknown, not as the next line¶
Every header field is parsed with a pattern bounded to its own line. That is
worth stating because it was not always true: the patterns used \s, which
matches a newline, so a blank field skipped the line break and captured the
following header line (LDM-#1633).
It was not hypothetical. Until LDM-#1639 verify_e2e_refactor.ps1 did not
populate Platform: on PowerShell 5.1 β $PSVersionTable.OS does not exist
there β so the committed PowerShell 5.1 report recorded its platform as
PowerShell: 5.1.22621.6133 (Desktop), the line beneath it. Across the
archive, 19 reports recorded a platform of Binary: β¦\ldm.exe and one recorded
a Docker engine version of running.
The canonical names and matrix rows were unaffected β the filename and the declared label carry the identity β but the collision guard above compares the identity each report states about itself, and that comparison must not be fed a string belonging to a different line.
A field that is present but empty now reports Unknown (or nothing at all,
where the caller has a fallback to fall through to, as Docker: does to the
doctor section).
The report-side half is closed too. verify_e2e_refactor.ps1 populated
Platform: from $PSVersionTable.OS, which arrived in PowerShell 6, so on
Windows PowerShell 5.1 the field was blank at source. It now falls back to
[System.Environment]::OSVersion.VersionString, which exists on 5.1 and 7
alike and needs no CIM/WMI call (LDM-#1639). Reports committed before that
change still carry a blank field, and read as Unknown β accurate, where the
previous value was not.
How a CI pass reaches the published matrix¶
On a stable tag push, the sync-compatibility job downloads the
verification artifacts, copies the passing Linux reports into
references/verification-results/, re-runs sync_compatibility.py and opens a
docs-only PR. Being labelled is therefore necessary but not sufficient β a
report also has to be fetched, and for a while three were not.
That job downloaded two artifacts by name, verification-results-ubuntu and
verification-results-fedora, and its pre-copy cleanup knew the matching two
slugs, while verify-linux ran five arms. Debian, Rocky and Alpine ran on
every stable tag, passed, uploaded a report β and had it discarded by the job
whose purpose is to publish it. Nothing went red, because a list that has
drifted out of step with another list looks exactly like a list that has not
(LDM-#1625).
It now fetches verification-results-* in a single step and selects what to
publish by the linux-workstation- prefix every verify-linux report carries,
so adding a distro to matrix.distro needs no change to the sync job at
all. Two consequences worth knowing:
- macOS and Windows artifacts are downloaded but never published. Those rows are curated by hand, and the filename selector β not the download step β is what keeps them that way.
- If nothing passing was downloaded, the step fails rather than clearing the committed Linux reports and replacing them with nothing.
The slug is derived from the report, never from the matrix key, and the three
differ: the rockylinux arm reports Rocky Linux 9.3 (Blue Onyx), writes a
raw file named β¦rocky-9.3β¦, and publishes as
verify-linux-workstation-rocky-linux-9.3-native-docker-pass.txt. Read the
canonical name out of a real report (or out of
sync_compatibility.get_report_metadata) rather than guessing it.
What the scripts verify¶
- Docker daemon connectivity and registry cleanups.
- Full project initialization, compose generation, and database sidecar startup.
- Automated snapshot extraction, integrity verification (SHA-256 signature generation), and directory structure restores.
- Metadata namespacing and port collision handling (confirming the
ldm forkcommand works cleanly without conflicts). - Up-front announcement of a project targeting a remote compute node, and its
deliberate suppression under
--json(LDM-#1341 / #1093). Provoked with an RFC 5737 TEST-NET-1 target whose Docker context is then deleted, so the project is classified remote without anything ever opening an SSH connection. - Late port-conflict handling: exit code
4plus guidance naming the port a re-run would pick, rather than telling you to go stop a process (LDM-#1350). - Teardown of resources and network isolation.
What the scripts cannot verify¶
Some behaviour is only reachable through a dependency the scripts can neither create nor clean up. Extending E2E coverage onto one of those would produce a check that reports a third party's state rather than LDM's, so the coverage lives in the unit suite instead. Recorded here so the gap stays visible:
- A package manifest that under-reports being corrected from its payload, on
the GitHub-release input (LDM-#1579).
_import_ldm_packageis reached from exactly one input: a GitHub repo URL whose latest release carries a.ldmpasset. The API host is hardcoded (api.github.com), so an end-to-end exercise of that input would mean publishing a deliberately malformed package to a real release (LDM-#1588).
Covered instead by ldm_core/tests/test_package_listing_recovery.py, which
drives the real download, checksum, extraction, verification and hydration
over a .ldmp built in the test with only requests.get mocked, and which
measures the routing above rather than asserting it in prose.
The manifest PARSE refusal on a local .ldmp is no longer in this
category, and is now asserted E2E. LDM-#1621 put manifest verification on
the import pipeline, and the parse refusal fires in
PackageVerificationStage before ProjectSetupStage creates anything -- so
it needs no network, no published release and no Liferay boot. Both scripts
build the package themselves and assert the refusal:
verify_ldmp_manifest_refusal() in the .sh, Test-LdmpManifestRefusal in
the .ps1.
Both assert the exit code and the reason, which is not belt-and-braces.
Measured on 2026-09-07 against a binary without #1621: the same package
reached verify_runtime_environment and exited 1 on
FATAL: VOLUME MOUNTING IS BROKEN. A check that looked only at the exit code
would have reported that as a pass.
What is still not asserted E2E is the recovery and the origin
reporting. Both continue past the manifest into cmd_restore, so exercising
them needs a real database restore and a Liferay boot against a package the
script would have to build from a running project. Covered by
ldm_core/tests/test_local_package_verification.py, which drives the real
cmd_import through the real pipeline over a package built in the test, with
only cmd_restore, cmd_run and the shared doctor preflight stubbed.
The two script functions are named, rather than inlined, so their own
assertion logic is testable without Docker or an ldm binary:
ldm_core/tests/test_verify_scripts.py drives each half against a stub ldm
that returns a chosen exit code and message, and checks the parity of the two
halves.
Exit status, and why CI cross-checks the report¶
Both scripts exit 0 only when the run reached ALL E2E VERIFICATIONS PASSED,
and 1 otherwise. The Bash script has always done this (set -e plus explicit
exit 1 on each failure path). The PowerShell script did not until
LDM-#1611: its top-level catch wrote the -fail report, printed
[FAILED] Verification FAILED (fail), and then fell off the end of the
script β and PowerShell exits 0 once an exception has been handled. Measured
on pwsh 7.6.5, pwsh script.ps1 and pwsh -File script.ps1 both exited 0
from that failure path.
The consequence was that LDM Platform Verification (Multi-OS) reported
success on the v2.21.0-pre.2 and v2.21.0 tag runs while uploading three
-fail.txt reports (both Windows shells and macOS/Colima; the macOS half came
from a continue-on-error: true on that job's suite step). A job that cannot
report failure is worse than an absent one, because the compatibility matrix is
built from its output: a red job says "unverified", whereas those green ones
said "verified" and were wrong.
Every verification job therefore now cross-checks the artefact as a second,
independent signal β the exit status alone has been lost once already. The step
fails if no verify-*.txt was produced, if a report's filename ends -fail, or
if a report lacks the ALL E2E VERIFICATIONS PASSED marker. It runs with
if: always(), after the artefact upload, so a failing run still ships its
report.
The .ps1's exit status is regression-tested by executing it β
TestPowerShellSuiteExitStatus in ldm_core/tests/test_verify_scripts.py
extracts the script's real try/catch/finally tail and epilogue, stubs
only the try body (the part needing Docker), and asserts the exit code in
real PowerShell for a failing run, a passing run and the "no branch recorded
an outcome" case. A source-text match was rejected deliberately: it passes
whenever the text survives, including when the behaviour is gone, which on
this issue of all issues would repeat the mistake.
Report size is a usable smoke signal alongside this: a genuine pass is ~4 kB, and the three false passes above were 608β794 b.
Which platforms CI actually verifies (LDM-#1662)¶
Making those jobs honest revealed what they had been hiding: they had never
passed. The red that followed PR #1622 was not a regression β it was the true
state arriving. The runner images were identical on the last "green" run
(34063433007, v2.21.0) and the first red one, so nothing about the
environment changed; only the reporting did.
The arms were therefore split:
| Workflow | Arms | May be red? |
|---|---|---|
LDM Platform Verification (Multi-OS) |
verify-linux (ubuntu, fedora, rockylinux, alpine, debian) |
No β it publishes the matrix |
LDM Platform Verification (Best Effort) |
verify-macos (Colima) |
Yes β diagnostic only |
The Windows arms were deleted, not demoted. GitHub-hosted Windows runners
run Windows containers, and every LDM image is a Linux image, so the job failed
at no matching manifest for windows(10.0.26100)/amd64 and then at
docker network create liferay-net. No workflow-level change makes that pass.
Windows coverage is the manual PowerShell 5.1 / 7 / WSL2 runs in the table at
the top of this document β which is where it always genuinely came from, since
sync-compatibility has only ever published reports carrying the
linux-workstation- prefix.
verify-macos is expected to be red for now. Colima does not start on
macos-26-arm64, so the suite finds no daemon at
unix:///var/run/docker.sock. It is kept running because the report and debug
logs it uploads are the only standing signal on that platform, and because the
failure is plausibly recoverable in a way the Windows one is not.
Do not "fix" a red best-effort arm with continue-on-error: true on the
job. That makes it report success, which is precisely the LDM-#1611 defect
described above. Best effort means red in a workflow allowed to be red, never
green in one that is not. test_ci_verification_reporting.py asserts this,
along with the moved job keeping its report cross-check and not drifting back
into the Linux workflow.
(The continue-on-error on the macOS Start Colima step is unrelated and
deliberate: it lets the suite step fail on its own and produce a report plus
debug logs, instead of aborting the job on a bare brew/colima trace.)
Fragment override: a CI job, deliberately not a script check (LDM-#1745)¶
The fragment-override chain β LDM's most intricate runtime feature β had no
test that exercised it end to end. verify_e2e_refactor.{sh,ps1} contain zero
fragment assertions, and the unit suite mocks both Docker and Liferay, so
nothing had ever demonstrated the thing users care about: a configured
override reaches the rendered page.
It is now measured by .github/workflows/fragment-override.yml, on v* tags
and workflow_dispatch, mirroring release-e2e.yml's shape. The job runs
scripts/fragment_override_harness.py, which builds a site-initializer client
extension, boots a licensed DXP, lets the Site Initializer place the fragment
on a page, has LDM apply the override, restarts, and asserts four observations
in order: the default value in fragmententrylink.editablevalues, the default
value in the rendered HTML, the overridden value in editablevalues, and the
overridden value in the rendered HTML with the default gone. The first two
exist so the last two cannot pass vacuously.
It stays out of the verification scripts on purpose. Fragment override is
platform-independent β LDM talking to Liferay's API and database, with nothing
that varies by host OS β so running it across macOS, Windows and Linux would
cost three Liferay boots for one bit of information. It also depends on a
Liferay boot and on Site Initializer population, which are durations the suite
does not own: the principle LDM-#1383 set out, LDM-#1444 applied, and LDM-#1728
was a reminder of. test_fragment_override_harness.py asserts both halves of
that decision β that neither script invokes the harness, and that the workflow
exists.
It needs a licensed DXP, which is why this one job takes a repository
secret. An unlicensed portal serves the DXP Activation page in place of the
site, so the two rendered-page observations β the only ones that answer the
question β cannot be made at all. The job reads LIFERAY_ACTIVATION_KEY_XML
(the XML text of a developer activation key, which carries no machine
binding and so works on an ephemeral runner), writes it to a file at run time,
and hands the path to the harness. The job fails loudly when the secret is
absent rather than running and reporting a weaker result as a pass. Never
commit an activation key.
Measured budget. A full run on a local Apple-silicon Colima VM (4 CPU,
8 GiB) against DXP 2026.q3.0 with the image already pulled took 341 s:
| Step | Seconds |
|---|---|
ldm import |
18.9 |
ldm run (first Liferay startup, 110.7 s of it) |
188.5 |
| Headless answering | 8.2 |
| Site Initializer placing the fragment | 10.1 |
ldm wait applying the override |
71.0 |
ldm restart (second startup is warm β 10.9 s) |
2.8 |
| Rendered page carrying the new value | 0.3 |
| Total | 341.1 |
Two of those are worth knowing. The override step is 71 s because LDM spends
its whole --fragment-patch-timeout waiting for two Headless rungs that are
known dead on a published site-initializer page before falling through to the
database fallback; the harness passes 60 rather than accept the 300 s default,
which is otherwise pure wall clock. And the restart is cheap because the OSGi
state and database are warm β 10.9 s against 110.7 s for the cold boot.
A GitHub ubuntu-latest runner is slower than this VM and a first run also pays
the ~5 GB DXP image pull, so the job's ceiling is 90 minutes. Do not treat the
table above as the CI budget β the workflow prints its own timings from
report.json on every run, which is the number to read.
Last Updated: 2026-10-15 | Last Reviewed: 2026-10-15