diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml
index 48d684a..1ebb0a6 100644
--- a/.github/workflows/dev-build.yml
+++ b/.github/workflows/dev-build.yml
@@ -107,11 +107,27 @@ jobs:
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
+ - name: Generate Java variant metadata
+ id: java-meta
+ if: steps.extract.outputs.image-name == 'p3-sandbox-ide'
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.extract.outputs.image-name }}
+ tags: |
+ type=ref,event=branch,prefix=dev-,suffix=-latest-java
+ type=sha,prefix=dev-{{branch}}-,suffix=-java
+ type=raw,value=dev-latest-java,enable={{is_default_branch}}
+ labels: |
+ org.opencontainers.image.title=${{ steps.extract.outputs.image-name }}
+ org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
+ org.opencontainers.image.revision=${{ github.sha }}
+
- name: Build and push container image
uses: docker/build-push-action@v5
with:
context: ${{ matrix.image-path }}
file: ${{ matrix.image-path }}/Dockerfile
+ target: ${{ steps.extract.outputs.image-name == 'p3-sandbox-ide' && 'base' || '' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -120,6 +136,21 @@ jobs:
cache-to: type=gha,mode=max
provenance: false
+ - name: Build and push Java variant
+ if: steps.extract.outputs.image-name == 'p3-sandbox-ide'
+ uses: docker/build-push-action@v5
+ with:
+ context: ${{ matrix.image-path }}
+ file: ${{ matrix.image-path }}/Dockerfile
+ target: java
+ push: true
+ tags: ${{ steps.java-meta.outputs.tags }}
+ labels: ${{ steps.java-meta.outputs.labels }}
+ platforms: linux/amd64,linux/arm64
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ provenance: false
+
- name: Output build summary
run: |
echo "## 🚀 Dev Build Complete" >> $GITHUB_STEP_SUMMARY
@@ -132,4 +163,7 @@ jobs:
echo "### 📦 Published Tags:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n' >> $GITHUB_STEP_SUMMARY
+ if [[ -n "${{ steps.java-meta.outputs.tags }}" ]]; then
+ echo "${{ steps.java-meta.outputs.tags }}" | tr ',' '\n' >> $GITHUB_STEP_SUMMARY
+ fi
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
index fa98aa6..4bb99c8 100644
--- a/.github/workflows/release-build.yml
+++ b/.github/workflows/release-build.yml
@@ -148,11 +148,28 @@ jobs:
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
+ - name: Generate Java variant metadata
+ id: java-meta
+ if: needs.parse-tag.outputs.image-name == 'p3-sandbox-ide'
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ needs.parse-tag.outputs.image-name }}
+ tags: |
+ type=raw,value=${{ needs.parse-tag.outputs.version }}-java
+ type=raw,value=v${{ needs.parse-tag.outputs.major }}.${{ needs.parse-tag.outputs.minor }}-java
+ type=raw,value=v${{ needs.parse-tag.outputs.major }}-java
+ labels: |
+ org.opencontainers.image.title=${{ needs.parse-tag.outputs.image-name }}
+ org.opencontainers.image.version=${{ needs.parse-tag.outputs.version }}-java
+ org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
+ org.opencontainers.image.revision=${{ github.sha }}
+
- name: Build and push container image
uses: docker/build-push-action@v5
with:
context: ${{ needs.parse-tag.outputs.image-path }}
file: ${{ needs.parse-tag.outputs.image-path }}/Dockerfile
+ target: ${{ needs.parse-tag.outputs.image-name == 'p3-sandbox-ide' && 'base' || '' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -161,8 +178,27 @@ jobs:
cache-to: type=gha,mode=max
provenance: false
+ - name: Build and push Java variant
+ if: needs.parse-tag.outputs.image-name == 'p3-sandbox-ide'
+ uses: docker/build-push-action@v5
+ with:
+ context: ${{ needs.parse-tag.outputs.image-path }}
+ file: ${{ needs.parse-tag.outputs.image-path }}/Dockerfile
+ target: java
+ push: true
+ tags: ${{ steps.java-meta.outputs.tags }}
+ labels: ${{ steps.java-meta.outputs.labels }}
+ platforms: linux/amd64,linux/arm64
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ provenance: false
+
- name: Output build summary
run: |
+ DOCKER_TAGS="${{ steps.meta.outputs.tags }}"
+ if [[ -n "${{ steps.java-meta.outputs.tags }}" ]]; then
+ DOCKER_TAGS+=$'\n${{ steps.java-meta.outputs.tags }}'
+ fi
devbox run -- make ci-build-summary \
IMAGE_NAME="${{ needs.parse-tag.outputs.image-name }}" \
IMAGE_PATH="${{ needs.parse-tag.outputs.image-path }}" \
@@ -170,5 +206,5 @@ jobs:
GIT_TAG="${{ github.ref_name }}" \
GIT_SHA="${{ github.sha }}" \
IS_LATEST_MAJOR="${{ needs.check-latest-version.outputs.is-latest-major }}" \
- DOCKER_TAGS="${{ steps.meta.outputs.tags }}" \
+ DOCKER_TAGS="$DOCKER_TAGS" \
DOCKER_LABELS="${{ steps.meta.outputs.labels }}"
diff --git a/images/p3-sandbox-ide/Dockerfile b/images/p3-sandbox-ide/Dockerfile
index 2ce7dd0..722ecee 100644
--- a/images/p3-sandbox-ide/Dockerfile
+++ b/images/p3-sandbox-ide/Dockerfile
@@ -1,11 +1,38 @@
-FROM codercom/code-server:4.22.0
+# syntax=docker/dockerfile:1.7
+
+FROM codercom/code-server:4.132.0 AS base
COPY --chown=1000:1000 settings.json /home/coder/.local/share/code-server/Machine/settings.json
COPY code-server.yaml /etc/code-server.yaml
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY remote-shell.sh /usr/local/bin/p3-ide-remote-shell
-# Expose code-server port
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+
+FROM eclipse-temurin:21.0.8_9-jdk@sha256:7d1d666ddafac14da0ded6b4b076becf76cf88b31f9d7953a76555cc82f86511 AS java-runtime
+
+FROM base AS java
+
+USER root
+COPY --from=java-runtime /opt/java/openjdk /opt/java/openjdk
+ENV JAVA_HOME=/opt/java/openjdk
+ENV PATH="${JAVA_HOME}/bin:${PATH}"
+
+USER 1000:1000
+RUN curl --fail --location --silent --show-error \
+ https://open-vsx.org/api/redhat/java/1.55.0/file/redhat.java-1.55.0.vsix \
+ --output /tmp/redhat.java.vsix \
+ && echo "011639c3ee347b9591895bfc77d8cf28f836c053d0a49ae4a83efe9dc473a603 /tmp/redhat.java.vsix" \
+ | sha256sum --check --strict \
+ && code-server --install-extension /tmp/redhat.java.vsix \
+ && rm /tmp/redhat.java.vsix
+
+# The sandbox operator mounts the target user's home at shared/workspace, so
+# jdt.ls resolves the prepared Maven repository directly from the VM.
+COPY --chown=1000:1000 variants/java/maven-settings.xml /home/coder/.m2/settings.xml
+
+# Keep the base image's terminal and watcher defaults at Machine scope. Java
+# policy lives at User scope so code-server merges both settings layers.
+COPY --chown=1000:1000 variants/java/settings.json /home/coder/.local/share/code-server/User/settings.json
diff --git a/images/p3-sandbox-ide/README.md b/images/p3-sandbox-ide/README.md
index 491d48b..4df77fa 100644
--- a/images/p3-sandbox-ide/README.md
+++ b/images/p3-sandbox-ide/README.md
@@ -54,6 +54,34 @@ docker run -p 8080:8080 \
p3-sandbox-ide
```
+## Java Variant
+
+The `java` target adds:
+
+- Eclipse Temurin JDK 21
+- Red Hat Java support for code-server
+- Offline Maven project import and dependency-source indexing
+
+The Java image uses the same repository with a tag suffix:
+`p3-sandbox-ide:v1.4.0-java`, `p3-sandbox-ide:v1.4-java`, or
+`p3-sandbox-ide:v1-java`. Sandbox definitions select it with
+`ide.version: v1-java`.
+
+Candidate sessions are not expected to download JDKs, extensions, Maven
+artifacts, plugins, or sources. The content build must prewarm the target VM
+user's Maven repository. The sandbox operator mounts that user's home at
+`/home/coder/shared/workspace`, where the Java IDE reads `.m2/repository`
+directly. Maven import runs offline in the IDE; authoritative builds and tests
+run through the integrated terminal on the target VM.
+
+### Local Verification
+
+```bash
+docker build --target base --tag p3-sandbox-ide:dev images/p3-sandbox-ide
+docker build --target java --tag p3-sandbox-ide:dev-java images/p3-sandbox-ide
+images/p3-sandbox-ide/tests/java-image-contract.test.sh
+```
+
## Troubleshooting
If the IDE fails to start:
diff --git a/images/p3-sandbox-ide/tests/java-image-contract.test.sh b/images/p3-sandbox-ide/tests/java-image-contract.test.sh
new file mode 100755
index 0000000..a35f530
--- /dev/null
+++ b/images/p3-sandbox-ide/tests/java-image-contract.test.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+image=${IMAGE:-p3-sandbox-ide:dev-java}
+base_image=${BASE_IMAGE:-p3-sandbox-ide:dev}
+
+base_extensions=$(docker run --rm --entrypoint code-server "$base_image" \
+ --list-extensions --show-versions)
+if printf '%s\n' "$base_extensions" | grep -F 'redhat.java@' >/dev/null; then
+ printf 'base image unexpectedly contains redhat.java\n' >&2
+ exit 1
+fi
+
+if docker run --rm --entrypoint test "$base_image" -x /opt/java/openjdk/bin/java; then
+ printf 'base image unexpectedly contains the Java runtime\n' >&2
+ exit 1
+fi
+
+version=$(docker run --rm --entrypoint code-server "$image" --version)
+case "$version" in
+ "4.132.0 "*) ;;
+ *)
+ printf 'unexpected code-server host: %s\n' "$version" >&2
+ exit 1
+ ;;
+esac
+
+extensions=$(docker run --rm --entrypoint code-server "$image" \
+ --list-extensions --show-versions)
+printf '%s\n' "$extensions" | grep -Fx 'redhat.java@1.55.0' >/dev/null
+
+java_properties=$(docker run --rm --entrypoint java "$image" \
+ -XshowSettings:properties -version 2>&1)
+printf '%s\n' "$java_properties" | grep -F 'java.specification.version = 21' >/dev/null
+
+docker run --rm --entrypoint cat "$image" \
+ /home/coder/.local/share/code-server/Machine/settings.json |
+ jq -e '
+ .["files.watcherInclude"] == ["**/*", "../.ide-refresh-marker"] and
+ .["files.watcherExclude"] == {} and
+ .["terminal.integrated.profiles.linux"]["Sandbox VM"].path ==
+ "/usr/local/bin/p3-ide-remote-shell" and
+ .["terminal.integrated.defaultProfile.linux"] == "Sandbox VM"
+ ' >/dev/null
+
+docker run --rm --entrypoint cat "$image" \
+ /home/coder/.local/share/code-server/User/settings.json |
+ jq -e '
+ .["extensions.autoCheckUpdates"] == false and
+ .["extensions.autoUpdate"] == false and
+ .["telemetry.telemetryLevel"] == "off" and
+ .["redhat.telemetry.enabled"] == false and
+ .["java.jdt.ls.java.home"] == "/opt/java/openjdk" and
+ .["java.configuration.runtimes"] == [{
+ "name": "JavaSE-21",
+ "path": "/opt/java/openjdk",
+ "default": true
+ }] and
+ .["java.server.launchMode"] == "Standard" and
+ .["java.project.importOnFirstTimeStartup"] == "automatic" and
+ .["java.configuration.updateBuildConfiguration"] == "automatic" and
+ .["java.import.maven.enabled"] == true and
+ .["java.import.maven.offline.enabled"] == true and
+ .["java.import.gradle.enabled"] == false and
+ .["java.maven.downloadSources"] == false and
+ .["java.eclipse.downloadSources"] == false and
+ .["java.maven.updateSnapshots"] == false
+ ' >/dev/null
+
+maven_settings=$(docker run --rm --entrypoint cat "$image" /home/coder/.m2/settings.xml)
+printf '%s\n' "$maven_settings" |
+ grep -F '/home/coder/shared/workspace/.m2/repository' >/dev/null
+printf '%s\n' "$maven_settings" | grep -F 'true' >/dev/null
diff --git a/images/p3-sandbox-ide/variants/java/maven-settings.xml b/images/p3-sandbox-ide/variants/java/maven-settings.xml
new file mode 100644
index 0000000..a9f9a20
--- /dev/null
+++ b/images/p3-sandbox-ide/variants/java/maven-settings.xml
@@ -0,0 +1,8 @@
+
+
+ /home/coder/shared/workspace/.m2/repository
+ true
+
diff --git a/images/p3-sandbox-ide/variants/java/settings.json b/images/p3-sandbox-ide/variants/java/settings.json
new file mode 100644
index 0000000..3434c0c
--- /dev/null
+++ b/images/p3-sandbox-ide/variants/java/settings.json
@@ -0,0 +1,26 @@
+{
+ "extensions.autoCheckUpdates": false,
+ "extensions.autoUpdate": false,
+ "telemetry.telemetryLevel": "off",
+ "redhat.telemetry.enabled": false,
+ "java.jdt.ls.java.home": "/opt/java/openjdk",
+ "java.configuration.runtimes": [
+ {
+ "name": "JavaSE-21",
+ "path": "/opt/java/openjdk",
+ "default": true
+ }
+ ],
+ "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xms100m -Xmx2G -Xlog:disable",
+ "java.server.launchMode": "Standard",
+ "java.project.importOnFirstTimeStartup": "automatic",
+ "java.configuration.updateBuildConfiguration": "automatic",
+ "java.import.maven.enabled": true,
+ "java.import.maven.offline.enabled": true,
+ "java.import.gradle.enabled": false,
+ "java.maven.downloadSources": false,
+ "java.eclipse.downloadSources": false,
+ "java.maven.updateSnapshots": false,
+ "java.configuration.detectJdksAtStart": false,
+ "java.showBuildStatusOnStart.enabled": "notification"
+}
diff --git a/images/p3-sandbox-sshfs-init/mount-sshfs.sh b/images/p3-sandbox-sshfs-init/mount-sshfs.sh
index 257946a..5c190dc 100644
--- a/images/p3-sandbox-sshfs-init/mount-sshfs.sh
+++ b/images/p3-sandbox-sshfs-init/mount-sshfs.sh
@@ -126,7 +126,6 @@ sshfs_cmd=(sshfs
-o gid=$SSHFS_GID
-o ServerAliveInterval=15
-o ServerAliveCountMax=3
- -o sshfs_debug
)
# Mount the remote filesystem using SSHFS
@@ -166,50 +165,10 @@ fi
echo "SUCCESS: Remote filesystem mounted successfully at $MOUNT_POINT"
-# Start background file watcher to trigger IDE refresh on remote changes
-start_file_watcher() {
- local marker_file="$1/.ide-refresh-marker"
- local watch_interval=${FILE_WATCH_INTERVAL:-2}
-
- (
- echo "Starting file watcher for $MOUNT_POINT (interval: ${watch_interval}s)"
- local last_hash=""
-
- while true; do
- if mount | grep -q "$MOUNT_POINT"; then
- # Generate hash of directory structure and file timestamps
- local current_hash
- if current_hash=$(find "$MOUNT_POINT" -type f -exec stat -c '%n %Y %s' {} \; 2>/dev/null | sort | sha256sum | cut -d' ' -f1 2>/dev/null); then
- if [ "$current_hash" != "$last_hash" ] && [ -n "$last_hash" ]; then
- echo "File changes detected, triggering IDE refresh"
- touch "$marker_file" 2>/dev/null || true
- fi
- last_hash="$current_hash"
- fi
- fi
- sleep "$watch_interval"
- done
- ) &
-
- # Store PID for cleanup
- echo $! > /tmp/file_watcher.pid
- echo "File watcher started with PID $(cat /tmp/file_watcher.pid)"
-}
-
# Enhanced cleanup handler
cleanup() {
echo "Received termination signal, cleaning up..."
- # Kill file watcher if running
- if [ -f /tmp/file_watcher.pid ]; then
- local watcher_pid=$(cat /tmp/file_watcher.pid)
- if kill -0 "$watcher_pid" 2>/dev/null; then
- echo "Stopping file watcher (PID: $watcher_pid)"
- kill "$watcher_pid" 2>/dev/null || true
- fi
- rm -f /tmp/file_watcher.pid
- fi
-
# Unmount SSHFS
retry_count=0
while [ $retry_count -lt 3 ]; do
@@ -234,9 +193,6 @@ cleanup() {
exit 0
}
-# Start the file watcher
-start_file_watcher "$(dirname "$MOUNT_POINT")"
-
# Create a monitoring loop to ensure mount stays active
# Use shorter sleep intervals to improve signal responsiveness
while true; do
diff --git a/scripts/ci/build-dev-image.sh b/scripts/ci/build-dev-image.sh
index 8541cd6..96d15d2 100755
--- a/scripts/ci/build-dev-image.sh
+++ b/scripts/ci/build-dev-image.sh
@@ -82,12 +82,12 @@ echo " SHA: $SHORT_SHA"
echo ""
# Build the image with multiple tags
-TAGS="--tag $DEV_TAG --tag $BRANCH_LATEST_TAG"
+TAGS=(--tag "$DEV_TAG" --tag "$BRANCH_LATEST_TAG")
# Add dev-latest tag only for main branch
if [ "$BRANCH_NAME" = "main" ]; then
DEV_LATEST_TAG="${FULL_IMAGE_NAME}:dev-latest"
- TAGS="$TAGS --tag $DEV_LATEST_TAG"
+ TAGS+=(--tag "$DEV_LATEST_TAG")
echo " Tags: dev-${BRANCH_NAME}-${SHORT_SHA}, dev-${BRANCH_NAME}-latest, dev-latest"
else
echo " Tags: dev-${BRANCH_NAME}-${SHORT_SHA}, dev-${BRANCH_NAME}-latest"
@@ -102,20 +102,39 @@ if [ -n "$REPO_URL" ]; then
REPO_URL=$(echo "$REPO_URL" | sed 's|git@github.com:|https://github.com/|' | sed 's|\.git$||')
fi
-# Build the image
-docker build $TAGS \
- --label "org.opencontainers.image.source=${REPO_URL}" \
- --label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \
- --label "org.opencontainers.image.created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
- --label "org.opencontainers.image.description=Dev build of ${IMAGE_NAME} from ${BRANCH_NAME}" \
- --label "org.opencontainers.image.licenses=Proprietary" \
- --label "dev.lf-certification.image.path=$IMAGE_PATH" \
- --label "dev.lf-certification.build.branch=$BRANCH_NAME" \
- "$IMAGE_PATH"
+LABELS=(
+ --label "org.opencontainers.image.source=${REPO_URL}"
+ --label "org.opencontainers.image.revision=$(git rev-parse HEAD)"
+ --label "org.opencontainers.image.created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
+ --label "org.opencontainers.image.description=Dev build of ${IMAGE_NAME} from ${BRANCH_NAME}"
+ --label "org.opencontainers.image.licenses=Proprietary"
+ --label "dev.lf-certification.image.path=$IMAGE_PATH"
+ --label "dev.lf-certification.build.branch=$BRANCH_NAME"
+)
+
+BUILD_COMMAND=(docker build)
+if [ "$IMAGE_NAME" = "p3-sandbox-ide" ]; then
+ BUILD_COMMAND+=(--target base)
+fi
+
+"${BUILD_COMMAND[@]}" "${TAGS[@]}" "${LABELS[@]}" "$IMAGE_PATH"
+
+if [ "$IMAGE_NAME" = "p3-sandbox-ide" ]; then
+ JAVA_DEV_TAG="${DEV_TAG}-java"
+ JAVA_BRANCH_LATEST_TAG="${BRANCH_LATEST_TAG}-java"
+ JAVA_TAGS=(--tag "$JAVA_DEV_TAG" --tag "$JAVA_BRANCH_LATEST_TAG")
+ if [ "$BRANCH_NAME" = "main" ]; then
+ JAVA_DEV_LATEST_TAG="${DEV_LATEST_TAG}-java"
+ JAVA_TAGS+=(--tag "$JAVA_DEV_LATEST_TAG")
+ fi
+
+ echo ""
+ echo "Building Java variant..."
+ docker build --target java "${JAVA_TAGS[@]}" "${LABELS[@]}" "$IMAGE_PATH"
+fi
echo "✅ Build complete!"
-# Push if requested
if [ "$PUSH_IMAGE" = true ]; then
echo ""
echo "Pushing to registry..."
@@ -126,6 +145,14 @@ if [ "$PUSH_IMAGE" = true ]; then
docker push "$DEV_LATEST_TAG"
fi
+ if [ "$IMAGE_NAME" = "p3-sandbox-ide" ]; then
+ docker push "$JAVA_DEV_TAG"
+ docker push "$JAVA_BRANCH_LATEST_TAG"
+ if [ "$BRANCH_NAME" = "main" ]; then
+ docker push "$JAVA_DEV_LATEST_TAG"
+ fi
+ fi
+
echo "✅ Push complete!"
echo ""
echo "Published tags:"
@@ -134,4 +161,11 @@ if [ "$PUSH_IMAGE" = true ]; then
if [ "$BRANCH_NAME" = "main" ]; then
echo " $DEV_LATEST_TAG"
fi
+ if [ "$IMAGE_NAME" = "p3-sandbox-ide" ]; then
+ echo " $JAVA_DEV_TAG"
+ echo " $JAVA_BRANCH_LATEST_TAG"
+ if [ "$BRANCH_NAME" = "main" ]; then
+ echo " $JAVA_DEV_LATEST_TAG"
+ fi
+ fi
fi
diff --git a/scripts/ci/detect-changed-images.sh b/scripts/ci/detect-changed-images.sh
index 468d419..e5c278a 100755
--- a/scripts/ci/detect-changed-images.sh
+++ b/scripts/ci/detect-changed-images.sh
@@ -51,8 +51,28 @@ if [ -z "$CHANGED_FILES" ]; then
exit 0
fi
-# Extract unique image directories (top-level only)
-CHANGED_IMAGES=$(echo "$CHANGED_FILES" | sed 's|^images/\([^/]*\)/.*|images/\1|; s|^images/\([^/]*\)$|images/\1|' | grep "^images/" | sort -u)
+# Extract unique image directories (top-level only), excluding image roots
+# deleted by the change under inspection.
+CHANGED_IMAGES=$(
+ echo "$CHANGED_FILES" |
+ sed 's|^images/\([^/]*\)/.*|images/\1|; s|^images/\([^/]*\)$|images/\1|' |
+ grep "^images/" |
+ sort -u |
+ while read -r image; do
+ if [ -f "$image/Dockerfile" ]; then
+ echo "$image"
+ fi
+ done
+)
+
+if [ -z "$CHANGED_IMAGES" ]; then
+ if [ "$OUTPUT_FORMAT" = "json" ]; then
+ echo '{"has_changes": false, "changed_images": []}'
+ else
+ echo "No buildable image changes detected in images/"
+ fi
+ exit 0
+fi
if [ "$OUTPUT_FORMAT" = "json" ]; then
# Output JSON format for CI consumption