E2E & orchestration
E2E Verification & LDM Orchestration¶
To ensure production-parity verification, the project includes an automated orchestrator using Liferay Docker Manager (LDM).
1. Environment Hardening¶
- Version Gate: Minimum LDM version
2.5.4is enforced. - Fail-Fast: The orchestrator runs
ldm doctor --skip-projectand verifies hostname resolution before attempting a boot, preventing wasted startup time on misconfigured hosts.
2. Filesystem Resilience (The SanDisk Rule)¶
Running Liferay Docker containers from external drives (common on macOS) often triggers fatal OSGi locking errors (Unable to create lock manager).
- The Strategy: The orchestrator script automatically detects if the workspace is located on an external volume (
/Volumes/). - The Fix: It dynamically patches the LDM-generated
docker-compose.ymlto remove bind-mounts forosgi/stateanddata. This forces Docker to use internal, high-performance Anonymous Volumes for these high-I/O directories, ensuring 100% boot stability regardless of the physical drive format.
3. Automated Setup Optimization¶
- Database: Standardized on
postgresqlfor E2E tests to bypass the mandatory password reset prompt enforced by Hypersonic on first login. - Boot Performance: Uses
--sidecarfor faster deployment monitoring and--no-captchato streamline automated authentication flows.--sidecaris a choice for this harness, not for consumers: it runs Elasticsearch inside the Liferay container, and LDM records that topology in bothcustom_envandfiles/portal-ext.properties, where it would otherwise override the shared search a consumer's project resolves to. The release workflow strips it withscripts/sanitize-ldm-package.cjs— see Quick Start and #563.
4. Responsive Visual Auditing¶
- Device Profiles: Playwright is configured to run tests across Desktop Chrome, iPhone, Pixel, and iPad.
- Visual Evidence: Automated full-page snapshots are captured for every screen and responsive state, saved to the
test-results/directory for manual verification.
---## The "Staging & Atomic Move" Deployment Pattern
To prevent race conditions with Liferay's aggressive auto-deployers (e.g., FragmentFileInstaller), all automated Docker deployments MUST follow the Staging & Atomic Move pattern.
1. The Problem: docker cp Race Condition¶
When running docker cp <local_file> <container>:/opt/liferay/deploy/, Docker performs two steps:
- Stream Data: Writes the file content.
- Finalize Metadata: Sets ownership and permissions (
lchown).
If Liferay's watcher detects and moves the file after Step 1 but before Step 2, Docker fails with: Error response from daemon: failed to Lchown ... no such file or directory.
2. The Solution: Implementation Steps¶
- Stage: Copy the artifact to a non-watched temporary directory (e.g.,
/tmp/aica-staging). - Atomic Move: Use
docker execto move the file into the target directory in a single operation.
# Example
docker exec <container> mkdir -p /tmp/aica-staging
docker cp ./artifact.zip <container>:/tmp/aica-staging/
docker exec -u 0 <container> mv /tmp/aica-staging/artifact.zip /opt/liferay/deploy/
3. Benefits¶
- Isolation: Prevents Liferay from seeing partial or incomplete files.
- Atomicity: The
mvcommand within the same filesystem is atomic in Linux. - Integrity: Guarantees deterministic deployment success.
---## E2E Log Analysis Hardening
- The Issue: In
scripts/test-e2e-orchestrator.js, the log analyzerscripts/analyze-e2e-logs.jsis spawned without passing the log file location (MS_LOG_FILE) as an argument. This causes the analyzer to exit with code 1 immediately, marking even successful test runs as failed in the orchestrator log verification phase. - The Fix: The orchestrator must explicitly pass
MS_LOG_FILEto the spawned analyzer process.
---## LDM Fast Login Configuration
- The Issue: On fresh Liferay database setups (including LDM imports from clean seeds), logging in with
test@liferay.comfrequently redirects the browser to the "Terms of Use" page or "Password Reminder" page. These prevent page navigation to the dashboard and trigger 60-second locator timeouts in specs. - The Fix: Add the
--fast-loginflag to theldm runcommand inscripts/run-e2e-ldm.sh. This ensures that LDM configures Liferay to bypass the Terms of Use and Password Reminder screens.
E2E Test Suite Connection & Import Reliability¶
- The Issue:
- On startup, Liferay Commerce takes 1-2 minutes to fully index and expose the default channel via its Headless APIs. If the E2E tests run immediately, the dropdown remains empty ("No channels found") and the "Generate" button stays disabled, timing out Playwright's click actions.
-
The E2E import test
import.spec.jswas trying to click an "Import Dataset" button and interact with a non-existent import modal, causing immediate timeouts since the frontend actually handles imports via a hidden#datasetImportfile input that stubs the operation. -
The Fix:
- Update
injectAndConnectAppin bothdashboard.spec.jsandimport.spec.jsto wait for the Channel dropdown to be populated with channels (i.e. not containing "No channels found") using a retry loop that clicks "Retry Connection" or "Connected" every 5 seconds if still loading. - Harden all "Generate" clicks in
dashboard.spec.jsby explicitly waiting for the button to become enabled. - Refactor
import.spec.jsto upload the sample JSON directly toinput#datasetImportand verify that the activity log logs the dataset import action.
Unicode Host Parsing in Orchestration¶
- The Issue: In
scripts/run-e2e-ldm.sh, resolving the hostname fromldm listviacut -d'│'failed in bash due to Unicode/locale constraints, resulting inBASE_URLbeing set tohttps://(which caused Playwright navigation errors and microservice configuration failures). - The Fix: Update
scripts/run-e2e-ldm.shto extract the target URL's hostname usinggrep -oEinstead of parsing with Unicode character delimiters.
AICAConfiguration Validation (configStatus Field)¶
- The Issue: When the microservice starts up and attempts to sync API keys to Liferay, it performs a POST request to
/o/c/aicaconfigurationsvia the SDK'supdateConfigmethod. Since the SDK does not include theconfigStatusfield in its payload, Liferay rejects the request with a400 Bad Request("No value was provided for required object field "configStatus""). - The Fix: Modify the
AICAConfigurationobject definition inclient-extensions/ai-commerce-accelerator-batch/batch/02-object-definition.batch-engine-data.jsonto set"required": falsefor theconfigStatusfield. This allows the configuration objects to be successfully created and updated without requiring the status field to be passed in every SDK request.
E2E Commerce Auto-Provisioning¶
- The Issue: Fresh bootstrapped Liferay database environments do not contain any commerce catalogs or channels by default, causing the E2E Playwright tests to timeout while waiting for the dropdown elements to populate.
- The Fix: Update
playwright/tests/e2e/auth.setup.jsto automatically check if any channels exist. If the channel count is zero, the setup script will auto-provision a default catalog ("Master") and channel ("Web Store") using Liferay's Headless REST APIs before caching the authentication state.
E2E Import Path Resolution¶
- The Issue: In
playwright/tests/e2e/import.spec.js, resolvingresources/sample-import.jsonusingpath.resolve('resources/sample-import.json')resolved relative to the current working directory of the process (which isplaywright/when tests run), causing file-not-found errors during test execution. - The Fix: Use
path.resolve(__dirname, '../../../resources/sample-import.json')to resolve the path relative to the test file itself.
Startup 404 Ignore Pattern in Log Analysis¶
- The Issue: During clean environment setup, the microservice attempts to poll and configure Liferay client extensions. Since Liferay starts up sequentially, early API calls to
/o/c/aicaconfigurationsresult in a404 Not Founderror. The forensic log analyzer incorrectly treats these expected transient startup delays as test failures. - The Fix: Add an exception pattern
/aicaconfigurations.*(404|No service was found|Not Found)/itoIGNORE_PATTERNSinscripts/analyze-e2e-logs.js.
E2E Button Locator Strict Mode Violation¶
- The Issue: In
playwright/tests/e2e/dashboard.spec.js, usingpage.getByRole('button', { name: /Generat/i })matches multiple elements (the collapsible accordion panel header "Data Generation Strategy" and the submit/generating buttons), causing strict mode violations in Playwright and failing the tests. - The Fix: Use the highly specific CSS selector
button[type="submit"]to uniquely target the inactive submit button, and assert on'Cancel Generation'and'Generating...'buttons individually when checking the active generating state.
Page Management API Feature Flag (LPD-35443)¶
- The Issue: To manage pages, page templates, and page template sets via REST APIs using external reference codes, Liferay requires enabling the experimental/beta feature flag for LPD-35443.
- The Fix: Add
feature.flag.LPD-35443=truetoconfigs/common/portal-ext.propertiesand add--feature LPD-35443to theldm runcommand inscripts/run-e2e-ldm.shso it is automatically enabled on boot.
SDK Page, Template, and Template Set Management Extensions¶
- The Fix: Implement
getSitePages,createSitePage,getSitePage,updateSitePage,deleteSitePage,patchSitePagealong with similar wrapper methods forPageTemplateandPageTemplateSetresources inside theLiferayServiceclass, backed by the generated fluent clientheadlessAdminSitebindings.
Remote Target Node Power: One Name, Credentials Everywhere, No Silent Failures¶
- The Issue:
.github/workflows/e2e-verification.ymlre-derived the target node name in every step that needed it, and the chains disagreed. Wake readgithub.event.inputs.node_target || vars.LDM_NODE_TARGET || secrets.LDM_NODE_TARGET || 'local'; the mandatory cleanup read... || env.LDM_NODE_TARGET || 'node-a', which never consultssecrets. The nightly's node name comes fromsecrets.LDM_NODE_TARGET, so the cleanup fell through to the literalnode-aand stopped an instance the suite had never used, while the instance it did use stayed up and billable. Compounding it, theStart LDM and run testsstep passed no AWS credentials, so bothnode_power.shcallsscripts/run-e2e-ldm.shmakes for itself — the wake at the top and thesleepin itsEXITtrap — died at the AWS CLI withNoCredentials, and|| trueon the wake discarded that for weeks (#718). - The Fix: The node name is resolved once into the job-level
E2E_TARGET_NODE, andConfigure SSH Key,Wake remote target node,Start LDM and run testsandSleep remote target nodeall read that one value, so they cannot name different nodes.AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYare on the step that runs the orchestrator as well as the wake and sleep steps. The env var is deliberately not calledLDM_NODE_TARGET: that is the orchestrator's own variable, set by its--nodeflag, and a job-level env of that name would enter every step's environment. - Why the Script Still Wakes:
scripts/run-e2e-ldm.shkeeps its own wake even though CI wakes the node a step earlier, because on a local run against a remote node it is the only wake there is. It is fatal now rather than|| true— a power-on failure stops the run wherever it happens. Thesleepin theEXITtrap stays non-fatal, since exiting non-zero from the trap would replace the run's own exit code, but it prints a warning naming the node instead of discarding the failure; the workflow's mandatory sleep step is the backstop, and it raises a workflow error annotation without failing the job.
Target Nodes Live in Secrets, Masked Value by Value¶
- The Issue: The remote target nodes are the maintainer's own infrastructure, not this project's. Their names, EC2 instance id, region and a public address were committed in workflows, scripts, tests and docs, and printed in public Actions logs. The node config itself was downloaded at run time from another public repository, which stopped hosting it (#1383).
- The Fix: Everything about a node comes from secrets, and nothing in this repository names one. Examples use placeholders:
node-a/node-b,203.0.113.0/24(RFC 5737) andi-0123456789abcdef0.scripts/test_no_infrastructure_in_repo.pyfails if a tracked file reintroduces a real instance id or public address.
| Secret | Holds |
|---|---|
NODE_POWER_CONFIG |
{"nodes": {"node-a": {"name": "node-a", "ec2_instance_id": "i-0123456789abcdef0", "region": "xx-example-1", "schedule": "auto", "user": "ubuntu"}}}, the same shape as a local .node-power-config.json (gitignored) |
LDM_NODE_TARGET |
The node the nightly targets |
LIFERAY_LICENSE_<NODE> |
Each remote node's own activation key (keys are issued per machine). Never named in the workflow: scripts/select_node_licence.py reads the target's with a computed key, secrets[...], after masking its name, and passes only the path of a private file |
LIFERAY_LICENSE |
A developer licence, used locally and as the named fallback |
- Masking: GitHub masks a secret only as its exact full value, and the parts of
NODE_POWER_CONFIGare printed separately (a node name in a log line, an instance id by the AWS CLI). So every job that touches a node runsscripts/node_config_from_secret.pyfirst. It emits::add-mask::for each node name, instance id, region and host, and with--writewrites.node-power-config.jsonfornode_power.shand the E2E. Masks are per job, so each job does this for itself. - What masking cannot reach: a
workflow_dispatchinput is shown on that run's page. Dispatch withnode_target=nightlyto run against the nightly's node from the secret without naming it; a node name typed there is published on that run. An empty input is not the nightly: GitHub fills it with the default,local.
LDM URL Resolution and Protocol Support¶
- The Issue: When running E2E tests against an existing project instance (e.g.
fragments-test-envrunning athttp://localhost:8080), the test orchestrator extracts the domain name without its protocol or port and prependshttps://. This leads to network timeouts because it attempts to query plain HTTP ports using HTTPS. Additionally, LDM list outputs can contain ANSI color escape sequences that contaminate the parsed URL. - The Fix: Update
scripts/run-e2e-ldm.shto extract the fullTARGET_URL(including protocol and port) from the output ofldm listusing a strict URL character regex (to strip out trailing ANSI terminal color escape codes) and bindBASE_URLto it directly.
TARGET_URL Is the Only Source of Truth for the Target URL¶
- The Issue:
TARGET_URLwas assigned only inside the two "host is not resolvable" fallback branches ofscripts/run-e2e-ldm.sh, while the correct URL for a resolvable host was derived separately intoLIFERAY_URL. In CI the workflow writes the host into/etc/hosts, so the host always resolves,TARGET_URLwas always empty, and the export block near the end of the script (BASE_URL="$TARGET_URL", thenLIFERAY_URL="$BASE_URL") overwrote the correct value with an empty one. Every consumer then took its own silent default — the orchestratorhttp://localhost:8080, Playwrighthttp://127.0.0.1:8080, the microservice "Liferay URL is not configured" — so the run failed 1h53m later as a Liferay startup timeout that named nothing. Forty consecutive nightly runs failed this way and the Playwright suite never started (#707). - The Fix:
TARGET_URLis derived once, unconditionally, before any consumer reads it:target_host_url()owns the protocol/host/port decision (NO_SSL,SSL_PORT_SUFFIX,TARGET_HOST) andmapped_container_url()owns the unresolvable-host fallback to the container's mapped Tomcat port.export_target_urls()re-derivesBASE_URL,LIFERAY_URL,LIFERAY_API_URLand theCOM_LIFERAY_LXC_DXP_*pair from it and is called at every point whereTARGET_URLcan change, so no consumer is left holding a URL the resolution has moved on from. - The Guard:
assert_target_urlaborts with a message naming the variable ifTARGET_URLis ever empty. The cost of this bug was not the wrong URL but the silence: the run continued for nearly two hours and reported a symptom unrelated to the cause. Any future URL-resolution change must fail at the point of derivation, not at the first consumer that times out.
LDM Reference Documentation¶
- Documentation Repository: peterrichards-lr/liferay-docker-manager
- Main Documentation Index: LDM README
- Environment Architecture & Routing Details: LDM Architecture
- Client Extension Routing & SSL Setup: LDM Networking & DNS Guide
- Local Replication of Cloud Environments: LDM PAAS Local Dev Guide
Last Reviewed: 2026-10-23