You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openserverless.apache.org by ms...@apache.org on 2026/09/07 14:38:58 UTC

(openserverless-runtimes) 23/28: reworked runtimes.json generation

This is an automated email from the ASF dual-hosted git repository.

sciabarracom pushed a commit to branch 0.9.0
in repository https://gitbox.apache.org/repos/asf/openserverless-runtimes.git

commit 0b3227e1bb66b5b8e51d26562beccbfdad8d0386
Author: Michele Sciabarra <mi...@sciabarra.com>
AuthorDate: Sun Sep 6 23:17:33 2026 +0200

    reworked runtimes.json generation
---
 .env.dist                                          |   6 +-
 Taskfile.yml                                       |  71 +-----
 {runtime/common => common}/common1.17.1/DISCLAIMER |   0
 {runtime/common => common}/common1.17.1/Dockerfile |   0
 {runtime/common => common}/common1.17.1/LICENSE    |   0
 {runtime/common => common}/common1.17.1/NOTICE     |   0
 {runtime/common => common}/common1.17.1/WARN       |   0
 {runtime/common => common}/common1.18.4/DISCLAIMER |   0
 {runtime/common => common}/common1.18.4/Dockerfile |   0
 {runtime/common => common}/common1.18.4/LICENSE    |   0
 {runtime/common => common}/common1.18.4/NOTICE     |   0
 {runtime/common => common}/common1.18.4/WARN       |   0
 {runtime => experimental}/php/v8.2/CHANGELOG.md    |   0
 {runtime => experimental}/php/v8.2/DISCLAIMER      |   0
 {runtime => experimental}/php/v8.2/Dockerfile      |   0
 {runtime => experimental}/php/v8.2/LICENSE         |   0
 {runtime => experimental}/php/v8.2/NOTICE          |   0
 {runtime => experimental}/php/v8.2/WARN            |   0
 {runtime => experimental}/php/v8.2/build.gradle    |   0
 {runtime => experimental}/php/v8.2/compile.php     |   0
 {runtime => experimental}/php/v8.2/composer.json   |   0
 {runtime => experimental}/php/v8.2/php.ini         |   0
 {runtime => experimental}/php/v8.2/runner.php      |   0
 {runtime => experimental}/php/v8.3/CHANGELOG.md    |   0
 {runtime => experimental}/php/v8.3/DISCLAIMER      |   0
 {runtime => experimental}/php/v8.3/Dockerfile      |   0
 {runtime => experimental}/php/v8.3/LICENSE         |   0
 {runtime => experimental}/php/v8.3/NOTICE          |   0
 {runtime => experimental}/php/v8.3/WARN            |   0
 {runtime => experimental}/php/v8.3/compile.php     |   0
 {runtime => experimental}/php/v8.3/composer.json   |   0
 {runtime => experimental}/php/v8.3/php.ini         |   0
 {runtime => experimental}/php/v8.3/runner.php      |   0
 .../python/v3.11cuda/Dockerfile                    |   0
 .../python/v3.11cuda/bin/compile                   |   0
 .../python/v3.11cuda/debug.sh                      |   0
 .../python/v3.11cuda/lib/launcher.py               |   0
 .../python/v3.11cuda/platforms.txt                 |   0
 .../python/v3.11cuda/requirements.txt              |   0
 runtime/go/v1.26/merge.json                        |   3 +
 runtime/java/v8/merge.json                         |   3 +
 runtime/nodejs/v22/merge.json                      |  16 ++
 runtime/python/v3.12/merge.json                    |  16 ++
 runtimes.json.tpl                                  | 252 ---------------------
 runtimes.py                                        | 175 ++++++++++++++
 45 files changed, 218 insertions(+), 324 deletions(-)

diff --git a/.env.dist b/.env.dist
index 85bf60e..26f0f89 100644
--- a/.env.dist
+++ b/.env.dist
@@ -1,3 +1,3 @@
-DOCKER_HUB_USER=docker hub user
-DOCKER_HUB_TOKEN=docker hub token
-DOCKER_HUB_REGISTRY=docker hub registry
+DOCKERHUB_USER=docker hub user
+DOCKERHUB_TOKEN=docker hub token
+DOCKERHUB_REGISTRY=docker hub registry
diff --git a/Taskfile.yml b/Taskfile.yml
index 3d2f171..e65a57a 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -233,76 +233,9 @@ tasks:
     cmds:
       - python3 packages/invoke.py run '{{.J}}'
 
