From df68e7114cf333f7e6873e58f431ac7392422c94 Mon Sep 17 00:00:00 2001 From: Bose Date: Thu, 23 Jul 2026 15:54:55 +0530 Subject: [PATCH 1/6] feat(devspaces): add workspace devfile, startup scripts, and Dev Spaces guide --- devfile.yaml | 446 +++++++++++++++++++++++ docs/rhdh-local-guide/devspaces-guide.md | 172 +++++++++ docs/rhdh-local-guide/index.md | 1 + mkdocs.yaml | 1 + scripts/start-orchestrator.sh | 222 +++++++++++ scripts/start-rhdh.sh | 0 6 files changed, 842 insertions(+) create mode 100644 devfile.yaml create mode 100644 docs/rhdh-local-guide/devspaces-guide.md create mode 100644 scripts/start-orchestrator.sh create mode 100644 scripts/start-rhdh.sh diff --git a/devfile.yaml b/devfile.yaml new file mode 100644 index 00000000..f0a47895 --- /dev/null +++ b/devfile.yaml @@ -0,0 +1,446 @@ +schemaVersion: 2.3.0 +metadata: + name: rhdh-local + version: 1.0.0 + displayName: Plugin Testing on Dev Spaces + description: >- + Dev Spaces workspace for developing and testing a Backstage dynamic plugin + against a local RHDH instance with SonataFlow. +attributes: + controller.devfile.io/storage-type: per-workspace + +projects: + # These repos are cloned alongside your plugin repo (which is auto-cloned) + - name: rhdh-local + git: + remotes: + origin: https://github.com/redhat-developer/rhdh-local.git +components: + - name: tools + container: + image: quay.io/devfile/universal-developer-image:ubi9-latest + mountSources: true + memoryLimit: 6Gi + memoryRequest: 521Mi + cpuLimit: "2" + cpuRequest: "500m" + env: + - name: DEVSPACE_SCRIPTS_DIR + value: /projects/rhdh-local + - name: CLUSTER_APPS_DOMAIN + value: "apps.example.com" + # ── Plugin Dev Settings ── + - name: BACKSTAGE_APP_NAME + value: backstage + - name: BACKSTAGE_APP_DIR + value: /projects/backstage + - name: PLUGIN_NAME + value: hello-world # changeme for custom plugin name (e.g., my-plugin) + - name: CREATE_APP_VERSION + value: "0.7.6" + - name: BACKSTAGE_YARN_VERSION + value: "3.8.7" + - name: PROXY_HOST + value: "" + - name: PROXY_PORT + value: "" + - name: DEV_FRONTEND_PORT + value: "3000" + - name: DEV_BACKEND_PORT + value: "7008" + endpoints: + - name: ide + exposure: public + targetPort: 3100 + protocol: https + attributes: + discoverable: true + urlRewriteSupported: true + - name: webpack-dev + exposure: public + targetPort: 3000 + protocol: https + attributes: + discoverable: true + urlRewriteSupported: false + - name: backstage-dev + exposure: public + targetPort: 7008 + protocol: https + attributes: + discoverable: true + urlRewriteSupported: false + + - name: rhdh + container: + image: quay.io/rhdh-community/rhdh:1.10 + command: ["tail"] + args: ["-f", "/dev/null"] + mountSources: true + memoryLimit: 6Gi + memoryRequest: 1Gi + cpuLimit: "2" + cpuRequest: "500m" + env: + - name: DEVSPACE_SCRIPTS_DIR + value: /projects/rhdh-local + - name: BASE_URL + value: auto + - name: RHDH_PUBLIC_ENDPOINT_NAME + value: rhdh + - name: CLUSTER_APPS_DOMAIN + value: "apps.example.com" + - name: NODE_ENV + value: development + - name: NODE_TLS_REJECT_UNAUTHORIZED + value: "0" + - name: NODE_OPTIONS + value: --inspect=0.0.0.0:9229 --no-node-snapshot + - name: CATALOG_INDEX_IMAGE + value: quay.io/rhdh/plugin-catalog-index:1.9 + - name: WAIT_FOR_PLUGINS_TIMEOUT + value: "180" + - name: CATALOG_ENTITIES_EXTRACT_DIR + value: /opt/app-root/src/extensions + - name: ENABLE_AUTH_PROVIDER_MODULE_OVERRIDE + value: "true" + - name: ENABLE_CORE_ROOTHTTPROUTER_OVERRIDE + value: "true" + endpoints: + - name: rhdh + exposure: public + targetPort: 7007 + protocol: https + attributes: + discoverable: true + urlRewriteSupported: false + - name: node-debug + exposure: internal + targetPort: 9229 + protocol: tcp + volumeMounts: + - name: dynamic-plugins-root + path: /opt/app-root/src/dynamic-plugins-root + - name: extensions-catalog + path: /opt/app-root/src/extensions + - name: rhdh-generated + path: /opt/app-root/src/generated + + - name: dynamic-plugins-root + volume: + size: 2Gi + + - name: extensions-catalog + volume: + size: 1Gi + + - name: rhdh-generated + volume: + size: 512Mi + + - name: sonataflow + container: + image: registry.redhat.io/openshift-serverless-1/logic-swf-devmode-rhel9:1.37.2 + command: ["tail"] + args: ["-f", "/dev/null"] + mountSources: true + memoryLimit: 6Gi + memoryRequest: 1Gi + cpuLimit: "2" + cpuRequest: "500m" + env: + - name: WORKFLOW_REPO_DIR + value: /projects/rhdh-local + - name: DEVSPACE_SCRIPTS_DIR + value: /projects/rhdh-local + # ── Network & Proxy Settings (can be overridden at workspace level) ── + - name: PROXY_HOST + value: "internet.gcp.ford.com" + - name: PROXY_PORT + value: "83" + - name: NON_PROXY_HOSTS + value: "172.30.0.1|localhost|127.0.0.0/8|19.0.0.0/8|10.0.0.0/8|172.16.0.0/12|192.168.0.0/16|*.ford.com|ford.com|*.appspot.com|appspot.com|*.cloudfunctions.net|cloudfunctions.net|*.cloudproxy.app|cloudproxy.app|*.composer.cloud.google.com|composer.cloud.google.com|*.composer.googleusercontent.com|composer.googleusercontent.com|*.datafusion.cloud.google.com|datafusion.cloud.google.com|*.datafusion.googleusercontent.com|datafusion.googleusercontent.com|*.gcr.io|gcr.io|*.internal|internal|*.googleadapis.com|googleadapis.com|*.googleapis.com|googleapis.com|*.gstatic.com|gstatic.com|*.ltsapis.goog|ltsapis.goog|*.packages.cloud.google.com|packages.cloud.google.com|*.pkg.dev|pkg.dev|*.pki.goog|pki.goog|*.run.app|run.app|*.source.developers.google.com|source.developers.google.com|*.gcp.cloud.es.io|gcp.cloud.es.io|*.smt-gce.susecloud.net|smt-gce.susecloud.net|*.fordcredit.com" + # ── QUARKUS & Kogito Settings ── + - name: QUARKUS_HTTP_PORT + value: "8899" + - name: KOGITO_SERVICE_URL + value: http://localhost:8899 + - name: KOGITO.CODEGEN.PROCESS.FAILONERROR + value: "false" + # ── Maven & Build Settings ── + - name: MAX_YAML_CODE_POINTS + value: "35000000" + - name: MAX_ENTRY_SIZE + value: "30000000" + - name: MAVEN_ARGS_APPEND + value: >- + -s /home/kogito/.m2/settings.xml + -DmaxYamlCodePoints=${MAX_YAML_CODE_POINTS} + -T1C + -Dmaven.artifact.threads=8 + -Djava.net.useSystemProxies=false + - name: QUARKUS_EXTENSIONS + value: >- + io.quarkiverse.openapi.generator:quarkus-openapi-generator:2.12.0, + org.kie:kie-addons-quarkus-monitoring-sonataflow, + io.quarkus:quarkus-resteasy-client-oidc-filter, + org.kie:kogito-addons-quarkus-jobs-knative-eventing + - name: BACKSTAGE_NOTIFICATIONS_URL + value: http://localhost:7007 + - name: QUARKUS_PROFILE + value: "local-dev" + endpoints: + - name: sonataflow + exposure: public + targetPort: 8899 + protocol: https + attributes: + discoverable: true + urlRewriteSupported: false + volumeMounts: + - name: sonataflow-m2-cache + path: /home/kogito/.m2/repository + - name: sonataflow-workflows + path: /home/kogito/serverless-workflow-project/src/main/resources + + - name: sonataflow-m2-cache + volume: + size: 2Gi + + - name: sonataflow-workflows + volume: + size: 256Mi + + - name: projects + volume: + size: 20Gi + +commands: + # ── RHDH Infrastructure ────────────────────────────────────────────────── + - id: start-rhdh + exec: + component: rhdh + workingDir: /projects/rhdh-local + commandLine: | + chmod +x "/projects/rhdh-local/scripts/start-rhdh.sh" 2>/dev/null + nohup bash -lc "/projects/rhdh-local/scripts/start-rhdh.sh && echo STARTED > /tmp/rhdh-status || echo FAILED > /tmp/rhdh-status" > /tmp/rhdh-start.log 2>&1 & + group: + kind: run + isDefault: true + hotReloadCapable: false + + - id: restart-rhdh + exec: + component: rhdh + workingDir: /projects/rhdh-local + commandLine: | + echo "[rhdh] Stopping RHDH..." + # Find and kill the exact PID holding port 7007 (hex 1B5F) + kill_port_7007() { + for fd in /proc/[0-9]*/fd/*; do + link=$(readlink "$fd" 2>/dev/null) || continue + case "$link" in socket:*) + inode=${link#socket:[}; inode=${inode%]} + if grep ':1B5F ' /proc/net/tcp6 2>/dev/null | grep -q "$inode"; then + pid=$(echo "$fd" | cut -d/ -f3) + echo " killing PID $pid (port 7007)" + kill "$1" "$pid" 2>/dev/null || true + return 0 + fi + ;; esac + done + return 1 + } + # Graceful kill + kill_port_7007 "" && sleep 2 + # Force kill if still there + kill_port_7007 "-9" && sleep 1 + # Also kill any remaining node processes + pkill -9 -f 'node.*packages/backend' 2>/dev/null || true + sleep 1 + # Verify port is free + for i in $(seq 1 15); do + if ! grep -q ':1B5F ' /proc/net/tcp6 2>/dev/null && \ + ! grep -q ':1B5F ' /proc/net/tcp 2>/dev/null; then + echo "[rhdh] Port 7007 is free" + break + fi + echo " port 7007 still in use ($i/15)..." + kill_port_7007 "-9" + sleep 1 + done + echo "[rhdh] Starting RHDH..." + chmod +x "/projects/rhdh-local/scripts/start-rhdh.sh" 2>/dev/null + "/projects/rhdh-local/scripts/start-rhdh.sh" + group: + kind: run + + # ── Plugin Dev ──────────────────────────────────────────────────────── + + # One-time: scaffold a new Backstage app + plugin (run once per workspace) + - id: develop-backstage-plugin + exec: + component: tools + workingDir: /projects + commandLine: | + set -e + _APP="${BACKSTAGE_APP_NAME:-backstage}" + _PLUGIN="${PLUGIN_NAME:-hello-world}" + _CREATE_VER="${CREATE_APP_VERSION:-0.7.6}" + _YARN_VER="${BACKSTAGE_YARN_VERSION:-3.8.7}" + + # Configure yarn proxy only when explicit proxy env vars are provided. + if [ -n "${PROXY_HOST:-}" ] && [ -n "${PROXY_PORT:-}" ]; then + _PROXY="http://${PROXY_HOST}:${PROXY_PORT}" + cat > .yarnrc.yml </dev/null + + # Force dev server to bind to 0.0.0.0 so the OpenShift route can reach it + if ! grep -q 'listen:' "$_APP_DIR"/app-config*.yaml 2>/dev/null; then + sed -i "/^app:/a\\ listen:\\n host: 0.0.0.0\\n port: $_FE_PORT" "$_APP_DIR"/app-config*.yaml + fi + + # Allow external host headers (webpack-dev route hostname) + find "$_APP_DIR/node_modules/@backstage/cli/dist" -name 'server.cjs.js' \ + -exec sed -i 's/allowedHosts: \[url.hostname\]/allowedHosts: "all"/' {} \; + + cd "$_APP_DIR/plugins/$_PLUGIN" + BROWSER=none yarn start + group: + kind: run + + - id: deploy-dynamic-plugin + exec: + component: tools + commandLine: | + set -e + _APP_DIR="${BACKSTAGE_APP_DIR:-/projects/${BACKSTAGE_APP_NAME:-backstage}}" + _PLUGIN="${PLUGIN_NAME:-}" + + _SCRIPTS_DIR="${DEVSPACE_SCRIPTS_DIR:-/projects/rhdh-devspace}" + + # Prompt for plugin name when PLUGIN_NAME is not set + if [ -z "$_PLUGIN" ] && [ -t 0 ]; then + read -r -p "[plugin] Enter plugin name: " _PLUGIN + fi + [ -z "$_PLUGIN" ] && { + echo "[plugin] ERROR: Plugin name is required (set PLUGIN_NAME or provide input)" + exit 1 + } + + echo "[plugin] Deploying the plugin: $_PLUGIN" + + mkdir -p "$_SCRIPTS_DIR/dynamic-root-plugins" + DYNAMIC_ROOT=$(find /projects -maxdepth 3 -type d -name 'dynamic-root*-plugins' 2>/dev/null | head -1) + [ -z "$DYNAMIC_ROOT" ] && { echo "ERROR: No dynamic-root-plugins found under /projects"; exit 1; } + + _PLUGIN_DIR="$_APP_DIR/plugins/$_PLUGIN" + + cd "$_PLUGIN_DIR" + + yarn add -D @red-hat-developer-hub/cli + + npm pkg set scripts.export-dynamic="rhdh-cli plugin export --clean" + + yarn export-dynamic + + echo "=== Exporting as dynamic plugin ===" + npx --yes @red-hat-developer-hub/cli@latest plugin export --dev \ + --dynamic-plugins-root "$DYNAMIC_ROOT" + group: + kind: build + + # ── SonataFlow ─────────────────────────────────────────────────────────── + - id: start-sonataflow + exec: + component: sonataflow + workingDir: /home/kogito/serverless-workflow-project + commandLine: | + chmod +x "/projects/rhdh-devspaces/start-sonataflow.sh" 2>/dev/null + "/projects/rhdh-devspaces/start-sonataflow.sh" + group: + kind: run + + - id: restart-sonataflow + exec: + component: sonataflow # 5005, 8899 + commandLine: | + echo "[sonataflow] Stopping SonataFlow server..." + # Find PID bound to tcp port 8899 (hex 22C3) without requiring lsof/netstat/fuser. + find_pid_by_port_hex() { + _hex_port="$1" + for fd in /proc/[0-9]*/fd/*; do + link=$(readlink "$fd" 2>/dev/null) || continue + case "$link" in socket:*) + inode=${link#socket:[}; inode=${inode%]} + if grep ":${_hex_port} " /proc/net/tcp6 2>/dev/null | grep -q "$inode" || \ + grep ":${_hex_port} " /proc/net/tcp 2>/dev/null | grep -q "$inode"; then + echo "$fd" | cut -d/ -f3 + return 0 + fi + ;; esac + done + return 1 + } + + PID=$(find_pid_by_port_hex 22C3) || true + if [ -n "${PID:-}" ]; then + kill "$PID" 2>/dev/null && echo "[sonataflow] ✓ Stopped SonataFlow server (PID $PID)" || \ + { kill -9 "$PID" 2>/dev/null; echo "[sonataflow] ✓ Force-killed SonataFlow server (PID $PID)"; } + else + echo "[sonataflow] SonataFlow server is not running on port 8899" + fi + + echo "[sonataflow] Starting sonataflow server..." + chmod +x "/projects/rhdh-devspaces/start-sonataflow.sh" 2>/dev/null + "/projects/rhdh-devspaces//start-sonataflow.sh" + group: + kind: run +events: + postStart: + - start-rhdh diff --git a/docs/rhdh-local-guide/devspaces-guide.md b/docs/rhdh-local-guide/devspaces-guide.md new file mode 100644 index 00000000..9a091553 --- /dev/null +++ b/docs/rhdh-local-guide/devspaces-guide.md @@ -0,0 +1,172 @@ +This guide explains how to run this repository in OpenShift Dev Spaces using the workspace [devfile.yaml](../../devfile.yaml). + +## What This Devfile Provides + +The workspace starts three main containers: + +- `tools`: Backstage and plugin development tasks +- `rhdh`: Red Hat Developer Hub runtime +- `sonataflow`: SonataFlow workflow runtime + +It also provisions persistent volumes for dynamic plugins, generated assets, Maven cache, and workspace projects. + +## Prerequisites + +Before using this devfile, ensure you have: + +1. Access to an OpenShift cluster with Dev Spaces installed +2. Permissions to create Dev Spaces workspaces +3. Network access to required image registries +4. Enough cluster resources (recommended: at least 2 CPU and 6Gi memory for each main runtime container) + +## Create A Workspace From This Devfile + +1. Open Dev Spaces and create a workspace from your fork/branch of this repository. +2. Make sure the workspace uses [devfile.yaml](../../devfile.yaml). +3. Start the workspace. + +The post-start event runs command `start-rhdh`, which calls: + +```bash +/projects/rhdh-local/scripts/start-rhdh.sh +``` + +This script prepares config links, dynamic plugin layout, and starts the RHDH backend. + +## Endpoints Exposed By The Workspace + +- RHDH UI/API: endpoint `rhdh` on port `7007` +- Plugin dev frontend: endpoint `webpack-dev` on port `3000` +- Plugin dev backend: endpoint `backstage-dev` on port `7008` +- SonataFlow: endpoint `sonataflow` on port `8899` +- Node inspector (internal): endpoint `node-debug` on port `9229` + +## Command Reference + +The following commands are defined in [devfile.yaml](../../devfile.yaml): + +### RHDH Infrastructure + +- `start-rhdh`: Starts RHDH asynchronously via `scripts/start-rhdh.sh` and writes status into `/tmp/rhdh-status`. +- `restart-rhdh`: Stops any process bound to port `7007` and starts RHDH again. + +### Plugin Development + +- `develop-backstage-plugin`: Scaffolds a Backstage app and frontend plugin in `/projects`. +- `start-plugin-dev`: Starts plugin dev mode and patches ports to avoid conflict with RHDH. +- `deploy-dynamic-plugin`: Builds and exports your plugin into a detected dynamic plugins root. + +### SonataFlow + +- `start-sonataflow`: Starts SonataFlow runtime. +- `restart-sonataflow`: Restarts SonataFlow runtime by freeing port `8899` and relaunching. + +SonataFlow does not start automatically. Start it after RHDH is up and running by running the `start-sonataflow` task in the terminal. The first startup can take about 15 minutes, and subsequent restarts usually take around 2 minutes. + +## Typical Development Flows + +### Flow 1: Start And Validate RHDH + +1. Start the workspace. +2. Wait for `start-rhdh` to complete. +3. Open the `rhdh` public endpoint. +4. Confirm the RHDH home page loads. + +If startup fails, check: + +```bash +cat /tmp/rhdh-status +cat /tmp/rhdh-start.log +``` + +### Flow 2: Build And Test A Dynamic Plugin + +1. Run `develop-backstage-plugin` once (per new workspace). +2. Run `start-plugin-dev` during active plugin coding. +3. Run `deploy-dynamic-plugin` after code changes you want to test in RHDH. +4. Run `restart-rhdh` to reload backend and plugin wiring. +5. Open the RHDH route and test your plugin page. + +### Flow 3: Work With SonataFlow + +1. Run `start-sonataflow` to launch workflows. +2. Use `restart-sonataflow` after workflow or config changes. +3. Validate on the SonataFlow endpoint. + +## Configuration Notes + +- Default environment variables are defined in [devfile.yaml](../../devfile.yaml). +- RHDH startup behavior is controlled by [scripts/start-rhdh.sh](../../scripts/start-rhdh.sh). +- RHDH app config and dynamic plugin files are sourced from [configs](../../configs). +- Plugin defaults are: + - `BACKSTAGE_APP_NAME=backstage` + - `BACKSTAGE_APP_DIR=/projects/backstage` + - `PLUGIN_NAME=hello-world` + +## Troubleshooting + +### Post-start does not launch RHDH + +- Confirm the event exists: + - `events.postStart: start-rhdh` +- Confirm command id exists: + - `commands[].id: start-rhdh` + +### Script permission errors + +Run: + +```bash +chmod +x /projects/rhdh-local/scripts/start-rhdh.sh +``` + +### RHDH not reachable + +1. Check `rhdh` container logs. +2. Check `/tmp/rhdh-start.log` in the `rhdh` container. +3. Run `restart-rhdh`. + +### Port conflicts during plugin dev + +- `start-plugin-dev` adjusts the plugin backend to `7008`. +- Keep RHDH on `7007`. + +### Dynamic plugin not visible in RHDH + +1. Re-run `deploy-dynamic-plugin`. +2. Re-run `restart-rhdh`. +3. Verify plugin export succeeded and target dynamic root exists. + +### To download private repository plugins, create a Kubernetes secret in the format below + +When the RHDH startup script runs, it reads this secret, creates an `.npmrc` file, and uses it to download private repository plugins. + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: rhdh-secrets + namespace: + labels: + controller.devfile.io/mount-to-devworkspace: "true" + controller.devfile.io/watch-secret: "true" + annotations: + controller.devfile.io/mount-as: env +stringData: + # ── NPM Registry Configuration ────────────────────────────────────────── + # The .npmrc content with all registry mappings and auth tokens. + NPMRC_CONTENT_B64: + | # Replace in the .npmrc content below + registry=https://registry.npmjs.org/ + @redhat:registry=https://npm.registry.redhat.com + @cloudtooling:registry=https://jfrog.ford.com/artifactory/api/npm/cloudtooling-npm-repository/ + //jfrog.com/artifactory/api/npm/npmjs/:_authToken= + //jfrog.com/artifactory/api/npm/cloudtooling-npm-repository/:_authToken= +type: Opaque +``` + +## Recommended Next Steps + +- Add this page to your onboarding checklist for Dev Spaces users. +- Keep command ids stable in [devfile.yaml](../../devfile.yaml) to prevent event wiring issues. +- If you rename scripts under [scripts](../../scripts), update all related command paths in [devfile.yaml](../../devfile.yaml). diff --git a/docs/rhdh-local-guide/index.md b/docs/rhdh-local-guide/index.md index 8a480223..d63e498a 100644 --- a/docs/rhdh-local-guide/index.md +++ b/docs/rhdh-local-guide/index.md @@ -74,6 +74,7 @@ To use RHDH Local effectively, you need: Ready to dive in? Start with our [Getting Started Guide](getting-started.md) for a quick setup, or explore specific topics: - [Configuration](configuration.md) - Understand RHDH Local configuration +- [Dev Spaces Guide](devspaces-guide.md) - Run and operate RHDH Local in OpenShift Dev Spaces - [Loading Content](loading-content.md) - Configure catalogs, templates, and TechDocs - [Dynamic Plugin Management](dynamic-plugins-management.md) - Add, remove, and configure plugins - [Local Plugin Development](plugins-guide.md) - Build plugins locally diff --git a/mkdocs.yaml b/mkdocs.yaml index bf09b52b..f0831597 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -26,6 +26,7 @@ nav: - "RHDH Local User Guide": - "What is RHDH Local And Why Use It?": rhdh-local-guide/index.md - "Getting Started With RHDH Local": rhdh-local-guide/getting-started.md + - "Dev Spaces Guide": rhdh-local-guide/devspaces-guide.md - "Configuration": rhdh-local-guide/configuration.md - "Loading Content": rhdh-local-guide/loading-content.md - "Dynamic Plugin Management": rhdh-local-guide/dynamic-plugins-management.md diff --git a/scripts/start-orchestrator.sh b/scripts/start-orchestrator.sh new file mode 100644 index 00000000..e665e25d --- /dev/null +++ b/scripts/start-orchestrator.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +# SonataFlow Dev Spaces entrypoint. +set -euo pipefail + +# Auto-discover workflow repos: any /projects/*/workflows dir is picked up. +# To pin specific roots instead, set WF_ROOTS (colon-separated): +# WF_ROOTS="/projects/workflow-repo/workflows:/projects/wf-app-registration/workflows" + +PROJECTS_DIR="/projects" +WF_DIR="/home/kogito/serverless-workflow-project" +LOG="/tmp/sonataflow-start.log" + +log() { echo "[sonataflow] $*"; } +warn() { echo "[sonataflow] WARN: $*"; } + +# Source the dev .env file so secrets like CLDCTL_GITHUB_TOKEN are available +# to the Quarkus JVM process started later in this script. +_env_file="${PROJECTS_DIR}/rhdh-local/.env" +if [ -f "$_env_file" ]; then + set -a + # shellcheck source=/dev/null + source "$_env_file" + set +a + log "Sourced env from $_env_file (CLDCTL_GITHUB_TOKEN set=$([ -n "${CLDCTL_GITHUB_TOKEN:-}" ] && echo yes || echo NO))" +else + warn ".env not found at $_env_file — CLDCTL_GITHUB_TOKEN will be empty; check WORKFLOW_REPO_DIR" +fi + +# Build the list of workflow roots +_wf_roots=() +if [[ -n "${WF_ROOTS:-}" ]]; then + IFS=':' read -ra _wf_roots <<< "$WF_ROOTS" +else + for d in "$PROJECTS_DIR"/*/workflows; do + [ -d "$d" ] && _wf_roots+=("$d") + done +fi +log "Workflow roots: ${_wf_roots[*]:-}" + +# --restart: kill existing process and clean stale files so edits are picked up +# if [[ "${1:-}" == "--restart" ]]; then +log "Stopping existing SonataFlow process..." +pkill -f 'java.*quarkus' 2>/dev/null || true + +# Wait until the old process is fully gone and port 8899 is free +for _w in $(seq 1 30); do + if ! pgrep -f 'java.*quarkus' >/dev/null 2>&1; then + log "Old process terminated after ${_w}s" + break + fi + if [ "$_w" -eq 15 ]; then + warn "Process still alive after 15s — sending SIGKILL" + pkill -9 -f 'java.*quarkus' 2>/dev/null || true + fi + sleep 1 +done + +# Ensure port 8899 is free +for _w in $(seq 1 10); do + if ! curl -sf http://localhost:8899/q/health >/dev/null 2>&1; then + break + fi + sleep 1 +done + +log "Cleaning workflow resources for fresh copy..." +rm -rf "$WF_DIR/src/main/resources/"* 2>/dev/null || true +rm -rf "$WF_DIR/src/main/java/"* 2>/dev/null || true +rm -rf "$WF_DIR/target/" 2>/dev/null || true +log "Cleaned sources and build cache" + +# Truncate old log so health check only sees new process output +: > "$LOG" +# fi + +# --- settings.xml: always write a clean file with Red Hat repos and correct proxy --- +mkdir -p /home/kogito/.m2 +_proxy_block="" +if [[ -n "${PROXY_HOST:-}" && -n "${PROXY_PORT:-}" ]]; then + # Strip any accidental http(s):// scheme from the host value + _ph="${PROXY_HOST#http://}" + _ph="${_ph#https://}" + _proxy_block=" + + devspace-proxy + true + http + ${_ph} + ${PROXY_PORT} + localhost|127.0.0.1 + + " + log "Maven proxy configured: ${_ph}:${PROXY_PORT}" +else + log "Maven proxy: none (PROXY_HOST/PROXY_PORT not set)" +fi + +cat > /home/kogito/.m2/settings.xml < +${_proxy_block} + + + redhat-repos + + + redhat-ga + https://maven.repository.redhat.com/ga/ + true + false + + + redhat-earlyaccess + https://maven.repository.redhat.com/earlyaccess/all/ + true + false + + + + + redhat-ga-plugins + https://maven.repository.redhat.com/ga/ + true + false + + + redhat-earlyaccess-plugins + https://maven.repository.redhat.com/earlyaccess/all/ + true + false + + + + + + redhat-repos + + +EOF +log "Wrote clean settings.xml (Red Hat GA + EarlyAccess repos, proxy=$([ -n "${_proxy_block:-}" ] && echo enabled || echo none))" + +# Clear cached Maven resolution failures left over from previous bad-proxy runs. +# Maven caches "not found" markers as *.lastUpdated files; stale ones block +# retries even after fixing the settings/network. +log "Clearing stale Maven resolution failure cache (.lastUpdated files)..." +find /home/kogito/.m2/repository -name "*.lastUpdated" -delete 2>/dev/null || true + +# --- Merge resources + Java from all workflow roots --- +_any_found=false +for _root in "${_wf_roots[@]}"; do + [ -d "$_root" ] || { warn "$_root not found — skipping"; continue; } + _any_found=true + log "Scanning workflow root: $_root" + for wf_dir in "$_root"/*/; do + [ -d "$wf_dir" ] || continue + wf=$(basename "$wf_dir") + if [ -d "$wf_dir/src/main/resources" ]; then + log "Loading resources from $wf ($_root)" + cp -rf "$wf_dir/src/main/resources/"* "$WF_DIR/src/main/resources/" 2>/dev/null || true + fi + if [ -d "$wf_dir/src/main/java" ]; then + log "Loading Java sources from $wf ($_root)" + mkdir -p "$WF_DIR/src/main/java" + cp -rf "$wf_dir/src/main/java/"* "$WF_DIR/src/main/java/" 2>/dev/null || true + fi + done +done + +if [ "$_any_found" = false ]; then + warn "No workflow roots found — seeding from examples" + cp -rf /projects/rhdh-local/orchestrator/workflow-examples/* "$WF_DIR/src/main/resources/" 2>/dev/null || true +fi + +# --- Start SonataFlow devmode --- +log "Starting SonataFlow devmode on port 8899..." + +# Check if launch script exists +if [ ! -f "/home/kogito/launch/run-app-devmode.sh" ]; then + warn "Launch script not found: /home/kogito/launch/run-app-devmode.sh" + ls -la /home/kogito/launch/ 2>&1 | head -20 + exit 1 +fi + +# Check Java availability +if ! command -v java &> /dev/null; then + warn "Java command not found in PATH" + which java || echo "java not in PATH" + exit 1 +fi + +cd "$WF_DIR" +log "Working directory: $(pwd)" +log "Maven settings: $HOME/.m2/settings.xml exists? $(test -f "$HOME/.m2/settings.xml" && echo YES || echo NO)" + +# Activate the local-dev Quarkus profile so %local-dev.* properties in +# application.properties are resolved (e.g. dev.github.token.override used +# by DevGithubTokenFilter). Without this the container defaults to the +# built-in "dev" profile and the filter config remains empty. +export QUARKUS_PROFILE=local-dev +log "QUARKUS_PROFILE set to: $QUARKUS_PROFILE" + +nohup /home/kogito/launch/run-app-devmode.sh > "$LOG" 2>&1 & +_PID=$! +log "PID=$_PID — logs at $LOG" +log "Waiting for process to write to log..." +sleep 2 + +# --- Health check (skip only on --no-wait) --- +if [[ "${1:-}" != "--no-wait" ]]; then + tail -f "$LOG" & + TAIL_PID=$! + for i in $(seq 1 900); do + curl -sf http://localhost:8899/q/health > /dev/null 2>&1 && { kill $TAIL_PID 2>/dev/null; log "UP after $((i*2))s"; exit 0; } + sleep 2 + done + kill $TAIL_PID 2>/dev/null + warn "health check not UP after 1800s (30min) — check $LOG" +else + log "Skipping health check (--no-wait) — monitor with: tail -f $LOG" +fi + \ No newline at end of file diff --git a/scripts/start-rhdh.sh b/scripts/start-rhdh.sh new file mode 100644 index 00000000..e69de29b From ae37b6145bfce7c52871a1a55738d6936f2e3242 Mon Sep 17 00:00:00 2001 From: Bose Date: Thu, 23 Jul 2026 16:03:10 +0530 Subject: [PATCH 2/6] feat(devspaces): add workspace devfile, startup scripts, and Dev Spaces guide --- devfile.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devfile.yaml b/devfile.yaml index f0a47895..bc281a7f 100644 --- a/devfile.yaml +++ b/devfile.yaml @@ -155,11 +155,11 @@ components: value: /projects/rhdh-local # ── Network & Proxy Settings (can be overridden at workspace level) ── - name: PROXY_HOST - value: "internet.gcp.ford.com" + value: "" - name: PROXY_PORT - value: "83" + value: "" - name: NON_PROXY_HOSTS - value: "172.30.0.1|localhost|127.0.0.0/8|19.0.0.0/8|10.0.0.0/8|172.16.0.0/12|192.168.0.0/16|*.ford.com|ford.com|*.appspot.com|appspot.com|*.cloudfunctions.net|cloudfunctions.net|*.cloudproxy.app|cloudproxy.app|*.composer.cloud.google.com|composer.cloud.google.com|*.composer.googleusercontent.com|composer.googleusercontent.com|*.datafusion.cloud.google.com|datafusion.cloud.google.com|*.datafusion.googleusercontent.com|datafusion.googleusercontent.com|*.gcr.io|gcr.io|*.internal|internal|*.googleadapis.com|googleadapis.com|*.googleapis.com|googleapis.com|*.gstatic.com|gstatic.com|*.ltsapis.goog|ltsapis.goog|*.packages.cloud.google.com|packages.cloud.google.com|*.pkg.dev|pkg.dev|*.pki.goog|pki.goog|*.run.app|run.app|*.source.developers.google.com|source.developers.google.com|*.gcp.cloud.es.io|gcp.cloud.es.io|*.smt-gce.susecloud.net|smt-gce.susecloud.net|*.fordcredit.com" + value: "" # ── QUARKUS & Kogito Settings ── - name: QUARKUS_HTTP_PORT value: "8899" From d8bcc0a17d5a6661d4915a7b2332a0696df7da8c Mon Sep 17 00:00:00 2001 From: Bose Date: Thu, 23 Jul 2026 16:25:37 +0530 Subject: [PATCH 3/6] feat(devspaces): add workspace devfile, startup scripts, and Dev Spaces guide --- devfile.yaml | 21 +++++++++++---------- scripts/start-orchestrator.sh | 13 +++++++++---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/devfile.yaml b/devfile.yaml index bc281a7f..6ccc4c04 100644 --- a/devfile.yaml +++ b/devfile.yaml @@ -92,12 +92,10 @@ components: value: "apps.example.com" - name: NODE_ENV value: development - - name: NODE_TLS_REJECT_UNAUTHORIZED - value: "0" - name: NODE_OPTIONS value: --inspect=0.0.0.0:9229 --no-node-snapshot - name: CATALOG_INDEX_IMAGE - value: quay.io/rhdh/plugin-catalog-index:1.9 + value: quay.io/rhdh/plugin-catalog-index:1.10 - name: WAIT_FOR_PLUGINS_TIMEOUT value: "180" - name: CATALOG_ENTITIES_EXTRACT_DIR @@ -121,6 +119,8 @@ components: volumeMounts: - name: dynamic-plugins-root path: /opt/app-root/src/dynamic-plugins-root + - name: dynamic-plugins-root + path: /dynamic-plugins-root - name: extensions-catalog path: /opt/app-root/src/extensions - name: rhdh-generated @@ -241,7 +241,8 @@ commands: link=$(readlink "$fd" 2>/dev/null) || continue case "$link" in socket:*) inode=${link#socket:[}; inode=${inode%]} - if grep ':1B5F ' /proc/net/tcp6 2>/dev/null | grep -q "$inode"; then + if grep ':1B5F ' /proc/net/tcp6 2>/dev/null | grep -q "$inode" || \ + grep ':1B5F ' /proc/net/tcp 2>/dev/null | grep -q "$inode"; then pid=$(echo "$fd" | cut -d/ -f3) echo " killing PID $pid (port 7007)" kill "$1" "$pid" 2>/dev/null || true @@ -251,8 +252,8 @@ commands: done return 1 } - # Graceful kill - kill_port_7007 "" && sleep 2 + # Graceful kill (SIGTERM) + kill_port_7007 "-15" && sleep 2 # Force kill if still there kill_port_7007 "-9" && sleep 1 # Also kill any remaining node processes @@ -401,8 +402,8 @@ commands: component: sonataflow workingDir: /home/kogito/serverless-workflow-project commandLine: | - chmod +x "/projects/rhdh-devspaces/start-sonataflow.sh" 2>/dev/null - "/projects/rhdh-devspaces/start-sonataflow.sh" + chmod +x "/projects/rhdh-local/scripts/start-orchestrator.sh" 2>/dev/null + "/projects/rhdh-local/scripts/start-orchestrator.sh" group: kind: run @@ -437,8 +438,8 @@ commands: fi echo "[sonataflow] Starting sonataflow server..." - chmod +x "/projects/rhdh-devspaces/start-sonataflow.sh" 2>/dev/null - "/projects/rhdh-devspaces//start-sonataflow.sh" + chmod +x "/projects/rhdh-local/scripts/start-orchestrator.sh" 2>/dev/null + "/projects/rhdh-local/scripts/start-orchestrator.sh" group: kind: run events: diff --git a/scripts/start-orchestrator.sh b/scripts/start-orchestrator.sh index e665e25d..f54992c5 100644 --- a/scripts/start-orchestrator.sh +++ b/scripts/start-orchestrator.sh @@ -13,15 +13,20 @@ LOG="/tmp/sonataflow-start.log" log() { echo "[sonataflow] $*"; } warn() { echo "[sonataflow] WARN: $*"; } -# Source the dev .env file so secrets like CLDCTL_GITHUB_TOKEN are available +# Load the dev .env file so secrets like CLDCTL_GITHUB_TOKEN are available # to the Quarkus JVM process started later in this script. +# Use safe parsing (only KEY=VALUE lines, skip comments) to prevent arbitrary code execution. _env_file="${PROJECTS_DIR}/rhdh-local/.env" if [ -f "$_env_file" ]; then set -a - # shellcheck source=/dev/null - source "$_env_file" + while IFS='=' read -r key value; do + # Skip empty lines and comments + [[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue + # Validate key format (alphanumeric and underscore only) + [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] && export "$key"="$value" + done < "$_env_file" set +a - log "Sourced env from $_env_file (CLDCTL_GITHUB_TOKEN set=$([ -n "${CLDCTL_GITHUB_TOKEN:-}" ] && echo yes || echo NO))" + log "Loaded env from $_env_file (CLDCTL_GITHUB_TOKEN set=$([ -n "${CLDCTL_GITHUB_TOKEN:-}" ] && echo yes || echo NO))" else warn ".env not found at $_env_file — CLDCTL_GITHUB_TOKEN will be empty; check WORKFLOW_REPO_DIR" fi From 563a00fb5b895a401f28c195536a9e0bb597a0bc Mon Sep 17 00:00:00 2001 From: Bose Date: Thu, 23 Jul 2026 20:19:37 +0530 Subject: [PATCH 4/6] feat(devspaces): add workspace devfile, startup scripts, and Dev Spaces guide --- scripts/start-orchestrator.sh | 2 +- scripts/start-rhdh.sh | 490 ++++++++++++++++++++++++++++++++++ 2 files changed, 491 insertions(+), 1 deletion(-) diff --git a/scripts/start-orchestrator.sh b/scripts/start-orchestrator.sh index f54992c5..fbd34da6 100644 --- a/scripts/start-orchestrator.sh +++ b/scripts/start-orchestrator.sh @@ -183,7 +183,7 @@ log "Starting SonataFlow devmode on port 8899..." # Check if launch script exists if [ ! -f "/home/kogito/launch/run-app-devmode.sh" ]; then warn "Launch script not found: /home/kogito/launch/run-app-devmode.sh" - ls -la /home/kogito/launch/ 2>&1 | head -20 + find /home/kogito/launch -mindepth 1 -maxdepth 1 -print 2>&1 | head -20 exit 1 fi diff --git a/scripts/start-rhdh.sh b/scripts/start-rhdh.sh index e69de29b..85388336 100644 --- a/scripts/start-rhdh.sh +++ b/scripts/start-rhdh.sh @@ -0,0 +1,490 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# RHDH startup for Dev Spaces. +# rhdh-local provides upstream scripts; all user config comes from this repo. +set -euo pipefail + +log() { printf '[devspaces-start] %s\n' "$*"; } +warn() { printf '[devspaces-start] WARN: %s\n' "$*" >&2; } +fail() { printf '[devspaces-start] ERROR: %s\n' "$*" >&2; exit 1; } + +run_script() { + local s="$1" mode="${2:-}" + [[ -f "$s" ]] || fail "$(basename "$s") not found" + if [[ "$mode" == "exec" ]]; then [[ -x "$s" ]] && exec "$s" || exec bash "$s" + elif [[ -x "$s" ]]; then + "$s" + else + bash "$s" + fi +} + +# Link all files from $1//* into $2// +link_dir() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + for f in "$src"/*; do [[ -e "$f" ]] && ln -sfn "$f" "$dst/$(basename "$f")"; done +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── Locate rhdh-local ──────────────────────────────────────────────────────── +for _dir in "${RHDH_LOCAL_DIR:-}" "$PWD" "/projects/rhdh-local" "$SCRIPT_DIR"; do + [[ -n "$_dir" && -d "$_dir" && -f "$_dir/prepare-and-install-dynamic-plugins.sh" ]] && ROOT_DIR="$_dir" && break +done +ROOT_DIR="${ROOT_DIR:-/projects/rhdh-local}" +log "ROOT_DIR=$ROOT_DIR SCRIPT_DIR=$SCRIPT_DIR" +cd "$ROOT_DIR" + +# ── Paths ──────────────────────────────────────────────────────────────────── +APP="/opt/app-root/src" +# Support both names. Prefer explicit env, then existing new-name dir, else legacy. +if [[ -n "${WORKSPACE_DYNAMIC_PLUGINS_DIR:-}" ]]; then + DYN="$WORKSPACE_DYNAMIC_PLUGINS_DIR" +elif [[ -d "$APP/dynamic-root-plugins" ]]; then + DYN="$APP/dynamic-root-plugins" +else + DYN="$APP/dynamic-plugins-root" +fi +EXT="${WORKSPACE_EXTENSIONS_DIR:-$APP/extensions}" +GEN="${WORKSPACE_GENERATED_DIR:-$APP/generated}" +TMP="${WORKSPACE_TMP_DIR:-/projects/.rhdh/tmp}" + +mkdir -p "$DYN" "$EXT" "$GEN" "$TMP" "$APP" + +# Keep repo plugin folders as real directories (not symlinks/files). +for _local_dir in "$ROOT_DIR/dynamic-root-plugins" "$ROOT_DIR/dynamic-plugins-root"; do + if [[ -L "$_local_dir" ]]; then + rm -f "$_local_dir" + elif [[ -e "$_local_dir" && ! -d "$_local_dir" ]]; then + fail "$_local_dir exists and is not a directory" + fi + mkdir -p "$_local_dir" +done + +# Generated/extensions should still map to writable workspace locations. +for _pair in "generated=$GEN" "extensions=$EXT"; do + _target="$ROOT_DIR/${_pair%%=*}" + [[ "$_target" == "/" ]] && fail "Refusing to remove root path" + rm -rf -- "$_target" && ln -sfn "${_pair#*=}" "$_target" +done + +# ── Env ────────────────────────────────────────────────────────────────────── +SAVED_BASE_URL="${BASE_URL:-auto}" +export NPM_CONFIG_CACHE="$TMP/.npm" YARN_CACHE_FOLDER="$TMP/.yarn" +mkdir -p "$NPM_CONFIG_CACHE" "$YARN_CACHE_FOLDER" + +for f in "$ROOT_DIR/default.env" "$ROOT_DIR/.env"; do + # shellcheck source=/dev/null + [[ -f "$f" ]] && { set -a; source "$f"; set +a; } +done +export BASE_URL="$SAVED_BASE_URL" + +# ── BASE_URL detection ─────────────────────────────────────────────────────── +if [[ "$BASE_URL" == auto || "$BASE_URL" == AUTO || "$BASE_URL" == http://localhost:* ]]; then + _ns="${DEVWORKSPACE_NAMESPACE:-}" _name="${DEVWORKSPACE_NAME:-}" _dom="${CLUSTER_APPS_DOMAIN:-}" + if [[ -n "$_ns" && -n "$_name" && -n "$_dom" ]]; then + export BASE_URL="https://${_ns%-devspaces}-${_name}-${RHDH_PUBLIC_ENDPOINT_NAME:-rhdh}.${_dom}" + log "BASE_URL=$BASE_URL" + else + export BASE_URL="${BASE_URL_FALLBACK:-http://localhost:7007}" + warn "Could not detect BASE_URL; using $BASE_URL" + fi +fi + +# ── No-op chown (OpenShift arbitrary UIDs) ─────────────────────────────────── +mkdir -p "$TMP/noop-bin" +printf '#!/bin/sh\nexit 0\n' > "$TMP/noop-bin/chown" && chmod +x "$TMP/noop-bin/chown" +export PATH="$TMP/noop-bin:$PATH" + +# ── Build writable configs dir ─────────────────────────────────────────────── +CFG="$ROOT_DIR/configs" +WCFG="$TMP/configs" +rm -rf "$WCFG" && mkdir -p "$WCFG/dynamic-plugins" "$WCFG/app-config" + +# Use only the cloned rhdh-local repository configs. +if [[ -d "$CFG" ]]; then + link_dir "$CFG/dynamic-plugins" "$WCFG/dynamic-plugins" + link_dir "$CFG/app-config" "$WCFG/app-config" + [[ -d "$CFG/extra-files" ]] && ln -sfn "$CFG/extra-files" "$WCFG/extra-files" + log "Linked rhdh-local configs" +fi + +# .npmrc: optional. Use a Kubernetes secret only when private npm auth is needed. +NPMRC_FILE="$WCFG/.npmrc" + +# Preferred key: NPMRC_CONTENT (raw) +# Backward-compatible key: NPMRC_CONTENT_B64 (base64) +# Also supports mounted secret files. +_npmrc_payload="" +_npmrc_source="" + +if [[ -n "${NPMRC_CONTENT:-}" ]]; then + _npmrc_payload="$NPMRC_CONTENT" + _npmrc_source="env:NPMRC_CONTENT" +elif [[ -n "${NPMRC_CONTENT_B64:-}" ]]; then + # Keep compatibility with existing setups where Kubernetes already injects + # this key as plain .npmrc content. + _npmrc_payload="$NPMRC_CONTENT_B64" + _npmrc_source="env:NPMRC_CONTENT_B64(raw)" + + # If the value looks base64-encoded, decode and prefer decoded content. + if [[ "$NPMRC_CONTENT_B64" =~ ^[A-Za-z0-9+/=[:space:]]+$ ]]; then + _decoded="" + if command -v base64 >/dev/null 2>&1; then + _decoded=$(printf '%s' "$NPMRC_CONTENT_B64" | base64 -d 2>/dev/null || true) + elif command -v openssl >/dev/null 2>&1; then + _decoded=$(printf '%s' "$NPMRC_CONTENT_B64" | openssl base64 -d -A 2>/dev/null || true) + fi + if [[ -n "$_decoded" ]]; then + _npmrc_payload="$_decoded" + _npmrc_source="env:NPMRC_CONTENT_B64(decoded)" + fi + fi +else + for _secret_file in \ + "${NPMRC_CONTENT_FILE:-}" \ + "/etc/secrets/rhdh-secrets/NPMRC_CONTENT" \ + "/var/run/secrets/rhdh-secrets/NPMRC_CONTENT" \ + "/projects/.codespaces/secrets/NPMRC_CONTENT"; do + [[ -n "$_secret_file" && -f "$_secret_file" ]] || continue + _payload_from_file=$(cat "$_secret_file" 2>/dev/null || true) + if [[ -n "$_payload_from_file" ]]; then + _npmrc_payload="$_payload_from_file" + _npmrc_source="file:$_secret_file" + break + fi + done +fi + +if [[ -n "$_npmrc_payload" ]]; then + printf '%s\n' "$_npmrc_payload" > "$NPMRC_FILE" + chmod 600 "$NPMRC_FILE" + log ".npmrc created from ${_npmrc_source} ($(wc -l < "$NPMRC_FILE") lines)" +elif [[ -f "$ROOT_DIR/.npmrc" ]]; then + cp "$ROOT_DIR/.npmrc" "$NPMRC_FILE" + chmod 600 "$NPMRC_FILE" + log ".npmrc copied from $ROOT_DIR/.npmrc" +elif [[ -f "$HOME/.npmrc" ]]; then + cp "$HOME/.npmrc" "$NPMRC_FILE" + chmod 600 "$NPMRC_FILE" + log ".npmrc copied from $HOME/.npmrc" +else + log ".npmrc not provided; continuing with public registries" + log "If you use private npm, provide NPMRC_CONTENT (or NPMRC_CONTENT_B64) and restart workspace" +fi + +# Tell npm/yarn to use this file directly (survives symlink breaks) +if [[ -f "$NPMRC_FILE" ]]; then + export NPM_CONFIG_USERCONFIG="$NPMRC_FILE" + for _d in "$APP" "$ROOT_DIR" "$HOME"; do + [[ -d "$_d" ]] && ln -sfn "$NPMRC_FILE" "$_d/.npmrc" 2>/dev/null || true + done + log ".npmrc linked to: $APP, $ROOT_DIR, $HOME" +fi + +# Symlink configs into RHDH app dir +rm -rf "$APP/configs" 2>/dev/null || true +ln -sfn "$WCFG" "$APP/configs" +log "configs → $WCFG | .npmrc: $(test -f "$WCFG/.npmrc" && echo present || echo MISSING)" + +# Homepage data.json +if [[ -f "$ROOT_DIR/configs/extra-files/data.json" ]]; then + mkdir -p "$APP/packages/app/dist/homepage" + ln -sfn "$ROOT_DIR/configs/extra-files/data.json" "$APP/packages/app/dist/homepage/data.json" +fi + +# Root-level symlinks for marketplace/extensions +ln -sfn "$EXT" /extensions 2>/dev/null || true +ln -sfn "$EXT" /marketplace 2>/dev/null || true +ln -sfn "$DYN" /dynamic-plugins-root 2>/dev/null || true +ln -sfn "$DYN" /dynamic-root-plugins 2>/dev/null || true + +# ── Prepare install script ──────────────────────────────────────────────────── +# The upstream prepare-and-install-dynamic-plugins.sh may have been overwritten +# by a previous run's NOOP stub. Restore from git if needed. +_install_script="$ROOT_DIR/prepare-and-install-dynamic-plugins.sh" + +_is_real_install_script() { + [[ -s "$1" ]] || return 1 + local _content + _content=$(< "$1") 2>/dev/null || return 1 + (( ${#_content} > 500 )) || return 1 + [[ "$_content" != *__DEVSPACES_NOOP_STUB__* && "$_content" != *'Plugin install skipped'* ]] +} + +if ! _is_real_install_script "$_install_script"; then + log "Install script is missing or was overwritten — restoring..." + + # Method 1: git checkout (works if git binary available, e.g. tools container) + if [[ -d "$ROOT_DIR/.git" ]] && command -v git &>/dev/null; then + git -C "$ROOT_DIR" checkout HEAD -- prepare-and-install-dynamic-plugins.sh 2>/dev/null || true + fi + + # Method 2: download from git remote (works without git binary) + if ! _is_real_install_script "$_install_script" && [[ -d "$ROOT_DIR/.git" ]]; then + # Parse remote URL and branch from .git/config and .git/HEAD + _remote_url=$(sed -n '/\[remote "origin"\]/,/url/{s/.*url = //p}' "$ROOT_DIR/.git/config" 2>/dev/null | head -1) + _branch=$(sed 's|ref: refs/heads/||' "$ROOT_DIR/.git/HEAD" 2>/dev/null) + _branch="${_branch:-main}" + # Convert GitHub URL to raw content URL + _raw_url=$(printf '%s' "$_remote_url" | sed 's|github\.com|raw.githubusercontent.com|; s|\.git$||') + _raw_url="${_raw_url}/${_branch}/prepare-and-install-dynamic-plugins.sh" + + if [[ "$_raw_url" == https://* ]]; then + # Try curl + if command -v curl &>/dev/null; then + curl -fsSL "$_raw_url" -o "$_install_script" 2>/dev/null && chmod +x "$_install_script" || true + fi + # Try node (always available in rhdh container) + if ! _is_real_install_script "$_install_script"; then + NODE_OPTIONS="" node -e ' + const https = require("https"), fs = require("fs"); + function fetch(u, cb) { + https.get(u, {headers:{"User-Agent":"rhdh"}}, r => { + if ([301,302].includes(r.statusCode)) return fetch(r.headers.location, cb); + let d = ""; r.on("data", c => d += c); r.on("end", () => cb(r.statusCode, d)); + }).on("error", () => cb(0, "")); + } + fetch(process.argv[1], (s, d) => { + if (s === 200 && d.length > 500) { fs.writeFileSync(process.argv[2], d, {mode:0o755}); process.exit(0); } + process.exit(1); + }); + ' "$_raw_url" "$_install_script" 2>/dev/null || true + fi + _is_real_install_script "$_install_script" && log "Install script restored from remote: $_raw_url" + fi + fi + + _is_real_install_script "$_install_script" || \ + fail "Cannot restore install script. Open a 'tools' terminal and run: cd $ROOT_DIR && git checkout HEAD -- prepare-and-install-dynamic-plugins.sh" + log "Install script restored" +fi + +# Snapshot to a PID-unique temp so the original stays untouched for future runs +_run_script="$TMP/.install-script-to-run.$$.sh" +cp "$_install_script" "$_run_script" +chmod +x "$_run_script" + +# Patch volume-mount paths + rm-rf handler in the snapshot (not the original) +sed -i "s|'/dynamic-plugins-root|'$DYN|g;s|\"/dynamic-plugins-root|\"$DYN|g;s| /dynamic-plugins-root| $DYN|g;s|'/extensions|'$EXT|g;s|\"/extensions|\"$EXT|g;s| /extensions| $EXT|g;s|rm -rf \\./dynamic-plugins-root|rm -rf ./dynamic-plugins-root/* ./dynamic-plugins-root/.[!.]* 2>/dev/null; true #|" "$_run_script" 2>/dev/null || true +[[ -f "$APP/install-dynamic-plugins.py" ]] && \ + sed -i "s|'/dynamic-plugins-root|'$DYN|g;s|\"/dynamic-plugins-root|\"$DYN|g;s| /dynamic-plugins-root| $DYN|g;s|'/extensions|'$EXT|g;s|\"/extensions|\"$EXT|g;s| /extensions| $EXT|g" "$APP/install-dynamic-plugins.py" 2>/dev/null || true + +# ── Link local dynamic plugins into APP for npm pack ───────────────────────── +# Search order: LOCAL_PLUGINS_DIR env > $SCRIPT_DIR candidates > auto-discover under /projects +_local_plugins_dir="" +for _candidate in "${LOCAL_PLUGINS_DIR:-}" \ + "$SCRIPT_DIR/dynamic-plugin-root" \ + "$SCRIPT_DIR/dynamic-root-plugins" \ + "$SCRIPT_DIR/dynamic-plugins-root"; do + [[ -n "$_candidate" && -d "$_candidate" ]] || continue + # Accept only real plugin source dirs that contain at least one package.json + # in a direct child directory. This avoids selecting runtime symlink targets + # such as $DYN when they are empty. + if find "$_candidate" -mindepth 2 -maxdepth 2 -type f -name package.json -print -quit 2>/dev/null | grep -q .; then + _local_plugins_dir="$_candidate" + break + fi +done + +# Auto-discover: find directories named dynamic-plugin*-root under /projects with plugin subdirs +if [[ -z "$_local_plugins_dir" ]]; then + while IFS= read -r _candidate; do + # Verify it contains at least one subdir with a package.json + for _sub in "$_candidate"/*/package.json; do + [[ -f "$_sub" ]] && _local_plugins_dir="$_candidate" && break 2 + done + done < <(find /projects -maxdepth 3 -type d \( -name 'dynamic-root-plugins' -o -name 'dynamic-plugins-root' -o -name 'dynamic-plugin*-root' \) ! -path '*/node_modules/*' ! -path '/opt/*' 2>/dev/null | sort) +fi + +if [[ -n "$_local_plugins_dir" ]]; then + for plugin_dir in "$_local_plugins_dir"/*/; do + [[ -d "$plugin_dir" ]] || continue + plugin_name=$(basename "$plugin_dir") + # Support package paths like ./ and ./dynamic-root-plugins/ + ln -sfn "$plugin_dir" "$DYN/$plugin_name" + ln -sfn "$plugin_dir" "$APP/$plugin_name" + log "Local plugin linked: $plugin_name → $DYN/$plugin_name and $APP/$plugin_name" + done +else + log "No local plugins dir found (set LOCAL_PLUGINS_DIR to override)" +fi + +# ── Install dynamic plugins ────────────────────────────────────────────────── +cd "$APP" + +GEN_CFG="$DYN/app-config.dynamic-plugins.yaml" +FORCE_REINSTALL="${FORCE_REINSTALL_PLUGINS:-false}" +CACHE_MARKER="$DYN/.cache-fingerprint" +PLUGIN_BACKUP="$TMP/.plugin-cache-backup" + +# Fingerprint: RHDH version + all dynamic plugin config YAMLs + catalog image +# NOTE: Hash the REAL source files ($CFG and $ROOT_DIR), not $WCFG which holds symlinks. +# find -type f silently skips symlinks, so hashing the symlink dir always returns "none". +_override_file="$WCFG/dynamic-plugins/dynamic-plugins.override.yaml" +_rhdh_version=$(NODE_OPTIONS="" node -p "require('$APP/package.json').version" 2>/dev/null || echo "unknown") + +_dynamic_plugins_cfg_hash() { + # Hash the real source dirs/files — follow symlinks with -L just in case. + # Include: + # 1. Our overlay: $CFG/dynamic-plugins (real files, not the WCFG symlink copies) + # 2. Upstream rhdh-local root-level dynamic-plugins*.yaml (the files the user edits) + { + [[ -d "$CFG/dynamic-plugins" ]] && \ + find -L "$CFG/dynamic-plugins" -type f \( -name '*.yaml' -o -name '*.yml' \) -print0 2>/dev/null | \ + sort -z | xargs -0 md5sum 2>/dev/null + find -L "$ROOT_DIR" -maxdepth 1 -type f -name 'dynamic-plugins*.yaml' -print0 2>/dev/null | \ + sort -z | xargs -0 md5sum 2>/dev/null + } | md5sum 2>/dev/null | cut -d' ' -f1 || echo "none" +} + +_dynamic_cfg_hash=$(_dynamic_plugins_cfg_hash) +CURRENT_FP="${_rhdh_version}:${CATALOG_INDEX_IMAGE:-none}:${_dynamic_cfg_hash}" +log "Cache fingerprint: $CURRENT_FP" + +# Check cache +CACHE_HIT=false +if [[ "$FORCE_REINSTALL" == "true" ]]; then + log "Force reinstall requested" +elif [[ ! -f "$GEN_CFG" || ! -f "$CACHE_MARKER" ]]; then + # DYN was wiped (workspace restart) — try restoring from persistent backup + if [[ -f "$PLUGIN_BACKUP/app-config.dynamic-plugins.yaml" && -f "$PLUGIN_BACKUP/.cache-fingerprint" ]] && \ + [[ "$(cat "$PLUGIN_BACKUP/.cache-fingerprint" 2>/dev/null)" == "$CURRENT_FP" ]]; then + log "DYN wiped but backup fingerprint matches — restoring from cache" + cp -a "$PLUGIN_BACKUP"/. "$DYN/" 2>/dev/null || true + CACHE_HIT=true + else + log "No plugin cache — first install" + fi +elif [[ "$(cat "$CACHE_MARKER" 2>/dev/null)" == "$CURRENT_FP" ]]; then + CACHE_HIT=true +else + log "Dynamic plugin config changed — reinstalling all plugins" +fi + +if [[ "$CACHE_HIT" == "true" ]]; then + log "Plugins cached (fingerprint match) — skipping install" + log " (set FORCE_REINSTALL_PLUGINS=true to force)" +else + log "Installing dynamic plugins..." + + # Validate local-path plugins before install. Do not skip silently. + if [[ -L "$_override_file" ]]; then + _resolved="$(readlink -f "$_override_file")" + rm -f "$_override_file" + cp "$_resolved" "$_override_file" + fi + + # Collect local-path package refs from active config files. + mapfile -t _required_local_pkgs < <( + grep -RhoE "package:[[:space:]]*[\"']?\./(dynamic-root-plugins|dynamic-plugins-root)/[^\"'[:space:]]+" "$APP" "$ROOT_DIR" "$WCFG" 2>/dev/null | + sed -E "s/^package:[[:space:]]*[\"']?\.\///" | + sort -u + ) + + if (( ${#_required_local_pkgs[@]} )); then + _missing_local_pkgs=() + for _pkg in "${_required_local_pkgs[@]}"; do + [[ -f "$APP/$_pkg/package.json" ]] || _missing_local_pkgs+=("$_pkg") + done + + if (( ${#_missing_local_pkgs[@]} )); then + warn "Local plugin package paths are referenced but not materialized:" + printf ' - ./%s\n' "${_missing_local_pkgs[@]}" >&2 + fail "Missing package.json for local plugin paths above. Run deploy-dynamic-plugin first, then restart-rhdh." + fi + fi + + # Back up DYN before wiping (fallback if install fails) + if [[ -f "$GEN_CFG" ]]; then + rm -rf "$PLUGIN_BACKUP" && mkdir -p "$PLUGIN_BACKUP" + cp -a "$DYN"/. "$PLUGIN_BACKUP/" 2>/dev/null || true + log "Backed up existing plugins for fallback" + fi + + find "$DYN" -mindepth 1 -delete 2>/dev/null || true + + # Re-link local plugins after cleanup so package paths like + # ./dynamic-root-plugins/ are present for npm pack. + if [[ -n "${_local_plugins_dir:-}" ]]; then + for plugin_dir in "$_local_plugins_dir"/*/; do + [[ -d "$plugin_dir" ]] || continue + plugin_name=$(basename "$plugin_dir") + ln -sfn "$plugin_dir" "$DYN/$plugin_name" + done + fi + + export CATALOG_ENTITIES_EXTRACT_DIR="$EXT" + export MAX_ENTRY_SIZE="${MAX_ENTRY_SIZE:-30000000}" + + # Run installer with errexit disabled so we can handle failures + _install_log="$TMP/plugin-install.log" + set +eo pipefail + bash "$_run_script" 2>&1 | tee "$_install_log" + _rc=${PIPESTATUS[0]} + set -eo pipefail + [[ $_rc -ne 0 ]] && warn "Install script exited with code $_rc" + + # Outcome: success, fallback, or fatal + if [[ -f "$GEN_CFG" ]]; then + log "Plugin install succeeded" + echo "$CURRENT_FP" > "$CACHE_MARKER" + elif [[ -f "$PLUGIN_BACKUP/app-config.dynamic-plugins.yaml" ]]; then + warn "Install FAILED — falling back to previous cached plugins." + warn "Log: $TMP/plugin-install.log | Fix then FORCE_REINSTALL_PLUGINS=true" + cp -a "$PLUGIN_BACKUP"/. "$DYN/" 2>/dev/null || true + else + [[ -f "$_install_log" ]] && { log "Last 30 lines:"; tail -30 "$_install_log" >&2; } + fail "Plugin install failed, no fallback available. See log: $TMP/plugin-install.log" + fi +fi +rm -f "$_run_script" 2>/dev/null || true +log "Plugin config ready" + +# ── Copy local dynamic plugins ─────────────────────────────────────────────── +if [[ -n "${_local_plugins_dir:-}" ]]; then + for plugin_dir in "$_local_plugins_dir"/*/; do + [[ -d "$plugin_dir" ]] || continue + plugin_name=$(basename "$plugin_dir") + dest="$DYN/$plugin_name" + rm -rf "$dest" && cp -r "$plugin_dir" "$dest" + log "Local plugin copied: $plugin_name" + done +fi + +# Save DYN for cache restore on workspace restart +if [[ -f "$GEN_CFG" ]]; then + rm -rf "$PLUGIN_BACKUP" && mkdir -p "$PLUGIN_BACKUP" + cp -a "$DYN"/. "$PLUGIN_BACKUP/" 2>/dev/null || true +fi + +# Restore configs symlink if upstream broke it +[[ "$(readlink "$APP/configs" 2>/dev/null)" == "$WCFG" ]] || { + warn "Restoring configs symlink" + rm -rf "$APP/configs" 2>/dev/null || true + ln -sfn "$WCFG" "$APP/configs" +} + +# ── Patch BASE_URL + extensions path in image configs ──────────────────────── +for cfg in "$APP"/app-config*.yaml "$GEN_CFG"; do + [[ -f "$cfg" ]] || continue + sed -i "s|baseUrl: http://localhost:[0-9]*|baseUrl: ${BASE_URL}|g;s|: /extensions|: ${EXT}|g" "$cfg" 2>/dev/null || true +done + +# ── Write generated/app-config.patched.yaml (only file Backstage reads) ───── +PATCHED="$GEN/app-config.patched.yaml" +: > "$PATCHED" +for f in "$ROOT_DIR/configs/app-config/app-config.yaml" \ + "$ROOT_DIR/configs/app-config/app-config.local.yaml"; do + [[ -f "$f" ]] && { cat "$f" >> "$PATCHED"; printf '\n' >> "$PATCHED"; log "Config: $f"; } +done + +# Stubs for required plugin keys +grep -q '^quay:' "$PATCHED" || printf 'quay:\n uiUrl: https://quay.io\n' >> "$PATCHED" +grep -q '^extensions:' "$PATCHED" || printf 'extensions:\n directory: %s\n' "$EXT" >> "$PATCHED" +grep -q '^marketplace:' "$PATCHED"|| printf 'marketplace:\n directory: %s\n' "$EXT" >> "$PATCHED" + +# ── Start ──────────────────────────────────────────────────────────────────── +log "Starting RHDH..." +run_script "$ROOT_DIR/wait-for-plugins-and-start.sh" exec From 09aac1757f684dabed85e159110d22dcedff5f7d Mon Sep 17 00:00:00 2001 From: Bose Date: Thu, 23 Jul 2026 20:38:01 +0530 Subject: [PATCH 5/6] feat(devspaces): add workspace devfile, startup scripts, and Dev Spaces guide --- scripts/start-rhdh.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/start-rhdh.sh b/scripts/start-rhdh.sh index 85388336..a2a6dfd4 100644 --- a/scripts/start-rhdh.sh +++ b/scripts/start-rhdh.sh @@ -11,7 +11,12 @@ fail() { printf '[devspaces-start] ERROR: %s\n' "$*" >&2; exit 1; } run_script() { local s="$1" mode="${2:-}" [[ -f "$s" ]] || fail "$(basename "$s") not found" - if [[ "$mode" == "exec" ]]; then [[ -x "$s" ]] && exec "$s" || exec bash "$s" + if [[ "$mode" == "exec" ]]; then + if [[ -x "$s" ]]; then + exec "$s" + else + exec bash "$s" + fi elif [[ -x "$s" ]]; then "$s" else From 939adfad29ed24df5bc894a7748e2b9dca6a2a07 Mon Sep 17 00:00:00 2001 From: Bose Date: Thu, 23 Jul 2026 20:43:45 +0530 Subject: [PATCH 6/6] feat(devspaces): add workspace devfile, startup scripts, and Dev Spaces guide --- scripts/start-orchestrator.sh | 24 ++++++++++++------------ scripts/start-rhdh.sh | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/start-orchestrator.sh b/scripts/start-orchestrator.sh index fbd34da6..ce8ddcd7 100644 --- a/scripts/start-orchestrator.sh +++ b/scripts/start-orchestrator.sh @@ -17,7 +17,7 @@ warn() { echo "[sonataflow] WARN: $*"; } # to the Quarkus JVM process started later in this script. # Use safe parsing (only KEY=VALUE lines, skip comments) to prevent arbitrary code execution. _env_file="${PROJECTS_DIR}/rhdh-local/.env" -if [ -f "$_env_file" ]; then +if [[ -f "$_env_file" ]]; then set -a while IFS='=' read -r key value; do # Skip empty lines and comments @@ -26,7 +26,7 @@ if [ -f "$_env_file" ]; then [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] && export "$key"="$value" done < "$_env_file" set +a - log "Loaded env from $_env_file (CLDCTL_GITHUB_TOKEN set=$([ -n "${CLDCTL_GITHUB_TOKEN:-}" ] && echo yes || echo NO))" + log "Loaded env from $_env_file (CLDCTL_GITHUB_TOKEN set=$( [[ -n "${CLDCTL_GITHUB_TOKEN:-}" ]] && echo yes || echo NO))" else warn ".env not found at $_env_file — CLDCTL_GITHUB_TOKEN will be empty; check WORKFLOW_REPO_DIR" fi @@ -37,7 +37,7 @@ if [[ -n "${WF_ROOTS:-}" ]]; then IFS=':' read -ra _wf_roots <<< "$WF_ROOTS" else for d in "$PROJECTS_DIR"/*/workflows; do - [ -d "$d" ] && _wf_roots+=("$d") + [[ -d "$d" ]] && _wf_roots+=("$d") done fi log "Workflow roots: ${_wf_roots[*]:-}" @@ -53,7 +53,7 @@ for _w in $(seq 1 30); do log "Old process terminated after ${_w}s" break fi - if [ "$_w" -eq 15 ]; then + if [[ "$_w" -eq 15 ]]; then warn "Process still alive after 15s — sending SIGKILL" pkill -9 -f 'java.*quarkus' 2>/dev/null || true fi @@ -143,7 +143,7 @@ ${_proxy_block} EOF -log "Wrote clean settings.xml (Red Hat GA + EarlyAccess repos, proxy=$([ -n "${_proxy_block:-}" ] && echo enabled || echo none))" +log "Wrote clean settings.xml (Red Hat GA + EarlyAccess repos, proxy=$( [[ -n "${_proxy_block:-}" ]] && echo enabled || echo none))" # Clear cached Maven resolution failures left over from previous bad-proxy runs. # Maven caches "not found" markers as *.lastUpdated files; stale ones block @@ -154,17 +154,17 @@ find /home/kogito/.m2/repository -name "*.lastUpdated" -delete 2>/dev/null || tr # --- Merge resources + Java from all workflow roots --- _any_found=false for _root in "${_wf_roots[@]}"; do - [ -d "$_root" ] || { warn "$_root not found — skipping"; continue; } + [[ -d "$_root" ]] || { warn "$_root not found — skipping"; continue; } _any_found=true log "Scanning workflow root: $_root" for wf_dir in "$_root"/*/; do - [ -d "$wf_dir" ] || continue + [[ -d "$wf_dir" ]] || continue wf=$(basename "$wf_dir") - if [ -d "$wf_dir/src/main/resources" ]; then + if [[ -d "$wf_dir/src/main/resources" ]]; then log "Loading resources from $wf ($_root)" cp -rf "$wf_dir/src/main/resources/"* "$WF_DIR/src/main/resources/" 2>/dev/null || true fi - if [ -d "$wf_dir/src/main/java" ]; then + if [[ -d "$wf_dir/src/main/java" ]]; then log "Loading Java sources from $wf ($_root)" mkdir -p "$WF_DIR/src/main/java" cp -rf "$wf_dir/src/main/java/"* "$WF_DIR/src/main/java/" 2>/dev/null || true @@ -172,7 +172,7 @@ for _root in "${_wf_roots[@]}"; do done done -if [ "$_any_found" = false ]; then +if [[ "$_any_found" == false ]]; then warn "No workflow roots found — seeding from examples" cp -rf /projects/rhdh-local/orchestrator/workflow-examples/* "$WF_DIR/src/main/resources/" 2>/dev/null || true fi @@ -181,7 +181,7 @@ fi log "Starting SonataFlow devmode on port 8899..." # Check if launch script exists -if [ ! -f "/home/kogito/launch/run-app-devmode.sh" ]; then +if [[ ! -f "/home/kogito/launch/run-app-devmode.sh" ]]; then warn "Launch script not found: /home/kogito/launch/run-app-devmode.sh" find /home/kogito/launch -mindepth 1 -maxdepth 1 -print 2>&1 | head -20 exit 1 @@ -196,7 +196,7 @@ fi cd "$WF_DIR" log "Working directory: $(pwd)" -log "Maven settings: $HOME/.m2/settings.xml exists? $(test -f "$HOME/.m2/settings.xml" && echo YES || echo NO)" +log "Maven settings: $HOME/.m2/settings.xml exists? $( [[ -f "$HOME/.m2/settings.xml" ]] && echo YES || echo NO)" # Activate the local-dev Quarkus profile so %local-dev.* properties in # application.properties are resolved (e.g. dev.github.token.override used diff --git a/scripts/start-rhdh.sh b/scripts/start-rhdh.sh index a2a6dfd4..373bfe80 100644 --- a/scripts/start-rhdh.sh +++ b/scripts/start-rhdh.sh @@ -191,7 +191,7 @@ fi # Symlink configs into RHDH app dir rm -rf "$APP/configs" 2>/dev/null || true ln -sfn "$WCFG" "$APP/configs" -log "configs → $WCFG | .npmrc: $(test -f "$WCFG/.npmrc" && echo present || echo MISSING)" +log "configs → $WCFG | .npmrc: $( [[ -f "$WCFG/.npmrc" ]] && echo present || echo MISSING)" # Homepage data.json if [[ -f "$ROOT_DIR/configs/extra-files/data.json" ]]; then @@ -338,10 +338,10 @@ _dynamic_plugins_cfg_hash() { { [[ -d "$CFG/dynamic-plugins" ]] && \ find -L "$CFG/dynamic-plugins" -type f \( -name '*.yaml' -o -name '*.yml' \) -print0 2>/dev/null | \ - sort -z | xargs -0 md5sum 2>/dev/null + sort -z | xargs -0 sha256sum 2>/dev/null find -L "$ROOT_DIR" -maxdepth 1 -type f -name 'dynamic-plugins*.yaml' -print0 2>/dev/null | \ - sort -z | xargs -0 md5sum 2>/dev/null - } | md5sum 2>/dev/null | cut -d' ' -f1 || echo "none" + sort -z | xargs -0 sha256sum 2>/dev/null + } | sha256sum 2>/dev/null | cut -d' ' -f1 || echo "none" } _dynamic_cfg_hash=$(_dynamic_plugins_cfg_hash)