From 910396757d8afcb161b0b5fd342933db9fbe4ff0 Mon Sep 17 00:00:00 2001 From: Juan Ayala Date: Sun, 24 May 2026 23:22:58 +0000 Subject: [PATCH 1/5] Add devcontainer lock file and update docker-in-docker feature version --- .devcontainer/devcontainer-lock.json | 24 ++++++++++++++++++++++++ .devcontainer/devcontainer.json | 7 +++++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .devcontainer/devcontainer-lock.json diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..9b2f1de --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,24 @@ +{ + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:3": { + "version": "3.0.1", + "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:ca2508495b01ba29eba93e8153772a2daa65eaa86471cc6863fe2a3d21933df9", + "integrity": "sha256:ca2508495b01ba29eba93e8153772a2daa65eaa86471cc6863fe2a3d21933df9" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/java@sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a", + "integrity": "sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "1.7.1", + "resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6", + "integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b6e0f63..74a49c3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,12 +21,15 @@ "ghcr.io/devcontainers/features/node:1": { "version": "20" }, - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/docker-in-docker:3": { + "moby": false + }, "ghcr.io/devcontainers/features/java:1": { "version": "21", "jdkDistro": "oracle", "installMaven": true - } + }, + "ghcr.io/devcontainers/features/github-cli:1": {} }, "updateContentCommand": "npm install -g @devcontainers/cli", "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}" From 4881e630477c01c76e7c5a9d1fcd0398d8f9bb55 Mon Sep 17 00:00:00 2001 From: Juan Ayala Date: Sun, 24 May 2026 23:23:28 +0000 Subject: [PATCH 2/5] Enhance aem-lts script with interactive mode support and improve test coverage --- src/aem-lts/bin/aem-lts | 62 +++++++++++++++++----- test/aem-lts/defaults-with-quickstart.sh | 67 +++++++++++++----------- 2 files changed, 86 insertions(+), 43 deletions(-) diff --git a/src/aem-lts/bin/aem-lts b/src/aem-lts/bin/aem-lts index 33c617b..d6455a0 100644 --- a/src/aem-lts/bin/aem-lts +++ b/src/aem-lts/bin/aem-lts @@ -1,8 +1,29 @@ #!/usr/bin/env bash +# +# aem-lts - Manage AEM LTS instances in a dev container +# +# Usage: +# aem-lts start [author|publish] [-i] Start an AEM instance +# aem-lts stop [author|publish] Stop a running AEM instance +# +# Options: +# -i Interactive mode (start only): runs the Java process in the +# foreground so logs stream to the terminal. Use CTRL+C to stop. +# +# When started without -i, the process runs in the background as a daemon +# and logs are written to /var/log/aem-.log. source "$(dirname $0)/_globals.sh" -action=$(get_action "${1}") -service=$(get_runmode "${2}") + +# parse -i flag from any position in the arguments +INTERACTIVE=false +ARGS=() +for arg in "$@"; do + [[ "${arg}" == "-i" ]] && INTERACTIVE=true || ARGS+=("${arg}") +done + +action=$(get_action "${ARGS[0]}") +service=$(get_runmode "${ARGS[1]}") echo "${action^}ing AEM ${service}..." @@ -26,7 +47,7 @@ if [[ ! -f "${licenceFile}" ]]; then aem_lts_err "License customer name not set" [[ -z "${AEM_LTS_LICENSE_DOWNLOAD_ID}" ]] && \ aem_lts_err "License download ID not set" - + cat < /dev/null license.customer.name=${AEM_LTS_LICENSE_CUSTOMER_NAME} license.downloadID=${AEM_LTS_LICENSE_DOWNLOAD_ID} @@ -47,28 +68,45 @@ licenseFileLink="${runmodedir}/license.properties" [[ ! -L "${licenseFileLink}" ]] && sudo ln -s "${licenceFile}" "${licenseFileLink}" # make user owner of crx-quickstart (it is a volume mount) -sudo chown ${USER} "${runmodedir}/crx-quickstart" +[[ -d "${runmodedir}/crx-quickstart" ]] && sudo chown ${USER} "${runmodedir}/crx-quickstart" + +PID_FILE="/tmp/aem-${service}.pid" + +# check if pid file exists and process is running +IS_RUNNING=false +[[ -f "${PID_FILE}" ]] && sudo kill -0 "$(cat "${PID_FILE}")" 2>/dev/null && IS_RUNNING=true case "${action}" in start) - [[ -f "/tmp/aem-${service}.pid" ]] && aem_lts_err "AEM ${service} already running" + ${IS_RUNNING} && aem_lts_err "AEM ${service} already running" + [[ -f "${PID_FILE}" ]] && sudo rm -f "${PID_FILE}" # remove stale pid file if present port=$(get_runmode_port ${service}) jvm_opts="-agentlib:jdwp=transport=dt_socket,address=*:3${port},server=y,suspend=n" cq_opts="-nofork -nobrowser -nointeractive -r ${service} -p ${port}" - sudo start-stop-daemon --start --quiet --background --chuid root \ - --make-pidfile --pidfile "/tmp/aem-${service}.pid" \ - --name ${service} \ - --chdir "$(dirname "${jarFileLink}")" \ - --startas /bin/bash -- -c "exec ${JAVA_HOME}/bin/java ${jvm_opts} -jar "${jarFileLink}" ${cq_opts} \ - > /var/log/aem-${service}.log 2>&1" + if ${INTERACTIVE}; then + # run in foreground; CTRL+C to stop + echo "Running in interactive mode, press CTRL+C to stop..." + ${JAVA_HOME}/bin/java ${jvm_opts} -jar "${jarFileLink}" ${cq_opts} + else + sudo start-stop-daemon --start --quiet --background --chuid root \ + --make-pidfile --pidfile "${PID_FILE}" \ + --name ${service} \ + --chdir "$(dirname "${jarFileLink}")" \ + --startas /bin/bash -- -c "exec ${JAVA_HOME}/bin/java ${jvm_opts} -jar "${jarFileLink}" ${cq_opts} \ + > /var/log/aem-${service}.log 2>&1" + fi ;; stop) + ${IS_RUNNING} || aem_lts_err "AEM ${service} is not running" + + # give it 2 minutes to shut down gracefully, force kill after 10 more seconds sudo start-stop-daemon --stop --verbose \ - --remove-pidfile --pidfile /tmp/aem-${service}.pid + --remove-pidfile --pidfile "${PID_FILE}" \ + --retry TERM/120/KILL/10 ;; *) aem_lts_err "Unknown action '${action}'" diff --git a/test/aem-lts/defaults-with-quickstart.sh b/test/aem-lts/defaults-with-quickstart.sh index 84e17fd..cfa147f 100644 --- a/test/aem-lts/defaults-with-quickstart.sh +++ b/test/aem-lts/defaults-with-quickstart.sh @@ -16,40 +16,45 @@ check "author port default" \ [ "${AEM_LTS_AUTHOR_PORT}" = "4502" ] check "publish port default" \ [ "${AEM_LTS_PUBLISH_PORT}" = "4503" ] + # Check aem-lts in PATH is executable check "aem-lts is +x" \ stat -c '%A' $(which aem-lts) | grep 'x.*x.*x' -# Check that author installs, starts & stops correctly -check "author: install & start" \ - aem-lts start author -sleep 3 # give it time to start -check "author: check log for start message" \ - grep 'Server started on port 4502' /var/log/aem-author.log -check "author: compare pid file to java process" \ - [ $(cat /tmp/aem-author.pid) -eq $(pgrep -x java) ] -check "author: stop" \ - aem-lts stop author -sleep 3 # give it time to stop -check "author: pid file should be removed" \ - [ ! -f /tmp/aem-author.pid ] -check "author: no java process should be running" \ - [ -z $(pgrep -x java) ] - -# Check that publish installs, starts & stops correctly -check "publish: install & start" \ - aem-lts start publish -sleep 3 # give it time to start -check "publish: check log for start message" \ - grep 'Server started on port 4503' /var/log/aem-publish.log -check "publish: compare pid file to java process" \ - [ $(cat /tmp/aem-publish.pid) -eq $(pgrep -x java) ] -check "publish: stop" \ - aem-lts stop publish -sleep 3 # give it time to stop -check "publish: pid file should be removed" \ - [ ! -f /tmp/aem-publish.pid ] -check "publish: no java process should be running" \ - [ -z $(pgrep -x java) ] +test_runmode() { + local service=$1 port=$2 + + # Simulate a stale pid file (process exited but file was left behind) + echo "99999" | sudo tee /tmp/aem-${service}.pid > /dev/null + + check "${service}: install & start" \ + aem-lts start ${service} + check "${service}: wait for server started on port ${port}" \ + timeout 10 bash -c "until grep -q 'Server started on port ${port}' /var/log/aem-${service}.log 2>/dev/null; do sleep 0.5; done" + check "${service}: compare pid file to java process" \ + [ $(cat /tmp/aem-${service}.pid) -eq $(pgrep -x java) ] + check "${service}: stop" \ + aem-lts stop ${service} + check "${service}: pid file should be removed" \ + [ ! -f /tmp/aem-${service}.pid ] + check "${service}: no java process should be running" \ + [ -z $(pgrep -x java) ] + + # Test interactive mode: start in background, capture stdout, verify process runs, then kill it + INTERACTIVE_LOG=$(mktemp) + aem-lts start ${service} -i > ${INTERACTIVE_LOG} & + INTERACTIVE_PID=$! + check "${service}: interactive mode wait for server started on port ${port}" \ + timeout 10 bash -c "until grep -q 'Server started on port ${port}' ${INTERACTIVE_LOG} 2>/dev/null; do sleep 0.5; done" + check "${service}: interactive mode pid matches java process" \ + [ $(pgrep -P ${INTERACTIVE_PID} -x java) -eq $(pgrep -x java) ] + kill $(pgrep -P ${INTERACTIVE_PID} -x java) 2>/dev/null + check "${service}: interactive mode java process stopped after kill" \ + timeout 10 bash -c "until ! pgrep -x java > /dev/null; do sleep 0.5; done" + rm -f ${INTERACTIVE_LOG} +} + +test_runmode author 4502 +test_runmode publish 4503 reportResults From cd0c8523519eec01fad13e84e964417fe344373d Mon Sep 17 00:00:00 2001 From: Juan Ayala Date: Sun, 24 May 2026 23:49:03 +0000 Subject: [PATCH 3/5] Add interactive mode instructions for aem-lts script in NOTES.md --- src/aem-lts/NOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/aem-lts/NOTES.md b/src/aem-lts/NOTES.md index 402bdc8..f804508 100644 --- a/src/aem-lts/NOTES.md +++ b/src/aem-lts/NOTES.md @@ -30,10 +30,14 @@ In VSCode, open the terminal window. This is a terminal inside the docker contai There will be a script named `aem-lts`. Use this to start the author, publish or dispatcher. * Start author: `aem-lts start author` +* Start author in interactive mode: `aem-lts start author -i` * Stop author: `aem-lts stop author` * Start publish: `aem-lts start publish` +* Start publish in interactive mode: `aem-lts start publish -i` * Stop publish: `aem-lts stop publish` +The `-i` flag runs the Java process in the foreground so logs stream to the terminal. Use `CTRL+C` to stop the interactive session. + The feature also sets up volume mounts for the author and publish services. This is where the services will persist the repository. So that if the container gets deleted and/or rebuilt, the repository will persist. ## References From 3e8e639a8071de7812dcd1361dbe9eda8357dfeb Mon Sep 17 00:00:00 2001 From: Juan Ayala Date: Mon, 25 May 2026 00:09:43 +0000 Subject: [PATCH 4/5] Add antigen feature with custom theme and bundles; update node version to 22 --- .devcontainer/devcontainer-lock.json | 5 +++++ .devcontainer/devcontainer.json | 13 +++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index 9b2f1de..9ebbe9c 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -19,6 +19,11 @@ "version": "1.7.1", "resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6", "integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6" + }, + "ghcr.io/phil-bell/devcontainer-features/antigen:1": { + "version": "1.0.3", + "resolved": "ghcr.io/phil-bell/devcontainer-features/antigen@sha256:75204591baac950dc64975ecf42a3a5908335348ed9a6260e0dc3d46d1cf9148", + "integrity": "sha256:75204591baac950dc64975ecf42a3a5908335348ed9a6260e0dc3d46d1cf9148" } } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 74a49c3..7b61f66 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,13 +13,22 @@ ] }, "extensions": [ - "mads-hartmann.bash-ide-vscode" + "mads-hartmann.bash-ide-vscode", + "ms-vscode.vscode-chat-customizations-evaluations", + "GitHub.vscode-github-actions" ] } }, "features": { + "ghcr.io/phil-bell/devcontainer-features/antigen:1": { + "theme": "spaceship-prompt/spaceship-prompt", + "bundles": [ + "zsh-users/zsh-syntax-highlighting", + "zsh-users/zsh-autosuggestions" + ] + }, "ghcr.io/devcontainers/features/node:1": { - "version": "20" + "version": "22" }, "ghcr.io/devcontainers/features/docker-in-docker:3": { "moby": false From b1d38453bff7004b9adb0b3cd6373822013c7cbc Mon Sep 17 00:00:00 2001 From: Juan Ayala Date: Mon, 25 May 2026 00:10:09 +0000 Subject: [PATCH 5/5] Add GitHub Copilot instructions for devcontainer-features repository --- .github/copilot-instructions.md | 103 ++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..d26e40d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,103 @@ +# GitHub Copilot Instructions - devcontainer-features + +## Repository role +You are working in a Dev Container Features repository that packages Adobe Experience Manager (AEM) tooling for local development. The repository is centered on feature definitions under src//, test fixtures under test//, and CI workflows under .github/workflows/. + +## Repository-specific conventions +- Feature IDs and folder names match exactly: + - aem-sdk + - aem-lts + - aem-repo-tool + - aem-universal-editor-service +- Each feature owns its own install.sh, devcontainer-feature.json, and supporting scripts under src//bin/. +- Test directories mirror the feature IDs under test// and contain at least test.sh plus scenario definitions when applicable. +- Generated documentation for each feature is stored in src//README.md and is derived from devcontainer-feature.json. Do not manually diverge from the JSON unless you intentionally want to change the generated output. + +## Feature implementation conventions +### devcontainer-feature.json +- Keep the JSON compliant with Dev Container Features Spec v1. +- Preserve existing option names and defaults when making incremental changes. +- Any new option must be documented in the options block and reflected in install.sh and, when relevant, in tests and scenarios. +- Preserve the existing containerEnv PATH entries and feature directories: + - aem-sdk: PATH includes /aem-sdk/bin:/aem-sdk/dispatcher/bin:${PATH} + - aem-lts: PATH includes /aem-lts/bin:/aem-lts/dispatcher/bin:${PATH} + - aem-repo-tool: PATH includes /aem-repo-tool:${PATH} + - aem-universal-editor-service: PATH includes /aem-ues/bin:${PATH} +- Preserve existing mounts where applicable. For example, aem-sdk and aem-lts mount persistent author/publish volumes under their feature directories. +- Keep dependsOn and installsAfter entries when a feature relies on Java, Docker-in-Docker, or Node. + +### install.sh +- Use /usr/bin/env bash as the shebang. +- Create the feature directory with mkdir -p and write options.sh in the feature root. +- Persist feature configuration by exporting values into options.sh and sourcing it immediately after creation. +- Copy the feature-specific bin scripts into the feature directory with cp -r. +- Use idempotent logic and avoid re-downloading or re-extracting unless necessary. +- Prefer explicit validation for required inputs and fail fast when required artifacts are missing. +- Clean up temporary archives when you create them during the same layer execution. + +### bin scripts +- Source the feature options file from the feature directory. +- Use helper functions for repeated logic instead of duplicating path and lookup code. +- Follow the current naming and behavior patterns already used in this repository: + - aem-sdk: get_runmode_port, get_runmode_jar, get_aem_sdk_zip, aem_sdk_not_found + - aem-lts: get_runmode_port, get_action, get_runmode, aem_lts_err + - aem-universal-editor-service: get_ues_zip, ues_zip_not_found +- Keep command entrypoints consistent with the current user-facing commands: + - start-aem author|publish|dispatcher + - start-ues + - aem-lts start|stop author|publish [-i] + +## Feature-specific behavior to preserve +### aem-sdk +- The feature installs SDK artifacts into /aem-sdk and exposes the start-aem command. +- start-aem must auto-install the requested service jar or dispatcher tools on first use. +- The feature expects the AEM SDK archive in a local directory, usually .devcontainer, and supports automatic version selection via sdkVersion=automatic. +- Persisted state must live in the mounted volume paths under /aem-sdk/author/crx-quickstart and /aem-sdk/publish/crx-quickstart. + +### aem-lts +- The feature copies the provided quickstart jar into the feature directory and generates license.properties from the configured license values. +- The aem-lts script should support start and stop actions for author and publish. +- Interactive mode must run in the foreground, while non-interactive mode should daemonize and write logs to /var/log/aem-.log. +- Keep the current defaults for authorPort and publishPort. + +### aem-repo-tool +- The repo tool is fetched from the Adobe-Marketing-Cloud tools release endpoint. +- The feature installs the binary at /aem-repo-tool/repo and makes it executable. +- Do not introduce extra packaging or download logic unless the upstream release path changes. + +### aem-universal-editor-service +- The feature expects one or more Universal Editor Service zip files in a local directory, usually .devcontainer. +- start-ues must source NVM, install the configured Node version, install required global packages, and then launch the service plus local SSL proxies. +- The feature also provisions the content package into the AEM author install directory when aem-sdk is present. + +## Testing and validation workflow +- Use the same commands already encoded in the GitHub Actions workflows under .github/workflows/. +- Validate feature JSON files with the devcontainers action: + - devcontainers/action@v1 with validate-only: true and base-path-to-features: ./src +- Run generated tests for a specific feature against a base image: + - devcontainer features test --skip-scenarios -f -i . +- Run scenario-based tests for a feature: + - devcontainer features test -f --skip-autogenerated --skip-duplicated . +- When changing a feature, update or add test.sh and scenario definitions under test// if the change affects behavior or options. + +## Documentation and examples +- Update feature README.md and NOTES.md when user-facing behavior changes. +- Use the existing documentation patterns in the repository: + - Example usage with ghcr.io/juan-ayala/devcontainer-features/:1 + - A .devcontainer/devcontainer.json snippet that points the feature to ${containerWorkspaceFolder}/.devcontainer + - Run instructions showing the exact command the user should execute inside the devcontainer +- Keep examples consistent with the current feature IDs and option names. +- Treat README.md as generated output when feasible; prefer changing devcontainer-feature.json and letting the existing release workflow regenerate it. + +## General implementation guidance +- Prefer portable Bash and avoid shell-specific behavior that is not already used in the repository. +- Preserve the current project style: minimal scripts, direct environment usage, and explicit path handling. +- Make every change idempotent and safe to rerun. +- When adding new logic, prefer the smallest change that matches the existing patterns in neighboring feature scripts. +- If a change affects how users configure or run a feature, update both the feature JSON and the user-facing documentation. + +## What to check before you claim completion +- Verify the relevant feature JSON remains valid. +- Run the appropriate test command for the changed feature. +- If the change affects user-facing commands or options, confirm the test or scenario coverage includes that behavior. +- If the change touches shared conventions, re-check the affected feature directories and any generated README output.