-  render-runtime:
-    requires: { vars: [RT, VER] }
-    dir: "runtime/{{.RT}}/{{.VER}}"
-    cmds:
-      - |
-        RUNTIME="{{.BASEIMG}}/openserverless-runtime-{{.RT}}:{{.VER}}-{{.TAG}}"
-        ENVVER=$(echo "{{.VER}}" | tr '.' '_' | tr '[:upper:][:lower:]' '[:lower:][:upper:]')
-        ENVRT=$(echo "{{.RT}}" | tr '[a-zA-Z]' '[A-Za-z]')
-        if test -n "{{.PUSH}}"
-        then
-          {{.DRY}} echo "OPS_RUNTIME_TAG_${ENVRT}_${ENVVER}={{.VER}}-{{.TAG}}"  >> {{.BASE_DIR}}/runtimes.env
-        else {{.DRY}} echo "OPS_RUNTIME_TAG_${ENVRT}_${ENVVER}={{.VER}}-{{.TAG}}"
-        fi
-        echo "Rendered $RUNTIME"
-
-  render-lang:
-    desc: render images for one runtime
-    requires: { vars: [RT, BASE_DIR] }
-    vars:
-      DIRS:
-        sh: ls -d  runtime/{{.RT}}/v* || echo ""
-    cmds:
-      - for: { var: DIRS }
-        task: render-runtime
-        vars:
-          VER: "{{base .ITEM}}"
-          BASE_DIR: "{{.BASE_DIR}}"
-
-  render:
-    silent: true
-    requires: { vars: [TAG, RUNTIMES] }
-    vars:
-      RT_REGEX:
-        sh: echo "{{.RUNTIMES}}" | tr ',' '|'
-      SPECIFIC_RT:
-        sh: echo "{{.TAG}}" | grep -E -o '({{.RT_REGEX}})' || echo ''
-      BASE_DIR:
-        sh: pwd
-    cmds:
-      - |
-        rm -f runtimes.env
-        T="{{.TAG}}"
-        TS="${T#*_}"
-        RT="${T%%_*}"
-        echo "OPS_RUNTIME_PREFIX={{.REPO_PATH}}" >> runtimes.env
-        echo "==> RENDERING RUNTIMES {{.RUNTIMES}}"
-        for ITEM in `echo {{.RUNTIMES}} | tr ',' ' '`; do
-          echo "==> RENDERING $ITEM:$TS"
-          task render-lang TAG=$TS RT=$ITEM DRY={{.DRY}} BASE_DIR="{{.BASE_DIR}}"
-        done
-
-  generate-runtimes:
-    silent: true
-    dotenv:
-      - runtimes.env
-    cmds:
-      - cat runtimes.json.tpl | envsubst  > runtimes.json
-      # envsubst silently expands unset vars to "", so catch template/runtime drift here
-      - |
-        if grep -q '"tag": ""' runtimes.json
-        then
-          echo "ERROR: unresolved image tags in runtimes.json (runtimes.json.tpl is out of sync with runtimes.env):" >&2
-          grep -n -B12 -A2 '"tag": ""' runtimes.json | grep -E '"(kind|name)"' >&2
-          exit 1
-        fi
-
   render-runtimes:
     silent: true
     desc: generate a runtimes.json file with all the runtime corresponding to the current TAG
     cmds:
-      - |
-        task render PUSH=y
-        task generate-runtimes
+      - echo "creating runtimes.json"
+      - uv run runtimes.py {{.TAG}} >runtimes.json
diff --git a/runtime/common/common1.17.1/DISCLAIMER b/common/common1.17.1/DISCLAIMER
similarity index 100%
rename from runtime/common/common1.17.1/DISCLAIMER
rename to common/common1.17.1/DISCLAIMER
diff --git a/runtime/common/common1.17.1/Dockerfile b/common/common1.17.1/Dockerfile
similarity index 100%
rename from runtime/common/common1.17.1/Dockerfile
rename to common/common1.17.1/Dockerfile
diff --git a/runtime/common/common1.17.1/LICENSE b/common/common1.17.1/LICENSE
similarity index 100%
rename from runtime/common/common1.17.1/LICENSE
rename to common/common1.17.1/LICENSE
diff --git a/runtime/common/common1.17.1/NOTICE b/common/common1.17.1/NOTICE
similarity index 100%
rename from runtime/common/common1.17.1/NOTICE
rename to common/common1.17.1/NOTICE
diff --git a/runtime/common/common1.17.1/WARN b/common/common1.17.1/WARN
similarity index 100%
rename from runtime/common/common1.17.1/WARN
rename to common/common1.17.1/WARN
diff --git a/runtime/common/common1.18.4/DISCLAIMER b/common/common1.18.4/DISCLAIMER
similarity index 100%
rename from runtime/common/common1.18.4/DISCLAIMER
rename to common/common1.18.4/DISCLAIMER
diff --git a/runtime/common/common1.18.4/Dockerfile b/common/common1.18.4/Dockerfile
similarity index 100%
rename from runtime/common/common1.18.4/Dockerfile
rename to common/common1.18.4/Dockerfile
diff --git a/runtime/common/common1.18.4/LICENSE b/common/common1.18.4/LICENSE
similarity index 100%
rename from runtime/common/common1.18.4/LICENSE
rename to common/common1.18.4/LICENSE
diff --git a/runtime/common/common1.18.4/NOTICE b/common/common1.18.4/NOTICE
similarity index 100%
rename from runtime/common/common1.18.4/NOTICE
rename to common/common1.18.4/NOTICE
diff --git a/runtime/common/common1.18.4/WARN b/common/common1.18.4/WARN
similarity index 100%
rename from runtime/common/common1.18.4/WARN
rename to common/common1.18.4/WARN
diff --git a/runtime/php/v8.2/CHANGELOG.md b/experimental/php/v8.2/CHANGELOG.md
similarity index 100%
rename from runtime/php/v8.2/CHANGELOG.md
rename to experimental/php/v8.2/CHANGELOG.md
diff --git a/runtime/php/v8.2/DISCLAIMER b/experimental/php/v8.2/DISCLAIMER
similarity index 100%
rename from runtime/php/v8.2/DISCLAIMER
rename to experimental/php/v8.2/DISCLAIMER
diff --git a/runtime/php/v8.2/Dockerfile b/experimental/php/v8.2/Dockerfile
similarity index 100%
rename from runtime/php/v8.2/Dockerfile
rename to experimental/php/v8.2/Dockerfile
diff --git a/runtime/php/v8.2/LICENSE b/experimental/php/v8.2/LICENSE
similarity index 100%
rename from runtime/php/v8.2/LICENSE
rename to experimental/php/v8.2/LICENSE
diff --git a/runtime/php/v8.2/NOTICE b/experimental/php/v8.2/NOTICE
similarity index 100%
rename from runtime/php/v8.2/NOTICE
rename to experimental/php/v8.2/NOTICE
diff --git a/runtime/php/v8.2/WARN b/experimental/php/v8.2/WARN
similarity index 100%
rename from runtime/php/v8.2/WARN
rename to experimental/php/v8.2/WARN
diff --git a/runtime/php/v8.2/build.gradle b/experimental/php/v8.2/build.gradle
similarity index 100%
rename from runtime/php/v8.2/build.gradle
rename to experimental/php/v8.2/build.gradle
diff --git a/runtime/php/v8.2/compile.php b/experimental/php/v8.2/compile.php
similarity index 100%
rename from runtime/php/v8.2/compile.php
rename to experimental/php/v8.2/compile.php
diff --git a/runtime/php/v8.2/composer.json b/experimental/php/v8.2/composer.json
similarity index 100%
rename from runtime/php/v8.2/composer.json
rename to experimental/php/v8.2/composer.json
diff --git a/runtime/php/v8.2/php.ini b/experimental/php/v8.2/php.ini
similarity index 100%
rename from runtime/php/v8.2/php.ini
rename to experimental/php/v8.2/php.ini
diff --git a/runtime/php/v8.2/runner.php b/experimental/php/v8.2/runner.php
similarity index 100%
rename from runtime/php/v8.2/runner.php
rename to experimental/php/v8.2/runner.php
diff --git a/runtime/php/v8.3/CHANGELOG.md b/experimental/php/v8.3/CHANGELOG.md
similarity index 100%
rename from runtime/php/v8.3/CHANGELOG.md
rename to experimental/php/v8.3/CHANGELOG.md
diff --git a/runtime/php/v8.3/DISCLAIMER b/experimental/php/v8.3/DISCLAIMER
similarity index 100%
rename from runtime/php/v8.3/DISCLAIMER
rename to experimental/php/v8.3/DISCLAIMER
diff --git a/runtime/php/v8.3/Dockerfile b/experimental/php/v8.3/Dockerfile
similarity index 100%
rename from runtime/php/v8.3/Dockerfile
rename to experimental/php/v8.3/Dockerfile
diff --git a/runtime/php/v8.3/LICENSE b/experimental/php/v8.3/LICENSE
similarity index 100%
rename from runtime/php/v8.3/LICENSE
rename to experimental/php/v8.3/LICENSE
diff --git a/runtime/php/v8.3/NOTICE b/experimental/php/v8.3/NOTICE
similarity index 100%
rename from runtime/php/v8.3/NOTICE
rename to experimental/php/v8.3/NOTICE
diff --git a/runtime/php/v8.3/WARN b/experimental/php/v8.3/WARN
similarity index 100%
rename from runtime/php/v8.3/WARN
rename to experimental/php/v8.3/WARN
diff --git a/runtime/php/v8.3/compile.php b/experimental/php/v8.3/compile.php
similarity index 100%
rename from runtime/php/v8.3/compile.php
rename to experimental/php/v8.3/compile.php
diff --git a/runtime/php/v8.3/composer.json b/experimental/php/v8.3/composer.json
similarity index 100%
rename from runtime/php/v8.3/composer.json
rename to experimental/php/v8.3/composer.json
diff --git a/runtime/php/v8.3/php.ini b/experimental/php/v8.3/php.ini
similarity index 100%
rename from runtime/php/v8.3/php.ini
rename to experimental/php/v8.3/php.ini
diff --git a/runtime/php/v8.3/runner.php b/experimental/php/v8.3/runner.php
similarity index 100%
rename from runtime/php/v8.3/runner.php
rename to experimental/php/v8.3/runner.php
diff --git a/runtime/experimental/python/v3.11cuda/Dockerfile b/experimental/python/v3.11cuda/Dockerfile
similarity index 100%
rename from runtime/experimental/python/v3.11cuda/Dockerfile
rename to experimental/python/v3.11cuda/Dockerfile
diff --git a/runtime/experimental/python/v3.11cuda/bin/compile b/experimental/python/v3.11cuda/bin/compile
similarity index 100%
rename from runtime/experimental/python/v3.11cuda/bin/compile
rename to experimental/python/v3.11cuda/bin/compile
diff --git a/runtime/experimental/python/v3.11cuda/debug.sh b/experimental/python/v3.11cuda/debug.sh
similarity index 100%
rename from runtime/experimental/python/v3.11cuda/debug.sh
rename to experimental/python/v3.11cuda/debug.sh
diff --git a/runtime/experimental/python/v3.11cuda/lib/launcher.py b/experimental/python/v3.11cuda/lib/launcher.py
similarity index 100%
rename from runtime/experimental/python/v3.11cuda/lib/launcher.py
rename to experimental/python/v3.11cuda/lib/launcher.py
diff --git a/runtime/experimental/python/v3.11cuda/platforms.txt b/experimental/python/v3.11cuda/platforms.txt
similarity index 100%
rename from runtime/experimental/python/v3.11cuda/platforms.txt
rename to experimental/python/v3.11cuda/platforms.txt
diff --git a/runtime/experimental/python/v3.11cuda/requirements.txt b/experimental/python/v3.11cuda/requirements.txt
similarity index 100%
rename from runtime/experimental/python/v3.11cuda/requirements.txt
rename to experimental/python/v3.11cuda/requirements.txt
diff --git a/runtime/go/v1.26/merge.json b/runtime/go/v1.26/merge.json
new file mode 100644
index 0000000..077a621
--- /dev/null
+++ b/runtime/go/v1.26/merge.json
@@ -0,0 +1,3 @@
+{
+    "default": true
+}
\ No newline at end of file
diff --git a/runtime/java/v8/merge.json b/runtime/java/v8/merge.json
new file mode 100644
index 0000000..077a621
--- /dev/null
+++ b/runtime/java/v8/merge.json
@@ -0,0 +1,3 @@
+{
+    "default": true
+}
\ No newline at end of file
diff --git a/runtime/nodejs/v22/merge.json b/runtime/nodejs/v22/merge.json
new file mode 100644
index 0000000..e2d5da4
--- /dev/null
+++ b/runtime/nodejs/v22/merge.json
@@ -0,0 +1,16 @@
+{
+"default": true,
+"stemCells": [
+    {
+        "initialCount": 1,
+        "memory": "256 MB",
+        "reactive": {
+            "minCount": 1,
+            "maxCount": 4,
+            "ttl": "2 minutes",
+            "threshold": 1,
+            "increment": 1
+        }
+    }
+]
+}
\ No newline at end of file
diff --git a/runtime/python/v3.12/merge.json b/runtime/python/v3.12/merge.json
new file mode 100644
index 0000000..e2d5da4
--- /dev/null
+++ b/runtime/python/v3.12/merge.json
@@ -0,0 +1,16 @@
+{
+"default": true,
+"stemCells": [
+    {
+        "initialCount": 1,
+        "memory": "256 MB",
+        "reactive": {
+            "minCount": 1,
+            "maxCount": 4,
+            "ttl": "2 minutes",
+            "threshold": 1,
+            "increment": 1
+        }
+    }
+]
+}
\ No newline at end of file
diff --git a/runtimes.json.tpl b/runtimes.json.tpl
deleted file mode 100644
index f05e22d..0000000
--- a/runtimes.json.tpl
+++ /dev/null
@@ -1,252 +0,0 @@
-{
-    "description": [
-        "This file describes the different languages (aka. managed action runtimes) supported by the system",
-        "as well as blackbox images that support the runtime specification.",
-        "Only actions with runtime families / kinds defined here can be created / read / updated / deleted / invoked.",
-        "Define a list of runtime families (example: 'nodejs') with at least one kind per family (example: 'nodejs:14').",
-        "Each runtime family needs a default kind (default: true).",
-        "When removing or renaming runtime families or runtime kinds from this file, preexisting actions",
-        "with the affected kinds can no longer be read / updated / deleted / invoked. In order to remove or rename",
-        "runtime families or runtime kinds, mark all affected runtime kinds as deprecated (deprecated: true) and",
-        "perform a manual migration of all affected actions.",
-        "",
-        "This file is meant to list all stable runtimes supported by the Apache Openwhisk community."
-    ],
-    "runtimes": {
-        "nodejs": [
-            {
-                "kind": "nodejs:24",
-                "default": true,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-nodejs",
-                    "tag": "$OPS_RUNTIME_TAG_NODEJS_V24"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "stemCells": [
-                    {
-                        "initialCount": 1,
-                        "memory": "256 MB",
-                        "reactive": {
-                            "minCount": 1,
-                            "maxCount": 4,
-                            "ttl": "2 minutes",
-                            "threshold": 1,
-                            "increment": 1
-                        }
-                    }
-                ]
-            },
-            {
-                "kind": "nodejs:22",
-                "default": false,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-nodejs",
-                    "tag": "$OPS_RUNTIME_TAG_NODEJS_V22"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                }
-            },
-            {
-                "kind": "nodejs:20",
-                "default": false,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-nodejs",
-                    "tag": "$OPS_RUNTIME_TAG_NODEJS_V20"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                }
-            }
-        ],
-        "python": [
-            {
-                "kind": "python:3",
-                "default": true,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-python",
-                    "tag": "$OPS_RUNTIME_TAG_PYTHON_V3_12"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "stemCells": [
-                    {
-                        "initialCount": 1,
-                        "memory": "256 MB",
-                        "reactive": {
-                            "minCount": 1,
-                            "maxCount": 4,
-                            "ttl": "2 minutes",
-                            "threshold": 1,
-                            "increment": 1
-                        }
-                    }
-                ]
-            },
-            {
-                "kind": "python:3.11",
-                "default": false,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-python",
-                    "tag": "$OPS_RUNTIME_TAG_PYTHON_V3_11"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                }
-            },
-            {
-                "kind": "python:3.12",
-                "default": false,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-python",
-                    "tag": "$OPS_RUNTIME_TAG_PYTHON_V3_12"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                }
-            },
-            {
-                "kind": "python:3.13",
-                "default": false,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-python",
-                    "tag": "$OPS_RUNTIME_TAG_PYTHON_V3_13"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                }
-            }
-        ],
-        "go": [
-            {
-                "kind": "go:1.27",
-                "default": true,
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-go",
-                    "tag": "$OPS_RUNTIME_TAG_GO_V1_27"
-                }
-            },
-            {
-                "kind": "go:1.26",
-                "default": false,
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-go",
-                    "tag": "$OPS_RUNTIME_TAG_GO_V1_26"
-                }
-            },
-            {
-                "kind": "go:1.25",
-                "default": false,
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-go",
-                    "tag": "$OPS_RUNTIME_TAG_GO_V1_25"
-                }
-            }
-        ],
-        "java": [
-            {
-                "kind": "java:8",
-                "default": true,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-java",
-                    "tag": "$OPS_RUNTIME_TAG_JAVA_V8"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "requireMain": true
-            },
-            {
-                "kind": "java:11",
-                "default": false,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-java",
-                    "tag": "$OPS_RUNTIME_TAG_JAVA_V11"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "requireMain": true
-            },
-            {
-                "kind": "java:17",
-                "default": false,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-java",
-                    "tag": "$OPS_RUNTIME_TAG_JAVA_V17"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "requireMain": true
-            },
-            {
-                "kind": "java:21",
-                "default": false,
-                "image": {
-                    "prefix": "$OPS_RUNTIME_PREFIX",
-                    "name": "openserverless-runtime-java",
-                    "tag": "$OPS_RUNTIME_TAG_JAVA_V21"
-                },
-                "deprecated": false,
-                "attached": {
-                    "attachmentName": "codefile",
-                    "attachmentType": "text/plain"
-                },
-                "requireMain": true
-            }
-        ]
-    },
-    "blackboxes": [ ]
-}
\ No newline at end of file
diff --git a/runtimes.py b/runtimes.py
new file mode 100755
index 0000000..00a1eec
--- /dev/null
+++ b/runtimes.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+"""Generate runtimes.json by walking the runtime/ subtree.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+  Each runtime/<language>/<version> directory becomes one kind: the language
+  directory gives the kind family, the version subdir gives both the kind
+  version and the image tag, with -<tag> appended. A merge.json in a version
+  directory is merged into that entry, which is how defaults, stemCells and
+  requireMain are declared.
+
+  Example:
+     ./runtime.py all_26i06r36-snapshot
+     ./runtime.py all_26i06r36-snapshot -o runtimes.json
+"""
+
+import argparse
+import json
+import os
+import sys
+
+DESCRIPTION = [
+    "This file describes the different languages (aka. managed action runtimes) supported by the system",
+    "as well as blackbox images that support the runtime specification.",
+    "Only actions with runtime families / kinds defined here can be created / read / updated / deleted / invoked.",
+    "Define a list of runtime families (example: 'nodejs') with at least one kind per family (example: 'nodejs:14').",
+    "Each runtime family needs a default kind (default: true).",
+    "When removing or renaming runtime families or runtime kinds from this file, preexisting actions",
+    "with the affected kinds can no longer be read / updated / deleted / invoked. In order to remove or rename",
+    "runtime families or runtime kinds, mark all affected runtime kinds as deprecated (deprecated: true) and",
+    "perform a manual migration of all affected actions.",
+    "",
+    "This file is meant to list all stable runtimes supported by the Apache Openwhisk community.",
+]
+
+ATTACHED = {"attachmentName": "codefile", "attachmentType": "text/plain"}
+
+
+def read_env(path):
+    """Parse a dotenv file into a dict; missing file yields an empty environment."""
+    env = {}
+    if not os.path.isfile(path):
+        return env
+    with open(path) as f:
+        for line in f:
+            line = line.strip()
+            if not line or line.startswith("#") or "=" not in line:
+                continue
+            key, value = line.split("=", 1)
+            env[key.strip()] = value.strip().strip('"').strip("'")
+    return env
+
+
+def strip_prefix(tag):
+    """Drop the leading `xxx_` build selector, e.g. all_26i06r36-snapshot -> 26i06r36-snapshot."""
+    return tag.split("_", 1)[1] if "_" in tag else tag
+
+
+def version_key(version):
+    """Sort v3.11/v1.27/v8 numerically, newest first, so ordering is stable."""
+    return [int(p) if p.isdigit() else p for p in version.lstrip("v").split(".")]
+
+
+def kind_entry(language, version, tag, prefix):
+    """Build one kind from a runtime/<language>/<version> directory."""
+    entry = {
+        "kind": "%s:%s" % (language, version.lstrip("v")),
+        "default": False,
+        "image": {
+            "prefix": prefix,
+            "name": "openserverless-runtime-%s" % language,
+            "tag": "%s-%s" % (version, tag),
+        },
+        "deprecated": False,
+        "attached": dict(ATTACHED),
+    }
+    return entry
+
+
+def walk(runtime_dir, tag, prefix):
+    """Walk runtime/<language>/<version>, yielding (language, [kinds])."""
+    for language in sorted(os.listdir(runtime_dir)):
+        lang_dir = os.path.join(runtime_dir, language)
+        if not os.path.isdir(lang_dir):
+            continue
+
+        versions = sorted(
+            (
+                d
+                for d in os.listdir(lang_dir)
+                if d.startswith("v") and os.path.isdir(os.path.join(lang_dir, d))
+            ),
+            key=version_key,
+            reverse=True,
+        )
+        if not versions:
+            continue
+
+        kinds = []
+        for version in versions:
+            entry = kind_entry(language, version, tag, prefix)
+            merge_file = os.path.join(lang_dir, version, "merge.json")
+            if os.path.isfile(merge_file):
+                with open(merge_file) as f:
+                    try:
+                        entry.update(json.load(f))
+                    except ValueError as err:
+                        sys.exit("ERROR: cannot parse %s: %s" % (merge_file, err))
+            kinds.append(entry)
+        yield language, kinds
+
+
+def build(runtime_dir, tag, prefix):
+    runtimes = {}
+    for language, kinds in walk(runtime_dir, tag, prefix):
+        runtimes[language] = kinds
+    return {"description": DESCRIPTION, "runtimes": runtimes, "blackboxes": []}
+
+
+def main():
+    here = os.path.dirname(os.path.abspath(__file__))
+    parser = argparse.ArgumentParser(
+        description="generate runtimes.json by walking the runtime/ subtree"
+    )
+    parser.add_argument(
+        "tag", help="build tag, e.g. all_26i06r36-snapshot (the xxx_ prefix is removed)"
+    )
+    parser.add_argument(
+        "--env",
+        default=os.path.join(here, ".env"),
+        help="dotenv file providing DOCKER_HUB_REGISTRY and DOCKERHUB_USER",
+    )
+    parser.add_argument(
+        "--runtime-dir",
+        default=os.path.join(here, "runtime"),
+        help="path to the runtime/ tree (default: alongside this script)",
+    )
+    parser.add_argument("-o", "--output", help="write to this file instead of stdout")
+    args = parser.parse_args()
+
+    # .env provides the defaults; a real environment variable still wins.
+    env = read_env(args.env)
+    registry = os.environ.get(
+        "DOCKER_HUB_REGISTRY", env.get("DOCKER_HUB_REGISTRY", "docker.io")
+    )
+    user = os.environ.get("DOCKERHUB_USER", env.get("DOCKERHUB_USER", "apache"))
+    prefix = "%s/%s" % (registry, user)
+
+    result = build(args.runtime_dir, strip_prefix(args.tag), prefix)
+    text = json.dumps(result, indent=4) + "\n"
+
+    if args.output:
+        with open(args.output, "w") as f:
+            f.write(text)
+        print("Generated %s" % args.output, file=sys.stderr)
+    else:
+        sys.stdout.write(text)
+
+
+if __name__ == "__main__":
+    main()